Why Your DuckDB Database Needs Encryption at Rest
In local, embedded, or CI-based workflows, DuckDB database files sit on disk in plaintext — on a laptop, inside a container, or in an S3 bucket. A single misconfigured access key, a stolen backup file, or a leaked physical medium makes all of that data readable by anyone who gets their hands on the file.
Starting from v1.4.0, DuckDB ships native transparent data encryption (TDE) using AES-256-GCM or AES-256-CTR, covering:
- Every 256KB data block in the main database file
- The write-ahead log (WAL)
- Spill and sort temporary files
Encryption is fully transparent to the SQL layer: table creation, querying, and backup behave exactly as they would with a plaintext database. The only difference is that opening the database now requires a key.
The Encryption Architecture in Three Layers

1. Plaintext Main Header, Fully Encrypted Data Blocks
At the start of every DuckDB database file sits a main header containing:
- The
DUCKDBmagic bytes - An encryption flag (4-byte flags field; the first bit indicates encryption is enabled)
- 16 random bytes acting as a salt for key derivation
- 8 bytes of metadata recording the cipher mode (GCM/CTR), KDF type, and key length
- An encrypted canary value used to verify that the supplied key is correct
The main header contains no sensitive data, so it stays plaintext. Everything after it — each 256KB data block — is encrypted independently. A single corrupted block does not compromise the rest of the database, and the nonce is built from a 12-byte random value plus a 4-byte counter, so identical content in different blocks produces different ciphertexts.
2. Block Headers Grow from 8 to 40 Bytes
In a plaintext database, the block header is an 8-byte checksum. In an encrypted database it expands to:
- 16 bytes of nonce/IV
- 16 bytes of authentication tag (GCM mode; CTR has no tag)
- 8 bytes of encrypted checksum
That is roughly 32 bytes of per-block metadata overhead, which is negligible for 256KB blocks (about 0.0125%).
3. Memory-Safe Key Management
DuckDB follows a “minimize exposure” principle for keys:
- KDF derivation: The user-supplied key is transformed into a 32-byte secure key
- Memory pinning: The derived key stays in memory and is never swapped to disk
- Immediate wiping: The original key is erased from memory as soon as derivation completes
- Per-session keys: Temporary files use independently generated keys that become useless if the database crashes
Quick Start: Enable Encryption in Three Steps
Step 1: Create an Encrypted Database
import duckdb
con = duckdb.connect(
"secure.db",
encryption_key="my-strong-password", # use a key file or KMS in production
encryption="AES-GCM-256", # optional: AES-GCM-256 (default) or AES-CTR-256
)
con.execute("CREATE TABLE t (id INT, secret TEXT)")
con.execute("INSERT INTO t VALUES (1, 'top secret')")
con.close()
Step 2: Verify Encryption Works
Inspect the file with xxd — the data region should be unreadable ciphertext:
xxd secure.db | grep -i "top secret" # no matches: plaintext is encrypted
xxd secure.db | head -5 # main header is still plaintext
Step 3: Wrong Keys Are Rejected
The canary value in the main header is encrypted with the correct key, so an incorrect key fails validation at open time:
import duckdb
try:
con = duckdb.connect("secure.db", encryption_key="wrong-key")
con.execute("SELECT * FROM t")
except duckdb.Error as e:
print("Key validation failed:", e)
Encrypted WAL: Protecting Data During Crash Recovery
The WAL carries all transaction logging before a crash recovery completes, so it must also be encrypted. DuckDB uses a separate session key for the encrypted WAL:
import duckdb
con = duckdb.connect("secure.db", encryption_key="my-strong-password")
con.execute("CREATE TABLE logs (ts TIMESTAMP, msg TEXT)")
con.execute("INSERT INTO logs VALUES (now(), 'wal entry')")
import subprocess
result = subprocess.run(["strings", "secure.db.wal"], capture_output=True, text=True)
print("Readable strings in WAL:", result.stdout.strip() or "none (encrypted)")
con.close()
GCM vs CTR: Which Cipher Should You Use?
Both are AES-256 variants, but they differ in integrity protection:
- AES-GCM-256 (recommended): Provides authenticated encryption (AEAD) with a 16-byte authentication tag per block. Detects tampering.
- AES-CTR-256: Encryption only, no integrity checks. Slightly higher throughput but cannot detect bit flips or malicious modification.
If your database may live in an untrusted environment (shared storage, multi-cloud backup), choose GCM. If throughput is the top priority and the environment is tightly controlled, CTR is acceptable.
Performance Benchmark: The Cost of Encryption
Test conditions: single process, DuckDB 1.4.x, 32GB RAM, NVMe SSD, a 10M-row integer table (~300MB), 16 threads running the same aggregate query.
- Plaintext scan +
SUM: approximately 82ms - AES-GCM-256 encrypted scan +
SUM: approximately 91ms - Inserting 10M rows: plaintext 410ms vs encrypted 448ms
Total overhead is around 8–10%, driven primarily by AES encrypt/decrypt and 128-byte-aligned nonce generation. For most OLAP workloads this cost is negligible; the security benefit far outweighs the performance trade-off.
Key Management Best Practices
- Never hard-code keys. Use a key file or a KMS (HashiCorp Vault, AWS KMS, etc.) and inject the key at startup
- Use keys of at least 32 bytes. KDF can compensate for weak passwords, but brute-force cost grows exponentially with length
- Rotate keys periodically. Create a new encrypted database,
ATTACHthe old one, and migrate data withCOPY - Store backups and keys separately. An encrypted backup is useless without the key; key loss means permanent data loss
- Handle keys in CI/CD carefully. Use secret managers (GitHub Actions Secrets, Vault Agent) and never commit keys to version control
# Production pattern: load the key from a file
import duckdb
with open("/run/secrets/duckdb_key.bin", "rb") as f:
key = f.read()
con = duckdb.connect("prod_analytics.duckdb", encryption_key=key)
Summary
DuckDB’s transparent data encryption covers the core requirements of data-at-rest protection: block-level encryption, encrypted WAL, KDF-based key derivation and wiping, and GCM authentication. The performance cost is roughly 8–10% for typical analytical workloads.
Next steps:
- Make encryption the default configuration for any database that stores PII or compliance-relevant data
- Practice migrating between encrypted and plaintext databases with
ATTACH - Verify your backup and recovery process works end-to-end with encrypted files, including key handling
The official DuckDB documentation provides the full API reference; this article focuses on the most common hands-on paths.