Document authenticated deployment

This commit is contained in:
Julian Appel 2026-09-28 22:42:11 +02:00
parent 847f4e9493
commit 3670f02ccd
9 changed files with 97 additions and 20 deletions

View file

@ -7,10 +7,9 @@ Zugang wird von einem Administrator über **Config → Benutzer** angelegt.
Es gibt genau zwei Rollen (`src/shared/constants/user-roles.ts`):
- **admin** – wie `user`, zusätzlich Zugriff auf den Config-Bereich
(Benutzerverwaltung, eigenes Konto)
- **admin** – wie `user`, zusätzlich Zugriff auf **Config → Benutzer**
- **user** – volle Nutzung der Leistungsbilanz (Projekte, Stromkreislisten,
Grobbilanz, …), kein Zugriff auf Config
Grobbilanz, …) und Zugriff auf das eigene Konto, keine Benutzerverwaltung
Es gibt (bewusst, siehe unten) keine konfigurierbare Rechte-Matrix und keine
Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle.
@ -44,6 +43,8 @@ Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle.
- **Frontend:** `AuthProvider` lädt `/api/auth/me`; `LoginGate`
(`src/app/layout.tsx`) zeigt Login- bzw. Passwortformular, solange nicht
angemeldet bzw. `mustChangePassword` gesetzt ist, sonst die App.
**Konto** ist für jede angemeldete Person erreichbar; **Benutzer** nur für
Administratoren.
## Schutzmechanismen
@ -52,12 +53,12 @@ Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle.
| Passwort-Raten (online) | `LoginThrottle`: 5 Fehlversuche je Anmeldename in 15 min → HTTP 429 bis das Fenster abläuft. Schlüssel ist der Name, nicht die IP (alle Anfragen kommen über den Next-Rewrite mit derselben Quell-IP). |
| Passwort-Raten (offline, DB-Diebstahl) | scrypt (N=16384, r=8, p=1), eigenes Salt je Hash |
| Nutzernamen ausspähen (Timing) | Unbekannte Namen werden gegen einen festen Dummy-Hash geprüft – immer genau ein scrypt-Lauf; einheitliche Fehlermeldung |
| CSRF | SameSite=Lax **plus** `requireSameOriginForWrites`: schreibende Anfragen nur mit `Sec-Fetch-Site: same-origin`. Nötig, weil andere Dienste auf demselben Host (andere Ports) als „same-site“ gelten |
| CSRF über reines HTTP | Browser senden `Sec-Fetch-Site` nur an HTTPS-Adressen. Beim Zugriff per `http://…` im LAN greift deshalb nur SameSite=Lax (schützt gegen fremde Domains, nicht gegen andere Ports desselben Hosts). |
| CSRF | SameSite=Lax **plus** `requireSameOriginForWrites`: jeder Schreibzugriff braucht `X-Leistungsbilanz-Request: 1`; fremde HTML-Formulare können den Header nicht setzen und ein fremder Fetch scheitert an der notwendigen CORS-Preflight-Anfrage. Ein vorhandener `Sec-Fetch-Site`-Header wird zusätzlich geprüft. |
| CSRF über reines HTTP | Der eigene Anwendungsheader gilt auch dann, wenn Browser im HTTP-LAN keinen `Sec-Fetch-Site`-Header senden. Skripte und manuelle API-Aufrufe müssen ihn bei `POST`, `PUT`, `PATCH` und `DELETE` ausdrücklich mitsenden. |
| Session-Fixation | Neue Session-ID bei jedem Login und nach jedem Passwortwechsel |
| Gestohlenes Cookie | httpOnly; gleitende Laufzeit (`AUTH_SESSION_HOURS`, 12 h) **und** harte Obergrenze (`AUTH_SESSION_MAX_DAYS`, 7 Tage) |
| Kompromittiertes Konto | Passwort-Reset und Deaktivieren beenden sofort alle Sessions des Kontos; Passwortwechsel beendet alle anderen |
| Aussperren | Der letzte aktive Admin kann weder herabgestuft noch deaktiviert werden; eigener Zugang ist in der Liste gesperrt |
| Aussperren | Der letzte aktive Admin kann weder herabgestuft noch deaktiviert werden; Prüfung und Änderung laufen atomar in einer SQLite-Transaktion. Der eigene Zugang ist in der Liste gesperrt. |
| Rechteausweitung / Mass Assignment | Rolle kommt aus der DB, nie aus der Anfrage; zod-Schemas mit `.strict()` |
| DoS über riesige Passwörter | Längengrenzen (Name 100, Passwort 200 Zeichen) vor scrypt |
| Look-alike-Namen | Anmeldename nur `a-z 0-9 . _ @ -`, klein geschrieben |
@ -77,6 +78,18 @@ ignoriert; jeder weitere Zugang entsteht über Config → Benutzer. Die Werte
gehören in eine lokale, nicht versionierte `.env` auf dem Deploy-Host, nicht
in `compose.yaml` selbst.
```dotenv
AUTH_BOOTSTRAP_USERNAME=admin
AUTH_BOOTSTRAP_PASSWORT=<eigenes Startpasswort mit mindestens 12 Zeichen>
AUTH_SESSION_HOURS=12
AUTH_SESSION_MAX_DAYS=7
COOKIE_SECURE=false
```
Bei Zugriff über HTTPS muss `COOKIE_SECURE=true` gesetzt werden. Bei direktem
HTTP-Zugriff im LAN bleibt es `false`, da der Browser das Session-Cookie sonst
nicht speichert.
## Bewusst nicht umgesetzt (Stand jetzt)
- Konfigurierbare Rechte-Matrix statt fixer `admin`/`user`-Rollen

