dorfteich/docs/vs-nfd/50-haertungsleitfaden.md
Claude Fable 5 9b7acab294
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m21s
CI / Build container images (pull_request) Successful in 3m59s
CI / Auth e2e pack (pull_request) Successful in 8m35s
CI / Import/export fidelity gate (pull_request) Successful in 1m1s
CD / Build and push images (push) Successful in 17s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m17s
CD / Promote to Int (push) Successful in 12s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 8m27s
CI / Import/export fidelity gate (push) Successful in 58s
CI / Lint, typecheck, test (push) Successful in 6m30s
#232: plugin allowlist with SHA-256 hash pinning
The install path records the SHA-256 of the delivered bundle ZIP
(plugins.bundle_hash; pre-#232 installs show it as unknown until
reinstalled). plugins.allowlist in instance_settings names permitted
ids with their pinned hashes: empty (default) = not enforced, existing
instances unchanged; non-empty = installs of unlisted or deviating
bundles are rejected (plugin_not_pinned / plugin_hash_mismatch, 403),
and an installed plugin outside the list or with a deviating hash does
not load — absent from pond mount lists, frame/assets 404. Every
rejection is audited (plugin.rejected, catalogue v1.5). A version bump
changes the hash and therefore requires an explicit re-pin — the
intended friction (ADR 0025). Admin UI shows observed vs pinned hash
per plugin with pin/re-pin/unpin. Scope stated honestly in
plugin-architecture.md: the pin answers "is this the reviewed bundle";
post-install disk tampering is platform integrity (ADR 0019), sandbox
containment stays the sandbox's job. Hardening guide row + catalog
advisory triage; residual risk R-03 resolved. e2e: empty-allowlist
compatibility, pinned load, unpinned and tampered installs rejected and
audited, pin drift blocks loading while the admin still sees the
mismatch, version bump needs re-pin. Full api suite 101 files / 561
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
2026-07-31 21:26:36 +02:00

