Harden nginx: real client IP, HSTS, custom dotfile 404 (PT-01/02/04)

PT-01: Add set_real_ip_from/real_ip_header/real_ip_recursive to restore
real client IP from X-Forwarded-For. Rate limiting now keys on actual
client IP instead of the Pangolin proxy IP.

PT-02: Add Strict-Transport-Security header (max-age 1 year) to both
the server block and static assets block.

PT-04: Replace bare 404 on dotfile requests with JSON response to
suppress nginx server identity disclosure in error pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-02 17:43:12 +08:00
parent ccfc5e151a
commit 7721bf5cec

View File

@ -21,15 +21,26 @@ server {
# Suppress nginx version in Server header # Suppress nginx version in Server header
server_tokens off; server_tokens off;
# ── Real client IP restoration (PT-01) ────────────────────────────
# Pangolin (TLS-terminating reverse proxy) connects via Docker bridge.
# Restore the real client IP from X-Forwarded-For so that limit_req_zone
# (which keys on $binary_remote_addr) throttles per-client, not per-proxy.
# Safe to trust all sources: nginx is only reachable via Docker networking,
# never directly internet-facing. Tighten if deployment model changes.
set_real_ip_from 0.0.0.0/0;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# Gzip compression # Gzip compression
gzip on; gzip on;
gzip_vary on; gzip_vary on;
gzip_min_length 1024; gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json; gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json;
# Block dotfiles (except .well-known for ACME/Let's Encrypt) # Block dotfiles (except .well-known for ACME/Let's Encrypt) (PT-04)
location ~ /\.(?!well-known) { location ~ /\.(?!well-known) {
return 404; default_type application/json;
return 404 '{"detail":"Not Found"}';
} }
# Rate-limited auth endpoints (keep in sync with proxy-params.conf) # Rate-limited auth endpoints (keep in sync with proxy-params.conf)
@ -104,6 +115,7 @@ server {
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
} }
# Security headers # Security headers
@ -111,4 +123,5 @@ server {
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; connect-src 'self';" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
} }