Skip to content

Latest commit

 

History

History
361 lines (280 loc) · 11.6 KB

File metadata and controls

361 lines (280 loc) · 11.6 KB

Cache Encryption

Fluxheim can encrypt disk cache objects before they are written to the filesystem or storage-bin backend. This is optional and disabled by default. Use it when cache files may contain private or regulated response bodies and the cache device must not expose plaintext while Fluxheim is stopped.

Cache encryption does not encrypt in-process memory cache contents, request or response logs, upstream responses in transit, or files served directly by [vhosts.web] before they enter an opt-in cache path. It is cache-at-rest protection.

What Gets Encrypted

Cache encryption wraps objects stored by [cache.disk], [vhosts.cache.disk], and [vhosts.routes.cache.disk]. It works with both disk backends:

  • backend = "filesystem": each persisted cache object is encrypted before it is written to its object file.
  • backend = "storage-bin": each persisted cache object is encrypted before it is placed into a bin file.

Memory cache remains plaintext inside the Fluxheim process. In a tiered memory-plus-disk cache, memory hits stay fast and the persisted disk copy is encrypted at rest.

The same encryption layer applies to reverse-proxy cache responses and to local/static cache responses when the selected cache policy has local_static = true.

Native runtime status: the native HTTP/1 proxy cache supports local-key and OpenBao Transit encrypted backend = "filesystem" and backend = "storage-bin" cache objects. OpenBao Transit support is enabled in Fluxheim's normal cache profile through the server crate's optional openbao-cache-encryption feature.

Providers

provider = "local" uses AES-256-GCM with a 64-character hex key loaded from one safe file or credential. It is simple and fast, but Fluxheim must be able to read the raw cache key at startup. Filesystem disk-cache fills use bounded encrypted chunks with this provider, preserving the streaming memory profile of plain filesystem cache writes.

provider = "openbao-transit" sends object bytes to OpenBao Transit encrypt and decrypt endpoints and stores only the returned vault:v... ciphertext in the cache backend. Use this when key custody, audit trails, and centralized rotation matter more than the added call latency on disk-cache reads and writes. Because the Transit API encrypts one complete plaintext value per request, Fluxheim caps concurrent OpenBao encrypted commit heap usage and can refuse a large cache fill under pressure rather than buffering too many objects at once.

Both providers bind the configured key_id as authenticated data. The combined cache key remains inside the encrypted object and is checked after decryption, so a stored encrypted object cannot be silently moved to another cache key and cache URLs are not exposed in envelope metadata. Encrypted filesystem names and storage-bin index entries use HMAC-SHA-256 with a separate root-bound index key, not an unkeyed digest, so offline readers cannot verify guessed URLs against persisted lookup identities.

Quick Start: Local Key

This is the simplest encrypted disk cache. It keeps the raw AES-256-GCM key on the host as a root-owned secret and exposes it to Fluxheim through a systemd/container credential.

[[vhosts]]
name = "assets"
hosts = ["assets.example.com"]

[vhosts.tls]
enabled = true

[vhosts.cache]
enabled = true
local_static = true
status_header = "x-cache-status"
image_extensions = ["webp", "png", "jpg", "jpeg"]
content_types = ["image/webp", "image/png", "image/jpeg"]
max_object_bytes = "32MiB"

[vhosts.cache.memory]
enabled = true
max_size_bytes = "512MiB"

[vhosts.cache.disk]
enabled = true
backend = "storage-bin"
path = "/var/cache/fluxheim/assets.example.com"
max_size_bytes = "20GiB"

[vhosts.cache.disk.encryption]
enabled = true
provider = "local"
algorithm = "aes-256-gcm"
key_id = "assets-cache-2026-05"
key_credential = "fluxheim-cache-key"

[vhosts.web]
root = "/srv/sites/assets.example.com"
index_files = ["index.html"]
deny_dotfiles = true

Generate and install the key:

install -d -m 0700 -o root -g root /etc/fluxheim/secrets
fluxheim cache-keygen | install -m 0600 -o root -g root /dev/stdin /etc/fluxheim/secrets/fluxheim-cache-key

For systemd:

[Service]
LoadCredential=fluxheim-cache-key:/etc/fluxheim/secrets/fluxheim-cache-key

For containers, mount the same value at:

/run/secrets/fluxheim-cache-key

Local Key Setup

Prefer credentials over paths so the same TOML works with systemd credentials, Podman/Docker secrets, and Kubernetes secrets:

[cache.disk.encryption]
enabled = true
provider = "local"
algorithm = "aes-256-gcm"
key_id = "local-cache-v1"
key_credential = "fluxheim-cache-key"

Create the key as a root-owned secret:

install -d -m 0700 -o root -g root /etc/fluxheim/secrets
fluxheim cache-keygen | install -m 0600 -o root -g root /dev/stdin /etc/fluxheim/secrets/fluxheim-cache-key

For systemd, expose it to Fluxheim with a drop-in:

[Service]
LoadCredential=fluxheim-cache-key:/etc/fluxheim/secrets/fluxheim-cache-key

Then run:

systemctl daemon-reload
systemctl restart fluxheim

For containers, mount the secret at /run/secrets/fluxheim-cache-key.

Quick Start: OpenBao Transit

Use OpenBao Transit when the cache encryption key should stay outside the Fluxheim host and when key operations should be auditable in a central service. Fluxheim stores only Transit ciphertext such as vault:v... in the cache backend.

[[vhosts]]
name = "repo"
hosts = ["repo.example.com"]

[vhosts.cache]
enabled = true
status_header = "x-cache-status"
content_types = ["application/octet-stream", "application/x-rpm"]
max_object_bytes = "256MiB"

