diff --git a/docs/vs-nfd/20-massnahmenplan.md b/docs/vs-nfd/20-massnahmenplan.md
index 9b36183..0d62aa9 100644
--- a/docs/vs-nfd/20-massnahmenplan.md
+++ b/docs/vs-nfd/20-massnahmenplan.md
@@ -187,8 +187,8 @@ nach sich zieht.
- [ ] **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
+- [x] **Sicherheitsdokumentation**: Architektur, Datenflüsse, Netzplan,
+ Ports/Dienste, Vertrauensgrenzen · 4 AT · #228 → `60-sicherheitsdokumentation.md`
- [ ] **Betriebshandbuch**: Installation (inkl. Airgap), Update, Backup/Restore,
Löschung und Vernichtung, Rollentrennung · 4–5 AT · #229
- [ ] **Zuarbeit IT-Grundschutz** APP.3.1 und CON.11.1, je Anforderung
diff --git a/docs/vs-nfd/60-sicherheitsdokumentation.md b/docs/vs-nfd/60-sicherheitsdokumentation.md
new file mode 100644
index 0000000..0cc7231
--- /dev/null
+++ b/docs/vs-nfd/60-sicherheitsdokumentation.md
@@ -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),
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,
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,
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 | ``-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).