diff --git a/docker-compose.yaml b/docker-compose.yaml index 00b5f93..5995249 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -377,7 +377,7 @@ services: retries: 3 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 # templated into the conf file by Ansible if the port ever needs to change. web: @@ -390,7 +390,7 @@ services: mcp: condition: service_healthy ports: - - "23181:80" + - "23081:80" volumes: - ./nginx/mnemosyne.conf:/etc/nginx/conf.d/default.conf:ro - static:/var/www/static:ro diff --git a/nginx/mnemosyne.conf b/nginx/mnemosyne.conf index 40bd624..e17bc51 100644 --- a/nginx/mnemosyne.conf +++ b/nginx/mnemosyne.conf @@ -32,6 +32,19 @@ # resolution at startup and returns 502 after `docker compose restart app`. 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 # termination on Titania); fall back to $scheme only if there's no upstream # header. Inside the compose network $scheme is always `http` because HAProxy