Secrets Management¶
Floh uses a pluggable SecretProvider interface to load sensitive configuration
values. The provider is selected at startup via the SECRETS_BACKEND environment
variable.
Backends¶
env (default)¶
Reads secrets from environment variables / .env files. This is the default
for local development — no additional setup required.
authifi¶
Fetches secrets from the Authifi tenant secrets vault at startup.
Authentication uses private_key_jwt (RFC 7523) so the only file-based
credential is an RSA private key.
Development Setup¶
No changes from the standard workflow. Leave SECRETS_BACKEND unset (or set
to env) and secrets are read from your .env file as before.
Production Setup (Authifi)¶
1. Register the vault client and its key pair¶
For a development tenant, let the setup script do both steps:
A genuinely fresh tenant needs two things first
--vault is additive, not exclusive, so this command is not a
vault-only shortcut past the normal setup.
- The resource servers must already exist.
main()callsresolveBindingResourceServersbefore it branches on--dry-run, so the run needs the console, portal, and transitional resource servers whatever it was invoked for. Create them first — see the bring-up checklist, steps 7–8. --identity-provideris required. The planner still queues creation of the console client when it is absent, and that action refuses to guess a provider. Use--vault --yes --identity-provider <provider>.
Neither applies once the tenant is set up: the resource servers persist, and the flag is unnecessary once the console client exists.
That registers floh-vault-client with private_key_jwt, asks Authifi to mint
the RSA key pair, and writes the private key to ~/.floh/vault-key.pem at mode
0600 — outside the repository, so it cannot be staged. The non-secret
bootstrap values land in env/vault.env; the PEM itself is never copied into an
env file. Override the location with --vault-key-file <path>.
Two behaviors are worth knowing before you run it a second time:
- The key file is never overwritten.
POST .../clients/{id}/jwkreplaces the client's key rather than adding to its JWKS, and the previous private key stops authenticating the moment the new one is issued. The script therefore exits if a key file already exists, before making the destructive call. - Rotation is a hard cutover. There is no overlap window. Move the old key aside, re-run, and restart the server against the new PEM in the same maintenance window. Automating this is tracked by LSA-9829.
For production, keep the key out of any developer's hands and generate it manually instead:
Then register the public key on the client as a JWK via
PATCH /auth/admin/tenants/{tenantId}/clients/{clientId}/jwk.
Floh signs its client assertions with RS256 and no kid header
(packages/server/src/config/jwt-assertion.ts). That is correct as long as the
client has exactly one registered key, which is what both paths above produce.
Do not add a kid unless you also thread the registered value through config —
Authifi fails closed on a kid that does not match a key in the JWKS.
2. Create secrets in Authifi¶
Using the Authifi admin API or UI, create tenant secrets with a FLOH_
prefix. Each secret name maps to the corresponding environment variable:
| Authifi secret name | Maps to |
|---|---|
FLOH_DB_PASSWORD |
DB_PASSWORD |
FLOH_JWT_SECRET |
JWT_SECRET |
FLOH_SESSION_SECRET |
SESSION_SECRET |
FLOH_OIDC_CLIENT_SECRET |
OIDC_CLIENT_SECRET |
FLOH_PORTAL_OIDC_CLIENT_SECRET |
PORTAL_OIDC_CLIENT_SECRET |
FLOH_CONNECTOR_ENCRYPTION_KEY |
CONNECTOR_ENCRYPTION_KEY |
FLOH_SESSION_ENCRYPTION_KEY |
SESSION_ENCRYPTION_KEY |
FLOH_AUDIT_CHECKPOINT_KEY |
AUDIT_CHECKPOINT_KEY |
FLOH_REDIS_PASSWORD |
REDIS_PASSWORD |
FLOH_SMTP_USER |
SMTP_USER |
FLOH_SMTP_PASS |
SMTP_PASS |
FLOH_CONNECTOR_ENCRYPTION_KEY_PREVIOUS |
CONNECTOR_ENCRYPTION_KEY_PREVIOUS |
FLOH_AUDIT_CHECKPOINT_KEY_PREVIOUS |
AUDIT_CHECKPOINT_KEY_PREVIOUS |
The two
_PREVIOUSentries only exist during a key rotation, and nothing validates them at startup. Because the provider is the only source consulted, switching to the vault mid-rotation without copying them across boots successfully and then cannot decrypt connector credentials written under the old key, or verify historical audit checkpoints. Create them whenever the corresponding environment variable is currently set.
3. Configure the Floh server¶
Set the following environment variables (these are non-secret bootstrap values):
SECRETS_BACKEND=authifi
AUTHIFI_VAULT_URL=https://auth.example.com/_api
AUTHIFI_VAULT_TENANT=my-tenant
AUTHIFI_VAULT_TENANT_ID=123
AUTHIFI_VAULT_CLIENT_ID=floh-vault-client
AUTHIFI_VAULT_KEY_FILE=/etc/floh/vault-key.pem
Mount the private key file into the container at the path specified by
AUTHIFI_VAULT_KEY_FILE.
On the
devdeployment this is already wired. The bootstrap values live inconfig/public/ci.env, the key is bind-mounted from theAUTHIFI_VAULT_PRIVATE_KEYsecret, and the cutover is theSECRETS_BACKENDGitHub variable. Follow the deployment runbook rather than setting these by hand.
Note that the provider is a total replacement: it serves only what the vault
returns, and loadConfig resolves secrets through the injected provider alone
rather than falling back to process.env. Reading the environment is what
EnvSecretProvider is for; a fallback behind the provider would let an
incomplete vault boot on whatever the environment happened to hold. Every
secret the server requires must therefore exist in the vault before the switch.
Secrets consumed by other containers (postgres, redis, portal-bff) are
unaffected by this setting — they read their environment directly and have no
vault client.
4. Required Authifi client scopes¶
The OAuth2 client needs these scopes:
SECRETS_MANAGER.LIST— enumerate tenant secretsSECRETS_MANAGER.PLAIN_SECRET— retrieve decrypted values
No write scopes are needed; secrets are provisioned separately by tenant admins.
These are requested at token time through AUTHIFI_VAULT_SCOPE, not stored on
the client — Authifi has no per-client scope-grant route, so this variable is
the only place the vault client's authority is expressed. The setup script
writes exactly the two read scopes above into env/vault.env.
The vault client is deliberately not assigned to the Floh API resource server: it talks to Authifi's secrets API, never to the Floh API, so an assignment would only widen the audience of its tokens.
One quirk to expect if you register the client by hand: Authifi rejects a token
request from a client whose callbackUrls is empty with
400 invalid_redirect_uri, even though this client uses client_credentials
and never sees a browser. The check runs before assertion validation, so it
looks like an authentication failure. Give the client a single placeholder
callback (scripts/authifi/floh-vault-client.json uses
https://localhost:4200) and never a real Floh origin.
How It Works¶
- On startup,
createSecretProvider()readsSECRETS_BACKEND, rejecting any value other thanenvorauthifirather than defaulting. - For
authifi, it loads the vault config from env vars and the RSA private key from disk. - A JWT assertion is signed and exchanged for an access token via the
Authifi token endpoint (
POST /{tenant}/oidc/token). - The provider lists all tenant secrets, filters by the
FLOH_prefix, and fetches the plaintext value for each. loadConfig()callsprovider.getSecret(key)for each sensitive field. The provider is the only source consulted — there is noprocess.envfallback behind it. A key the provider does not supply resolves to that field's default, andvalidateProductionSecretsthen rejects the required ones outside dev/test, so a partially populated vault fails the boot instead of silently retaining environment-backed values.
Architecture¶
┌─────────────┐
│ loadConfig │
│ (async) │
└──────┬───────┘
│
▼
┌──────────────────┐
│ SecretProvider │◄── interface
└──────┬───────────┘
│
┌────┴────────────────┐
│ │
▼ ▼
┌──────────────┐ ┌─────────────────────┐
│ EnvSecret │ │ AuthifiSecret │
│ Provider │ │ Provider │
│ (process.env)│ │ (vault + JWT auth) │
└──────────────┘ └─────────────────────┘
Optional Configuration¶
| Variable | Default | Description |
|---|---|---|
AUTHIFI_VAULT_SECRET_PREFIX |
FLOH_ |
Prefix for filtering tenant secrets |
AUTHIFI_VAULT_SCOPE |
SECRETS_MANAGER.LIST SECRETS_MANAGER.PLAIN_SECRET |
OAuth2 scopes |
See Also¶
- Encryption Keys — AES-256-GCM keys for connector secrets, session data, and audit checkpoints
- Authifi Secret Manager API — look for the Tenant Secret Manager tag