Files
bf-infra/CLAUDE.md
Christoph Emsenhuber b52d6aa49e TP45 Schicht A: Nautobot .30/.31/.32/.34 -> .154/.155/.156/.158
Oktett-Klasse .150-.199 (App-Services). Nautobot gibt damit .30/.31 an
AGH frei. .157 ist gitea, deshalb nimmt redis .158.

- Datenmigration ueber neues nautobot/tp45-ip-move.py: Nautobot verbietet
  das Aendern einer IP nach dem Anlegen -> anlegen/primary_ip4 umhaengen/
  Altobjekt loeschen, in dieser Reihenfolge
- Stack-Redeploy (PUT /api/stacks/203), Caddy-Backend, Core-Literal *24
  ueber GC deployt (Plan 51a90e25) mit Wirksamkeitspruefung am Geraet
- caddy/deploy.sh: push_verified() ersetzt den push||push-Fallback.
  `pct push` meldet Exit 0 AUCH wenn es die Quelle nicht lesen kann, der
  Fallback griff deshalb nie und `caddy validate` pruefte die ALTE Datei
  -> gruene Abnahme bei unveraendertem Zustand (Symptom: 502 bei
  gesundem Backend)
- 39 Referenzen in 28 Dateien mitgezogen; lebende Doku nachgezogen
  (CLAUDE.md, ip-plan.md, cf-tunnel-mapping §0-Kopf, TP16.3/TP31-Prosa)
- GC-Compliance 140/140, 0 Drifts

Neue Funde (TP45-Prosa + CLAUDE.md):
- Die Core-Adressliste `nautobot` wird von GC nirgends gerendert -> ein
  Recovery von null erzeugt einen Core, in den Nautobot nicht mehr per
  SSH kommt. Gleiche Klasse wie die fehlenden DHCP-Reservierungen.
- Regel *24 (Literal) und *28 (Liste) sind Dubletten; der Literal steht
  bei 0 Paketen. Zusammenlegung bleibt offen.
- gc-manual-plan.py: /api/extras/job-log-entries/ liefert 404 (Nautobot
  3.x) -> bei gescheitertem Deploy fehlt die Fehlermeldung.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-21 13:14:35 +02:00