View file

@ -33,6 +33,15 @@ mit Hot Reload. Das Frontend leitet `/api/*` über `API_INTERNAL_URL` an die API
weiter; beim gebauten Stand wird das Ziel zur Build-Zeit festgelegt. Die
Datenbank liegt über einen Host-Mount außerhalb des Containers.
Vor allen fachlichen `/api/*`-Routen liegt die Cookie-basierte Anmeldung aus
`src/server/auth`. Nur Login, Status und Logout sind ohne Session erreichbar.
Schreibzugriffe verlangen zusätzlich den Anwendungsheader
`X-Leistungsbilanz-Request: 1`, damit der Schutz auch beim HTTP-Zugriff im LAN
ohne Fetch-Metadata-Header greift. `/api/users/*` ist Administratoren
vorbehalten. Konten sind globale Betriebsdaten und gehören nicht zu
Projektkommandos, Snapshots oder Projekttransfers. Sessions liegen im Speicher
des einzelnen API-Prozesses und enden bei dessen Neustart.
Neue IDs für Stromkreise, Gerätezeilen, Gruppen und Verteilerkomponenten erzeugt
der Editor über `src/frontend/utils/client-id.ts` (`createClientId`). Die
Hilfsfunktion nutzt `crypto.randomUUID()`, sofern verfügbar, andernfalls
@ -57,6 +66,11 @@ vor dem Command und bleiben über Undo/Redo unverändert.
- `src/frontend/components/circuit-grid-*.ts` – reine Grid-Projektion,
Zellbesitz, Einfügen und Sicherheitsregeln
- `src/frontend/utils/api.ts` – typisierte Frontend-API-Aufrufe
- `src/frontend/components/AuthProvider.tsx` und `LoginGate.tsx` – Anmeldung
und erzwungener Wechsel eines Startpassworts
- `src/server/auth/` – Passwort-Hashing, Sessions und Zugriffsschutz
- `src/domain/services/user.service.ts` und `src/domain/ports/user.store.ts` –
kontobezogene Regeln und schmale Persistenzgrenze
- `src/server/index.ts` und `src/server/routes/` – API-Komposition
- `src/domain/services/` – fachliche Command- und Synchronisierungsregeln
- `src/server/composition/` – Verdrahtung fachlicher Services mit konkreten

View file

