Skip to content

Encryption

AnvilBase encrypts data at rest, in transit, and at the secret level. Most of it is on by default; a few pieces are operator responsibilities you wire up for production.

At a glance

LayerMechanismDefaultWho configures
Postgres at restLUKS volume encryption (host) + pgcrypto for columnsrecommendedoperator (host)
Storage at restMinIO SSE-S3, per-project keyonMINIO_KMS_SECRET_KEY
Secrets vaultpgcrypto pgp_sym_encrypt, keyed on CONTROL_PLANE_SECRETonautomatic
Settings & project secretsAES-256-GCM (enc:v1:), keyed on CONTROL_PLANE_SECRETonautomatic
In transit (external)TLS via Traefik (Let’s Encrypt)configure for prodoperator
In transit (internal)mTLS between servicesoff (opt-in)ANVILBASE_MTLS_ENABLED
WAL/backupswal-g with libsodium encryptionoff (opt-in)ANVILBASE_WALG_ENABLED
Key managementKMS abstraction (local → external)localoperator

At rest

Storage (SSE-S3)

Every project bucket is encrypted server-side with a per-project key. This requires a KMS key — set MINIO_KMS_SECRET_KEY in key-id:base64(32 bytes) form. Project provisioning calls put_bucket_encryption on every new bucket, so without this key, project creation fails and rolls back:

Terminal window
KEY_ID=anvilbase-prod
BASE64=$(openssl rand -base64 32)
echo "MINIO_KMS_SECRET_KEY=${KEY_ID}:${BASE64}" # put in .env

Use an operator-managed key in production (not the dev default), and back it up securely — losing it means losing access to encrypted objects.

Database

Encrypt the Postgres data volume at the host level (LUKS or your cloud provider’s volume encryption). For sensitive columns, use pgcrypto (Extensions). Host-level encryption of the platform-DB volume remains a load-bearing defense-in-depth control — see API Keys & Scopes.

Settings & project secrets (AES-256-GCM)

Per-deployment secrets are encrypted at rest in the platform database with AES-256-GCM, using a key HKDF-derived from CONTROL_PLANE_SECRET. The stored wire format is enc:v1:<base64(nonce‖ciphertext)>, and the API never returns these values in plaintext (responses redact or mask them). This covers:

  • Settings secretssettings.smtp.password, OAuth provider client_secrets, and the SMS provider auth token.
  • Webhook signing secretswebhook_endpoints.secret.
  • Per-project credential columns — the projects table’s jwt_secret, anon_key, service_role_key, and previous_jwt_secret. These were previously stored plaintext; a single read of the platform DB could forge service_role tokens for the whole fleet. They are now encrypted at the write sites (project + branch creation, JWT rotation, API-key rotation) and decrypted transparently when a project is loaded, so the cache and every caller see plaintext while the rows on disk are ciphertext.

Encryption is automatic and on by default. Existing plaintext rows converge to enc:v1: via a non-fatal sweep on every control-plane start, or on demand via POST /api/v1/admin/encrypt-settings-secrets (idempotent; legacy-plaintext values keep working on read until they are converged). CONTROL_PLANE_SECRET must stay stable for the life of the deployment — rotating it would strand every encrypted value (there is no automated re-key path).

Secrets vault

Application secrets are encrypted with pgp_sym_encrypt, keyed on the deployment’s CONTROL_PLANE_SECRET, and never returned in plaintext by the API. See Secrets Vault.

In transit

External: TLS at the edge

Traefik terminates TLS and can obtain certificates automatically via Let’s Encrypt. Only Traefik should be internet-facing. Configure your domain and ACME settings, and disable the Traefik dashboard port in production. See Network Security and the Production Checklist.

Internal: mTLS between services

For defense in depth, AnvilBase supports mutual TLS between internal services. It’s off by default (local dev doesn’t need it). To enable:

Terminal window
docker/mtls/generate-certs.sh # generate CA + per-service certs
# set in .env:
ANVILBASE_MTLS_ENABLED=true

Cert/key paths are mounted read-only into containers at fixed locations (/etc/anvilbase/tls/...). Rotate certificates per your policy. The flag secures both legs — listeners require client certs and every internal client presents its service certificate; see Internal mTLS for the complete edge map and known gaps.

Backups & WAL (wal-g)

Point-in-time recovery uses wal-g to archive WAL, with libsodium encryption of the archived segments. It’s off by default; enabling it is an operator setup step (archive bucket + archive_mode, which needs a Postgres restart to activate):

Terminal window
ANVILBASE_WALG_ENABLED=true
WALG_S3_PREFIX=s3://anvilbase-wal/
WALG_LIBSODIUM_KEY=$(openssl rand -hex 32) # KEEP THIS SAFE — losing it loses recovery

See Point-in-Time Recovery.

Key management

The control plane has a KMS abstraction: by default it uses a local key store, and it’s designed to be pluggable to an external KMS (e.g. AWS KMS, HashiCorp Vault) as your requirements grow. Treat these keys as your highest-value secrets:

  • CONTROL_PLANE_SECRET — signs PATs and is the key material for the secrets vault and the AES-256-GCM settings/project-secret encryption (above). Must stay stable for the deployment’s lifetime.
  • MINIO_KMS_SECRET_KEY — storage encryption.
  • WALG_LIBSODIUM_KEY — backup encryption.
  • per-project jwt_secret — managed by the platform, rotatable per project, stored AES-256-GCM-encrypted at rest in the platform DB.

Store them in a real secrets manager, never in version control, and have a recovery plan for each.

Next: Audit Logs.