Rotating Keys and Secrets¶
Every key and secret SystemForge verifies rotates the same way: the current value N and the previous value N-1 are both valid, and a rotation retires only N-2. A rotation therefore never signs anybody out, and rolling back a deploy always returns to a configuration that still accepts everything issued so far. This page is the runbook; each section links to the full reference.
The rule¶
- Pre-stage. Make the new value N known to every verifier while N-1 is still the one in use. Wait until every instance runs it.
- Promote. Start using N. Keep N-1 valid.
- Verify. Confirm N works everywhere: tokens verify, clients authenticate, sessions open, provisioning succeeds.
- Retire. Drop N-1 only once nothing still needs it (its tokens have expired, every caller uses N), or leave it for the next rotation, which drops it as N-2.
Never introduce a new value and drop the previous one in the same deploy: a rollback would then reject everything the new value produced.
| What | Where it is set | Pre-staged? | Reference |
|---|---|---|---|
| SystemAuth signing key | keys.key_id, keys.verification_keys |
Yes | Deploying SystemAuth |
identity/oauth signing key |
Config.KeyID, Config.VerificationKeys |
Yes | DPoP Enforcement |
session/jwt keys |
jwt.Config.KeyID, VerificationKeys |
Yes | JWT Audience |
| OAuth client secrets | EntStorage.RotateClientSecret |
No: N and N-1 both authenticate | OAuth Apps |
| BFF session sealing keys | bff.NewKeyringSealer, or bff.NewKeysSealer |
Yes | BFF Sessions, Ent Store |
| SCIM tenant tokens | scimtoken Rotate, or POST .../scim-tokens/rotate |
No: N and N-1 both authenticate | SCIM Provisioning |
| Connections key ring | keyforge, on schedule | Automatic | Key Management |
SystemAuth signing keys¶
Tokens carry the kid of the key that signed them, and a token with an
unknown kid is refused.
- Generate key N and add it to
keys.verification_keyswhile N-1 signs. Relying parties see it in the JWKS before any token signed with it exists. - Make N the signing key (
keys.private_key_fileorprivate_key_pem, andkeys.key_id), and move N-1 intokeys.verification_keys. Drop N-2. - Check that new tokens carry the new
kidand that relying parties accept them. - Keep N-1 listed until the longest-lived token it signed has expired: ID tokens (1 hour by default) and JWT access tokens (at most 15 minutes).
keys:
private_key_file: /etc/systemauth/signing-3.pem # N, signing
key_id: signing-3
verification_keys:
- key_id: signing-2 # N-1
public_key_file: /etc/systemauth/signing-2.pem
The identity/oauth provider and session/jwt follow the same steps with
their own KeyID and VerificationKeys. For session/jwt, give the
current key a KeyID first and wait until tokens issued without one have
expired before the first rotation.
OAuth client secrets¶
A confidential client authenticates with its two newest active secrets.
EntStorage.RotateClientSecret issues N, keeps N-1 and revokes anything
older in one step:
rotated, err := storage.RotateClientSecret(ctx, "reporting-service")
if err != nil {
return err
}
// rotated.Secret is shown once: hand it to the client's secret store.
- Rotate, and deploy the new secret to the client.
- Watch the client authenticate with it.
- End the overlap with
RevokeClientSecret(ctx, clientID, previousID), or leave N-1 for the next rotation. If the new secret is bad, revoke it instead: N-1 is current again.
A client listed under clients in the SystemAuth configuration file is
created from its secret the first time SystemAuth starts with a
database. Later starts update the client's other settings but not its
secret, so rotate that client's secret with RotateClientSecret too
rather than by editing the file.
BFF session sealing keys¶
Session secrets (tokens and DPoP private keys) are sealed under a key ID,
and each sealed value names its key. With bff.NewKeyringSealer, rotate in
separate deploys:
// 1. Pre-stage k2 while k1 stays active.
sealer, err := bff.NewKeyringSealer("k1", map[string][]byte{"k1": k1, "k2": k2})
// 2. Promote k2 and keep k1.
sealer, err = bff.NewKeyringSealer("k2", map[string][]byte{"k1": k1, "k2": k2})
// 3. Next rotation: pre-stage k3 with k1 still present, then promote k3
// with {"k2": k2, "k3": k3}.
Sessions are sealed again under the active key whenever they are updated,
for example on token refresh. Sessions still sealed with N-2 when it is
removed no longer open, and their users sign in again.
bff.NewKeysSealer seals with a keyforge purpose key instead, which
versions and rotates its keys itself.
SCIM tenant tokens¶
An organization has at most two valid SCIM tokens, the current one and the
previous one. Organization owners and admins rotate them through
/api/v1/organizations/{slug}/scim-tokens with an access token granted
systemforge:scim_tokens:
POST /api/v1/organizations/{slug}/scim-tokens/rotate. The response carries the new token once, and lists any tokens the rotation revoked (N-2 and older).- Configure the identity provider with the new token and watch
provisioning succeed. If it fails, revoke the new token
(
DELETE .../scim-tokens/{token_id}); the previous one is current again. - Revoke the previous token once the switch is confirmed, or leave it for the next rotation.
GET .../scim-tokens lists each token's status (current, previous,
revoked, expired or superseded) and when it was last used.
Keys that are not rotated this way¶
- The connections key ring. keyforge rotates its keys on schedule
without re-encrypting data; see Key Management.
After restoring or moving the keys directory, or once after upgrading to
v0.21.0, run
systemauth keys accept-rollback --confirm. - A relying party's DPoP key (
relyingparty.Config.DPoPKey). Tokens and refresh tokens are bound to it, so sessions holding tokens bound to the old key cannot refresh them, and their users sign in again.