Key rotation
Rotating the master key re-wraps every DEK under a new key. Secret values are never re-encrypted and never materialise as plaintext — only the DEK layer changes (Envelope scheme), which is what makes the operation affordable at all.
It is also the most powerful operation in the extension: it rewrites every
envelope and rekeys the audit chain in one transaction. It carries its own
permission, master_key.rotate.
Warning
The command does not install the new key for you. It re-wraps everything under the key you hand it and then tells you to update the configuration. Between the commit and that configuration change, secrets cannot be decrypted. Plan for that window — see The configuration switch — and the window.
When to rotate
- After any suspected exposure of the key material, or of a host that could read it.
- After personnel with filesystem or KMS access leave.
- On a schedule — annually is a common policy.
- When moving between providers, for example off
typo3as part of Migrating from standard to hardened.
Rotating the master key is not the same as rotating a secret. If a single
credential leaked, rotate that credential and use vault:rotate; a master
key rotation changes nothing about a leaked credential value.
The command
vendor/bin/typo3 vault:rotate-master-key \
--old-key=/path/to/old.key \
--new-key=/path/to/new.key \
[--dry-run] [--confirm]
| Option | Meaning |
|---|---|
--old-key | Path to a file containing the old master key. Omitted: the currently configured provider's key is used. |
--new-key | Path to a file containing the new master key. Omitted: the
currently configured provider's key is used — which, with
--old-key also omitted, means both are identical and the
command refuses. |
--dry-run | Inventory, confirmation and the old-key smoke test run; the re-encryption does not. No changes are made. |
--confirm | Required for an actual run. Without it (and without
--dry-run) the command aborts rather than asking. |
Both keys are read from files, not from the command line, so they do not land
in shell history or a process listing. Both copies are wiped with
sodium_ when the command returns.
Preflight
The command performs these checks itself, in this order, and each one aborts:
- Permission.
master_key.rotatemust be granted. On CLI without a backend user that meansallowCliAccess = 1andmaster_key.rotatelisted in cliAllowedOperations, which excludes it by default; as a backend user it means the admin override or an explicit group grant. - Keys must differ. Compared with
hash_. Identical keys are refused as "Nothing to rotate."equals () - Inventory. Vault secrets are counted, and so are consumer-owned foreign envelopes registered by other extensions (ADR-033: Master-key rotation reaches consumer-owned envelopes) — a vault with no secrets of its own may still be the key authority for thousands of them. If both counts are zero the command stops with a warning.
- Confirmation.
--confirmfor a real run. -
Old-key smoke test. One real secret is re-encrypted to prove the supplied old key is actually the one that wrapped the envelopes. This is the check that turns "wrong key" from a half-rotated vault into a clean abort.
Note
With no vault secrets of its own the smoke test cannot run, and the command says so. A wrong key then surfaces as a failure of the consumer-envelope pass, which rolls the rotation back — a lost early warning, not a lost safety net.
Your own preflight, before running any of it:
- [ ] A verified backup of the database and of the current key material.
- If the rotation fails in a way the transaction cannot undo, this is the only way back. See Backup and restore.
- [ ] **The new key exists, is 32 bytes, and is stored where the provider
- will read it after the switch.**
- [ ] The audit chain verifies now. Rotating over an already-broken
-
chain destroys your ability to tell the two problems apart:
vendor/bin/typo3 vault:audit-verifyCopied!
- [ ] A maintenance window. Vault reads fail between the commit and the
- configuration switch.
- [ ] ``vault:doctor`` is clean, so a pre-existing misconfiguration does
- not surface mid-rotation.
Dry run
Always run this first:
vendor/bin/typo3 vault:rotate-master-key \
--old-key=/path/to/old.key --new-key=/path/to/new.key --dry-run
The dry run is genuinely useful rather than cosmetic, because the old-key smoke test happens before the short-circuit: a dry run that succeeds has proved that the old key opens a real envelope. It reports the number of secrets and foreign envelopes that would be re-wrapped and makes no changes.
A dry run that fails the smoke test means the old key is wrong. Fix that
before going further; do not proceed with --confirm hoping the real run
behaves differently.
What the real run does
Everything below happens inside one database transaction, and any failure rolls back secrets, audit events and the chain rewrite together.
- A
master_key_rotate_startaudit row is written under the pseudo identifier__master_key__. - Every secret's DEK is unwrapped with the old key and re-wrapped with the new one, with a fresh nonce. Values, value nonces and the version/algorithm markers are untouched.
- Registered foreign envelopes are re-wrapped the same way.
- The audit advisory lock is taken for the remainder of the transaction, so no concurrent writer can chain onto a tip hash that is about to be rewritten.
- The
audit_chain_rekeyand successfulmaster_key_rotate_endrows are appended — still sealed with the old, provider-derived HMAC key. - The whole audit chain is rewritten under the HMAC key derived from the new master key, including the two rows just appended, so the committed chain verifies under the new key from first row to last with no old-keyed tail.
- Commit, then
Masteris dispatched.Key Rotated Event
Two properties of the rekey worth knowing:
- Per-row epochs are preserved. Re-keying changes the key, never the payload format. An epoch-2 row stays epoch 2.
- An all-epoch-0 chain passes through untouched. Keyless SHA-256 hashes do not depend on the master key, so rows are only updated when their hashes actually change.
The configuration switch — and the window
On success the command prints its next steps, and they are not advisory:
- Update the configuration to use the new master key immediately. Until
then, secrets cannot be decrypted and audit-chain verification runs
against the old key. Depending on the provider: replace the key file
(
file), re-inject the environment variable and restart (env), or re-wrap the new key with the KMS (transit). - Securely archive or destroy the old key. Archive it if you may still need to open an older backup; destroy it if you may not. Do not leave it where the vault could pick it up again.
- Test retrieval and verify the chain — see below.
- Re-seal any rows written in the gap. Audit rows written between the
rotation commit and the configuration switch are sealed with the old
HMAC key while the rest of the chain is under the new one. Re-seal them
with
vault:audit-migrate-hmac.
That fourth step is the reason to keep the window short and to make it a maintenance window: every audited operation during the gap adds a row that will need re-sealing.
Verification
# 1. The provider resolves and the configuration is coherent.
vendor/bin/typo3 vault:doctor --format=json
# 2. Plaintext comes back — the real proof. On the CLI this needs
# allowCliAccess = 1 AND secret.reveal in cliAllowedOperations;
# a reveal in the backend module proves the same decrypt path.
vendor/bin/typo3 vault:retrieve <a-known-identifier>
# 3. The chain verifies under the new key, end to end.
vendor/bin/typo3 vault:audit-verify
# 4. Anchor the new tip; the old anchor's baseline is now stale.
vendor/bin/typo3 vault:audit-anchor
# 5. The in-database anchor is re-sealed by the rotation itself. Confirm
# it reads "Tip anchor: ok" — a re-key that left it UNREADABLE means
# the re-seal did not complete, and that is a finding, not a nuisance.
vendor/bin/typo3 vault:audit --verify
Probe at least two secrets, and pick them from different encryption versions if the installation has both — see Restore verification: the probe decrypt for why.
Note
Re-anchoring after a rotation is not optional. The rewritten chain has new
hashes throughout, so an anchor taken before the rotation will now report
TABLE_RESET against it. Publish a fresh anchor, and keep the old one
with your rotation record so the finding is explainable rather than
alarming.
If it fails
- Aborted during preflight — nothing changed. Fix and retry.
- Rolled back — the transaction reverted secrets, the appended events
and the chain rewrite together. A
master_key_rotate_endrow withsuccess = 0and the reason "Unexpected error during rotation; transaction rolled back" records the attempt. The old key is still the right one; do not switch the configuration. - Committed but the configuration was never switched — secrets stay undecryptable and verification uses the wrong key. Complete the switch; this is a half-finished rotation, not a failed one.
- Committed, configuration switched, and reads still fail — the key you
installed is not the
--new-keyyou rotated to. Install the correct one. Do not rotate again to "fix" it: a second rotation from the wrong old key will fail its smoke test, which is the safety net working.
Warning
Never run vault:init to recover from a failed rotation. It generates a
fresh master key, and a fresh key over a rotated vault makes every secret
unreadable — the command's own --force warning says as much.
Transit key rotation is a different operation
With the transit provider there are two independent rotations, and
confusing them is a common mistake.
| Operation | Effect |
|---|---|
vault:rotate-master-key | Changes the nr-vault master key: every DEK is re-wrapped and the audit chain is rekeyed. Installing the new key afterwards means wrapping it with Vault and replacing the local blob — the command does not do that for you. This is what the rest of this page describes. |
vault write -f transit/keys/<name>/rotate | Rotates the Vault transit key. New ciphertexts are wrapped under the new transit key version; existing ciphertexts stay decryptable under their recorded version. The nr-vault master key does not change, so no DEK is re-wrapped, the audit chain is untouched, and no secret is affected. |
So: rotating the transit key is a KMS hygiene operation with no vault downtime and no re-encryption. Rotating the master key is the operation that touches every envelope. Do the first freely; schedule the second.
To re-wrap the existing master key under the newest transit key version
without changing the master key itself, use Vault's rewrap endpoint on the
stored ciphertext and replace the local blob — again with no effect on any
secret. Keep the file mode at 0600 and write it atomically, as the
provider itself does.