Files
dtf-system/deploy/nginx.conf.template
Cauê Faleiros 3b92813491
Some checks failed
Build and deploy / Validate source (push) Successful in 7s
Build and deploy / Integration suite on a real stack (push) Failing after 51s
Build and deploy / Secret scan and release gate (push) Successful in 9s
Build and deploy / Publish images and notify Portainer (push) Has been skipped
fix: recover the customer address behind the host reverse proxy
The production gateway does not face the internet: nginx-proxy-manager owns
80/443 on the host and proxies to it. So $remote_addr inside the gateway is that
proxy, and overwriting X-Forwarded-For with it discarded the customer address
the proxy had already recorded. Every request would have been attributed to one
internal address, which is exactly the fault 2.1 set out to fix, reintroduced in
production only.

Use real_ip to take the customer address from the proxy's header, trusting only
private networks. A request that reaches the published port directly from the
internet is not trusted, so its header is ignored and $remote_addr stays the
real peer: the anti-spoofing property is kept.

Also downgrade 2.9. TLS is not missing, it is terminated by that proxy. The gap
is that the repository never says so, which would break every session cookie if
the stack moved to a host without one.

Validated with nginx -t against the rendered production configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:07:56 -03:00

56 lines
2.9 KiB
Plaintext

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=20r/s;
server {
listen 8080;
server_name ${PUBLIC_HOST};
if ($host != ${PUBLIC_HOST}) { return 400; }
# Resolve through Docker's embedded DNS at request time. This prevents
# Nginx from exiting during a Swarm rollout when the API task is briefly
# unavailable or still creating its database schema.
resolver 127.0.0.11 ipv6=off valid=10s;
set $api_upstream api:8000;
# This gateway sits behind the host's reverse proxy, so $remote_addr is that
# proxy, not the customer. Recover the real address from the header it sets,
# and only when the connection comes from a private network: a request that
# reaches the published port directly from the internet is not trusted, so
# its X-Forwarded-For is ignored and $remote_addr stays the actual peer.
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;
root /usr/share/nginx/html;
index ${WEB_INDEX};
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer always;
add_header X-Frame-Options DENY always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' @SCRIPT_HASHES@ https://cdnjs.cloudflare.com; script-src-attr 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob: https://cdn.vnda.com.br; connect-src 'self' ${S3_PUBLIC_ENDPOINT} https://cdn.vnda.com.br https://cdnjs.cloudflare.com; worker-src 'self' blob: https://cdnjs.cloudflare.com; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'" always;
location = /health { access_log off; return 200 'ok'; }
location /api/ {
limit_req zone=api_limit burst=100 nodelay;
limit_req_status 429;
proxy_pass http://$api_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
# Overwrite, never append: $proxy_add_x_forwarded_for keeps any header the
# client sent, and the leftmost value would then be attacker-controlled.
# After real_ip above, $remote_addr is the customer even behind the proxy.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
client_max_body_size 2m;
}
# Always revalidate HTML/JS/CSS after a deployment. Without this, a browser
# can pair a new Kanban page with a cached older script after a rollout.
location / {
expires -1;
try_files $uri $uri/ =404;
}
}