diff --git a/deploy/README.md b/deploy/README.md index cea3154..f3c1b89 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -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 diff --git a/deploy/nginx/site.conf.template b/deploy/nginx/site.conf.template index 167ac15..fbbdc09 100644 --- a/deploy/nginx/site.conf.template +++ b/deploy/nginx/site.conf.template @@ -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; } diff --git a/deploy/nginx/snippets/security-headers.conf b/deploy/nginx/snippets/security-headers.conf new file mode 100644 index 0000000..b5c3f6b --- /dev/null +++ b/deploy/nginx/snippets/security-headers.conf @@ -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; diff --git a/deploy/nginx/snippets/ssl-params.conf b/deploy/nginx/snippets/ssl-params.conf index 7e95f13..ff89aa7 100644 --- a/deploy/nginx/snippets/ssl-params.conf +++ b/deploy/nginx/snippets/ssl-params.conf @@ -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;