Percona PostgreSQL TDE
pg_tde fetches its principal key from a real KMIP server the Cockpit runs — proven end-to-end against a live Percona container, down to the ciphertext on disk.
Overview
Percona Distribution for PostgreSQL ships pg_tde, an extension providing Transparent Data Encryption for tables and, optionally, the write-ahead log (WAL). pg_tde's KMIP key provider fetches a principal (master) key from an external KMIP server and uses it to wrap the internal table and WAL keys.
The DuoKey Cockpit is that KMIP server. pg_tde speaks binary KMIP — OASIS TTLV over mutual TLS — directly to the Cockpit's KMIP listener. The principal key is generated, sealed at rest, and audited centrally; the PostgreSQL host itself never stores the master key on disk.
pg_tde's KMIP key provider talks directly to the Cockpit's KMIP listener; the principal key never leaves DuoKey custody.
Key hierarchy: Principal Key and Internal Keys
pg_tde uses a two-tier key hierarchy, in Percona's own terms:
- The Principal Key is the one key managed externally — by the DuoKey KMIP server, in this integration.
pg_tdeuses one principal key per database. - Every Internal Key is generated locally, one per encrypted database object (a table and each of its indexes each get their own internal key, keyed by object identifier), and is itself wrapped under the principal key. Internal keys — not the principal key — do the actual page-level encryption of table data.
Retrieval of the principal key from the KMIP provider is cached in PostgreSQL backend memory, so pg_tde calls out to the Cockpit only when the key is not already held locally — not on every table read or write. WAL encryption uses the same principal key but is switched on independently of table encryption, via its own server-wide setting; a WAL segment can contain both encrypted and unencrypted records depending on when that setting was toggled.
Unlike the other engines in the Database Encryption catalog, this integration has a genuine, automated end-to-end proof: a real KMIP server, a real pg_tde client performing the actual Register/Get sequence over TTLV/mTLS, and an integration test that starts a live Percona 17.5 container, runs the full setup SQL, and greps the on-disk table file to confirm plaintext is absent from the encrypted table and present in a plaintext control table.
| Property | Value |
|---|---|
| Encryption model | Engine-native TDE (table + optional WAL) |
| Key transport | KMIP (TTLV) over mutual TLS, port 5696 |
| Key custody | Principal key sealed at rest in the DuoKey vault; never exported to the database host |
| Onboarding | App detail page (richer config surface: pooling, failover, replicas) |
| Proof status | End-to-end proven against a live Percona 17.5 / pg_tde 2.0 container |
How pg_tde talks to the Cockpit
pg_tde's KMIP client drives this exact sequence, validated against pg_tde 2.0:
Register
pg_tde_create_key_using_global_key_provider generates the principal key locally and Registers its bytes on the Cockpit's KMIP server. The Cockpit seals the material at rest and returns a KMIP unique identifier.
Get
pg_tde_set_key_using_global_key_provider Gets the key back from the Cockpit to activate it as the database's principal key.
Wrap and encrypt
pg_tde wraps its internal table/WAL keys under that principal key. Table data is then encrypted locally by the tde_heap storage access method — the Cockpit is never on the row-level data path.
pg_tde's KMIP client segfaults if a Get for a previously registered key omits the key material. The Cockpit therefore persists the client-supplied bytes on Register, so the subsequent Get always returns the real key — closing an interop gap that would otherwise crash pg_tde at startup.
What happens when PostgreSQL starts
The five steps below are what actually runs end to end once pg_tde is configured and PostgreSQL (re)starts, or when a backend first needs the principal key:
The principal key crosses the network once per cache miss; every subsequent table or WAL read/write is served from the cached, unwrapped internal keys.
Configuration
The app's configuration surface is deliberately close to a production PostgreSQL client's — connection pooling, failover and replica awareness are first-class fields, not an afterthought.
| Field | Purpose | |||
|---|---|---|---|---|
host | port | PostgreSQL primary connection endpoint. | ||
database | Target database for TDE. | |||
user | ssl_mode | Application connection credentials and TLS mode. | ||
tde_algorithm | Table encryption algorithm (as accepted by pg_tde). | |||
tde_scope | tde_tablespaces | Database-wide default, or an explicit list of tablespaces to convert to <code>tde_heap</code>. | ||
wal_encryption | Whether write-ahead log segments are also encrypted (requires a restart). | |||
key_rotation_days | Target cadence for principal-key rotation. | |||
kmip_auth_mode | kmip_port | Authentication mode and port of the Cockpit KMIP listener (default 5696). | ||
pool_max | pool_min | pool_idle_timeout_secs | pool_max_lifetime_secs | Connection-pool sizing toward the target database. |
health_check_interval_secs | How often the app polls database health. | |||
failover_mode | replica_hosts | target_session_attrs | retry_policy | Read-replica awareness and failover behavior for the connection layer. |
vault_id | key_id | Vault and (optionally) an existing key to link as the principal key. |
Deployment SQL
Deploying the app generates a ready-to-run setup script. Provider name is dke_cockpit; <host> and <port> (default 5696) point at the Cockpit's KMIP listener.
CREATE EXTENSION IF NOT EXISTS pg_tde;
SELECT pg_tde_add_global_key_provider_kmip(
'dke_cockpit', '<host>', <port>,
'/etc/percona/certs/client.pem', -- client cert (mTLS)
'/etc/percona/certs/client-key.pem',-- client key
'/etc/percona/certs/ca.pem'); -- CA validating the cockpit KMIP cert
SELECT pg_tde_create_key_using_global_key_provider('<label>', 'dke_cockpit');
SELECT pg_tde_set_key_using_global_key_provider('<label>', 'dke_cockpit');
-- Encrypt data (database-wide default, or per-table conversion):
ALTER DATABASE "<db>" SET default_table_access_method = 'tde_heap';
-- ALTER TABLE "<t>" SET ACCESS METHOD tde_heap;
-- Optional WAL encryption (requires a restart):
SELECT pg_tde_create_key_using_global_key_provider('<label>-wal', 'dke_cockpit');
SELECT pg_tde_set_server_key_using_global_key_provider('<label>-wal', 'dke_cockpit');
ALTER SYSTEM SET pg_tde.wal_encrypt = on;
-- Verify (returns 't'):
SELECT pg_tde_is_encrypted('<your_table>');The KMIP listener validates client certificates against its configured CA bundle when one is set — mutual TLS is required in production, and the listener refuses to start without it.
What the end-to-end proof actually verifies
The proof is not a mocked round-trip. A dedicated integration test:
- serves the real Cockpit KMIP listener,
- brings up a genuine
percona/percona-distribution-postgresql:17.5-3container reaching the Cockpit host over the network, - runs the exact deployment SQL shown above,
- asserts
pg_tde_is_encrypted()returns true, and - inspects the on-disk
tde_heapfile directly: a plaintext marker string is absent from the encrypted table's file but present in a plaintext control table's file — direct evidence of ciphertext at rest, keyed by a principal key served over KMIP.
The same suite also exercises key rotation (existing rows survive re-wrap under a new principal key) and restart durability (the principal key is correctly reloaded from the KMIP server after a PostgreSQL restart).
Health and self-test
| Check | What it reports | Live probe today |
|---|---|---|
| Health | Instance status envelope | Not yet — enable/disable and health return status without probing the live customer database; a follow-up once an on-host health agent lands |
| Self-test / e2e suite | Register→Get, ciphertext-at-rest, rotation, restart durability | Yes, in the opt-in integration test against a live Percona container |
Performance
The figures below come from a single development host and are meaningful as a relative comparison, not an absolute benchmark. Benchmark against your own environment before capacity planning.
pg_tde encrypts at rest: once a page is read into shared memory it stays decrypted there, so a working set that fits in cache shows negligible difference between plaintext and encrypted access. The measurable cost sits on the disk-write path (WAL and checkpoint pages), which shows up under concurrent write load.
| Metric | Plaintext heap | Encrypted tde_heap | Overhead |
|---|---|---|---|
| Bulk write (single-shot) | ~0.23–0.48 M rows/s | ~0.27–0.49 M rows/s | within noise |
| Sequential read (full-scan aggregate) | ~0.3–0.6 s | ~0.3–0.5 s | within noise |
| Concurrent insert load (16 clients) | 2 444 tps | 2 344 tps | ≈ +4% |
| KMIP principal-key set (Create+Get round-trip) | — | — | ~0.4–1.0 s, off the hot path |