Languages: English · 日本語
This runbook covers rotating the four secrets the framework consumes at build and runtime:
| Env var | Consumer | Rotation class |
|---|---|---|
JWT_SECRET |
HS256 JWT gate | Cold on AWS; coordinated cutover on Cloudflare |
JWKS_URL / kids |
RS256 JWT gate | Hot (publish new kid, wait, revoke) |
URL_SIGNING_SECRET |
Signed URL gate | Cold; already-issued URLs are invalidated |
EDGE_ADMIN_TOKEN |
static_token gate |
Cold (baked into dist/edge/ at build time) |
ORIGIN_SECRET |
Origin auth header | Cold on AWS; coordinated with origin |
Current contract:
secret_envaccepts one environment-variable name. The framework does not supportsecret_envsor multiple accepted secrets in one gate. Do not model a dual-secret window by stacking identical routes: every matching gate is evaluated, so that configuration requires both credentials rather than either credential. Use RS256/JWKS when verifier-side overlap is required.
HS256 is a symmetric shared secret. Rotation requires issuer + verifier to swap it together. The framework only verifies; the issuer is your identity provider or API.
- Pre-flight: know the JWT lifetime (
exp - iat) of the longest-lived token you issue. Typical: 1h access, 7d refresh. - Generate a new 32-byte random secret; store it under a new name:
Upload to your secret store with a new key, e.g.
openssl rand -base64 32
JWT_SECRET_V2. - Schedule a coordinated cutoff because an HS256 gate accepts one secret. Stop issuing long-lived V1 tokens and wait for their TTL where possible.
- Update the existing
JWT_SECRETvalue, switch the issuer to V2, then rebuild and deploy AWSorigin-request.js. Cloudflare reads the runtime secret, but the issuer and Worker secret still need a coordinated cutover. - Verify V2 canaries immediately and revoke V1 after propagation. If verifier-side overlap is mandatory, migrate the gate to RS256/JWKS before rotating.
- Synthetic canary: issue a token with
V2and hit/api/health; expect 200. - Log grep:
block_reason: "Invalid token"on the edge should be flat. If you see spikes, extend the grace window; do not revokeV1yet.
RS256 keys are asymmetric; rotation is driven by the kid claim and the JWKS endpoint.
- Publish a new key to the JWKS endpoint with a new
kid. Keep the old key in the JWKS response. - Update the issuer to start signing with the new
kid. - Wait
firewall.jwks.cache_ttl_sec + firewall.jwks.stale_if_error_sec + max_token_ttl. The framework caches JWKS responses, so until the cached response is refreshed, the edge will not see the newkid. - Remove the old key from the JWKS endpoint.
- Wait another
cache_ttl_secfor the removal to propagate. - Verify that tokens signed by the old key now return
401 block_reason: "Unknown kid".
- If
firewall.jwks.cache_ttl_secis large (e.g. 1h), step 3 must wait a full hour plus token TTL. Short TTLs give faster rotation but more JWKS endpoint load. - Never remove the old kid before waiting the cache window. Issued tokens carrying the old kid will fail and users will be logged out mid-session.
Signed URLs embed a signature computed at issue time. Rotating the secret invalidates every URL already in a user's inbox, email, or share sheet.
- Decide a grace window equal to
max(signed_url.default_ttl, email_delivery_window). 72h is a common floor. - Stop issuing V1 URLs and wait for the grace window if the old secret is not compromised.
- Replace
URL_SIGNING_SECRET, update the issuer, then rebuild/deploy AWS or update the Cloudflare Worker secret in one coordinated window. - Verify a newly issued URL and revoke the old value. The framework currently cannot accept old and new signed-URL secrets simultaneously.
If the old secret is compromised, skip the grace window:
- Rotate the issuer to
V2immediately. - Deploy verifier with only
V2. - Accept that already-issued URLs break. Communicate to users ("your previous download link is no longer valid — request a new one").
- Audit logs for requests that verified against
V1in the compromise window.
CloudFront Functions cannot read env vars at runtime. The static_token gate bakes the token into dist/edge/viewer-request.js at build time. Rotation therefore requires a rebuild + redeploy, not just a secret-store update.
- Generate a new token:
openssl rand -hex 32. - Update the secret store (
EDGE_ADMIN_TOKENvalue). - Rebuild:
EDGE_ADMIN_TOKEN=<new> npm run build. - Deploy
dist/edge/viewer-request.jsto CloudFront. There is a brief cut-over window (CloudFront global propagation: 2–5 min) where some edges serve the old token, some the new. Plan admin access accordingly. - Communicate the new token to admin operators.
The gate accepts one value per build. Stacking two routes with the same prefix does not provide OR semantics; both gates run. Plan a maintenance/cutover window or put a separately managed authenticator in front of the edge.
The origin auth gate adds either a shared-secret header (custom_header) or HMAC signature headers (hmac_signature) to every origin request. Rotation requires the origin (ALB, NGINX, CF Worker, or app) to accept both values during the window.
- Generate
ORIGIN_SECRET_V2. - Update the origin to accept requests carrying either
V1orV2. - Rebuild and deploy the edge with
V2(the edge signs/forwards with a single secret; the origin owns the dual-accept). - Wait deploy propagation + a conservative buffer (5–15 min).
- Update the origin to accept only
V2. - Revoke
V1.
- Synthetic request hitting origin directly (bypassing the edge) with
V1: expect 401 after step 5. - Edge request: expect 200 throughout.
After any rotation:
- Canary: a synthetic request that exercises the rotated gate.
- Log grep: spike in
block_reasonrelated to the rotated credential within the first 10 minutes means the grace window was too short. - User-impact sampling: check support channels and front-end error rates.
- Audit: verify the old secret is purged from the secret store, CI env, and any cached
.envfiles.
If a rotation causes user-visible failures:
- Restore the old secret to the secret store (you kept it, right? See below).
- Revert the verifier policy to accept both old and new.
- Rebuild and deploy.
- Investigate before re-attempting: likely cause is an under-sized grace window or a cache you didn't account for.
Always keep the previous secret in an offline vault for at least 24 hours post-rotation. Do not purge immediately.