#227: hardening guide / VS-NfD reference configuration #275
@ -184,13 +184,14 @@ 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 · 4–5 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 · 4–5 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" · 3–4 AT · #230
|
||||
- [ ] **Restrisikoliste** mit bewusst offenen Punkten · 1 AT · #231
|
||||
|
||||
@ -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`).
|
||||
|
||||
94
docs/vs-nfd/50-haertungsleitfaden.md
Normal file
94
docs/vs-nfd/50-haertungsleitfaden.md
Normal 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).
|
||||
277
docs/vs-nfd/60-sicherheitsdokumentation.md
Normal file
277
docs/vs-nfd/60-sicherheitsdokumentation.md
Normal 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).
|
||||
210
docs/vs-nfd/70-betriebshandbuch.md
Normal file
210
docs/vs-nfd/70-betriebshandbuch.md
Normal 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.
|
||||
Loading…
Reference in New Issue
Block a user