docs: Installationsanleitung fuer vorhandenes Docker und externen Proxy

Die Anleitung setzt jetzt voraus, was ohnehin da ist: ein Server mit
laufendem Docker und ein Reverse Proxy, der schon steht - auch auf einem
anderen Rechner. Docker-Installation und Betriebssystemhaertung sind
raus, dafuer ist der Proxy-Abschnitt der ausfuehrlichste geworden.

Damit ein externer Proxy die Anwendung ueberhaupt erreicht, war eine
Aenderung noetig: Die Bindeadresse stand fest auf 127.0.0.1. Jetzt gibt
es APP_BIND, Standard bleibt die Loopback-Adresse.

Der Weg dorthin ist nach Vorzug sortiert - privates Netz, dann
WireGuard, dann als Notloesung oeffentliche Adresse mit Firewallregel.
Zwischen Proxy und Anwendung laeuft unverschluesseltes HTTP; das gehoert
nicht ins offene Netz. Deshalb steht in der Kontrolle eine Gegenprobe,
die fehlschlagen MUSS: Antwortet der Anwendungsport von aussen, ist die
Firewallregel falsch.

Fertige Konfigurationen fuer Caddy, nginx und Traefik, jeweils mit den
Werten, die sonst uebersehen werden:

- Zeitgrenze 10 Minuten - sonst brechen Belegmappe, Rechnungslegung und
  Vollexport mitten im Erzeugen ab. Die Standardwerte von nginx liegen
  bei 60 Sekunden.
- Antwort nicht puffern - der Vollexport kommt als Strom. Puffert der
  Proxy, wartet der Browser, bis alles im Speicher des Proxys liegt, und
  laeuft dabei in die Zeitgrenze.
- Und der Hinweis, dass der Proxy KEINE eigene CSP setzen darf: Sie
  enthaelt eine Nonce, die pro Anfrage neu ist. Eine feste CSP blockiert
  die eigenen Skripte - die Seite laedt dann, laesst sich aber nicht
  bedienen.

Der A-Record zeigt auf den Proxy-Server, nicht auf den Anwendungsserver.
Stand vorher falsch da.

Etappe: 8
Phase: Haertung
This commit is contained in:
Florian Wilde
2026-08-22 11:00:29 +02:00
parent 89bc5fe5a8
commit c35fc16133
4 changed files with 411 additions and 285 deletions
+31 -47
View File
@@ -51,7 +51,7 @@ eines Angehörigen: Fristen, Vorgänge, Belege, Vermögen, Sozialhilfe, Berichte
| Wofür | Wo |
|---|---|
| **Einen Server aufsetzen** — von der leeren Maschine bis zur laufenden Anwendung, mit Kontrolle nach jedem Schritt | [`docs/INSTALLATION.md`](docs/INSTALLATION.md) |
| **Die Anwendung aufsetzen** — auf einem Server, der Docker und einen Proxy schon hat, mit Kontrolle nach jedem Schritt | [`docs/INSTALLATION.md`](docs/INSTALLATION.md) |
| **Täglich damit arbeiten** — die zwölf Handgriffe, der Jahresrhythmus, was tun wenn etwas schiefgeht | [`docs/ANLEITUNG.md`](docs/ANLEITUNG.md), in der Anwendung unter `/anleitung` |
| **Wissen, was sich ändert** — was neu ist, was du vor einem Update beachten musst | [`CHANGELOG.md`](CHANGELOG.md) |
@@ -188,61 +188,45 @@ Backup anstoßen** (siehe unten) — eine Migration lässt sich nicht zurückrol
## Reverse Proxy
Die Anwendung erwartet, dass der Proxy TLS beendet und die echte Client-IP
weiterreicht. Sie liest `X-Forwarded-For` bzw. `X-Real-IP` für das
Rate-Limiting am Login und für das Audit-Log (dort nur als Hash).
Die Anwendung **beendet selbst kein TLS** und bringt keinen Proxy mit. Sie
horcht standardmäßig auf `127.0.0.1:3000`.
**Caddy**
**Läuft der Proxy auf demselben Server**, ist damit alles gesagt: Er zeigt
auf `127.0.0.1:3000`, fertig.
```caddyfile
cockpit.example.de {
encode zstd gzip
reverse_proxy 127.0.0.1:3000
**Läuft er auf einem anderen Server**, setz in der `.env` die Adresse, über
die er herankommt — am besten die eines privaten Netzes oder eines
WireGuard-Tunnels:
# Uploads: Fotos von Behördenschreiben können groß sein.
request_body {
max_size 30MB
}
}
```bash
APP_BIND=10.0.0.5
APP_PORT=3000
```
**nginx**
`0.0.0.0` nur, wenn eine Firewall den Port ausschließlich für die IP des
Proxy-Servers öffnet. Zwischen Proxy und Anwendung läuft unverschlüsseltes
HTTP — dieser Weg darf nie durchs offene Netz gehen.
```nginx
server {
listen 443 ssl http2;
server_name cockpit.example.de;
### Was der Proxy leisten muss
ssl_certificate /etc/letsencrypt/live/cockpit.example.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cockpit.example.de/privkey.pem;
| Anforderung | Was ohne sie passiert |
|---|---|
| TLS beenden | Der Browser lehnt Cookies mit `Secure`-Kennzeichen ab, die Anmeldung schlägt stumm fehl |
| `Host` durchreichen | Auth.js vergleicht ihn mit `AUTH_URL` und weist die Anmeldung ab |
| `X-Forwarded-Proto: https` | Die Anwendung baut Adressen mit `http` |
| `X-Forwarded-For` | Die Login-Sperre zählt alle Zugriffe als denselben Anschluss |
| Uploads ≥ 30 MB | Fotos mehrseitiger Bescheide werden abgeschnitten |
| Zeitgrenze ≥ 10 Minuten | Belegmappe, Rechnungslegung und Vollexport brechen ab |
| Antwort nicht puffern | Der Vollexport kommt als Strom |
client_max_body_size 30M;
**Keine eigenen Sicherheitskopfzeilen setzen.** CSP, `X-Frame-Options`,
`nosniff` und HSTS kommen aus der Anwendung. Die CSP enthält eine Nonce,
die pro Anfrage neu ist — eine feste CSP aus dem Proxy blockiert die
eigenen Skripte.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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 Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
**Traefik** (Labels am `app`-Dienst)
```yaml
labels:
- traefik.enable=true
- traefik.http.routers.cockpit.rule=Host(`cockpit.example.de`)
- traefik.http.routers.cockpit.tls.certresolver=letsencrypt
- traefik.http.services.cockpit.loadbalancer.server.port=3000
```
In allen Fällen muss `AUTH_URL` in der `.env` auf die öffentliche Adresse
zeigen (`https://cockpit.example.de`), sonst schlägt die Anmeldung fehl.
**Fertige Konfigurationen** für Caddy, nginx und Traefik, mit Zeitgrenzen
und den Gegenproben, stehen in
[`docs/INSTALLATION.md`](docs/INSTALLATION.md), Schritt 7.
---