external secret providers
Fetch secrets: values from AWS
Secrets Manager or GCP Secret Manager at boot, so production API keys,
webhook HMAC keys and notification tokens never live in .env or a mounted
file. Optional: .env stays the zero-config default — a project with no
secret_providers: behaves exactly as before, and both forms coexist in one
secrets: map.
secret_providers:
aws-prod:
aws_secrets_manager:
region: eu-west-1
prefix: /rflow/prod/
secrets:
PRICE_API_KEY:
from:
provider: aws-prod
key: price-api-key # fetches /rflow/prod/price-api-key
HOOK_KEY: ${BACKEND_HMAC_KEY} # the plain env form still worksAnywhere a secret is used, nothing changes: ${{ secrets.PRICE_API_KEY }} in
steps, triggers and config.server.auth.token reads the fetched value exactly
like an env-sourced one.
The providers
Exactly one provider kind per entry (validation error otherwise). Neither
carries static credentials in rflow.yaml — auth is ambient, mirroring the
signer's cloud providers.
aws_secrets_manager
secret_providers:
aws-prod:
aws_secrets_manager:
region: eu-west-1
prefix: /rflow/prod/ # optional — prepended to every from.key
# endpoint_url: http://localhost:4566 # optional — LocalStack / VPC endpointFetches with GetSecretValue using the AWS default credential chain (env
vars, shared profile, IMDS/IRSA role). The remote secret must hold a string
value (binary secrets are not supported). Grant the runtime identity
secretsmanager:GetSecretValue on the secrets under your prefix — see the
AWS IAM docs
(rflow reads secrets; it never manages IAM).
gcp_secret_manager
secret_providers:
gcp-prod:
gcp_secret_manager:
project: my-project
# service_account_key_path: ./gcp-sa.json # optional — default: Application Default Credentials
# prefix: rflow-prod- # optional — prepended to every from.key
# version: latest # optional — default latestFetches with accessSecretVersion. Auth is a service-account key file
(path relative to the project dir) when service_account_key_path is set,
otherwise Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS,
a gcloud auth application-default login, or the metadata server). Grant the
identity roles/secretmanager.secretAccessor — see the
GCP access control docs.
The from: form
Each provider-backed secret names its provider and the remote key:
secrets:
PRICE_API_KEY:
from:
provider: aws-prod # a secret_providers: entry (typo = validation error)
key: price-api-key # the remote secret name (provider prefix prepended)Fail-closed resolution
Providers resolve once, at boot (rflow start, before validation-dependent
work runs). A missing, denied or unreachable secret is a hard error naming
the provider and key — never a silent empty value — and every failure is
reported in one message:
failed to resolve 2 secret(s) from `secret_providers:` (rflow fails closed - no secret is left empty):
- secret 'PRICE_API_KEY': provider 'aws-prod' key 'price-api-key' access was denied (AccessDeniedException: ...)
- secret 'HOOK_KEY': provider 'aws-prod' key 'webhook-hmac' was not found (no such secret)Values are fetched at boot only — rotating a remote secret takes effect on the
next boot (rflow start, or a start --watch reload, which re-runs the full
boot). A background refresh-on-interval is a possible future nicety, not
implemented today.
The same fail-closed resolve also runs in every CLI command that renders or
redacts secrets in-process: rflow test, replay and command test (they
render ${{ secrets.* }}), and the journal read paths — history, runs show/export and archive — whose scrubber can only strip a
provider-fetched value it actually holds. If the provider is unreachable,
those commands error rather than print journal output the scrubber could not
fully redact. Commands that never touch secret values (ls, status,
spend, …) skip resolution entirely.
Redaction guarantee
A provider-fetched value is redacted identically to a ${VAR}-sourced one,
in every surface: the journal, run history and its JSON/CSV exports, the
/api/* responses, archive exports, CLI output, and notify/approval messages
([redacted:<name>]). The config version
snapshot and hash treat it as a redacted secret value — the fetched plaintext
never lands in rflow.config_versions, and rotating the remote value never
mints a new version.
secrets ls / doctor
rflow secrets ls # name / source (env | provider:<name>) / remote key / resolved? / version — never values
rflow secrets doctor # fetch-check every provider secret; exit non-zero on any failuredoctor reports ok / denied / not-found per secret without printing
values and exits non-zero on any failure — the CI-friendly way to verify IAM
before a deploy. The same per-secret checks run inside
rflow doctor's deep suite whenever a
project declares provider-backed secrets (unlike the signer's cloud providers,
a Secret Manager read is side-effect-free, so doctor checks it live).
Non-goals
rflow is not a secret manager: it never writes or rotates remote secrets, never
manages IAM policies, and never shows raw values. Signing-key material stays
with the signer providers (rrelayer's own AWS/GCP/KMS
support) — secret_providers: is for workflow configuration secrets.