#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
This commit is contained in:
Claude Fable 5 2026-07-31 17:04:43 +02:00
parent 4f6596e8a2
commit 0462f5702f
3 changed files with 123 additions and 7 deletions

View File

@ -73,7 +73,8 @@ gehen" ist dort eine schlechtere Antwort als „getestet, hier ist die Anleitung
- [x] Mirror-Verfahren in interne Registry dokumentieren · 1 AT · #218
- [x] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
Prebuilt-Images) · 23 AT · #219
- [ ] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220
- [x] Testlauf in netzisolierter Umgebung, Protokoll als Beleg · 2 AT · #220
`97-isolationslauf-protokoll.md`
- [ ] Offline-Update-Pfad inkl. Migrationen · 23 AT · #221
---
@ -234,8 +235,9 @@ fertiges Produkt ohne Gesprächspartner.
kein Ausschlusskriterium (Ist-Aufnahme I-40)
- [x] Fließen Labels heute in Exporte? **Nein**`export.service.ts` lädt
`labelIds` nur für die Permission-Filterung (Ist-Aufnahme I-03a)
- [ ] Was bricht ohne Internetzugang? → wird durch den Testlauf #220
beantwortet (Ist-Aufnahme I-28)
- [x] Was bricht ohne Internetzugang? **Nur die Mail-Zustellung** — der
Testlauf #220 zeigt null Egress-Pakete außer dem SMTP-Versuch;
Beleg: `97-isolationslauf-protokoll.md` (Ist-Aufnahme I-28)
- [x] MCP-Gate-Duplikat: **divergiert nicht** — nutzt den zentralen
`PermissionService`, eigenständig sind nur die Schalter
(Ist-Aufnahme I-33)

View File

@ -63,10 +63,17 @@ dokumentiert die realen Instanzen).
einzige Grenze: das installierbare drawio-Plugin-ZIP braucht sein
vorab abgelegtes Vendor-Tarball). Entscheidung und Prozedur:
ADR 0024 §Decisions, Nachweis: `96-offline-build-protokoll.md`.
- ⏳ offen: 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).
- **Isolationslauf (#220): ✅ live verifiziert.** Vollständiges
Deployment (Setup, Login, Live-Kollaboration, Suche, Upload, alle
Exporte, Backup + Restore) in einer Umgebung ohne jeden Egress-Pfad
(`internal: true`-Netze, tcpdump-Vollcapture: null Pakete nach
außen). Einzige nach außen gerichtete Verbindung ist SMTP; ohne
Egress bleibt die Instanz voll funktionsfähig, nur die
Mail-Zustellung scheitert kontrolliert (Outbox, 5 Versuche, dann
FAILED). Nachweis: `97-isolationslauf-protokoll.md`.
- ⏳ offen: 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

View File

@ -0,0 +1,107 @@
# 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.