BREAKING CHANGE: the production container listens on 8080 instead of 80. nginx now runs as an unprivileged user and cannot bind a privileged port without CAP_NET_BIND_SERVICE, which compose drops. Anything routing to this container must be repointed - in the homelab Traefik config, traefik.http.services.movieloop.loadbalancer.server.port 80 -> 8080. nginx's add_header is not additive across levels: a block declaring any add_header of its own discards every header inherited from its parent. Both /assets/ and /config.js declared one, so X-Frame-Options, X-Content-Type-Options and Referrer-Policy were silently absent from every JavaScript and CSS response. The headers now come from a snippet included in each location - verified 6/6 on /, on a hashed asset bundle, and on /config.js. Adds a CSP, HSTS and Permissions-Policy. connect-src is derived at container start from API_URL, because the API is deliberately not same-origin in production; asserting same-origin would have broken the deployment outright. The inline theme script is allowed by SHA-256 hash rather than unsafe-inline, which would have bought almost nothing - editing that script means regenerating the hash. config.js is also no longer rendered with envsubst, which does no escaping: an API_URL containing a double quote closed the JS string literal and the remainder executed on every page load. Addresses M31, M34.
46 lines
1.7 KiB
Nginx Configuration File
46 lines
1.7 KiB
Nginx Configuration File
server {
|
|
# 8080, not 80: the server runs unprivileged and cannot bind a
|
|
# privileged port without a capability that compose drops.
|
|
listen 8080;
|
|
listen [::]:8080;
|
|
server_name localhost;
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
# Security headers are pulled in from a snippet rendered at container start
|
|
# (docker/40-render-config.sh), because the CSP has to name the API origin
|
|
# and that is only known from API_URL at runtime.
|
|
#
|
|
# The include is repeated in every location that declares an add_header of
|
|
# its own. nginx's add_header is NOT additive across levels: a block that
|
|
# sets any add_header discards every header inherited from its parent. That
|
|
# is why these headers were silently missing from every JavaScript and CSS
|
|
# response, which is most of what a browser loads.
|
|
include /etc/nginx/snippets/security-headers.conf;
|
|
|
|
# Gzip compression
|
|
gzip on;
|
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
|
|
gzip_min_length 1000;
|
|
|
|
# Cache static assets
|
|
location /assets/ {
|
|
include /etc/nginx/snippets/security-headers.conf;
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable" always;
|
|
}
|
|
|
|
# Runtime config — rendered fresh on each container start. Never cache.
|
|
location = /config.js {
|
|
include /etc/nginx/snippets/security-headers.conf;
|
|
add_header Cache-Control "no-store" always;
|
|
try_files /config.js =404;
|
|
}
|
|
|
|
# SPA fallback — serve index.html for all non-file routes
|
|
location / {
|
|
include /etc/nginx/snippets/security-headers.conf;
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
}
|