Skip to content

Storing Sensitive Config Values

Mark configs as secret to encrypt them at rest and mask them in the UI.

API keys, passwords, and webhook signing keys are stored as ordinary configs with config_type: secret. The platform encrypts the value at rest using Fernet, masks it in the UI, and excludes it from portable exports. There is no separate “secret” entity — secret is just a value type on Config, alongside string, int, bool, and json.

Mark it secret if leaking the value would compromise an account, allow impersonation, or violate compliance — API keys, OAuth client secrets, database passwords, webhook signing keys, third-party tokens. Don’t mark connection identifiers like tenant_id or company_id as secret; they’re not sensitive and masking them in the UI just makes debugging harder.

Settings → Configuration → + Add Config

  • Key: stripe_api_key
  • Type: Secret
  • Value: sk_live_xxxxx

The value is encrypted in the database before it’s persisted; you cannot read it back through the UI.

There’s no special read API. config.get() returns the decrypted value:

from bifrost import config
@workflow(name="charge_card")
async def charge_card(context: ExecutionContext, amount: int):
api_key = await config.get("stripe_api_key")
response = requests.post(
"https://api.stripe.com/v1/charges",
headers={"Authorization": f"Bearer {api_key}"},
json={"amount": amount},
)
return response.json()

The platform automatically registers the returned value with the execution-output scrubber, so it won’t appear in workflow logs or execution payloads even if your code accidentally returns it.

  • The value is masked (••••••) on the configuration list.
  • “Copy” and “Reveal” buttons are intentionally absent.
  • Editing requires re-entering the full value — there is no “show current value to edit” affordance.
  • Audit-log entries record reads and writes but never the value itself.

Secrets follow the same scope cascade as any other config (see Scopes). An org-specific secret shadows a global one with the same key:

# Org A's stripe_api_key = sk_live_aaa
# Org B's stripe_api_key = sk_live_bbb
# Workflow running in Org A's context:
api_key = await config.get("stripe_api_key") # sk_live_aaa

Set it again with the new value:

await config.set("stripe_api_key", "sk_live_new_value", is_secret=True)

In the UI, edit the config row and enter a new value. The previous value is overwritten — there is no version history.

bifrost export --portable writes the config row with its key, description, and config_type: secret intact, but with value: null. After importing the bundle into a target environment, re-enter the value in Settings → Configuration for each org that needs it. See Exporting and Importing.

  • Read the secret once near the top of your workflow and never log the variable.
  • Never include a secret in a return value or in form output.
  • Rotate the secret in the source system and update the Bifrost config in the same change window. Workflows pick up the new value on the next config.get() call — there’s no cache to invalidate.
  • For high-value secrets that need stronger custody (HSM-backed, audit pipelines, etc.), front Bifrost with an external secret manager and store only a short-lived token here.

KeyError on config.get("...") — the config doesn’t exist for this org. Check Settings → Configuration and confirm the scope matches your execution context.

Decryption errors after a redeployBIFROST_SECRET_KEY changed. The encryption key is derived from this env var; rotating it invalidates every existing encrypted value. Plan key rotations like a database migration.