Rotating auth.secret¶
auth.secret is the root of every secret ShinyHub stores. It is HKDF-derived
(domain-separated) into:
- the AES-256-GCM key that encrypts app environment secrets at rest,
- the key that encrypts the worker CA private key at rest (HA / off-host workers),
- short-lived signing keys for identity JWTs and sticky/session cookies.
Changing auth.secret therefore invalidates everything encrypted under the old
value. Sticky and session cookies recover on their own (a user signs in again
and gets a cookie under the new key), but the at-rest secrets - app env
secrets and the worker CA key - do not: after a naive change they can no longer
be decrypted, and the affected apps fail to start. That makes an otherwise
routine response to a suspected leak a data-loss event.
Identity JWTs do not recover on their own either, for a different reason: an
app process receives its SHINYHUB_IDENTITY_KEY in its environment when it is
spawned, and a running app keeps the key it started with (the server
re-adopts live app processes across its own restart). Until each app is
restarted, the server mints tokens under the new key that the app rejects, and
the client helpers report bad_signature. Step 5 below is part of the
rotation, not optional cleanup.
shinyhub rotate-secret closes that gap: it re-encrypts every at-rest secret
from the current auth.secret to a new one, atomically.
When to rotate¶
- The
auth.secretvalue may have leaked (logs, a shared.env, a backup that left your control). - Routine key hygiene.
Procedure¶
Run with the server stopped (it is a maintenance operation, like backup /
restore), so nothing writes new secrets under the old key mid-rotation.
# 1. Generate the new secret.
NEW=$(openssl rand -hex 32)
# 2. Stop the server.
systemctl stop shinyhub # or however you run it
# 3. Re-encrypt all at-rest secrets. The CURRENT secret is read the usual way
# (config / SHINYHUB_AUTH_SECRET); the NEW one via SHINYHUB_NEW_AUTH_SECRET.
SHINYHUB_NEW_AUTH_SECRET="$NEW" shinyhub rotate-secret --config /etc/shinyhub/shinyhub.yaml
# 4. Switch the server to the new secret (update the config file or the
# SHINYHUB_AUTH_SECRET env used by the unit) and start it.
systemctl start shinyhub
# 5. Restart every app so each picks up its new identity key. A process that
# was already running keeps the key it was spawned with.
for slug in $(shinyhub apps list --json | jq -r '.items[].slug'); do
shinyhub apps restart "$slug"
done
rotate-secret re-encrypts in a single database transaction: if anything
fails (a wrong current secret, a DB error) nothing is committed, so it is safe
to retry - you never end up half old / half new. On success it reports how many
app-env secrets were re-encrypted and whether the worker CA key was rotated.
Notes¶
- The command refuses a new secret shorter than 32 characters, the example placeholder, or one identical to the current secret.
- Take a
shinyhub backupfirst if you want a rollback point; the rotation only touches the encrypted columns, but a backup is cheap insurance. - Non-secret env vars are stored in plaintext and are left untouched.
- HA: run the rotation once against the shared database; the worker CA key lives in the database (not on disk), so all control-plane instances pick up the re-encrypted key when they restart on the new secret.