# Rate Limiting fuer Anmelde- und Registrierungsrouten. # Zweite Verteidigungslinie - die Anwendung begrenzt zusaetzlich pro # Konto, was nginx nicht kann. limit_req_zone $binary_remote_addr zone=auth_zone:10m rate=10r/m; limit_req_status 429; # Zugriffsprotokoll ohne vollstaendige IP-Adresse. # # Fuer den Betrieb genuegt, WAS abgerufen wurde und ob es geklappt hat. # Die letzte Stelle der Adresse traegt dazu nichts bei, macht den # Eintrag aber personenbeziehbar - und Protokolle wandern in # Sicherungen und leben laenger als der Anlass. map $remote_addr $ip_gekuerzt { ~(?\d+\.\d+\.\d+)\.\d+ $v4.0; ~(?[0-9a-fA-F]+:[0-9a-fA-F]+):.* $v6::; default 0.0.0.0; } log_format gekuerzt '$ip_gekuerzt - [$time_local] "$request" $status ' '$body_bytes_sent "$http_user_agent"'; # Kein $http_referer: Bei einem oeffentlichen Listenlink stuende dort # der Token, sobald jemand von dort aus weiterklickt. access_log /var/log/nginx/access.log gekuerzt; server { listen 8080; server_name _; server_tokens off; root /usr/share/nginx/html; index index.html; gzip on; gzip_types text/css application/javascript application/json image/svg+xml; gzip_min_length 1024; client_max_body_size 2m; # Eingebauter DNS-Server von Docker. Ohne diesen loest nginx den # Namen "api" nur EINMAL beim Start auf und merkt sich die IP # dauerhaft - nach jedem Neustart des api-Containers gaebe es 502. resolver 127.0.0.11 valid=10s ipv6=off; resolver_timeout 5s; # Steht in proxy_pass eine Variable, loest nginx pro Request neu auf. # Dann muss die Ziel-URI aber explizit angehaengt werden - deshalb # ueberall $request_uri (enthaelt auch den Query-String). set $api_backend "http://api:8000"; # Sicherheitskopfzeilen. Siehe Kommentar in der Datei: sie muss in # JEDER location wiederholt werden, die eigene add_header setzt. include /etc/nginx/security_headers.conf; # ------------------------------------------------------------------ # API # ------------------------------------------------------------------ # Diese Pfade tragen einen Token im URI. Nicht protokollieren - # siehe Begründung bei /s/. location ~ ^/api/(public|invites)/ { proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; access_log off; } location /api/auth/welcome { proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; access_log off; } location /api/auth/email-change { proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; access_log off; } location /api/auth/verify { proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; access_log off; } location /api/ { proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; } # Ereigniskanal (Server-Sent Events). location ~ ^/api/lists/[^/]+/events$ { proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; # proxy_buffering, proxy_cache und die langen Zeitüberschreitungen # stehen bereits in proxy_common.conf - hier nicht wiederholen, # nginx lässt jede Direktive nur einmal je Kontext zu. # Neu ist nur: keine Komprimierung, die würde die Ereignisse # sammeln statt einzeln durchzureichen. gzip off; } location /api/auth/ { limit_req zone=auth_zone burst=20 nodelay; proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; } # Interaktive API-Dokumentation. Vor Produktivbetrieb auskommentieren # oder per allow/deny auf das interne Netz beschraenken. location ~ ^/(docs|redoc|openapi\.json)$ { proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; } location ~ ^/(healthz|readyz)$ { access_log off; proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; } # Klartext statt HTML-Fehlerseite, damit ein fetch() im Browser eine # verwertbare Meldung bekommt und nicht an JSON.parse scheitert. error_page 502 503 504 = @backend_down; location @backend_down { default_type application/json; return 503 '{"status":"error","detail":"Backend nicht erreichbar. Bitte docker compose logs api pruefen."}'; } # ------------------------------------------------------------------ # Statische PWA # ------------------------------------------------------------------ location = /sw.js { include /etc/nginx/security_headers.conf; add_header Cache-Control "no-cache, no-store, must-revalidate" always; try_files $uri =404; } # Vom api-Container erzeugt, damit der Anwendungsname aus der .env # auch auf dem Startbildschirm erscheint - ohne den web-Container # bei jeder Umbenennung neu bauen zu müssen. location = /manifest.webmanifest { proxy_pass $api_backend$request_uri; include /etc/nginx/proxy_common.conf; } # Von Vite gebaute Dateien tragen einen Hash im Namen - lange cachen. location /assets/ { include /etc/nginx/security_headers.conf; add_header Cache-Control "public, max-age=31536000, immutable" always; try_files $uri =404; } # Icons aendern sich praktisch nie. location /icons/ { include /etc/nginx/security_headers.conf; add_header Cache-Control "public, max-age=604800" always; try_files $uri =404; } # Öffentliche Listenlinks: nie zwischenspeichern, nie indexieren. location /s/ { include /etc/nginx/security_headers.conf; add_header Cache-Control "no-store" always; add_header X-Robots-Tag "noindex, nofollow, noarchive" always; # Der Token IST die Berechtigung. Stünde er im Zugriffsprotokoll, # käme jeder mit Leserecht auf die Logdateien an alle geteilten # Listen - und Logdateien wandern in Sicherungen, werden an # Auswertungswerkzeuge gereicht und leben länger als der Link. access_log off; try_files $uri /index.html; } location = /robots.txt { include /etc/nginx/security_headers.conf; default_type text/plain; return 200 "User-agent: *\nDisallow: /s/\nDisallow: /invite\nDisallow: /reset\nDisallow: /willkommen\nAllow: /impressum.html\nAllow: /datenschutz.html\n"; } # Aus demselben Grund wie bei /s/: Einladungs- und # Passwort-Zurücksetzen-Links tragen ihren Token im Query-String, # und den schreibt nginx sonst mit ins Protokoll. location = /invite { include /etc/nginx/security_headers.conf; add_header Cache-Control "no-store" always; add_header X-Robots-Tag "noindex, nofollow" always; access_log off; try_files $uri /index.html; } location = /willkommen { include /etc/nginx/security_headers.conf; add_header Cache-Control "no-store" always; add_header X-Robots-Tag "noindex, nofollow" always; access_log off; try_files $uri /index.html; } location = /reset { include /etc/nginx/security_headers.conf; add_header Cache-Control "no-store" always; add_header X-Robots-Tag "noindex, nofollow" always; access_log off; try_files $uri /index.html; } # Ohne Bundler tragen die JS-Dateien keinen Hash im Namen. "no-cache" # heisst nicht "nicht speichern", sondern "vor Benutzung rueckfragen" - # der Browser holt also nur bei Aenderung neu. location / { include /etc/nginx/security_headers.conf; add_header Cache-Control "no-cache" always; try_files $uri $uri/ /index.html; } }