From af56f810a8cfedf2b2fb2226b426ea3edb3a78a5 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Fri, 16 Oct 2026 06:38:04 -0400 Subject: [PATCH] Prod compose: record the hardening running on the box since 2026-10-15 The box's compose got these during the security audit and the repo copy did not, so a rebuild from the repo would have undone them: - Redis requires a password (FN_REDIS_PASSWORD, written to a config file inside the container; healthcheck via REDISCLI_AUTH); FN_REDIS_URL carries it. - The internal network is internal: true, so the database and Redis have no route off the box. The relay reaches S3 and APNs through web. - relay, redis and obfs4bridge drop every capability; db keeps the five its entrypoint needs. All four set no-new-privileges. - obfs4bridge is on web only. .env.example documents FN_REDIS_PASSWORD. The deploy itself uses the box's compose, so this changes nothing that is running. Co-Authored-By: Claude Opus 5.5 --- .env.example | 5 +++++ docker-compose.prod.yml | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index ad9b1a7..908c0dc 100644 --- a/.env.example +++ b/.env.example @@ -261,6 +261,11 @@ FN_ATTACHMENT_BYTES_PER_MONTH=524288000 # .federated.yml run a Redis and set this themselves. Set it here only if you # point the base file at a Redis of your own. # FN_REDIS_URL=redis://redis:6379 +# +# docker-compose.prod.yml gives its Redis a password. Set both, with the same +# value: hex is safest, since the URL carries it unencoded. +# FN_REDIS_PASSWORD= +# FN_REDIS_URL=redis://:@redis:6379 # Concurrency knobs, measured in docs/CAPACITY.md. Redis connections per event # loop: 32 by default for one relay, which waits in memory. With diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 0f8eb7c..42d797b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -33,6 +33,10 @@ services: POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}" volumes: - relay-db:/var/lib/postgresql/data + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + # The entrypoint fixes data-dir ownership and drops to the postgres user. + cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETGID, SETUID] networks: [internal] healthcheck: test: ["CMD-SHELL", "pg_isready -U flamenet -d flamenet"] @@ -44,12 +48,22 @@ services: # takes out both. Not persisted on purpose, see the note in RoutesWake.swift. redis: image: redis:7-alpine - command: ["redis-server", "--save", "", "--appendonly", "no", - "--maxmemory", "64mb", "--maxmemory-policy", "allkeys-lru"] + # A password since 2026-10-15. It goes into a config file inside the + # container so it never appears in the host's process list; redis-cli + # reads it from REDISCLI_AUTH for the same reason. FN_REDIS_URL carries it + # as redis://:@redis:6379. + user: redis + environment: + REDIS_PASSWORD: "${FN_REDIS_PASSWORD:?set FN_REDIS_PASSWORD in .env}" + command: ["sh", "-c", "printf 'requirepass %s\\n' \"$$REDIS_PASSWORD\" > /tmp/auth.conf && exec redis-server /tmp/auth.conf --save '' --appendonly no --maxmemory 64mb --maxmemory-policy allkeys-lru"] restart: unless-stopped + # No Linux capabilities beyond what the process needs, and no privilege + # gain through setuid binaries (security audit 2026-10-15). + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] networks: [internal] healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD-SHELL", "REDISCLI_AUTH=\"$$REDIS_PASSWORD\" redis-cli ping | grep -q PONG"] interval: 10s timeout: 3s retries: 5 @@ -193,6 +207,10 @@ services: - .env volumes: - relay-attachments:/data/attachments + # No Linux capabilities beyond what the process needs, and no privilege + # gain through setuid binaries (security audit 2026-10-15). + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] networks: [web, internal] labels: traefik.enable: "true" @@ -424,8 +442,13 @@ services: # start rather than silently re-key, so an unmounted volume fails loudly # instead of quietly breaking reconnects. - obfs4-state:/var/lib/obfs4 + # No Linux capabilities beyond what the process needs, and no privilege + # gain through setuid binaries (security audit 2026-10-15). + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + # Only on the web network: the bridge forwards to traefik:443 and needs + # nothing on internal, where the database and Redis live. networks: - internal: {} # On the edge proxy's network to reach 443, at a fixed address because # the proxy's PROXY-protocol trust is pinned to it. An unpinned address # can be handed to another container after a restart, and that @@ -438,8 +461,11 @@ services: networks: web: external: true + # No route off the box (2026-10-15). The database and Redis need none; the + # relay reaches S3 and APNs through web. internal: driver: bridge + internal: true volumes: relay-db: -- 2.54.0