fix(web): trust XFF for real client IP and correct port to 23081
All checks were successful
CVE Scan & Docker Build / security-scan (push) Successful in 3m41s
Build & Deploy Docs / build-and-deploy (push) Successful in 1m9s
CVE Scan & Docker Build / build-and-push (push) Successful in 3m29s

- Configure nginx `set_real_ip_from` for RFC1918 ranges and enable
  `real_ip_recursive` so allowlists evaluate the true client IP
  instead of Docker's NAT gateway, preventing public exposure of
  `/metrics` and `/nginx_status`
- Update published port from 23181 to 23081 in docker-compose
This commit is contained in:
2026-06-17 06:58:36 -04:00
parent ec4f12d601
commit 4dde063299
2 changed files with 15 additions and 2 deletions

View File

@@ -377,7 +377,7 @@ services:
retries: 3 retries: 3
start_period: 60s start_period: 60s
# ── Web: nginx reverse proxy, public port 23181 ──────────────────────────── # ── Web: nginx reverse proxy, public port 23081 ────────────────────────────
# No Django env — nginx only knows how to route. Public listener is # No Django env — nginx only knows how to route. Public listener is
# templated into the conf file by Ansible if the port ever needs to change. # templated into the conf file by Ansible if the port ever needs to change.
web: web:
@@ -390,7 +390,7 @@ services:
mcp: mcp:
condition: service_healthy condition: service_healthy
ports: ports:
- "23181:80" - "23081:80"
volumes: volumes:
- ./nginx/mnemosyne.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx/mnemosyne.conf:/etc/nginx/conf.d/default.conf:ro
- static:/var/www/static:ro - static:/var/www/static:ro

View File

@@ -32,6 +32,19 @@
# resolution at startup and returns 502 after `docker compose restart app`. # resolution at startup and returns 502 after `docker compose restart app`.
resolver 127.0.0.11 valid=10s; resolver 127.0.0.11 valid=10s;
# Recover the real client IP from X-Forwarded-For (set by HAProxy on Titania)
# before evaluating the RFC1918 allowlists below. nginx runs as a sidecar with
# a published port, so every proxied request arrives via Docker's NAT gateway
# (an RFC1918 address) — without this, the allowlists match that gateway and
# pass ALL external traffic, exposing /metrics and /nginx_status publicly.
# HAProxy's own health checks (e.g. to /healthz) carry no XFF and keep their
# real 10.10.x.x source, so they stay allowed.
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Preserve X-Forwarded-Proto from the upstream reverse proxy (HAProxy TLS # Preserve X-Forwarded-Proto from the upstream reverse proxy (HAProxy TLS
# termination on Titania); fall back to $scheme only if there's no upstream # termination on Titania); fall back to $scheme only if there's no upstream
# header. Inside the compose network $scheme is always `http` because HAProxy # header. Inside the compose network $scheme is always `http` because HAProxy