@ -16,10 +16,10 @@ und macOS keine inotify-Events durchreichen; das kostet dauerhaft CPU, auch
ohne Benutzeraktivität. Dieser Stack gehört deshalb nur auf einen
Entwicklungsrechner.
Beide Stacks enthalten weder TLS, Authentifizierung, einen vorgeschalteten
Reverse Proxy noch ein zentral betriebenes Datenbanksystem. Die oben genannten
Container-Schutzmaßnahmen ersetzen diese Funktionen nicht. Der Stack darf
deshalb nicht öffentlich erreichbar gemacht werden.
Beide Stacks enthalten Anmeldung mit festen Rollen, aber weder TLS, einen
vorgeschalteten Reverse Proxy noch ein zentral betriebenes Datenbanksystem.
Passwörter und Session-Cookies werden bei reinem HTTP unverschlüsselt
übertragen. Der Stack darf deshalb nicht öffentlich erreichbar gemacht werden.
## Topologie
@ -29,9 +29,10 @@ deshalb nicht öffentlich erreichbar gemacht werden.
| Express API | 3000 | nicht veröffentlicht (Server) / `127.0.0.1:3000` (Dev) | `GET /health` | `./data:/app/data` |
| SQLite | Datei | – | Integritäts-/FK-Prüfung via Backup und Skript | `data/leistungsbilanz.db` |
Die Express-API wird im Server-Stack nicht mehr auf dem Host veröffentlicht
(`expose` statt `ports`). Sie hat keine Authentifizierung; jeder, der sie
erreicht, kann jedes Projekt lesen und ändern. Erreichbar ist sie nur aus dem
Die Express-API wird im Server-Stack nicht auf dem Host veröffentlicht
(`expose` statt `ports`). Alle fachlichen API-Routen verlangen eine aktive
Session; Schreibzugriffe zusätzlich den Anwendungsheader
`X-Leistungsbilanz-Request: 1`. Erreichbar ist die API nur aus dem
Compose-Netz, praktisch also über den Next.js-Rewrite. Im Entwicklungs-Stack
hängen beide Ports am Loopback und sind nicht aus dem LAN erreichbar.
@ -72,6 +73,26 @@ Verwendete Umgebungsvariablen:
JSON-Loggers (`error`, `warn`, `info`, `verbose`, `debug`), Standard `info`.
Setzbar über eine `.env`-Datei neben `compose.yaml` oder
`LOG_LEVEL=verbose docker compose up`.
- `AUTH_BOOTSTRAP_USERNAME` und `AUTH_BOOTSTRAP_PASSWORT` – erzeugen nur bei
leerer Benutzertabelle den ersten Administrator; das Startpasswort hat
mindestens 12 Zeichen.
- `AUTH_SESSION_HOURS` – Inaktivitätsgrenze einer Session, Standard 12 Stunden.
- `AUTH_SESSION_MAX_DAYS` – absolute Session-Laufzeit, Standard 7 Tage.
- `COOKIE_SECURE` – hinter HTTPS `true`; beim direkten HTTP-Zugriff im LAN
`false`, da Browser das Cookie sonst nicht speichern.
Vor dem ersten Start müssen mindestens die Bootstrap-Werte in einer lokalen,
nicht versionierten `.env` neben `compose.yaml` gesetzt werden:
```dotenv
AUTH_BOOTSTRAP_USERNAME=admin
AUTH_BOOTSTRAP_PASSWORT=<eigenes Startpasswort mit mindestens 12 Zeichen>
```
Nach dem ersten erfolgreichen Start werden diese Werte ignoriert, solange
mindestens ein Benutzer existiert. Weitere Zugänge legt ein Administrator in
**Config → Benutzer** an. Details stehen in
[Anmeldung und Benutzerverwaltung](authentication.md).
Beim API-Start laufen zuerst die Migrationen und die Schemaprüfung
(`scripts/run-migrations.js` und `scripts/db-verify-circuit-schema.js`, im
@ -137,7 +158,8 @@ bleibt `sh` der Vordergrundprozess, und `SIGTERM` erreicht Node nie.
Vor einer produktiven Installation werden mindestens benötigt:
- TLS-Termination und Reverse Proxy
- Authentifizierung, Autorisierung und Benutzer-/Rollenmodell
- zentraler Identitätsanbieter oder eine feinere Rechte-Matrix, falls die zwei
festen Rollen später nicht ausreichen
- definierte Secrets- und Konfigurationsverwaltung
- persistenter, gesicherter Datenbankbetrieb
- kontrollierter einmaliger Migrationsschritt vor dem API-Rollout