106 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Härtungsleitfaden — Referenzkonfiguration „VS-NfD-Betrieb" (Issue #227)
Zweck: **eine** benannte Konfiguration, die ein Betreiber als Ganzes
übernehmen kann. Jeder Eintrag nennt den exakten Schalter, den Wert und
das **Warum** — wer abweicht, tut es wissentlich. Der Leitfaden macht
zugleich die in M1/M2/M4 gebauten Schalter prüfbar.
**Pflegeregel (verbindlich):** Jeder PR, der einen neuen Instanz- oder
Deploy-Schalter einführt, ergänzt diesen Leitfaden **im selben PR** um
dessen Referenzwert. Ein Schalter ohne Leitfaden-Zeile gilt im Review
als unvollständig. (Gleiches Muster wie der Ereigniskatalog-Zaun #201.)
Geltungsbereich: Konfiguration der Anwendung. Die Härtung der Plattform
(Betriebssystem, Netz, Reverse Proxy, Datenträger) ist Betreibersache
(Abgrenzungserklärung `40-abgrenzungserklaerung.md` — von dort wird
hierher verwiesen; das IT-Grundschutz-Mapping
`80-grundschutz-mapping.md` nimmt diese Referenzkonfiguration als
Produkt-Beleg).
---
## 1 Referenzkonfiguration
### 1.1 Instanz-Settings (Site-Admin → Einstellungen; Tabelle `instance_settings`)
Nach jeder Änderung an Instanz-Settings die api neu starten — der
Settings-Cache ist in-process (operations.md).
| Setting | Referenzwert | Default | Warum |
| ----------------------------------------------------------------------------------------------------------- | --------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth.registrationMode` | `closed` | `open` | Konten entstehen in einer VS-Umgebung nur kontrolliert; Selbstregistrierung öffnet den Nutzerkreis unkontrolliert. |
| `api.enabled` | `false` | `false` | Public REST API ist ein zusätzlicher Egress-Kanal; ohne dokumentierten Bedarf bleibt er zu (404 auf allen `/api/public/v1`-Routen). |
| `mcp.enabled` | `false` | `false` | gleiches Argument für den MCP-Endpoint (`/api/mcp`); unabhängiger Schalter. |
| `feeds.enabled` | `false` | `true` | **explizit setzen** — Atom-Feeds liefern Inhalte an Reader außerhalb der Kontrolle der Instanz (Feed-Token umgehen die Session); Kopien in Feed-Readern sind nicht einholbar (Kopienliste, Sicherheitsdokumentation §5). Schaltet Routen UND Feed-Token-Verwaltung auf 404. |
| `plugins.enabled` | `false` | `true` | **explizit setzen** — kein Fremdcode in der VS-Zone (#200): alle Plugin-Flächen 404, Dropzone quarantänisiert; bestehende Blöcke degradieren zu ihrem deklarierten Text-Fallback. Der Kill-Switch deckt das Risiko für diesen Betriebsmodus vollständig; wer abweichend Plugins betreibt, nutzt zusätzlich das Hash-Pinning (`plugins.allowlist`, #232). |
| `plugins.allowlist` | leer (Plugins sind aus) | leer | Hash-Pinning-Allowlist (#232, ADR 0025): nicht-leer erzwingt, dass nur gepinnte Plugin-Ids mit exakt übereinstimmendem Bundle-SHA-256 installieren und laden (Abweisung auditiert `plugin.rejected`; Versionswechsel = explizites Re-Pin). In der Referenzkonfiguration bleibt sie leer, weil `plugins.enabled=false` das Risiko vollständig schließt; wer abweichend Plugins betreibt, pinnt GENAU die geprüften Bundles. |
| `classification.newPageDefault` | `vs_nfd` | `unclassified` | in einer VS-NfD-Instanz beginnt nichts unmarkiert (#204); die Vererbung (#205) hält den Baum konsistent. |
| `classification.uploadPolicy` | `block` | `warn` | Anhänge können die Kennzeichnung im Inhalt nicht tragen (#212) — die Referenzkonfiguration lehnt Uploads auf eingestufte Seiten serverseitig ab (403 `classified_upload_blocked`, #213) statt nur zu warnen. |
| `upload.svgPolicy` | `reject` | `sanitize` | SVG ist aktiver Inhalt; die Sanitisierung ist gut getestet, aber Ablehnen ist die kleinere Angriffsfläche. Abweichung vertretbar, wenn SVG gebraucht wird. |
| `upload.allowedExtensions` | nur das dienstlich Nötige (z. B. `pdf`) | Standardliste | jede zusätzliche Endung vergrößert die Menge nicht prüfbarer Binärformate im Bestand. Bilder sind davon unabhängig immer erlaubt (Magic-Byte-geprüft). |
| `backup.nextcloud.enabled` | `false` | `false` | „Backup nur lokal": kein Anwendungs-Upload von Restore-Sets zu Drittdiensten. Fernspiegel regelt ausschließlich die Deploy-Allowlist (1.2). |
| `trash.retentionDays`, `audit.retentionDays`, `conversion.payloadRetentionDays`, `mail.outboxRetentionDays` | Defaults (30/365/30/30) | ebd. | Aufbewahrung bewusst begrenzt; Verkürzung nach Betreiber-Löschkonzept zulässig (Betriebshandbuch §5). |
| `readTrail.enabled` | `true` | `false` | **explizit setzen** — der Lesetrail (#222#225) evidenziert Lesezugriffe auf eingestufte Seiten; Default aus, weil Lesebeobachtung mitbestimmungsrelevant ist. Einschalten NUR zusammen mit der Zweckbindung (Sicherheitsdokumentation §7); die api meldet die Schalterstellung beim Start. |
| `readTrail.dedupWindowMinutes` | Default (5) | `5` | Dedup-Fenster des Lesetrails (#223): je (Sitzung, Seite, Kanal) ein Ereignis pro Fenster — begrenzt die Ereignisflut einer Live-Sitzung auf ~12/h. Kleiner = feineres Protokoll und mehr Zeilen; Änderung mit dem Zweckbindungs-Dokument (#225) abstimmen. |
| `readTrail.retentionDays` | Default (365) | `365` | eigene Aufbewahrung des Lesetrails (#224), bewusst getrennt von `audit.retentionDays`; Löschläufe sind selbst auditiert (`read_trail.pruned`). Dauer mit der Zweckbindung (#225) und dem Betreiber-Löschkonzept abstimmen. |
| `idpMapping.rules` | Gruppen→Rollen der Behörde abbilden | `[]` | deklaratives Claim-Mapping (#217): IdP-Gruppen werden bei jedem OIDC-Login auf Teich-Rollen und das Site-Admin-Flag abgeglichen — sonst pflegt die Behörde Berechtigungen doppelt und die zweite Kopie driftet. Mapping fasst nur eigene Grants an (manuell gewinnt); Details: permissions.md §IdP claim mapping. |
| `legal.imprint`, `legal.privacyPolicy` | befüllt | leer | Betreiberpflicht; leere Seiten zeigen einen Warnbanner. |
### 1.2 Deploy-Konfiguration (`.env` / Compose — nur Plattformzugriff, bewusst nicht per Admin-UI)
| Variable | Referenzwert | Warum |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BACKUP_ALLOWED_TARGETS` | leer lassen **oder** exakt der eine freigegebene Spiegel-Host | leere Allowlist schaltet ALLE Fernziele hart ab (ADR 0026, #192) — „Backup nur lokal" ist damit deploy-seitig erzwungen und vom Site-Admin nicht aufweichbar (Rollentrennung, Betriebshandbuch §6). |
| `SESSION_ABSOLUTE_HOURS` | `12` (Default 168) | eine Sitzung überdauert keinen Arbeitstag; Neuanmeldung am nächsten Tag ist der Preis. |
| `SESSION_IDLE_HOURS` | `2` (Default 72) | unbeaufsichtigte, noch angemeldete Arbeitsplätze fallen schnell zurück auf die Anmeldemaske. |
| `SMTP_HOST` etc. | **unkonfiguriert lassen** (oder internes Relay) | ohne SMTP verlassen keinerlei Inhaltstitel die Instanz per Mail (Digest-Restrisiko I-23 entfällt vollständig). Konsequenz ehrlich benannt: dann gibt es keine Verifikations- und Passwort-Reset-Mails — Kontenpflege läuft über den Site-Admin. Wer Mail braucht, nutzt ein internes Relay und akzeptiert I-23 (Restrisikoliste). |
| `WEB_PORT`/`API_PORT`/`COLLAB_PORT` | Defaults (127.0.0.1-gebunden) | Anwendungscontainer sind nie direkt exponiert; einzige Eintrittsstelle ist der Reverse Proxy (Sicherheitsdokumentation §2). |
| `LOG_LEVEL` | `info` | Audit-Zeilen (`audit: `-Präfix) müssen den Collector erreichen; `debug` nur zur Störungssuche. |
| `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_SCOPES`, `OIDC_PROVIDER_LABEL` | IdP der Behörde konfigurieren | Fremdauthentisierung (#214, ADR 0021): Authorization Code + PKCE gegen den IdP der Umgebung; deploy-seitig, weil die Authentisierungshoheit Plattformsache ist. Erst-Login legt Konten just-in-time an; bestehende lokale Konten werden NIE stillschweigend per E-Mail übernommen (expliziter Link-Flow). Konfigurationsdetails: security.md §External authentication. |
| `AUTH_PROXY_HEADER`, `AUTH_PROXY_TRUSTED_PEERS`, `AUTH_PROXY_MAP`, `AUTH_PROXY_MODE`, `AUTH_PROXY_DN_ATTRIBUTE` | nur bei Perimeter-Authentisierung setzen | Alternativpfad (#215): Identität aus dem Proxy-Header, gültig NUR vom TCP-Peer der Allowlist; fremder Peer mit Header wird abgewiesen und auditiert (`auth.proxy_rejected`). Der Proxy MUSS den Header aus eingehendem Verkehr strippen. mTLS-Variante über weitergereichten Zertifikats-DN (`mtls-dn`). Ohne Perimeter-Auth: unkonfiguriert lassen (Header wirkungslos). |
| `AUTH_LOCAL_ENABLED` | `false` (Default `true`) | **der** harte Schalter aus ADR 0021 (#216, deploy-seitige Realisierung von `auth.local.enabled`): `false` schaltet JEDEN lokalen Credential-Flow auf 404 (Login, Signup, Verifikation, Passwort vergessen/zuruecksetzen/aendern) — Anmeldung ausschliesslich ueber OIDC (#214) bzw. Perimeter-Auth (#215). Deploy-seitig, damit ein kompromittierter Site-Admin ihn nicht zurueckdrehen kann. Reihenfolge: erst Setup/Bootstrap (SETUP_ADMIN_*), dann auf `false`. PAT-/Feed-Token-Ausgabe bleibt fuer IdP-authentisierte Nutzer verfuegbar (eigene Schalter `api.enabled`/`feeds.enabled`; bewusste Entscheidung #216). |
| `VS_NFD_MODE` | `enforced` (Default `off`) | Härtungsprofil-Modus (#243, ADR 0027): die Anwendung kennt diese Referenzkonfiguration als maschinenlesbaren Katalog und bewertet die laufende Konfiguration dagegen (Site-Admin → Einstellungen). `marked` markiert Abweichungen, `hidden` blendet abweichende Optionen aus, `enforced` weist abweichende Schreibzugriffe serverseitig ab — alle drei Behandlungsstufen sind umgesetzt (#244#246); für den VS-NfD-Referenzbetrieb ist `enforced` der empfohlene Modus — nur er hält auch skriptgesteuerte Clients an das Profil. Bestehende Abweichungen werden beim Start und in der Admin-Ansicht gemeldet, nie automatisch geändert. Deploy-seitig wie `BACKUP_ALLOWED_TARGETS`: ein kompromittierter Site-Admin kann den Modus nicht aufweichen. Außerhalb von VS-Kontexten bleibt der Default `off` — keinerlei Markierung. |
### 1.3 Noch nicht verfügbar (Regel: landet hier im selben PR)
Derzeit leer — `auth.local.enabled` ist mit #216 als `AUTH_LOCAL_ENABLED` (1.2) scharfgestellt.
| Schalter | Referenzwert (geplant) | Status |
| -------- | ---------------------- | ------ |
## 2 Verifikations-Checkliste
Auf der laufenden Instanz (ersetze `HOST`); Erwartung jeweils dahinter.
Die vier 404-Prüfungen laufen unauthentifiziert:
```sh
curl -s -o /dev/null -w '%{http_code}\n' https://HOST/api/public/v1/ponds # 404 (api.enabled=false)
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://HOST/api/mcp # 404 (mcp.enabled=false)
curl -s -o /dev/null -w '%{http_code}\n' https://HOST/api/v1/public/IRGENDEIN-TEICH/feed.xml # 404 (feeds.enabled=false)
curl -s -o /dev/null -w '%{http_code}\n' https://HOST/api/v1/admin/plugins # 401/404, nie 200 ohne Session
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://HOST/api/v1/auth/login # 404 (AUTH_LOCAL_ENABLED=false)
curl -s https://HOST/api/v1/readyz # status ok
```
Als Site-Admin (UI → Administration bzw. `GET /api/v1/admin/settings`):
- [ ] Registrierung „geschlossen"; neue Seite entsteht mit Kennzeichnung
(Einstufung neuer Seiten = VS-NfD); Upload auf eingestufte Seite
wird abgelehnt (403).
- [ ] Plugin-Verwaltung antwortet 404 (Kill-Switch aktiv).
- [ ] Backup-Panel zeigt keine aktiven Fernziele; auf dem Host ist
`BACKUP_ALLOWED_TARGETS` leer bzw. exakt der freigegebene Spiegel.
- [ ] Eine Testsitzung läuft nach `SESSION_IDLE_HOURS` Inaktivität ab.
- [ ] Kennzeichnungs-Stichprobe: eingestufte Seite zeigt den Aufdruck in
Web, Druckvorschau und PDF-Export (Konventionen: Kommentare auf
Issue #228 bzw. `60-sicherheitsdokumentation.md` §3.5).
## 3 Querbezüge
Abgrenzungserklärung (`40-abgrenzungserklaerung.md`, verweist hierher);
IT-Grundschutz-Mapping (`80-grundschutz-mapping.md`, nutzt dieses
Profil als Produkt-Beleg); Betriebshandbuch (`70-betriebshandbuch.md`,
Installation §1 wendet dieses Profil an); Restrisikoliste
(`90-restrisiken.md` — Abweichungen von der Referenzkonfiguration
gehören dorthin, wenn sie dauerhaft sind).