diff --git a/Dockerfile b/Dockerfile index e1eb340..5eaceab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,8 @@ FROM nginx:1.27-alpine # Static single-page app — no build step. COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY index.html impressum.html datenschutz.html legal.css manifest.webmanifest sw.js icon.svg /usr/share/nginx/html/ +COPY security-headers.conf /etc/nginx/security-headers.conf +COPY index.html impressum.html datenschutz.html legal.css icon-mono.svg manifest.webmanifest sw.js icon.svg /usr/share/nginx/html/ COPY vendor/ /usr/share/nginx/html/vendor/ EXPOSE 80 diff --git a/nginx.conf b/nginx.conf index f030789..0518722 100644 --- a/nginx.conf +++ b/nginx.conf @@ -4,53 +4,41 @@ server { root /usr/share/nginx/html; index index.html; - # Gzip for the text assets. gzip on; gzip_types text/html text/css application/javascript application/json image/svg+xml; gzip_min_length 512; - # Lightweight health endpoint for Coolify / Docker healthcheck. + include /etc/nginx/security-headers.conf; + location = /healthz { access_log off; add_header Content-Type text/plain; return 200 "ok\n"; } - # Vendor libs + fonts can be cached hard. location /vendor/ { + include /etc/nginx/security-headers.conf; expires 30d; add_header Cache-Control "public, immutable"; } - # Service worker and manifest must always revalidate. location = /sw.js { + include /etc/nginx/security-headers.conf; add_header Cache-Control "no-cache"; } + location = /manifest.webmanifest { + include /etc/nginx/security-headers.conf; default_type application/manifest+json; add_header Cache-Control "no-cache"; } - # Never cache the app shell, so updates land immediately. location = /index.html { + include /etc/nginx/security-headers.conf; add_header Cache-Control "no-cache"; } location / { try_files $uri $uri.html $uri/ /index.html; } - - # Hardening headers. - add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "DENY" always; - add_header Referrer-Policy "no-referrer" always; - add_header Strict-Transport-Security "max-age=31536000" always; - add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()" always; - - # Content Security Policy. - # connect-src 'none' ist hier das eigentliche Versprechen: der Browser selbst - # unterbindet jede fetch/XHR/WebSocket-Verbindung. "Nothing is uploaded" ist damit - # nicht nur behauptet, sondern erzwungen. img-src erlaubt https, damit Bilder aus - # eingefuegtem Markdown in der Vorschau sichtbar bleiben. - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self'; connect-src 'none'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'; manifest-src 'self'; worker-src 'self'" always; } diff --git a/security-headers.conf b/security-headers.conf new file mode 100644 index 0000000..117adbd --- /dev/null +++ b/security-headers.conf @@ -0,0 +1,8 @@ +# Sicherheitsheader. MUSS in jeden location-Block eingebunden werden, der ein +# eigenes add_header setzt - nginx vererbt add_header nicht in solche Bloecke hinein. +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "DENY" always; +add_header Referrer-Policy "no-referrer" always; +add_header Strict-Transport-Security "max-age=31536000" always; +add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self'; connect-src 'none'; object-src 'none'; frame-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'; manifest-src 'self'; worker-src 'self'" always;