[vhosts.cache.disk]
enabled = true
backend = "storage-bin"
path = "/var/cache/fluxheim/repo.example.com"
max_size_bytes = "200GiB"

[vhosts.cache.disk.encryption]
enabled = true
provider = "openbao-transit"
key_id = "repo-cache-openbao-v1"

[vhosts.cache.disk.encryption.openbao]
address = "https://openbao.internal.example"
mount = "transit"
key_name = "fluxheim-repo-cache"
token_credential = "openbao-token"

[vhosts.proxy]
upstreams = ["repo-backend:8080"]
upstream_tls = false

Create a minimal Transit policy:

path "transit/encrypt/fluxheim-repo-cache" {
  capabilities = ["update"]
}

path "transit/decrypt/fluxheim-repo-cache" {
  capabilities = ["update"]
}

Then expose the OpenBao token as a systemd/container credential named openbao-token.

OpenBao Transit Setup

The OpenBao provider expects a Transit key and a token that can encrypt and decrypt with that key:

[cache.disk.encryption]
enabled = true
provider = "openbao-transit"
key_id = "openbao-cache-v1"

[cache.disk.encryption.openbao]
address = "https://openbao.internal.example"
mount = "transit"
key_name = "fluxheim-cache"
token_credential = "openbao-token"

A minimal OpenBao policy for one cache key is:

path "transit/encrypt/fluxheim-cache" {
  capabilities = ["update"]
}

path "transit/decrypt/fluxheim-cache" {
  capabilities = ["update"]
}

Fluxheim accepts HTTPS OpenBao URLs, plus loopback HTTP URLs for local testing. Non-loopback plaintext HTTP OpenBao addresses are rejected. Transit encrypt/decrypt calls do not follow HTTP redirects, and Transit JSON responses are read through a bounded buffer before parsing so a compromised or misconfigured OpenBao endpoint cannot stream unbounded response bodies into the cache process.

Verifying Runtime Behavior

Enable a cache status header while validating a new policy:

[vhosts.cache]
status_header = "x-cache-status"
status_reason_header = "x-cache-reason"

Then request the same cacheable object twice:

curl -sD - -o /dev/null https://assets.example.com/logo.webp
curl -sD - -o /dev/null https://assets.example.com/logo.webp

Expected behavior:

  • first request: x-cache-status: MISS
  • second request: x-cache-status: HIT
  • disk cache files or bin files should not contain the plaintext response body

For a storage-bin cache, inspect the bin directory:

find /var/cache/fluxheim/assets.example.com -maxdepth 2 -type f -ls

For OpenBao Transit, persisted objects should contain vault:v... ciphertext markers rather than plaintext response bodies.

Rotation

For local-key encryption, changing the raw key should also change key_id. Fluxheim detects the effective root-key change and cold-purges the encrypted cache before accepting objects under the new key. Moving to a new cache.disk.path remains useful for an explicit namespace cutover.

The local provider creates a persistent random root identity and derives separate AES-256-GCM and HMAC index keys from the configured master key. This makes the effective encryption key unique to each independently initialized cache root even when operators reuse one master secret. Fluxheim durably reserves each random-nonce object-encryption invocation in a locked counter, warns as the effective root key approaches 2^32 invocations, and fails closed at that bound. Preserve .fluxheim-encryption-*, .fluxheim-gcm-*, and .fluxheim-index-key-* files with the cache root. Missing or damaged state on an established root is never allowed to reuse an AES key with a reset counter: missing active-key or counter state fails startup, while a missing root identity creates a new effective key and requires a cold purge.

The first 1.7.12 startup against an older encrypted root creates root-bound state and cold-purges legacy objects and indexes. Envelope v1 objects are no longer accepted. Never clone one initialized encrypted root into multiple live replicas, and do not roll its hidden cryptographic state back from a snapshot. A complete filesystem rollback cannot be detected using only state stored on that filesystem. Use independent roots plus peer fill; use OpenBao or another external key-management boundary when externally enforced rollback resistance is required.

For OpenBao Transit, the usual rotation path is to keep the same Fluxheim key_id, mount, and key_name, then rotate the Transit key inside OpenBao. OpenBao can decrypt older vault:v... ciphertext while retaining the necessary old key versions. If you change Fluxheim key_id or key_name, treat it as a cache namespace cutover and purge or move the disk cache.

OpenBao-backed roots generate a random index-HMAC key once and persist only its Transit-encrypted ciphertext. Preserve that state file with the active root; the bearer token is not used as deterministic index-key material.

Local Validation

Run the local-key storage-bin smoke without external services:

cargo build
scripts/smoke_cache_encryption_local.sh

Run the optional OpenBao Transit smoke with Podman:

cargo build
scripts/smoke_openbao_cache_encryption.sh

The OpenBao smoke starts a disposable OpenBao dev container, enables Transit, creates a cache key, runs Fluxheim against a local origin, verifies MISS followed by HIT, and checks that the cache object contains Transit ciphertext rather than the plaintext response body.

Operational Notes

  • Keep cache encryption opt-in. It adds CPU work for local-key encryption and network/service dependency for OpenBao Transit.
  • Prefer the local provider when large filesystem cache fills must preserve streaming memory behavior; OpenBao Transit is a whole-object external crypto boundary by design.
  • Prefer storage-bin for high-churn encrypted caches that would otherwise create many small encrypted object files.
  • Use memory plus encrypted disk when hot objects should remain fast but disk persistence must be protected.
  • Treat key_id as cache-object authentication metadata. Changing it is a cache namespace cutover.
  • Keep cache-status headers disabled in normal production responses unless they are needed for debugging.