Skip to content

Secret Rotation

Secret Rotation Frequency Impact of Rotation
JWT_SECRET / JWT_SIGNING_KEYRING Every 90 days None, if rotated via the keyring (see below); all active JWTs invalidated immediately if JWT_SECRET is rotated in legacy single-secret mode
APP_ENCRYPTION_KEY Annually Requires data re-encryption migration
MFA_ENCRYPTION_KEY Annually Requires re-encryption migration; users locked out if skipped
ENROLLMENT_KEY_PEPPER Annually All unexpired enrollment keys invalidated
MFA_RECOVERY_CODE_PEPPER Annually All existing MFA recovery codes invalidated
AGENT_ENROLLMENT_SECRET Every 90 days Only affects new enrollments
SESSION_SECRET Every 90 days No session impact (session tokens are random, not derived from this secret); invalidates in-flight OAuth state and the legacy encryption-at-rest fallback that uses it
POSTGRES_PASSWORD Every 90 days Requires coordinated DB + app restart
REDIS_URL credentials Every 90 days Brief reconnection
S3_ACCESS_KEY / S3_SECRET_KEY Every 90 days None
CLOUDFLARE_API_TOKEN Every 90 days None (existing certs unaffected)
TURN_SECRET Every 90 days Active remote sessions may drop
METRICS_SCRAPE_TOKEN Every 180 days Brief gap in metrics collection
RESEND_API_KEY / SMTP password Per provider policy Brief notification gap
Twilio credentials Per provider policy Brief SMS notification gap
ANTHROPIC_API_KEY Per provider policy AI assistant unavailable
User API keys User responsibility Immediate

Zero-downtime rotation is done through JWT_SIGNING_KEYRING, a JSON map of kid (key ID) to secret, plus JWT_ACTIVE_KID to select which entry signs new tokens. Every other kid in the keyring stays verify-only, which is what lets a rotation drain safely.

  1. Add a new kid alongside the current one in JWT_SIGNING_KEYRING (each secret must be at least 32 characters), but leave JWT_ACTIVE_KID pointed at the existing kid:

    Terminal window
    # existing: JWT_SIGNING_KEYRING={"2026-05":"<current-secret>"}
    # JWT_ACTIVE_KID=2026-05
    JWT_SIGNING_KEYRING={"2026-05":"<current-secret>","2026-08":"<new-secret-from-openssl-rand-base64-64>"}
    JWT_ACTIVE_KID=2026-05
  2. Update .env.prod and restart the API. Both kids now verify incoming tokens; only 2026-05 signs new ones.

  3. Flip JWT_ACTIVE_KID to the new kid and restart again:

    Terminal window
    JWT_ACTIVE_KID=2026-08

    New tokens sign under 2026-08; tokens already issued under 2026-05 keep verifying until they expire.

  4. After one full refresh-token lifetime (7 days), remove the old kid from JWT_SIGNING_KEYRING and restart.

  1. Generate a new key:

    Terminal window
    openssl rand -hex 32
  2. Record the old key securely — you need it for the re-encryption migration

  3. Run the re-encryption migration:

    Terminal window
    OLD_ENCRYPTION_KEY=<old-key> \
    APP_ENCRYPTION_KEY=<new-key> \
    npx tsx scripts/re-encrypt-secrets.ts
  4. Verify decryption of several records with the new key

  5. Update APP_ENCRYPTION_KEY in .env.prod to the new key

  6. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  1. Generate a new key:

    Terminal window
    openssl rand -hex 32
  2. Run a re-encryption migration targeting MFA secret columns (same approach as APP_ENCRYPTION_KEY)

  3. Update MFA_ENCRYPTION_KEY in .env.prod

  4. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  1. Generate a new pepper:

    Terminal window
    openssl rand -hex 32
  2. Update ENROLLMENT_KEY_PEPPER in .env.prod

  3. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  4. Regenerate enrollment keys and redistribute to anyone deploying new agents

  1. Generate a new pepper:

    Terminal window
    openssl rand -hex 32
  2. Update MFA_RECOVERY_CODE_PEPPER in .env.prod

  3. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  4. Notify users to regenerate their MFA recovery codes via Settings, or trigger a bulk regeneration as an admin

  1. Generate a new secret:

    Terminal window
    openssl rand -hex 32
  2. Update .env.prod with the new AGENT_ENROLLMENT_SECRET

  3. Restart the API

  4. Update any deployment scripts or MDM policies with the new secret

  1. Generate a new secret:

    Terminal window
    openssl rand -base64 64
  2. Update SESSION_SECRET in .env.prod

  3. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  4. Active sessions are not invalidated — session tokens are random values, not derived from SESSION_SECRET. Rotation invalidates in-flight OAuth state (accounting and mailbox-connect authorization flows) and removes the old value as a legacy fallback key for secret encryption-at-rest.

  1. Generate a new password:

    Terminal window
    openssl rand -base64 24 | tr -d '/+='
  2. Update the password in PostgreSQL:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml exec postgres \
    psql -U breeze -c "ALTER USER breeze PASSWORD 'new-password';"
  3. Update POSTGRES_PASSWORD and DATABASE_URL in .env.prod

  4. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  1. Set or update the Redis password:

    Terminal window
    redis-cli CONFIG SET requirepass "new-redis-password"
    redis-cli -a "new-redis-password" CONFIG REWRITE
  2. Update REDIS_URL in .env.prod:

    REDIS_URL=redis://:new-redis-password@localhost:6379
  3. Restart the API and worker processes:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api worker
  1. Create a new access key pair in your S3/R2/MinIO console

  2. Update S3_ACCESS_KEY and S3_SECRET_KEY in .env.prod

  3. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  4. Verify by uploading and downloading a test file

  5. Delete the old access key in your storage provider’s console

  1. In the Cloudflare dashboard, go to My Profile > API Tokens

  2. Create a new token with the same permissions (Client Certificates: Edit for the relevant zone)

  3. Update CLOUDFLARE_API_TOKEN in .env.prod

  4. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  5. Verify by triggering a certificate enrollment or renewal

  6. Delete the old token in the Cloudflare dashboard

  1. Generate a new secret:

    Terminal window
    openssl rand -hex 32
  2. Update TURN_SECRET in both the TURN server configuration and .env.prod

  3. Restart the TURN server and the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api turn
  1. Generate a new token:

    Terminal window
    openssl rand -hex 32
  2. Update METRICS_SCRAPE_TOKEN in .env.prod

  3. Update the token file:

    Terminal window
    echo "new-token" > monitoring/secrets/metrics_scrape_token
    chmod 600 monitoring/secrets/metrics_scrape_token
  4. Restart API and Prometheus:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api prometheus

