Key Rotation
Wallow's identity provider signs and encrypts tokens with two X.509
certificates, seals cookies with ASP.NET Data Protection keys, and (on the BFF
side) seals its own session cookies with COOKIE_PASSWORD(S). Each has a
different rotation story. This runbook covers what each key protects, what
breaks when it changes, and the procedure for rotating it.
The key inventory
| Key | Where it lives | What it protects | Rotation |
|---|---|---|---|
| OpenIddict signing certificate | signing.pfx at OpenIddict:SigningCertPath (production: the api_certs volume, /app/certs/signing.pfx) |
Signs access and identity tokens; its public key is published in the JWKS at /.well-known/openid-configuration → jwks_uri |
Manual, swap-and-restart (below) |
| OpenIddict encryption certificate | encryption.pfx at OpenIddict:EncryptionCertPath (production: /app/certs/encryption.pfx) |
Encrypts refresh tokens and authorization codes (access tokens are deliberately not encrypted — see Access Token Format) | Manual, swap-and-restart (below) |
.pfx passwords |
OPENIDDICT_SIGNING_CERT_PASSWORD / OPENIDDICT_ENCRYPTION_CERT_PASSWORD in .env.production |
Protect the certificate files at rest | Only together with a new certificate — the password is baked into the .pfx at generation time |
| ASP.NET Data Protection keys | Valkey/Redis, key DataProtection-Keys |
Seal the IdP's own session and antiforgery cookies | Automatic — ASP.NET rotates them (~90 days) and keeps old keys for unprotection; no operator action |
| BFF cookie seal password | COOKIE_PASSWORD / COOKIE_PASSWORDS on each relying-party BFF |
Seals the RP's session, transaction, and store-reference cookies | Keyed rotation without logging anyone out — see Rotating the Cookie Password |
In development none of this applies: OpenIddict uses its ephemeral development certificates, regenerated per machine.
How the certificates come to exist
On first startup in a non-development environment, if the configured .pfx
files are missing, the API self-signs a 10-year 2048-bit RSA certificate for
each role and writes it to the configured path, protected by the configured
password (EnsureCertificateExists in IdentityInfrastructureExtensions). In
the production compose stack those paths sit on the persistent api_certs
volume, so the certificates survive restarts and image upgrades.
Self-signed is correct here: nothing chains these certificates to a CA. Clients trust the keys via the discovery document's JWKS, not the certificate chain.
Rotating the signing or encryption certificate
Wallow currently loads one certificate per role, so rotation is swap-and-restart: there is no overlap window where old and new keys are both live. Know the blast radius before you start.
Blast radius
- Rotating
signing.pfxinvalidates every outstanding access and identity token (default lifetimes: 15 and 10 minutes). API calls with an old access token start failing signature validation immediately. - Rotating
encryption.pfxinvalidates every outstanding refresh token (default lifetime: 7 days) and any in-flight authorization codes. Sessions cannot silently refresh and must re-authenticate. - What survives: the IdP's own session cookie is sealed with Data
Protection keys in Valkey, not with these certificates. Because that session
survives, a relying party whose tokens just died is bounced through
/connect/authorize, gets silent SSO against the still-valid IdP session, and lands back signed in — users see a redirect flicker, not a login form. - External relying parties that cache the JWKS re-fetch it when they see an
unknown
kid, so ID-token validation recovers on its own.
Practical consequence: rotating both certificates during quiet hours costs users one silent re-auth. It is disruptive on the order of seconds, not a log-everyone-out event.
Procedure
Generate the replacement
.pfxfiles. Easiest is to let the API regenerate them: with the stack stopped, remove the old files from the volume, then start the API. Update the password variables first if you are rotating those too — the new password is baked into the newly generated files.docker compose -f docker/docker-compose.production.yml stop wallow-api docker compose -f docker/docker-compose.production.yml run --rm --entrypoint sh wallow-api \ -c 'rm /app/certs/signing.pfx /app/certs/encryption.pfx' docker compose -f docker/docker-compose.production.yml up -d wallow-apiTo bring your own key material instead, export a PKCS#12 bundle to the same paths (
openssl pkcs12 -export ...), protected by the passwords from.env.production, before starting the API.Verify the new key is being served:
curl -s https://<host>/api/.well-known/openid-configuration | jq -r .jwks_uri curl -s <jwks_uri> | jq '.keys[].kid'Confirm a login round-trip works (any RP, or the auth app itself). Expired BFF sessions self-heal through silent SSO; nothing else to clean up.
If a private key is compromised
Rotate immediately as above, and additionally revoke standing grants: a stolen signing key mints arbitrary access tokens until the JWKS no longer carries it, so treat the rotation as the containment step and audit token issuance around the suspected window (see Audit Events). Restart all API replicas together — a replica still holding the old signing key keeps minting tokens the others reject, and vice versa.
Multi-replica deployments
All API replicas must load the same certificate files — share the
api_certs volume or pre-provision identical .pfx files on every node. The
first-boot auto-generation is per-filesystem: two replicas with separate empty
volumes will each mint their own keys and reject each other's tokens.
Future enhancement (not built)
Zero-flicker rotation needs the JWKS to publish the incoming signing key
before it starts signing, and the outgoing key to stay valid for verification
until the last old token expires. OpenIddict supports registering multiple
signing credentials, so this is an additive config change
(OpenIddict:AdditionalSigningCertPaths or similar) if the silent-SSO
re-auth ever becomes too disruptive. Until then, swap-and-restart is the
documented and supported path.