From 722c149f168c37212ef49f584bd53bd3ef777eaf Mon Sep 17 00:00:00 2001 From: Thore Cimbal Date: Fri, 21 Aug 2026 12:00:00 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20Gate=203=20for=20#0088=20=E2=80=94=20an?= =?UTF-8?q?d=20a=20hole=20in=20yesterday's=20restriction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from the preparation change the design. There is no split-horizon DNS: every public name of the platform resolves inside the cluster to the node's external address. Services that reach each other by public name therefore leave the cluster and come back through Traefik. And traffic to the node's own address is not subject to the policy. Measured twice, from two workloads restricted since the 20th, with different tools: the storage box, rohana and 1.1.1.1 are all blocked, the node address is reachable, and CoreDNS answers as the positive control. The restriction works - just not against the node itself. That is a property of the k3s enforcement, not a manifest error, and no NetworkPolicy can close it, because policies allow rather than forbid. It cuts both ways. Class A grows and gets safer, because services talking over public names keep working under restriction. And the ten workloads restricted yesterday can still reach anything published through Traefik - the gap between what the manifest promises and what holds belongs written down rather than smoothed over. I also walked into the trap my own acceptance criterion warns about. The first counter-probe used /dev/tcp in a container whose sh does not have it, so everything came back "blocked", including the reachable node. Same mistake as on the 20th, in the undertaking whose criterion 4 names it. Repeated with nc and a positive control; only then was the result worth anything. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01F2Q4Ri8NGwyTZzScvKnWFM --- STATUS.md | 2 +- docs/design/2026-08-21-egress-vollstaendig.md | 128 +++++++++++++++++- docs/ledger/2026-08-21-egress-vollstaendig.md | 11 ++ 3 files changed, 138 insertions(+), 3 deletions(-) diff --git a/STATUS.md b/STATUS.md index 79e5ba0..a5e0c20 100644 --- a/STATUS.md +++ b/STATUS.md @@ -89,7 +89,7 @@ Bedeutung der Meilensteine: siehe [roadmap.md](roadmap.md). | Design | Gate | Title | |---|---|---| -| [2026-08-21-egress-vollstaendig](docs/design/2026-08-21-egress-vollstaendig.md) | gate-2 | Design: Den ausgehenden Verkehr zu Ende schließen (#0088) | +| [2026-08-21-egress-vollstaendig](docs/design/2026-08-21-egress-vollstaendig.md) | gate-3 | Design: Den ausgehenden Verkehr zu Ende schließen (#0088) | ## ADRs (24) diff --git a/docs/design/2026-08-21-egress-vollstaendig.md b/docs/design/2026-08-21-egress-vollstaendig.md index 73f7866..8c9e536 100644 --- a/docs/design/2026-08-21-egress-vollstaendig.md +++ b/docs/design/2026-08-21-egress-vollstaendig.md @@ -1,6 +1,6 @@ --- type: design -status: gate-2 +status: gate-3 date: 2026-08-21 size: L related: @@ -178,4 +178,128 @@ das Ziel hinter einem CDN liegt.** Das ist eine dauerhafte Richtung, sie begründet eine bleibende Ausnahme (ClamAV), und ohne sie wiederholt der nächste Durchlauf die Cloudflare-Abwägung von vorn. -> **STOP — Freigabe für Gate 2.** +> **Gate 2 freigegeben durch sorb, 2026-08-21.** + +## Gate 3 — Program Design + +### ⚠️ Zwei Funde aus der Vorbereitung, die den Entwurf ändern + +**1. Es gibt kein Split-Horizon-DNS.** Alle öffentlichen Namen der +Plattform lösen **im Cluster** auf die externe Knoten-Adresse auf: + +``` +account.axion1337.chat 49.13.132.245 EXTERN +matrix.axion1337.chat 49.13.132.245 EXTERN +auth.axion1337.chat 49.13.132.245 EXTERN +``` + +Jeder Dienst, der einen anderen über dessen **öffentlichen** Namen +anspricht, verlässt also den Cluster und kommt über Traefik zurück. + +**2. Verkehr zur Knoten-Adresse unterliegt der Policy nicht.** Aus einer +Arbeitslast, die seit dem 2026-08-20 „nur intern" darf, gemessen — zweimal, +aus `draupnir` und aus `element-admin`, mit unterschiedlichen Werkzeugen: + +| Ziel | Ergebnis | +|---|---| +| Storage Box `91.98.246.178:23` | **blockiert** | +| rohana `188.245.193.243:443` | **blockiert** | +| `1.1.1.1:443` | **blockiert** | +| Knoten-IP `49.13.132.245:443` | **ERREICHBAR** | +| CoreDNS `10.43.0.10:53` (Positivkontrolle) | erreichbar | + +Die Sperre wirkt also — aber **nicht gegen die eigene Knoten-Adresse**. Das +ist eine Eigenschaft der k3s-Durchsetzung, kein Fehler im Manifest, und mit +einer NetworkPolicy nicht zu schließen: Policies erlauben, sie verbieten +nicht, und dieser Pfad wird vor den Policy-Ketten bedient. + +**Was daraus folgt, in beide Richtungen.** Angenehm: Dienste, die sich über +öffentliche Namen erreichen, funktionieren unter Einschränkung weiter — die +Klasse A ist dadurch größer und ungefährlicher als gedacht. Unangenehm: Die +zehn seit gestern „nur intern" laufenden Arbeitslasten können weiterhin +**jeden über Traefik veröffentlichten Endpunkt** erreichen. Der Unterschied +zwischen der Zusage des Manifests und der Wirklichkeit gehört benannt, +nicht kleingeredet. + +⚠️ **Und ich habe mir dabei die Falle aus Kriterium 4 selbst gestellt.** +Die erste Gegenprobe in `element-admin` nutzte `/dev/tcp` — eine +bash-Eigenheit, die in dessen `sh` **immer** fehlschlägt und deshalb alles +als „blockiert" meldete, auch den erreichbaren Knoten. Dasselbe Werkzeug, +derselbe Fehler wie am 20.08., in demselben Vorhaben, dessen +Abnahmekriterium ausdrücklich davor warnt. Wiederholt mit `nc` **und** einer +Positivkontrolle gegen CoreDNS — ohne die wäre „blockiert" wieder nur eine +Vermutung gewesen. + +### Files + +*Angefasst:* + +| Pfad | Änderung | +|---|---| +| `apps/production/networkpolicy.yaml` | Welle 1: Klasse A in `egress-nur-intern` aufnehmen. Welle 2: neue Policy `egress-ein-ziel` mit den `/32`. Welle 3: Begründungen als Kommentar an Klasse C | +| `apps/authentik/networkpolicy.yaml` | Welle 3: `egress-nur-intern` für den Namespace, Ausnahme für `authentik-backup` → Storage Box | +| `apps/monitoring/networkpolicy.yaml` | Welle 3: dito, Ausnahmen nach Bedarf der dortigen Arbeitslasten | + +*Neu:* `docs/adr/00NN-egress-ausnahmen-als-cidr.md` im management-Repo. + +### Auswahl und Regelform + +```yaml +# Klasse A — vorhandene Policy, Liste erweitert +podSelector: { matchExpressions: [{ key: app.kubernetes.io/name, operator: In, values: [ … ] }] } +egress: + - to: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: kube-system } } }] + ports: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }] + - to: [{ ipBlock: { cidr: 10.42.0.0/16 } }, { ipBlock: { cidr: 10.43.0.0/16 } }] + +# Klasse B — dieselbe Basis, plus genau ein Ziel je Regel + - to: [{ ipBlock: { cidr: 91.98.246.178/32 } }] + ports: [{ protocol: TCP, port: 23 }] +``` + +Die Zuordnung Job-artiger Arbeitslasten greift **erst beim nächsten Lauf**, +weil der Pod die Labels seines Templates trägt. Der Nachweis kommt deshalb +aus einem ausgelösten Lauf. + +### Test assertions + +1. **Bestand:** Zahl der Arbeitslasten, für die nur `egress-block-metadata` + gilt, ist **0** — je Namespace, nach `status.phase` getrennt. +2. **Sperre wirkt:** Aus je einer neu eingeschränkten Arbeitslast sind + `91.98.246.178:23`, `188.245.193.243:443` und `1.1.1.1:443` **nicht** + erreichbar. ⚠️ Immer mit Positivkontrolle (CoreDNS) im selben Lauf und + **nie** mit `/dev/tcp`. +3. **Erlaubnis wirkt:** Aus `synapse-backup` ist die Storage Box erreichbar, + aus `turn-secret-rotation` rohana — je im ausgelösten Lauf. +4. **Kein Ausfall:** Föderation, Anmeldung, ein Call, ein Backup-Lauf, + ClamAV-Signaturen, Wiki-Zugriff — jede Zeile mit Ausgabe. +5. **Rückweg geprüft:** Für Welle 1 wird ein `git revert` einmal + durchgespielt und der Ausgangszustand gemessen, bevor Welle 2 beginnt. + +### Boundaries — DO NOT CHANGE + +- **Ingress-Regeln**, `default-deny-ingress`, `egress-block-metadata`. +- **`flux-system`** und `kube-system`. +- **Synapses `url_preview_ip_range_blacklist`** — die Kontrolle bleibt, wo + sie ist; die Vorschau wird nicht abgeschaltet. +- **Die Knoten-Adresse.** Sie ist nicht zu sperren, nur zu dokumentieren. +- Angenommene ADRs, `docs/sources/**`. + +### Shakiest calls + +1. **Die Klasse-A-Liste beruht auf Zweck, nicht auf Messung.** Für + `init-secrets`, `synapse-check-config` und die Deployment-Marker habe ich + keinen Lauf beobachtet — sie laufen nur beim Deploy. Wenn einer davon + doch nach draußen muss, fällt es beim nächsten ESS-Rollout auf, nicht + heute. Deshalb ist Welle 1 die erste und der Rückweg vorher geprobt. +2. **MAS bleibt vorerst in Klasse C.** Die Konfiguration steht nicht im + Repo, der Container ist distroless, und Anmeldung ist der Dienst, den man + am wenigsten kaputt machen will. Nach Fund 2 spricht viel dafür, dass er + einschränkbar ist — belegt ist es nicht. +3. **Die Knoten-Adresse als offener Pfad** ist gemessen, aber ihre + Reichweite nicht: Was genau über Traefik erreichbar ist, hängt an den + Ingress-Regeln und ist hier nicht auserzählt. +4. **`nc -z` misst TCP-Verbindungsaufbau, nicht Nutzbarkeit.** Für UDP + (coturn, SFU) sagt es nichts — deren Prüfung braucht einen echten Call. + +> **STOP — Freigabe für Gate 3.** diff --git a/docs/ledger/2026-08-21-egress-vollstaendig.md b/docs/ledger/2026-08-21-egress-vollstaendig.md index 437af92..ed3686b 100644 --- a/docs/ledger/2026-08-21-egress-vollstaendig.md +++ b/docs/ledger/2026-08-21-egress-vollstaendig.md @@ -14,6 +14,7 @@ related: | gate | commit | approval | status | note | |---|---|---|---|---| +| 2 | 1dc5439 | sorb | DONE | Drei Klassen, CIDR-only, ClamAV als harter Fall benannt. | | 1 | 04b1b84 | sorb | DONE | Umfang und vier zaehlbare Abnahmekriterien; #0088 auf in-progress. | ## Ladder @@ -24,9 +25,19 @@ related: | die Bestandsaufnahme aus der Vormessung, statt neu zu zaehlen | 10 eingeschraenkt, 7 laufend offen, 8 Job-artig offen, 2 Namespaces gar nicht erfasst | reused: die Zahlen aus #0088 vom selben Tag, samt Methodik | | | die Durchsetzungsschicht, bevor eine Regelform gewaehlt wird | kein separater CNI-/Policy-Pod — k3s' eingebauter Controller, also Standard-NetworkPolicy | reused: die bestehende Erlaubnisform (DNS + 10.42/16 + 10.43/16), kein neues Modell | | | ob IPv6 eine Umgehung waere | podCIDRs nur IPv4, v6-Verbindung scheitert mit OSError | reused: IPv4-CIDRs genuegen — kein Zusatzaufwand fuer eine Adressfamilie, die es nicht gibt | | +| das git-storage-Ziel von wikijs, statt es zu raten | steht nicht im Manifest, sondern in der Wiki.js-Datenbank: `https://rohana.axion1337.de/sorb/ThreadNetWiki.git` | reused: die laufende Konfiguration als Quelle — Klasse B bestaetigt | | +| ob die Sperre von gestern ueberhaupt beisst, bevor neue dazukommen | drei echt externe Ziele blockiert, die Knoten-Adresse erreichbar | built: nichts — aber der Fund aendert die Klasseneinteilung und gehoert ins Issue | | ## Notes +⚠️ **Ich habe mir die Falle aus meinem eigenen Abnahmekriterium gestellt.** +Kriterium 4 verlangt ausdruecklich ein Werkzeug, das im Container +existiert — und die erste Gegenprobe in `element-admin` lief mit +`/dev/tcp`, das dessen `sh` nicht kennt und deshalb alles als blockiert +meldete. Derselbe Fehler wie am 20.08., im selben Vorhaben, dessen +Kriterium davor warnt. Wiederholt mit `nc` und einer Positivkontrolle +gegen CoreDNS; erst damit war das Ergebnis belastbar. + **Die Manifeste geben die Ziele nicht her.** Externe Adressen stehen in SOPS-Secrets und in den Images, nicht im YAML — ein Grep ueber die Manifeste liefert nichts. Die Ziele der Klasse B stammen deshalb aus aufgeloesten