Database Encryption Proxy¶
The CloudTaser database proxy provides transparent field-level encryption for PostgreSQL. It runs as a sidecar container, intercepting the PostgreSQL wire protocol to encrypt specified columns on write and decrypt them on read. The managed database only ever stores ciphertext in encrypted columns.
The database proxy ensures that even if a cloud provider, its DBAs, or a government subpoena (e.g., CLOUD Act warrant) gains access to the managed database, encrypted columns contain only ciphertext. Decryption requires the key encryption key (KEK) which is held exclusively in the EU-hosted OpenBao/Vault Transit engine.
How It Works¶
The proxy sits between the application and the managed database (Cloud SQL, RDS, Azure Database for PostgreSQL):
- Application connects to localhost:5432 (the proxy)
- Proxy establishes a separate TLS connection to the managed database
- On INSERT/UPDATE (
Bindmessages): encrypt parameter values targeting encrypted columns - On SELECT responses (
RowDescription+DataRow): decrypt encrypted column values before forwarding to the application - All other protocol messages pass through unmodified
The proxy uses the jackc/pgx/v5/pgproto3 library for PostgreSQL wire protocol parsing.
Encryption Protocol¶
- Algorithm: AES-256-GCM
- Per-value random DEK (32 bytes) and nonce (12 bytes) -- each individual value has its own unique key
- DEK wrapped by Vault Transit engine -- the key encryption key (KEK) never leaves the EU vault
- Encrypted value format:
version(1) + nonce(12) + wrapped_dek_len(2) + wrapped_dek(N) + ciphertext(M) - Storage: Encrypted values are stored as
byteain the database column
Because each value uses a random DEK and nonce, identical plaintext values produce different ciphertext. This provides strong security but means encrypted columns cannot be used in WHERE, JOIN, ORDER BY, or GROUP BY clauses. See DB Proxy Search Impact for details.
Configuration¶
The proxy is configured via environment variables. When deployed as a sidecar by the CloudTaser operator, these are set automatically from pod annotations.
Required Environment Variables¶
| Variable | Description | Default |
|---|---|---|
CLOUDTASER_DBPROXY_DB_ENDPOINT |
Upstream database host:port | -- (required) |
VAULT_ADDR |
OpenBao/Vault address | -- (required) |
CLOUDTASER_DBPROXY_TRANSIT_KEY |
Vault Transit engine key name | -- (required) |
CLOUDTASER_DBPROXY_ENCRYPT_COLUMNS |
Comma-separated table.column pairs to encrypt |
-- (required) |
Optional Environment Variables¶
| Variable | Description | Default |
|---|---|---|
CLOUDTASER_DBPROXY_LISTEN_ADDR |
Proxy listen address | :5432 |
CLOUDTASER_DBPROXY_DB_TLS |
Connect to upstream with TLS | true |
CLOUDTASER_DBPROXY_DB_TLS_SKIP_VERIFY |
Skip TLS verification for upstream (development only) | false |
CLOUDTASER_DBPROXY_TRANSIT_MOUNT |
Vault Transit engine mount path | transit |
CLOUDTASER_DBPROXY_HEALTH_ADDR |
Health check listen address | :9091 |
VAULT_TOKEN |
Vault token (development/testing only) | -- |
VAULT_AUTH_METHOD |
Auth method: kubernetes or token |
kubernetes |
VAULT_AUTH_ROLE |
Kubernetes auth role name | -- |
VAULT_AUTH_MOUNT_PATH |
Auth mount path | kubernetes |
VAULT_SKIP_VERIFY |
Skip TLS verification for Vault (development only) | false |
Column Specification Format¶
Encrypted columns are specified as comma-separated table.column pairs:
Each entry must contain exactly one dot separator. The table name is used for Bind message column matching, and the column name alone is used for RowDescription matching on SELECT responses.
Deployment via Operator¶
The operator injects the DB proxy sidecar when the cloudtaser.io/db-proxy: "true" annotation is set on the pod. The proxy listens on port 8192 (not 5432) when injected as a sidecar, with a health check on port 8193.
Pod Annotations¶
| Annotation | Description | Required |
|---|---|---|
cloudtaser.io/db-proxy |
Set to "true" to enable DB proxy sidecar |
Yes |
cloudtaser.io/db-proxy-upstream |
Upstream database address (host:port) | Yes |
cloudtaser.io/db-proxy-listen-port |
Override local listen port | No (default: 8192) |
cloudtaser.io/db-proxy-transit-key |
Vault Transit key name | Yes |
cloudtaser.io/db-proxy-transit-mount |
Vault Transit mount path | No (default: transit) |
Example¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
metadata:
annotations:
cloudtaser.io/inject: "true"
cloudtaser.io/vault-address: "https://vault.eu.example.com:8200"
cloudtaser.io/vault-role: "myapp"
cloudtaser.io/secret-paths: "secret/data/myapp/db"
cloudtaser.io/env-map: "password=PGPASSWORD,username=PGUSER"
cloudtaser.io/db-proxy: "true"
cloudtaser.io/db-proxy-upstream: "my-db.cloud-sql:5432"
cloudtaser.io/db-proxy-transit-key: "myapp-db"
spec:
containers:
- name: myapp
env:
- name: PGHOST
value: "localhost"
- name: PGPORT
value: "8192" # Connect to the proxy, not the database directly
Vault Transit Setup¶
The proxy requires a Vault Transit engine with a key for DEK wrapping:
# Enable Transit engine (if not already enabled)
vault secrets enable transit
# Create a key for the database proxy
vault write -f transit/keys/myapp-db type=aes256-gcm96
# Grant the CloudTaser role access to the key
vault policy write cloudtaser-db - <<EOF
path "transit/encrypt/myapp-db" {
capabilities = ["update"]
}
path "transit/decrypt/myapp-db" {
capabilities = ["update"]
}
EOF
Validation¶
The proxy validates all required configuration at startup and exits with an error if any is missing:
DBEndpoint(CLOUDTASER_DBPROXY_DB_ENDPOINT)VaultAddr(VAULT_ADDR)VaultTransitKey(CLOUDTASER_DBPROXY_TRANSIT_KEY)EncryptColumns(CLOUDTASER_DBPROXY_ENCRYPT_COLUMNS)
The auth method must be "token" or "kubernetes". When using "token", VAULT_TOKEN is also required.