Vault secrets engine for Cloudflare runtime secrets and account-owned API tokens.
This project implements the MVP described in SPEC.md:
- Configure Cloudflare bootstrap API access.
- Configure default sync behavior.
- Store explicit KV v2 to Cloudflare Secrets Store sync rules.
- Generate Terraform-friendly Worker Secrets Store binding metadata for sync rules.
- Run one sync rule or all enabled explicit sync rules.
- Store sync status and delete recorded Cloudflare sync entries.
- Store Cloudflare API token roles.
- Issue leased Cloudflare account-owned API tokens.
- Renew leased Cloudflare account-owned API tokens when roles allow renewal.
- Revoke Cloudflare API tokens when Vault leases are revoked.
- Roll Cloudflare API token values.
- Store and retrieve named static Cloudflare API tokens.
- Discover Cloudflare token permission groups.
go test ./...
go build -o vault-plugin-secrets-cloudflare .GitHub Actions runs tests and validates the GoReleaser configuration on pushes and pull requests to main or master.
Tagged releases create Linux amd64 and arm64 binary archives, SHA-256 checksums, a GitHub Release, and a multi-arch GHCR image:
git tag v0.1.0
git push origin v0.1.0The container image is published as:
ghcr.io/<owner>/<repo>:v0.1.0
ghcr.io/<owner>/<repo>:v0
ghcr.io/<owner>/<repo>:v0.1
ghcr.io/<owner>/<repo>:latest
GoReleaser uses Dockerfile.goreleaser, which layers the prebuilt plugin binary onto the shart-cloud Vault image (ghcr.io/shart-cloud/vault-openrouter) at /vault/plugins. The published image is therefore a full Vault image shipping both the OpenRouter and Cloudflare secrets plugins, suitable for use directly as the Vault server image.
Example local registration flow:
SHA256=$(sha256sum vault-plugin-secrets-cloudflare | cut -d ' ' -f1)
vault plugin register \
-sha256="$SHA256" \
-command="vault-plugin-secrets-cloudflare" \
secret \
vault-plugin-secrets-cloudflare
vault secrets enable \
-path=cloudflare \
-plugin-name=vault-plugin-secrets-cloudflare \
pluginThe binary must be available in Vault's configured plugin directory.
The bootstrap token must have the Cloudflare permissions needed for the workflows used:
Account Secrets Store EditandAccount Secrets Store Readfor sync.API Tokens WriteandAPI Tokens Readfor leased token creation and permission group discovery.
vault write cloudflare/config/cloudflare \
api_token="$CLOUDFLARE_API_TOKEN" \
account_id="$CLOUDFLARE_ACCOUNT_ID" \
secrets_store_id="$CLOUDFLARE_SECRETS_STORE_ID"The configured API token is never returned by reads:
vault read cloudflare/config/cloudflarevault write cloudflare/config/sync \
default_scopes="workers" \
default_chunk_size_bytes=760 \
max_parts=3 \
delete_removed_parts=trueCloudflare Secrets Store values are limited to 1024 bytes. For chunked secrets, default_chunk_size_bytes is the maximum base64 data field size before it is wrapped in the JSON part envelope. The default is intentionally conservative so the final JSON value stays under the Cloudflare limit.
The source must be a KV v2 secret. The plugin reads it through Vault as:
{vault_mount}/data/{vault_path}
The current implementation uses Vault's ExtendedSystemView.ForwardGenericRequest to read across mounts. Deployments where that system view is unavailable cannot run sync rules yet; token issuance and config paths are unaffected.
Example source secret:
vault kv put kv/apps/api/prod/jwt_private_key value=@jwt.pemExample sync rule:
vault write cloudflare/sync/rules/api-jwt \
vault_mount=kv \
vault_path=apps/api/prod/jwt_private_key \
vault_field=value \
cloudflare_name=JWT_PRIVATE_KEY \
binding_name=JWT_PRIVATE_KEY \
scopes="workers" \
split=auto \
encoding=raw \
enabled=trueRun one rule:
vault write cloudflare/sync/run/api-jwtDry-run one rule:
vault write cloudflare/sync/run/api-jwt dry_run=trueRun all enabled explicit rules:
vault write cloudflare/sync/runRun all enabled rules with a prefix:
vault write cloudflare/sync/run rule_prefix=api-Read status:
vault read cloudflare/sync/status/api-jwtDelete Cloudflare entries recorded for a rule:
vault write cloudflare/sync/delete/api-jwt dry_run=true
vault write cloudflare/sync/delete/api-jwt delete_cloudflare_secrets=trueThis does not delete the Vault source secret or the sync rule.
Reconcile sync rules, stored plugin state, and Cloudflare metadata:
vault read cloudflare/sync/reconcile
vault write cloudflare/sync/reconcile dry_run=trueDelete managed Cloudflare secrets that have no matching rule/state:
vault write cloudflare/sync/reconcile dry_run=false delete_orphans=trueReconciliation reports missing Cloudflare entries, managed orphan entries, stale comments or versions, and chunk part drift.
Terraform should bind Cloudflare Workers to Secrets Store entries by name and store ID while this plugin owns secret values.
After creating a sync rule, trigger sync before deploying the Worker:
vault write cloudflare/sync/run/api-jwtRead binding metadata for Terraform:
vault read cloudflare/sync/bindings/api-jwtThe response includes bindings_json, a JSON array of Cloudflare Worker binding objects:
[
{
"name": "JWT_PRIVATE_KEY",
"type": "secrets_store_secret",
"store_id": "<store_id>",
"secret_name": "JWT_PRIVATE_KEY"
}
]For chunked secrets, the endpoint returns every required binding, including *_META and *_PART_NNN entries. The endpoint does not write Cloudflare and does not return secret values.
The recommended Terraform workflow is:
- CI obtains a short-lived Cloudflare API token from
cloudflare/token/creds/<role>and exports it asCLOUDFLARE_API_TOKEN. - Terraform writes/updates
sync/rules/{name}. - Terraform triggers
sync/run/{name}. - Terraform reads
sync/bindings/{name}and assignsjsondecode(bindings_json)to Workerbindings.
See examples/terraform/worker-secrets-store.
When chunking is enabled, parts are written before metadata:
JWT_PRIVATE_KEY__PART_001
JWT_PRIVATE_KEY__PART_002
JWT_PRIVATE_KEY__META
Each part and metadata entry uses the JSON envelope described in SPEC.md so Workers can reject mixed versions or invalid hashes.
Discover permission group IDs before creating roles:
vault read cloudflare/token/permission-groups name=workers
vault read cloudflare/token/permission-groups scope=com.cloudflare.api.accountRoles should store permission group IDs, not names.
vault write cloudflare/token/roles/worker-read \
token_type=account \
permission_groups="<permission_group_id>" \
resources='{"com.cloudflare.api.account.<account_id>":"*"}' \
default_ttl=1h \
max_ttl=4h \
enabled=trueThe default token name template is vault-{{role}}-{{display_name}}-{{unix_time}}-{{random}}. Supported template values are {{role}}, {{display_name}}, {{unix_time}}, {{timestamp}}, and {{random}}.
Issue a leased token:
vault write cloudflare/token/creds/worker-read display_name=ci ttl=30mThe response includes the Cloudflare token value once. The plugin stores token ID, account ID, role name, issue time, and expiration only.
If the role has renewable=true, Vault lease renewal updates the Cloudflare token expiration without returning a new token value. Renewal is capped by the role's max_ttl.
When the Vault lease is revoked or expires, Vault calls the plugin revoke callback and the plugin deletes the Cloudflare token.
Roll a managed token value and return the replacement value once:
vault write cloudflare/token/roll/<token_id>Store an existing static Cloudflare API token:
vault write cloudflare/token/static/ci \
token="$CLOUDFLARE_STATIC_TOKEN" \
account_id="$CLOUDFLARE_ACCOUNT_ID" \
token_id="<optional_cloudflare_token_id>"Create a static token once from a configured role and store the returned token value:
vault write cloudflare/token/static/ci \
role=worker-read \
display_name=ci \
ttl=4hRead the stored token value:
vault read cloudflare/token/static/ciDelete the Vault static token entry:
vault delete cloudflare/token/static/ciSet delete_cloudflare_token_on_delete=true when writing the static entry if deleting the Vault entry should also delete the Cloudflare token.
Reconcile issued token state with Cloudflare:
vault read cloudflare/token/reconcile
vault write cloudflare/token/reconcile dry_run=trueDelete plugin-managed Cloudflare tokens that have no matching Vault token state:
vault write cloudflare/token/reconcile dry_run=false delete_orphans=trueReconciliation reports active plugin-managed tokens without Vault state, expired managed tokens still present in Cloudflare, and Vault token state whose Cloudflare token is missing. Named static token entries with Cloudflare token IDs are also treated as valid plugin state during reconciliation.
The following are still deferred from the MVP or not yet implemented:
- KV metadata discovery scans.
- Automatic Worker deployment updates.
- Cloudflare user-owned token creation.
- Root credential rotation automation.