Contents
Key Rotation
pg_vault_tde supports two independent kinds of rotation. Knowing which one you need is the first decision in any rotation runbook:
| DEK rotation | KEK rotation | |
|---|---|---|
| What changes | The per-table Data Encryption Key | The master Key Encryption Key that wraps every DEK |
| What gets re-encrypted | Every row of the target table (and its tde_btree indexes, if rebuilt) |
Nothing — only the wrapped DEK blobs in pg_vault_tde_catalog are re-wrapped |
| Function | pg_vault_tde_rotate_online(regclass, batch_size) |
pg_vault_tde_rotate_kek() |
| Scope | One table (or index) at a time | Cluster-wide (the KEK is per-database, per-provider) |
| Locking | RowExclusiveLock while re-encrypting; never AccessExclusiveLock on the table |
Transactional catalog updates only |
| Typical trigger | Suspected compromise of a specific table’s key, routine per-table key hygiene | Suspected KEK compromise, compliance-mandated rotation interval, wallet passphrase change |
Both are online operations — no downtime and no exclusive lock that blocks reads for the duration of the rotation.
DEK Rotation
SELECT pg_vault_tde_rotate_online('mytable', 1000); -- 1000 = batch size
-- Monitor progress for one relation:
SELECT * FROM pg_vault_tde_get_rotation_status('mytable');
-- Or cluster-wide, across every in-progress/completed rotation:
SELECT * FROM pg_vault_tde_rotation_status; -- view, readable by pg_monitor
rotate_online accepts either a table or a tde_btree index relation:
| Target | What happens |
|---|---|
encrypted_heap table |
Generates a new table DEK, re-encrypts every tuple in place (RowExclusiveLock), then rebuilds any tde_btree indexes on the table so their AES-SIV ciphertexts match the new DEK. Plain btree indexes on encrypted columns need no rebuild. |
tde_btree index |
Generates a new index DEK, then rebuilds the index (AccessExclusiveLock on the index only, not the table) with keys encrypted under the new DEK. The parent table’s DEK and heap data are untouched. Passing a non-tde_btree index raises an error before touching shared memory or the catalog. |
Mechanically, rotation works by generation epoch, not a blocking in-place key swap:
- The current DEK is promoted to
prev_dekand the shared-memory copy of the live DEK is wiped; the per-relation generation counter is incremented. - Each backend detects the generation mismatch lazily, on its next encrypt/decrypt call for that relation — no signal or broadcast is needed.
- Rows still encrypted under the previous generation remain readable via
prev_dekthroughout the rotation window. - Once
rotate_onlinefinishes re-encrypting every row, the window closes.
If a table has tde_btree indexes, rotating the table implicitly produces
correct index entries under the new table DEK (the index rebuild reads the
newly re-encrypted heap rows). To also rotate an index’s own DEK, call
rotate_online on the index relation directly, as a separate step.
KEK Rotation
-- Works identically for the local wallet, Vault Transit, and PKCS#11 providers:
SELECT pg_vault_tde_rotate_kek();
This re-wraps every stored DEK under a brand-new KEK. No tuple data is touched — this is purely a catalog-level operation and is typically much faster than a DEK rotation of any single large table.
Local wallet users:
pg_vault_tde_wallet_change_passphrase(old, new)already rotates the KEK as part of changing the passphrase — do not callrotate_kek()separately afterward. See KMS: Local Wallet.PKCS#11 users: old KEK generations are never deleted from the token, so a crash mid-rotation is always safe to retry — see KMS: PKCS#11 / HSM for the generation model.
Recommended Rotation Cadence
pg_vault_tde does not enforce a rotation schedule — this is a policy decision driven by your compliance requirements (e.g. PCI DSS) and threat model. As a starting point:
- KEK rotation: on suspected compromise, on offboarding anyone with access to KEK material, or on a fixed compliance-mandated interval (e.g. annually). Cheap enough to run more often if policy requires it.
- DEK rotation: for tables holding the most sensitive data, on a schedule aligned with your data classification policy; otherwise, on suspected compromise of a specific table’s key or generation.
Interaction With Backups
If a KEK or DEK is rotated after a physical backup was sealed (see
Backup and Restore), the primary and the standby/backup
can drift out of sync. Re-run pg_vault_tde_seal_keys() (or
pg_basebackup_tde) after any rotation, and re-run
pg_vault_tde_unseal_keys() on the standby to realign it.
Do not run pg_vault_tde_unseal_keys() concurrently with
pg_vault_tde_rotate_online() on the same table — PostgreSQL’s own MVCC
checks make the conflict fail safely (a tuple concurrently updated or
duplicate-key error, nothing partially imported); simply re-run
unseal_keys() once the rotation has finished.