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/<db_oid>/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.
