Files
bf-infra/CLAUDE.md
Christoph Emsenhuber f63149a769 TP48 (Gitea #75) angelegt: Recovery-Luecke + Ansible-Abloese-Evaluation
Anlass aus TP10-Abschluss: DDNS-Updater lebt als /system script auf dem Core, in keinem
Template — Recovery von null haette ihn nicht. Systematische Klasse, nicht Einzelfund.

- Gitea #75: TP48 mit status:todo + thema:nautobot-doku angelegt
- docs/teilprojekte.md: Prosa-Abschnitt TP48 (Inventur + Evaluationsfragen)
- Live gemessener Kernbefund: Nautobot ist heute nur TEILWEISE SSoT — datengetrieben sind
  nur Device-Felder, interfaces/IPs und zwei ConfigContexts; 7 VLAN-Defs, 37 bridge-port-
  und 19 bridge-vlan-Zeilen, Core-Firewall, NAT/Mangle/Queue/Routen stehen LITERAL im Jinja.
  Der Abloese-Vergleich lautet damit 'Ansible vs. Jinja-im-Repo', nicht 'vs. Nautobot-SSoT'.
- Nebenbefund dokumentiert (nicht angefasst): WG-Hub-Privkey im Klartext im Recovery-Skript
  tp21-wan-wg-restore auf dem Core
- Korrektur: 'Abloesung durch Komodo' (CLAUDE.md + Archiv) war eine Kategorie-Verwechslung —
  Komodo verwaltet Container, nicht RouterOS-Config; es kann diese Luecke nicht schliessen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 19:05:02 +02:00

29 KiB
Raw Blame History

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), Firewall Least-Privilege (TP5 deny-by-default scharf seit 2026-08-18), Multi-Site VPN.

Pflichtlektüre pro Sitzung (Reihenfolge)

  1. Status/Index = Gitea-Board (seit TP36-Cutover 2026-09-07): admin/bf-infra · Issues — TP = Issue, Status = Label status:*, Gruppierung = thema:*. Detail/Kontext eines TP → ### TPnn in docs/teilprojekte.md (aktiv) bzw. docs/teilprojekte-archiv.md.
  2. docs/handoff-*.md (neueste) — wo die letzte Session aufgehört hat.
  3. Bei Live-Writes/Inventur: docs/cf-tunnel-mapping.md, docs/gotchas.md (ausführliche Diagnose-Fallen), docs/teilprojekte-archiv.md.

