# Backup and Restore
pg_vault_tde ships three tools to keep backups confidential and keep primary/
standby key state in sync: `pg_dump_tde` / `pg_restore_tde` for logical
backups, and `pg_basebackup_tde` (a wrapper around `pg_basebackup`) for
physical backups. This page also covers doing it manually with plain
`pg_basebackup` plus the key-sealing functions directly.
## Logical Backups: `pg_dump_tde` / `pg_restore_tde`
Plain `pg_dump` reads rows through the normal decrypt-on-read path, so **the
resulting dump file is plaintext** — encryption doesn't survive a logical
dump unless you use these wrappers.
```bash
# Encrypted dump
pg_dump_tde -h localhost -U postgres -d mydb -o /backup/mydb.tde
# Restore encrypted dump
pg_restore_tde -h localhost -U postgres -d mydb -i /backup/mydb.tde
```
All other `pg_dump`/`pg_restore` options are forwarded through. Internally:
1. `pg_dump_tde` forks `pg_dump -Fc`, capturing its stdout through a pipe.
2. It reads `pg_vault_tde.kms_provider` from the target database's GUCs,
generates a fresh DEK, wraps it via the active KMS provider, and writes a
backup header (magic + format version + wrapped DEK) to the output file.
3. The `pg_dump` stream is encrypted in independently-authenticated 64 KB
blocks (the block sequence number is bound as GCM AAD, so reordering
blocks is detectable).
4. If `pg_dump` fails mid-stream, the partial output file is deleted
automatically — you will never be left with a truncated, silently
"successful" backup file.
### Current Limitations
1. Only `-Fc` (custom) dump format is supported.
2. `-j` (parallel jobs) is **not** supported — `pg_dump`'s parallel mode only
works with the directory format (`-Fd`), which these tools don't produce.
3. Fixed block size of 64 KB (not configurable).
4. **Restore is locked to the original KEK** used to wrap the dump's DEK. If
you need to restore into a database using a *different* wallet (`local`
provider) than the one that created the dump, restore will fail — you
need either the original wallet, or a new wallet that has re-imported the
original KEK.
- **`DROP DATABASE` deletes that database's `.p12` wallet file.** Any
`pg_dump_tde` backup created before the drop becomes permanently
undecryptable if no other copy of the wallet survives. Treat the wallet
file as part of what you must retain, not just the dump.
5. File-only input/output: `-o`/`--output` (dump) and `-i`/`--input`
(restore) are mandatory. Neither tool supports piping or stdin/stdout.
6. `kms_provider = 'pkcs11'` is **not** supported by these tools yet — they
exit with a clear error. Use `pg_basebackup`/`pg_basebackup_tde` for
PKCS#11-backed databases instead.
7. Running plain `pg_dump` against a pg_vault_tde database still produces a
plaintext backup — this is expected (see above), not a bug in `pg_dump`.
The same is true of a manual `COPY
TO ...`: encryption lives in the
table access method's read callbacks, which can't distinguish a `COPY TO`
from a `SELECT`. No `WARNING` is emitted in either case today — a
GUC-gated warning was designed but never implemented (see
[Known Limitations and Troubleshooting](Known-Limitations-and-Troubleshooting), item 12).
## Physical Backups: `pg_basebackup`
`pg_basebackup` works against a pg_vault_tde cluster with **no special
configuration on the primary**: encrypted relation files are copied as-is,
and the wrapped DEKs travel with the data directory inside the
`pg_vault_tde_catalog` table. Critically, **the KEK never travels with the
backup** — it stays in the KMS/wallet/HSM, the same model used by Oracle
RMAN, SQL Server TDE, and Percona `pg_tde`.
### Provisioning the KEK on a Standby / Restore Target
The wrapped DEKs arrive for free with the base backup; the KEK must be made
available on the target separately, depending on provider:
| Provider | What to do on the standby/target |
|---|---|
| `local` | Copy the primary's `wallet.p12` to the standby (it lives outside `PGDATA`, so a plain base backup does **not** include it) |
| `vault` | Point the standby at the **same** Vault/OpenBao instance — nothing to copy |
| `pkcs11` | Ensure the standby has access to the same (or a replicated) HSM token under the same label |
### Sealing the DEK Catalog (Manual)
`pg_vault_tde_seal_keys()` writes a signed, point-in-time snapshot of every
wrapped DEK to accompany a backup; `pg_vault_tde_unseal_keys()` verifies and
re-imports it on the target. This makes the key state **tamper-evident** and
guards against key-rotation drift between primary and standby.
```sql
-- On the primary, before pg_basebackup:
SELECT pg_vault_tde_seal_keys('/backup/keys.sealed', 'a-seal-passphrase');
```
```bash
pg_basebackup -h primary -D /backup/data -X stream
# local provider only: also transport the wallet, e.g.
# scp /path/to/wallet.p12 standby:/path/to/wallet.p12
```
```sql
-- On the standby, after restoring the data directory and provisioning the KEK:
SELECT pg_vault_tde_unseal_keys('/backup/keys.sealed', 'a-seal-passphrase');
```
The HMAC key protecting the sealed bundle is derived from the seal
passphrase (PBKDF2-SHA256) and is independent of the KMS provider, so the
same bundle format works across local and Vault deployments.
`unseal_keys()` verifies the HMAC **before** touching the catalog — a
tampered bundle or wrong passphrase is rejected and nothing is written.
> **Rotation drift.** If the KEK or a DEK is rotated after a backup was
> sealed, primary and standby can drift apart. Re-run `seal_keys()` (or
> `pg_basebackup_tde`) after any rotation, and `unseal_keys()` on the
> standby, to realign. See [Key Rotation](Key-Rotation).
> **Concurrency.** Don't run `unseal_keys()` while
> `pg_vault_tde_rotate_online()` is rotating the same table — PostgreSQL's
> MVCC checks make this fail safely (a `tuple concurrently updated` or
> duplicate-key error, nothing partially imported); just re-run
> `unseal_keys()` once the rotation finishes.
### Automatic Key Sealing: `pg_basebackup_tde`
`pg_basebackup_tde` wraps `pg_basebackup` and performs the sealing step
**automatically**, for every database in the cluster that has the extension
installed (the DEK catalog is per-database; `pg_basebackup` itself is
cluster-wide):
```bash
# Passphrase from a 0600 file (the ~/.pgpass pattern):
pg_basebackup_tde -h primary -D /backup/data -X stream \
--seal-passphrase-file /etc/pg_vault_tde/seal.pass
# ...or from the environment:
export PG_VAULT_TDE_SEAL_PASSPHRASE='a-seal-passphrase'
pg_basebackup_tde -h primary -D /backup/data -X stream
```
The passphrase is **never** accepted as a command-line argument — it would
leak into `ps` output and shell history. `--seal-passphrase-file` (first
line of the file) takes precedence over the environment variable.
All other options are forwarded verbatim to `pg_basebackup`. For every
database with `pg_vault_tde` installed, the wrapper calls
`pg_vault_tde_seal_keys_bytea()` **before** the backup starts (a
point-in-time key snapshot) and, **only if the backup succeeds**, writes one
bundle per database next to the backup:
```
/backup/data/pg_vault_tde_keys..sealed (mode 0600)
```
Use `--keys-dir DIR` to store the bundles elsewhere (e.g. outside `PGDATA`).
Databases without the extension are skipped; a failed backup leaves no
bundle files behind, so you never end up with a sealed-keys file that
doesn't correspond to a valid backup.
Restore stays manual: restore the data directory, provision the KEK on the
target, then for each database:
```sql
SELECT pg_vault_tde_unseal_keys('/backup/data/pg_vault_tde_keys..sealed', '...');
```
The tar format (`-Ft`) is **not** supported by `pg_basebackup_tde` — use the
plain format, or fall back to running `pg_vault_tde_seal_keys()` manually
alongside a tar-format `pg_basebackup`.
## Choosing an Approach
| Scenario | Recommended tool |
|---|---|
| Single-database logical backup, portable across major versions | `pg_dump_tde` / `pg_restore_tde` |
| Standby provisioning / point-in-time recovery base | `pg_basebackup_tde` (or `pg_basebackup` + manual `seal_keys`/`unseal_keys`) |
| `kms_provider = 'pkcs11'` | `pg_basebackup_tde` only — the logical tools don't support PKCS#11 yet |
| Cluster with multiple databases, some without the extension | `pg_basebackup_tde` (skips databases without the extension automatically) |
## See Also
- [Key Management Overview](Key-Management-Overview)
- [Key Rotation](Key-Rotation)
- [SQL Function Reference](SQL-Function-Reference)
- [Known Limitations and Troubleshooting](Known-Limitations-and-Troubleshooting)