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, 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 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 (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.