Covers RESEND_API_KEY, SMTP_USER / SMTP_PASS, and MAILGUN_API_KEY.

  1. Rotate the credential in the provider’s dashboard (Resend, Mailgun, or your SMTP provider)

  2. Update the corresponding env var(s) in .env.prod

  3. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  4. Verify by triggering a test notification (e.g., password reset email)

Rotating SMS Provider Credentials (Twilio)

Section titled “Rotating SMS Provider Credentials (Twilio)”

Covers TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN.

  1. In the Twilio console, generate a new Auth Token (or create a new API key pair)

  2. Update TWILIO_ACCOUNT_SID and/or TWILIO_AUTH_TOKEN in .env.prod

  3. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  4. Verify by triggering a test SMS alert

  5. Revoke the old credential in the Twilio console

  1. Generate a new API key in the Anthropic console

  2. Update ANTHROPIC_API_KEY in .env.prod

  3. Restart the API:

    Terminal window
    docker compose -f deploy/docker-compose.prod.yml restart api
  4. Revoke the old key in the Anthropic console

User-facing API keys are managed through the application, not through environment variables.

  • Individual rotation: Users regenerate their API key via Settings > API Keys > Regenerate, or via POST /api/v1/api-keys/:id/rotate — this reissues the secret in place on the same key record (same id, scopes, and rate limit) and resets usage stats; it requires MFA
  • Admin revocation: Admins can revoke any API key via the admin panel or DELETE /api/v1/api-keys/:id
  • Bulk revocation: In a security incident, an admin can revoke all API keys; users must generate new ones

No environment variable changes or restarts are required. The old key is immediately invalidated upon regeneration.

If you suspect any secret has been compromised:

  1. Rotate the compromised secret immediately using the procedures above
  2. Check audit logs for unauthorized access during the exposure window
  3. Rotate related secrets — if JWT_SECRET was compromised, also rotate SESSION_SECRET; if database credentials leaked, also rotate APP_ENCRYPTION_KEY in case encrypted data was exfiltrated
  4. Notify affected users if their data may have been accessed
  5. File a post-incident report documenting the timeline, impact, and remediation