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:
co-authored by
Claude Opus 5
parent
51f39c9b88
commit
9736024e60
@@ -77,6 +77,7 @@ mkdir -p /srv/exo/{fitnes,hotel,medcenterphysio,medcenterstart}/releases
|
||||
mkdir -p /etc/exo /var/www/certbot
|
||||
chown -R exo:exo /srv/exo
|
||||
chmod 755 /srv/exo # www-data must traverse to reach dist/client
|
||||
chown root:exo /etc/exo # without the group, exo cannot traverse it at 750
|
||||
chmod 750 /etc/exo
|
||||
|
||||
for app in fitnes hotel medcenterphysio medcenterstart; do
|
||||
@@ -149,6 +150,13 @@ MaxRetentionSec=14day
|
||||
install -m 644 nginx/snippets/*.conf /etc/nginx/snippets/
|
||||
install -m 644 nginx/http-extras.conf /etc/nginx/conf.d/00-exo-http.conf
|
||||
|
||||
# Debian's stock nginx.conf sets some of the same http{} directives (gzip,
|
||||
# server_tokens), and nginx refuses to start on a duplicate. Comment out every
|
||||
# stock directive that 00-exo-http.conf now owns.
|
||||
for d in $(grep -oE '^[a-z_]+' /etc/nginx/conf.d/00-exo-http.conf | sort -u); do
|
||||
sed -i -E "s@^([[:space:]]*)($d[[:space:]]+[^;]*;)@\\1# \\2@" /etc/nginx/nginx.conf
|
||||
done
|
||||
|
||||
bin/exo-render-nginx /etc/nginx/sites-available # after filling in apps.conf
|
||||
ln -s /etc/nginx/sites-available/exo-*.conf /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
@@ -46,20 +46,24 @@ server {
|
||||
root /srv/exo/{{APP}}/current/dist/client;
|
||||
index index.html;
|
||||
|
||||
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;
|
||||
include /etc/nginx/snippets/security-headers.conf;
|
||||
|
||||
# Vite fingerprints everything under /assets — cache for a year.
|
||||
location ^~ /assets/ {
|
||||
access_log off;
|
||||
# Re-included: this location's own add_header would drop the inherited ones.
|
||||
include /etc/nginx/snippets/security-headers.conf;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# The one file that must never be cached, or a deploy goes unseen.
|
||||
location = /index.html { add_header Cache-Control "no-cache" always; }
|
||||
# `/` lands here via try_files, so this is where the security headers
|
||||
# actually have to be re-stated (see snippets/security-headers.conf).
|
||||
location = /index.html {
|
||||
include /etc/nginx/snippets/security-headers.conf;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
location = /robots.txt { access_log off; }
|
||||
location = /sitemap.xml { access_log off; }
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
Reference in New Issue
Block a user