Revision history for pg_vault_tde This file tracks default_version bumps in pg_vault_tde.control / VERSION. Dates are taken from git history (`git log --follow -- VERSION pg_vault_tde.control`) or, where available, from the annotated release tag; "in-tree" means the version string changed in this branch's history but the commit was not (yet) tagged as a release at the time of writing. See doc/ROADMAP.md for the full feature-by-feature history, including v1.0-v1.3. 1.7.2 (in-tree since 2026-09-19; C-only bugfixes, no SQL/catalog change — pg_vault_tde_build_version() distinguishes these builds from 1.7.1) - PSQLE-156: pg_vault_tde.preload_keys — warm a database's DEK cache at startup instead of on first access, so the first query on an encrypted table does not pay a KMS round-trip. A launcher registered in _PG_init reads the shared pg_database catalog and runs one short-lived worker per database, sequentially: everything the warm-up needs is per-database and unreachable from elsewhere (pg_vault_tde_catalog is a per-database table, the wallet holding the KEK is a per-database file) and a background worker may connect exactly once in its life. Off by default, PGC_SUSET so it can be scoped with ALTER DATABASE SET; the worker reads it after connecting, since a per-database value does not resolve before that. It never blocks startup — the postmaster accepts connections while it runs, so a slow or unreachable KMS costs a cold cache, not availability — and it stops at max_encrypted_relations rather than spending KMS round-trips the cache would refuse. Requires a KMS that opens without an interactive unlock: wallet_passphrase_command or wallet_passphrase_env. - PSQLE-156: pg_vault_tde.preload_max_failures (default 5) — consecutive unwrap failures the preload tolerates in one database before giving up on it. Consecutive rather than total, so a systemic fault stops the pass immediately while a one-off does not and the next success clears the count; 0 restores stop-at-the-first. It matters for the local wallet as much as for a remote KMS: the KEK is deliberately re-derived from the wallet file on every unwrap instead of being cached, to keep it out of process memory between operations, so a preload reopens the wallet once per relation and a wallet on NFS or SMB fails the same transient way a network KMS does. Without a limit a closed wallet also meant one provider WARNING per relation in the log while the server was still starting. - UPGRADE NOTE — pg_vault_tde.max_encrypted_relations is now enforced. It never was: the size passed to ShmemInitHash() sizes the initial allocation and the bucket directory, and once the freelist empties dynahash keeps allocating elements from the main shared memory segment, so the cache grew to many times the configured number and only failed when shared memory as a whole ran out. Measured: 2000 relations cached against a setting of 64. This matters because the cache holds plaintext DEKs — the number an administrator sets is how much key material sits unencrypted in shared memory, and it is a cluster-wide budget shared by every database (entries are keyed by (dbid, relid)). BEFORE UPGRADING, count your encrypted relations — tables plus tde_btree indexes, summed across all databases — and raise max_encrypted_relations past that number if it exceeds the setting; it needs a restart. A cluster left below its real count keeps working, but relations past the budget are re-unwrapped from the KMS on every access, which is a significant slowdown with a network KMS. Each backend says so once, at WARNING. The maximum accepted value is raised from 65536 to 1048576 for clusters that genuinely need it; the cache costs about 112 bytes per relation, reserved at startup. - Relations past the budget lose their cache entry, never their data. tde_rel_dek_cache_store() used to ereport(ERROR) when it could not insert, even though pg_vault_tde_kms_get_rel_dek() had already unwrapped the DEK and filled the caller's buffer before calling it — so a serviceable request failed because its memoisation did. It now returns false and the operation proceeds. - Fix a segfault when a value crosses TOAST_TUPLE_THRESHOLD only after encryption: the pre-TOAST gate now tests the encrypted length and the TOAST writer reserves TDE_V4_OVERHEAD in its target, so core is never handed ciphertext it would try to TOAST (INSERT, COPY, UPDATE, UPSERT and the VACUUM FULL / CLUSTER rewrite). - Fix an assertion failure at startup on assert-enabled servers: _PG_init read GetUserId() to test its own validity. - Fix unregistered snapshots passed to systable_beginscan (10 sites). - Fix try_relation_open(NoLock) without holding a lock (3 sites) and a hint-bit write without the buffer content lock in the rewrite path. - Fix uninitialised VacuumCutoffs driving freeze decisions during VACUUM FULL / CLUSTER; the redundant freeze call was removed. - Add missing volatile on locals read by PG_CATCH after longjmp. - PSQLE-158: key the shared-memory DEK cache on (dbid, relid) instead of relid alone. A relid is unique only within a database, but TdeRelDekMap is one segment read by the backends of every database, so two databases holding the same relid — the normal case after CREATE DATABASE ... TEMPLATE, which copies pg_class OIDs verbatim — shared a single entry: the first one to populate it handed its DEK to the other. The GCM AAD binds MyDatabaseId, so this surfaced as "AES-256-GCM authentication FAILED" on intact data rather than as wrong plaintext, and a DROP TABLE in one database evicted the other's live entry. No SQL or on-disk change: pg_vault_tde_catalog is per database already. Note that max_encrypted_relations now budgets entries across all databases. - PSQLE-158: fix pg_vault_tde_rotate_online() against a relation whose shmem DEK entry is cold — the normal state after a restart, or after wallet_lock()/wallet_unlock(), which evict. zero_rel_dek() populated prev_dek[] straight from the cache entry and did nothing at all when that entry was absent, so the rotation re-keyed the catalog and then could not decrypt a single pre-rotation row ("decryption failed"). Phase 2 runs in one transaction, so the catalog re-key rolled back and no data was lost, but the rotation could never succeed on a cold relation. It now recovers the outgoing DEK via pg_vault_tde_kms_get_rel_dek() before demoting it, and errors out rather than proceeding when that DEK cannot be recovered. - PSQLE-158: scope the administrative DEK flush to the current database. All six callers act on per-database state — the wallet lives at /var/lib/pg_vault_tde//wallet.p12 and pg_vault_tde_catalog is a per-database table — so wallet_lock/_unlock/_change_passphrase, migrate_vault_to_wallet, unseal_keys and rotate_kek can only ever invalidate this database's keys. pg_vault_tde_catalog_evict_all() is replaced by pg_vault_tde_catalog_evict_db(); evicting every database's keys forced unrelated backends into a needless KMS round-trip, or an outright failure if their own wallet was locked. No cluster-wide variant is kept: nothing calls one, and an uncalled one would be untested code pretending to be a feature. - PSQLE-158: stop the shmem DEK cache drifting a generation ahead of the catalog. zero_rel_dek() bumped the generation in shared memory, which no transaction rolls back, while update_rel_dek() bumped the catalog copy inside the rotation's transaction, which does. An aborted rotation left the two disagreeing for the lifetime of the cluster; reads still resolved via prev_dek, so it stayed quiet, but rows written afterwards were tagged N+1 while encrypted under DEK N, and the next rotation moved shared memory to N+2 and could match neither branch for the oldest rows — they stopped decrypting and the relation could no longer be rotated. The cached generation is now read only next to a live DEK (otherwise the catalog is consulted), is written only alongside the DEK it describes, and zero_rel_dek() no longer bumps it, so an aborted rotation simply re-reads the catalog. - ci: stop packaging host-built objects. run-install-test.sh tar'd the working tree into the distro container without excluding *.o/*.bc/*.so, so a build left behind by an earlier stage was picked up as up to date and linked into the RPM — producing packages that required GLIBC_2.38/CURL_OPENSSL_4 and would not install on EL9. - Warn before a KEK rotation that can only cover one of the databases sharing a local wallet. pg_vault_tde_catalog_rewrap_all() walks a per-database table, so it covers exactly the database it runs in, while the local provider replaces the wallet file outright — so with a wallet shared between databases the first one to run pg_vault_tde_rotate_kek() or pg_vault_tde_wallet_change_passphrase() strands every other database's DEKs under a KEK that no longer exists. A WARNING rather than an ERROR, because a single-database cluster may legitimately point wallet_path elsewhere. It fires on the effective path differing from this database's default, not on wallet_path merely being set: wallet_init() persists the resolved path per database, so the GUC is always populated after an ordinary setup. Vault and PKCS#11 are unaffected — both keep previous KEK versions. 1.7.1 (in-tree since 2026-09-05; C-only bugfixes, no SQL/catalog change — pg_vault_tde_build_version() distinguishes these builds from 1.7.0) - PSQLE-148: pg_dump_tde creates its output file with mode 0600 instead of leaving it to the ambient umask. The dump is ciphertext, but its header carries the sealed DEK, so a world-readable file handed anyone on the machine an offline target. Dumps written by earlier versions keep the permissions they were created with: check them and chmod 0600. - PSQLE-148: the passphrase-file readers — the local KMS provider and pg_dump_tde — now validate the descriptor they opened, via open(O_RDONLY|O_NOFOLLOW) + fstat() + fdopen(), instead of stat()ing the path and opening it in a second step. The owner-only permission check could previously be side-stepped by repointing the path in between, and a symlink is now refused outright. - PSQLE-135: fix AAD relid resolution and HEAP_HASEXTERNAL on decrypt. - PSQLE-126: KMS GUCs are now genuinely independent settings: the provider is initialised on first use (tde_kms_provider()) instead of from the pg_vault_tde.kms_provider assign hook, so the order and the scope of the ALTER DATABASE SET / ALTER ROLE ... IN DATABASE SET statements no longer matter. Fixes the spurious "local wallet passphrase env var ... not set" WARNING on every connection, and the silent use of the per-database default wallet_path when wallet_path was set after kms_provider. - The local wallet path is no longer snapshotted per backend; changing a KMS GUC mid-session re-initialises the provider and drops any cached KEK. - pg_vault_tde_wallet_status() resolves the provider before reporting, so wallet_open still reflects the wallet and not "nothing has opened it in this session yet" now that init() is lazy. - SHOW pg_vault_tde.wallet_path now reports the computed per-database default instead of an empty string (show_hook was never registered). - The postmaster no longer attempts to open a local wallet at startup (it has no database context), removing the "cannot open wallet \"\"" WARNING from every cluster-level 'local' setup. - New TAP file: tap/18_guc_order_independence.t. - PSQLE-145: postgresqlNN-devel does not pull in clang/llvm, so the rpm spec declares them as BuildRequires. Without them a rebuild from the SRPM in a clean buildroot — which is how mock, and therefore yum.postgresql.org, builds — failed on the first LLVM bitcode target. - PSQLE-145: the source build instructions in README.md were incomplete. RHEL/Rocky additionally needs EPEL and CRB (for perl(IPC::Run)), redhat-rpm-config and clang/llvm-devel; Debian/Ubuntu needs build-essential; and both need the PGDG repository for any major the distribution does not ship itself. Following them on a fresh host now works. - PSQLE-144: new `make check-standalone`, which creates a throwaway cluster, runs the regression test and tears it down — no container, no KMS service, no cluster to configure. PGXS declines `make check` for out-of-tree extensions, and plain `make installcheck` needs a server that already preloads the library, so neither was usable when building from source. - PSQLE-143: the PGXN release bundle is named after VERSION (1.7.1) rather than the control file's default_version (1.7), and its content is filtered through .gitattributes: internal CI, the wiki sources and the container-only test suites no longer ship inside it. - PSQLE-146: the PostgreSQL support tables that had drifted into three copies are reduced to one, and doc/pg_vault_tde.md gains a generated Support Matrix — which (OS, PostgreSQL) combinations are packaged, and which of them are actually exercised in CI rather than merely compiled — plus a KMS Provider Coverage table. - PSQLE-147: CodeQL static analysis runs over the C sources for both supported majors and over the workflows themselves; the two PSQLE-148 findings above came out of its first scan. 1.7.0 (in-tree since 2026-06-09; current default_version, unreleased) - TOAST chunk-level storage encryption, KEK hierarchy / key rotation for all KMS providers, PKCS#11 / HSM support, audit logging, fixed-size type index key encryption for tde_btree (int4, int8, uuid, date, timestamptz), CREATE INDEX CONCURRENTLY / REINDEX CONCURRENTLY support on encrypted_heap tables. - See doc/ROADMAP.md "v1.7" section for the complete list. 1.6.0 (tagged 2026-05-26) - Local PKCS#12 wallet KMS provider (production-ready offline encryption): passphrase via command/env/file, dev-mode passphrase convenience, wallet lock/unlock, Vault-to-wallet migration. 1.5.0 (in-tree since 2026-03-04) - Per-table DEK isolation, online key rotation (pg_vault_tde_rotate_online), wire format v3 with per-tuple authenticated associated data. 1.4.0 (in-tree since 2026-02-28) - Containerized CI/CD pipeline, tde_btree encrypted index access method (AES-256-SIV), wire format v2. 1.0.0 - 1.3.0 - Initial development: AES-256-GCM encrypted_heap Table Access Method, HashiCorp Vault / OpenBao KMS integration, logical decoding compatibility, multi_insert / background worker / health_check. - Not individually dated in this file; see doc/ROADMAP.md.