Compare commits

...

4 Commits

Author SHA1 Message Date
7fe3ffd936 #231: residual-risk list
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m22s
CI / Build container images (pull_request) Successful in 28s
CI / Auth e2e pack (pull_request) Successful in 9m4s
CI / Import/export fidelity gate (pull_request) Successful in 1m3s
docs/vs-nfd/90-restrisiken.md: nine entries, each with risk, why it is
accepted, compensating control and decider — unmarked attachment
content (#212), local auth not yet switchable incl. the open runtime-
flippability question (#216), deferred plugin hash pinning (#232), the
one-time git-history secret check with its pattern caveat (#198),
digest-mail titles (I-23, revisit M32), page_links slug residue (I-24),
the IndexedDB endpoint copy (I-25), deliberately unscheduled features,
and the Site-Admin read bypass. Binding same-PR maintenance rule
stated; referenced from the delimitation statement and consumed by the
Grundschutz mapping.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:01:11 +02:00
0dc9789cb1 #227: hardening guide with the VS-NfD reference configuration
Some checks failed
CI / Lint, typecheck, test (pull_request) Failing after 15s
CI / Auth e2e pack (pull_request) Has been skipped
CI / Import/export fidelity gate (pull_request) Has been skipped
CI / Build container images (pull_request) Has been skipped
docs/vs-nfd/50-haertungsleitfaden.md: one adoptable profile — every
entry with the exact switch name, value, default and the reason, split
into instance settings (registration closed, api/mcp off, feeds off,
plugins off, classification defaults vs_nfd + upload block, svg reject,
minimal extension list) and deploy-level configuration (empty
BACKUP_ALLOWED_TARGETS enforces backup-local-only outside Site-Admin
reach; tightened session hours; SMTP deliberately unconfigured with the
consequence stated honestly). auth.local.enabled is listed as the one
pending row (#216) with its compensation until then; the guide states
the binding updated-in-same-PR rule for every future switch. Includes
an operator verification checklist (four unauthenticated 404 curls +
readyz + admin spot checks). Cross-referenced from the delimitation
statement (file names made concrete) and consumed by the Grundschutz
mapping (#230).

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:59:27 +02:00
d641c5dc8a #229: operations manual (install, update, backup/restore, deletion, roles)
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 6m33s
CI / Build container images (pull_request) Successful in 15s
CI / Auth e2e pack (pull_request) Successful in 9m20s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
docs/vs-nfd/70-betriebshandbuch.md: installation as run on the real
stages (airgap variant explicitly pending #218-#221 with what already
exists as groundwork), update/rollback incl. the no-down-migrations
caveat, backup/restore with the ADR-0026 target allowlist and the
rehearsed monthly restore drill (evidence: logs on #98), the full
scheduler-job table (cadences verified against code), the deletion-and-
destruction chapter built on the #228 copy list (per content type:
what deletion reaches, what remains, immediate-destruction path,
decommissioning), and role separation incl. the deliberate limits of a
Site Admin and the honest note that Site Admin read-bypass makes the
content/platform split non-absolute app-side. Every procedure carries
its evidence level (erprobt / nicht geprobt / offen) — nothing claimed
above what was actually executed.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:57:16 +02:00
1cf0458593 #228: security documentation (architecture, data flows, network plan)
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m26s
CI / Build container images (pull_request) Successful in 13s
CI / Auth e2e pack (pull_request) Successful in 9m27s
CI / Import/export fidelity gate (pull_request) Failing after 13s
docs/vs-nfd/60-sicherheitsdokumentation.md: component diagram with per-
service purpose and privileges, network plan digit-exact against the
deploy compose (127.0.0.1-only app bindings, internal-only data zone),
data-flow diagrams (auth, realtime editing incl. LISTEN/NOTIFY and the
60s collab token, export via the pinned sidecars, backup incl. the
ADR-0026 allowlist, and every read channel), named trust boundaries
(reverse proxy, plugin sandbox, outbound SMTP/mirror), and the complete
list of content copies — in-database, on-volume and outside the
instance — that the deletion concept in #229 builds on. Mermaid only,
German (assessor audience), with the maintained-in-same-PR rule stated.

Co-Authored-By: Claude Fable 5 (1M context) <noreply@anthropic.com>
2026-07-31 09:54:01 +02:00
6 changed files with 749 additions and 10 deletions

View File

@ -184,16 +184,17 @@ nach sich zieht.
- [x] **Abgrenzungserklärung §52 VSA** — welche Sicherheitsgrundfunktionen die
Anwendung _nicht_ erbringt und wem sie zufallen. Wichtigstes
Einzeldokument. · 3 AT · #226`40-abgrenzungserklaerung.md`
- [ ] **Härtungsleitfaden** mit Referenzkonfiguration „VS-NfD-Betrieb":
lokale Auth aus, Public-API aus, MCP aus, Feeds aus, Plugins aus,
Backup nur lokal · 3 AT · #227
- [ ] **Sicherheitsdokumentation**: Architektur, Datenflüsse, Netzplan,
Ports/Dienste, Vertrauensgrenzen · 4 AT · #228
- [ ] **Betriebshandbuch**: Installation (inkl. Airgap), Update, Backup/Restore,
Löschung und Vernichtung, Rollentrennung · 45 AT · #229
- [x] **Härtungsleitfaden** mit Referenzkonfiguration „VS-NfD-Betrieb":
lokale Auth aus (Zeile ⏳ bis #216), Public-API aus, MCP aus, Feeds aus,
Plugins aus, Backup nur lokal · 3 AT · #227 → \`50-haertungsleitfaden.md\`
- [x] **Sicherheitsdokumentation**: Architektur, Datenflüsse, Netzplan,
Ports/Dienste, Vertrauensgrenzen · 4 AT · #228`60-sicherheitsdokumentation.md`
- [x] **Betriebshandbuch**: Installation (inkl. Airgap), Update, Backup/Restore,
Löschung und Vernichtung, Rollentrennung · 45 AT · #229`70-betriebshandbuch.md`
(Airgap-Kapitel verweist auf offene #218#221)
- [ ] **Zuarbeit IT-Grundschutz** APP.3.1 und CON.11.1, je Anforderung
„Produkt / Betreiber / nicht anwendbar" · 34 AT · #230
- [ ] **Restrisikoliste** mit bewusst offenen Punkten · 1 AT · #231
- [x] **Restrisikoliste** mit bewusst offenen Punkten · 1 AT · #231`90-restrisiken.md`
---

View File

@ -313,5 +313,8 @@ Slug-Residuum falls in #235 so entschieden), führt die Restrisikoliste
gekürzt; die Erklärung ist erst dann uneingeschränkt gültig, wenn die
Delta-Liste leer ist oder alle Restpositionen in der Restrisikoliste
(#231) vom Betreiber gebilligt sind.
- Querbezüge: Härtungsleitfaden (#227), Sicherheitsdokumentation (#228),
Betriebshandbuch (#229), IT-Grundschutz-Zuarbeit (#230).
- Querbezüge: Härtungsleitfaden (#227, `50-haertungsleitfaden.md`),
Sicherheitsdokumentation (#228, `60-sicherheitsdokumentation.md`),
Betriebshandbuch (#229, `70-betriebshandbuch.md`),
IT-Grundschutz-Zuarbeit (#230, `80-grundschutz-mapping.md`),
Restrisikoliste (#231, `90-restrisiken.md`).

View File

@ -0,0 +1,94 @@
# 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. Hash-Pinning ist verschoben (#232, Restrisikoliste) — der Kill-Switch deckt das Risiko für diesen Betriebsmodus vollständig. |
| `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). |
| `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. |
### 1.3 Noch nicht verfügbar (Regel: landet hier im selben PR)
| Schalter | Referenzwert (geplant) | Status |
| -------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auth.local.enabled` | `false` — lokale Passwort-Auth aus, Anmeldung nur über die Fremdauthentisierung der Behörde | ⏳ kommt mit #216 (M27); bis dahin bleibt lokale Auth der einzige Anmeldeweg und `auth.registrationMode=closed` + Session-Verkürzung sind die Kompensation. Zeile wird im #216-PR scharfgestellt. |
## 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 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).

View File

@ -0,0 +1,277 @@
# Sicherheitsdokumentation (VS-NfD, Issue #228)
Zweck: das Dokument, das ein Prüfer zuerst liest — Architektur,
Datenflüsse, Netzplan, Vertrauensgrenzen und die vollständige Liste
aller Inhaltskopien. Es konsolidiert bestehende Referenzdokumente,
erfindet nichts neu und ist **auf die Ziffer genau** an
`deploy/compose/docker-compose.yml` ausgerichtet (die Compose-Datei ist
die maßgebliche Dienst- und Portliste; weicht dieses Dokument ab, ist
das ein Fehler in diesem Dokument).
Einordnung: Dorfteich erbringt **keine Sicherheitsgrundfunktion**
(§52 VSA, ADR 0019, Abgrenzungserklärung
`40-abgrenzungserklaerung.md`). Diese Dokumentation beschreibt, was die
Anwendung tut — Verschlüsselung, Netztrennung, Datenträgerschutz und
Beweissicherung auf Plattformebene fallen dem Betreiber zu.
Quell-Dokumente (englisch, maßgeblich für Details):
`docs/architecture/security.md`, `deployment.md`, `data-model.md`,
`permissions.md`, `realtime-collaboration.md`,
`plugin-architecture.md`, `operations.md`, `audit-events.md`;
Deploy-Sicht: `deploy/stages.md`, `deploy/compose/docker-compose.yml`.
Pflegeregel: Änderungen an Diensten, Ports, Datenflüssen oder
Speicherorten werden **im selben PR** hier nachgezogen (dieselbe Regel
wie beim Härtungsleitfaden `50-haertungsleitfaden.md`).
---
## 1 Komponenten
```mermaid
flowchart LR
subgraph client [Endgerät]
B[Browser SPA]
end
subgraph host [Betreiber-Host / Reverse-Proxy-Zone]
RP[Reverse Proxy TLS]
end
subgraph frontend [Docker-Netz frontend]
W[web nginx statisch]
A[api NestJS]
C[collab Hocuspocus]
end
subgraph internal [Docker-Netz internal]
DB[(PostgreSQL 17.5)]
P[pandoc 3.6]
G[gotenberg 8]
BK[backup Sidecar]
end
SMTP[(SMTP-Relay extern)]
MIR[(Backup-Spiegel rsync/WebDAV, Allowlist)]
B -->|HTTPS 443| RP
RP -->|/| W
RP -->|/api| A
RP -->|/collab WebSocket| C
A --> DB
C --> DB
A -->|HTTP pandoc:3030| P
A -->|HTTP gotenberg:3000| G
A -->|Mail| SMTP
BK --> DB
BK -.->|nur bei Konfiguration + Allowlist| MIR
BK -->|Fehler-Mail| SMTP
```
| Dienst | Image (gepinnt, #203/#236) | Zweck | Privilegien |
| ---------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `web` | eigenes Image (nginx-unprivileged 1.27, uid 101) | statische SPA-Auslieferung | kein Volume, nur Netz `frontend`, kein DB-Zugang |
| `api` | eigenes Image (Node 22.15.1-alpine, User `node`) | REST-API, Permissions, Scheduler-Jobs, Import/Export-Orchestrierung, Audit | Volumes `uploads`, `plugins`, `secrets`, `backups` (ro); Netze `frontend`+`internal`; DB-Vollzugriff |
| `collab` | eigenes Image (Node, User `node`) | Echtzeit-Editing (Yjs/Hocuspocus), Persistenz der Dokumente | Netze `frontend`+`internal`; DB-Vollzugriff; keine Datei-Volumes |
| `db` | `postgres:17.5-alpine@sha256:…` | einziger Datenbestand (außer Uploads) | nur Netz `internal`; Volume `db-data` |
| `pandoc` | `pandoc/core:3.6@sha256:…` | Dokumentkonvertierung (Import/Export DOCX/ODT) | nur `internal`; zustandslos, kein Volume, kein DB-Zugang |
| `gotenberg` | `gotenberg/gotenberg:8@sha256:…` | HTML→PDF-Rendering (Chromium) | nur `internal`; zustandslos, kein Volume, kein DB-Zugang |
| `backup` | eigenes Image | nächtlicher `pg_dump -Fc` + Volume-Tar, Retention, optionaler Spiegel | nur `internal`; Volumes `uploads`, `plugins`, `secrets` (ro), `backups` (rw); DB-Zugang |
| `caddy` (Compose-Profil, optional) | `caddy:2.10-alpine@sha256:…` | TLS-Terminierung, wenn kein Host-Proxy existiert | Ports 80/443 nach außen; Netz `frontend` |
Alle eigenen Images laufen als non-root (`USER node`); die Stages auf
ONE nutzen einen Host-Reverse-Proxy (Caddy des Hosts), das
`caddy`-Profil bleibt dort inaktiv.
## 2 Netzplan (Ports und Protokolle)
Maßgeblich: `deploy/compose/docker-compose.yml`. Alle
Anwendungs-Portbindungen sind **auf 127.0.0.1 beschränkt** — von außen
erreichbar ist ausschließlich der Reverse Proxy.
| Von | Nach | Port/Protokoll | Exposition |
| ----------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| Client | Reverse Proxy | 443/TCP HTTPS (80 nur Redirect) | **extern** |
| Reverse Proxy | `web` | `127.0.0.1:${WEB_PORT:-8100}` → Container 8080, HTTP | Host-lokal |
| Reverse Proxy | `api` | `127.0.0.1:${API_PORT:-8101}` → Container 3000, HTTP | Host-lokal |
| Reverse Proxy | `collab` | `127.0.0.1:${COLLAB_PORT:-8102}` → Container 3000, HTTP + WebSocket-Upgrade | Host-lokal |
| `api`/`collab`/`backup` | `db` | 5432/TCP (nur Docker-Netz `internal`) | intern |
| `api` | `pandoc` | `http://pandoc:3030` (nur `internal`) | intern |
| `api` | `gotenberg` | `http://gotenberg:3000` (nur `internal`) | intern |
| `api`, `backup` | SMTP-Relay | vom Betreiber konfiguriert (`SMTP_*`) | **ausgehend extern** |
| `backup` | Spiegel-Ziel | rsync über SSH (Port konfigurierbar) bzw. WebDAV/HTTPS — **nur** an Hosts der Deploy-Allowlist `BACKUP_ALLOWED_TARGETS` (ADR 0026, #192); leere Allowlist = alle Fernziele hart aus | **ausgehend extern, allowlist-beschränkt** |
| optional `caddy` | Client | 80/443 | extern (nur wenn Profil aktiv) |
Es gibt **keine eingehenden** Verbindungen außer über den Reverse
Proxy. `db`, `pandoc`, `gotenberg`, `backup` haben keinerlei
Portbindung an den Host.
## 3 Datenflüsse
### 3.1 Authentifizierung
```mermaid
sequenceDiagram
participant B as Browser
participant A as api
participant DB as PostgreSQL
B->>A: POST /api/v1/auth/login (Origin-Header Pflicht, fail-closed CSRF #189)
A->>DB: user_identities (Argon2id-Hash prüfen), rate_limits (10/min/IP)
A-->>B: Set-Cookie dt_session (HttpOnly, Secure, SameSite=Lax)
Note over A,DB: Session-Row: gehashte Id, expiresAt (SESSION_ABSOLUTE_HOURS),<br/>Idle-Grenze (SESSION_IDLE_HOURS, #190); Audit auth.login_*
```
Externe Authentisierung (OIDC/Proxy-Header) ist geplant (M27,
#214#217) und ändert diesen Fluss; bis dahin ist lokale
Passwort-Authentifizierung der einzige Weg.
### 3.2 Editieren (Echtzeit)
```mermaid
sequenceDiagram
participant B as Browser
participant A as api
participant C as collab
participant DB as PostgreSQL
B->>A: GET /pages/:id/collab-token (Guard: Read-Recht)
A-->>B: Kurzlebiges Token (60 s TTL, HKDF-Subkey aus COLLAB_TOKEN_SECRET, #188), mode rw/ro
B->>C: WebSocket /collab + Token
C->>C: Token-Verifikation (shared token-crypto), rw nur bei Write-Grant
B-->>C: Yjs-Updates (CRDT)
C->>DB: debounced Persist: pages.ydoc_state + page_updates-Log,<br/>abgeleiteter page_content_cache (plain/markdown/html/outline/tsvector)
A-->>C: Rechteänderungen via Postgres LISTEN/NOTIFY (Kanal-Konstanten shared) → close(4205)
```
### 3.3 Export (PDF/DOCX/ODT/ZIP)
```mermaid
sequenceDiagram
participant B as Browser
participant A as api
participant P as pandoc
participant G as gotenberg
B->>A: POST /pages/:id/export {format}
A->>A: conversion_jobs-Row (input = aufbereitetes Markdown/HTML,<br/>bei eingestufter Seite Option {marking}, #208/#209)
A->>G: html → pdf (Kennzeichnung im Header/Footer-Template je Seite)
A->>P: gfm → docx/odt (reference-doc mit Kennzeichnung, in-Request-Datei)
A-->>B: Poll GET /jobs/:id → Download /jobs/:id/result
Note over A: Payloads werden nach conversion.payloadRetentionDays genullt (#233)
```
Der Pond-ZIP-Export streamt direkt aus `page_content_cache` +
`uploads`-Volume (permissionsgefiltert) und enthält seit M26
Frontmatter/Aufdruck, `manifest.json` und Begleitdateien für
eingestufte Inhalte (#210/#212).
### 3.4 Backup
```mermaid
sequenceDiagram
participant BK as backup-Sidecar
participant DB as PostgreSQL
participant V as Volumes uploads/plugins
participant M as Spiegel (optional)
BK->>DB: nächtlich pg_dump -Fc
BK->>V: tar der Volumes (lesend)
BK->>BK: Restore-Set aufs backups-Volume, Retention (BACKUP_RETENTION_DAYS)
BK-->>M: rsync/WebDAV NUR an BACKUP_ALLOWED_TARGETS (ADR 0026); leer = aus
BK-->>BK: status.json; Fehler-Mail direkt via SMTP
```
Backups sind **unverschlüsselt by design** (ADR 0015/0019) —
Datenträger- und Transportschutz ist Plattformsache
(Abgrenzungserklärung §2).
### 3.5 Lesekanäle
Jeder Kanal, über den Seiteninhalt die Anwendung verlässt (zugleich die
Instrumentierungsliste für den Lesetrail, #222):
| Kanal | Pfad | Rechteprüfung | Kennzeichnung (M26) |
| -------------------------- | -------------------------------- | -------------------------------------- | ----------------------------------- |
| SPA-Seitenansicht | `GET /api/v1/pages/:id` u. a. | Guard (shared Resolver) | Banner oben+unten (#206) |
| Public-/No-JS-Ansicht | `GET /api/v1/public/:pond/:page` | `public`-Grant, sonst 404 | Banner oben+unten (#211) |
| Public REST API | `/api/public/v1/**` | `api.enabled` + Pond-Opt-in + PAT | `classification`-Feld (#211) |
| MCP-Endpoint | `/api/mcp` | `mcp.enabled` + Pond-Opt-in + PAT | wie Public API (gleiches Modell) |
| Atom-Feeds | `…/feed.xml` | `feeds.enabled`; Grant bzw. Feed-Token | `<category>`-Element (#211) |
| Suche | `GET /api/v1/search` | per-Treffer-Resolution | Level je Treffer (#211) |
| Attachment-Download | `GET /api/v1/media/:fileId` | Guard über Seite/Pond | Dateinamens-Präfix `VS-NfD_` (#212) |
| Export (PDF/Office/ZIP/MD) | s. 3.3 | Guard; ZIP permissionsgefiltert | je Kanal (#207#210) |
| Collab-WebSocket | `/collab` | Token aus 3.2 | Inhalt = Editor-Ansicht (#206) |
## 4 Vertrauensgrenzen
1. **Client ↔ Instanz** (Internet/Behördennetz): TLS am Reverse Proxy
terminiert (Betreiber-Zone). Die Anwendung setzt die
Security-Header selbst (eigene Middleware, #197: CSP `script-src
'self'`, `X-Frame-Options: SAMEORIGIN`, restriktives CORS);
Cookie-Mutationen sind Origin-pflichtig (fail-closed, #189).
2. **Reverse Proxy ↔ Anwendungscontainer**: nur 127.0.0.1-Bindungen;
der Proxy ist die einzige Eintrittsstelle. Der geplante
Proxy-Header-/mTLS-Authentisierungspfad (#215) verschiebt die
Authentisierungs-Vertrauensgrenze an genau diese Stelle — bis dahin
trägt der Proxy nur Transport.
3. **Anwendungs- ↔ Datenzone**: `db`, `pandoc`, `gotenberg`, `backup`
sind nur im Docker-Netz `internal` erreichbar; `web` hat keinerlei
Zugang dorthin.
4. **Plugin-Sandbox** (ADR 0008): Plugin-Code läuft ausschließlich im
sandboxed iframe (eigene Origin-lose Umgebung, Message-Protokoll,
deklarierte Capabilities); serverseitig wird beim Install ein
Static-Gate erzwungen. Instanzweiter Kill-Switch `plugins.enabled`
(#200); Hash-Pinning ist bewusst verschoben (#232,
Restrisikoliste). Der Sandbox-Escape-Regressionstest (bösartiges
Fixture-Plugin) läuft in CI.
5. **Ausgehende Kanäle**: SMTP (Betreiber-Relay) und Backup-Spiegel
(Allowlist, ADR 0026) sind die einzigen initiierten
Außenverbindungen. Es gibt keine Telemetrie, keine Update-Pings,
keine externen Font-/CDN-Ladungen (ADR 0016: Fonts self-hosted).
6. **Betreiber-Plattform**: Host, Docker-Daemon, Volumes, Netzwerk und
Backups liegen außerhalb der Anwendungsverantwortung
(Abgrenzungserklärung).
## 5 Vollständige Liste der Inhaltskopien
Grundlage des Löschkonzepts im Betriebshandbuch
(`70-betriebshandbuch.md` §5) — Löschen ist nur vollständig, wenn es
jede dieser Kopien erreicht oder ihren Verbleib begründet.
**In der Datenbank (Volume `db-data`):**
| Ort | Inhalt | Lebenszyklus |
| ------------------------------ | ------------------------------------------------- | ----------------------------------------------------- |
| `pages.ydoc_state` | aktuelles Dokument (Yjs-Binärzustand) | bis Purge der Seite |
| `page_updates` | inkrementelles Update-Log | Compaction; Purge löscht |
| `page_content_cache` | Klartext, Markdown, HTML, Outline, **Suchvektor** | bei jedem Persist ersetzt; Purge löscht |
| `page_versions` | eigenständige Snapshots (Historie) | bis Purge der Seite |
| `comments` | Kommentartexte | Purge der Seite kaskadiert |
| `conversion_jobs.input/result` | Export-/Import-Payloads (ganze Dokumente) | genullt nach `conversion.payloadRetentionDays` (#233) |
| `mail_outbox` | Mail-Bodies (Digest-Titel!) | gelöscht nach `mail.outboxRetentionDays` (#234) |
| `page_links.target_slug` | Slug-Text auch nach Ziel-Purge | bewusst behalten (#235, Restrisiko I-24) |
| `audit_log` | Metadaten (nie Inhalt) | `audit.retentionDays` (#196) |
| `attachments` (Row) | Metadaten + SHA-256 | Purge der Seite/des Ponds |
**Auf Volumes:**
| Ort | Inhalt | Lebenszyklus |
| --------- | -------------------------------------------- | --------------------------------------------------------------- |
| `uploads` | Attachment-Bytes | Purge löscht Datei + Quota; Orphan-Sweep (#194) räumt Verwaiste |
| `backups` | nächtliche Restore-Sets (Dump + Volume-Tars) | `BACKUP_RETENTION_DAYS` |
| `plugins` | Plugin-Pakete (kein Seiteninhalt) | Uninstall |
| `secrets` | Secret-Store (SMTP etc., kein Seiteninhalt) | Betreiber |
**Außerhalb der Instanz (nicht von Anwendungs-Löschung erreichbar —
Löschkonzept muss sie benennen):**
| Ort | Inhalt | Kontrolle |
| ----------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------ |
| Backup-Spiegel (BASEL/WebDAV) | vollständige Restore-Sets | Betreiber; Retention auf Zielsystem |
| Export-Artefakte | PDF/DOCX/ODT/ZIP beim Nutzer | organisatorisch (VS-Handhabung, Kennzeichnung M26) |
| Browser-IndexedDB | Offline-Kopie zuletzt geöffneter Seiten | Endgeräteschutz (Restrisiko I-25) |
| Feed-Reader / API-Konsumenten | abonnierte Inhalte | organisatorisch; Referenzkonfiguration schaltet Feeds/API ab |
| Mails beim Empfänger | Digest-/Benachrichtigungstexte (Titel) | Restrisiko I-23; Referenzkonfiguration ohne SMTP |
| Container-stdout-Logs | Metadaten inkl. Audit-Zeilen (nie Seiteninhalt, security.md §Logging) | Docker json-file mit Rotation; Collector des Betreibers |
## 6 Kryptografie-Inventar (Bestand, kein Anspruch)
Zur Einordnung (vollständige Liste: Abgrenzungserklärung §3): Argon2id
für Passwort-Hashes, HMAC-signierte Kurzzeit-Tokens über `jose` mit
HKDF-Zweckableitung (#188), SHA-256-Integritätshashes für Attachments
(#199, fail-closed beim Download). Keine Inhalts- oder
Backup-Verschlüsselung, kein eigenes Schlüsselmanagement — bewusst
(§52 VSA).

View File

@ -0,0 +1,210 @@
# Betriebshandbuch (VS-NfD, Issue #229)
Zweck: der Betreiber führt die Instanz ohne uns — auch an dem Tag, an
dem etwas ausfällt. Dieses Handbuch bündelt die betrieblichen
Prozeduren und verweist für Schrittfolgen auf die maßgeblichen
Runbooks im Repository, statt sie zu duplizieren. Das Kapitel mit dem
größten VS-Gewicht ist **§5 Löschung und Vernichtung**; es baut auf der
vollständigen Kopienliste der Sicherheitsdokumentation auf
(`60-sicherheitsdokumentation.md` §5).
**Belegstufen.** Jede Prozedur nennt am Ende ihren Erprobungsstand:
- ✅ _erprobt_ — vom Autor bzw. automatisiert mindestens einmal real
ausgeführt, mit Beleg.
- ⚠️ _Mechanik vorhanden, nicht geprobt_ — implementiert und getestet,
aber noch nie im Ernstfall/als Übung durchgespielt.
- ⏳ _offen_ — kommt mit dem genannten Issue.
---
## 1 Installation
Referenz: `deploy/stages.md` (maßgebliche Schrittfolge),
`docs/self-hosting/README.md`.
1. Host-Voraussetzungen: Docker + Compose, ein Reverse Proxy mit TLS
(oder das mitgelieferte `caddy`-Compose-Profil).
2. `deploy/compose/docker-compose.yml` + `.env` (Vorlage
`.env.example`; **niemals** eine echte `.env` ins Repo — CI-Zaun
#198) auf den Host bringen; `COMPOSE_PROJECT_NAME`, Ports
(`WEB_PORT`/`API_PORT`/`COLLAB_PORT`), `POSTGRES_PASSWORD`, Secrets
setzen.
3. `docker compose pull && docker compose up -d` — die api wendet
Migrationen beim Start selbst an (`MIGRATE_ON_START`); es gibt
keinen separaten Migrationsschritt.
4. Erststart: der Setup-Wizard (Issue #80) legt das Admin-Konto an und
verriegelt sich danach dauerhaft; bis zum Abschluss ist nur
`/setup/*` erreichbar.
5. Reverse Proxy: `/` → web, `/api` → api, `/collab` → collab mit
WebSocket-Upgrade (Portliste: Sicherheitsdokumentation §2).
6. Für VS-NfD-Betrieb anschließend die Referenzkonfiguration aus dem
Härtungsleitfaden (`50-haertungsleitfaden.md`) anwenden und mit
dessen Checkliste verifizieren.
Belegstufe: ✅ erprobt — die Stages Test/Int/Prod auf ONE sind exakt
nach dieser Prozedur aufgesetzt und laufen produktiv (`deploy/stages.md`
dokumentiert die realen Instanzen).
**Airgap-/Offline-Variante:** ⏳ offen — Mirror-Verfahren (#218),
netzloser Build (#219), Testlauf in isolierter Umgebung (#220),
Offline-Update-Pfad (#221); Meilenstein M28. Bereits vorhanden als
Grundlage: alle Dritt-Images digest-gepinnt (#203), ein authoritativer
Node-Pin (#236), SBOMs je Release (#202).
## 2 Update und Rollback
Referenz: `docs/architecture/operations.md` §Update strategy,
`deploy/stages.md`.
- **Stages:** Merge auf `main` → CD baut Images, deployt Test, führt
Smoke-Tests aus, promotet Int. CD synct **keine** Compose-Dateien —
Compose-Änderungen werden von Hand auf den Host übernommen, sonst
räumt `--remove-orphans` manuell ergänzte Container ab.
Belegstufe: ✅ erprobt (läuft bei jedem Merge; zuletzt CD-Lauf 568).
- **Produktion:** ausschließlich getaggte Releases. `git tag vX.Y.Z`
baut die Versions-Images und führt die Update-Simulation als Gate
aus; `git tag prod-vX.Y.Z-initial` pinnt die Version in der
Prod-`.env` und rollt aus (readyz-Poll). Migrationen laufen beim
api-Start; **Rückwärtsmigrationen gibt es nicht** — Rollback setzt
deshalb ein DB-kompatibles Vorgängerimage voraus (Migrations-Kaveat;
im Zweifel Restore aus dem Backup-Set der Vorversion).
Belegstufe: ✅ erprobt (alle Prod-Versionen bis v0.12.0 so
ausgerollt).
- **Rollback:** `git tag prod-v<prev>-rollback1` pinnt die Vorversion
zurück. Belegstufe: ⚠️ Mechanik vorhanden, nicht geprobt — ein realer
Rollback war bisher nie nötig; die Übung steht aus.
- **Dritt-Image-Digest heben:** Prozedur `deploy/stages.md` §5a
(imagetools inspect → Compose-Referenz ändern → CI bestätigt → Stage-
Composes von Hand nachziehen → `docker inspect` verifiziert).
Belegstufe: ✅ erprobt (Rollout #203 am 31.07.2026).
- **Offline-Update:** ⏳ offen (#221, M28).
## 3 Backup und Restore
Referenz: `docs/operations/restore-runbook.md` (maßgeblich),
`deploy/backup-basel.md`, ADR 0015/0026.
- Nächtlich erzeugt der backup-Sidecar ein konsistentes Restore-Set
(`pg_dump -Fc` + Tar der Volumes `uploads`/`plugins`) auf dem
`backups`-Volume; Retention `BACKUP_RETENTION_DAYS` (Default 30,
Test/Int 7). Status in `status.json`, Fehler alarmieren per Mail
direkt via SMTP (bewusst nicht über die api — sie könnte das kaputte
Teil sein).
- **Zielbeschränkung (#192, ADR 0026):** Fernziele (rsync-Spiegel,
WebDAV) funktionieren nur gegen Hosts der **Deploy-Allowlist**
`BACKUP_ALLOWED_TARGETS`; eine leere Allowlist schaltet alle
Fernziele hart ab. Die Allowlist ist bewusst NICHT über die Admin-UI
änderbar (Rollentrennung, §6).
- **Restore:** `deploy/backup/restore.sh` fährt den Stack kontrolliert
herunter, spielt Dump + Volumes zurück und startet neu; Details und
Totalverlust-Szenario im Runbook.
- **Geprobter Restore:** ✅ erprobt — ein automatischer monatlicher
Restore-Drill (`drill-*`-Tag → `drill.yml`) stellt das jüngste
Prod-Set in einer Wegwerf-Umgebung wieder her und prüft Inhalte; das
Protokoll jedes Laufs steht als Kommentar an Issue #98.
Backups sind unverschlüsselt by design — Datenträgerschutz ist
Plattformsache (Abgrenzungserklärung).
## 4 Wartungsjobs (Scheduler)
Alle Jobs laufen in der api (in-app Scheduler, `jobs`-Tabelle), sind im
Site-Admin-Systempanel sichtbar und dort manuell auslösbar (auditiert
als `job.triggered`). Aktueller Bestand (9):
| Job | Rhythmus | Wirkung |
| -------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `trash-purge` | täglich | endgültiges Löschen abgelaufener Papierkorb-Seiten **und** -Teiche (`trash.retentionDays`, #31/#193) |
| `orphan-file-sweep` | täglich | verwaiste Uploads entfernen (#194); trägt nachts auch den SHA-256-Backfill (#199) |
| `page-compaction` | stündlich | `page_updates`-Log in den Zustand mergen |
| `version-thinning` | täglich | automatische Versions-Snapshots ausdünnen |
| `audit-retention` | täglich | `audit_log` nach `audit.retentionDays` beschneiden (#196), Lücke selbst auditiert (`audit.pruned`) |
| `conversion-payload-prune` | täglich | Import-/Export-Payloads fertiger Jobs nullen (#233) |
| `mail-outbox-retention` | täglich | SENT/endgültig FAILED Outbox-Zeilen löschen (#234) |
| `data-export-purge` | stündlich | abgelaufene DSGVO-Datenexporte entfernen (#68) |
| `notification-digest` | alle 15 min (Versand nach Nutzer-Präferenz) | Benachrichtigungs-Digests versenden |
Der e2e-Zaun `apps/web/e2e/system.spec.ts` pinnt diese Zahl — ein
neuer Job ohne Handbuch-/Zaun-Anpassung wird rot.
## 5 Löschung und Vernichtung
Grundsatz: „Gelöscht" heißt in Dorfteich erst dann gelöscht, wenn alle
Kopien aus `60-sicherheitsdokumentation.md` §5 erreicht sind. Die
Tabelle nennt je Inhaltstyp, was die Löschung tut, welche Kopien sie
erreicht und was **stehen bleibt**.
| Vorgang | Wirkung | erreichte Kopien | Rückstände / Fristen |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Seite in den Papierkorb | Soft-Delete (`deleted_at`), aus Suchindex entfernt (#195) | Suchvektor sofort | Inhalt vollständig vorhanden, wiederherstellbar bis Purge |
| Seiten-Purge (automatisch nach `trash.retentionDays`, Default 30, oder manuell) | löscht Zustand, Update-Log, Content-Cache, Versionen, Kommentare, Attachments (Bytes + Quota), Watches; Kinder rücken auf | DB-Zeilen + `uploads`-Bytes | `page_links.target_slug` bleibt bewusst (#235, Restrisiko I-24); Backups/Spiegel bis Ablauf ihrer Retention; Export-/Endgeräte-Kopien organisatorisch |
| Teich-Purge (#193, automatisch/manuell, Site-Admin) | löscht alle Seiten samt Kaskade, Labels, Grants, Nutzungszähler, Plugin-Opt-ins, Conversion-Jobs, Files | wie oben, teichweit | wie oben |
| Nutzer löschen / DSGVO | Konto löschen bzw. Autorschaft pseudonymisieren (`user.pseudonymized`) | Identitätsdaten | von ihm erstellte Inhalte gehören dem Teich |
| Export-Payloads | `conversion-payload-prune` nullt `input`/`result` fertiger Jobs (Default 30 d) | DB | Job-Zeile bleibt für Status/Audit |
| Mail-Kopien | `mail-outbox-retention` löscht SENT/endgültig FAILED (Default 30 d) | DB | zugestellte Mails beim Empfänger (Restrisiko I-23) |
| Backup-Sets | Retention des Sidecars bzw. des Spiegels | `backups`-Volume, Spiegel | ein gelöschter Inhalt lebt maximal bis zum Ablauf der längsten Backup-Retention weiter — bei sofortigem Vernichtungsbedarf Sets manuell löschen (Host-Zugriff) und Spiegel bereinigen |
| Audit-Trail | `audit-retention` (Default 365 d) | DB | Metadaten, nie Inhalt |
**Sofortige Vernichtung einzelner Inhalte** (über die Fristen hinaus):
Seite manuell purgen (Papierkorb → endgültig löschen), danach auf dem
Host die Backup-Sets der Aufbewahrungskette löschen bzw. den Spiegel
bereinigen und Endgeräte-/Exportkopien organisatorisch einsammeln.
Belegstufe Purge-Pfade: ✅ erprobt (laufen täglich produktiv; Purge-
Semantik durch Tests gepinnt). Belegstufe „Backup-Kette manuell
vernichten": ⚠️ nicht geprobt.
**Außerbetriebnahme einer Instanz:** `docker compose down -v` entfernt
Container und **alle benannten Volumes** (`db-data`, `uploads`,
`plugins`, `secrets`, `backups`, Caddy-Volumes); anschließend Spiegel-
Bestände löschen und Host-Datenträger nach Betreiber-Vorgabe
vernichten — die physische Vernichtung ist Plattformsache
(Abgrenzungserklärung). Belegstufe: ✅ erprobt für den Stack-Teil (der
stillgelegte Alt-VPS wurde so zurückgebaut; Datenträger dort noch als
Rollback-Reserve vorhanden).
## 6 Rollentrennung
| Aufgabe | braucht |
| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| Instanz-Settings, Nutzer-/Quota-Verwaltung, Plugins, Wartungsjobs anstoßen, Teich-Purge, Audit-Panel | **Site-Admin** (App-Rolle) |
| Deploy, `.env`/Compose ändern, Backup-Allowlist, Secrets-Volume, Restore, Backup-Sets vernichten, DB-Direktzugriff, Log-Collector | **Plattformzugriff** (Host) |
| Release nach Prod | Plattformzugriff + Freigabeprozess (Tag) |
Was ein Site-Admin **nicht** kann (bewusste Grenzen):
- die Backup-Zielliste ändern oder Fernziele aktivieren
(`BACKUP_ALLOWED_TARGETS` ist Deploy-Konfiguration, ADR 0026);
- Secrets lesen oder setzen, die im Secret-Store/der `.env` liegen
(die Settings-Tabelle trägt nie Secrets, security.md §Secrets);
- Deploy-/Composeänderungen, Image-Versionen, Migrationen auslösen;
- den Audit-Trail editieren (nur lesen; Retention läuft als Job und
protokolliert sich selbst);
- Backups herunterladen ist möglich (In-App-Restore-Pfad, #103), aber
ihre Vernichtung auf Host/Spiegel nicht.
Zu beachten: ein Site-Admin **liest** per Rollen-Bypass jeden Inhalt
der Instanz (permissions.md) — die Trennung „wer darf Inhalte sehen"
von „wer betreibt die Plattform" ist damit App-seitig bewusst NICHT
absolut; wo das nicht tragbar ist, ist es organisatorisch zu regeln
(Vier-Augen-Prinzip bei Vergabe der Site-Admin-Rolle). Der Lesetrail
für eingestufte Inhalte (M29, #222#225) macht solche Zugriffe
nachvollziehbar.
## 7 Monitoring und Störung
Referenz: `deploy/monitoring.md`, `docs/architecture/operations.md`
§Health & monitoring.
- `GET /api/v1/readyz` prüft DB, Migrationen, Konverter, Renderer,
Backup-Status; `healthz` je Container. Externe Überwachung der
Referenzinstanzen: Uptime-Kuma mit Alarmierung.
- Logs: alle Dienste loggen JSON auf stdout (Docker json-file mit
Rotation); Audit-Ereignisse mit `msg`-Präfix `audit: ` und
`severity`-Feld — Weiterleitung an SIEM/Syslog übernimmt der
Collector des Betreibers (Katalog:
`docs/architecture/audit-events.md`, v1.1, mit
Kompatibilitätsversprechen).
- Integritätsalarm: `file.integrity_failed` (critical) = Download-Hash
≠ Upload-Hash → Objekt als manipuliert/korrupt behandeln, Datei aus
Backup-Set wiederherstellen (Runbook), erneut laden; der
Audit-Eintrag trägt beide Hashes.

View File

@ -0,0 +1,154 @@
# Restrisikoliste (VS-NfD, Issue #231)
Zweck: benennen, was **bewusst** offen bleibt. Ein Prüfer, der eine
Lücke hier bereits verzeichnet findet, kann ihr zustimmen; eine
unverzeichnete Lücke diskreditiert die gesamte Einreichung. Jeder
Eintrag nennt das Risiko, warum es akzeptiert ist, die kompensierende
Kontrolle und wer entschieden hat.
**Pflegeregel (verbindlich):** Schließt ein Issue mit einem wissentlich
offenen Rest, wird diese Liste **im selben PR** ergänzt. Die formale
Billigung der Einträge durch den jeweiligen Betreiber ist Teil seiner
Risikoübernahme (Abgrenzungserklärung §8) — Bewertung/Scoring ist
bewusst nicht Teil dieser Liste.
Entscheidungsvermerk: „Projektleitung" = Stefan Waidele;
Entscheidungen sind über die genannten PRs/Issues/ADRs im Repository
nachvollziehbar.
---
## R-01 Attachment-Inhalt trägt keine interne Kennzeichnung
- **Risiko:** Eine heruntergeladene, umbenannte Datei (bzw. eine ohne
Begleitdatei weitergegebene) ist ein eingestuftes Binärobjekt ohne
sichtbare Kennzeichnung.
- **Warum akzeptiert:** Kennzeichnung **in** beliebige Binärformate zu
schreiben hieße, fremde Formate umzuschreiben — ausgeschlossen durch
ADR 0019/0022 (keine Übernahme von Grundfunktions-/Formatgarantien).
- **Kompensation:** Download-Präfix `VS-NfD_` + Begleitdatei/Manifest
im ZIP (#212); alle internen Darstellungen und alle übrigen
Exportkanäle kennzeichnen selbst (#206#211); Upload-Warnung bzw.
serverseitiger Block (#213); organisatorische VS-Handhabung beim
Empfänger.
- **Entscheidung:** Projektleitung, PR #271 / Issue #212, 31.07.2026.
## R-02 Lokale Passwort-Authentifizierung noch nicht abschaltbar
- **Risiko:** Bis #216 (M27) existiert kein Schalter
`auth.local.enabled=false`; die Anwendung führt eigene
Passwort-Konten, obwohl die Behördenumgebung Fremdauthentisierung
vorsieht. Zusätzlich ist noch offen, ob der Schalter zur Laufzeit
umschaltbar sein wird oder einen Neustart verlangt — das entscheidet
#216 und trägt es hier nach.
- **Warum akzeptiert:** Reihenfolge der Umsetzung (M26 vor M27);
produktiver VS-NfD-Betrieb beginnt erst nach M27.
- **Kompensation:** Referenzkonfiguration (`50-haertungsleitfaden.md`):
Registrierung geschlossen, kurze Sessions (12 h absolut / 2 h idle),
Argon2id-Hashes, Rate-Limits, Audit der Anmeldungen.
- **Entscheidung:** Projektleitung, Maßnahmenplan Rev. 2 (M27-Planung),
30.07.2026.
## R-03 Plugin-Hash-Pinning verschoben
- **Risiko:** Installierte Plugin-Pakete sind nicht gegen einen
festgeschriebenen Hash verankert (#232); ein manipuliertes Paket
gleichen Namens wäre beim Neuinstallieren nicht erkennbar.
- **Warum akzeptiert:** Die Referenzkonfiguration betreibt Plugins
**gar nicht** (`plugins.enabled=false`, #200) — der Kill-Switch deckt
den VS-NfD-Betriebsmodus vollständig; Pinning lohnt erst, wenn ein
Betreiber Plugins tatsächlich freigibt.
- **Kompensation:** Kill-Switch (alle Plugin-Flächen 404, Dropzone
quarantänisiert); Sandbox mit Capability-Modell + CI-Escape-
Regressionstest (ADR 0008/0025); Install nur durch Site-Admin.
- **Entscheidung:** Projektleitung, ADR 0025 / Issue #232, 30.07.2026.
## R-04 Git-Historie: einmalige Secret-Prüfung mit begrenztem Muster
- **Risiko:** Die einmalige Prüfung der gesamten Repository-Historie
(#198, 30.07.2026) fand **keine** getrackten `.env`-Dateien und
**keine** Treffer der Muster Private-Key-Block / AKIA / ghp\_ /
glpat- / xox[baprs]-. Unstrukturierte Passwörter als schlichte
Strings würde ein generisches Muster jedoch nicht finden.
- **Warum akzeptiert:** Ein Negativbeweis über beliebige Strings ist
nicht führbar; das strukturierte Muster deckt die realistischen
Token-Formate.
- **Kompensation:** CI-Zaun „No tracked .env files or secret material"
auf jedem PR (#198) hält beide Invarianten ab jetzt; Review-Disziplin
- Regel „Passwörter nie als CLI-Argument/Commit".
- **Entscheidung:** Projektleitung, Issue #198, 30.07.2026 (Protokoll
als Kommentar auf #231).
## R-05 Digest-/Benachrichtigungs-Mails tragen Seitentitel (I-23)
- **Risiko:** Zugestellte Mails enthalten Titel (Inhalts-Metadaten)
eingestufter Seiten und liegen beim Empfänger außerhalb der Instanz.
- **Warum akzeptiert:** Es gab bis M26 kein Einstufungs-Metadatum für
eine Unterdrückung; die titelbasierte Unterdrückung wird in M32
(#243#246, `VS_NFD_MODE`) revisitiert.
- **Kompensation:** Kopie zeitlich begrenzt
(`mail.outboxRetentionDays`, #234); Referenzkonfiguration lässt SMTP
unkonfiguriert — dann verlässt kein Titel die Instanz per Mail
(`50-haertungsleitfaden.md` §1.2).
- **Entscheidung:** Projektleitung, PR #255 / Issue #234, 30.07.2026;
Revisit-Marker M32.
## R-06 `page_links.target_slug` überlebt den Ziel-Purge (I-24)
- **Risiko:** Nach dem endgültigen Löschen einer Seite bleibt ihr Slug
(≈ Titel) als Linkziel-Text in den Zeilen verweisender Seiten.
- **Warum akzeptiert:** Der Slug steht ohnehin sichtbar im Inhalt der
verweisenden Seite (deren Autor das Ziel lesen durfte); die Zeile zu
löschen entfernte nichts Sichtbares, bräche aber die gewollte
Phantom-Link-Reauflösung.
- **Kompensation:** Zugriff auf die verweisende Seite bleibt
permissions-geprüft; vollständige Begründung in
`docs/architecture/operations.md` (Purge-Abschnitt).
- **Entscheidung:** Projektleitung, PR #256 / Issue #235, 30.07.2026.
## R-07 Offline-Kopie im Browser (IndexedDB, I-25)
- **Risiko:** Der Editor hält zuletzt geöffnete Seiten als
Yjs-Offline-Kopie in der IndexedDB des Endgeräts
(`apps/web/src/editor/use-collab-provider.ts`); sie übersteht
Browser-Crashes und existiert für unsynchronisierte Offline-Edits.
- **Warum akzeptiert:** Die Kopie ist Voraussetzung für
verlustfreies kollaboratives Arbeiten (CRDT) und liegt in der
Endgeräte-Zone, deren Schutz (Festplattenverschlüsselung,
Gerätekontrolle) nach Abgrenzungserklärung ohnehin der Plattform-
bzw. Organisationsverantwortung zufällt.
- **Kompensation:** VS-Endgeräte-Vorgaben des Betreibers
(Datenträgerverschlüsselung, kontrollierte Geräte); kurze Sessions
(Referenzkonfiguration) begrenzen den angemeldeten Zeitraum.
- **Entscheidung:** Projektleitung, Ist-Aufnahme I-25
(`10-ist-aufnahme.md`), 29.07.2026.
## R-08 Bewusst nicht geplante Funktionen
- **Risiko/Inhalt:** Externe Suchengine (Suche bleibt in Postgres),
Admin-Freigabe von Selbstregistrierungen, Plugin-Netzwerk-Allowlist —
bewusst unscheduled.
- **Warum akzeptiert:** Kein Bedarf im Zielbetrieb: die
Referenzkonfiguration schließt Selbstregistrierung und Plugins ohnehin
aus; die interne Suche vermeidet einen weiteren Dienst mit
Inhaltskopie (die Kopienliste bliebe sonst nicht klein).
- **Kompensation:** entfällt (kein zusätzliches Risiko gegenüber dem
Status quo; Wiedervorlage bei realem Bedarf).
- **Entscheidung:** Projektleitung, Projektplanung (dokumentiert im
Workspace-Handoff), Stand 07/2026.
## R-09 Site-Admin liest jeden Inhalt (Rollen-Bypass)
- **Risiko:** Die App-Rolle Site-Admin umgeht die Permission-Resolution
vollständig (permissions.md) — App-seitig gibt es keine absolute
Trennung zwischen Plattform-Betrieb und Inhalts-Kenntnisnahme.
- **Warum akzeptiert:** Ein Admin ohne Durchgriff könnte zentrale
Pflichten (Purge, Quoten, Störungsanalyse) nicht erfüllen; eine
echte Trennung wäre eine neue Grundfunktion (ADR 0019).
- **Kompensation:** organisatorisch (Vergabe der Rolle nach
Need-to-know, Vier-Augen-Prinzip); Lesetrail für eingestufte Inhalte
macht Zugriffe nachvollziehbar (M29, #222#225 — bis dahin trägt der
bestehende Audit-Trail nur Verwaltungsereignisse); Betriebshandbuch
§6 benennt die Grenze ausdrücklich.
- **Entscheidung:** Projektleitung, permissions.md-Design (Issue #51)
bzw. Aufnahme hier, 31.07.2026.