external secret providers
Fetch secrets: values from AWS
Secrets Manager or GCP Secret Manager at boot, so production API keys,
HMAC keys and notification tokens never live in .env or a mounted file.
Optional: .env stays the zero-config default, and both forms coexist in
one secrets: map.
rflow_version: 1
name: price-alerts
config:
port: 3940
db_connection: ${DATABASE_URL}
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 works
workflows:
hourly-price-report:
trigger:
cron:
expression: "0 * * * *"
steps:
- id: fetch
http_call:
method: GET
url: https://api.example.com/v1/price
headers:
Authorization: "Bearer ${{ secrets.PRICE_API_KEY }}"Usage is unchanged: ${{ secrets.PRICE_API_KEY }} in steps, triggers and
config.server.auth.token reads the fetched value 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
rflow_version: 1
name: price-alerts
config:
port: 3940
db_connection: ${DATABASE_URL}
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 endpoint #
secrets:
PRICE_API_KEY:
from:
provider: aws-prod
key: price-api-key
workflows:
hourly-price-report:
trigger:
cron:
expression: "0 * * * *"
steps:
- id: fetch
http_call:
method: GET
url: https://api.example.com/v1/price
headers:
Authorization: "Bearer ${{ secrets.PRICE_API_KEY }}"Fetches 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
rflow_version: 1
name: price-alerts
config:
port: 3940
db_connection: ${DATABASE_URL}
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 latest #
secrets:
PRICE_API_KEY:
from:
provider: gcp-prod
key: price-api-key
workflows:
hourly-price-report:
trigger:
cron:
expression: "0 * * * *"
steps:
- id: fetch
http_call:
method: GET
url: https://api.example.com/v1/price
headers:
Authorization: "Bearer ${{ secrets.PRICE_API_KEY }}"Fetches 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:
rflow_version: 1
name: price-alerts
config:
port: 3940
db_connection: ${DATABASE_URL}
secret_providers:
aws-prod:
aws_secrets_manager:
region: eu-west-1
prefix: /rflow/prod/
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)
workflows:
hourly-price-report:
trigger:
cron:
expression: "0 * * * *"
steps:
- id: fetch
http_call:
method: GET
url: https://api.example.com/v1/price
headers:
Authorization: "Bearer ${{ secrets.PRICE_API_KEY }}"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). Background
refresh-on-interval is not implemented.
The same fail-closed resolve runs in every CLI command that renders or redacts
secrets: rflow test, replay and command test (they render
${{ secrets.* }}), plus the journal read paths (history, runs show/export, archive), whose scrubber can only strip a value it actually
holds. If the provider is unreachable, those commands error rather than print
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 (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.