Skip to content

Manage secrets and rotate keys

Cirrus stores four Worker secrets, each with a different lifetime and rotation profile:

SecretControlsRotation cost
SIGNING_KEYRepository signatures, service JWTsHigh — changes the DID document
JWT_SECRETSession JWT signing (120 min access tokens, 90 day refresh tokens)Low — invalidates active sessions
PASSWORD_HASHAccount password verificationLow — forces a sign-in next time
AUTH_TOKENStatic bearer token for adminLow — invalidates current scripts

The signing key has a sibling public value, SIGNING_KEY_PUBLIC, that lives in wrangler.jsonc as a var (not a secret). It is the public half of the signing keypair and is used to serve the DID document. Rotating the signing key requires updating both.

This page covers how to rotate each.

In local development, secrets are in .dev.vars (gitignored).

In production, secrets are Worker secrets stored on Cloudflare. They are write-only: once set, the dashboard and API do not return their value. Push secrets from .dev.vars to production with:

Terminal window
pnpm pds init --production

This runs the wizard in production mode: it reads each secret from .dev.vars and uploads it as a Worker secret.

For a single secret, use wrangler directly:

Terminal window
pnpm wrangler secret put SIGNING_KEY

The command prompts for the value (it is not visible in shell history).

Rotating JWT_SECRET invalidates all existing session JWTs. Users must sign in again.

Terminal window
pnpm pds secret jwt

The pds secret jwt command generates a fresh secret and uploads it to Cloudflare as the JWT_SECRET Worker secret via wrangler secret put.

To write the new secret to .dev.vars instead of uploading it, pass --local:

Terminal window
pnpm pds secret jwt --local

After rotation, the next Bluesky app sign-in re-issues a session signed with the new secret. Existing sessions silently stop working.

Rotating the password invalidates the previous password. App passwords are unaffected (they are stored independently).

Terminal window
pnpm pds secret password

The pds secret password command prompts for the new password (or generates one), bcrypts it, and uploads the hash to Cloudflare as the PASSWORD_HASH Worker secret.

Pass --local to write the hash to .dev.vars instead:

Terminal window
pnpm pds secret password --local

After rotation, sign in with the new password.

AUTH_TOKEN is the long-lived admin bearer. Rotate by generating a new random value and uploading it:

Terminal window
# Generate a fresh token (any method works; e.g.):
openssl rand -hex 32
pnpm wrangler secret put AUTH_TOKEN

Update any scripts that use the previous token.

Rotating SIGNING_KEY is the most consequential operation. The signing key is referenced by the DID document; rotation requires publishing an updated DID document.

pds secret key generates a new secp256k1 keypair and updates both halves in one step:

  • The private half is uploaded to Cloudflare as the SIGNING_KEY Worker secret.
  • The public half is written to the SIGNING_KEY_PUBLIC var in wrangler.jsonc.

SIGNING_KEY_PUBLIC is a public configuration value, not a secret. It must be edited in wrangler.jsonc (or via pds secret key) and picked up by the next wrangler deploy. Running wrangler secret put SIGNING_KEY_PUBLIC is wrong — it will not affect the served DID document.

The PLC operation handles the rotation:

  1. Generate a new signing key. This uploads SIGNING_KEY and updates SIGNING_KEY_PUBLIC in wrangler.jsonc:
    Terminal window
    pnpm pds secret key
  2. Sign a PLC operation with the recovery key that swaps the active signing key to the new one. The Bluesky app’s settings or the PLC tooling can do this. The new public key is now in wrangler.jsonc; use it in the operation.
  3. Submit the operation to plc.directory.
  4. Redeploy so the Worker picks up the new SIGNING_KEY_PUBLIC value:
    Terminal window
    pnpm run deploy

The DID is preserved. Existing signed commits remain verifiable against the previous public key, which is still recorded in the DID document’s history.

For did:web, the DID document is served by Cirrus itself. Rotation is simpler but breaks cryptographic continuity:

  1. Generate a new signing key. This uploads SIGNING_KEY and updates SIGNING_KEY_PUBLIC in wrangler.jsonc:
    Terminal window
    pnpm pds secret key
  2. Redeploy:
    Terminal window
    pnpm run deploy

The served DID document now lists the new public key. Old signed commits do not verify against the new key. Followers and content survive — relays accept new commits because the DID document advertises the new key — but the cryptographic chain is reset.

  • SIGNING_KEY: on suspected compromise, periodically (annually is reasonable hygiene), and after a recovery scenario.
  • JWT_SECRET: on suspected compromise. Otherwise rarely.
  • PASSWORD_HASH: on suspected compromise, or when the password is reused elsewhere.
  • AUTH_TOKEN: when an admin script holding it is decommissioned or when collaborators with access change.

Every rotation produces a new secret value. Update the backup (password manager, encrypted file) at the same time the production secret is rotated. See Back up your signing key — the same advice applies to all four secrets, with the signing key being the highest-stakes one.