-
Notifications
You must be signed in to change notification settings - Fork 0
IT Reverse Proxy und HTTPS
Caution
Diese Seite richtet sich ausschliesslich an Informatikerinnen und Informatiker. Nicht für Laien geeignet: Eine falsche Proxy-Konfiguration kann Anmeldungen verhindern, Rate-Limits und IP-Filter aushebeln oder Konfigurationsdateien öffentlich machen. Ohne Fachkenntnisse bitte den Bereich Installation Schritt für Schritt verwenden.
Für wen ist diese Seite? Für Informatikerinnen und Informatiker, die SmallTime 2027 per HTTPS erreichbar machen. Node.js terminiert kein TLS; ein Reverse-Proxy (nginx, Caddy, Plesk, IIS …) nimmt HTTPS an und leitet an http://127.0.0.1:55000 weiter.
- Anforderungen an den Proxy
- Einstellungen in SmallTime
- nginx
- Caddy
- Plesk
- Cloudflare und Länder-Header
- HSTS
- Content-Security-Policy
- Prüfen
- TLS-Terminierung, HTTP → HTTPS-Weiterleitung.
-
Ein Proxy-Hop vor Node.js (
trust proxy = 1). Bei mehreren Hops (CDN → nginx → Node) muss der letzte Proxy die echte Client-IP inX-Forwarded-Foreinsetzen, siehe Cloudflare. -
X-Forwarded-ForundX-Forwarded-Protosetzen;Hostdurchreichen (Lizenzprüfung nutzt den Hostnamen der Anfrage). -
WebSocket-Upgrade für
/api/live(lange Leerlaufzeiten, Heartbeat alle 30 s). - Request-Body bis ca. 50 MB (10 Dateien à 5 MB pro Upload).
- Keine eigene Content-Security-Policy setzen oder überschreiben; die App setzt sie mit Nonce pro Anfrage.
- Port 55000 nicht öffentlich erreichbar machen (Bind auf localhost bzw. Firewall).
.env:
NODE_ENV=production
FRONTEND_ORIGIN=https://zeit.example.ch
TRUST_PROXY=true
SECURE_COOKIE=trueconfig/security.json: "allowLocalhost": false. Länderfilter (allowedCountries) nur aktivieren, wenn der Proxy/CDN zuverlässig einen Länder-Header setzt.
| Einstellung | Warum |
|---|---|
FRONTEND_ORIGIN |
exakt die öffentliche Origin. Andere Origins → 403 E1006. Bestimmt die Lizenz-Domain. |
TRUST_PROXY=true |
request.ip = Client-IP aus X-Forwarded-For (für Rate-Limits, Kontosperre-Protokoll, IP-Filter), request.secure aus X-Forwarded-Proto. Ohne diese Einstellung sehen alle Anfragen wie 127.0.0.1 aus. |
SECURE_COOKIE=true |
Cookies immer Secure, HSTS immer – unabhängig davon, ob der Proxy X-Forwarded-Proto korrekt setzt. |
Warning
TRUST_PROXY=true ohne vorgeschalteten Proxy erlaubt Clients, ihre IP per X-Forwarded-For zu fälschen. Nur setzen, wenn Node.js ausschliesslich über den Proxy erreichbar ist.
# /etc/nginx/sites-available/smalltime.conf (Beispiel)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream smalltime {
server 127.0.0.1:55000;
keepalive 16;
}
server {
listen 80;
listen [::]:80;
server_name zeit.example.ch;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name zeit.example.ch;
ssl_certificate /etc/letsencrypt/live/zeit.example.ch/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/zeit.example.ch/privkey.pem;
client_max_body_size 55m;
# WebSocket für Live-Updates
location = /api/live {
proxy_pass http://smalltime;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}
location / {
proxy_pass http://smalltime;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
}Hinweise:
-
X-Forwarded-For $remote_addrersetzt einen vom Client mitgeschickten Header; mit$proxy_add_x_forwarded_forfunktioniert es ebenfalls (Express nimmt beitrust proxy = 1den letzten Eintrag), der ersetzende Wert ist aber eindeutiger. - Keine
add_header Content-Security-Policyund kein zusätzlichesStrict-Transport-Securityim Proxy; beides setzt die App. -
gzipim Proxy ist nicht nötig, wennENABLE_COMPRESSION=true. Wird im Proxy komprimiert,ENABLE_COMPRESSION=falsesetzen und/api/authsowie/api/csrf-tokenvon der Kompression ausnehmen (BREACH).
Caddy holt Zertifikate automatisch, setzt X-Forwarded-For/X-Forwarded-Proto und reicht WebSocket-Upgrades ohne Zusatzkonfiguration durch.
# /etc/caddy/Caddyfile (Beispiel)
zeit.example.ch {
request_body {
max_size 55MB
}
reverse_proxy 127.0.0.1:55000
}Caddy ersetzt X-Forwarded-For standardmässig durch die Client-IP, solange keine trusted_proxies konfiguriert sind. Das passt zu trust proxy = 1.
Plesk startet die App über Phusion Passenger (app.cjs), nginx terminiert TLS. Einrichtung gemäss README.md im Repository:
- Anwendungsstamm
/smalltime, Dokumentstamm/smalltime/public, Startdateiapp.cjs, Modusproduction, Node.js 26.9.0. -
.envoder Plesk-Umgebungsvariablen:FRONTEND_ORIGIN,SESSION_SECRET,SECURE_COOKIE=true,TRUST_PROXY=true.PORTsetzt Passenger. - «NPM install», danach «App neu starten».
Caution
Der Dokumentstamm muss auf public zeigen. Zeigt er auf den Anwendungsstamm, liefert nginx .env, config/ und data/ direkt aus.
Note
Die .htaccess-Sperre für .js/.css aus allgemeinen Plesk-Anleitungen nicht verwenden, sonst lädt das Frontend nicht. Ob WebSockets (/api/live) auf dem jeweiligen Plesk/Passenger durchgereicht werden, hängt von der Hosting-Konfiguration ab. Ohne WebSocket funktioniert die App, Änderungen anderer Personen erscheinen dann erst nach dem Neuladen.
Der Länderfilter (allowedCountries in security.json) liest den Ländercode aus einem der countryHeaders, z. B. CF-IPCountry von Cloudflare oder CloudFront-Viewer-Country von AWS CloudFront. Die Header werden nur mit TRUST_PROXY=true ausgewertet.
Bei Cloudflare → nginx → Node.js sind es zwei Hops. Damit request.ip die echte Client-IP ist, muss nginx sie aus Cloudflare übernehmen:
# in http{} oder server{}: offizielle Cloudflare-Bereiche (https://www.cloudflare.com/ips/)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# … alle weiteren IPv4-/IPv6-Bereiche von Cloudflare …
real_ip_header CF-Connecting-IP;
# in den location-Blöcken wie oben:
proxy_set_header X-Forwarded-For $remote_addr; # jetzt = echte Client-IPWarning
- Ist der Ursprungsserver auch direkt (an Cloudflare vorbei) erreichbar, kann ein Client
CF-IPCountryselbst senden. Eingehenden Verkehr auf Port 443 auf die Cloudflare-Bereiche beschränken (Firewall oder Authenticated Origin Pulls), oder den Header im nginx nur für Cloudflare-Quellen durchreichen. - Ohne Länder-Header und mit nicht leerer
allowedCountrieswird jede externe Anfrage mit 403 abgelehnt, auch/api/health. -
allowedIpRanges/blockedIpRangeswirken unabhängig vom Länderfilter, siehe Konfiguration.
Die App setzt Strict-Transport-Security: max-age=31536000; includeSubDomains, wenn die Anfrage als sicher gilt: request.secure (mit TRUST_PROXY aus X-Forwarded-Proto: https) oder SECURE_COOKIE=true.
-
includeSubDomainsgilt für alle Unterdomains des aufgerufenen Hosts (beizeit.example.chalso*.zeit.example.ch, nichtexample.ch). - Kein
preload. Wer HSTS-Preload will, setzt den Header im Proxy und muss dann die App-Variante akzeptieren (zwei Header vermeiden: im Proxyproxy_hide_header Strict-Transport-Security;). - Über reines HTTP ignorieren Browser den Header.
Die App setzt die CSP per Helmet, mit einer Nonce pro Anfrage für Styles (Emotion/MUI). Der Platzhalter __CSP_NONCE__ in index.html wird beim Ausliefern ersetzt.
default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self';
frame-src 'self' blob:; object-src 'none'; script-src 'self';
style-src 'self' https://fonts.googleapis.com 'nonce-…';
font-src 'self' https://fonts.gstatic.com data:; img-src 'self' data: blob:;
connect-src 'self'; media-src 'self'; manifest-src 'self';
script-src-attr 'none'
(script-src-attr stammt aus den Helmet-Standards. upgrade-insecure-requests ist bewusst abgeschaltet, damit der Betrieb über http:// im internen Netz funktioniert.)
-
frame-src blob:ist für die PDF-Vorschau nötig (iframe mitblob:-URL). Eine Proxy-CSP ohneblob:bricht die Vorschau. - Betrieb über reines
http://im internen Netz ist möglich (FRONTEND_ORIGINauf die interne Adresse setzen). Für den Zugriff aus dem Internet immer HTTPS über einen Reverse-Proxy verwenden. - Weitere Header:
X-Frame-Options: SAMEORIGIN,Referrer-Policy: strict-origin-when-cross-origin,Cross-Origin-Opener-Policy: same-origin,Cross-Origin-Resource-Policy: same-origin,Permissions-Policy: camera=(), microphone=(), geolocation=(), keinX-Powered-By.
# Gesundheit (öffentlich, unterliegt Länder-/IP-Filter)
curl -s https://zeit.example.ch/api/health
# Header prüfen: CSP, HSTS, Cookies
curl -sI https://zeit.example.ch/ | grep -iE 'content-security|strict-transport'
# WebSocket-Upgrade (erwartet 101 mit gültiger Session; ohne Session wird die Verbindung abgewiesen,
# aber nicht mit 404 – 404 heisst: Proxy leitet /api/live nicht weiter)
curl -si -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' https://zeit.example.ch/api/live | head -1Im Browser: Anmelden, Entwicklertools → Netzwerk → live muss den Status 101 haben. Unter Administration → Protokoll → Anmeldungen muss die echte Client-IP erscheinen, nicht 127.0.0.1 oder die Proxy-IP.
Weiter mit: Sicherheit und Betrieb · Konfiguration · Docker
SmallTime 2027 · Version 1.0 · Zur Startseite · Fragen? Zuerst in den häufigen Fragen für Anwender oder Admins nachsehen.
👤 Anwender
🧑💼 Admins
- Erste Schritte
- Personen
- Arbeitsmodelle
- Ferien und Absenzen
- Saldo und Auszahlungen
- Monatsabschluss
- Gruppen und Rechte
- Stammdaten
- Einstellungen
- Terminals und Badges
- Protokoll und Sicherheit
- Häufige Fragen
🛠️ Einrichten
💻 Informatik (nur Fachleute)