Skip to content

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:

pnpm run setup-authifi-oidc-clients -- --vault --yes

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.

  1. The resource servers must already exist. main() calls resolveBindingResourceServers before 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.
  2. --identity-provider is 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}/jwk replaces 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:

openssl genrsa -out vault-key.pem 2048
openssl rsa -in vault-key.pem -pubout -out vault-key.pub

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 _PREVIOUS entries 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 dev deployment this is already wired. The bootstrap values live in config/public/ci.env, the key is bind-mounted from the AUTHIFI_VAULT_PRIVATE_KEY secret, and the cutover is the SECRETS_BACKEND GitHub 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 secrets
  • SECRETS_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

  1. On startup, createSecretProvider() reads SECRETS_BACKEND, rejecting any value other than env or authifi rather than defaulting.
  2. For authifi, it loads the vault config from env vars and the RSA private key from disk.
  3. A JWT assertion is signed and exchanged for an access token via the Authifi token endpoint (POST /{tenant}/oidc/token).
  4. The provider lists all tenant secrets, filters by the FLOH_ prefix, and fetches the plaintext value for each.
  5. loadConfig() calls provider.getSecret(key) for each sensitive field. The provider is the only source consulted — there is no process.env fallback behind it. A key the provider does not supply resolves to that field's default, and validateProductionSecrets then 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