dorfteich/docs/vs-nfd/97-isolationslauf-protokoll.md
Claude Fable 5 0462f5702f #220: protocol of the isolated deployment run
Full deployment exercised in a compose stack whose networks are all
internal: true — setup, login, live collaboration, search, upload, all
export formats, backup and restore. tcpdump full capture on both
bridges: zero packets leave the isolated subnets; the only outbound
attempt the application makes is SMTP, which fails contained in the
outbox (5 retries, then FAILED) while the instance stays fully
functional. The restore finding became #288, fixed earlier in this
chain and re-verified in the same stack. Plan checkbox P1-3 and the
I-28 open question ticked; operations handbook airgap section updated.

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

108 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Isolationslauf-Protokoll — Deployment in netzisolierter Umgebung (Issue #220)
Nachweis für Maßnahmenplan P1-3 (ADR 0024): ein vollständiges Deployment
läuft mit blockiertem Egress. Beantwortet zugleich die offene Frage der
Ist-Aufnahme I-28 („Was bricht ohne Internetzugang?"). Belegstufe:
**live verifiziert** — alle Ergebnisse stammen aus dem beschriebenen
Lauf, Abweichungen sind ausgewiesen.
- **Datum:** 2026-07-31
- **Host:** lokaler Docker-Host (aarch64); Compose-Projekt
`dorfteich-isolated` aus dem Produktions-Compose
(`deploy/compose/docker-compose.yml`) plus Override.
- **Netzisolation:** beide Compose-Netze (`frontend`, `internal`) mit
`internal: true` — Docker legt die Bridges ohne NAT/Default-Route an;
es existiert kein Egress-Pfad. Port-Publishing ist dadurch wirkungslos,
alle Prüfzugriffe erfolgen aus einem Treiber-Container
(`node:22.15.1-alpine`, fetch-basierte Prüfskripte) im `frontend`-Netz
gegen `http://api:3000`.
- **Eigene Images:** `dorfteich-iso-{web,api,collab,backup}:isolated`,
lokal gebaut aus `main@a758c9d`; das backup-Image nach dem Befund
unten neu aus `4f6596e` (#288). Image-IDs: web `9d65c54f05fc`, api
`1826dedd662a`, collab `8a1b2841b91c`, backup `117ff18c7e71`.
- **Dritt-Images (digest-gepinnt, Compose-Pins):**
`postgres:17.5-alpine@sha256:6567bca8…`,
`pandoc/core:3.6@sha256:5b8a29d9…`,
`gotenberg/gotenberg:8@sha256:67097317…`.
- **Egress-Beweisführung:** `tcpdump -n -l not arp` auf beiden
Bridge-Interfaces des Hosts über die gesamte Laufzeit (Setup bis nach
dem Restore-Re-Run).
## Prüfschritte und Ergebnisse
| Schritt | Ergebnis |
| ------------------------------ | ---------------------------------------------------------------------------------------------- |
| Installation / First-Run-Setup | ✅ migrate-on-start (41 Migrationen) + Pre-Seed-Setup über `SETUP_*`-Env (Registrierung closed) |
| `/readyz` | ✅ alle Checks ok (backup-Check erst nach dem ersten Backup-Lauf, s. Abweichung 1) |
| Login (Session-Cookie) | ✅ Status 200 |
| Seite anlegen | ✅ Seite `3331f670…` im Personal-Teich |
| Live-Kollaboration | ✅ echter Collab-Client (HocuspocusProvider) über den collab-Dienst, Inhalt synced + persistiert |
| Markdown-Export | ✅ 78 Zeichen, Inhalt vorhanden |
| Suche (Postgres-Volltext) | ✅ 1 Treffer auf den Seiteninhalt |
| Datei-Upload + Download | ✅ 201 / 200 |
| DOCX-Export (pandoc-Sidecar) | ✅ 9 926 Bytes |
| PDF-Export (Gotenberg-Sidecar) | ✅ 20 903 Bytes |
| Mail-Enqueue | ✅ 204 — Zustellverhalten s. eigener Abschnitt |
| Backup | ✅ Set `20260731-132200` (Dump + Volume-Archiv), readyz-backup-Check danach ok |
| Restore | ✅ nach Fix #288 — Exit 0, Inhalt exakt der Backup-Stand (s. Abweichung 2) |
## Egress-Bilanz
Vollständige Capture-Auswertung (alle Pakete, beide Bridges):
| Bridge | Pakete | Ziel-Verteilung |
| ---------- | ------ | --------------------------------------------------------------------------- |
| `internal` | 37 897 | ausnahmslos `192.168.156.0/24``192.168.156.0/24` (api/collab/backup ↔ db) |
| `frontend` | 448 | ausnahmslos `192.168.158.0/24``192.168.158.0/24` (Treiber ↔ api/Sidecars) |
Einzige weitere Frames: 2 × IPv6 Link-Local → `ff02::16` (MLDv2
Multicast Listener Report, Kernel-Housekeeping der Bridge — kein
Egress). **Kein einziges Paket an ein Ziel außerhalb der isolierten
Subnetze, keine DNS-Anfragen auf den Bridges.** Die Anwendung
unternimmt genau **einen** nach außen gerichteten Verbindungsversuch:
SMTP (s. u.). Damit ist I-28 beantwortet: ohne Internetzugang bricht
nichts außer der Mail-**Zustellung**; Telemetrie-, Update-, CDN- oder
Font-Abrufe existieren nicht (bestätigt die Ist-Aufnahme §0.2).
## SMTP-Verhalten ohne Egress
Die einzige nach außen gerichtete Verbindung; genau der Punkt, den eine
Behörde zulassen oder untersagen wird:
- `POST`-Auslöser antwortet **204** — die Outbox entkoppelt Annahme und
Zustellung; kein Nutzer-Request hängt oder scheitert an fehlendem
Egress.
- Der Zustell-Worker scheitert bei jedem Versuch an der
DNS-Auflösung: `getaddrinfo EAI_AGAIN smtp.egress-nachweis.example`
(Dockers embedded DNS löst in `internal`-Netzen nicht extern auf; auf
den Bridges ist dabei kein Paket sichtbar).
- Retry-Leiter: 5 Versuche, danach Log
`mail delivery failed permanently` (Level warn), Outbox-Status
`FAILED` mit `last_error`. Die Instanz bleibt voll funktionsfähig.
- **Betriebsempfehlung:** entweder ein von der Behörde zugelassenes
internes SMTP-Relay konfigurieren oder bewusst ohne Mail betreiben
(Einschränkung: keine Verifikations-/Benachrichtigungs-Mails;
Site-Admin legt Konten dann manuell verifiziert an).
## Abweichungen und Befunde
1. **readyz-backup-Check `warn` vor dem ersten Backup-Lauf** („no backup
status recorded yet") — erwartetes Verhalten des Status-File-Checks,
nach dem ersten Lauf `ok`. Keine Maßnahme.
2. **Restore meldete FAILED trotz inhaltlich korrekter
Wiederherstellung** → Befund **#288**: `pg_restore --clean` scheiterte
an den geerbten Partitions-PKs von `read_events` (partitioniert seit
#224), Exit ≠ 0 bei 4 ignorierten Fehlern. Fix (Schema-Reset vor
`pg_restore`, macht den Restore zugleich exakt) in derselben
PR-Kette; Re-Run im selben isolierten Stack: Exit 0, bewusst
angelegte Post-Backup-Tabelle entfernt, Seiteninhalt und alle
`read_events`-Partitionen intakt, `/readyz` danach vollständig grün.
## Rückfluss (Akzeptanzkriterium)
- **#288** — aus dem Lauf entstandener Fix, in der M28-PR-Kette.
- **#221** — der Offline-Update-Pfad wird in derselben isolierten
Umgebung mit denselben Images geprobt.
- **#229** — Betriebshandbuch §1 (Airgap-Variante) auf diesen Nachweis
aktualisiert.