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
| Layer | Mechanism | Default | Who configures |
|---|---|---|---|
| Postgres at rest | LUKS volume encryption (host) + pgcrypto for columns | recommended | operator (host) |
| Storage at rest | MinIO SSE-S3, per-project key | on | MINIO_KMS_SECRET_KEY |
| Secrets vault | pgcrypto pgp_sym_encrypt, keyed on CONTROL_PLANE_SECRET | on | automatic |
| Settings & project secrets | AES-256-GCM (enc:v1:), keyed on CONTROL_PLANE_SECRET | on | automatic |
| In transit (external) | TLS via Traefik (Let’s Encrypt) | configure for prod | operator |
| In transit (internal) | mTLS between services | off (opt-in) | ANVILBASE_MTLS_ENABLED |
| WAL/backups | wal-g with libsodium encryption | off (opt-in) | ANVILBASE_WALG_ENABLED |
| Key management | KMS abstraction (local → external) | local | operator |
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:
KEY_ID=anvilbase-prodBASE64=$(openssl rand -base64 32)echo "MINIO_KMS_SECRET_KEY=${KEY_ID}:${BASE64}" # put in .envUse 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 secrets —
settings.smtp.password, OAuth providerclient_secrets, and the SMS provider auth token. - Webhook signing secrets —
webhook_endpoints.secret. - Per-project credential columns — the
projectstable’sjwt_secret,anon_key,service_role_key, andprevious_jwt_secret. These were previously stored plaintext; a single read of the platform DB could forgeservice_roletokens 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:
docker/mtls/generate-certs.sh # generate CA + per-service certs# set in .env:ANVILBASE_MTLS_ENABLED=trueCert/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):
ANVILBASE_WALG_ENABLED=trueWALG_S3_PREFIX=s3://anvilbase-wal/WALG_LIBSODIUM_KEY=$(openssl rand -hex 32) # KEEP THIS SAFE — losing it loses recoveryKey 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.