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.

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 typo3 as 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]
Copied!
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_memzero() when the command returns.

Preflight 

The command performs these checks itself, in this order, and each one aborts:

  1. Permission. master_key.rotate must be granted. On CLI without a backend user that means allowCliAccess = 1 and master_key.rotate listed in cliAllowedOperations, which excludes it by default; as a backend user it means the admin override or an explicit group grant.
  2. Keys must differ. Compared with hash_equals() . Identical keys are refused as "Nothing to rotate."
  3. 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.
  4. Confirmation. --confirm for a real run.
  5. 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.

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-verify
    Copied!
  • [ ] 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
Copied!

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.

  1. A master_key_rotate_start audit row is written under the pseudo identifier __master_key__.
  2. 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.
  3. Registered foreign envelopes are re-wrapped the same way.
  4. 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.
  5. The audit_chain_rekey and successful master_key_rotate_end rows are appended — still sealed with the old, provider-derived HMAC key.
  6. 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.
  7. Commit, then MasterKeyRotatedEvent is dispatched.

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:

  1. 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).
  2. 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.
  3. Test retrieval and verify the chain — see below.
  4. 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
Copied!

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.

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_end row with success = 0 and 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-key you 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.

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.