#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:
parent
4f6596e8a2
commit
0462f5702f
@ -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] Mirror-Verfahren in interne Registry dokumentieren · 1 AT · #218
|
||||||
- [x] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
|
- [x] Build ohne Netz reproduzierbar (pnpm Offline-Store / reine
|
||||||
Prebuilt-Images) · 2–3 AT · #219
|
Prebuilt-Images) · 2–3 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 · 2–3 AT · #221
|
- [ ] Offline-Update-Pfad inkl. Migrationen · 2–3 AT · #221
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -234,8 +235,9 @@ fertiges Produkt ohne Gesprächspartner.
|
|||||||
kein Ausschlusskriterium (Ist-Aufnahme I-40)
|
kein Ausschlusskriterium (Ist-Aufnahme I-40)
|
||||||
- [x] Fließen Labels heute in Exporte? **Nein** — `export.service.ts` lädt
|
- [x] Fließen Labels heute in Exporte? **Nein** — `export.service.ts` lädt
|
||||||
`labelIds` nur für die Permission-Filterung (Ist-Aufnahme I-03a)
|
`labelIds` nur für die Permission-Filterung (Ist-Aufnahme I-03a)
|
||||||
- [ ] Was bricht ohne Internetzugang? → wird durch den Testlauf #220
|
- [x] Was bricht ohne Internetzugang? **Nur die Mail-Zustellung** — der
|
||||||
beantwortet (Ist-Aufnahme I-28)
|
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
|
- [x] MCP-Gate-Duplikat: **divergiert nicht** — nutzt den zentralen
|
||||||
`PermissionService`, eigenständig sind nur die Schalter
|
`PermissionService`, eigenständig sind nur die Schalter
|
||||||
(Ist-Aufnahme I-33)
|
(Ist-Aufnahme I-33)
|
||||||
|
|||||||
@ -63,10 +63,17 @@ dokumentiert die realen Instanzen).
|
|||||||
einzige Grenze: das installierbare drawio-Plugin-ZIP braucht sein
|
einzige Grenze: das installierbare drawio-Plugin-ZIP braucht sein
|
||||||
vorab abgelegtes Vendor-Tarball). Entscheidung und Prozedur:
|
vorab abgelegtes Vendor-Tarball). Entscheidung und Prozedur:
|
||||||
ADR 0024 §Decisions, Nachweis: `96-offline-build-protokoll.md`.
|
ADR 0024 §Decisions, Nachweis: `96-offline-build-protokoll.md`.
|
||||||
- ⏳ offen: Testlauf in isolierter Umgebung (#220), Offline-Update-Pfad
|
- **Isolationslauf (#220): ✅ live verifiziert.** Vollständiges
|
||||||
(#221); Meilenstein M28. Bereits vorhanden als Grundlage: alle
|
Deployment (Setup, Login, Live-Kollaboration, Suche, Upload, alle
|
||||||
Dritt-Images digest-gepinnt (#203), ein authoritativer Node-Pin
|
Exporte, Backup + Restore) in einer Umgebung ohne jeden Egress-Pfad
|
||||||
(#236), SBOMs je Release (#202).
|
(`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
|
## 2 Update und Rollback
|
||||||
|
|
||||||
|
|||||||
107
docs/vs-nfd/97-isolationslauf-protokoll.md
Normal file
107
docs/vs-nfd/97-isolationslauf-protokoll.md
Normal 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.
|
||||||
Loading…
Reference in New Issue
Block a user