Files
bf-infra/docs/tp5-deny-by-default-runbook.md

239 lines
15 KiB
Markdown
Raw Permalink 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.
# TP5 — Deny-by-default auf `crs326-core` · Session-Runbook
> **Stand der Vorbereitung: 2026-08-14** (Live-Erhebung auf `crs326-core` via MikroMCP).
> **Status: vorbereitet, nicht ausgeführt.** Diese Datei ist der Einstieg für die TP5-Session —
> kanonischer Scope bleibt `docs/teilprojekte.md` → TP5.
---
## 0. Worum es geht (in einem Absatz)
Die `forward`-Kette auf dem Core endet heute **ohne Drop**: alles, was nicht von einer der 6 TP5-Hole-Regeln
erfasst wird, läuft trotzdem durch. Die VLAN-Segmentierung ist damit **dokumentiert, aber nicht durchgesetzt**.
TP5 schließt genau diese eine Lücke — plus die Whitelists, die es dafür braucht. Es ist der letzte Schritt
vor TP6 (CAKE), dem eigentlichen Projektziel.
**Das Risiko ist asymmetrisch:** ein zu früh gesetzter Drop nimmt DHCP, DNS, Mgmt und das halbe Smart-Home
gleichzeitig mit — und zwar auch den Pfad, über den man den Fehler zurücknehmen wollte. Deshalb ist unten
**Etappe 1 ein Log-Lauf ohne jede Blockierung.** Wer direkt zum Drop springt, arbeitet blind.
---
## 1. Ist-Stand (live erhoben 2026-08-14, `crs326-core`)
### 1.1 `forward` — 13 Regeln, kein abschließender Drop
| # | Aktion | Bedeutung |
|---|---|---|
| 1 | accept `established,related,untracked` | defconf |
| 2 | fasttrack `established,related` | defconf (`hw-offload=true`) |
| 3 | drop `invalid` | defconf |
| 4 | drop in-list=WAN `!dstnat` | defconf |
| 5–6 | accept in/out `wg-hub` | TP10 (FM/DW-Spokes) |
| 7–8 | accept `vlan10↔vlan40` | TP5: MAIN↔MEDIA |
| 9–10 | accept `vlan20↔vlan40` | TP5: INFRA↔MEDIA |
| 11–12 | accept `vlan90↔vlan40` | TP5: GUEST↔MEDIA |
| — | **(nichts)** | ⬅️ **hier fehlt der Deny-by-default** |
### 1.2 `input` — 12 Regeln, LAN pauschal offen
TP10-Seal komplett (`est` · `inv` · `dhcp` · `icmp` · **`lan`** · `wandrop`) + WG-Listen 51821 + 5× mDNS.
⚠️ **`TP10-seal-lan` (`in-interface-list=LAN`, accept) nimmt jeden LAN-Input an** — inkl. VLAN99-Mgmt,
VLAN90-Guest und VLAN50. Die 5 `TP5: mDNS in vlanXX`-Regeln stehen **dahinter** und zählen daher
**0 Pakete** (seal-lan: 3,5 Mio.). Sie sind nicht kaputt, nur noch nicht dran.
### 1.3 NAT + Interface-Listen
- `srcnat`: `accept dst-address=10.40.0.0/16` (**no-NAT inter-VLAN, steht vor masquerade**) → `masquerade out-list=WAN`.
- `WAN` = `vlan100` · `LAN` = `vlan10,20,30,40,50,90,99` (**vlan99 ist Mitglied von LAN** — relevant für 1.2 und für D4).
---
## 2. Was heute real über VLAN-Grenzen läuft
Erhoben aus `/ip/firewall/connection` (Momentaufnahme) + den kumulativen Regel-Countern.
⚠️ **Beides zusammen lesen:** conntrack zeigt nur, was *gerade* offen ist; die Counter zeigen, was *jemals* lief.
Beispiel: `INFRA→MEDIA` hat 178.142 Pakete auf dem Zähler, stand aber im Snapshot auf 0 Verbindungen.
| Richtung | Snapshot | Was genau |
|---|---|---|
| **v20 → v30** | 24 | **HA (`.20.105`) pollt 22 Shellys auf `tcp/80`** + `.30.137:6638` |
| **v10 → v30** | 224 | **223× ICMP von `.10.82` („Mac") auf alle Shellys** + 1× udp/3722 |
| **v10 → v20** | 10 | `.10.82`/`.10.183` → NAS `.20.10:6690` (Synology Drive), `.20.173:3000` (pve-mcp), `.20.167:49152` |
| **v20 → v10** | 4 | Mac mini `.20.167` ↔ iPad `.10.180` udp/3722; HA → `.10.6:161` (SNMP) |
| **v30 → v20** | 3 | `.30.249` → **HA-API `.20.105:8123`**; udp/5540 (Matter) → HA; `.30.201` → `.20.167:49152` |
| **v30 → v10** | 4 | Rückrichtungen zu `.10.82`/`.10.180`/`.10.183` (ephemere Ports) |
| **v10 → v99** | 1 | `.10.82` → `core .99.1` **tcp/22 + tcp/80** (= Mgmt-Zugriff) |
| **v40 → \*** | 0 | MEDIA initiiert nichts nach außerhalb |
| **v50 / v90** | 0 | **komplett leer — Internet-only-Pinning ist hier risikofrei** |
### 2.1 Drei Funde, die vor dem Drop eine Entscheidung brauchen
1. 📱 **Ein iPhone sitzt in VLAN30 (IOT):** `10.40.30.201`, Hostname `iPhone`, und es spricht mit dem
Mac mini (`udp/3722`, Apple-Continuity-Muster). Entweder hat sich jemand bewusst ins `dirtyvader-iot`-SSID
gehängt, oder das ist ein Ausrutscher. **Ein IOT-Deny trennt dieses Gerät** — vorher klären, nicht danach.
2. ✅ **`.10.82` („Mac") pingt alle ~30 Shellys — GEKLÄRT (User 2026-08-14): das ist `lanscan.app`**, also
ein Ad-hoc-Scan vom eigenen Arbeitsplatz, kein Dauerdienst. **Konsequenz: keine permanente
MAIN→IOT-ICMP-Regel bauen** — der Scan gehört in die **JIT-Admin-Gruppe** (address-list mit
`timeout`, s. §3/D3). Beim Log-Lauf erzeugt er sonst viel Rauschen: entweder während Etappe 1
nicht scannen, oder die Treffer beim Auswerten bewusst ausfiltern.
3. 🔑 **VLAN99 ist Mitglied der `LAN`-Liste** → `TP10-seal-lan` erlaubt Mgmt-Input aus *jedem* VLAN.
„MGMT isolieren" (TP5-Ziel) heißt also: `seal-lan` verengen — das ist derselbe Griff, der die
5 mDNS-Regeln scharf schaltet. **Beides passiert zusammen oder gar nicht.**
---
## 3. Offene Entscheidungen (vor der Session klären)
| # | Frage | Vorschlag |
|---|---|---|
| **D1** | MAIN→INFRA: welche Dienste? | Beobachtet: NAS `6690`, pve-mcp `3000`, macmini `49152`, `3722`. Vorschlag: **erst mit dem Log-Lauf (Etappe 1) vervollständigen**, dann Whitelist — nicht jetzt raten. |
| **D2** | ~~iPhone `.30.201` in IOT~~ | ✅ **ENTSCHIEDEN (2026-08-14): ins MAIN-SSID umhängen.** Hands-on bei Christoph (SSID wechseln + `dirtyvader-iot` vergessen lassen). Beleg, dass es dasselbe Telefon ist: die statische Reservierung `10.40.10.196` „iPhone" trägt eine *andere* private MAC — iOS vergibt **pro SSID** eine eigene Private Address. Genau darum wäre eine Whitelist auf `.30.201` instabil. |
| **D3** | ~~ICMP-Sweep `.10.82`→IOT~~ | ✅ **ENTSCHIEDEN (2026-08-14): `lanscan.app`, Ad-hoc vom Arbeitsplatz.** Keine Dauerregel — läuft über die **JIT-Admin-Gruppe** (selbstverfallend). Deckt sich mit D4. |
| **D4** | Wer darf auf VLAN99-Mgmt? | Vorschlag: nur MAIN (`vlan10`), Rest drop. `seal-lan` entsprechend verengen. |
| **D5** | IOT→INFRA-Whitelist | Doku nennt MQTT 1883/8883, HA-API, Influx 8086. Beobachtet real: **`8123` (HA), `5540/udp` (Matter)**. Vorschlag: die beobachteten + MQTT. |
| **D6** | UNTRUSTED/GUEST | Beide tragen **null** Inter-VLAN-Verkehr → Internet-only sofort und gefahrlos pinnbar. |
---
## 4. Umsetzung in 4 Etappen
> **Governance (TP22):** alles über **Nautobot Golden Config**, `plan_type=manual`, feature-scoped —
> **kein** Direkt-Write, **kein** „full intended" (Regel 1: `<<REDACTED>>`-Sektionen nie pushen).
> Vor **jedem** Push `/system backup` auf dem Core (Regel 3). Und: **nur die Gegenprüfung am Gerät zählt**,
> Job- und Plan-Status sind kein Wirksamkeitsnachweis (zweimal belegt, TP21.1/TP22).
#### Push-Werkzeug + Fallen (aus dem Etappe-1-Rollout, 2026-08-14 am Gerät belegt)
Werkzeug: `nautobot/gc-manual-plan.py --device <name> --commands-file <datei> --change-id <id> [--dry-run|--deploy]`.
Es legt den manual Plan an, prüft `plan.device` gegen den Namen, gibt frei, deployt und liest
**den Plan-Status** zurück (nicht den Job-Status). Kommentar-/Leerzeilen werden abgestreift.
1. 🔴 **Bindestriche in `name=` werden abgelehnt.** `add name="tp5-remote"` und `add name="tp5-abc"`
werfen, `add name="tp5syslog"` geht — **Quoten hilft nicht.** Das hat vier Deploys gekostet, weil die
naheliegende Erklärung („`remote=` darf nicht ins selbe `add` wie `target=remote`") *falsch* war:
`set remote=… remote-port=…` in einem Statement läuft anstandslos. Bei neuen benannten Objekten
also **Namen ohne Bindestrich** wählen, oder das Gegenteil erst beweisen.
2. 🔴 **Ein gescheiterter Netmiko-Deploy hinterlässt einen TEIL-Zustand.** Die TP22-Notiz
„gescheiterter Deploy = sauberer Fehlschlag" galt dem **NAPALM**-Weg, der scheiterte, *bevor* er das
Gerät anfasste. Netmiko schickt Zeile für Zeile und lässt Abgesetztes stehen: der erste Push hier
hinterließ Zeile 3+4 ohne 1+2. **Nach jedem Fehlschlag den Ist-Stand am Gerät prüfen.**
3. **Keine `print`-Zeilen in einen Config Plan.** Ein `/system identity print` als Kanarienzeile hat die
unmittelbar folgende Zeile verschluckt und eine Fehldiagnose erzeugt.
4. **Selbstprotokollierende Pläne sind die Diagnosemethode.** Der Deploy-Job liefert nur
`E1028: found bad/failed/failure in output` und zeigt den Geräte-Output **nicht**. Jedes Kommando in
`:do {…} on-error={ :log info "…" }` wickeln und danach eine Zählzeile
`:log info ("… count=" . [:tostr [:len [… find …]]])` setzen — anschließend per `get_log` zeilengenau
auslesen. Damit ist Root-Cause-Analyse **ohne Direkt-Write** möglich.
### Etappe 1 — Log-Lauf (blockiert nichts, Dauer: einige Tage) ▸ 🟢 **SAMMELPHASE LÄUFT (seit 2026-08-16)**
> Historie: am 2026-08-14 **vor** der Sammelphase pausiert, weil 76 % des Verkehrs `.41.x`-Altlast war.
> Nach dem VLAN1-Rückbau (TP19) am 2026-08-16 wieder scharf — `routeros/tp5-etappe1-resume.rsc`.
>
> ✅ **Abnahme bestanden: `out:bridge` = 0** über ein 10-Minuten-Fenster (Logdatei vorher geleert).
> Rate von ~166 auf ~34 Zeilen/min gefallen. Die beobachtete Matrix steht in
> `docs/teilprojekte.md` → TP5 und ist ab jetzt das Arbeitsmaterial für Etappe 2.
>
> **Zwei Regeln stehen damit schon fest und brauchen keinen weiteren Beleg aus dem Log:**
> 1. `accept out-interface-list=WAN` — ohne sie nimmt der Drop das ganze Haus vom Netz.
> 2. Der **Management-Pfad** `10.40.20.105 → 10.40.99.{2,11,12}:8728` (HA-`mikrotik`-Integrationen)
> plus MikroMCP vom Arbeitsplatz — sonst sperrt der Drop die Netzverwaltung aus.
> 3. **Same-VLAN-mDNS** (`vlan10 → vlan10`, `5353`, vom Repeater auf dem Core erzeugt) erreicht die
> `forward`-Kette und würde von einem blanken Drop getroffen.
Ausgerollt über Nautobot GC (manual Config Plans, `nautobot/gc-manual-plan.py`), Quelle
`routeros/tp5-etappe1-log-lauf.rsc`. Auf dem Gerät:
```
/ip firewall filter add chain=forward action=log log-prefix="TP5-WOULD-DROP" \
comment="TP5: log-only, Vorstufe zum deny-by-default" out-interface-list="!WAN"
```
Das protokolliert exakt die Pakete, die der spätere Drop treffen würde — inklusive der Dinge, an die
niemand denkt (Drucker-Discovery, Backup-Jobs, ein Gerät, das nur einmal pro Woche funkt). **Diese Liste
ist die Whitelist**, nicht eine am Schreibtisch ausgedachte. Danach D1/D5 final beantworten.
**Log-Ziel steht:** syslog-ng-Container `tp5-syslog` auf dem Docker-LXC, **`10.40.20.18:514`**,
Dateien unter `/opt/tp5-syslog/log/` (`tp5-would-drop.log` gefiltert + Host-Dateien).
Auf dem Core: Action `tp5syslog` + `logging add topics=firewall action=tp5syslog`.
Disk-Logging auf dem Core scheidet aus (**nur ~3 MB freier Flash**), der Memory-Ring (1000 Zeilen)
wickelt sich in Minuten um. `firewall` ist aus dem Memory-Ring genommen (`topics=info,!firewall`),
damit der On-Box-Log während des mehrtägigen Laufs für andere Fehlersuche brauchbar bleibt —
**am Ende von Etappe 1 auf `topics=info` zurückbauen.**
⚠️ Der LXC hat nur ~2,5 GB frei; `/usr/local/bin/tp5-log-guard.sh` (Cron, alle 10 min) kappt bei 500 MB
je Datei per Truncate-in-place. Ein vollgelaufenes Dateisystem risse dort auch adguard/cloudflared mit.
#### 🔴 `out-interface-list="!WAN"` — warum die Regel nicht bare steht
Bare gefasst fing sie **auch den gesamten Internet-Verkehr** (`out:vlan100`): in einer ruhigen Minute
242 Zeilen/min, davon ¼ WAN-bound, unter Last deutlich mehr. LAN→WAN ist aber eine **garantierte**
Whitelist-Zeile — ohne sie hat kein VLAN Internet, auch die „Internet-only"-VLANs 50/90 nicht.
Es wäre also Rauschen genau in der Matrix, die Etappe 2 ableiten soll.
**Merkposten für Etappe 3:** der Drop darf aus demselben Grund **nicht** bare ans Kettenende —
`accept out-interface-list=WAN` gehört davor, sonst ist das ganze Haus offline. Diese Zeile ist
bekannt und braucht keinen Beleg aus dem Log.
#### Auswertung
```bash
ssh root@10.40.20.100 'pct exec 400 -- grep -o "in:vlan[0-9]* out:[a-z0-9]*" /opt/tp5-syslog/log/tp5-would-drop.log | sort | uniq -c | sort -rn'
```
### Etappe 2 — Whitelists bauen (noch immer ohne Drop)
Die aus Etappe 1 abgeleiteten Regeln **vor** der Log-Regel einfügen: MAIN→INFRA (D1), IOT→INFRA (D5),
ggf. MAIN→IOT-ICMP (D3). Danach muss die Log-Regel **verstummen** — solange sie noch Nutzverkehr sieht,
ist die Whitelist unvollständig. **Das ist das Abbruchkriterium für Etappe 3.**
### Etappe 3 — Drop scharf schalten
`action=log` → `action=drop` (bzw. Log behalten und Drop dahinter). **Erst wenn Etappe 2 still ist.**
🛟 **Sicherung — auf dem Core möglich, anders als auf den hAPs:** `crs326-core` läuft **nicht** in
`device-mode=home`, also stehen `scheduler` und `fetch` zur Verfügung. Vor dem Drop einen Auto-Revert
scharfstellen, der die Regel nach n Minuten selbst wieder abschaltet, wenn niemand ihn abbestellt:
```
/system scheduler add name=tp5-autorevert interval=10m on-event="/ip firewall filter set [find comment~\"TP5: deny-by-default\"] disabled=yes; /system scheduler remove tp5-autorevert"
```
Läuft alles sauber → Scheduler manuell entfernen. Läuft es nicht → das Netz heilt sich in ≤10 Minuten selbst.
**Diese Absicherung gibt es auf ap1/ap2 nicht** — dort ist sie durch `device-mode=home` blockiert (TP22-Regel 2).
### Etappe 4 — UNTRUSTED/GUEST + MGMT (D4/D6)
`vlan50`/`vlan90` auf Internet-only pinnen (risikofrei, s. §2), `seal-lan` auf MAIN verengen → MGMT isoliert,
und als Nebeneffekt werden die 5 mDNS-`input`-Regeln endlich wirksam.
---
## 5. Verifikation (nach jeder Etappe, immer am Gerät)
1. `list_firewall_rules` chain=forward/input → Regel existiert, `disabled=false`, Kommentar korrekt.
2. **Funktionstest quer durchs Haus:** Sonos aus MAIN steuern · HA erreicht die Shellys (`v20→v30:80`) ·
Shelly meldet an HA (`v30→v20:8123`) · NAS-Zugriff aus MAIN (`6690`) · Drucker · AirPlay/Cast ·
Gast-WLAN kommt ins Internet und **sonst nirgendwohin**.
3. Counter der neuen Regeln lesen — eine Drop-Regel, die *gar nichts* zählt, ist genauso verdächtig wie
eine, die zu viel zählt.
4. Nach der Geräteänderung: **Backup → Intended → Compliance** in dieser Reihenfolge
(GC vergleicht gegen das letzte Backup, nicht gegen das Live-Gerät — TP22-Falle).
## 6. Rollback
- Sofort: `disabled=yes` auf der Drop-Regel (bzw. Auto-Revert greift von selbst).
- Ganz zurück: `/system backup` von vor der Session einspielen.
- 🚨 Wenn der Mgmt-Pfad weg ist: Rettung ist der **Kabelweg**, nicht WLAN. `crs326-core` steht in
Technik UG; VLAN10-Fallback `10.40.10.1` existiert zusätzlich zu `.99.1`.
- ⚠️ **WARP-Falle:** „Ping auf alles tot, TCP geht" ist **kein** Firewall-Befund, sondern ein aktiver
WARP-Client. Vor jeder Firewall-Fehlersuche zuerst WARP trennen (`ref_warp_local_icmp_trap`).
---
## 7. Anschluss
**TP5.1** (DNS-Redirect + Torch für Low-Trust) hängt am UNTRUSTED-Pinning aus Etappe 4.
Danach ist der Weg frei für **TP6 — CAKE**, das Projektziel.