# ============================================================================ # nginx.conf.example — Production reverse proxy for the receipt-scanner app # (Next.js 15 App Router, standalone build: `node server.js` on :3000) # # English / Deutsch: comments alternate between English and German so both the # team and German-speaking operators can follow the reasoning. Replace the # placeholders (, ...) and drop this file into # /etc/nginx/conf.d/ as a real `server {}` block. # # SECURITY PRINCIPLE (Sicherheitsprinzip): # * `autoindex off;` — Verzeichnislisting ist explizit deaktiviert. Directory # listing is explicitly disabled: nginx will never render an index of a # directory, it always answers 403/404 for directories without an index # file. This is defense in depth — the Next standalone server below already # never lists directories and only serves `public/`. # * All sensitive requests are rejected AT THE PROXY (before they ever reach # the app): dotfiles, source/build artifacts, markdown, keys, logs, env # files. The app's own middleware (src/lib/http/sensitivePaths.ts) applies # the same policy again inside the container. # * Sensitive paths get `deny all` (403), NOT a redirect — a redirect would # confirm the resource exists (information leak). # ============================================================================ # ---------------------------------------------------------------------------- # HTTP → HTTPS redirect (only serves the HSTS upgrade, no app traffic) # HTTP-Datenverkehr wird ausschließlich auf HTTPS umgeleitet. # ---------------------------------------------------------------------------- server { listen 80; listen [::]:80; server_name example.com www.example.com; # Let's Encrypt / certbot webroot — kein App-Traffic hier. # `^~` ist wichtig: damit hat dieses Präfix Vorrang vor der Regex-Location # `~ /\.` weiter unten, die dotfiles sperrt — sonst würde die ACME-Challenge # unter /.well-known/ fälschlich mit 403 beantwortet. `^~` matters: without # it the dotfile-deny regex below would shadow the ACME challenge. location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$host$request_uri; } } # ---------------------------------------------------------------------------- # HTTPS server — the actual reverse proxy # ---------------------------------------------------------------------------- server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com www.example.com; # TLS-Zertifikate (Let's Encrypt empfohlen). TLS certificates — adjust paths. ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; # --- GLOBAL HARDENING --------------------------------------------------- # Verzeichnislisting ist explizit deaktiviert (Directory listing off): autoindex off; # Zweite Verteidigungsschicht gegen übergroße Uploads (Second layer of # defense against oversized uploads): the app already caps uploads at # MAX_UPLOAD_BYTES = 10 MB (src/lib/limits.ts) and, since the 2026-08-17 # hardening pass, streams+aborts multipart bodies that lack a trustworthy # Content-Length (readFormDataSized in src/lib/http/requestSize.ts). # This directive rejects oversized bodies at the proxy — before nginx even # finishes buffering them into the upstream connection — with 413. Set a # little above the app's 10 MB cap to leave room for multipart # boundary/header overhead on a legitimate max-size upload. client_max_body_size 11m; # Blockiert alle dotfiles/dot-Verzeichnisse (/.env, /.git/, /.next/, # /.next-corrupt-*/...) — bereits an der Proxy-Ebene, bevor der Request die # App erreicht. Blocks any URI containing a "/." segment (dotfiles etc.). # Deny (403), nie ein Redirect — kein Information Leak. # Einzige Ausnahme: /.well-known/ (ACME) ist oben per `^~` ausgenommen. location ~ /\. { deny all; } # Sensible Datei-Endungen (sensitive file extensions): Markdown (Doku), # private keys/Zertifikate, Logs und .env-Dateien — egal auf welcher Tiefe. location ~* \.(md|pem|key|crt|log|env.*)$ { deny all; } # Explizite Sperre für Projekt-/Build-Dateien im Repo-Root (explicit deny # für bekannte sensitive Namen — wirft 403 statt den Request weiterzuleiten). location = /docker-compose.yml { deny all; } location = /docker-compose.yaml { deny all; } location = /docker-compose.override.yml { deny all; } location = /docker-compose.override.yaml { deny all; } location = /Dockerfile { deny all; } location = /build_err.txt { deny all; } location = /package.json { deny all; } location = /package-lock.json { deny all; } location = /tsconfig.json { deny all; } location = /tsconfig.tsbuildinfo { deny all; } location = /next.config.ts { deny all; } location = /next.config.mjs { deny all; } location = /drizzle.config.ts { deny all; } # --- Security headers (Sicherheits-Header) ------------------------------ # HSTS: nur über HTTPS gesendet (this block is HTTPS-only, so unconditional # `always` is correct here). 2 Jahre, alle Subdomains, Preload. add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; # Frame-Einbettung verbieten (Clickjacking-Schutz): add_header X-Frame-Options "DENY" always; # MIME-Sniffing deaktivieren (nosniff): add_header X-Content-Type-Options "nosniff" always; # Referrer-Politik: keine sensiblen Daten im Referrer nach außen. add_header Referrer-Policy "strict-origin-when-cross-origin" always; # --- gzip (Komprimierung) ---------------------------------------------- gzip on; gzip_vary on; gzip_min_length 1024; gzip_comp_level 6; gzip_proxied any; gzip_types text/plain text/css text/javascript application/javascript application/json application/xml image/svg+xml font/woff2; # --- Proxy to the Next.js standalone server (node server.js) ------------ # Alles andere wird an den App-Container auf Port 3000 durchgereicht. location / { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; # Next.js braucht die Original-Host-Header und WebSocket-Support (dev/WS). proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Wichtig: X-Forwarded-Proto wird gesetzt, damit die App weiß, dass der # Client über HTTPS kommt (die App emittiert HSTS dann korrekt). proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; proxy_read_timeout 60s; proxy_connect_timeout 5s; } }