Erste Produktivversion
This commit is contained in:
130
docs/reverse-proxy.md
Normal file
130
docs/reverse-proxy.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Betrieb hinter dem Reverse Proxy (OPNsense)
|
||||
|
||||
Aufbau: Browser → HTTPS → nginx auf der OPNsense → HTTP → Port 46600 auf dem
|
||||
Docker-Host → nginx im `web`-Container → `api`.
|
||||
|
||||
## Drei Werte in der `.env`
|
||||
|
||||
```
|
||||
PUBLIC_BASE_URL=https://einkauf.example.de
|
||||
COOKIE_SECURE=true
|
||||
FORWARDED_ALLOW_IPS=<IP des Docker-Hosts oder *>
|
||||
```
|
||||
|
||||
Nach jeder Änderung `docker compose up -d`, nicht `restart` – ein Neustart
|
||||
übernimmt keine geänderten Umgebungswerte.
|
||||
|
||||
**`PUBLIC_BASE_URL`** landet in jeder Verifikations-, Einladungs- und
|
||||
Passwort-Reset-Mail. Steht dort noch `http://…:46600`, führen die Links ins
|
||||
Leere, sobald jemand von außen klickt.
|
||||
|
||||
**`COOKIE_SECURE=true`** ist ab jetzt Pflicht, aber auch erst ab jetzt möglich:
|
||||
Ein `Secure`-Cookie über reines HTTP wird vom Browser verworfen, und die
|
||||
Anmeldung scheitert dann ohne verwertbare Meldung. Beides muss gleichzeitig
|
||||
umgestellt werden.
|
||||
|
||||
## Was die OPNsense weiterreichen muss
|
||||
|
||||
Im nginx-Plugin unter *Http(s) → Location* für den Upstream:
|
||||
|
||||
```
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
```
|
||||
|
||||
`X-Forwarded-Proto` ist der wichtigste Wert: Ohne ihn hält die Anwendung die
|
||||
Verbindung für unverschlüsselt.
|
||||
|
||||
Zusätzlich für Server-Sent Events (ab Phase 4):
|
||||
|
||||
```
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 3600s;
|
||||
```
|
||||
|
||||
Ohne `proxy_buffering off` sammelt nginx die Ereignisse und gibt sie erst
|
||||
gebündelt weiter – die Liste aktualisiert sich dann mit Verzögerung oder gar
|
||||
nicht.
|
||||
|
||||
## Client-IP fürs Rate Limiting
|
||||
|
||||
Mit zwei Proxys davor sieht die Anwendung ohne Weiterreichung nur die IP des
|
||||
`web`-Containers. Dann würde eine einzige IP das Limit für alle auslösen.
|
||||
`X-Forwarded-For` wird durch beide Stufen durchgereicht; uvicorn wertet es
|
||||
wegen `--proxy-headers` aus.
|
||||
|
||||
`FORWARDED_ALLOW_IPS=*` ist vertretbar, solange der `api`-Container keinen
|
||||
veröffentlichten Port hat – dann kann den Header nur der `web`-Container
|
||||
setzen. Sobald du den API-Port für Debugging freigibst, gehört dort die
|
||||
konkrete IP hinein.
|
||||
|
||||
## PWA-Installation
|
||||
|
||||
Erst über HTTPS bietet der Browser „Zur Startseite hinzufügen" an und lässt den
|
||||
Service Worker zu. Bei iOS ist die Installation zusätzlich Voraussetzung dafür,
|
||||
dass Web Push in Phase 8 überhaupt funktioniert – im normalen Safari-Tab gibt
|
||||
es keine Benachrichtigungen.
|
||||
|
||||
## Prüfen
|
||||
|
||||
```bash
|
||||
curl -sI https://einkauf.example.de/ | grep -i strict-transport
|
||||
curl -s https://einkauf.example.de/readyz
|
||||
```
|
||||
|
||||
Und nach der Anmeldung im Browser unter *Entwicklerwerkzeuge → Anwendung →
|
||||
Cookies*: `ea_session` muss `Secure` und `HttpOnly` tragen, `ea_csrf` nur
|
||||
`Secure`.
|
||||
|
||||
|
||||
## Kopfzeilen: was aus dem Protokoll hervorgeht
|
||||
|
||||
Eine Auswertung der tatsächlich ausgelieferten Antworten hat drei Punkte
|
||||
ergeben, die auf der OPNsense-Seite liegen.
|
||||
|
||||
### `X-XSS-Protection: 1` entfernen
|
||||
|
||||
Die OPNsense setzt diese Kopfzeile. Sie ist überholt: Chrome hat den
|
||||
zugehörigen Filter entfernt, und in den Browsern, die ihn noch kennen, kann er
|
||||
in Einzelfällen selbst Lücken aufreißen, indem er Teile der Seite unterdrückt.
|
||||
Der Wert `1` ohne `mode=block` ist dabei die ungünstigste Variante.
|
||||
|
||||
Empfehlung: im nginx-Plugin abschalten oder auf `0` setzen. Der Schutz kommt
|
||||
von der Content-Security-Policy, nicht von dieser Kopfzeile.
|
||||
|
||||
### HSTS: `preload` passt nicht zur Laufzeit
|
||||
|
||||
```
|
||||
strict-transport-security: max-age=15768000; includeSubDomains; preload
|
||||
```
|
||||
|
||||
Das sind gut sechs Monate. Für die Aufnahme in die Preload-Liste verlangen die
|
||||
Browserhersteller mindestens ein Jahr (`31536000`); mit dem jetzigen Wert wird
|
||||
das `preload` schlicht ignoriert.
|
||||
|
||||
Entweder auf `31536000` erhöhen – dann aber im Bewusstsein, dass ein Eintrag in
|
||||
der Preload-Liste die Domain für alle Unterdomains dauerhaft auf HTTPS
|
||||
festlegt und sich nur mit Monaten Vorlauf rückgängig machen lässt – oder das
|
||||
`preload` weglassen.
|
||||
|
||||
### Doppelte Kopfzeilen
|
||||
|
||||
`X-Content-Type-Options`, `X-Frame-Options` und `Permissions-Policy` kommen
|
||||
jeweils zweimal an: einmal vom `web`-Container, einmal von der OPNsense. Das
|
||||
ist unschädlich, aber unnötig.
|
||||
|
||||
Wichtiger ist, was daraus folgt: **Eine doppelte
|
||||
`Content-Security-Policy` wäre nicht unschädlich.** Browser wenden dann beide
|
||||
an und lassen nur zu, was in *beiden* erlaubt ist. Eine zusätzliche CSP auf der
|
||||
OPNsense könnte die Anwendung also lahmlegen, ohne dass eine Fehlermeldung
|
||||
darauf hinweist – sichtbar wäre nur, dass Module oder Stile nicht laden.
|
||||
Derzeit setzt die OPNsense keine, das sollte so bleiben.
|
||||
|
||||
Die OPNsense überschreibt außerdem `Referrer-Policy` mit `same-origin` statt
|
||||
des strengeren `no-referrer` aus dem Container. Unkritisch, solange die
|
||||
Anwendung nicht nach außen verlinkt – für die öffentlichen Listenlinks unter
|
||||
`/s/<token>` bedeutet es, dass der Token nur an die eigene Herkunft übertragen
|
||||
würde. Wer es strenger mag, stellt die OPNsense auf `no-referrer` um.
|
||||
Reference in New Issue
Block a user