From 22213a5592f0618936febf495f2f54ef1e842e79 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 24 Sep 2026 15:05:44 +0200 Subject: [PATCH] Harden the edge: catch-all server, modern TLS and HSTS - Unknown hostnames and direct IP access no longer land on the first vhost (Gitea): port 80 closes the connection, except for ACME challenges, and port 443 refuses the handshake. - Mozilla intermediate ciphers (forward secrecy only, no CBC), session cache, no session tickets, and HSTS with a short max-age for now. - Set X-Forwarded-For to the client address instead of appending to whatever the client sent. --- README.md | 7 +++++++ edge/conf.d/01-default.conf | 25 +++++++++++++++++++++++++ edge/snippets/proxy.conf | 4 +++- edge/snippets/tls.conf | 13 ++++++++++++- 4 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 edge/conf.d/01-default.conf diff --git a/README.md b/README.md index 763413e..7d132da 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,13 @@ docker compose -f edge/compose.yml up -d starting. Upstreams use the target's `container_name` (see "Running it" above for why). Shared bits are in `edge/snippets/`. Check a change with `docker exec edge-nginx nginx -t`, then `docker exec edge-nginx nginx -s reload`. +- **Unknown hostnames** hit `01-default.conf`: port 80 closes the connection + (ACME challenges still work), port 443 refuses the TLS handshake. Nothing + reaches a project unless its vhost names the host. +- **TLS** settings (`snippets/tls.conf`) follow Mozilla's intermediate + profile and send HSTS with a 1-day `max-age`. Raise it once every hostname + is confirmed to work over https: browsers remember it, so a long value + can't be taken back quickly. - **Certificates** are state, not versioned (`edge/certbot/`). To issue one for new hostnames (their DNS has to already point at the host): diff --git a/edge/conf.d/01-default.conf b/edge/conf.d/01-default.conf new file mode 100644 index 0000000..3c78331 --- /dev/null +++ b/edge/conf.d/01-default.conf @@ -0,0 +1,25 @@ +# Catch-all for Host headers no vhost claims (scanners, direct IP access, +# stale DNS). Without it nginx hands them to the first vhost it loaded, +# i.e. Gitea. ACME stays reachable here so a certificate for a new hostname +# can be issued before its vhost exists. +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 444; + } +} + +# No certificate to present for an unknown name, so refuse the handshake. +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name _; + ssl_reject_handshake on; +} diff --git a/edge/snippets/proxy.conf b/edge/snippets/proxy.conf index 56bd34f..a3386ab 100644 --- a/edge/snippets/proxy.conf +++ b/edge/snippets/proxy.conf @@ -1,7 +1,9 @@ proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# The edge is the first hop, so whatever X-Forwarded-For the client sent is +# made up: replace it instead of appending to it. +proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; diff --git a/edge/snippets/tls.conf b/edge/snippets/tls.conf index e79c9dd..e1afd10 100644 --- a/edge/snippets/tls.conf +++ b/edge/snippets/tls.conf @@ -1,2 +1,13 @@ +# Mozilla "intermediate" profile, without the DHE suites (they'd need a +# dhparam file): forward secrecy on every connection, no CBC. ssl_protocols TLSv1.2 TLSv1.3; -ssl_ciphers HIGH:!aNULL:!MD5; +ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; +ssl_prefer_server_ciphers off; +ssl_session_cache shared:SSL:10m; +ssl_session_timeout 1d; +ssl_session_tickets off; + +# Short max-age for now: raise it (e.g. 63072000) once every hostname is +# confirmed working over https. A location that sets its own add_header +# stops inheriting this one. +add_header Strict-Transport-Security "max-age=86400" always;