426 lines
60 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.
# BF-Netzwerk — Projekt-Kontext (auto-read)
**Standort BF:** VLAN-segmentiertes Heimnetz auf MikroTik/RouterOS. Ziel: CAKE/QoS gegen Bufferbloat,
DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der Standalone-Entscheid von
2026-07-22 ist revidiert), Firewall Least-Privilege (**TP5 deny-by-default scharf seit 2026-08-18**), Multi-Site VPN.
## Pflichtlektüre pro Sitzung (Reihenfolge)
0. 🔴 **TP45 = aktuell dringendster Posten (2026-09-21, Gitea #71):** interne `*.bf.emsenhuber.net`
lösen im Browser **nicht** auf (AGH wertet Rewrites im **Filter-Pfad** aus → für unfiltrierte
Clients übersprungen → Fallback auf den Core, der nur 10 von 26 Namen hält), und `ds-bf` ist
**öffentlich kaputt** (einziger Ingress-Eintrag, der Caddy umgeht → TP5-Drop). Reihenfolge:
VLAN20 ins TP32-Schema → VLAN30-Pool-Grenze → DNS/FQDN. Ausführlich: `### TP45`.
**`### TP46`** = CF-/Tunnel-SSoT + Recovery (eigene Baustelle).
1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Für den **Detail/Kontext** eines TP dann den `### TPnn`-Prosa-Abschnitt in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`** lesen. *(Die frühere „Quick-Index-Block, 1 Read reicht"-Zeile ist überholt — Index/Status stehen nicht mehr in der Datei.)*
2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat.
3. Bei Live-Writes oder Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/teilprojekte-archiv.md`**.
4. ✅ **TP16.3 §5 durch (2026-09-01):** [`docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md`](docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md)
— alle vier oeffentlichen Namen laufen ueber `bf-public`/Caddy; Alttunnel bleiben als Rueckfallebene
(§6 offen). Enthaelt den 10-Sekunden-DNS-Fund und drei Messfallen.
5. ✅ **WLAN/Fund E — GESCHLOSSEN 2026-09-02: 7.24.1 war die Ursache, nicht `ft-over-ds`:**
[`docs/handoff-20260902-tp25-fund-e-messung.md`](docs/handoff-20260902-tp25-fund-e-messung.md)
— die Abschlussmessung ergab am 09-02 nur **4 `reauthenticating` in 8,6 h** (alle iPhone `F2:62`,
isolierte Einzel, 0 Ausfaelle), Verlauf **218 → 40 → 4**; Vorbedingung geprueft (Core-Uptime 1d18h,
`caps joined=0`). Hebel 1 (`ft-over-ds=no`) war widerlegt (6,4 h allein → 124× + 2 Ausfaelle) und ist
am 09-01 zurueckgebaut; Hebel 2–4 entfallen, **kein Geraete-Write** (Soll stand: `ft=yes ft-over-ds=yes`,
**Compliance 140/140**). **Am 2026-09-04 hat Christoph `ft=no` auf `sec-main` als Dauerzustand
entschieden** (Hebel 2 doch gezogen: Template + bf-golden-config + Nautobot-Sync, `wifi-security` gruen).
⚠️ **Rueckgenommen am 2026-09-14 (Terrasse-In-situ-Session): `ft=yes ft-over-ds=yes` zurueck** — mit
`ft=no` war jeder Roam eine Voll-Reassoziation, darin starben 2 Anrufe; Rueckkehrstand = Fund-E-Known-Good
von 09-02 (ruhig auf 7.24.1). Weg: Template + `push-gc-template.py` + `gc-configplan.py
--refresh-compliance` + `gc-manual-plan.py --deploy` (Plan `b1135f3e`, Completed) → **140/140, 0 Drifts**;
CAP-Abnahme: **keine** Inline-Overrides auf ap1/ap2/ap3. Commands:
`docs/transaktionen/TP25-ft-yes-secmain.commands.rsc`, s. Handoff 2026-09-14.
🔴 **Nachtrag 09-04: `ft=no` war NICHT uniform.** ap3 trug einen Werks-Inline `security.ft=yes` auf
`wifi1/wifi2` (Bring-up unset `!ft-over-ds`, aber nicht `!ft`; Abnahme zählt nur benannte
security-Objekte → Inline-Wert unsichtbar). Gemischtes FT-ESS → Strong-Signal-Abbrüche (−43/−45 dBm)
**nur auf ap3** bei 3/3 Strichen. Fix: `!security.ft` auf ap3 (uniform `ft=no`), Quelle
`tp28-ap3-phase2-cap.rsc` korrigiert. **Merkregel: nach jeder `sec-main`-FT-Änderung jeden CAP einzeln
`get wifiN security.ft` prüfen — CAP-lokale Inline-Overrides sind compliance-unsichtbar.**
Der TP25-Kanalplan ist am 09-04 committed (DFS-Frequenzlisten + ap1-Provisioning, **140/140, 0 Drifts**).
Offen in TP25 bleibt nur **ap2 unterperformt** (2 m/−80 dBm). Vorstufen:
[Befund](docs/handoff-20260831-tp27-upgrade-und-hebel1-befund.md) ·
[Eingriff](docs/handoff-20260831-fund-e-hebel1-gezogen.md) ·
[Rueckbau](docs/handoff-20260901-fund-e-hebel1-zurueckgebaut.md).
6. ap3-Bring-up 2026-08-30: [`docs/handoff-20260830-ap3.md`](docs/handoff-20260830-ap3.md) — sechs Funde zu Werks-MikroTiks, CAPsMAN und Golden-Config-Lücken.
7. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift.
## SSoT & Branch-Konvention (TP36-Cutover 2026-09-07)
- **SSoT-Dreiteilung:** **Index/Status = Gitea-Issues** (`admin/bf-infra`, Labels `status:*`/`thema:*`) ·
**Prosa/Detail = Repo** (`docs/teilprojekte.md` aktiv + `teilprojekte-archiv.md`, `### TPnn`-Abschnitte) ·
**Live-Gerätedaten = Nautobot** (Compliance-Zahl, RouterOS-Version …). Ein Status ändert sich **in Gitea**,
nicht mehr in einer Repo-Datei. Sync-/Abgleich-Werkzeug fürs Archiv: `bin/gitea-tp-sync.py` (Dry-Run default,
Token aus `~/.hermes/.env`, **nie ausgeben**).
- **Branch-Konvention:** Branch `tpNN-kurz` **jederzeit** mitten in der Session möglich (`git switch -c`,
uncommittete Arbeit wandert mit); für Multi-Session-TPs der Normalfall. **Merge nach `main`, sobald der
Code/Config des TP steht.** Weil Steuerungs-Updates nach **Gitea** gehen (nicht in `teilprojekte.md`),
kollidieren TP-Branches nicht mehr auf der Steuerungs-Doku — nur noch der eigene `### TPnn`-Prosa-Abschnitt
wird angefasst.
- **TP anlegen/schließen:** neues aktives TP = **direkt als Gitea-Issue** (Labels setzen), Prosa-Abschnitt
im Repo dazu; abschließen = Issue `closed`, Prosa wandert bei Bedarf ins Archiv. **Keine Handtabelle mehr.**
## Was du sofort wissen musst (Inventur 2026-08-27)
- **`bf-infra`** ist aktiver Master **mit Gitea-Remote**: `origin` = `https://gitea.emsenhuber.net/admin/bf-infra.git` (live verifiziert 2026-08-30). ⚠️ **Die frühere Zeile „lokal, kein Gitea-Remote → verloren bei Festplattencrash" ist überholt** — sie stammt aus der Inventur vom 2026-08-27 und war damals richtig; das Remote existiert seit spätestens 2026-08-28 (`origin/main` = `b917691`). **Die Backup-Lücke ist damit keine Struktur-, sondern eine Disziplinfrage: `git push`.** Am 2026-08-30 lagen vier Commits nur lokal. 🔴 **Das Remote ist nur so viel wert wie der letzte Push** — der Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5) bricht Commits ab, solange kein Backup markiert ist; **das ist die Erinnerung ans Pushen, kein Hindernis.** Nicht mit `--no-verify` umgehen. 🔴 **Merkregel:** diese Zeile hat eine Session dazu gebracht, „kein Remote" zu behaupten, statt `git remote -v` zu tippen — vor jeder Aussage über Remotes/Backups **live prüfen**.
- **`admin/bf-network-r6`** (Gitea) ist Archiv, letzte aktive Commits 2026-08-25 → read-only-Header.
- **`admin/bf-golden-config`** (Gitea) ist Bot-Master, nicht manuell. 🔴 **Korrigiert 2026-09-12:** die frühere Angabe „2×/Tag" war nie real — es gab **keinen** Scheduler, Backup/Intended/Compliance liefen nur manuell aus Sessions (zuletzt 09-04, 8 Tage Stillstand). Jetzt **täglich 03:30 UTC** per ScheduledJob **„GC Backup+Compliance – taeglich"** (Nautobot-Job *Execute All Golden Configuration Jobs - Multiple Device*, Scope `platform=RouterOS`, Reihenfolge Backup→Intended→Compliance). Stand prüfen: `backup_last_success_date` am Gerät bzw. `scheduled-jobs` in Nautobot.
- **`admin/blaueblueten-n8n`** (Gitea) ist Master für terpene-Repo.
- **`inventory/` ist tot** (Pipeline seit 2025-06 stale). Live-Daten aus MikroMCP/CF/Portainer.
- **`mcp/INVENTORY.md`** ist BF-Netz-SSoT; `~/.claude.json`/`opencode.jsonc` sind globalere Werkzeugkästen.
- **Terpenfinder = `420.teilzeitladen.xyz`** (CF-Worker, Repo `admin/blaueblueten-n8n`, hinter CF Access), **kein Caddy, kein Mac mini im Pfad**. `terpene.*` **existiert nicht und soll es nicht** (live geprüft 2026-09-10). Einziger terpene-benannter CF-Gegenstand: der **interne** Tunnel `terpene-db` (`f2d2fc8a`, ohne öffentlichen Hostname, Hyperdrive→VPC→Postgres, s. `docs/cf-tunnel-mapping.md` §17). ⚠️ **Grenze:** blaueblüten ist ein eigenes Projekt — nicht anfassen. *(Die frühere „terpene.teilzeitladen.xyz:8890 via Tunnel"-Zeile war veraltet — bf-mm ist gelöscht.)*
- **shelly-mcp / portainer-mcp bewusst NICHT als MCP-Client** (User-Direktive 2026-08-28): shelly-mcp-Container auf DS923 wird via **Portainer-API** (Skript-Aufrufe, `mcp/portainer/.env` als Token-Quelle) deployt, nicht via MCP-Protokoll. Inventar-Eintrag spiegelt das. `mcp-host-policy.md` ist SSoT für die Host-/IP-Konvention.
- **Mac mini `10.40.20.167`** ist der **n8n-Host** (n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — **NICHT** der zentrale MCP-Host. Performance-Überlegung: MCP-Clients würden auf dem macmini (User-Workstation) laufen und die Box zusätzlich belasten; deshalb liegt der MCP-Host auf der **DS923** (`nas-bf`, Portainer Endpoint 14). gitea-mcp läuft aktuell noch auf dem macmini (historisch, eigene Container-Definition dort) — gehört **bei Gelegenheit** auf die DS923 umgezogen (separater Punkt, **nicht** Teil dieser Session). Container-Definitionen für n8n leben auf dem macmini (Docker Desktop); FQDN→Backend-Mapping im `caddy/Caddyfile`.
## TP-Management (2026-08-26)
Wenn während einer Session ein neues Teilprojekt, eine Abhängigkeit oder ein Querschnittsthema sichtbar wird, das bisher in keinem TP abgebildet ist:
- Vorschlagen und mit dir abstimmen (nicht eigenmächtig anlegen).
- Auf dein Okay hin anlegen: **Gitea-Issue** `TPnn — <Kurztitel>` mit Labels `status:🔜`(todo) +
passendem `thema:*` + Body (Anriss + Verweis auf den Prosa-Abschnitt) **und** einen
`### TP<n>`-Prosa-Abschnitt in `docs/teilprojekte.md` (Scope, Abhängigkeiten, Abgrenzung).
Nächste freie Nummer vergeben (höchste vergebene + 1; Lücken aus verworfenen TPs bleiben frei).
- **Status/Reihenfolge/„was lief zuletzt" gehen ins Gitea-Issue** (Label ändern, Kommentar), **nicht**
in eine Quick-Index-Tabelle oder einen Ist-Stand-Block — die gibt es seit dem TP36-Cutover (2026-09-07)
nicht mehr in der Datei.
**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis) und ihr Gitea-Issue ist `closed` (Label `status:✅`/`❌`). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt** (Issue dann `reopen`). Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`) — nur noch der betroffene `### TPnn`-Prosa-Abschnitt wird angefasst.
---
## VLAN-Modell (kanonisch)
| VLAN | Name | Subnetz | Zweck |
|---|---|---|---|
| 10 | MAIN | 10.40.10.0/24 | Trusted (Handy/Laptop) |
| 20 | INFRA | 10.40.20.0/24 | NAS/VMs/Docker/HA |
| 30 | IOT | 10.40.30.0/24 | Smart-Home |
| 40 | MEDIA | 10.40.40.0/24 | Sonos/AppleTV/TV/Cast |
| 50 | UNTRUSTED | 10.40.50.0/24 | Konsole/Low-Trust |
| 90 | GUEST | 10.40.90.0/24 | nur Internet |
| 99 | MGMT | 10.40.99.0/24 | Switch/AP/Hypervisor-Mgmt |
| 100 | WAN | (Arris-Transfer) | zum Arris-Kabelmodem (Liwest, Public-IP) |
GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`, ap1 `.99.11`, ap2 `.99.12`
---
## Hardware & Zugang
| Gerät | IP | Zugang |
|---|---|---|
| ~~ALLNET Core-Switch (sw1)~~ | — | ❌ **außer Betrieb (2026-08-08)** — durch `crs326-core` (L3-Router + Core-Switch, Technik UG) ersetzt; physisch abgelöst. (Telnet-Runner `switch/` = nur noch historisch.) |
| ~~Omada Access-Switch (sw2)~~ | — | ❌ **abgebaut 2026-07-23** (TP15 Branch-Tausch) — durch `crs326-branch` in Technik OG ersetzt, physisch entfernt |
| **crs326-branch** (CRS326, Technik OG) | **10.40.99.2** (VLAN99) · Bootstrap Rev4 2026-08-08 | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_BRANCH_*`) · REST `http://10.40.99.2/rest` · MikroMCP `crs326-branch` ✅ **live** (2026-08-16 verifiziert) · **TP15 ✅: ersetzt sw2** (L2-Switch), volle Portbelegung (ether1 Uplink-Trunk, 2/8 NAS, 3 Drucker, 4 Dock, 5/6 Sonos, 7 macmini, 24 ap3) · sw-id RGZJ-UK0T, MAC 04:F4:1C:B4:15:58 |
| **crs326-core** (CRS326, Technik UG) | **10.40.99.1** (VLAN99-SVI = L3-GW; same-subnet-Fallback `10.40.10.1`) · Identity `crs326-core` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`CRS326_CORE_*`) · MikroMCP routerId `crs326-core` ✅ **live** (2026-08-16 verifiziert) · **TP15/TP21.1 ✅ 2026-08-08: PRODUKTIVER L3-Router** (ersetzt ap1) — Routing/DHCP (7 Server + 54 Leases)/DNS (**DoH zum CF-Gateway mit `verify-doh-cert=yes`**, TP23 Stufe 1 ✅ 2026-08-18; `servers=1.1.1.1` nur noch Bootstrap) · **NTP-Server für den Standort**/NAT/WG-Hub (fm+dw live); WAN `vlan100`=Liwest `90.146.33.177` · sw-id 4W04-URHT, MAC 04:F4:1C:B4:17:76 |
| hAP ax² #1 (ap1) | **10.40.99.11** (VLAN99) · Identity `ap1` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP1_*`) · REST `http://10.40.99.11/rest` · MikroMCP `ap1` ✅ live · **TP28 ✅ 2026-08-17: reiner CAP** (Manager = `crs326-core`) · ✅ **aktiv am Sideboard** (Wohnzimmer; Uplink `ether1` → core `ether3`, PoE-Port `poe-out=off`) — Kanäle `5260/Ce` (Band B, DFS, Fallback 5280) + `2412`/ch1, vom Manager · MAC D0:EA:11:4D:40:E0 · Config-Referenz `routeros/.secrets/ap1-prov-base.rsc` |
| hAP ax² #2 (ap2) | **10.40.99.12** (VLAN99) · Identity `ap2` | RouterOS **7.24.1** · `admin`/`~/.secrets` (`ROUTER_AP2_*`) · REST `http://10.40.99.12/rest` · MikroMCP `ap2` · **TP28 ✅ 2026-08-17: reiner CAP** — **Technik UG**, Uplink `ether1` (PoE) an core `ether21`; Kanäle `5300/Ce` (Band B, DFS, Fallback 5320) + `2462`/ch11 · MAC D0:EA:11:4D:40:39 (ether1; Bridge/Mgmt `:3A`) · Config `routeros/.secrets/ap2-prov-base.rsc` |
| **cAP ax (ap3)** | **10.40.99.13** (VLAN99) · Identity `ap3` | ✅ **LIVE seit 2026-08-30** (TP28-Nachtrag) · `cAPGi-5HaxD2HaxD`, RouterOS **7.24.1** + RouterBOOT 7.24.1 (**seit 2026-08-31 ist die ganze Flotte auf 7.24.1**, TP27-Angleichung erledigt) · reiner CAP, Manager `crs326-core` · **Technik OG**, Uplink `ether1` → **branch `ether24`** (Trunk) · Kanäle **`5180/ax`** (Band A, non-DFS, 5 GHz) + **`2437`** (ch6) nach TP25-Plan · 🔴 **nur 2 Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out, **kein** Bridge-Port) → `ap.extra_ports=[]` · `device-mode=basic` (scheduler/fetch/romon **erlaubt**, anders als ap1/ap2) · Dateien `routeros/tp28-ap3-phase{1,2}-*.rsc` + `-service-accounts.rsc` + `-werksreste.rsc` |
| **🔑 Einheitliches Admin-PW (2026-08-08)** | alle 4 MikroTik (core/branch/ap1/ap2) | user `admin`, gleiches PW (`~/.secrets/credentials.env` → `ROUTER_*_PASS`/`CRS326_*_PASS`; „Überlebensversicherung", User-Vorgabe). Werks-Defaults ap1/ap2 damit obsolet. |
| NAS (nas-bf, DS923) | 10.40.20.10 | SSH `adm.christoph@10.40.20.10` (kein PW nötig) · Docker (Portainer endpointId=14) |
| platform-pg (LXC 105, pve) | 10.40.20.20 | SSH via pve (`pct exec 105 --`) · **TP20 ✅ 2026-07-16: PG16, DB `nautobot`, Creds `postgres/.secrets`** |
| Nautobot (Portainer-Stack, NAS) | **10.40.20.154** (TP45 Schicht A 2026-09-21: `.30/.31/.32/.34` → `.154/.155/.156/.158`) | Portainer ep14, Stack Id 203 · **TP21 ✅ 2026-07-16: `ghcr.io/nautobot/nautobot:3.1.7`, DB=platform-pg, Creds in `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored)** · **TP22 ✅ 2026-08-13: Golden-Config-App + Device-Adoption DURCH** — alle 4 MikroTik adoptiert, Deploy-Pipeline scharf (Image `3.1.7-gc9`), **68/68 Compliance-Checks grün / 0 Drifts** (2026-08-16) |
| ~~Omada-Controller~~ | — | ❌ **entfernt 2026-07-16** (User hat den Container gelöscht — overkill für 1 Switch auf der Abschussliste, s. TP2.1/TP15) |
| ~~omada-mcp~~ | — | ❌ **entfernt 2026-07-16** — mit dem Controller gelöscht. ✅ **Aufräumen erledigt/gegenstandslos (geprüft 2026-08-16):** nicht mehr in `mcpServers` registriert (nur noch als „disabled" in ein paar Projekt-Scopes = harmloser Merkposten), und **kein `omada`-Hostname mehr im CF-Ingress** |
| pve-mcp | 10.40.20.173 | **StreamableHTTP `http://10.40.20.173:3000/` (Root-Pfad, NICHT /sse)** · NAS-Stack ep14 (=nas-bf `.20.10`, NICHT LXC .126!) id 202 (vlan20-only, TP14.1 2026-07-10) · **mcp-pve v0.6.1 (Upgrade 2026-07-10: Single-Session-Bug 0.4.8 „Server already initialized" gefixt, TP14.2a)** · `~/.claude.json` ✓ |
| Proxmox (pve) | 10.40.20.100 | `ssh root@10.40.20.100` (SSH-Key ✓) · **TP14 2026-07-13: Host-Mgmt VLAN1→VLAN20** (vmbr0.20 + Default-GW-Flip); `.41.100` bleibt interim ohne Route |
| Docker-LXC (auf pve) | 10.40.20.126 | Portainer CE 2.40.0 (`https://10.40.20.126:9443`) · **TP14.2 2026-07-10: LXC 400 net0 tag=20/.20.126 (iface heißt jetzt `veth20`, NICHT eth0); Docker-Netz `vlan20`=ipvlan parent `veth20`; adguard_bf1 .20.16, adguard_fm2 .20.17** |
| Mac mini | **10.40.20.167** (VLAN20) | `ssh macmini` · ⚠️ die früher hier stehende `10.40.41.167` war **stale** (VLAN1) — live korrigiert 2026-08-16 (ARP `reachable` auf `vlan20`) |
| Arris-Kabelmodem | (Bridge-Mode, keine Mgmt-IP im BF-LAN) | Liwest-Edge · speist **`crs326-core vlan100`** (VLAN100-Transfer, WAN-IP `90.146.33.177`) — seit Core-Cutover 2026-08-08, davor via sw1 gi16 → ap1 · **FritzBox 6660 seit 2026-07-01 komplett ausgebaut** (Liwest-Tausch, kein Zurück); Telefonie nicht weitergeführt |
---
## Tooling
- **MikroTik (core/branch/ap1/ap2):** MikroMCP-Server live → `mcp__mikromcp__*`-Tools (routerId `crs326-core`/`crs326-branch`/`ap1`/`ap2`).
Reads immer safe; Writes mit `dryRun:true` previewen, dann `apply_plan`. **Passwort setzen via `manage_user set-password`** (`/user set*` ist per Deny-Policy blockiert). ⚠️ **Reboot geht auf ROS 7.24 gar nicht ueber MikroMCP:** das `reboot`-Tool sendet `delay`, die REST-API kennt den Parameter nicht (`unknown parameter delay`), und `run_command` ist fuer `/system reboot*` gesperrt — Weg ist `SSHPASS="$PW" sshpass -e ssh admin@<ip> "/system reboot"` (PW aus `~/.secrets`, im Output
maskieren). 🔴 **Niemals `sshpass -p`** — das schreibt das Passwort in die Kommandozeile, wo es in
`ps` und jedem argv-Trace steht; **genau so ist am 2026-08-22 das Core-Passwort in ein
Sitzungstranskript geraten** und musste rotiert werden. `-e` hält es in der Umgebung des Kindprozesses.
✅ **Alle sechs Skripte im Repo sind am 2026-08-31 umgestellt** (`nautobot/gc-transaktion.py`,
`routeros/import-core-rsc.py` und die vier inzwischen nach
`routeros/archiv-tp15-cutover/` archivierten Cutover-Skripte) — gemessen
mit `ps`-Stichproben plus Negativkontrolle, nicht nur gelesen. **Neue Skripte erben die Regel nicht
von selbst**: wer einen sshpass-Aufruf schreibt, baut `env=dict(os.environ, SSHPASS=pw)` mit ein. `bridge/vlan/set` nur auf `[find … and !dynamic]`.
🔴 **Zweiter Leak-Weg, gleiche Klasse: `list_wifi_interfaces` gibt `security.passphrase` ALLER
Profile im Klartext zurück** — am 2026-08-24 hat ein Read, der nur die `cap-wifiN`-Zuordnung finden
sollte, damit **fünf WLAN-PSKs** (`main`/`iot`/`media`/`untrusted`/`guest`) in ein Transkript
geschrieben. Kein Werkzeugfehler — das falsche Werkzeug für die Frage. **Für Kanäle
`/interface wifi channel print detail`, für die CAP-Zuordnung `… print where master=yes` mit
Feldauswahl.** Der Scan selbst (`/interface wifi scan`, **nur am Manager**) ist unbedenklich.
Merkregel dahinter: **vor jedem Read fragen, was das Tool AUSSER dem Gesuchten noch ausgibt.**
- **~~ALLNET-Switch (sw1)~~:** ❌ **Gerät ausgebaut 2026-08-08** → der Telnet-Runner unter `switch/` ist **Archiv, nicht mehr ausführbar** (kein Endpunkt). Siehe `switch/README.md`. Nicht mehr als Werkzeug anbieten.
- **~~Omada-Switch (sw2)~~:** ❌ abgebaut 2026-07-23 (TP15, durch `crs326-branch` ersetzt). Controller + omada-mcp komplett gelöscht — **Omada spielt keine Rolle mehr, nie wieder aufgreifen** (User-Entscheid 2026-07-22). Aufräum-Reste ✅ geprüft 2026-08-16 (s. Tabelle oben).
- **Nautobot Golden Config (= der einzige legitime Config-Weg für Netzgeräte):** `nautobot/gc-configplan.py`
(Token = `NAUTOBOT_SUPERUSER_API_TOKEN` aus `docs/nas-vlan20/stacks/210-nautobot.yml`, gitignored).
`--job <name>` legt manuelle Pläne an (Dry-Run default), `--deploy-ids …` deployt gestaffelt,
`--refresh-compliance` fährt **Backup → Intended → Compliance** in der einzig richtigen Reihenfolge.
⚠️ **`POST /api/plugins/golden-config/config-plan/` ist `405` — das ViewSet ist read-only.** ConfigPlans
entstehen nur über den Job **`Generate Config Plans`** (`plan_type=manual` + `commands`-TextVar).
- **Docker-Management:** Portainer API `https://10.40.20.126:9443`, Key in `mcp/portainer/.env`.
- 🔴 **`pct push` meldet Exit 0, auch wenn es die Quelldatei NICHT lesen kann** (gemessen 2026-09-21).
`pct push 130 <fehlende-datei> /etc/caddy/Caddyfile` → `failed to open … for reading`, **`rc=0`**.
Ein `&&`/`||` hinter dem Push **beweist nichts**, und ein anschließendes `validate`/`reload` prüft
dann die **alte** Datei → **grüne Abnahme bei unverändertem Zustand**. Genau so sah der
Nautobot-Umzug am 2026-09-21 „gelandet" aus, während live noch die alte Backend-IP stand
(Symptom: 502 bei gesundem Backend). **Regel: Push-Werkzeug wird geprüft, nicht geglaubt** — nach
jedem `pct push` den Inhalt vergleichen (`md5sum` diesseits/jenseits in *einem* Remote-Aufruf,
Abbruch bei Abweichung). Muster: `push_verified()` in `caddy/deploy.sh`. Dieselbe Klasse wie
`nc -z` (09-01) und das `grep|sed`-Maskierungsmuster (08-31): **erst das eigene Werkzeug
verdächtigen, dann das Netz.**
- 🔴 **Die Core-Adressliste `nautobot` wird von Golden Config NIRGENDS gerendert** (Fund 2026-09-21).
Der Mgmt-Zugang des Cores für die Deploy-Pipeline läuft über `src-address-list=nautobot`
(Regel `*28`) — die Liste selbst stammt aber aus dem **handgelaufenen**
`routeros/tp5-etappe2-whitelists.rsc` und steht in **keinem** Template (`grep address-list` im
Template findet nur Kommentare). 🔴 **Ein GC-Recovery von null erzeugt damit einen Core, in den
Nautobot nicht mehr per SSH kommt** — die Pipeline wäre tot und könnte sich nicht selbst
reparieren. Gleiche Lücke wie die DHCP-Reservierungen. **Merkregel: was auf dem Gerät existiert,
aber nicht im Template, ist eine Recovery-Lücke, auch wenn Compliance 140/140 grün meldet** —
die Zahl prüft nur, was gerendert wird.
Ablösung durch Komodo (native MCP) geplant.
---
## Kritische Gotchas
- 📜 **~~ALLNET-Gotchas (1 Telnet-VTY, `save` nicht vergessen)~~ — gegenstandslos seit 2026-08-08** (Gerät ausgebaut). Nur noch historisch relevant, s. `switch/README.md`.
- 📝 **WLAN-Logs sind seit 2026-08-30 persistent — und der Topic heißt `wireless`, nicht `wifi`.**
Auch auf dem neuen `wifi-qcom`-Stack tragen Roaming-/Connect-Events `wireless;info` bzw.
`wireless,debug`. Ein geratenes `topics=wifi` wäre wirkungslos geblieben. Zwei Regeln auf dem
**Core** (nicht auf den CAPs — unter CAPsMAN führt der Manager die Events) schicken `wireless`
und `caps` in denselben Kollektor wie TP5 (`10.40.20.18:514`, Tagesdateien unter
`/opt/tp5-syslog/log/10.40.20.1/`). ⚠️ **RouterOS UND-verknüpft Topics innerhalb EINER Regel**
(`topics=info,!firewall` = info UND NICHT firewall) — `topics=wireless,caps` hätte **nichts**
gematcht, es braucht zwei Regeln. Vorher lief alles nur in einen ~75-Zeilen-Ringpuffer, der von
einer Stunde Arbeit überschrieben war; ein seit Wochen gemeldetes Client-Problem war deshalb
**rückwirkend nicht diagnostizierbar**. `/system logging` steht jetzt unter der 27. ComplianceRule
`system-logging` — vorher unter keiner. **Merkregel: bevor man ein sporadisches Problem sucht,
erst prüfen, ob es überhaupt irgendwo mitgeschrieben wird.**
- 🧮 **Ein Log-Burst über VIELE MACs ist Infrastruktur, über EINE MAC ein Client** (Fund 2026-08-31).
Am 31.08. sahen 13 × `caps,info … joined`, 4 × `activity timeout` und zwei Massenbursts
`association rejected, no cached PMK` (74 Zeilen, **38 verschiedene MACs**) wie eine CAPWAP-
Instabilität aus, die das ganze Netz trifft. Es waren die **Reboots des TP27-Upgrades**. Jedes
Ereignis fiel auf einen Neustart. 🔴 **Vor jeder Client-Diagnose erst `grep " caps,"` und
`/system resource get uptime` lesen** — sonst sucht man einen Gerätefehler, der ein geplanter
Reboot war. Der Umkehrschluss gilt auch: 93 von 93 `reauthenticating` von **einer** MAC auf
**einer** SSID sind ein echter Client-/SSID-Befund.
- 🗺️ **Die `cap-wifiN`-Karte verschiebt sich bei JEDEM CAP-Rejoin, nicht nur beim Provisionieren.**
Am 2026-08-31 wanderte sie zweimal an einem Tag (08:09 Re-Provisionierung, 14:36 Core-Reboot);
eine im Handoff festgehaltene Tabelle war nach sechs Stunden falsch. ⇒ **Interface-Namen in
Logzeilen sind ohne einen zeitnahen Karten-Snapshot NICHT zuschreibbar.** Vor jeder Auswertung
`/interface wifi radio print terse proplist=cap,radio-mac,interface` am Manager lesen — und wenn
zwischen Ereignis und Auswertung ein Rejoin liegt, die Zuordnung als **unbekannt** behandeln
statt zu raten.
- 🔑 **CAPsMAN verteilt KEINE Login-Passwörter — nur WLAN-PSKs.** Zwei verschiedene Dinge heißen
„Passwort": die **WPA-Passphrase** (`/interface wifi security`) liegt am Manager und wird an die
CAPs verteilt ✅; der **Geräte-Login** (`/user`) ist eine **lokale Benutzer-DB auf jedem Gerät** ❌.
Beleg: ap1 trägt `admin`, `ha`, `nautobot` lokal, mit gerätelokalen `last-logged-in`-Zeitstempeln.
Das einheitliche Admin-PW ist eine **von Hand angewendete Konvention**, keine Automatik — und
logisch kann es nicht anders sein: ein Gerät *wird* erst zum CAP, nachdem sich jemand eingeloggt hat.
- 🔴 **Ein neuer CAP braucht `configuration.manager=capsman` auf JEDEM Master-Radio.**
`/interface wifi cap set enabled=yes` allein reicht nicht — ohne das Feld bietet der CAP seine
Radios dem Manager gar nicht erst an. **Symptom (2026-08-30, ap3):** `remote-cap print` zeigt
`state=Ok`, aber `/interface wifi radio print` am Manager listet für dieses Gerät **nichts**, und
weil die Provisioning-Regel dann nicht matcht, sucht man den Fehler in der `radio-mac`. Im
Werkszustand ist das Feld leer (= `local`).
- 🔌 **Werks-MikroTik am Trunk: `ether1` ist WAN, und das sieht aus wie ein Netzproblem.** Beim
cAP ax (und den hAPs) steht `ether1` per defconf in der Interface-Liste `WAN`. Folge: Discovery
aus (**kein MNDP → in `/ip neighbor` unsichtbar**), Input-TCP verworfen (22/80/8291/8728 **zu**),
**ICMP aber erlaubt** — es *pingt*, was sich wie „erreichbar" liest. Kein Registry-Eintrag und
kein MikroMCP hilft, denn Port 80 ist zu. **Der Weg rein ist die LAN-Seite** (`ether2` oder das
Werks-WLAN); dort `/interface list member remove [find where interface=ether1]` — das ist der Fix.
- 🔀 **`pvid` ist INGRESS, `tagged`/`untagged` ist EGRESS.** Ein Port mit `pvid=99`, der in der
*tagged*-Liste von VLAN99 steht, nimmt untagged an und antwortet **getaggt**. Symptom
(2026-08-30): ein DHCP-Lease hing ewig auf `offered` und ARP auf `failed` — der DISCOVER kam an,
der ACK nie. Für ein Werksgerät gehört der Port **untagged** in die VLAN-Zeile.
- 🕳️ **Ein neues Gerät ohne `secrets_group` erzeugt eine LÜCKE, keine Fehlermeldung.** Ohne
Secrets Group scheitert der Golden-Config-**Backup**-Job still; die *intended* config wird
trotzdem sauber gerendert, aber ohne Backup entstehen **keine Compliance-Einträge** — und die
Gesamtzahl bleibt unauffällig (bei uns 104/104), weil ein Gerät ohne Ergebnisse auch keine Drift
meldet. 🔴 **Merkregel: eine gleichbleibende Compliance-Zahl nach dem Hinzufügen eines Geräts ist
ein BEFUND, kein Erfolg.** Erst `130/130` bewies, dass ap3 wirklich geprüft wird. Zweite
Voraussetzung: die dedizierten Service-Accounts `nautobot`/`ha` (TP-PWD) existieren auf einem
Werksgerät nicht.
- 🔢 **Firewall-Regelnummern aus `print where` sind Handles, keine Positionen.** Nach einem
`place-before` zeigte `/ip firewall filter print terse where comment~"..."` die Nummer **60**,
während `drop` auf **59** stand — die Regel lag trotzdem davor. Beweis ist der **Zähler**: eine
Regel hinter dem `drop` kann nicht zählen. Wer nach der Zahl geht statt nach `print stats`,
„repariert" eine funktionierende Regel kaputt.
- 🔌 **`crs326-core` hat KEIN PoE-out** (`/interface ethernet poe` existiert dort gar nicht,
CRS326-24G-2S+). Ein AP an einem Core-Port braucht eigene Stromversorgung. Der **cAP ax** bringt
dafür **Netzteil UND Gigabit-PoE-Injector im Karton** mit (Produktseite, 2026-08-30) — die
TP25-Notiz „kein Netzteil im Lieferumfang üblich" gilt für dieses Modell **nicht**. Symptom, wenn
man das übersieht: der Core-Port steht auf `no-link` und man sucht den Fehler im VLAN-Tagging.
- 🧩 **Die AP-Flotte ist seit ap3 heterogen — Literal-Listen im GC-Template sind dadurch gefährlich
geworden.** Der AP-Zweig hatte `ether3/ether4/ether5` als Bridge-Ports **hartkodiert**; das war die
stille Annahme *„AP == hAP ax² mit 5 Ports"*. Der **cAP ax hat zwei**. Seit 2026-08-30 kommt die
Liste aus **`ap.extra_ports`** (ConfigContext `ap-common`, Default = hAP-ax²-Bestand; ap3
überschreibt sie per `local_config_context_data` mit `[]`). ⚠️ **Nautobot merged Dicts tief, Listen
ERSETZEND** — für diesen Fall genau richtig, bei `zones`/`radios` wäre es eine Falle.
Merkregel: **bevor ein zweites Modell derselben Rolle dazukommt, die Literal-Blöcke im Template
daraufhin lesen, welche Zeile eigentlich „Gerätetyp" meint und nicht „Rolle".**
- 🔵 **Container auf MikroTik — korrigiert 2026-08-25, die alte Pauschalregel war nie nachgemessen.**
Es hiess hier „hAP ax² hat 128 MB Flash, kein USB → Container NICHT auf dem Router". Live gelesen:
**ap1/ap2 sind `arm64` mit 95 MB FREIEM Flash und 690 MB freiem RAM** — das reicht für **einen**
kleinen Container (USB ist laut MikroTik-Doku nur für schnellere Extraktion, nicht Pflicht).
Faustregel: **~70 MB nutzbar je AP**, weil RouterOS Reserve für sein eigenes `.npk` braucht.
🔴 **`crs326-core` dagegen NIE:** `arm` **32-bit**, **2,9 MB frei von 16 MB**. Der Core ist der
Netz-SPOF, aber **kein Anwendungs-Host** — das ist eine Hardware-Tatsache, keine Politik.
🔑 **Und die TP16.1-Aussage „Connector als RouterOS-Container ❌ nicht möglich" gilt nur für den
WARP-Connector**, weil der ein **TUN-Device** braucht (RouterOS reicht keines durch).
**`cloudflared` braucht kein TUN** — Userspace-Proxy. Die Schlussfolgerung überträgt sich nicht.
Merkregel: **bei einer „strukturellen" Einschränkung erst prüfen, ob sie ein Default ist oder ein
Gesetz** — und ob sie dem konkreten Dienst überhaupt gilt.
- **Schwere Container laufen weiterhin auf NAS/pve**, nicht am Router (Caddy, AdGuard: die eine Config
ändert sich staendig, die andere schreibt dauernd Logs auf NAND).
- ⏱️ **Ein konstanter 10-Sekunden-Hänger ist ein DNS-Timeout, kein Lastproblem** (Fund 2026-09-01,
TP16.3-Randbefund). `gitea` hing bei ~10 % der Anfragen exakt 10,1 s — quer über Web-UI,
`/api/healthz` **und** den Git-Pfad, also kein Rendering. Ursache: **MariaDB (LXC 302) hatte
`skip-name-resolve` auskommentiert** und machte bei **jeder neuen Verbindung** einen
Reverse-Lookup, während ihr `/etc/resolv.conf` noch auf den toten `10.40.41.254` zeigte
(glibc: 5 s × 2 Versuche = 10 s). Pool-Verbindungen waren schnell ⇒ es wirkte sporadisch.
🔑 **Drei Merkregeln daraus:** (a) **immer gegen eine zweite Sache messen, die denselben Weg
nimmt** — `vw`/`hass` über dieselbe Strecke hatten 0 von 30 Hängern, das hat CF, Tunnel, Firewall
und Caddy in einem Schritt entlastet; (b) **wer an einer DB hängt, sagt die Verbindungstabelle**
(`ss -tn | grep 3306`), nicht die Vermutung — hier hingen **Vaultwarden, Home Assistant und gitea**
an derselben MariaDB, und die Annahme „Vaultwarden nutzt SQLite" war schlicht falsch;
(c) **ein toter Resolver ist selten allein** — der Sweep über alle LXCs fand **fünf** Container mit
`10.40.41.254`. Sweep: `for c in $(pct list|awk 'NR>1{print $1}'); do pct exec $c -- grep ^nameserver /etc/resolv.conf; done`
- 🔌 **`networking.service` failed bei funktionierendem Netz = tote `auto`-Stanzas in
`/etc/network/interfaces`** (Ursache gefunden + behoben 2026-09-01). In vier LXCs standen dort noch
Einträge aus dem **alten VLAN1-Schema** (`eth0` → `10.40.41.x`) und aus früheren Versuchen
(`veth16`, `veth24`, Tippfehler `eth01`) — auf Interfaces, die es im Container gar nicht gibt.
`ifup -a` scheitert daran, das echte `veth20` kommt trotzdem hoch. **Fix:** nur die Stanzas
entfernen, deren Interface in `ip -br link` fehlt (`lo` und die echten behalten), Backup daneben.
🔴 **Abnahme ist ein echter Reboot, nicht `systemctl is-active`** — der Servicestatus sagt nichts
darüber, ob beim nächsten Start etwas hochkommt.
- 🧪 **Ein Messwerkzeug, das etwas anderes beantwortet als die Frage, kostet mehr Zeit als der Fehler**
(zweimal in einer Nacht, 2026-09-01). (a) **`nc -z` lieferte falsch-negative** „Port zu" für neun
Dienste gleichzeitig — inklusive des SSH-Ports, über den die Session gerade arbeitete; `curl` bekam
von demselben Host **HTTP 200 in 24 ms**. **Bei „alles gleichzeitig kaputt" zuerst das eigene
Werkzeug verdächtigen**, dann das Netz: ein Python-Socket oder `curl` entscheidet in Sekunden.
(b) **Ein Firewall-Zähler taugt nicht als Wegbeweis**, wenn der Client die Verbindung offenhält
(`cloudflared`): er zählt nur NEUE Verbindungen und steht still, obwohl alles läuft — richtig ist
die **Verbindungstabelle** (`/ip firewall connection`).
- 🔌 **`networking.service` kann in einem LXC dauerhaft `failed` sein, ohne dass das Netz fehlt** —
und dann startet alles mit `After=network-online.target` nach einem Reboot **nicht**
(Fund 2026-09-01: `101`, `102`, `106`, `107`; **influxdb2 kam nach dem Neustart nicht hoch**,
Unit `enabled`, **kein Startversuch im Journal**). Fällt nur auf, wenn man neu startet — die
Container liefen 13+ Tage. **Vor jedem geplanten LXC-Neustart `systemctl --failed` lesen und
danach die Ports gegen vorher vergleichen**, sonst verliert man still einen Dienst.
- **VLAN20-Docker-Hosts: Host-DNS nach dem Umzug prüfen (2026-07-10, TP14.2a):** NAS + pve-LXC zeigten `/etc/resolv.conf` noch auf totes `.41.254` → `docker pull` ghcr scheitert (`EAI_AGAIN`, aber Egress ok!). Fix = `.20.1`: LXC `pct set 400 --nameserver 10.40.20.1`; **Synology-Falle: DSM-„Allgemein"-DNS ≠ was `dockerd` liest** — Interface zieht DNS per DHCP → Fix an der *Netzwerkschnittstelle* „DNS manuell". Laufende Container frieren den `127.0.0.11`-Upstream zum Erstellzeitpunkt ein (Recreate nötig). Merkregel: `docker-pull-hängt` + `dns.lookup EAI_AGAIN` aber `TCP zu ghcr-IP:443 ok` = toter Resolver, nicht Egress.
- 📜 **~~ALLNET VLAN-DB (TP1)~~ — historisch.** Die VLAN-Semantik gilt weiter (**40=MEDIA, 50=UNTRUSTED**), die Portangaben (GE1/GE2/GE8) beziehen sich auf ein ausgebautes Gerät.
- **ap3 hängt an `crs326-branch ether24`** (Trunk, native VLAN1 + tagged 10-99); **branch-Uplink `ether1` → `crs326-core ether8`** (korrigiert 2026-08-16 — die frühere Angabe „→ sw1 GE8" stammt aus der Zeit vor dem Core-Tausch; MAC-hart belegt in TP22 Schritt 5). ap2 hängt seit dem TP25-Tausch (2026-09-15) an `core ether21` (Technik UG), ap1 an `core ether3` (Sideboard).
- **Mesh = NEIN** (beide APs haben wired Trunk-Uplink → Loop-Risiko).
- **Beide APs = CAPs, Manager ist `crs326-core` (TP28 ✅ 2026-08-17).** Der Entscheid „kein CAPsMAN" von 2026-07-22 ist **revidiert**: er galt der Konstellation ap1=Manager+AP, in der ein Manager seine eigenen Radios nicht provisionieren kann — mit dem Core als Manager sind beide APs symmetrisch. Der Core braucht dafür **kein** `wifi-qcom`, `capsman` steckt im Basispaket.
- 🔴 **`/interface wifi scan` geht am CAP NICHT — nur am Manager** (Fund 2026-08-23). Das Gerät
antwortet `failure: not allowed (/interface/wifi/scan)`; das ist **weder device-mode noch eine
Policy-Lücke** (Gegentest: `/tool fetch` sagt auf demselben Gerät explizit „not allowed **by
device-mode**", und die Gruppe `full` hat `test`+`sniff`). Ein CAP besitzt sein Radio nicht.
Richtig ist `/interface wifi scan cap-wifiN duration=…` **am Core** — dort läuft es einwandfrei.
⚠️ Die `cap-wifiN`-Zuordnung vorher über `radio-mac` bestimmen, sie ist nicht stabil.
- 🔴 **Messfalle: unter CAPsMAN führt der MANAGER die Tabellen.** `/interface/wifi/registration-table` und die `wireless`-Logs sind auf ap1/ap2 **leer**, obwohl das WLAN läuft. **Immer am Manager zählen.**
- 🔴 **Die CAPs finden den Manager per Layer-2-Discovery** (`discovery-interfaces=vlan99`, `current-caps-man-address` ist eine **MAC**, keine IP). Deshalb müssen **MNDP (udp/5678)** und **CAPWAP (udp/5246-5247)** in der `input`-Kette des Core erlaubt bleiben — sonst kommt das WLAN nach dem nächsten AP-Reboot nicht zurück, während laufende Sessions unauffällig weiterlaufen (Fund 2026-08-18, TP5 Etappe 4).
- 🔁 **Slave-VAPs aendern heisst zweimal arbeiten (2026-08-22).** `slave-configurations` in einer
`provisioning`-Zeile zu setzen bewirkt am Funk **nichts**, solange nicht
`/interface wifi capsman remote-cap provision [find where identity="apX"]` nachgezogen wird —
und das gilt in **beide** Richtungen: auch beim Entfernen senden die VAPs sonst munter weiter.
🔴 **Und die Abnahme nicht am Kommentar festmachen:** beim Rueckbau wird der Interface-Kommentar
geleert, waehrend der VAP steht. Wer auf `comment~"ssid"` prueft, sieht "weg" und liegt falsch.
Richtig ist `datapath.vlan-id` bzw. die Interface-Zahl **am CAP** (`/interface wifi print` dort).
Beim Interface-*Zustand* ist ueberhaupt der CAP die Wahrheit: der Manager zeigt nach einer
Re-Provisionierung `running=false` fuer alle `cap-*`, waehrend am CAP alles laeuft — das
Gegenstueck zur Regel, dass die *Registration-Table* am Manager zu lesen ist.
- **Local Forwarding ist keine Einstellung mehr:** im neuen `/interface/wifi` gibt es kein zentrales Forwarding, `datapath` existiert dort nicht.
- 🔴🔴 **`/interface wifi cap slaves-datapath` MUSS auf beiden APs gesetzt sein** (`dp-caps-slaves`, ein lokales datapath-Objekt mit `bridge=bridge` und **ohne** `vlan-id`). Es ist das Einzige, was die vom Manager erzeugten **Slave-VAPs** lokal in die Bridge haengt: der Manager schickt `datapath.bridge=bridge` mit, **der CAP uebernimmt davon nur die `vlan-id`**. Fehlt es, assoziieren Clients auf `-iot`/`-media`/`-untrusted`/`-guest` sauber und bekommen **nie DHCP**, waehrend `dirtyvader-main` weiterlaeuft — dessen Master-Radios `wifi1`/`wifi2` sind statische Bridge-Ports. Genau so lagen IOT+MEDIA am 2026-08-18 rund 14 Stunden (Ursache: CAPsMAN-Cutover 08-17, nicht die TP5-Haertung). **Abnahme fuer jeden WLAN-Umbau ist eine frische DHCP-Lease pro SSID — NICHT die Registration-Table**, die zeigte waehrend des Ausfalls 25 zufriedene IOT-Clients.
- 🔎 **Merkregel „connect ok, kein DHCP":** zuerst `torch` auf dem betroffenen VLAN am Core UND den TP5-Drop-Log lesen. Sind **beide leer**, wird nichts verworfen — es kommt nichts an, und die Firewall ist entlastet. Dann liegt es an L2 (Bridge-Mitgliedschaft/Tagging), nicht am Regelwerk.
- mgmt-VLAN-Wechsel am Switch = Lockout-Risiko: VLAN muss existieren + Port muss es tragen, in einem Schritt.
- 🔥 **Die Firewall ist seit 2026-08-18 deny-by-default — in BEIDEN Ketten.** Ein neuer Dienst über eine VLAN-Grenze braucht ab jetzt eine explizite Regel, sonst funktioniert er einfach nicht. Diagnosewerkzeug ist die **Log-Regel direkt vor dem Drop** (`TP5-WOULD-DROP` / `TP5-INPUT-WOULD-DROP`), die weiterläuft und in den Kollektor `10.40.20.18:514` schreibt (Dateien auf dem Docker-LXC unter `/opt/tp5-syslog/log/`). **Erst dort nachsehen, bevor irgendwo anders gesucht wird.** **MAIN→INFRA/MEDIA/MGMT/IOT stehen dauerhaft offen** (IOT seit TP5 E5, 2026-08-19 — Drucker + Shellys); **IOT→MAIN bleibt bewusst zu**, dort sitzt der Wert der Segmentierung. Ad-hoc-Zugriff aus MAIN in ein **noch geschlossenes** VLAN (UNTRUSTED, GUEST) läuft über die selbstverfallende Liste: `/ip firewall address-list add list=jit-admin address=<deine-IP> timeout=1h`. *(Die früher hier genannten Beispiele „Scannen, IoT anlernen" brauchen jit-admin seit E5 nicht mehr.)*
- 🖨️ **Merkregel aus dem E5-Drop-Log:** der Drucker fällt mit **udp/161 (SNMP)** auf, nicht mit 631 — macOS fragt zuerst den SNMP-Status ab und bricht ab, bevor es zu IPP kommt. **Eine Whitelist auf 631 hätte nichts repariert.** Bei „Gerät antwortet nicht" also das Log lesen, statt den erwarteten Port zu raten.
- ⏰ **Zeitquelle ist der Core** (`/system ntp server`, seit 2026-08-18), ausgeliefert per **DHCP-Option 42** (`ntp-server=` in jedem der 7 Netze) und drift-überwacht (ComplianceRule `system-ntp`). Vorher lief **nirgends** eine Synchronisation: die anderen drei Geräte zeigten auf `10.40.99.1`, wo kein Server lief, und der Core selbst hatte gar keinen NTP-Client — ap2 war dadurch **17 Stunden** versetzt. 🔴 **Zwei Fallen dabei:** (a) ein NTP-Server mit `use-local-clock=no` und unsynchronisiertem Client liefert **Stratum 16**, das jeder Client verwirft — Server einschalten allein reicht nicht; (b) wird der Server eingeschaltet, *bevor* sein Client synchron ist, hält er diesen Zustand fest → **`enabled=no`/`=yes` nach der Synchronisation**. Merkregel: bei widersprüchlichen Zeitstempeln zwischen Geräten zuerst `/system ntp client get status` lesen, und zum Eingrenzen einen Client testweise auf eine öffentliche Quelle zeigen lassen.
- 📡 **SSDP-Relay für die macOS-Sonos-App:** LXC **108 `mcast-relay`** auf pve, zweibeinig `10.40.10.21` (VLAN10) + `10.40.40.21` (VLAN40), `multicast-relay` mit **`--noMDNS`** (mDNS macht der Core). Unit im Repo: **`mcast-relay/multicast-relay.service`**. **Zwei Beine sind zwingend** — SSDP ist link-lokal mit TTL 1, ein Relay muss Mitglied beider Broadcast-Domänen sein; Routing hilft nicht.
- 🔴 **`--ssdpUnicastAddr` gehört in ein zweibeiniges Relay NICHT — die Flagge hat den Hinweg 7 Wochen lang blockiert** (drin 2026-08-18, ausgebaut **2026-08-25**). Sie schreibt in `multicast-relay.py` **Z. 630** die Quelle *jedes* SSDP-Pakets an `239.255.255.250:1900` auf `<addr>:1901` um (**auch NOTIFY**); beim Senden greift dann **Z. 691** `if srcAddr == ssdpUnicastAddr and not onNetwork(srcAddr, tx['addr'], tx['netmask']): continue` — `10.40.10.21` liegt nicht im VLAN40-Netz, also **verwarf der Relay die M-SEARCH selbst, bevor sie die Player erreichte**. Der Rückweg gehört stattdessen **geroutet**: `forward TP5: MEDIA->MAIN` ist offen, die Player antworten unicast an den echten Sucher. (Upstream nennt den Rückweg selbst `# FIXME: record more than one?` — es gibt nur **einen globalen** Merkslot.)
- 🔎 **Symptom-Merkregel:** *App sucht hunderte Male, sieht NOTIFYs, baut aber nie eine Verbindung auf* = **Relay, nicht Netz**. Diagnose in einer Zeile — **Richtungsbilanz** im Journal: `grep -c "via eth1"` (VLAN10→VLAN40) gegen `grep -c "via eth0"`. **0 zu n heißt: der Hinweg fehlt.**
- ⚠️ **Aus dem Relay heraus lässt sich der Relay nicht testen** (Loop-Schutz ignoriert eigene Quelladressen) — am echten Client testen oder `journalctl -u multicast-relay` mitlesen. **Und am Client nicht auf Port 1900 messen:** Sonos-App *und* Spotify halten dort je ein `SO_REUSEPORT`-Socket, die Unicast-Antwort bekommt nur **eines** davon. Richtig ist eine Probe mit **ephemerem Quellport** (`bind((ifaddr, 0))`), genau wie ein echter Client sucht.
- 🔴 **Zweite, unabhängige Ursache desselben Falls (08-25): die Sonos-App suchte gar nicht.** Nach der Relay-Reparatur lag weiter nichts an — der Mitschnitt auf der VLAN40-Leitung (`pct exec 108 -- tcpdump -i eth1 -n -s0 -A "udp and host <client>"`) zeigte: **jedes** SSDP-Paket des MacBooks kam von **Chrome** (`ST: urn:dial-multiscreen-org:…dial:1`, `USER-AGENT: Chromium`), **kein einziges** von Sonos. Die App hatte Sockets offen (`*:1900`, `*:6969`, `TCP *:3400`) und redete mit der Cloud, legte aber nichts auf die Leitung. Ursache war das **Local-Network-Gate von macOS**: es stand auf **„an" und griff trotzdem nicht**. Fix = Schalter unter *Datenschutz & Sicherheit → Lokales Netzwerk* **aus, App beenden, wieder an, App starten** (hart: `sudo tccutil reset LocalNetwork com.sonos.macController2`).
⇒ **Merkregel: „die App sucht hunderte Male" ist erst dann ein Befund, wenn der Mitschnitt sagt, WER sucht.** Im Handoff vom Vormittag waren die gezählten 291 Suchen Chrome + eigene Proben; die Zahl hat drei Stunden in die falsche Richtung gezeigt.
- 🖥️ **Das MacBook ist dual-homed in VLAN10:** `en5` = **10.40.10.183** (USB-Dock, Default-Route) und `en0` = **10.40.10.82** (WLAN). Die früher als „zweiter Mac" gelesene `.183` ist **dasselbe Gerät** — beim Messen immer beide Beine prüfen.
- 🏠 **Home Assistant: der `http:`-Block in `configuration.yaml` ist WIRKUNGSLOS** (Fund 2026-08-26,
TP31). HA hat die http-Config nach **`.storage/http`** migriert (`"yaml_migration_done": true`);
seither wird die YAML kommentarlos ignoriert — selbst ein ungueltiger Schluessel darin erzeugt
**keinen** Config-Fehler. 🔴 **Die Logzeile fuehrt aktiv in die Irre:** `Received X-Forwarded-For
header from an untrusted proxy <ip>` faellt laut Quelltext nur bei **aktivem**
`use_x_forwarded_for` — man haelt die Option fuer wirksam und die Liste fuer falsch und editiert
die falsche Datei. Richtig ist der WS-Zweischritt **`http/config/configure`** (setzt `pending`,
startet HA neu) → binnen **5 Minuten** **`http/config/promote`**, sonst Auto-Revert auf `stable`.
Merkregel zur Eingrenzung: ein Testskript in `<config>/python_scripts/` + `python_script.reload` —
taucht der Service auf, liest HA das Verzeichnis, und die Frage ist nicht mehr *welche Datei*,
sondern *warum ein gelesener Block nicht wirkt*.
- 🐳 **Nautobot auf der DS923 braucht einen abgeschalteten Image-Healthcheck.** Der eingebackene Check ruft alle 10 s `nautobot-server health_check` = einen vollen Django-Start; unter Last dauert der >2 min, die Aufrufe **stapeln sich unbegrenzt** → Speicher alle → `fork()` mit ENOMEM → DSM räumt den ganzen Stack ab (am 2026-08-17 zweimal passiert, Load bis 73 bei 4 Kernen). Im Stack stehen deshalb `healthcheck: disable: true` auf worker+scheduler, `--concurrency=2` und `mem_limit`. **`cpus:` geht auf Synology nicht** (Kernel ohne CFS-Quota, der Stack-Deploy schlägt sonst fehl).
- 📜 **~~Omada ES208G = Easy Managed~~ — gegenstandslos** (Gerät abgebaut 2026-07-23, Controller gelöscht).
- **L3-Router = `crs326-core` (TP21.1 ✅ 2026-08-08, ersetzt ap1).** Bridge vlan-filtering=yes; VLAN-SVIs `.1` je VLAN mit DHCP (7 Server / 54 Leases); DNS über **DoH** zum CF-Gateway, `verify-doh-cert=yes` (TP23 Stufe 1 ✅ 2026-08-18). Die alte Notiz „core hat kein CA" war zu pessimistisch: es braucht kein Bundle, sondern **genau einen Root** (GTS Root R4, ~1 KB) — die Kette ist Leaf → GTS WE1 → R4. **Beide Firewall-Ketten enden seit 2026-08-18 mit `drop`** (TP5 E3/E4); `TP10-seal-lan` ist `disabled`, Mgmt-Input kommt nur noch aus VLAN10, von HA (`8728`), von Nautobot (`22`) und über `cf-mesh`. **ap1/ap2 sind NUR noch APs** — kein DHCP/DNS/NAT/Routing/WG dort. (Die frühere „TP4: ap1 ist L3-Router"-Zeile ist überholt.)
- **✅✅ Edge-Cutover (2026-07-01; Router-Umzug 2026-08-08): FritzBox ausgebaut, Arris im Bridge-Mode.** Liwest hat die FB6660 dauerhaft gegen ein Arris getauscht (physisch weg). WAN terminiert auf **`crs326-core vlan100`** (VLAN100-Transfer), core zieht die öffentliche Liwest-IP `90.146.33.177` → kein Double-NAT. **core = Public-Edge + WG-Hub** (fm/dw live), NICHT mehr ap1. Telefonie nicht weitergeführt. Runbook `routeros/wan-cutover-runbook.md` (ap1-Teile historisch).
- **hAP `device-mode=home` (ap1/ap2): scheduler + fetch blockiert** (`not allowed by device-mode`) → AP-Provisionierung idempotent **ohne** Scheduler. **`[/ping]` gibt nil in 7.23**. REST je AP `http://10.40.99.11|12/rest` (Creds = `ROUTER_AP{1,2}` in `~/.claude.json`). **Der L3-Router crs326-core ist KEIN device-mode=home** → dort gehen scheduler/fetch (z.B. DDNS-Updater).
- **Erstzugriff Werks-hAP am CRS-Fabric:** Uplink `ether1` = Werks-WAN → `192.168.88.1` (LAN) darüber NICHT erreichbar. Weg: Gerät auf **LAN-Port (ether2)** patchen ODER **Core-Temp-Pfad** `192.168.88.2/24` auf core-`bridge` + srcnat masq `192.168.88.0/24`, dann scp+`/import` (danach Temp-Pfad + stale ARP `.88.1` entfernen). Winbox-Neighbors zeigt die APs NICHT (MNDP pro-VLAN, Mgmt auf vlan99) — kein L2-Fehler.
- 🖨️ **ZWEI Drucker, nicht verwechseln (geklärt 2026-08-23):** der **Brother MFC** ist der
*eigentliche* Drucker und hängt auf **VLAN10** (`10.40.10.6`, MAC `30:05:5C:78:86:20`, **statisch**,
keine DHCP-Reservierung) — er steht damit im *selben* VLAN wie die Macs, die `forward`-Kette greift
dort gar nicht. Der **P750W ist der Etikettendrucker** auf **VLAN30** (`10.40.30.5`) — nur für ihn
gilt der VLAN-übergreifende SNMP-Fall unten.
- **P750W (Brother, VLAN30) läuft auf `dirtyvader-iot`** (User-bestätigt 2026-08-08) — **kein** Access-List-Pin nötig; die alte „nur am co-located L3-GW-AP erreichbar"-Logik ist mit L3-GW auf dem Core hinfällig. Coverage-Kanten/Terrasse grenzwertig (−76..−89 dBm); TX am AT-Max (18/15 dBm); echter Fix = Platzierung. SSoT `docs/wifi-p750w-coverage-and-map.md`. ⚠️ **Power-Save ist KEIN Thema dieses Netzes** — die früher hier stehende Merkregel „DHCP-geht-aber-Ping-nicht = ARP/Power-Save" war eine Fehldiagnose (2026-08-13 korrigiert: der Drucker war schlicht ausgeschaltet, das Ping-Symptom war WARP, s.u.). Nicht wieder einführen.
- **Ping auf JEDE lokale IP tot, aber Web/App/Print (TCP) geht überall = WARP-Client lokal versehentlich noch aktiv** (2026-08-13 live beobachtet, BF vor Ort): `cloudflared`/WARP ist QUIC-Proxy, kann **kein ICMP**, reicht TCP aber durch — auch wenn man physisch im BF-LAN steht, nimmt der Client bei aktivem WARP trotzdem den Tunnel-Umweg. **Fix: WARP trennen**, kein Router-Eingriff nötig. **Vor jeder Firewall-/Mangle-/VLAN-Suche zuerst prüfen**, wenn das Symptom „Ping auf ALLES tot, TCP geht" lautet — das ist ein Client-, kein Netzproblem. Strukturell schließt **TP16.1** (natives WireGuard-CF-Mesh auf `crs326-core` statt cloudflared-QUIC) genau diese ICMP-Lücke. Memory `ref_warp_local_icmp_trap`.
---
## Lessons for Sessions (2026-08-27, aus der Inventur-Session)
Verhaltensregeln für nachfolgende AI-Sessions — kleiner, klarer, weniger Drift:
- **Bestandsaufnahme vor Plan.** Erst was ist da, wo ist der Schaden — dann Vorschläge. Nicht umgekehrt.
- **Eine große Fragenrunde statt vieler kleiner.** Wenn der Plan von Annahmen abhängt, sammle die Annahmen in **einer** Runde, nicht in 3–4 Mikrorunden.
- **Master-Frage früh stellen.** Bei jedem Repo, jeder Datei, jedem Tooling: „Was ist die SSoT, wo lebt der Master, was ist Mirror?" — **bevor** du eine Lösung vorschlägst.
- **Drift zuerst, Lösungen später.** Drift-Tabelle posten + fragen, was richtig ist. Keine Lösungen vor dem Befund.
- **MCP-Erstellung/-Stilllegung = aktiv melden** (2026-08-27, Christoph-Direktive). Wer einen neuen MCP anlegt, ändert oder abschaltet — **meldet das proaktiv** an den User, nicht stillschweigend im Commit. Begründung: jede AI-Session ist gleichzeitig **Oberhand über Container** (MCP-Hosts) und **Garbage-Collector** auf Infra-Ebene — wenn Drift unbemerkt entsteht, sammelt sich Müll.
- **MCPs brauchen Host-Verifikation** vor Edit. Wenn der MCP verändert/editiert wird: vorher per Live-Read (nc/ps/curl) prüfen, ob der Host noch antwortet. Beispiel: 2026-08-27 — `shelly-mcp` zeigte auf `10.40.20.30:3030`, aber kein Host antwortet. Konfig auf `enabled=false`, dokumentieren statt hart fehlschlagen zu lassen. **(Stand 2026-08-28: shelly-mcp wird auf DS923 `10.40.20.180` deployt — `BUILD.md` liegt bereit.)**
- **Mac mini ist n8n-Host, nicht zentraler MCP-Host** (User-Direktive 2026-08-28): MCP-Dienste gehören auf die **DS923** (`nas-bf`, Portainer Endpoint 14) — nicht auf die User-Workstation. Aktuell laufen gitea-mcp + n8n-mcp **noch** auf dem macmini (historisch), gehören **bei Gelegenheit** auf die DS923 umgezogen.
- **Tote MCP-Skripte → ins Projekt-Repo, nicht global.** Wenn ein MCP nur für ein Projekt relevant ist (`cannaleo` für terpene, `terpene-pg` für terpene): gehört in `~/dev/<projekt>/mcp.json` + Skript im Projekt-Repo. Globale `~/.claude.json`/`~/.config/opencode/opencode.jsonc` = nur projekt-übergreifende Tools.
- **opencode-Skills: Default-Pfad kopieren, NICHT symlinken** (TP35 Schicht 6, 2026-08-28). Claude-Skill in `~/.claude/skills/<name>/SKILL.md` ist die Quelle; für opencode gehört er nach `~/.config/opencode/skills/<name>/SKILL.md` (`cp`, nicht `ln -s`). Begründung: opencode scannt diesen Pfad als Default — **kein** `skills.paths`-Eintrag in `~/.config/opencode/opencode.jsonc` nötig, das wäre redundant. Symlink wurde in der Inventur-Session testweise angelegt, hat Christophs "opencode = eigene Pfade"-Direktive verletzt und wurde wieder entfernt. Schema-Referenz: <https://opencode.ai/config.json> → `Config.skills.paths`. **Lehre aus dem Fall:** opencode-CLI-Verhalten nicht raten, Doku lesen — und ein "load-bearing"-Detail (Default-Scan-Pfad) zuerst verifizieren, bevor man ihn umgeht.
- **Skills sind IMMER global, niemals pro Projekt** (Christoph-Direktive 2026-08-28). `projektstatus` und alle übrigen Skills liegen **ausschließlich** unter `~/.claude/skills/` und `~/.config/opencode/skills/`. **Kein** Repo-lokaler Skill unter `.opencode/skill/` oder `.claude/skill/`. Begründung: keine Redundanz, keine Drift zwischen Skill-Versionen, eine Quelle pro Skill. **Merkregel:** wer einen Skill „überschreiben" will, editiert **die globale Datei** oder schreibt einen zweiten Skill mit anderem Namen — niemals ein Repo-Override.
- **Kommentar in Code ≠ Verhalten.** `mcp/shelly/.env` trug den Kommentar "gitignored, NICHT committen" — es gab aber **kein** `mcp/shelly/.gitignore`. Der Kommentar war Wunsch, nicht Wirklichkeit. `routeros/cloudflared/credentials.json` (Cloudflare-Tunnel-PrivateKey base64) lag ungegittoret im Working Tree. **Merkregel:** Behauptungen in Kommentaren/Konfig-Kommentaren durch `git check-ignore -v <datei>` (oder `git status`) verifizieren, bevor man darauf vertraut. **Lessons-Block erweitert 2026-08-28 nach `commit`-Session.**
- **opencode-TUI: führendes Leerzeichen vor `/command` manchmal nötig** (Beobachtung 2026-08-28, Skill „projektstatus" getestet; **identifiziert als bekannter Bug**). Symptom: Tippen von `/projektstatus` führt nicht zum Skill, ` /projektstatus` (führendes Leerzeichen) schon. Doku <https://opencode.ai/docs/commands> zeigt den Aufruf ohne Space. **Verifiziert:** Issue **anomalyco/opencode#27528** „ACP: prompt starting with '/' is silently dropped if command name is not recognized" — Status **Not planned (skipped)**, closed 2026-07-14. Maintainer haben es also gesehen und nicht gefixt. **Konsequenz:** Workaround (führendes Leerzeichen) ist der offizielle Pfad; Skill selbst ist nicht kaputt. Vor jedem neuen opencode-Build prüfen, ob das Verhalten weg ist.
- **Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, wenn sie lang wird.** Befund 2026-08-28: der „Nächster Schritt (2026-08-26): TP31 Schritt 5"-Satz stand zwei Tage nach seiner Formulierung noch im Ist-Stand-Block, obwohl ich in derselben Zeit drei Folge-Sessions eröffnet hatte. **Merkregel pro Edit:** drei Stellen berücksichtigen — Quick-Index (Tabelle), TP-Abschnitt, Ist-Stand-Block. Keine schlägt die andere; wer eine ändert, **muss** die anderen mitlesen. Strukturelle Antwort = TP36 (SSoT via Nautobot, Datei wird read-only Snapshot) — eigene Session, nicht nebenbei. **Lessons-Block erweitert 2026-08-28 nach Skill-Verifikation in opencode.**
- **`sed 's/=.*/=<set>/'` maskiert NICHT, wenn das `.*` greedy ist und der Wert Slashes/Backslashes enthält** (Befund 2026-08-28, shelly-mcp-Deploy). Ich habe `sed 's/SHELLY_DEVICE_PW=.*/<set>/'` auf die Stack-YML angewendet — der **Wert selbst** wurde in den Output geschrieben, weil mein Replacement-String nicht greedy genug war und ich die Pipe nicht mit `cat -v` oder `head -15 | sed 's|=.*|=<set>|'` abgesichert hatte. Folge: **`SHELLY_DEVICE_PW` ist jetzt als verbrannt zu betrachten** — wandert in **TP30 Posten #3** mit Hochstufung. **Merkregel:** bei jeder Ausgabe von Secret-haltigen Dateien entweder `head -c <n>` (Abbruch nach Bytes), oder die Datei vor dem Read mit `tr -d '\n' | sed 's|=.*|=<set>|'` über einen Stream-Filter schicken — **niemals** einfach `cat` und auf einen Replacement-Pattern vertrauen. Vergleichbar zum PSK-Leak 2026-08-24.
- **Vor jeder Drift-Bewertung `python3 nautobot/gc-transaktion.py status` lesen** (Befund 2026-08-31,
zweimal am selben Tag gestolpert). **Drift aus einem laufenden Versuch sieht identisch aus wie ein
Versehen** — die Compliance-Zahl unterscheidet nicht. Konkret: `crs326-core`/`wifi-security`
(`ft-over-ds=no`) wurde dem 7.24.1-Upgrade zugeschrieben und als „braucht eine Entscheidung"
weitergereicht; tatsächlich ist es **TP25 Fund E Hebel 1**, um 08:09 bewusst gesetzt, während der
Core erst um 14:36 gehoben wurde, geführt als offene Transaktion `TP25-fund-e-ft-over-ds`.
🔴 **Eine solche Drift grün zu machen macht den Versuch unsichtbar.** Umgekehrt gilt: was **dauerhaft**
gelten soll (z.B. das `veth`/`masquerade` aus TP16.3 §3.4), gehört **nicht** in eine Transaktion,
sondern ins **Soll** — Transaktion = befristeter Versuch, Template = Dauerzustand.
⚠️ Das Skript löst den Token-Pfad gegen die Repo-Wurzel auf und findet ihn **aus einem Worktree
heraus nicht** (`210-nautobot.yml` liegt nur im Haupt-Checkout) — die Transaktionsliste kommt
trotzdem, nur die Compliance-Spalte fehlt dann.
- **Maskierungs-Muster auf den SCHLÜSSELNAMEN ankern, nie auf ein Trennzeichen** (Befund 2026-08-31,
zweiter Fall dieser Klasse nach `SHELLY_DEVICE_PW`). `grep -n "TOKEN" datei | sed 's/:.*TOKEN/…/'`
maskiert **nicht**: `grep -n` stellt `<zeile>:` voran, und das `:.*TOKEN` matcht diesen Präfix statt
den Wert — ein nachgeschaltetes `cut -c1-60` schneidet dann genau nichts weg. So sind zwei
Nautobot-Tokens ins Transkript geraten. **Sicher ist nur**
`sed -E 's/(TOKEN|PASSWORD|SECRET|KEY|PASS)=.*/\1=<redacted>/'`. Und: **Werte nie suchen, um sie zu
sehen** — den Schlüssel per `sed -n 's/^…KEY=//p'` direkt in eine Variable holen, nie über `grep`
mit Ausgabe. Vergleichbar zum PSK-Leak 2026-08-24 und zum `sed`-Fall 2026-08-28.
- **Vor jedem Flash-Budget auf MikroTik `docker export | wc -c` messen, nicht `docker image inspect
.Size`** (Befund 2026-08-31, TP16.3). `.Size` meldete für `cloudflared` 27,5 MB, entpackt waren es
**69,5 MiB** — das offizielle Image passt damit auf keinen hAP/cAP. RouterOS braucht beim Import
**Tar UND entpacktes Abbild gleichzeitig**. 🔴 **Und `docker save` schreibt seit Docker 25
(containerd-Store) OCI statt docker-v1** → RouterOS meldet `no config found in manifest`, was nach
kaputtem Image klingt und ein Formatproblem ist. Beides in `docs/tp16.3-bf-public-ap-connector-runbook.md` §2.2.
**Checkliste vor jedem Edit-Vorschlag:**
1. Habe ich die SSoT für die Datei/das Repo benannt?
2. Habe ich die Drift zwischen Doku und Live dokumentiert (nicht stillschweigend korrigiert)?
3. Habe ich **eine** Fragenrunde mit allen offenen Annahmen gestellt?
4. Habe ich für jeden Edit **deine** Bestätigung eingeholt (nicht nur für den Plan-Output)?
5. **MCP-Änderung?** Vorher Live-Check (Port/Prozess), aktiv melden, Garbage-Collector-Pflicht.
6. **Output-Länge.** Christoph hat sich heute (2026-08-28) wiederholt in langen Outputs verirrt. **Merkregel:** eine Antwort pro Edit-Vorschlag, eine Zeile pro Bestätigung, keine Wiederholung dessen, was im Diff steht. Keine Frage-Runden, wenn keine echte Annahme kollidiert. Erklärungstexte unter 6 Zeilen, sofern nicht ausdrücklich verlangt.