Secret Rotation Runbook
Every secret configured forapps/platform (see
apps/platform/.env.example), plus the
compliance signing key, which is deliberately not committed to .env.example
since it must never take the placeholder value changethis. For each: what
it protects, rotation steps, and blast radius if it leaks or rotation goes
wrong.
Secrets live in Railway environment variables for staging/production (see
docs/project/deployment.md) and in a local .env for
development. Rotating a secret always means: generate the new value, set it
in Railway, redeploy, confirm health, then invalidate the old value at the
provider if the provider supports that (API keys) — never the other way
around, or you create a window with no working credential.
Database — POSTGRES_PASSWORD (or DATABASE_URL)
Protects: the primary Postgres database — every run, action, ledger row,
policy, and user record.
Rotation steps:
- In the Postgres provider (Railway-managed or external), create a new
password for the
POSTGRES_USERrole without revoking the old one yet. - Update
POSTGRES_PASSWORD(or the fullDATABASE_URL, which takes precedence perapp/core/config.py) in the platform’s Railway environment. - Redeploy the platform service and confirm
GET /api/v1/utils/health-check/returns healthy and a real query path (e.g. list runs) succeeds. - Revoke the old password at the database.
docs/project/incident-protocol.md, rotate
immediately, and check pg_stat_activity (or the provider’s connection
audit log) for connections you don’t recognize before and after rotation.
Compliance Signing Key — COMPLIANCE_SIGNING_KEY_ED25519 (+ COMPLIANCE_SIGNING_KEY_ID)
Protects: the Ed25519 private key ComplianceSigner
(apps/platform/app/services/compliance_signing.py) uses to sign compliance
export manifests. It is what makes an export tamper-evident and
auditor-verifiable.
Rotation steps:
- Generate a new key pair:
- Choose a new
COMPLIANCE_SIGNING_KEY_ID— do not reuse the old ID, since exports already signed under the old ID must remain verifiable against the old public key. - Publish the new key’s public key (
ComplianceSigner.get_public_key_pem()) wherever existing public keys are distributed for verification, keyed by the newCOMPLIANCE_SIGNING_KEY_ID. Do not delete the old public key — past exports still need it. - Set
COMPLIANCE_SIGNING_KEY_ED25519andCOMPLIANCE_SIGNING_KEY_IDto the new values in Railway and redeploy. - Confirm a fresh compliance export is signed under the new key ID.
COMPLIANCE_SIGNING_KEY_ID with a
new key — that breaks verification for every export already issued under
that ID.
Clerk — CLERK_SECRET_KEY, CLERK_PUBLISHABLE_KEY, CLERK_WEBHOOK_SECRET
Protects: authentication. CLERK_SECRET_KEY backs server-side Clerk API
calls (apps/platform/app/core/clerk.py); CLERK_PUBLISHABLE_KEY is used
client-side and is not sensitive by design; CLERK_WEBHOOK_SECRET verifies
that inbound webhooks (apps/platform/app/api/routes/clerk_webhooks.py)
actually came from Clerk.
Rotation steps:
- In the Clerk dashboard, generate a new secret key (and webhook signing secret, if rotating that too) for the instance.
- Set
CLERK_SECRET_KEY(and/orCLERK_WEBHOOK_SECRET) in Railway and redeploy. - Confirm sign-in and at least one webhook-driven flow (e.g. a user creation event) still work.
- Revoke the old key in the Clerk dashboard.
CLERK_PUBLISHABLE_KEY also requires updating the console’s build
(apps/console-sentinel) and redeploying it, since that key is baked into
the client bundle.
Blast radius: a leaked CLERK_SECRET_KEY lets an attacker act as the
Clerk backend against your instance — impersonate the platform’s own calls
to Clerk (user lookups, session verification). A leaked
CLERK_WEBHOOK_SECRET lets an attacker forge webhook events (e.g. fake user
creation/deletion) since verify_webhook_signature would accept them. Both
are auth-adjacent — treat as SEV1/SEV2 per the incident protocol.
Resend — RESEND_API_KEY
Protects: outbound transactional email (approval notifications, password
resets) sent via apps/platform/app/utils.py. If unset, email sending is
skipped with a warning, not a failure.
Rotation steps:
- Create a new API key in the Resend dashboard.
- Set
RESEND_API_KEYin Railway and redeploy. - Send a test email (e.g. trigger a password reset) to confirm delivery.
- Revoke the old key in Resend.
EMAILS_FROM_EMAIL (noreply@agentsentinel.dev) through your Resend
account — reputational and phishing risk, and potential quota/billing abuse.
It does not expose stored data. SEV2 unless there’s evidence of abuse
already in progress.
Gemini — GEMINI_API_KEY
Protects: nothing customer-sensitive by itself, but it authorizes calls
to Google’s Gemini API, billed to your account. Used for prose policy
compilation (app/services/policy_compiler.py), the MCP policy advisor
(app/services/mcp_advisor.py), intervention enrichment
(app/services/intervention_enricher.py), and eval scenario generation
(app/services/gemini_scenario_generator.py). All of these degrade
gracefully (log and skip) when the key is absent — rotating it never causes
an outage, at worst a temporary loss of these optional features.
Rotation steps:
- Create a new key at https://aistudio.google.com/apikey.
- Set
GEMINI_API_KEYin Railway and redeploy. - Delete the old key in AI Studio.
Platform Session Signing — SECRET_KEY
Protects: HS256 signing for session/access JWTs and password-reset
tokens (app/core/security.py, app/api/deps.py, app/utils.py) and, in
local/offline-auth mode, dev tokens (app/core/clerk_dev.py). Ships with a
random default in code, but .env.example uses the placeholder
changethis — Settings._enforce_non_default_secrets refuses to start in
staging/production if it’s still changethis.
Rotation steps:
- Generate a new value, e.g.
python -c "import secrets; print(secrets.token_urlsafe(32))". - Set
SECRET_KEYin Railway and redeploy.
SECRET_KEY lets an attacker forge valid session tokens for any user.
Rotate immediately on suspected leak (SEV1) and accept the forced
sign-out as the cost of containment.
First Superuser — FIRST_SUPERUSER_PASSWORD
Protects: the bootstrap admin account (FIRST_SUPERUSER). Also refused
at startup if left as changethis outside local.
Rotation steps:
- Change the password through the platform’s own user-management path (or
update
FIRST_SUPERUSER_PASSWORDand re-run the seed step if the account is recreated from env on deploy). - If rotating in Railway, redeploy after setting the new value.
SMTP — SMTP_PASSWORD
Protects: local-development email delivery only in this deployment
(Mailcatcher in local, unused in staging/production where RESEND_API_KEY
is the real email path — see .env.example’s local-development block).
Rotation steps: rotate at the SMTP provider if one is ever configured
for staging/production; for local Mailcatcher, there’s nothing to rotate.
Blast radius: minimal in the current deployment (local dev only); would
follow the same email-abuse blast radius as Resend if a real SMTP provider
is ever wired up for a non-local environment.
Sentry — SENTRY_DSN
Protects: nothing by itself (a DSN is not a secret Sentry treats as
sensitive — it’s meant to be embedded in client code), but rotating it stops
error reports from reaching your Sentry project, which matters if a former
collaborator’s Sentry access needs to be cut off.
Rotation steps: generate a new DSN in the Sentry project settings, set
SENTRY_DSN in Railway, redeploy.
Blast radius: low. Worst case is noise (someone sends fake error events
to your DSN) or a gap in error visibility during rotation.