deploy: fix nginx security headers dropped by add_header inheritance

Found while verifying the first real provision: none of the four
security headers reached the browser on any HTML page.

nginx's add_header inheritance is all-or-nothing — a location that sets
any add_header of its own discards every header inherited from the
server block. "/" resolves through try_files to `location = /index.html`,
which sets Cache-Control, so HSTS, X-Content-Type-Options,
X-Frame-Options and Referrer-Policy were silently dropped exactly where
they matter. /assets/ lost them the same way.

Move the four into snippets/security-headers.conf and include it in the
server block and in both locations that add a header of their own.

Also from the same provision run:
- ssl_stapling is dead config now that Let's Encrypt certificates carry
  no OCSP responder URL; it only logs a warning per cert on each reload;
- README step 3 chmod'ed /etc/exo to 750 but never set its group, so the
  exo user could not traverse it and exo-deploy died on "cannot read
  /etc/exo/<app>.env";
- README step 7 dropped http-extras.conf into conf.d without disabling
  the same directives in Debian's stock nginx.conf, and nginx refuses to
  start on a duplicate gzip / server_tokens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Yuriy Panov
2026-09-06 23:56:16 +06:00
co-authored by Claude Opus 5
parent 51f39c9b88
commit 9736024e60
4 changed files with 32 additions and 7 deletions
@@ -0,0 +1,11 @@
# /etc/nginx/snippets/security-headers.conf
#
# nginx's add_header inheritance is all-or-nothing: a location that sets any
# add_header of its own silently discards every header inherited from the
# server block. `/` resolves through try_files to `location = /index.html`,
# which sets Cache-Control — so without re-including this snippet there, HTML
# pages ship with no security headers at all.
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
+4 -2
View File
@@ -4,8 +4,10 @@ ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
# Let's Encrypt stopped putting an OCSP responder URL in its certificates, so
# stapling is a no-op that only produces a warning per cert on every reload.
# ssl_stapling on;
# ssl_stapling_verify on;
# Yandex DNS — reachable from Russian datacentres, unlike 8.8.8.8 at times.
resolver 77.88.8.8 77.88.8.1 valid=300s;
resolver_timeout 5s;