Appearance
Rotate the NSK wrap key
Every Node Secret Key (NSK) is stored wrapped at rest. The software wrap-key provider seals each NSK with AES-256-GCM under a single static key the binary reads from PLEXSPHERE_NSK_WRAP_KEY_B64. The server holds exactly one wrap key at a time, so it can only unwrap rows that were sealed under that same key.
Rotating the wrap key therefore has two halves that must happen in order: re-wrap every stored row from the old key to the new key, then flip the server's PLEXSPHERE_NSK_WRAP_KEY_B64 to the new key. The re-wrap is driven by the nsk-rewrap subcommand of the plexsphere-backup operator CLI implemented in ../../../cmd/plexsphere-backup/nsk_rewrap.go.
A KMS- or OpenBao-backed wrap-key provider (external custody, no env key at all) is a planned follow-up. Until it ships, the wrap key is an operator-held env value and this runbook is the sanctioned way to rotate it.
Prerequisites
Operator access to the plexsphere core Postgres and a
plexsphere-backupbinary that can reach it.The current wrap key (the value of
PLEXSPHERE_NSK_WRAP_KEY_B64on the running deployment).A new 32-byte wrap key. Generate one with:
shellhead -c 32 /dev/urandom | base64A maintenance window on the NSK-serving path. Between the re-wrap and the env flip, rows are sealed under the new key while the server still holds the old one, so heartbeat and secret-fetch unwraps fail until the flip completes. Drain or scale down the API before step 3.
Steps
1. Dry-run against the live database
The --dry-run pass unwraps every stored row under the old key and re-seals it in memory, but writes nothing and commits nothing. Run it first to prove the old key is correct and see how many rows a real run would touch:
shell
export PLEXSPHERE_DSN='postgres://…'
export PLEXSPHERE_NSK_WRAP_KEY_B64_OLD='<current key>'
export PLEXSPHERE_NSK_WRAP_KEY_B64_NEW='<new key>'
plexsphere-backup nsk-rewrap --dry-run
# level=INFO msg="nsk-rewrap complete" dry_run=true scanned=42 rewrapped=42 already_new=0A scanned count that does not match your Node fleet, or an error naming a node whose row unwraps under neither key, means the old key is wrong — stop and re-check before touching production data. The subcommand exits 0 on success, 2 for a configuration error (missing DSN, a key that is not 32 bytes, or identical old and new keys), and 1 for an operation error (database failure, or a row that unwraps under neither key).
2. Drain the NSK-serving path
Scale the API deployment to zero, or otherwise stop it serving heartbeat and secret-fetch requests, so no unwrap runs against half-rotated data.
3. Re-wrap every stored key
Run the same command without --dry-run. The whole sweep runs inside a single transaction, so a mid-run failure rolls back and every row stays on the old key. It also holds a SHARE ROW EXCLUSIVE lock on plexsphere.node_secret_key for its duration: reads are unaffected, but a Node registering while the sweep runs waits for it. That is deliberate — a registration that slipped between the sweep's snapshot and your key flip would store its key under the old wrap key, and nothing would hold that key afterwards:
shell
plexsphere-backup nsk-rewrap
# level=INFO msg="nsk-rewrap complete" dry_run=false scanned=42 rewrapped=42 already_new=0The command is safe to re-run: a row already sealed under the new key is counted in already_new and left untouched, so a retried run after a transient failure converges rather than double-wrapping. Rows for retired NSKs are re-wrapped too, so historical secret deliveries stay forensically recoverable under the new key.
4. Flip the server wrap key
Set PLEXSPHERE_NSK_WRAP_KEY_B64 to the new key on the API deployment (and the signer, if it shares the wrap key) and restart.
5. Bring the NSK-serving path back
Scale the API back up. Fresh heartbeats and secret fetches now unwrap against the new key.
Flags
Each flag falls back to the named environment variable.
| Flag | Environment variable | Meaning |
|---|---|---|
--dsn | PLEXSPHERE_DSN | PostgreSQL connection string. |
--old-key-b64 | PLEXSPHERE_NSK_WRAP_KEY_B64_OLD | Current base64 32-byte wrap key. |
--new-key-b64 | PLEXSPHERE_NSK_WRAP_KEY_B64_NEW | Replacement base64 32-byte wrap key. |
--dry-run | — | Verify only; write and commit nothing. |
Verification
- The
nsk-rewrapsummary line reportsrewrappedequal to the number of rows that were still on the old key andalready_newequal to the rows a re-run would skip; a second immediate run should reportrewrapped=0. - After the env flip and restart, a Node heartbeat succeeds — the server unwraps the Node's freshly re-wrapped NSK under the new key. A row that still failed to re-wrap would surface as a
401on the Node's next heartbeat.
See also
- Rotate the signing key — the companion key-rotation runbook for the Ed25519 signing keys.
../../contexts/identity/registration.md— the Node registration service and its NSK issuance contract.