Guides: storage & data

How to encrypt data at rest

This guide shows you how to enable at-rest encryption on a storage backend. Encryption is backend-scoped: every bucket routed to the backend inherits it. For the threat model behind each mode, see encryption at rest.

1. Pick a mode

  • If the backend is a filesystem, or an S3 provider you don't fully trust with plaintext, use aes256-gcm-proxy — the proxy encrypts before the backend sees the bytes.
  • If the backend is AWS S3 and you want per-decrypt audit logs and KMS key management, use sse-kms; if AWS-managed AES256 is enough, use sse-s3.
  • If the bucket's contents are public anyway, keep none — encryption is pure overhead there.

The full mode matrix, field list, and wire format are in the encryption reference.

2. Generate and place the key (proxy-AES only)

Generate a 32-byte hex key and put it in the proxy's environment — never in the config file. The env-var name is derived from the backend name (uppercase, -/._):

# for backend hetzner-fsn1
export DGP_BACKEND_HETZNER_FSN1_ENCRYPTION_KEY=$(openssl rand -hex 32)
# singleton-backend deployments use:
export DGP_ENCRYPTION_KEY=$(openssl rand -hex 32)

Before going further, store the key off-box — a secrets manager, an operator vault, a sealed envelope. If you lose a proxy-AES key, the encrypted objects on that backend are unrecoverable. The proxy does not escrow keys; there is no recovery path.

From the admin UI: Settings → Storage → Backends — each backend card has an encryption subsection with a mode dropdown and a key-generation widget. Keys are generated in-browser (crypto.getRandomValues) and never round-trip through the server before Apply; the panel shows a red key-loss banner and gates Apply behind an "I have stored this key safely" checkbox.

Enable encryption on a backend

3. Configure the backend

One worked example per mode.

Proxy-AES on a named S3 backend — key comes from the env var in step 2, so no key field appears in YAML:

# validate
storage:
  default_backend: hetzner-fsn1
  backends:
    - name: hetzner-fsn1
      type: s3
      endpoint: https://fsn1.your-objectstorage.com
      region: fsn1
      force_path_style: true
      encryption:
        mode: aes256-gcm-proxy
        key_id: hetzner-2026-06    # optional but recommended — stamps objects with a stable key generation
  buckets:
    db-archive:
      backend: hetzner-fsn1

Proxy-AES on a singleton filesystem backend (no # validate here — the ${env:…} reference only expands when DGP_ENCRYPTION_KEY is set):

storage:
  backend:
    type: filesystem
    path: /var/lib/deltaglider_proxy/data
  backend_encryption:
    mode: aes256-gcm-proxy
    key: "${env:DGP_ENCRYPTION_KEY}"
    key_id: local-2026-06

SSE-KMS on an AWS backend — the proxy never touches key material; AWS does the crypto:

# validate
storage:
  default_backend: aws-dr
  backends:
    - name: aws-dr
      type: s3
      region: eu-west-1
      encryption:
        mode: sse-kms
        kms_key_id: arn:aws:kms:eu-west-1:123456789012:key/abcd-ef01
        bucket_key_enabled: true   # reduces per-request KMS cost

SSE-S3 on an AWS backend:

      encryption:
        mode: sse-s3

Note the native SSE modes are S3-only; the proxy rejects them on filesystem backends at config check.

4. Restart and verify

Restart the proxy (or apply from the UI) so the env var and config load together, then prove the round trip:

  1. Write and read back through the proxy — clients must notice nothing:

    aws --endpoint-url https://s3.acme.example s3 cp dump.sql s3://db-archive/nightly/dump.sql
    aws --endpoint-url https://s3.acme.example s3 cp s3://db-archive/nightly/dump.sql - | sha256sum
    

    The hash must match the original.

  2. Check the stored object is actually ciphertext — look at it on the raw backend, bypassing the proxy. In proxy-AES mode the body starts with the DGE1 magic (chunked) or is opaque GCM ciphertext, and the backend-side user metadata carries the dg-encrypted marker (an aes-256-gcm-* value) plus dg-encryption-key-id. On a filesystem backend the markers live in the user.dg.metadata xattr:

    xattr -p user.dg.metadata /var/lib/deltaglider_proxy/data/db-archive/nightly/dump.sql
    

    Native modes stamp dg-encrypted-native: sse-kms (or sse-s3) instead — that marker is harmless to expose.

  3. Old plaintext objects still read fine: the decrypt path dispatches on the marker, and absent marker means "serve as-is."

5. Encrypt the historical objects

Enabling encryption is not retroactive — only new writes are encrypted; existing objects stay in their stored form. When you change a backend's encryption in the admin UI, the Backends page proposes a Re-encrypt job that rewrites every object not matching the new config.

Re-encrypt proposal

Accept it (or start one later from Settings → Jobs → + New job → Re-encrypt buckets…). The job write-gates each bucket while it runs and survives restarts — the mechanics, including what the write gate means for clients, are covered in How to rotate or change encryption keys.

Verify

  • A fresh PUT through the proxy reads back byte-identical (step 4.1).
  • The raw backend stores ciphertext and the dg-encrypted / dg-encrypted-native marker (step 4.2).
  • The key is stored somewhere safe outside the proxy host.
  • If you ran a re-encrypt job, its row at Settings → Jobs shows succeeded and a raw-backend spot-check of an old object now shows the marker too.