Browse the docs
Self-hosting
Rotate the server key
Replace SERVER_KEY without signing anyone out or breaking share links and invitations: keep the previous key during the transition, then remove it safely.
Last updated
On this page
SERVER_KEY is the main secret of a self-hosted Crunu server. You can replace it without signing anyone out and without breaking share links, invitations or authenticator-app sign-in.
What the key protects
SERVER_KEY is 32 random bytes in .env and in your platform's secrets. The server derives subkeys from it to:
- compute HMACs of tokens: sessions of people and machines, invitations, enrollment codes and share link keys. The database stores only the HMAC. The tokens themselves stay with their holders;
- sign one-time sign-in challenges;
- encrypt authenticator-app secrets, alert webhook addresses and signing secrets, and the key the server uses to answer sign-in attempts for unknown email addresses.
Environment data is not encrypted with this key. Its keys belong only to people and machines, so a leaked SERVER_KEY doesn't reveal any variable. If the key and a database copy both leak, together they reveal the authenticator-app secrets, which weakens the second factor, and the alert webhook details. The password and Secret Key are still needed to sign in.
Replacing the key in one step would sign everyone out, because old HMACs would stop matching. So rotation has a transition period. The previous key stays in SERVER_KEY_PREVIOUS. The server checks old tokens with it and re-hashes each one with the new key the first time it's used, unnoticed by people. Everything new uses only the new key.
When to rotate
.envor the platform secrets may have been seen by someone: a stolen laptop,.envpasted into a chat, a repository or a CI log, or a former employee with access to the cloud account.- As routine hygiene, for example once a year.
- Right after a move, if copies of the secrets stayed at the old platform. Move first, rotate after.
Steps
1. Save a copy of the current .env
Put it in a password manager. If anything goes wrong, you can go back with it.
2. Generate the new key
node scripts/crunu-server.mjs new-server-key --env-file .envThe command moves the current SERVER_KEY into SERVER_KEY_PREVIOUS, writes a new random SERVER_KEY and prints non-secret identifiers of both keys. Other lines stay as they are. If a rotation is already in progress, it refuses, because the previous key would otherwise be lost.
3. Deploy the server with the new file
Use the same deploy command as always for your platform. Then check it with doctor. You stay signed in to the web app, because your session was re-hashed on the fly. For the first seconds of the deploy, while old instances still run, a single request may ask you to sign in again. Signing in again works, and no data is lost.
4. Re-encrypt secrets and see what still needs the old key
node scripts/crunu-server.mjs rotate-key --env-file .env --db <database>The command:
- re-encrypts with the new key the authenticator-app secrets, alert channels and the unknown-address key that are still under the old one;
- counts tokens still checked with the old key. They can't be re-hashed in advance, because the server knows only their HMACs. Each is re-hashed on first use, and an unused one lives until it expires;
- counts enrollment codes issued under the old key, including used and expired ones. The server recognizes a reused code, and raises a possible-interception alert, only while the code is stored and the old key is present. Codes are kept 30 days past their expiry;
- names records kept indefinitely, such as dead share links and invalid invitations, whose server answers will change once the old key is gone;
- says when to remove
SERVER_KEY_PREVIOUSand prints the exact command.
If there isn't a single record under the new key yet, it refuses, because re-encrypting before the deploy would break sign-in and alert delivery on the running server. --dry-run only counts, and --json prints a report for programs.
<database> is sqlite:/data/crunu.db for your own server, postgres:env:DATABASE_URL for Postgres, or d1:<database name> with --remote for Cloudflare.
Useful lifetimes: a person's session lasts at most 7 days and ends after 12 hours idle; a machine session lasts 1 hour; an invitation lasts up to 7 days; an enrollment code up to a day. If you issued enrollment codes before the rotation, the old key is usually needed for about a month.
5. Remove the previous key
On the date from the report, or when you decide not to wait longer:
node scripts/crunu-server.mjs drop-previous-key --env-file .env --db <database>With --db, the command checks the database again and refuses if the old key is still needed. Add --force to remove it anyway. Removing it early means:
- people with unused sessions sign in again;
- unused invitations and enrollment codes from before the rotation must be issued again;
- share links from the report stop opening and must be issued again;
- alert channels and authenticator-app secrets that weren't re-encrypted stop working;
- a reused enrollment code from before the rotation no longer raises an alert.
Then deploy once more so the old key disappears from the platform too. On Cloudflare, Vercel, Cloud Run and App Runner, add --prune. On Cloud Run and App Runner, the secret also stays in Secret Manager or Parameter Store, and the script prints the command to delete it.
6. Check
Run doctor, and rotate-key … --dry-run again. The report should say there's no rotation in progress. Save the new .env in your password manager instead of the old copy.
Share links nobody opened
A share link has two secrets: the read key in the link itself and the management token held by its author. The server re-hashes each only when it's presented. After the old key is removed, a link nobody opened during the rotation stops opening, and a link nobody managed by token can't be updated or revoked by token. It can still be revoked from its environment in the web app. You can wait for links with an expiry to run out, issue new links and revoke the old ones, or let them stop working on purpose with --force.
If the key leaked together with a database copy
Re-encryption protects only against future leaks. Ask people to set up authenticator-app sign-in again, or to switch to passkeys. Recreate the unknown-address key with crunu-server new-fake-seed … --force. Issue new signing secrets for alert webhooks and update the receivers, and if a webhook address contains the receiver's token, as with Slack or Teams, issue a new address too. Passwords, Secret Keys and environment data aren't affected.