Files
movieloop-frontend/nginx.conf
T
TehRiehlDeal ca3862d5a7 harden(docker)!: run nginx unprivileged and stop dropping security headers
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.
2026-09-10 22:51:03 -07:00

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;
}
}