Aktueller Fokus: TP45 (IP-Schichten A/B/E, C4→TP34.2), TP34.2 (interne DNS-Namen als Daten, Gitea #73) und TP46 (CF-/Tunnel-SSoT) offen — Status im Gitea-Issue, Detail in den ### TPnn-Abschnitten von teilprojekte.md.

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. Archiv-Backfill-Werkzeug bin/gitea-tp-sync.py (Dry-Run default, Token aus ~/.hermes/.env, nie ausgeben).
  • 🔴 Die TP-Header in beiden Dateien tragen KEINEN Status mehr (seit 2026-10-03). Form: ### TP25 · Gitea #17 — <Titel> — <Caveat>. Der · Gitea #N-Zeiger ist die Brücke zum Issue. Wer einen Status sucht, liest das Label im Board — nicht den Datei-Kopf. Der alte ▸ ✅ FERTIG …-Anspruch war eine dritte Statusquelle und ist entfernt; die Header-Marker waren teils älter als die Issues. ⚠️ Status-Marker im Fließtext (in └─-Zeilen, Unterpunkten, Messchronik) bleiben erlaubt — sie beschreiben historische Ereignisse, keinen aktuellen Stand.
  • 🔴 bin/gitea-tp-sync.py schreibt nur noch NEUE Issues für Archiv-TPs, die im Board fehlen. Existierende Issues werden nie angefasst — kein Label-, kein State-Patch. Die Vorgängerversion glich bestehende Issues gegen den Datei-Marker ab und zog so #24 (status:dropped → done) und #32 auf falsche Labels. Richtung Datei → Gitea ist nur fürs Anlegen zulässig, nie für Status.
  • 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 = aktiver Master mit Gitea-Remote: origin = https://gitea.emsenhuber.net/admin/bf-infra.git. Das Remote ist nur so viel wert wie der letzte Push — Backup ist eine Disziplinfrage: git 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. 🔴 Vor jeder Aussage über Remotes/Backups git remote -v live prüfen (eine Session hat „kein Remote" behauptet, statt zu tippen).
  • admin/bf-network-r6 (Gitea) ist Archiv → read-only-Header.
  • admin/bf-golden-config (Gitea) ist Bot-Master, nicht manuell. 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 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 (s. docs/cf-tunnel-mapping.md §17). ⚠️ Grenze: blaueblüten ist ein eigenes Projekt — nicht anfassen.
  • 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. 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. Der MCP-Host liegt auf der DS923 (nas-bf, Portainer Endpoint 14). gitea-mcp läuft noch auf dem macmini (historisch) — gehört bei Gelegenheit auf die DS923 umgezogen. 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).


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
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 · TP15 ✅: ersetzt sw2 (L2-Switch), 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 · PRODUKTIVER L3-Router — Routing/DHCP (7 Server + 54 Leases)/DNS (DoH zum CF-Gateway mit verify-doh-cert=yes, TP23 ✅)/NTP-Server/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 · 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 · MAC D0:EA:11:4D:40:E0 · Config 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 · 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 · cAPGi-5HaxD2HaxD, RouterOS 7.24.1 + RouterBOOT 7.24.1 · reiner CAP, Manager crs326-core · Technik OG, Uplink ether1 → branch ether24 (Trunk) · Kanäle 5180/ax (Band A, non-DFS) + 2437 (ch6) · 🔴 nur 2 Ethernet-Ports → ap.extra_ports=[] · device-mode=basic (scheduler/fetch/romon erlaubt) · Dateien routeros/tp28-ap3-phase{1,2}-*.rsc
🔑 Einheitliches Admin-PW alle 4 MikroTik (core/branch/ap1/ap2) user admin, gleiches PW (~/.secrets/credentials.env → ROUTER_*_PASS/CRS326_*_PASS; „Überlebensversicherung", User-Vorgabe).
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 ✅: PG16, DB nautobot, Creds postgres/.secrets
Nautobot (Portainer-Stack, NAS) 10.40.20.154 Portainer ep14, Stack Id 203 · TP21 ✅: ghcr.io/nautobot/nautobot:3.1.7, DB=platform-pg, Creds docs/nas-vlan20/stacks/210-nautobot.yml (gitignored) · TP22 ✅: Golden-Config-App + Device-Adoption DURCH (alle 4 MikroTik, Image 3.1.7-gc9)
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) · mcp-pve v0.6.1 (TP14.2a) · ~/.claude.json ✓
Proxmox (pve) 10.40.20.100 ssh root@10.40.20.100 (SSH-Key ✓) · TP14: Host-Mgmt VLAN1→VLAN20 (vmbr0.20 + Default-GW-Flip)
Docker-LXC (auf pve) 10.40.20.126 Portainer CE 2.40.0 (https://10.40.20.126:9443) · TP14.2: LXC 400 net0 tag=20/.20.126 (iface heißt 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 · n8n-Host (siehe oben)
Arris-Kabelmodem (Bridge-Mode, keine Mgmt-IP im BF-LAN) Liwest-Edge · speist crs326-core vlan100 (VLAN100-Transfer, WAN-IP 90.146.33.177) · FritzBox 6660 seit 2026-07-01 ausgebaut (Liwest-Tausch, kein Zurück); Telefonie nicht weitergeführt

Ausgemusterte Geräte (ALLNET sw1, Omada sw2/Controller/ES208G, omada-mcp): Historie in TP15/TP2.1-Archiv + switch/README.md.


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 über MikroMCP — 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 (ps/argv-Trace; so ist am 2026-08-22 das Core-PW in ein Transkript geraten). -e hält es in der Umgebung des Kindprozesses. Neue Skripte erben die Regel nicht von selbst: env=dict(os.environ, SSHPASS=pw) einbauen. bridge/vlan/set nur auf [find … and !dynamic]. 🔴 list_wifi_interfaces gibt security.passphrase ALLER Profile im Klartext zurück (2026-08-24: fünf WLAN-PSKs ins Transkript). Für Kanäle /interface wifi channel print detail, für die CAP-Zuordnung … print where master=yes mit Feldauswahl. Der Scan (/interface wifi scan, nur am Manager) ist unbedenklich. Merkregel: vor jedem Read fragen, was das Tool AUSSER dem Gesuchten noch ausgibt.
  • 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 — ViewSet 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). rc=0 trotz failed to open … for reading; ein &&/|| dahinter beweist nichts, und validate/reload prüft dann die alte Datei → grüne Abnahme bei unverändertem Zustand (so sah der Nautobot-Umzug „gelandet" aus, live stand noch die alte Backend-IP). Regel: Push-Werkzeug wird geprüft, nicht geglaubt — nach jedem pct push den Inhalt vergleichen (md5sum diesseits/jenseits in einem Remote-Aufruf). Muster: push_verified() in caddy/deploy.sh. 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 für die Deploy-Pipeline läuft über src-address-list=nautobot (Regel *28), die Liste stammt aber aus dem handgelaufenen routeros/tp5-etappe2-whitelists.rsc — steht in keinem Template. 🔴 Ein GC-Recovery von null erzeugt einen Core, in den Nautobot nicht mehr per SSH kommt — die Pipeline wäre tot. 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. Systematisch erfasst in TP48 (Gitea #75), der auch prüft, ob Ansible Nautobot als SSoT ablösen kann. ⚠️ Nicht Komodo — es verwaltet Container, nicht RouterOS-Config (frühere Notiz hier war eine Kategorie-Verwechslung).

Kritische Gotchas

  • 🔥 Die Firewall ist seit 2026-08-18 deny-by-default — in BEIDEN Ketten. Ein neuer Dienst über eine VLAN-Grenze braucht eine explizite Regel, sonst funktioniert er nicht. Diagnosewerkzeug = Log-Regel direkt vor dem Drop (TP5-WOULD-DROP / TP5-INPUT-WOULD-DROP, Kollektor 10.40.20.18:514, Dateien Docker-LXC /opt/tp5-syslog/log/). Erst dort nachsehen, bevor irgendwo anders gesucht wird. MAIN→INFRA/MEDIA/MGMT/IOT offen (IOT seit TP5 E5, 2026-08-19); IOT→MAIN bewusst zu. Ad-hoc-Zugriff aus MAIN in geschlossenes VLAN (UNTRUSTED, GUEST): /ip firewall address-list add list=jit-admin address=<deine-IP> timeout=1h.
    • 🖨️ Drucker fällt mit udp/161 (SNMP) auf, nicht 631 — macOS fragt zuerst SNMP, bricht dann ab. Bei „Gerät antwortet nicht" das Log lesen, statt den erwarteten Port zu raten.
  • Beide APs = CAPs, Manager ist crs326-core (TP28 ✅). Mesh = NEIN (beide APs haben wired Trunk-Uplink → Loop-Risiko). Der Core braucht dafür kein wifi-qcom (capsman steckt im Basispaket).
    • 🔴🔴 /interface wifi cap slaves-datapath MUSS auf beiden APs gesetzt sein (dp-caps-slaves, bridge=bridge, ohne vlan-id). Fehlt es, assoziieren Clients auf -iot/-media/-untrusted/-guest sauber und bekommen nie DHCP, während dirtyvader-main weiterläuft (so lagen IOT+MEDIA am 2026-08-18 ~14 h). Abnahme für jeden WLAN-Umbau = frische DHCP-Lease pro SSID — NICHT die Registration-Table.
    • 🔴 /interface wifi scan geht am CAP NICHT — nur am Manager. cap-wifiN-Zuordnung vorher über radio-mac bestimmen, sie ist nicht stabil.
    • 🔴 CAPs finden den Manager per L2-Discovery (discovery-interfaces=vlan99, current-caps-man-address = MAC). Deshalb MNDP (udp/5678) + CAPWAP (udp/5246-5247) in der input-Kette des Core offen lassen.
    • 🔁 Slave-VAPs ändern = zweimal arbeiten: nach slave-configurations-Änderung /interface wifi capsman remote-cap provision [find where identity="apX"] nachziehen (beide Richtungen). Abnahme nicht am Kommentar (comment~"ssid"), sondern datapath.vlan-id bzw. Interface-Zahl am CAP.
    • 🔎 „connect ok, kein DHCP": zuerst torch auf dem VLAN am Core UND den TP5-Drop-Log lesen. Beide leer ⇒ nichts wird verworfen, es kommt nichts an → L2 (Bridge-Mitgliedschaft/Tagging), nicht Regelwerk.
  • 📝 WLAN-Log-Topic heißt wireless, nicht wifi. Zwei Regeln auf dem Core (nicht den CAPs) schicken wireless + caps zum Kollektor 10.40.20.18:514. ⚠️ RouterOS UND-verknüpft Topics in EINER Regel — topics=wireless,caps matcht nichts, es braucht zwei Regeln. /system logging steht unter der ComplianceRule system-logging. Merkregel: bevor man ein sporadisches Problem sucht, erst prüfen, ob es überhaupt mitgeschrieben wird.
  • 🧮 Log-Burst über VIELE MACs = Infrastruktur, über EINE MAC = Client. Vor jeder Client-Diagnose erst grep " caps," + /system resource get uptime lesen (ein Massenburst war oft ein geplanter Reboot). Umkehrschluss: 93/93 reauthenticating von einer MAC auf einer SSID = echter Client-/SSID-Befund.
  • 🗺️ Die cap-wifiN-Karte verschiebt sich bei JEDEM CAP-Rejoin. Vor jeder Auswertung /interface wifi radio print terse proplist=cap,radio-mac,interface am Manager lesen; liegt zwischen Ereignis und Auswertung ein Rejoin, Zuordnung als unbekannt behandeln statt raten.
  • 🔑 CAPsMAN verteilt KEINE Login-Passwörter — nur WLAN-PSKs. WPA-Passphrase (/interface wifi security) liegt am Manager ✅; Geräte-Login (/user) ist eine lokale DB je Gerät. Das einheitliche Admin-PW ist eine Hand-Konvention, keine Automatik.
  • 🔴 Ein neuer CAP braucht configuration.manager=capsman auf JEDEM Master-Radio. /interface wifi cap set enabled=yes allein reicht nicht — im Werkszustand ist das Feld leer (= local).
  • 🔌 Werks-MikroTik am Trunk: ether1 ist WAN. Interface-Liste WAN → Discovery aus (kein MNDP), Input-TCP (22/80/8291/8728) zu, ICMP erlaubt → es pingt, wirkt erreichbar. Weg rein ist die LAN-Seite (ether2), dort /interface list member remove [find where interface=ether1].
  • 🔀 pvid ist INGRESS, tagged/untagged ist EGRESS. Werksgerät-Port gehört untagged in die VLAN-Zeile.
  • 🕳️ Ein neues Gerät ohne secrets_group erzeugt eine LÜCKE, keine Fehlermeldung. Backup-Job scheitert still, keine Compliance-Einträge. 🔴 Eine gleichbleibende Compliance-Zahl nach dem Hinzufügen eines Geräts ist ein BEFUND, kein Erfolg. Zweite Voraussetzung: dedizierte Service-Accounts nautobot/ha existieren auf einem Werksgerät nicht.
  • 🔢 Firewall-Regelnummern aus print where sind Handles, keine Positionen. Beweis ist der Zähler (print stats), nicht die Zahl.
  • 🔌 crs326-core hat KEIN PoE-out (CRS326-24G-2S+). AP am Core-Port braucht eigenes Netzteil; der cAP ax bringt Netzteil + PoE-Injector im Karton mit.
  • 🧩 Die AP-Flotte ist heterogen (cAP ax = 2 Ports vs hAP = 5). Bridge-Ports kommen aus ap.extra_ports (ConfigContext ap-common; ap3 überschreibt per local_config_context_data mit []). ⚠️ Nautobot merged Dicts tief, Listen ERSETZEND. Bevor ein zweites Modell derselben Rolle dazukommt, Literal-Blöcke lesen („Gerätetyp" vs „Rolle").
  • 🔵 Container auf MikroTik: ap1/ap2 = arm64, 95 MB Flash frei → ein kleiner Container ok (~70 MB nutzbar). crs326-core dagegen NIE (arm 32-bit, 2,9 MB frei) — SPOF, kein Anwendungs-Host (Hardware-Tatsache). cloudflared braucht kein TUN (Userspace-Proxy) → RouterOS-Container möglich; der WARP-Connector nicht (braucht TUN). Schwere Container laufen weiterhin auf NAS/pve, nicht am Router.
  • hAP device-mode=home (ap1/ap2): scheduler + fetch blockiert → AP-Provisionierung idempotent ohne Scheduler. crs326-core ist KEIN device-mode=home → dort gehen scheduler/fetch (DDNS-Updater).
  • Erstzugriff Werks-hAP am CRS-Fabric: Uplink ether1 = Werks-WAN → 192.168.88.1 (LAN) darüber NICHT erreichbar. Weg: LAN-Port (ether2) patchen ODER Core-Temp-Pfad 192.168.88.2/24 + srcnat masq 192.168.88.0/24, dann scp+/import (danach Temp-Pfad + stale ARP .88.1 entfernen).
  • 🖨️ ZWEI Drucker, nicht verwechseln: Brother MFC = eigentlicher Drucker, VLAN10 (10.40.10.6, MAC 30:05:5C:78:86:20, statisch). P750W = Etikettendrucker, VLAN30 (10.40.30.5), läuft auf dirtyvader-iot. P750W-Coverage SSoT docs/wifi-p750w-coverage-and-map.md. ⚠️ Power-Save ist KEIN Thema (frühere „DHCP-geht-aber-Ping-nicht=ARP/Power-Save"-Merkregel war Fehldiagnose).
  • Ping auf JEDE lokale IP tot, aber TCP geht überall = WARP-Client lokal noch aktiv. cloudflared/WARP ist QUIC-Proxy, kann kein ICMP. Fix: WARP trennen, kein Router-Eingriff. Memory ref_warp_local_icmp_trap.
  • L3-Router = crs326-core (TP21.1 ✅, 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 ✅). Beide Firewall-Ketten enden mit drop; Mgmt-Input nur aus VLAN10, von HA (8728), von Nautobot (22) und über cf-mesh. ap1/ap2 sind NUR noch APs.
  • ✅✅ Edge-Cutover: FritzBox aus, Arris Bridge-Mode. WAN terminiert auf crs326-core vlan100, öffentliche Liwest-IP 90.146.33.177, kein Double-NAT. core = Public-Edge + WG-Hub. Runbook routeros/wan-cutover-runbook.md.
  • mgmt-VLAN-Wechsel am Switch = Lockout-Risiko: VLAN muss existieren + Port muss es tragen, in einem Schritt.

Diagnose-Fallen (Detail in docs/gotchas.md, hier nur die Merkregel):

  • ⏰ Zeitstempel zwischen Geräten widersprüchlich → /system ntp client get status (Zeitquelle = Core, DHCP-Option 42, ComplianceRule system-ntp).
  • 🔌 Vor jedem geplanten LXC-Neustart systemctl --failed lesen + danach Ports gegen vorher vergleichen.
  • 🧪 „alles gleichzeitig kaputt" → zuerst das eigene Werkzeug verdächtigen (nc -z lügt), dann das Netz.
  • 📡 Sonos sucht hunderte Male ohne Verbindung → Relay, nicht Netz (Richtungsbilanz grep -c "via eth1" vs "via eth0").
  • ⏱️ Konstanter 10-s-Hänger = DNS-Timeout, nicht Last (DB-Reverse-Lookup auf toten Resolver).
  • 🏠 HA-http:-Block in configuration.yaml wirkungslos → .storage/http + WS http/config/configure→promote.
  • 🐳 Nautobot auf DS923: Image-Healthcheck aus; cpus: geht auf Synology nicht.

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. Jede AI-Session ist zugleich Oberhand über Container und Garbage-Collector auf Infra-Ebene.
  • MCPs brauchen Host-Verifikation vor Edit: per Live-Read (nc/ps/curl) prüfen, ob der Host noch antwortet. Beispiel: shelly-mcp zeigte auf 10.40.20.30:3030, kein Host antwortet → enabled=false, dokumentieren. (Stand 2026-08-28: shelly-mcp auf DS923 10.40.20.180 deployt.)
  • Tote MCP-Skripte → ins Projekt-Repo, nicht global. Projekt-spezifische MCPs (cannaleo, terpene-pg) → ~/dev/<projekt>/mcp.json + Skript im Projekt-Repo. Globale ~/.claude.json/opencode.jsonc = nur projekt-übergreifende Tools.
  • opencode-Skills: Default-Pfad kopieren, NICHT symlinken (TP35 Schicht 6). Claude-Skill ~/.claude/skills/<name>/SKILL.md ist die Quelle; für opencode cp nach ~/.config/opencode/skills/<name>/SKILL.md (kein skills.paths-Eintrag nötig). Symlink verletzt Christophs „opencode = eigene Pfade"-Direktive. Lehre: opencode-Verhalten nicht raten, Doku lesen.
  • Skills sind IMMER global, niemals pro Projekt (Christoph-Direktive). Nur ~/.claude/skills/ und ~/.config/opencode/skills/. Kein Repo-Override. Wer überschreiben will, editiert die globale Datei oder schreibt einen zweiten Skill.
  • Skills sind seit 2026-09-25 versioniert (Befund: sie waren reiner Home-Ordner-State, kein Git, kein Backup — bei 22 Skills über mehrere Monate gewachsen). Master = Gitea admin/claude-skills (privat), ~/.claude/skills/ ist dessen Arbeitsbaum. Ablauf ab jetzt: editieren → committen → pushen → cp -R ~/.claude/skills/<name> ~/.config/opencode/skills/<name>. ⚠️ Der cp-Schritt ist der, den man vergisst — genau so entsteht stille Drift zwischen den beiden Kopien. Secret-Gebot gilt auch hier: keine Tokens/Keys in Skills, Platzhalter (REPLACE_WITH_…) sind okay.
  • Kommentar in Code ≠ Verhalten. mcp/shelly/.env trug „gitignored", es gab aber kein .gitignore. Behauptungen in Kommentaren per git check-ignore -v <datei> verifizieren, bevor man darauf vertraut.
  • opencode-TUI: führendes Leerzeichen vor /command manchmal nötig (bekannter Bug, anomalyco/opencode#27528, „Not planned"). Workaround (führendes Leerzeichen) ist der offizielle Pfad.
  • Manuell gepflegte Doku verliert gegen die Realität, wenn sie lang wird. Wer eine Stelle ändert, muss die anderen mitlesen (Quick-Index, TP-Abschnitt, Ist-Stand). Strukturelle Antwort = TP36 (SSoT via Nautobot).
  • Maskierungs-Muster: auf den SCHLÜSSELNAMEN ankern, nie auf ein Trennzeichen. Sicher ist nur sed -E 's/(TOKEN|PASSWORD|SECRET|KEY|PASS)=.*/\1=<redacted>/' — greedy sed 's/=.*/…/' schneidet Slashes/Backslashes nicht. Werte nie suchen, um sie zu sehen — den Schlüssel per sed -n 's/^…KEY=//p' direkt in eine Variable holen. Drei Leaks dieser Klasse: PSK 2026-08-24, SHELLY_DEVICE_PW 2026-08-28, Nautobot-Tokens 2026-08-31.
  • Vor jeder Drift-Bewertung python3 nautobot/gc-transaktion.py status lesen (Befund 2026-08-31, zweimal gestolpert). Drift aus einem laufenden Versuch sieht identisch aus wie ein Versehen — die Compliance-Zahl unterscheidet nicht. 🔴 Eine solche Drift grün zu machen macht den Versuch unsichtbar. Was dauerhaft gelten soll, gehört ins Soll/Template, nicht in eine Transaktion. ⚠️ Das Skript findet den Token aus einem Worktree heraus nicht (210-nautobot.yml nur im Haupt-Checkout).
  • 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 69,5 MiB. 🔴 docker save schreibt seit Docker 25 OCI statt docker-v1 → RouterOS meldet no config found in manifest. 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 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.