Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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 works

Anywhere 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 endpoint

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

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

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:

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 failure

doctor 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.