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