239 lines
15 KiB
Markdown
239 lines
15 KiB
Markdown
# 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.
|