Prod compose: record the hardening running on the box since 2026-10-15
ci / checks (pull_request) Successful in 12s
ci / swift (pull_request) Successful in 8m50s
ci / image (pull_request) Successful in 50s
ci / deploy (pull_request) Skipped

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 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-10-16 06:38:04 -04:00
co-authored by Claude Opus 5.5
parent 91af917aa4
commit af56f810a8
2 changed files with 35 additions and 4 deletions
+5
View File
@@ -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=<openssl rand -hex 32>
# FN_REDIS_URL=redis://:<same password>@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
+30 -4
View File
@@ -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://:<password>@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: