Skip to content

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 (localhost:5432) -> CloudTaser DB Proxy -> Managed Database (host:port)
  1. Application connects to localhost:5432 (the proxy)
  2. Proxy establishes a separate TLS connection to the managed database
  3. On INSERT/UPDATE (Bind messages): encrypt parameter values targeting encrypted columns
  4. On SELECT responses (RowDescription + DataRow): decrypt encrypted column values before forwarding to the application
  5. 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 bytea in 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:

CLOUDTASER_DBPROXY_ENCRYPT_COLUMNS=users.ssn,users.email,payments.card_number

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.