Files
bf-infra/docs/teilprojekte.md
Christoph Emsenhuber 72db50fce2 TP31 abgeschlossen: Schritt 5+6 via TP16.3 (09-01), cf-sync.py-Fehlalarm gefixt
Kern von TP31 (Caddy als Host->Origin-SoT, cloudflared 6->1 Knoten) lief
über den Piloten TP16.3 durch: bf-public 12 conns, vier öffentliche Namen
über Caddy (https://10.40.20.40:443), Alttunnel bf-nas/bf-pve/bf-mm weg.

cf-sync.py:107 akzeptierte nur http://<caddy>; der Flip legte die Namen auf
https://...:443 -> Abnahme-Tool meldete vier Fehlalarme. Fix: auch https://
als "zeigt auf Caddy". Drift-Check jetzt grün (nur ds-bf als bewusst intern).

Schritt-6-Teil "MCP-Clients auf FQDN" durch TP35 Schicht 1 umgekehrt (LAN-IP),
damit gegenstandslos. Quick-Index/TP-Abschnitt/Ist-Stand-Block nachgezogen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-02 03:07:15 +02:00

306 KiB
Raw Blame History

BF-Netzwerk — Teilprojekte (kanonische Steuerung, Stand 2026-06-10)

Zweck: Das große Netz-Projekt in einzeln durchziehbare Teilprojekte (TP) zerlegt — jedes mit klarem Scope, aktuellen Entscheidungen, Abhängigkeiten und Status. Jedes TP ist so geschnitten, dass es eine frische Session mit sauberem Kontext straight abarbeiten kann. Architektur/Historie: ~/.claude/plans/ich-will-das-delegieren-atomic-sunrise.md.

📚 Diese Datei führt den Ist-Stand, den kritischen Pfad und die OFFENEN Teilprojekte. Abgeschlossene TPs stehen im Archiv — im Volltext, unverändert, mit Inhaltsverzeichnis. Für den Einstieg reicht diese Datei; ins Archiv sieht man, wenn man wissen will, warum etwas so entschieden wurde. (Aufgeteilt am 2026-08-23, Doku-Session Punkt F: 2217 Zeilen waren zu 49 % Historie erledigter TPs.)

Kanonisches VLAN-Modell (gültig — überschreibt ältere Tabellen)

VLAN Name Subnetz (Ziel) Zweck
10 MAIN 10.40.10.0/24 Trusted (Handy/Laptop/Apple Watch)
20 INFRA 10.40.20.0/24 NAS/VMs/Docker/HA
30 IOT 10.40.30.0/24 Smart-Home (HA-managed), Waage, Fitness-Tracker
40 MEDIA 10.40.40.0/24 Sonos, AppleTV, TV, VuPlus, Cast-Ziele (getauscht ggü. früher)
50 UNTRUSTED 10.40.50.0/24 Spielekonsole, eBook-Reader, Low-Trust-Internet
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, Bridge-Mode, Public-IP an crs326-core)
IP-Blöcke: .1–10 Router/Gateways · .11–20 Switches/APs. GW je VLAN = .1 (crs326-core).
Mgmt (VLAN99, Ist 2026-08-16): core .99.1 · branch .99.2 · ap1 .99.11 · ap2 .99.12.
(Historisch, überholt: „ap1=.1, ap2=.2 · ALLNET=.11, Omada=.12" — aus der Zeit vor dem Core-Cutover.)

Ist-Stand (Kurz)

Stand 2026-08-31 — live gegen die Geräte geprüft (MikroMCP/REST/SSH), nicht aus der Doku abgeschrieben. Compliance am 2026-08-31 nach vollem Refresh (Backup→Intended→Compliance) gelesen: 128/135. (Die Fassung vom 2026-08-16 stand bis zum 18. hier und war an vier Stellen überholt. Wer diesen Block liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)

  • ⏸️ TP41 CF-Access-Reparatur — ZURÜCKGESTELLT 2026-09-01 (User-Entscheid; Rest wartet auf Info): Session 720h, login_method-Loch zu (bf-users email-only), zt--Gruppen, vw voll Bypass + vw/admin per WAF geblockt (Mobilfunk-verifiziert), API-Token auf vollständiges …4148 konsolidiert. T4 ✅ (ein Access-Service-Token zt-svc, Terpene-Worker per wrangler auf zt-svc umgestellt, Alt-Tokens gelöscht). T3 ✅ (WARP Default-Profil fallback_domains emsenhuber.net → .36/.37). T2 ✅ (WAF-Regel gelöscht, Caddy-Origin-404 für /admin via CF-Header; Edge=404, lokal=200). T5 ✅ (GitHub- und Google-Login laufen, User-bestätigt 2026-09-01; Google via Consent-Fix §A). Facebook/Meta für TP41 abgesagt (optional, kein CF-IdP angelegt) — Geli über OTP angelikahy@outlook.de + Google pillepalle670@gmail.com voll gedeckt. Token-Altlast geklärt: b00722=live Caddy-DNS-Token, d47435=stale Kopie → ✅ d47435 revoked, ✅ T3-WARP-Client-Verify (User). Geli CF-seitig fertig (Enrollment via zt-humans + geli-Policy, 2026-09-01 live geprüft) → verbleibt nur ein Device-Handgriff bei Geli (WARP-Client, Team emsenhuber). Claude-/CF-Seite damit vollständig. Handoff: docs/handoff-20260901-tp41.md · Follow-up.

  • ✅ TP14.4 als in TP41 T4 aufgegangen markiert (2026-09-02): kein eigenständiger claude-mcp-Token mehr — er wurde in TP41 T4 zu zt-svc konsolidiert, mcp/cloudflare/.env trägt jetzt CF_ACCESS_CLIENT_ID/_SECRET. Die TP14.4-Ist-Stand-Notiz („.env leer") war seit 09-01 stale; Quick-Index + TP-Abschnitt nachgezogen.

  • ✅ TP31 abgeschlossen markiert (2026-09-02): Schritt 5+6 sind über den Piloten TP16.3 (09-01) miterledigt — bf-public 12 conns, vier öffentliche Namen über Caddy, Alttunnel weg. Abnahme-Tool caddy/cf-sync.py von einem Fehlalarm befreit (prüfte nur http://, gebaut ist https://…:443) und grün (nur ds-bf als bewusst intern). Schritt-6-Teil „MCP auf FQDN" durch TP35 Schicht 1 umgekehrt (LAN-IP), damit gegenstandslos.

  • 🔜 TP42 Namens-Vereinheitlichung (2026-09-01, angelegt): internes .bf.lan (Core-DNS, DHCP-Search- Domain) und die .bf.emsenhuber.net-Aliase (Caddyfile) auf einen Namensraum emsenhuber.net ziehen; stale .16/.17-Rewrites abbauen; Sollzustand in Nautobot. Handoff: docs/handoff-20260901-tp42-naming.md.

  • 🔜 TP43 dw-Standort-DNS-Uplink (2026-09-01, angelegt): dem dw-Standort einen eigenen Resolver/ DNS-Uplink mit site-spezifischen Overrides geben (erster Fall 420.teilzeitladen.xyz → BF-Caddy über WG), inkl. Verhalten bei WG-Disconnect (Override auf BF-interne IP fällt sonst aus). FritzBox standard kann keinen Per-Host-Override. Geli-Sofortlösung = WARP (nicht Teil von TP43). Cross-Projekt.

  • ✅ TP16.3 ABGESCHLOSSEN (2026-09-01): Flip und Abbau durch. Öffentlich laufen nur noch bf-public (12 Connectoren) und fm — die drei Alttunnel bf-mm/bf-nas/bf-pve und der projektfremde terpene-db sind gelöscht, 7 cloudflared-Container entfernt (inkl. terpene-cloudflared, weil 420.teilzeitladen.xyz nun über Caddy/CF läuft — vor der Löschung mit echtem Origin-Inhalt belegt). §6 wurde auf User-Entscheid vorgezogen. TP wandert ins Archiv.

  • 🆕 TP16.3 §5 DURCH (2026-09-01): n8n/gitea/hass/vw laufen über bf-public — der öffentliche Verkehr geht damit erstmals über Caddy statt an ihm vorbei, und die Host→Origin-Zuordnung lebt nur noch im Caddyfile. Je Name einzeln geflippt, dazwischen gemessen; Antwortzeiten vorher/nachher unverändert (Caddy im Pfad kostet nichts Messbares). Nachweise: gitea echte Gitea-Antwort · hass WebSocket 101 Switching Protocols (= der Companion-App-Pfad, den das Runbook als „nur vom User prüfbar" führte) · vw /alive + /api/config 200, WS-Hub 401 von Vaultwarden selbst · n8n von Christoph im Browser bestätigt. 🚫 §6 (Abbau) bewusst NICHT — die drei Alttunnel bleiben als Rückfallebene (healthy/8). 🔴 Christoph hat den Flip ausdrücklich vorgezogen freigegeben („aus Gründen"); die 7-Tage-Regel betrifft ohnehin §6, nicht den Flip.

  • 🆕 TP39 ABGESCHLOSSEN + archiviert (2026-09-01): letzter offener Punkt — der Resolver-Sweep — ist durch: pve alle 13 LXCs auf 10.40.20.1, und die DS923-Container via Portainer (ep14) gegengeprüft — kein .41.254-Override, alle laufenden lösen in 66–110 ms auf (inkl. Grenzfall gitea, erstellt 2026-07-10), kein eingefrorener Dead-Resolver, kein Recreate nötig. Die Abnahme-Regel („vor jedem LXC-Reboot systemctl --failed + Port-Vergleich") steht als CLAUDE.md-Gotcha, der Sweep ist als wiederkehrende Kontrolle dort verankert. Volltext → Archiv. 🔴 Rest offen nur in anderen TPs: root-DB-PW-Rotation (→ TP30), HA-Recorder-Retention (→ TP40).

  • 🆕 TP39 fast durch (2026-09-01): skip-name-resolve auf der MariaDB aktiviert (mysql.user vorher geprüft — kein Hostname-Grant, gefahrlos; Wirkung: gitea/vw/hass 0 von 36 frischen Anfragen langsam). LXC 302 war 100 % voll → +30G online (95 GB/69 %). 🔴 root-DB-Passwort dabei ins Transkript geraten (.my.cnf ohne =), zu rotieren (→ TP30).

  • 🆕 TP40 angelegt (2026-09-01, User-Okay): HA-Recorder-Retention — die homeassistant-DB frisst 59 GB (states.ibd 41 GB), unbegrenzt gewachsen. Das +30G aus TP39 ist ein Puffer; die dauerhafte Lösung (purge_keep_days, exclude, purge repack) ist HA-seitig und Christophs Policy-Entscheidung — kein eigenmächtiges Löschen.

  • 🆕 TP39: networking.service in vier Containern repariert (2026-09-01). Ursache war dieselbe Altlast wie beim Resolver: tote auto-Stanzas in /etc/network/interfaces aus dem VLAN1-Schema (eth0 → 10.40.41.x) und alten Versuchen — ifup -a scheiterte an Interfaces, die es nicht gibt, während veth20 lief. Abnahme war ein echter Reboot: influxdb kam danach von selbst hoch (vorher gar kein Startversuch). Alle 13 LXCs stehen jetzt auf dns=10.40.20.1, networking überall sauber. Offen in TP39: nur noch skip-name-resolve und der wiederkehrende Sweep.

  • 🆕 TP39 angelegt (2026-09-01, User-Okay): LXC-Hygiene auf pve — die zwei Restpunkte aus dem Wartungsfenster unten (skip-name-resolve auf der MariaDB, kaputtes networking.service in vier Containern) plus Resolver-Sweep als wiederkehrende Kontrolle. Ein TP, weil beides dieselbe Ecke betrifft und in einem Fenster erledigt wird.

  • ✅ GELÖST 2026-09-01: der 10-Sekunden-Zoll auf gitea/vw/hass — es war Reverse-DNS auf einen toten Resolver. MariaDB (LXC 302, 10.40.20.23) hatte skip-name-resolve auskommentiert und machte damit bei jeder neuen Verbindung einen Reverse-Lookup; ihr /etc/resolv.conf zeigte auf 10.40.41.254 — den toten VLAN1-Resolver aus dem alten Schema (nc bestätigt tot). Gemessen im Container: 10.010 ms je Lookup. Bestehende Pool-Verbindungen waren schnell, neue kosteten 10 s — daher wirkte es sporadisch und unerklärlich. 🔑 Die Blast-Radius war größer als gedacht: an derselben DB hängen Vaultwarden (.127), Home Assistant (.105) und gitea (.157) — belegt über die Verbindungstabelle, nicht vermutet. (Die erste Zuschreibung „vaultwarden nutzt SQLite, also nicht betroffen" war eine Annahme und falsch — Christoph hat sie korrigiert, die Verbindungsliste hat es bewiesen.) Fix (Wartungsfenster, User-Freigabe): pct set <id> --nameserver 10.40.20.1 + Reboot für fünf LXCs — 302 mariadb, 102 mqtt5, 106 influxdb2, 107 redis, 150 iob-exposed. Nachher: Lookup 21–24 ms statt 10.010 ms; gitea 0 von 25 Proben langsam (vorher ~10 % bei 10,1 s), langsamste Probe 75 ms. Alle drei Dienste wieder verbunden. ⚠️ Strukturell offen: skip-name-resolve ist weiterhin aus — der Lookup ist jetzt nur schnell, nicht abgeschafft. Ihn zu setzen bricht alle Grants, die auf Hostnamen statt IPs lauten; das gehört vorher gegen mysql.user geprüft (DB-Zugang fehlt: root@localhost ohne Passwort verweigert).

  • 🔴 Latenter Fund aus demselben Wartungsfenster: networking.service und ifupdown-wait-online.service sind in 101, 102, 106, 107 fehlgeschlagen. Folge: Dienste mit After=network-online.target starten nach einem Reboot nicht von selbst — influxdb2 kam nach dem Neustart nicht hoch, Unit enabled, aber kein einziger Startversuch im Journal. Von Hand gestartet lief es sofort. 🔑 Nicht durch diese Session verursacht: 101 iob-master wurde nie angefasst und zeigt dieselben fehlgeschlagenen Units — die Container liefen nur 13+ Tage ohne Neustart, deshalb fiel es niemandem auf. Merkregel: diese vier Container sind einen Reboot davon entfernt, still Dienste zu verlieren.

  • 🔴 Befund am Rand des Flips, NICHT von ihm verursacht: gitea hängt sporadisch exakt ~10,2 s. ~10 % der Anfragen, quer über Web-UI, /api/healthz und den Git-Pfad (info/refs) — also kein Rendering-Problem. vw und hass über dieselbe Strecke: 0 von 30. Damit sind CF-Edge, AP-Connector, Firewall und Caddy entlastet; übrig bleibt Caddy → 10.40.20.157:3000 oder gitea selbst. Immer ~10,15–10,43 s = Timeout-Signatur (klassisch: ein toter Resolver, s. den VLAN20-Docker-DNS-Gotcha in der CLAUDE.md), kein Lastproblem. Weiter nur mit LAN-Zugriff.

  • 📎 Drei Messfallen, die diese Session gekostet haben (alle drei sind Standort-, keine Netzfehler):

    1. Split-Horizon macht Tests von zuhause wertlos: der Haus-Resolver liefert für alle vier Namen direkt 10.40.20.40, die Anfrage sieht Cloudflare nie. Prüfen geht nur mit festgenagelter CF-Edge-IP (curl --resolve <name>:443:188.114.96.10) oder von außerhalb.
    2. Access antwortet je nach Standpunkt verschieden: n8n gibt aus dem Haus 200 (die bf4-Policy trägt die WAN-IP), von außen 302 zum Login. Beides gesund. Für die drei bypass-Namen (hass/vw/gitea) gibt es gar keinen Login — die Access-App heisst zu Recht hass, vw, gitea, sie führt alle drei in self_hosted_domains (das Feld domain zeigt nur den ersten). Ein Rename wäre eine Verschlechterung gewesen.
    3. 🔴 „Der Host ist ausgefallen" war der eigene Standortwechsel. Ping/SSH/Caddy waren tot, weil die Session-Maschine auf einen Hotspot gewandert war (Default-GW 172.20.10.1) — dieselbe Klasse wie die WARP-Falle von 2026-08-13, nur ohne WARP. Es wurde daraufhin ein bereits erfolgreicher n8n-Flip grundlos zurückgerollt. Merkregel: bevor ein Ausfall gemeldet wird, route -n get default lesen — ein Standortwechsel sieht exakt aus wie ein Totalausfall.
  • ⚠️ Nebenbefund: Caddy führt gar kein Access-Log. Im Caddyfile steht keine log-Direktive, im Journal landen nur info/warn. Der Abnahmepunkt „Caddy sieht den Request" (Runbook §5.3) kann nicht funktionieren — ein erfolgreicher Request hinterlässt dort nichts, und leere Ausgabe liest sich wie „kam nie an". Dass der öffentliche Reverse Proxy keine Requests protokolliert, ist für sich eine Lücke (gehört zu TP31/TP34).

  • 🆕 TP16.3: die sechs AP-Drifts sind ins Soll gehoben (2026-08-31) — Compliance 139/140. veth / ip address / ip firewall nat der drei cloudflared-Connectoren stehen jetzt im AP-Template (Block [7]) und im ConfigContext ap-common (ap.container), nicht in einer gc-transaktion: ein Dauerzustand gehört ins Soll, eine Transaktion ist ein befristeter Versuch. Kein Re-Baseline, kein Rückbau, keine Zeile aufs Gerät — nur die SoT nachgezogen. 🆕 28. ComplianceRule interface-veth (vorher gab es für /interface veth keine ⇒ ein gelöschtes veth hätte nichts gemeldet). 🔴 Zwei Funde dabei: (a) der veth-Export trägt zwei gerätevergebene MACs — eine nackte Regel hätte Dauer-Drift auf allen drei APs erzeugt; gelöst mit zwei eng auf die veth-Zeile verankerten ConfigReplace-Regeln (60-/61-veth-*, nautobot/ingest-veth-rule.py), bewusst mit der vorhandenen Maske <<REDACTED>>, weil die im Repo zugleich die Push-Sperre ist. (b) Der erste Compliance-Lauf meldete SUCCESS und war trotzdem unvollständig (139 statt 140 Einträge, crs326-core/interface-veth fehlte; Job-Log: RENDER COMPLIANCE TASK GROUP failed: Broken pipe). Zweiter Lauf sauber. ⚠️ Merkregel: nach einem Refresh nicht nur die Konformitätsquote lesen, sondern die ANZAHL der Einträge — sie muss Regeln × Geräte sein. Eine fehlende Kombination sieht aus wie „die neue Regel greift nicht" und ist in Wahrheit ein halb gelaufener Job. (Übrig bleibt genau crs326-core/wifi-security = offene Transaktion TP25-fund-e-ft-over-ds.)

  • 🆕 MCP-Bestand 2026-08-28 korrigiert: 9 MCPs identisch in ~/.claude.json und ~/.config/opencode/opencode.jsonc (vorher Asymmetrie 7 vs. 11 — portainer-mcp/shelly-mcp raus, gitea-mcp/n8n-mcp rein). Mac mini = n8n-Host, NICHT zentraler MCP-Host (Performance-Restriktion); zentraler MCP-Host = DS923 (nas-bf, Portainer Endpoint 14). Inventur-Stand mcp/INVENTORY.md + Policy docs/mcp-host-policy.md.

  • 🆕 MCP-Migration FQDN→IP (2026-08-29, TP35 Schicht 1): gitea-mcp und n8n-mcp in ~/.config/opencode/opencode.jsonc von CF-FQDN auf LAN-IP umgestellt — gitea-mcp → http://10.40.20.167:3000/mcp (macmini, ohne Bearer, intern), n8n-mcp → http://10.40.20.167:3001/mcp (mit Bearer {env:N8N_MCP_BEARER}). Befund dabei: der Handoff 1.2 nannte für gitea-mcp die falsche IP .157:3000 (das ist die Gitea-Web-UI, 400 Invalid CSRF); der echte gitea-mcp läuft auf .167:3000 (serverInfo: gitea-mcp 0.0.10). cloudflare-MCP bleibt auf dem offiziellen FQDN (User-Direktive).

  • 🆕 TP16.3 §2/§3 durchgezogen (2026-08-31, Vor-Ort): je ein cloudflared-Container auf ap1/ap2/ap3, bf-public healthy mit 12 Connectoren — der Tunnel hat damit zum ersten Mal überhaupt Connectoren. 🔴 Zwei Runbook-Fehler dabei aufgedeckt: die Flash-Rechnung war um Faktor 2,5 zu optimistisch (entpacktes cloudflared = 69,5 MiB, nicht 28 MB ⇒ das offizielle Image passt auf keinen der drei APs; Ersatz ist ein selbst gebautes FROM scratch-Image mit 35,3 MiB, Binary-SHA e93824f6… gepinnt), und /container/envs heisst list=, nicht name=. 🔴 Offen: Compliance 128/135. §3.4 verlangt Direkt-Writes für ip address/nat, die unter GC-Kontrolle stehen ⇒ sechs neue extra-Drifts (zwei je AP), inhaltlich richtig, aber nicht im Soll. Braucht eine Entscheidung (ins Template heben), kein Re-Baseline. Details im TP16.3-Abschnitt.

  • 🆕 TP37 angelegt (2026-08-30, User-Okay): Nautobot soll durchgehende Netzwerkdoku werden, erster Anwendungsfall die CF-Connectoren. 🔴 Befund dabei, live gegen die API: Nautobot kennt heute nur die 8 Netzgeräte (4 aktiv, 4 decommissioned) — ipam.Service, CustomField, Relationship, VirtualMachine und Tag sind alle 0, installiert ist nur golden-config. Kein Host, kein Container, kein Dienst ist erfasst. TP37 ist damit zuerst eine Datenmodell-Frage und liefert das Fundament, das TP34 (Caddyfile via GC) und TP36 (SSoT für diese Datei) ebenfalls brauchen — Reihenfolge: TP37 vor TP34.

  • 🆕 CF-Public-Strang eingedampft (2026-08-30, TP16.3 grosser Schnitt): 12 Hostnames aus Ingress und DNS entfernt — pve/portainer/pve-mcp/ha-mcp/ds-bf/ccu/iobroker/z2m25/pbs plus die drei CF-Access-SSH-Ziele. Ingress: bf-mm 6→4, bf-nas 6→3, bf-pve 9→2. Öffentlich bleiben nur vw/hass/n8n/gitea (weiterhin HTTP 200). Intern unverändert über AdGuard→Caddy (9× HTTP 200). ✅ Nachtrag 2026-08-30 (F1 durch): gitea-mcp/n8n-mcp sind jetzt AUCH geschnitten — damit 14 Hostnames gesamt und bf-mm 4→2 (nur noch n8n + Catch-all). Möglich wurde das, weil zuvor 12 MCP-Client-Configs auf LAN-IP migriert wurden (nicht 10 wie geschätzt: zwei Worktree-Kopien in bf-network-r6{,.opencode} kamen dazu). 🔴 bf-public hat weiterhin 0 Connectoren — er gehört laut Entscheid auf ap1/ap2, dort fehlen aber container-Paket und device-mode container=yes (physische Bestätigung nötig). Bis dahin stehen bf-mm/bf-nas/bf-pve mit 6 Containern. Backups: docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json.

  • 🆕 TP16.3/TP31 Schritt 5 vorbereitet (2026-08-30): Runbook für die Vor-Ort-Session geschrieben (tp16.3-bf-public-ap-connector-runbook.md), CF-Rollback-Snapshots unter docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json neu angelegt (es gab sie vorher nicht). docs/tp31-schritt5-runbook.md ist damit abgelöst (read-only-Header): es kannte ap3 nicht, unterstellte einen Registry-Pull (fetch: no auf ap1/ap2) und enthielt eine falsche Firewall-Freigabe-Behauptung. CF-Stand: bf-public inactive / 0 Connectoren; die vier öffentlichen Namen laufen noch über bf-mm/bf-nas/bf-pve an Caddy vorbei direkt aufs Backend.

  • 🆕 TP38 angelegt (2026-08-31): OOB-Management — je ein untagged-VLAN99-Port auf beiden Switches. Auslöser: beim Core-Neustart war crs326-branch nicht erreichbar, weil sein Mgmt ausschließlich über den Core läuft (vlan99 dort tagged=ether1,ether24,bridge, kein untagged-Member). 🔴 Nebenbefund mit eigenem Gewicht: crs326-branch hat gar keine input-Firewallregeln — sein Schutz ist heute allein die Erreichbarkeit über den gehärteten Core. Das ist die Hauptauflage des TPs.

  • 🆕 ROS-Upgrade 7.24.1 auf allen fünf (2026-08-31, User). In Nautobot nachgezogen: ap1 stand als einziges Gerät noch auf 7.24 (die anderen vier waren schon richtig) ⇒ auf 7.24.1 gesetzt. ⚠️ Korrektur (2026-08-31, nachträglich): die dabei gesehene wifi-security-Drift auf crs326-core (sec-main ft-over-ds=no statt yes) hatte nichts mit dem Upgrade zu tun — sie ist TP25 Fund E, Hebel 1, um 08:09 bewusst gesetzt (Core-Upgrade war erst 14:36) und als offene Transaktion TP25-fund-e-ft-over-ds geführt (Scope wifi-security, Ablauf 2026-09-03). Die 134/135 sind Absicht, nicht Schaden: grün machen würde den laufenden Versuch unsichtbar machen, und gc-transaktion.py verweigert den Commit ohnehin, solange der Scope driftet. 🔑 Lehre: vor jeder Drift-Bewertung nautobot/gc-transaktion.py status lesen — Drift aus einem laufenden Versuch sieht identisch aus wie ein Versehen, und genau dafür gibt es das Werkzeug.

  • 🆕 TP16.3 §1-Blocker remote erledigt (2026-08-31): Ingress von bf-public auf https://10.40.20.40:443 + originServerName; Firewall-Freigabe vlan99 → vlan20:443 über Nautobot GC (Plan Completed, Re-Baseline ingest-core-firewall.py --apply, danach 135/135 compliant / 0 Drifts); ROUTER_AP3_PASS in ~/.secrets. Offen bleibt nur die Vor-Ort-Session (§2/§3: container.npk, device-mode, Container) und danach Flip + Abbau.

  • 🆕 Globale Verhaltensregeln konsolidiert (2026-08-29, TP35 Schicht 3): ~/.config/opencode/AGENTS.md ist jetzt die SSoT für globale Verhaltensregeln (Secrets, VLAN-Schema, 5 Lessons, SoC, Übergabe-Mechanik, Mid-Action-Checkpoint, Commit-Hinweis, Backup-Guard). ~/.claude/CLAUDE.md hält denselben Block als Claude-Code-Spiegelung mit Verweis auf die SSoT. opencode liest AGENTS.md mit Vorrang vor ~/.claude/CLAUDE.md.

  • Alle fünf MikroTik laufen auf RouterOS 7.24.1 (🆕 Angleichung am 2026-08-31 von Christoph selbst durchgezogen, TP27 damit erledigt — ap2 08:56, core 14:36, ap1 14:50, branch 14:52; ap3 kam bereits so. RouterBOOT jeweils = upgrade-firmware = 7.24.1, live gegengelesen). Vorher: vier auf 7.24 (08-17), nur ap3 auf 7.24.1. Neu in der Flotte ap3 (cAP ax cAPGi-5HaxD2HaxD, 10.40.99.13, Uplink core ether21). ✅ Folge für TP16.3: die versionsgebundene container.npk ist damit eine Datei für alle drei APs (alle arm64) — die Zwei-Dateien-Regel vom 08-30 war rein versionsbedingt und ist erledigt. (Hier stand bis 08-29 „Alle vier MikroTik … 7.24" — durch ap3 und das Upgrade zweifach überholt.)

  • L3 + WAN-Edge = crs326-core (CRS326-24G-2S+, Technik UG): Routing, DHCP (7 Server, 54 Reservierungen), DNS, NAT, NTP-Server für den Standort und der WG-Hub (TP10, Peers fm/dw) laufen hier. Mgmt-SVI 10.40.99.1 (vlan99), Gateway .1 je VLAN. WAN = vlan100 — DHCP-Client hält die öffentliche Liwest-IP 90.146.33.177/24 direkt vom Arris (Bridge-Mode) → kein Double-NAT. DNS-Upstream = DoH zum CF-Gateway mit verify-doh-cert=yes (TP23 Stufe 1 ✅ 08-18); servers=1.1.1.1 steht nur noch als Bootstrap für den DoH-Hostnamen.

  • 🔥 Die Firewall ist deny-by-default — in BEIDEN Ketten (TP5 E3/E4 ✅ 08-18). TP10-seal-lan ist disabled; Mgmt-Input kommt nur noch aus VLAN10, von HA (8728), von Nautobot (22) und über cf-mesh. Ein neuer Dienst über eine VLAN-Grenze braucht ab jetzt eine explizite Regel.

  • L2-Branch = crs326-branch (Technik OG): reiner L2-Switch, Mgmt 10.40.99.2, Uplink ether1 → core ether8.

  • WLAN = CAPsMAN auf dem Core (TP28 ✅ 08-17, GC-Modell nachgezogen 08-18): ap1 10.40.99.11 (an core ether15), ap2 10.40.99.12 (an branch ether24), beide reine CAPs — cap enabled=yes, discovery-interfaces=vlan99. Die APs halten keine eigene WLAN-Config mehr; Kanäle, SSIDs, Security und Steering kommen vom Manager. 802.11r/k/v aktiv, ft-over-ds=yes. 🆕 Dritter AP ap3 (cAP ax) ✅ LIVE seit 2026-08-30 (TP28 dafür wieder geöffnet): 10.40.99.13, Uplink ether1 → core ether21 (Trunk), reiner CAP, RouterOS 7.24.1 (🆕 seit 2026-08-31 die ganze Flotte — die frühere Versionsdifferenz ist aufgelöst). Kanäle 5180/ax/Ce/I + 2437/ax (ch6) nach dem TP25-Plan vom 2026-08-24, am Gerät nachgemessen. 🔴 ap1 ist funkstill — es steht bis zu seinem Umzug ans Sideboard direkt neben ap3 und würde auf 5180 kollidieren; es hat deshalb bewusst keine Provisioning-Regel am Manager, bleibt aber erreichbar. Beim Umzug: ap1 → 5260/Ce (Band B, DFS, Frequenz-Liste statt Einzelwert), ap2 → 5300/Ce, Uplink ap1 → core ether3. Compliance jetzt 130/130 / 0 Drifts über 5 Geräte (vorher 104/104 über 4).

  • SSDP-Relay (TP7.1, nachgebessert 08-25): LXC 108 mcast-relay auf pve, zweibeinig 10.40.10.21 (VLAN10) + 10.40.40.21 (VLAN40) — dafür trägt core ether13 beide VLANs; mDNS macht weiterhin der Core. Läuft seit 08-25 ohne --ssdpUnicastAddr — die Flagge verwarf die M-SEARCH der App auf dem Weg nach VLAN40 (Messung: 0 Pakete via eth1 gegen 308 via eth0 in 20 min, 0× Received SSDP Unicast im ganzen Journal seit 08-18). Der Rückweg läuft jetzt nativ geroutet über die längst offene Regel TP5: MEDIA->MAIN; nachgemessen kommen HTTP/1.1 200 OK von 10.40.40.193/.195 an beiden MacBook-Beinen an. Unit liegt im Repo: mcast-relay/multicast-relay.service, Details als Gotcha in CLAUDE.md. ⚠️ Der Fall hatte zwei unabhängige Ursachen — nach der Relay-Reparatur suchte die App immer noch nicht (macOS-Local-Network-Gate stand auf „an" und griff nicht; Fix war Schalter aus/ein + Neustart, User-bestätigt 08-25). Beide sind erledigt.

  • Reverse Proxy = caddy-bf1 (LXC 130 auf pve, 10.40.20.40, TP31 ✅ Schritt 1–3 seit 2026-08-25, hass 400 behoben 2026-08-26, Split-Horizon seit 2026-08-26): Caddy 2.11.4, 16 Sites (4 public, 12 intern mit .bf.-Suffix), Zertifikate ausschliesslich per DNS-01 mit einem eng geschnittenen Zone:DNS:Edit-Token (CF_DNS_API_TOKEN, nicht das account-weite). Abnahme über den Host-Header von einem zweiten Host: 11× 200, nautobot 302. Offen: Schritt 5–6 (Connector für bf-public, CNAME-Flip, Abbau der sechs Container) — Runbook steht: tp16.3-bf-public-ap-connector-runbook.md, Vor-Ort-Session an ap1/ap2/ap3. ✅ Die drei §1-Blocker sind am 2026-08-31 remote erledigt — (a) Ingress auf https://10.40.20.40:443 + originServerName je Regel umgestellt (die alte :80-Zeile hätte über CF eine Redirect-Schleife gebaut, weil Caddy dort 308 auf https antwortet); (b) Freigabe vlan99 → vlan20:443 über Nautobot GC deployt (ip-firewall-filter steht unter Drift-Kontrolle, TP26 ⇒ kein Direkt-Write), Position vor TP5: log-only verifiziert, 135/135 compliant, 0 Drifts; (c) ROUTER_AP3_PASS in ~/.secrets nachgetragen (REST-Auth 200). ⇒ Vor Ort startet die Arbeit bei §2/§3 (Pakete, device-mode, Container).

  • DNS/Resolver = AdGuard-Paar agh-bf1/agh-bf2 (LXC 110/111 auf pve, 10.40.20.36/.37, TP23 Stufe 2 + TP31 Schritt 4 ✅ seit 2026-08-26): Upstream DoH zum CF-Gateway, conditional [/bf.emsenhuber.net/] → 10.40.20.1, 47 Rewrites (16 Service → Caddy .20.40 + 10 Host-Namen mit .bf.-Suffix + alte suffixlose parallel). DHCP liefert .36/.37 als DNS in allen 7 VLANs (domain=emsenhuber.net), Firewall forward UDP+TCP/53 auf beide. Der Core-Resolver (10.40.X.1) bleibt als Bootstrap unverändert (DoH, allow-remote-requests=yes, mdns-repeat-ifaces).

  • Ausgebaut / gelöscht: ALLNET-sw1 und Omada-sw2 sind physisch weg (2026-08-08 bzw. 2026-07-23, in Nautobot Decommissioned); Omada-Controller + omada-mcp gelöscht (2026-07-16); FritzBox 6660 raus seit 2026-07-01 (Liwest-Tausch auf Arris). VLAN1 ist leer (/ip arp auf bridge = 0 Einträge, 08-18) — TP19 ist geschlossen; cudy1 hängt stromlos an core ether11, physischer Ausbau ist hands-on.

  • 🆕 WLAN-Logging persistent seit 2026-08-30 (TP25 Fund E): wireless + caps gehen vom Core in den TP5-Kollektor (10.40.20.18:514); /system logging steht unter der 27. ComplianceRule system-logging. Vorher nur Ringpuffer → ein seit Wochen gemeldeter iPhone-Abbruch war rückwirkend nicht diagnostizierbar.

  • ⚖️ TP25 Fund E — Hebel 1 ZURÜCKGEBAUT, Entscheidung faellt am 2026-09-02 (Stand 09-01 09:2x): Zwei Sessions haben denselben Fall bewertet und kamen zu entgegengesetzten Schluessen. Aufgeloest zugunsten der Kollektor-Messung:

    • Die Nacht-Session las get_log … sinceMinutes=1440 → total 0 und schloss auf „geheilt". Das ist der fluechtige Ringpuffer des Routers: live nachgemessen 1000 Zeilen, aeltester Eintrag 01.09. 00:38:58 — keine 9 h Tiefe. Der Core-Reboot 14:36 hatte ihn geleert, der 31.08. Vormittag war darin nicht sichtbar. 🔴 Genau die Falle, wegen der am 30.08. der persistente Kollektor eingerichtet wurde.
    • Im Kollektor stehen fuer 08:09–14:36 (Hebel 1 allein, vor dem Upgrade) 124× reauthenticating, Stundenverteilung 09h=1 · 11h=2 · 12h=20 · 13h=31 · 14h=70, alle vom iPhone allein (= Client-Befund nach der eigenen Merkregel „viele MACs = Infrastruktur"), dazu Christophs zwei erlebte Ausfaelle um 13:16 und 14:23. Zwischen 09:00 und 14:36 gab es keinen Reboot, der sie erklaeren koennte.
    • ⇒ Hebel 1 hat im upgrade-freien Fenster nichts bewirkt; der Einbruch faellt exakt auf 7.24.1. Deshalb am 2026-09-01 zurueckgebaut (ft-over-ds=yes, Provisionierung nachgezogen, frische DHCP-Lease abgenommen, Transaktion zurueckgebaut, Compliance 140/140). Der Rueckbau ist das Experiment: bleibt es ruhig, war 7.24.1 die Ursache; kehrt das Haemmern zurueck, hatte die Nacht-Session recht und Hebel 1 wird als neue Transaktion erneut gezogen. Bis zur Messung am 2026-09-02 frueh committet niemand. Handoff: handoff-20260901-fund-e-hebel1-zurueckgebaut.md.
  • 🔴 ABER: neuer Teilbefund am 23:02 (2026-08-31) — ap2 unterperformt, NICHT FT. Christoph am Schreibtisch, 5G, kein Telefonat. Log: iPhone F2:62:4C:E1:AF:09 ping-pongt im Sekundentakt cap-wifi5 ↔ cap-wifi6 = beide Radios von ap2 (5G 5220 ↔ 2,4G ch11/2462, Band-Steering-Thrash auf demselben AP), disconnected, not responding um 22:37:40 und 23:01:00. Feldstärke −75…−88 dBm — bei laut Christoph 2 m Abstand (ap2 auf 2,3 m, gleicher Raum). Bei 2 m wären −35…−45 normal. Aktuell 0 Clients auf ap2, alle 29 auf ap3. ⇒ kein Coverage-/FT-Problem, sondern ap2s Funk selbst (RF/Antenne/HW) und/oder die DTU-Störung auf ap2/ch11 (Transaktion TP17.1-dtu-pin-ap2, −59 dBm auf ap2s 2,4-Radio). Als eigener 🔬-Unterpunkt geführt, In-situ zu messen. Handoff: handoff-20260901-tp25-ap2-verdacht.md.

  • 🔴 TP25 Fund E — Hebel 1 ist GESCHEITERT (Stand 2026-08-31 nachmittags): sec-main steht am Core auf ft=yes ft-over-ds=no (seit 08:09), aber das Symptom blieb: 124× reauthenticating nach dem Eingriff und zwei von Christoph erlebte Ausfälle — 13:16 (Videocall bricht ab, 7 min ohne WLAN) und 14:23 (5,5 min, nur durch WLAN-Toggle beendet). Stundenvergleich auf gleicher Firmware: 07:00–08:00 = 79 · 14:00–15:00 = 70. Erfolgskriterium war „~0" ⇒ nicht eingetreten. Gewertet wird bewusst nur 08:09–14:36 (danach kommt das TP27-Upgrade als zweite Variable dazu; Christoph-Entscheid: „Vormittag als Ergebnis werten"). ⇒ Transaktion TP25-fund-e-ft-over-ds bleibt OFFEN (Ablauf 2026-09-03), Compliance weiter bewusst 134/135; Rollback oder Commit ist eine Entscheidung, kein Aufräumen. Geräte-Backup pre-tp25-funde-ftoverds.backup liegt auf dem Core. Nächste Entscheidung: Hebel 2 (ft=no) oder authentication-types=wpa2-psk,wpa3-psk — sec-main ist auch die einzige SSID ohne wpa2-psk, und association rejected, no cached PMK (157× am 31.08., 0× am 30.08.) ist eine SAE/PMKSA-Meldung. Vorher prüfen, ob 7.24.1 auf dem Manager das Verhalten schon verändert hat — zwischen 14:37 und 15:12 stand nur 1 reauthenticating im Log, keines vom iPhone (35 min = noch keine Aussage). Details: handoff-20260831-tp27-upgrade-und-hebel1-befund.md.

  • ⚠️ Messfalle, die diese Session fast erwischt hat: die 13 CAP-joined/activity timeout vom 31.08. sahen nach CAPWAP-Instabilität aus — es waren die Reboots des TP27-Upgrades. Und die cap-wifiN-Karte hat sich an einem Tag zweimal verschoben (08:09, 14:36), womit die im Vorgänger-Handoff festgehaltene Tabelle nach sechs Stunden falsch war. Interface-Namen in Logzeilen sind ohne zeitnahen Karten-Snapshot nicht zuschreibbar.

  • Kontrolle / Governance: MikroMCP für Reads/Debug (alle 5 Geräte in der Registry), Config-Änderungen ausschließlich über Nautobot Golden Config (TP21/TP22 — Deploy-Pipeline scharf, 135/135 Compliance-Checks grün, 0 Drifts bei 27 Rules × 5 Geräten (Stand 2026-08-30; seit 2026-08-31 vorübergehend 134/135 — der eine Drift ist die offene Transaktion TP25-fund-e-ft-over-ds, s. oben, kein Fehler) — live gegen die API geprüft am 2026-08-23; zuletzt dazugekommen: wifi-access-list (08-22), queue (08-23, TP6) und ip-firewall-mangle (08-23, TP6.1), s. TP22-Nachtrag im Archiv). (Hier stand bis zum 08-23 „100/100 bei 25 Rules" — eine Zählung, die TP6.1 am selben Tag überholt hat. Die Merkregel gilt: der Ist-Stand-Block wird bei jeder Änderung mitgezogen, nicht nur der TP.)

📜 Historische Fassung (Stand bis 2026-08-08, Topologie vor dem Core-Cutover — bewusst erhalten, nicht als Ist lesen)

ALLNET-Core .11 (FW2.2.1, VLANs 10/20/30/40/50/90/99/100; GE1/GE2/GE8 Trunk; TP1 ✅ 2026-06-15). Omada-Switch .12 (adopted, TP2 ✅ 2026-06-15; Trunk-ALL auf P1+P2, Access-Profile auf P3-P8; Profile noch PVID=1 bis Omada-PVID-Fix). Omada-Controller .175 (DS923/macvlan, mbentley 6.2, 🟢 live). omada-mcp .176 (pve-LXC, StreamableHTTP, 🟢 live, 84 read-Tools; Controller-API v2 intern via christoph@emsenhuber.com). 2× hAP ax² (7.23.1): ap1 .1 (ALLNET P1) — TP4 ✅ 2026-07-01: Router + WAN-Edge (VLAN-Subinterfaces, DHCP, DNS, NAT, Public-Edge). ap2 .2 (Omada P2 — Trunk). Edge-Cutover ✅ 2026-07-01: FB6660 physisch entfernt (Liwest-Kabelmodemtausch, kein Zurück), Arris-Kabelmodem im Bridge-Mode; WAN = sw1 gi16 Access VLAN100 → gi15 Trunk → ap1 ether2 → vlan100 auf bridge (NICHT ether1); ap1 hält Public-IP direkt (Liwest, kein CGNAT), Double-NAT weg. Telefonie war auf der FB und wird nicht weitergeführt. Vorarbeit: TP10-Kern durch (2026-06-27, WG-Hub live) → Bridge-Gate vorher gelöst. Runbook: routeros/wan-cutover-runbook.md. Kontrolle: MikroMCP (MikroTik) live · ALLNET = Klickanleitungen · Omada → Controller + omada-mcp 🟢.

Nächster Schritt (Stand 2026-08-30): Vor-Ort-Session ap1/ap2 — sie ist der einzige Weg, den Public-Strang auf bf-public zu heben und die drei Alt-Tunnel samt sechs Containern abzubauen (TP16.3 Follow-up F2). ✅ F1 ist am 2026-08-30 durch (12 Client-Configs auf LAN-IP, danach gitea-mcp/n8n-mcp aus Ingress+DNS geschnitten) — davor unabhängig machbar bleibt nur noch die Announcement-Doku (TP35 Schicht 4), die den anderen Projekten die neuen MCP-URLs mitteilt.

Vorherige Fassung (Stand 2026-08-28): drei Folge-Sessions parallel (siehe docs/handoff-20260828-mcp-bestand-und-performance.md):

  • ✅ Session A — /projektstatus-Skill global für opencode (TP35 Schicht 6). Funktionsbeweis erbracht 2026-08-28.
  • 🔜 Session B — shelly-mcp-Container auf DS923 (10.40.20.10) deployen (live, ssh). Repo-Materialien committed (mcp/shelly/{Dockerfile,BUILD.md,requirements.txt,.gitignore}); .env und Tunnel-Secret lokal — kein MCP-Client (User-Direktive).
  • 🔜 Session C — caddy/cf-sync.py-Drift-Check + Backup-Guard (~/dev/.bin/backup-guard.sh, TP35 Schicht 5).

🆕 Dazu seit 2026-08-30: TP16.3/TP31 Schritt 5 = Vor-Ort-Session an ap1/ap2/ap3 (Runbook). Reihenfolge beachten: die drei Blocker in §1 sind remote zu erledigen, bevor jemand in den Keller geht — sonst steht man vor einem Connector, der healthy meldet, während die Seite eine Redirect-Schleife dreht.

🆕 Strukturelle Folge = TP36: SSoT für teilprojekte.md-Live-Daten via Nautobot (eigene Session, nicht heute). Befund 2026-08-28: manuell gepflegte Datei verliert gegen die Realität, sobald sie lang wird. Quickref-Index, TP-Abschnitt und Ist-Stand-Block müssen bei jedem Edit mitgezogen werden — keine schlägt die andere. Merkregel im Lessons-Block.

🆕 Neu am 2026-08-24/25: TP31 löst TP16 und TP16.2 ab und dampft cloudflared von sechs Containern auf einen ein; TP32 gibt die Nummern- und Adress-Systematik vor (und hat nebenbei die falsche Startreihenfolge auf pve repariert — HA startete vor seiner Datenbank). TP16 ✅, TP25 ✅ und TP16.3 (Pilot) ✅ sind am 24./25. durchgezogen worden.

Vorherige Fassung (Stand 2026-08-23): Christoph entscheidet — der kritische Pfad ist LEER.

🎯 Das Projekt ist in seinem Kern fertig. deny-by-default steht (TP5), Golden Config steht und meldet 104/104 compliant bei 0 Drifts (live geprüft 2026-08-23), WLAN steht über CAPsMAN auf dem Core (TP28), VLAN1 ist leer (TP19) — und TP6 ist beantwortet statt offengelassen: CAKE gebaut, gemessen, wieder ausgebaut. Nichts blockiert mehr etwas. 🟢 Was zuletzt wirklich etwas gebracht hat, war TP6.1 (2026-08-23): nicht Shapen, sondern Klassentrennung — NAS-Bulk priority=8 gegen interaktiv priority=1 drückt dieselbe Strecke unter derselben Sättigung von 40,8 auf 11,0 ms (−73 %). Die zuerst notierte DOCSIS-Erklärung aus TP6 ist damit widerlegt; der Fehler lag in der Messmethode (Last und Messung vom selben Host). 📋 Offen und ohne Blockadewirkung: TP25 (Fund F = 5:05-Takt bei IoT, plus die Entscheidung „5 GHz auf 20 MHz") · TP16 (CF-Tunnel-HA degradiert, vier tote Alt-Tunnel) · TP16.2/16.3 · TP23 Stufe 2 · TP24 (DC-USV) · TP13/TP14.4 (beide mit fertigem Runbook) · ❌ TP17.1 VERWORFEN 2026-08-23 (Zigbee-Weg bewiesen, Ziel damit nicht erreichbar — das Gerät bleibt WLAN-Client; s9 zurückgebaut) · ✅ TP17.3 ERLEDIGT (Ursache eco_mode bewiesen; s23 + s26 repariert; eco_mode flottenweit aus, 22/22 außer s24) · ➡️ TP17.2 ist am 2026-08-24 in TP30 aufgegangen (Shelly-Gerätepasswort = Erstvergabe eines Credentials; TP30 ist das Sammelbecken dafür) · 🔑 Alle Passwort-/Token-Rotationen sind seit 2026-08-24 TP30 (Sammel-TP) — bewusst gebündelt gegen Projektende, User-Entscheid. Damit ist die ältere Einreihung „hinter TP17.1" gegenstandslos (TP17.1 ist verworfen). Etappe 1 — die Service-Accounts — ist durch; das admin-Passwort selbst ist nicht rotiert und bleibt es bis dahin bewusst. 📌 Einstieg für die nächste Session: handoff-20260825-agh-dns-cutover.md — AdGuard als Resolver + Split-Horizon (TP23 Stufe 2 ∥ TP31 Schritt 4). Bewusst eigene Session: der Schritt verschiebt die DNS-Auflösung des ganzen Standorts in einen Container. 📜 Älter: handoff-20260824-session.md — ⚠️ dessen Empfehlung „Start bei der Passwort-Rotation Etappe 2" ist durch den TP30-Entscheid überholt; der Rest der Übergabe gilt. Ältere Sitzungsvorschläge: handoff-20260822-naechste-schritte.md. ✅ crs326-core ist transaktionsfrei (seit 2026-08-22, geprüft): ein gemeldeter Drift ist ab jetzt echt, nicht beabsichtigt — python3 nautobot/gc-transaktion.py status.

📜 Vorherige Fassungen dieses Absatzes (2026-07-22 → 2026-08-21, bewusst erhalten)

Fassung 2026-08-21 abends (erledigt — TP25s Dead Spot ist am 08-22 repariert, TP6 am 08-23 geschlossen): Nächster Schritt: TP25 — Steering-Test in den Schlafräumen. 📋 Einstieg: handoff-tp25-session-20260821.md — dort steht der verifizierte Ist-Stand, die Messfallen und der nächste Test. ✅ crs326-core ist transaktionsfrei (2026-08-22). TEST-ether3-trunk-ap1-sideboard und TEST-infra-ssid-matter-pairing sind beide zurückgerollt und geschlossen; ap1 hängt wieder auf ether15, ether3 ist wieder untagged VLAN40. Ein gemeldeter Drift ist ab jetzt echt, nicht beabsichtigt — python3 nautobot/gc-transaktion.py status. 🆕 TP29 (Anwesenheitserkennung fürs Energiesparen) ist als Idee angelegt, nicht auf dem Pfad. 📌 Die TP6-Prämisse ist am 2026-08-20/21 gemessen (Runbook §5): formal Fall B (+28…+47 ms unter Dauer-Upload), aber im Videocall nicht wahrnehmbar — kein einziger Problem-Klick über sechs Lastphasen. Laut war stattdessen der Funk: Sticky Client + Band-Ping-Pong, zwölf Wechsel in neun Minuten, je Wechsel bis zu 4 s ohne Durchsatz → tp25-wlan-roaming-messung-20260820.md. Deshalb steht jetzt TP25 vor TP6; TP6 bleibt offen und begründet, aber nachrangig.

Vorherige Fassung (Stand 2026-08-18, erledigt): TP6 wird hier seit Monaten als „DAS Ziel" geführt, aber docs/call-quality-diagnosis.md hat den Bufferbloat 2026-06-16 bereits gemessen (A/A+, „CAKE wäre marginal, nicht deployt"). Die Messung stammt aus der FritzBox-Ära, der Edge ist seither getauscht — also einmal gegen den heutigen Edge nachmessen und dann entscheiden. Blocker gibt es keine mehr. TP5 ist in der Nacht auf den 18. abgeschlossen worden (Etappe 3 + 4: deny-by-default in forward und input, MGMT isoliert, seal-lan abgeschaltet), TP23 Stufe 1 (DoH auf dem Core) ebenfalls. Nebenher offen, blockiert nichts: TP25 (Signal-Boden + Re-Walk), TP28-Rest (die fünf wifi-*-Features vom AP-Scope auf den Core umhängen — die einzigen verbliebenen Compliance-Drifts), TP7.1 (SSDP-Relay für die macOS-Sonos-App), TP16.3, TP24. Vorherige Fassung (Stand 2026-08-14, überholt): „TP5 — Deny-by-default am Ende von forward." Alles Davorliegende ist durch: CRS326-Bring-up + L3-Umzug ap1→crs326-core (TP15/TP21.1 ✅ 2026-08-08), WLAN-Neuprovisionierung ohne CAPsMAN (TP3 ✅ 2026-08-08), Nautobot-Plattform + Fleet-Adoption + Deploy-Pipeline (TP21/TP22 ✅ 2026-08-13). Danach TP6 (CAKE) = das Projektziel. Kleine Follow-ups nebenher: DoH-Cloudflare auf core, DC-USV (TP24, seit 2026-08-16 eigener TP). ⚪ Omada-PVID-Fix ist obsolet (User-Entscheid 2026-07-22 — Omada spielt seit der TP15-Korrektur keine Rolle mehr im Netz, nie mehr aufgreifen). Vorherige Fassung (Stand 2026-07-22, erledigt): „TP15 CRS326-Bring-up als neuer L3-Core, danach WLAN-Neuprovisionierung ohne CAPsMAN; TP5 folgt nach TP21/CRS326-Adoption."

🎯 Ziel: sauberes System „from scratch", keine Leichen/Altlasten. Migrations-Session-Endstand = VLAN1 + altes cudy-WLAN komplett weg (VLAN1 nur noch Mgmt, defconf-Client-DHCP zurückgezogen, cudy-Uplinks aufs Ziel-Client-VLAN getaggt oder cudy abgebaut). Unterpunkte/Aufräum-Liste bei Migration: (a) die Interim-Härtung ap2 wifi1/wifi2 frame-types=admit-only-vlan-tagged (TP3.1, 2026-07-06) bleibt gültig — verschwindet automatisch mit VLAN1; (b) st-main rrm/wnm = reines Roaming-Provisorium → kein Provisorium mehr (2026-08-13, TP3.3): Steering ist fester Bestandteil des Golden-Config-Templates + ComplianceFeature wifi-steering — es war die Neuprovisionierung, die es stillschweigend verloren hatte; (c) cudy1 = verbleibender aktiver VLAN1-Leak → ERLEDIGT (User-bestätigt + verifiziert 2026-08-14): cudy1 ist offline. core ether11 trägt kein Link (kein R-Flag), Nautobot-Status Offline (TP22 Schritt 5). Damit existiert kein aktiver VLAN1-Leak mehr; der Dual-DHCP-Race aus project_vlan1_dhcp_incident ist Historie. Der formale Teardown (Gerät ausbauen, Doku/Inventar) bleibt TP19. ⚠️ HW-Constraint hAP ax²: 1 GB RAM, aber nur 128 MB Flash + KEIN USB → Container auf dem Router NICHT praktikabel. Folge: native RouterOS-Features bevorzugen (z.B. Built-in-mDNS statt Container); Container (mDNS-Reflector, BIND9-DNS, Omada-Controller) laufen auf NAS/pve, nicht am hAP. 🔒 Governance-Entscheid (User, 2026-07-16, ausgeweitet 2026-07-16): Config ausschließlich über Nautobot — keine Direkt-Changes mehr an einzelnen im Netz verstreuten Geräten. Ursprünglich als ap1/ap2-Config-Freeze bis TP21 formuliert, noch am selben Tag verallgemeinert: jede Netz-Gerätekonfiguration (Router, APs, künftig auch die CRS326-Switches) läuft ab jetzt exklusiv über Nautobot Golden Config (TP21), sobald das steht — keine ad-hoc MikroMCP-Writes/UI-Klicks mehr an Einzelgeräten. TP19.1 war die letzte Ausnahme (ap1-DNS-Fix + ap2/sw1/sw2-Mgmt-VLAN-Umzug, s.u.) — davon ist nur der risikolose ap1-Teil noch direkt gemacht worden, ap2 wurde bewusst auf „nach Nautobot" vertagt. Macht Nautobot + alle Prerequisites (TP18→TP20→TP20.1→TP21, jetzt auch Komodo) zum aktuell kritischen Pfad, vor TP5/TP6. Betrifft nicht Software-Migrationen auf NAS/pve (anderes Substrat) und die Frage, ob sw1/sw2 vor der CRS326-Ablösung noch angefasst werden (offen, s. TP15/TP19.1). ✅ Genehmigte Ausnahme (User, 2026-07-17): der zentrale CF-Tunnel-Zugang auf ap1 (TP16/TP16.1). Alle für den zentralen Cloudflare-Zugang auf ap1 notwendigen (Vor-)Arbeiten dürfen direkt via MikroMCP/CF-API gemacht werden, ohne auf Nautobot GC zu warten — konkret die nativen WireGuard-Mesh-Zeilen aus TP16.1 (und analoge cloudflared/WG-Anpassungen am ap1-Edge). Begründung: Remote-Erreichbarkeit ist die Voraussetzung, um überhaupt sicher am Netz zu arbeiten (Henne-Ei zu Nautobot). Scope-Grenze: Die Ausnahme gilt nur für ap1. Sobald der zentrale Zugang später auf die CRS326 wandert (TP15), gilt die Ausnahme dafür vorerst NICHT — CRS326-Config läuft dann regulär über Nautobot GC (oder braucht eine eigene, dann neu zu erteilende Ausnahme). Nachtrag der ap1-Ausnahmezeilen in die GC als Follow-up, sobald Nautobot steht. ✅ Genehmigte Ausnahme #4 (User, 2026-08-16): die WireGuard-Zeilen des Cloudflare-Mesh-Bring-ups auf crs326-core (TP16.1). Dürfen direkt via MikroMCP/REST gesetzt werden. Begründung ist strukturell, nicht bequem: TP22-Regel 1 schließt jede Sektion mit <<REDACTED>> generell vom GC-Push aus, und /interface wireguard ist genau so eine — ein privater Schlüssel ist über Golden Config nicht deploybar. Damit ist die frühere TP16.1-Zusage „CRS326-Zeilen laufen regulär über GC" gegenstandslos. Auflage: vorher /system backup, danach Ist-Stand als intended config zurück nach Nautobot. Scope-Grenze: nur der Mesh-Bring-up auf crs326-core — nicht spätere WG-Änderungen, nicht andere Geräte, und sie verlängert sich nicht automatisch. Langfristige Auflösung = das Secret-Handling-Design in TP16.1 (Privkey ist Geräte-Identität, nicht Config). ✅ Genehmigte Ausnahme #2 (User, 2026-07-22): der CRS326-Bring-up (TP15). Die Grundkonfiguration beider CRS326 — Management-IP, Bridge, VLAN-Filtering, Trunks/Access-Ports — darf direkt via MikroMCP/Winbox gemacht werden. Begründung = Henne-Ei: Golden Config kann nicht auf ein Gerät pushen, das im Werkszustand keine erreichbare IP hat, und die GC-Templates existieren noch nicht (TP21 „offen: Templates/intended config"). Auflage: der so entstandene Ist-Stand wird anschließend als intended config nach Nautobot zurückgeschrieben, danach übernimmt GC — die Ausnahme deckt das Bootstrapping, nicht den Dauerbetrieb. Scope-Grenze: gilt für die beiden CRS326 im Bring-up. Sie hebt die Regel für ap1/ap2 nicht auf (deren Neuprovisionierung läuft weiterhin über GC) und verlängert sich nicht automatisch auf spätere CRS326-Änderungen. Damit ist die frühere Formulierung in TP16.1 („CRS326-Config läuft regulär über Nautobot GC oder braucht eine eigene, dann neu zu erteilende Ausnahme") erfüllt — dies ist diese Ausnahme.


Offene Teilprojekte

Alles hier hat noch offene Arbeit. Abgeschlossenes steht im Archiv (Volltext, mit Inhaltsverzeichnis) — die Zuordnung ist mechanisch: ein TP wandert erst dann ins Archiv, wenn nichts mehr daran zu tun ist — und der Weg zurück ist offen: ein archiviertes TP ist ein geschlossenes Ticket, kein versiegeltes. Kommt an derselben Baustelle neue Arbeit dazu, wandert der Volltext zurück hierher (Zeiger bleibt im Archiv stehen, damit alte Links halten) — statt ein Zwillings-TP mit neuer Nummer anzulegen. (Christoph-Direktive 2026-08-30, erster Fall: TP28 wegen ap3.) Wo nur ein Sub-TP offen war, steht dieses hier und das erledigte Eltern-TP im Archiv (so bei TP17.2 ↔ TP17 (TP17.2 ist am 2026-08-24 in TP30 aufgegangen und kein eigenes TP mehr) und TP20.1 ↔ TP20).

📊 Quick-Index (nur dieser Block ist für den Statusbaum nˆtig — 1 Read)

# Status Kurztitel Abschluss
TP5.1 ⏸️ DNS-Visibility Airfryer —
TP8 ⏸️ Identity / 802.1X —
TP11 ⏸️ Monitoring —
TP12 ⏸️ IPv6 (3 Standorte) —
TP12.1 🔜 IPv6 ULA + v6-Firewall —
TP13 🔜 Botschafts-WLANs —
TP14.4 ✅ CF-Access-Token — in TP41 T4 aufgegangen (claude-mcp→zt-svc, .env geschrieben) 2026-09-01
TP16 ✅ CF-Tunnel HA wieder voll 2026-08-24
TP16.2 🔜 Härtung bypass-Dienste —
TP16.3 ✅ cloudflared-Reduktion — Flip + Abbau durch; nur noch bf-public+fm, 7 Container weg 2026-09-01
TP17.1 ❌ Shelly Gen4 → Zigbee 2026-08-23
TP17.3 ✅ Shelly-Regression behoben 2026-08-23
TP18 🔜 Garage-Objektspeicher —
TP20.1 🔜 Vaultwarden → Platform-PG —
TP23 ✅ DNS-Policy (Stufe 1+2 ✅) 2026-08-26
TP24 🔜 DC-USV —
TP25 🔬 WLAN-Stabilisierung (Kern ✅) — Fund E: Hebel 1 widerlegt + zurückgebaut, Messung bis 09-02; ap2 unterperformt (2 m/−80, In-situ) 2026-09-01
TP28 ✅ WLAN/CAPsMAN — dritter AP ap3 (cAP ax) live 2026-08-30
TP29 💡 Anwesenheitserkennung (Idee) —
TP30 🔜 Credentials (Etappe 1 ✅, Rest gebündelt) —
TP31 ✅ Reverse Proxy Caddy — 1–4 ✅ (08-25/26), 5+6 via TP16.3 (09-01); Abnahme-Tool cf-sync.py grün 2026-09-01
TP32 🟢 Nummern-Systematik (Konzept) —
TP33 💡 Sonos fern (Idee, zurückgestellt) —
TP34 🔜 Caddyfile via Nautobot/GC (Stub, Inventur-Handoff 2026-08-27) —
TP35 ⏳ MCP-Schichtung + 3-Schicht-CLAUDE.md + Backup-Guard (Schicht 1+3+6 ✅) —
TP36 🔜 SSoT via Nautobot für teilprojekte.md-Live-Daten + Datei-Aufteilung —
TP37 🔜 Nautobot als Netzwerkdoku: Hosts/Container/Dienste + CF-Connectoren (Okay 08-30) —
TP38 🔜 OOB-Mgmt: untagged-VLAN99-Port je Switch —
TP40 🔜 HA-Recorder-Retention: 41 GB states beschneiden (Okay 09-01) —
TP41 ⏸️ CF-Access-Reparatur zurückgestellt 2026-09-01 (User): T2/T3/T4/T5 ✅ (GitHub+Google-Login live); Facebook abgesagt; Geli CF-seitig fertig (zt-humans+geli-Policy) → offen nur noch WARP-Client auf Gelis Gerät, wartet auf User-Info —
TP42 🔜 Namens-Vereinheitlichung bf.lan→emsenhuber.net: .bf.lan/.bf.emsenhuber.net aus allen Geräten raus, in Nautobot festhalten —
TP43 🔜 dw-Standort-DNS-Uplink: eigener Resolver mit site-Overrides (420.teilzeitladen.xyz→BF-Caddy/WG), WG-Disconnect bedacht; Geli sofort per WARP —
— — 35 archivierte TPs in teilprojekte-archiv.md (inkl. TP39 ✅ 2026-09-01) —

TP5.1 — DNS-Visibility & Flow-Observability für Low-Trust-Geräte (Airfryer-Fall) ▸ vorgemerkt

  • Kontext (2026-07-08/09): Airfryer „airschreier" (Philips/Versuni, ESP, 10.40.30.180) bekam per DHCP-Option 6 dns 1.1.1.1 als Override, weil das OTA-Update über den ap1-DoH-Gateway-Pfad nicht durchging (DoH-Latenz beim primitiven ESP-Stack — kein Content-Block, via Gateway-Logs bewiesen, s. memory/ref_esp_ota_doh_not_block). Nebenwirkung: public 1.1.1.1 = anonymer Consumer-Resolver ≠ accountgebundener Gateway-DoH (px35ynlol3.cloudflare-gateway.com) → DNS-Sicht auf das Gerät weg, Gerät umgeht zudem die Filterung.
  • (a) DNS-Redirect statt Geräte-Override (der dauerhafte Fix): dst-nat „UDP/53 (+TCP/53) von Low-Trust-Src → auf einen plain Resolver umbiegen" gibt Logging zurück UND funktionierendes OTA (kein langsamer DoH-Pfad fürs Gerät). Redirect-Ziel-Optionen: (i) ap1-Resolver mit nicht-DoH-Upstream für dieses Src-Set, (ii) direkt public 1.1.1.1 (dann nur Flow-Logging), (iii) eigener plain-Resolver. Sauberer als „Gerät verschwindet auf 1.1.1.1". Verortung: Teil des UNTRUSTED-Internet-only-Pinnings (TP5-„Offen"); Entscheidung v50-DNS-Policy (gefiltert vs. plain) hängt dran.
  • (b) Live-Torch als Ad-hoc-Observability: DNS ist bei direktem 1.1.1.1 blind, Flows nicht (NATen über ap1). torch auf vlan30/vlan50 gefiltert auf die Geräte-IP zeigt Ziel-IPs live (Versuni → AWS eu-west-1 IoT erkennbar). Voraussetzung: Gerät aktiv. Merkregel: Airfryer nur online wenn er kocht → Capture ans Einschalten koppeln.
  • Abh.: UNTRUSTED-Move des Airfryers (Kandidat, s. VLAN50) + TP5-Deny-by-default. Kein Blocker.

TP8 — Identity & Access-Control ▸ research-gated

  • RADIUS = FreeRADIUS auf der NAS (✏️ User-Manager v5 kann KEIN LDAP → openldap nur via FreeRADIUS anbindbar).
  • EAP-TLS (Zert) je Gerät + dynamische VLAN-Zuweisung (RADIUS-Attribut Tunnel-Private-Group-ID). Einfachste Heim-Alternative: pro VLAN eine SSID mit WPA3-PSK (kein RADIUS) — Abwägung Komfort vs. Admin-Aufwand.
  • Abh.: TP3. (Research erledigt, s. TP-R.)

TP11 — Monitoring & Mgmt-Control-Plane ▸ research-gated

  • MikroMCP + omada-mcp + SNMP/icinga-mikrotik, RoMON, NTP, E-Mail-Alerts, Netwatch. → Teil von docs/research/routeros-capabilities.md.
  • CF-Tunnel #1033 Track A ✅ live (2026-06-30): Round-Robin-1033er über 3 asymmetrische Connectors behoben durch Reduktion auf pve-only Connector (NAS + macmini scheiterten an pve-lokalen/stalen Origins). SSoT: docs/cf-tunnel-mapping.md. Track B/C läuft über TP14 (Ingress bereits v116 .41→.20 für NAS; pve-Connector→VLAN20 via TP14 Session B = #1033-Zielbild).

TP12 — IPv6 (intern + extern, 3 Standorte) ▸ research-gated

  • Konzept für FM/BF/DW. → TP-R.

TP12.1 — IPv6 im LAN (ULA) + v6-Firewall ▸ 🟠 KONKRET GEWORDEN (2026-08-22), Treiber: Matter/Thread

Anlass: der runde IKEA-Fühler TIMMERFLOTTE (Matter over Thread) ließ sich nicht anlernen — „pairing failed" in der Companion-App. Ursache ist nicht die Firewall, sondern dass zwischen VLAN10 und VLAN20 überhaupt kein IPv6 geroutet wird. Damit ist TP12 vom Konzept-TP zu einem Punkt mit einem konkreten, wiederkehrenden Schaden geworden.

  • Befund am crs326-core (live gelesen 2026-08-22):

    • IPv6-Adressen: ausschließlich Link-Local (fe80::…) auf jedem Interface. Keine ULA, keine GUA.
    • IPv6-Routing-Tabelle: nur ::1/128 und fe80::/64 je Interface. Keine Route auf den Thread-OMR-Prefix fd92:505a:c64e:1::/64 und keine auf fd03:cace:953f:e13d::/64 (den On-Link-Prefix, den der OTBR auf VLAN20 wirbt).
    • /ipv6 nd: Default-Eintrag, advertise-dns=no — es gibt aber ohnehin keine Prefixe zu werben.
  • 🔴🔴 Der eigentliche Kostenpunkt ist nicht IPv6, sondern die Firewall: /ipv6 firewall filter ist komplett leer. Heute harmlos, weil ohne Adressen nichts geroutet wird. In der Sekunde, in der Adressen vergeben werden, steht IOT→MAIN offen, GUEST→INFRA offen — an TP5 vorbei. Das gesamte TP5-Regelwerk muss vorher in v6 nachgebaut sein, sonst reißt dieser TP genau die Segmentierung auf, die TP5 am 2026-08-18 geschlossen hat. Das ist die Arbeit, nicht die Adressvergabe.

  • 🔎 Warum Matter das braucht: Commissioning läuft in zwei Etappen. Etappe 1 (BLE zum Gerät) ist netzunabhängig. In Etappe 2 muss der Commissioner — das iPhone — das Gerät auf dem operativen Thread-Netz wiederfinden (DNS-SD _matter._tcp, dann CASE). Dafür braucht das Telefon eine IPv6-Route in den OMR-Prefix hinein. iPhone hängt auf VLAN10, OTBR auf VLAN20 → Etappe 2 bricht ab. Die ~30 bereits laufenden Matter-Knoten widersprechen dem nicht: HA und OTBR sitzen auf demselben Host (wpan0 ist lokal), im Betrieb quert nichts die VLAN-Grenze. Nur das Anlernen tut es.

  • ⚠️ Zwei Fallen, die den naiven Weg verbauen:

    • (a) Der OMR-Prefix ist generiert und wandert. Am 2026-08-22 um 17:33 (OTBR-Neustart) sind alle Knoten von fd92:505a:c64e:1::/64 auf Mesh-Local fd66:986:170:bb1a::/64 gefallen — und Mesh-Local ist per Definition nicht off-mesh routbar. Eine statische Route auf den OMR-Prefix ist also nur so stabil wie der nächste Neustart. Ihn im Thread-Dataset festzunageln formiert das Netz neu — mit 30+ produktiven Knoten daran kein Nebenbei-Change.
    • (b) RouterOS lernt fremde RAs nicht. Der OTBR wirbt den OMR-Prefix per RA mit Route-Information-Option auf VLAN20. Der Core müsste die annehmen — als forwardender Router für dieses Interface tut er das nicht sauber.
  • 📡 Dritter Baustein: DNS-SD über die VLAN-Grenze. mdns-repeat-ifaces am Core (vlan10/20/30/40/90) ist IPv4. Für Matter-Discovery bräuchte es zusätzlich einen v6-fähigen Reflector — der mcast-relay-LXC (108) kann das prinzipiell, aber als zweite Instanz mit Beinen in VLAN10+VLAN20 (SSDP-Instanz ist VLAN10+VLAN40, s. Gotcha in CLAUDE.md).

  • ✅ Der Workaround, der das umgeht statt löst: iPhone zum Anlernen auf VLAN20 — dann sind Telefon und Border Router auf demselben L2-Link, der OTBR wirbt dort On-Link-Prefix und Route, und iOS wertet beides aus. cfg-infra / SSID dirtyvader-infra (dp-infra, VLAN20, sec-infra mit PSK) existiert am Manager bereits, ist nur in keiner provisioning-Zeile als Slave hinterlegt. Runbook: docs/matter-pairing-infra-ssid-runbook.md.

  • ✅ 2026-08-22 durchgefuehrt und wieder zurueckgebaut. dirtyvader-infra temporaer auf ap2 (Transaktion TEST-infra-ssid-matter-pairing, Scope wifi-provisioning), iPhone dort hinein, TIMMERFLOTTE per Companion-App angelernt — liefert 25,44 °C / 53,1 % rF / Batterie 100 % (Bereich Technik UG). Danach SSID deaktiviert; cfg-infra/dp-infra/sec-infra bleiben auf User-Wunsch inaktiv am Manager stehen, damit der Weg beim naechsten Thread-Geraet ein Zweizeiler ist. Beide Akten geschlossen, crs326-core transaktionsfrei. ⇒ Der Workaround ist damit bewiesen — und TP12.1 bleibt trotzdem offen: er umgeht das fehlende IPv6-Routing, er behebt es nicht.

  • ✅ Der Weg, der das Problem dauerhaft erledigt: Matter-Server-Option ble_proxy: true (ab HA 2026.06, hier 2026.8.3). Dann kommissioniert HA selbst über seinen Bluetooth-Stack; das Telefon ist raus und die VLAN-Grenze wird nie berührt. Braucht eine connectable BLE-Quelle — Shelly-Proxies sind reine Advertisement-Scanner (kein GATT), ein ESP32 als ESPHome-BT-Proxy (active: true) reicht. Der ESPHome Device Builder ist bereits installiert. Damit wäre TP12.1 für Matter optional. 🔴 User-Entscheid 2026-08-22: ESP32 wird NICHT angeschafft. Damit bleibt ble_proxy zwar an (schadet nicht, kostet nichts), ist aber wirkungslos — HA hat keine connectable BLE-Quelle und wird auch keine bekommen. Konsequenz, die man beim nächsten Thread-Gerät kennen muss: der Weg ist dann wieder die temporäre -infra-SSID nach dem Runbook. Die ist inzwischen ein Zweizeiler, weil cfg-infra/dp-infra/sec-infra am Manager stehen bleiben — aber sie ist jedes Mal ein Config-Change am Core plus eine Transaktion. Wer das dauerhaft loswerden will, muss TP12.1 bauen (v6-Firewall zuerst); ein dritter Weg ist damit nicht mehr vorgesehen.

  • 🔎 Nebenbefund 2026-08-22, gehört zur Thread-Gesundheit, NICHT zu IPv6: drei Matter-Knoten flappen dauerhaft — @1:a („Diele OG/Treppenauge"), @1:f und @1:10. Muster im Matter-Server-Log: IpServiceStatus … Resolving (address is unreachable), dann offline grace period expired, marking unavailable, im Abstand von 15–30 Minuten, über Stunden. Die übrigen ~20 Knoten sind stabil, es ist also kein Border-Router- oder Fabric-Problem. ⚠️ Vor einer Diagnose die Sonderlage von heute abziehen: der OTBR ist am 2026-08-22 um 17:33 neu gestartet (Knoten fielen kurzzeitig von OMR fd92:… auf Mesh-Local fd66:… zurück), und ap1 war zwischenzeitlich vom Netz. Erst nachsehen, ob das Flappen danach noch besteht — sonst diagnostiziert man das Nachbeben eines Umbaus. Erste Verdachtsrichtungen, wenn es bleibt: Thread-Routing/Parent-Wechsel dieser drei Knoten, schwache Funkstrecke zum nächsten Router-Knoten, oder eine ICD-/Sleepy-End-Device-Eigenheit dieser Geräteklasse.

  • Abh.: hängt an nichts. Blockiert nichts — außer künftige Thread-Geräte, solange weder ble_proxy noch die -infra-SSID steht. Reihenfolge innerhalb des TP ist zwingend: v6-Firewall nachbauen → dann erst Adressen vergeben. Gehört unter TP12 (Gesamtkonzept 3 Standorte), ist aber allein für BF ausführbar.

TP13 — „Botschafts-WLANs" ▸ 🔜 AUSFÜHRBAR (Runbook 2026-08-19) — docs/tp13-botschafts-wlan-runbook.md · fun/low-prio

📌 Am 2026-08-19 überarbeitet: der Technikteil beschrieb noch Standalone-APs (SSIDs entstehen seit TP28 am Manager, also ein Deploy statt zwei), und der Entwurf „nur ssid=, kein Security" war ein Denkfehler — eine SSID ohne Security ist ein offenes WLAN, also genau der Fall, den der Rechtsvorbehalt ausschließen soll. Richtig: PSK, den niemand kennt. Dazu die Datapath-Falle (Slave-VAP ohne vlan-id landet ungetaggt in der Bridge — dieselbe Mechanik wie der 14-h-Ausfall).

  • Idee: eine/mehrere Broadcast-only-SSIDs als „schwarzes Brett" an die Funk-Nachbarschaft — der SSID-Name ist die Botschaft (z.B. Kanal-11-bitte-freihalten, WLAN-Tuning-siehe-Portal). Optional ein offenes WLAN mit Captive-Portal auf eine statische Info-HTML (Kanalabsprache, Auto-Kanalwechsel-Bitte). Leitgedanke: wer die SSID sieht, kann auch stören → die Infoseite erreicht genau die Zielgruppe.
  • Technik (jetzt einfach, da WLAN standalone seit TP3-Rebuild): reine Namens-SSIDs = zusätzliche /interface/wifi Slave-VAPs auf wifi1/wifi2 mit eigener configuration (nur ssid=, kein Security/Datapath) ⚠️ Überholt 2026-08-19, in zwei Punkten:
    • (a) WLAN ist seit TP28 nicht mehr standalone. SSIDs entstehen am Manager crs326-core (9 configuration-, 6 datapath-Objekte, 4 provisioning-Zeilen — live gelesen 08-19). Das ist für TP13 sogar einfacher: ein Deploy statt zwei.
    • (b) 🔴 „kein Security" ist ein offenes WLAN — genau das, was der Rechtsvorbehalt unten ausschließen will. Richtig ist eine SSID mit WPA2/WPA3-PSK, den niemand kennt: sie beacont den Namen, ist aber nicht beitretbar.
    • Captive-Portal-Variante (RouterOS-Hotspot, isoliertes VLAN, walled-garden) bleibt als Option bestehen — s. Rechts-Gate unten.
  • Umsetzungs-Skizze: … Beacon-SSIDs in routeros/.secrets/ap{1,2}-prov-base.rsc ergänzen … → diese Dateien sind für den WLAN-Teil Historie (die CAPs sind auf die cap-Zeile eingedampft). Aktuelle Umsetzung: docs/tp13-botschafts-wlan-runbook.md (2026-08-19).
    • 🔴🔴 Die Falle, die dort ausführlich steht: ein Slave-VAP ohne explizite vlan-id am Manager landet ungetaggt in bridge — weil der CAP vom Manager-Datapath nur die vlan-id übernimmt. Das ist dasselbe Leck wie TP3.1 (cfg-main-2g ohne datapath) und dieselbe Mechanik wie der 14-h-Ausfall vom 08-18. Die Botschafts-SSID braucht deshalb einen Datapath mit totem vlan-id — PSK und toter Datapath, nicht eines von beidem.
    • ✅ VAP-Budget geprüft (live 08-19), unkritisch: max-interfaces=16 je Radio, belegt sind 4 (5 GHz) bzw. 5 (2,4 GHz). Die Grenze ist Airtime, nicht die Zahl — jeder VAP beacont eigenständig. Empfehlung: genau eine SSID, auf 2,4 GHz (mehr Reichweite = mehr Nachbarn, und 5 GHz bleibt für Nutzverkehr frei).
  • ⚠️ Recht vor Scharfschalten prüfen (User privat, AT): offenes WLAN + Portal berührt Impressumspflicht/Störerhaftung — reine Namens-SSIDs ohne offenen Zugang sind unkritisch (kein Fremd-Datenverkehr); im Zweifel nur die Beacon-Variante.
  • Abh.: TP3-Rebuild ✅ (Standalone-APs — SSID-Änderung = 1 Deploy/GC-Job) → TP28 ✅ (CAPsMAN auf dem Core) + TP22 ✅ (GC-Deploy-Pipeline). SSID-Änderung = ein Deploy auf den Core; Direkt-Edit am Gerät ist durch den Governance-Entscheid ausgeschlossen.

TP14.4 — Session D: CF-Access-Profil für Claude ▸ ✅ IN TP41 T4 AUFGEGANGEN (2026-09-01) — docs/tp14.4-cf-access-token-runbook.md (historisch)

✅ Erledigt über TP41 T4 (2026-09-01), nicht als eigenständiger claude-mcp-Token: der claude-mcp-Service-Token war die Basis (t4a.py), wurde → zt-svc umbenannt + rotiert und über Gruppe zt-automation konsolidiert. mcp/cloudflare/.env trägt jetzt beide Keys CF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRET (Block „TP41 T4 zt-svc"), verifiziert (test_whoami.py → admin:true, common_name=zt-svc). Handoff: handoff-20260901-tp41.md §T4. 📌 Überholt (Ist-Stand vor 09-01, bewusst erhalten): „in mcp/cloudflare/.env stehen weder CF_ACCESS_CLIENT_ID noch _SECRET — nichts halb erledigt." Das war bis zum 2026-09-01 richtig; TP41 T4 hat die Keys dann geschrieben. Abnahme-Regel bleibt gültig (mit Headern antwortet / ohne 403).

  • CF-Token „claude code mcp" hat jetzt Access: Service Tokens: Write+Read → claude-mcp rotieren (POST …/access/service_tokens/{id}/rotate) → CF_ACCESS_CLIENT_ID/_SECRET in mcp/cloudflare/.env (chmod600). Memory ref-cf-access-claude-profile.
  • 📋 Runbook: docs/tp14.4-cf-access-token-runbook.md (2026-08-19). Ist-Stand verifiziert: mcp/cloudflare/.env enthält bisher weder CF_ACCESS_CLIENT_ID noch _SECRET — es ist nichts halb erledigt. 🔴 Das Secret wird nur bei der Rotation ausgegeben, wer es nicht sofort schreibt, muss erneut rotieren. Abnahme = ein Aufruf mit den Headern (antwortet) und einer ohne (403) — der Erfolgsfall allein beweist nichts, der Hostname könnte auf bypass stehen.

TP16 — CF-Tunnel: Round-Robin killen + reziprokes HA ▸ ✅ HA WIEDER VOLL (2026-08-24) — Kern 2026-07-10 — docs/cf-tunnel-mapping.md §10 + §15, #1033

  • ✅ GEFIXT 2026-08-24 — Ursache war ein docker stop, kein QUIC-Flap. Die beiden macmini-Connectoren (cloudflared-bfnas, cloudflared-bfpve) standen auf Exited (0), beide finishedAt auf derselben Sekunde 2026-07-21T13:00:53, bei korrekt gesetztem restart=unless-stopped und http2. Fix = docker start der zwei, sonst nichts. Abnahme: alle drei bf-Tunnel je 2 Connectoren / 8 conns, und 36 Anfragen über 6 FQDNs beider betroffener Tunnel mit 530er = 0. Details §15.
    • 🔴 Die Datierung im Befund unten war falsch: degradiert seit 2026-07-21, nicht seit dem 08-16 — der 08-16 war der Tag der Entdeckung. Vier Wochen unbemerkt.
    • 🔴 Die eigentliche Lehre ist die Nachweislücke: ein Tunnel mit 1 von 2 Connectoren meldet healthy. Über den Tunnel-Status ist die Degradation unsichtbar, nur die Connector-Anzahl zeigt sie. ⇒ Check nach TP11: connectoren < 2 je bf-Tunnel = Alarm. Ein HA-Design ohne Zähler auf die Redundanz ist kein HA-Design.
  • 📜 Ursprünglicher Befund 2026-08-16 (Historie, Ursache s.o.):
    • bf-mm = 2 Connectoren (je 4 conns) ✅ · bf-nas = 1 Connector (3 conns) 🟠 · bf-pve = 1 Connector (4 conns) 🟠.
    • Bedeutung: die Dienste hinter bf-nas (gitea, grafana, pve-mcp, vw, ds-bf, nautobot) und bf-pve (pve, ccu, iobroker, z2m25, portainer, hass, ha-mcp) hängen jeweils an einem einzigen cloudflared-Prozess. Fällt dessen Host aus, sind sie weg — genau der Zustand, den TP16 beseitigt hat. Kein 530/Round-Robin-Problem (das bleibt strukturell gelöst), sondern Redundanzverlust.
    • Bekanntes Muster: identisch zum Incident vom 2026-07-16 🔴 war es NICHT — s. Fix oben. Ursprungstext: Bekanntes Muster: identisch zum Incident vom 2026-07-16 (macmini-Connectoren flappten unter QUIC → Fix TUNNEL_TRANSPORT_PROTOCOL=http2). Diesmal sind die macmini-Connectoren oben (bf-mm hat 2), es fehlt je ein Gegenstück auf NAS/pve. Nächster Schritt (eigene Session): auf NAS und pve nachsehen, welcher der beiden cloudflared-Container nicht läuft, und ob es der http2-Konventionsfall ist.
  • ✅ GELÖSCHT 2026-08-24 (User-Entscheid „alle vier"): openclaw, coolify, johnny2, TeilzeitCharlie sind über die CF-API entfernt, alle vier mit success: true. Gegenprobe: 5 Tunnel verbleiben (bf-mm/bf-nas/bf-pve je 2 Connectoren, fm und terpene-db je 1 — beide standortfremd bzw. eigenes Projekt, kein Defekt). Damit sind auch die letzten VLAN1-Origins (10.40.41.147/.148/.139/.133) aus der Cloudflare-Config verschwunden — der Rest von TP19.
  • 📜 Ursprünglicher Nebenbefund (Historie): Alt-Tunnel mit VLAN1-Origins (alle down, 0 Connectoren, daher aktuell harmlos):** openclaw (→ 10.40.41.147/.148/.139), coolify (→ 10.40.41.133), johnny2, TeilzeitCharlie. ⚠️ openclaw beansprucht n8n.emsenhuber.net, das produktiv auf bf-mm läuft — käme der Tunnel je wieder hoch, entstünde exakt das alte Round-Robin-Muster auf diesem Hostname. Empfehlung: die vier toten Tunnel löschen (user-gated, Löschen ist nicht umkehrbar) 🔵 Begründung korrigiert 2026-08-24: die Sorge „käme der Tunnel wieder hoch, entstünde das alte Round-Robin auf diesem Hostname" ist so nicht richtig — welcher Tunnel einen Hostname bedient, entscheidet das DNS, und live zeigt kein einziger CNAME auf die vier (n8n zeigt korrekt auf bf-mm). Ein hochkommender openclaw wäre inert. Der reale Pfad ist ein aktives cloudflared tunnel route dns, das den CNAME umschreibt. ⇒ Löschen bleibt richtig — als Hygiene und um genau diese Falle zu entfernen, nicht als Störungsbehebung. — gehört fachlich zu TP19 (VLAN1-Retirement: .41.133 war eines der „unidentifizierten VLAN1-Geräte" aus TP19 B1.2 — hiermit als coolify-Origin identifiziert).

✅ Umgesetzt (2026-07-10, eigene Session): reziprokes 3-Tunnel-HA live (bf-nas 70d6a4cf / bf-pve f7fdf3a6 / bf-mm 7a322261), 6 Connectoren (2/Host, je Fremdhost), 19 FQDNs Loop-Smoke 0×530, HA per Host-Ausfall-Sim bewiesen. Round-Robin-#1033 strukturell weg. As-Built = docs/cf-tunnel-mapping.md §10. Schritt 5 Access-Strang: vw bypass→geschützt via WARP — 🔴 GESTRICHEN (User-Constraint 2026-08-14, s. TP16.2). Verbleibend offen: hass/ha-mcp 400/403; gateway-dw (WG→404) klären.

  • Symptom (User): FQDNs über CF werden sporadisch / „sehr unsicher" aufgelöst = #1033-Muster zurück („jeden 3. Schuss eine Niete"). User hat alle 3 cloudflared-Images + Container aktualisiert/neu gestartet.

  • ✅ Root-Cause endgültig (User-Diagnose bestätigt): macvlan/ipvlan-Host-Isolation. Ein cloudflared-Connector ist blind für macvlan/ipvlan-Container auf seinem EIGENEN Host (Linux-Kernel-Regel), erreicht aber Fremdhost-L2 + co-lokierte Bridge-Container. Mehrere Connectoren auf EINEM Tunnel = geteilter Ingress + CF-Round-Robin → jede Origin, die auf dem Host eines Connectors liegt, fällt bei ~1/N aus. Strukturell, nicht durch „Origins fixen" heilbar. Bsp: omada .20.175/pve-mcp .20.173/gitea .20.157/grafana .20.109 = NAS-macvlan → vom NAS-Connector NICHT erreichbar.

  • Invariante: innerhalb EINES Tunnels ist kein Route→Connector-Pinning möglich (alle Connectoren teilen Ingress, CF round-robined). „Ausgewählte Routen für ausgewählte Connectoren" = getrennte Tunnel.

  • Ziel-Design (HA): reziproke Per-Host-Tunnel — jeder Host-Dienst wird von den Connectoren der ANDEREN Hosts bedient (nie vom eigenen):

    Tunnel (= Dienste eines Hosts) Connectoren auf HA
    pve-Dienste NAS + macmini 1 abdrehbar
    NAS-Dienste (omada/pve-mcp/gitea/grafana/…) pve + macmini 1 abdrehbar
    macmini-Dienste (.20.167) pve + NAS 1 abdrehbar

    3 Tunnel, jeder Host fährt 2 cloudflared → Host-Ausfall ⇒ jeder Tunnel ≥1 Connector = voll HA. Voraussetzung: jede Origin L2-erreichbar.

  • Ist-Inventory (live 2026-07-10, s. cf-tunnel-mapping.md §9): Ingress v119, 24 Hostnames, 2 live Connectoren. DOWN/stale: pbs .20.135, nginx-bf .41.123 (noch VLAN1!), hermes+hermes-geli .20.167. Zu klären (Host/Netz): vw .20.127/ccu .20.232/iobroker .20.107/z2m25 .20.106 — ggf. vw/ccu auf VLAN20-macvlan heben. gateway-dw (192.168.178.1) = WG-Sonderfall.

  • CF-Access-Strang (verzahnt, s. TP11 Track-B): hass-400 in der HA-iPhone-App = CF-Access davor (Companion-App kann Access-Login nicht) → Fix ohne Passthrough: iPhone in WARP enrollen (Device-Identity erfüllt Access; „Warp Login App" existiert bereits allow:christoph). ⚠️ vw = aktuell bypass (öffentlich!) → von bypass auf geschützt heben. 🔴 Überholt (2026-08-14): vw BLEIBT bypass — harter User-Constraint, Härtung stattdessen über TP16.2. „Passthrough-Hosts sind die kritischen" (User).

  • Umsetzungs-Reihenfolge (für die eigene Session): (1) Reachability-Matrix je Host (pve/NAS/macmini → jede Origin, read-only) BEVOR gruppiert wird; (2) Interim-Sofortfix: bf-Tunnel auf pve-only (pve erreicht alles außer gateway-dw) → 0×1033, reversibel; (3) reziprokes 3-Tunnel-HA bauen (CNAMEs re-pointen, 6 Connectoren, Token je Tunnel; NAS/macmini-Alt-Token entfernen); (4) tote Origins bereinigen; (5) Access-Strang (WARP + vw); (6) Loop-Gegentest.

  • Abh.: TP14.1/14.2 ✅. Verwandt: TP11 (CF-Tunnel-Track-B/C + Access), #1033. SSoT: docs/cf-tunnel-mapping.md. Memory project_cf_tunnel_1033.

  • ✅ Incident 2026-07-16 (User unterwegs, WARP „spinnt", keine BF-IPs mehr) ▸ Public-FQDN-Teil gefixt: macmini-Connectoren (cloudflared-bfnas/-bfpve) flappten live unter QUIC/UDP (Docker-Desktop-Bridge-NAT) → bf-nas/bf-pve von 2 auf 1 Connector degradiert (HA faktisch weg, nicht down). Fix: beide auf TUNNEL_TRANSPORT_PROTOCOL=http2 umgestellt (rollend, 0 Downtime), wieder voll HA. Konvention: macmini-Connectoren künftig immer http2. ICMP-Befund (getrennt, strukturell): TCP über die WARP-Route ging überall, ICMP nicht (fm 36 %/bf 100 %/dw 100 % Loss) — cloudflared ist QUIC-Proxy, kein WireGuard. Kurz als „bf-pve-Route wegen USV" notiert (Commit 1c1a676) → REVIDIERT (Begründung invertiert: bf-pve-Connectoren laufen auf NAS+macmini, nicht pve). Details: docs/cf-tunnel-mapping.md §13. → Richtungswechsel für Private Access = TP16.1.

TP16.2 — Härtung der öffentlich erreichbaren Dienste (bypass-Strang) ▸ 📋 KONZEPT (2026-08-14), Umsetzung offen

  • 🔴 HARTER CONSTRAINT (User 2026-08-14): vw wird NICHT WARP-/Access-geschützt. Gilt dauerhaft, nicht „vorerst". Begründung (technisch belastbar, nicht Bequemlichkeit): die Bitwarden-Clients (Browser-Extension, Mobile, Desktop) können den interaktiven Access-Login nicht durchführen — dasselbe Muster, das die HA-Companion-App auf 400 laufen lässt. Ein Passwortmanager hinter einem Client, den man im Notfall (fremdes Gerät, Neuinstallation, Handyverlust) nicht installieren kann, ist ein Aussperr-Risiko gegen den eigenen Schlüsselbund — und damit gefährlicher als das, wovor die Maßnahme schützen soll. Die frühere TP16-Schritt-5-Absicht „vw von bypass auf geschützt heben" ist damit gestrichen.
  • Ziel stattdessen: Schutz erhöhen, ohne Clients zu brechen — gilt für alle bypass-Dienste (vw, hass, gitea).
  • ✅ Schicht 0 — bereits vorhanden, nicht unterschätzen: keine offenen Ports, alles ausschließlich über CF-Tunnel (3-Tunnel-HA, TP16), TLS terminiert an der Edge, DDoS-Schutz. Der Origin ist nicht direkt erreichbar. Das ist bereits mehr als bei den meisten Selfhostern.
  • ✅ Schicht 1 — chirurgisch: Access nur auf die Pfade, die kein Client anfasst. CF Access wertet die spezifischere Application zuerst → man kann eine zweite Access-App auf vw.emsenhuber.net/admin* legen (echte Policy) und den Host im Übrigen auf bypass lassen. Das Admin-Panel wird ausschließlich per Browser bedient — dort funktioniert der Access-Login einwandfrei, während /api, /identity und /notifications (die Client-Pfade) unberührt bleiben. Kein Client bricht, das wertvollste Ziel ist weg.
    • Alternativ noch härter: Admin-Panel ganz aus (ADMIN_TOKEN leer lassen) und nur bei Bedarf temporär aktivieren.
    • Analog für gitea: /admin* + Registrierung path-scoped schützen (Git-über-HTTPS mit Token bleibt offen).
  • ✅ Schicht 2 — App-eigene Bremsen (kostenlos, sofort): Vaultwarden hat Rate-Limits per Default an (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10, dazu ADMIN_RATELIMIT_*) → Werte prüfen/verschärfen; SIGNUPS_ALLOWED=false, INVITATIONS_ALLOWED=false, SHOW_PASSWORD_HINT=false, 2FA erzwingen. HA hat ip_ban_enabled + login_attempts_threshold. Gitea: Registrierung aus.
  • ✅ Schicht 3 — Edge-Regeln (CF), wirkt für alle Dienste gemeinsam: WAF-Custom-Rules + Rate Limiting gezielt auf die Login-Endpunkte (/identity/connect/token bei vw, /auth/login bei HA, /user/login bei Gitea); optional Geo-/ASN-Filter. ⚠️ Free-Plan-Kontingente vorher im Dashboard prüfen (nicht belegt).
  • ✅ Schicht 4 — ein Mechanismus für alle: CrowdSec (oder fail2ban) mit Cloudflare-Bouncer. Bans landen dann an der Edge statt erst am Origin. Vaultwarden hat ein offizielles Fail2Ban-Setup-Wiki (Log-Muster für Login und Admin-Panel).
  • 🔴 Voraussetzung für Schicht 2–4, und der eigentliche Knackpunkt: die echte Client-IP muss am Origin ankommen. Hinter dem CF-Tunnel sieht die Anwendung sonst nur die IP des cloudflared-Containers → jede IP-Sperre bannt den Tunnel statt den Angreifer = Selbstaussperrung aller Nutzer, und Rate-Limits greifen ins Leere. Genau dieser Defekt ist bereits offen dokumentiert: HAs trusted_proxies fehlen .20.10/.20.167 (TP16, hass-400-Strang). ➡️ X-Forwarded-For/CF-Connecting-IP sauber durchreichen ist Schritt 1 dieses TPs, alles andere baut darauf auf.
  • Reihenfolge: (1) Client-IP-Weitergabe fixen (inkl. HA trusted_proxies) → (2) /admin-Pfade path-scoped schützen → (3) App-Limits + 2FA → (4) Edge-Rules → (5) CrowdSec/fail2ban mit CF-Bouncer.
  • Abh.: TP16 ✅ (Tunnel-HA). Verwandt: TP20.1 (vw wandert auf Platform-Postgres — DB-HA ≠ Access, getrennt halten), TP23 (dessen WARP-Managed-Network-Idee ist durch diesen Constraint entlastet: vw verlangt keine Device-Identity, also kollidiert „WARP zuhause aus" nicht mehr mit dem Access-Strang).

TP16.3 — cloudflared-Reduktion: was der Mesh als Datenpfad kann (und was nicht) ▸ ✅ ABGESCHLOSSEN 2026-09-01 — Flip (§5) + Abbau (§6) durch; öffentlich nur noch bf-public (+fm) — docs/tp16.3-bf-public-ap-connector-runbook.md

✅ §6 Abbau — AUSGEFÜHRT 2026-09-01 (vom User vorgezogen, „es wird nichts mehr sein")

Die 7-Tage-Wartefrist war auf User-Entscheid hinfällig. Reihenfolge eingehalten: erst Container, dann Tunnel (umgekehrt gäbe es 530er).

  • Vorbedingung geprüft: alle vier Namen auf bf-public, kein Name mehr an den Alttunneln ⇒ Abbau gefahrlos.
  • 7 Container entfernt (per exakter Namensliste, nicht per Filter — „Namen lesen, nicht Zeilen zählen"): LXC 400 cloudflared-bfmm/-bfnas, macmini -bfpve/-bfnas, NAS ep14 -bfpve/-bfmm, plus terpene-cloudflared — Letzterer auf zusätzlichen User-Entscheid, weil 420.teilzeitladen.xyz jetzt über Caddy/CF läuft. Kein Stack dahinter ⇒ kein Wiederauferstehen.
  • Beweis vor der Tunnel-Löschung: 420.teilzeitladen.xyz lieferte mit bereits entferntem terpene-cloudflared echten Origin-Inhalt (assetlinks-JSON, 200) — der Dienst hängt also nicht mehr am Tunnel.
  • 4 Tunnel gelöscht: bf-mm, bf-nas, bf-pve, terpene-db — jede ID gegen die Behalten-Liste (bf-public, fm) geprüft.
  • Schlusskontrolle: nur noch bf-public (12) + fm.emsenhuber.net (4) healthy; alle vier BF-Namen und 420 weiterhin 200.
  • ℹ️ Der terpene-db-Tunnel-Token stand kurz im docker inspect-Output — mit der Tunnel-Löschung ungültig, kein Rotationsbedarf.

🔜 Follow-up F2 — bf-public-Connector auf die APs (Runbook 2026-08-30)

Runbook: tp16.3-bf-public-ap-connector-runbook.md — Vor-Ort-Session an ap1 + ap3 (Technik UG) und ap2 (Technik OG), weil der device-mode-Wechsel eine physische Bestätigung am Gerät verlangt.

  • 🆕 ap3 ist neu und nicht baugleich (live 08-30): cAP ax (cAPGi-5HaxD2HaxD, 10.40.99.13), arm64, 96 MB Flash frei, 689 MB RAM frei. Zwei verschiedene container.npk — überholt seit dem ROS-Upgrade 08-31: alle drei auf 7.24.1/arm64 ⇒ EINE Datei (container-7.24.1-arm64.npk, 1.187.985 B — exakt die Grösse, die die APs selbst für das Paket melden, damit belegt statt angenommen). ap3 steht auf device-mode=basic (nicht home), dort ist fetch bereits erlaubt — wir nutzen trotzdem auf allen dreien den .tar-Import, damit alle dasselbe Image fahren.
  • ✅ Blocker A — Redirect-Schleife, behoben 2026-08-31. bf-public zeigt auf http://10.40.20.40:80; Caddy antwortet dort mit 308 → https://<name>/, also zurück durch Cloudflare in denselben Tunnel ⇒ ERR_TOO_MANY_REDIRECTS für alle vier Namen. Fällt heute nicht auf, weil die drei Alttunnel an Caddy vorbei direkt aufs Backend gehen — die Schleife entsteht erst mit dem Umstieg. Fix = https://10.40.20.40:443 + originServerName je Regel (über 443 mit korrekter SNI: 4× 200).
  • ✅ Blocker B — die Freigabe fehlte, angelegt 2026-08-31 über Nautobot GC. Die forward-Kette des Core wurde vollständig gelesen (36 Regeln, Nr. 35 = TP5: deny-by-default): keine Regel vlan99 → vlan20 für 80/443. Die einzige MGMT→INFRA-Regel (Nr. 26) ist ein src-port=8728-Rückweg der API. ⚠️ Der Vorgänger-Runbook behauptet das Gegenteil („Regel 27 deckt vlan99→vlan20:80 ab ✅") — Regel 27 ist IOT->INFRA http-rueckweg, in-interface=vlan30. Ohne neue Regel läuft der Connector in TP5-WOULD-DROP + Drop. Egress zu CF (Regel 13 LAN->WAN) und DNS (Regeln 30–33) sind dagegen ✅. ⇒ Das masquerade des veth-Netzes ist deshalb Pflicht, nicht Kosmetik: sonst kommen die Pakete mit 172.17.0.2 am Core an, wo weder Route noch Regel sie kennt.
  • ✅ Blocker C — ROUTER_AP3_PASS fehlte in ~/.secrets/credentials.env, nachgetragen 2026-08-31 (steht nur in ~/.claude.json), gegen die Credential-Store-Konvention. Der sshpass -e-Reboot-Weg braucht es dort. ⚠️ Nebenbefund: credentials.env lässt sich in zsh nicht sourcen — ab ~Zeile 69 stehen rohe WireGuard-Zeilen, zsh bricht ab und lässt die Variablen leer; das sieht aus wie ein ungültiger Token. Alle Snippets im Runbook laufen deshalb in bash -c.
  • 🔩 Kein PoE als Abkürzung: beide CRS326 haben nachweislich gar kein PoE-Menü (bad command name poe) ⇒ es gibt keinen Remote-Power-Cycle, die Bestätigung ist wirklich physisch.
  • 🧹 Abbau (frühestens 7 Tage nach dem Flip): Tunnel bf-mm/bf-nas/bf-pve löschen und die sechs Container entfernen — LXC 400 (cloudflared-bfmm, -bfnas), macmini (-bfpve, -bfnas), NAS ep14 (-bfpve, -bfmm); alle drei Hosts 08-30 verifiziert. Erst Container, dann Tunnel — umgekehrt erzeugt man 530er. 🚫 terpene-cloudflared (LXC 400) und fm.emsenhuber.net bleiben.
  • 🔴 Ehrliche Einschränkung (aus TP31, gilt unverändert): solange Caddy nur auf caddy-bf1 liegt, ist das Origin aller drei Connectoren derselbe Rechner — drei Connectoren kaufen Hardware-Diversität am Eingang, aber keine Verfügbarkeit. Der echte Gewinn kommt mit caddy-bf2 (LXC 131); für vw gilt dabei die TP20.1-Bremse (Schreibzugriffe auf einen read-only Standby).
  • Rollback: docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json (vier Ingress-Configs + CNAMEs inkl. Record-IDs), am 2026-08-30 neu angelegt.
  • ▶️ Schnitt ausgeführt 2026-08-24: nautobot.emsenhuber.net ist aus dem Ingress von bf-nas entfernt (8 → 7 Regeln, Catch-all weiterhin letzte). Rollback-Sicherung liegt unter docs/cf-backup/bf-nas-ingress-20260824-vor-tp16.3.json — Rückweg ist ein PUT, eine Minute. Abnahme vollständig: private IP mit WARP von außerhalb des LAN 302 in 0,16 s · die übrigen fünf bf-nas-Hostnames unverändert · und der Punkt, den man übersieht — Golden Config lief durch: Backup → Intended → Compliance, 104/104, 0 Drifts.
    • 🎯 Der Testaufbau war der einzige, der etwas beweist: die Session lief auf einem Host im Hotspot-Netz 172.20.10.13 mit aktivem WARP, also nicht im BF-LAN. Aus dem LAN heraus wäre der Zugriff ohnehin gegangen und hätte nichts gezeigt (Merkregel feedback_adresse_lesen_nicht_ansagen).
    • 🔴 Befund, der die Abnahmetabelle des Runbooks korrigiert: der öffentliche FQDN taugt nicht als Test. nautobot.emsenhuber.net liefert vor und nach dem Schnitt HTTP 302 — der kommt von Cloudflare Access, nicht vom Origin (location: emsenhuber.cloudflareaccess.com/cdn-cgi/access/…, JWT auth_status: NONE bei is_warp: true). Access greift vor dem Tunnel. ⇒ Gültiger Nachweis ist das Zurücklesen der Ingress-Config, nicht die Antwort des Frontends — dieselbe TP22/TP23-Regel.
    • 📌 Und die Erwartung wird damit ehrlicher: Nautobot war nie ohne Access-Login öffentlich erreichbar. Der Schnitt schließt keine offene Tür, er entfernt eine überflüssige zweite.
    • ⚠️ Vorprüfung, die vor jedem weiteren Schnitt zu wiederholen ist: hängt Werkzeug am öffentlichen Namen? Hier nein — gc-configplan.py spricht http://10.40.20.30:8080/api, und kein Repo-Skript nutzt den FQDN.
  • ✅ Betriebsfenster ausgewertet 2026-08-30 (Tag 6/7): nautobot bleibt mesh-only. Kein Rückweg gebraucht, kein Ausfall. nautobot.emsenhuber.net hat inzwischen auch keinen DNS-Record mehr (mit den CF-Leichen am 08-29 gelöscht, Commit 39ba5da) — der Dienst ist vollständig aus dem öffentlichen Strang heraus und ausschließlich über 10.40.20.30:8080 bzw. intern über Caddy erreichbar.
✅ §2/§3 durchgezogen — Vor-Ort-Session 2026-08-31

Ergebnis: je ein cloudflared auf ap1/ap2/ap3, bf-public healthy mit 12 Connectoren (3 APs × 4 CF-Edges, alle 2026.6.1, Egress über 90.146.33.177). WLAN auf allen drei zurück (CAPs state=Ok), Flash je AP 54–55 MiB frei.

  • 🔴 Die Platzrechnung des Runbooks war falsch — Faktor 2,5. §3.5 rechnete mit „28 MB .tar
    • ~28 MB entpackte Layer ≈ 39 MB Rest". Die 28 MB sind die komprimierte Archivgrösse; das entpackte Image misst 72.926.208 B = 69,5 MiB (docker export | wc -c). Spitzenbedarf beim Import ist Tar + entpacktes Abbild gleichzeitig ⇒ 26,2 + 69,5 = 95,7 MiB gegen 90,5 MiB frei. Das offizielle cloudflare/cloudflared-Image passt auf diese Geräte nicht — auf keinem der drei. ⚠️ docker image inspect .Size führt hier in die Irre (meldete 27.463.950); docker history summiert auf ~70 MB. Für Flash-Budgets docker export messen, nicht inspect.
  • ✅ Lösung: eigenes Minimal-Image, FROM scratch. Das Binary im offiziellen Image ist statisch gelinkt (verifiziert) und 36,8 MB gross; die restlichen ~33 MB sind distroless-Debian, das cloudflared nicht braucht. Das selbst gebaute Archiv (Binary + CA-Bundle) misst 35,3 MiB, Spitzenbedarf 70,6 MiB. Gepinnt über die Binary-SHA256 e93824f6… — cloudflared bestätigt sie beim Start selbst (Version 2026.6.1 (Checksum e93824f6…)), das ist der Reproduzierbarkeits-Nachweis. Build-Skript erzeugt das docker-v1-Archiv direkt, ohne Docker-Umweg.
  • 🔴 docker save taugt seit Docker 25 nicht mehr als Quelle. Mit containerd-Image-Store schreibt es ein OCI-Layout (blobs/sha256/, gzip-Layer); RouterOS will docker-v1 und quittiert das mit no config found in manifest — eine Meldung, die nach kaputtem Image klingt, nicht nach Format.
  • 🔴 /container/envs heisst auf ROS 7.24.1 list=, nicht name=. Das Runbook (§3.5) und das MikroMCP-Tool manage_container_env benutzen beide name= und scheitern mit unknown parameter name. Token-Übergabe deshalb per .rsc + /import (Datei danach löschen) — so steht der Wert weder in ps noch im Transkript.
  • 🔴 Der device-mode-Bestätigungs-Boot installiert KEINE Pakete. Auf ap1 wurde die .npk vor dem Setzen von device-mode hochgeladen, um Christoph einen Reboot zu sparen: der Stromzyklus hat container: yes bestätigt und die Datei verbraucht, ohne sie zu installieren — /system package print zeigte danach nur zwei Pakete. Reihenfolge ist also Pflicht: erst Paket-Reboot, dann device-mode. Nachholen ging ohne physischen Gang (SSH-Reboot).
  • 🟡 A - AVAILABLE bei /system package print ist ein Download-Katalog, kein Paket auf dem Gerät. Gegenprobe: crs326-core hat 2,9 MB frei von 16 MB, listet aber wifi-qcom (10,1 MB) und rose-storage (4,3 MB) als „available"; die Grössen sind zudem arch-spezifisch. Ein /system package enable löst also einen Internet-Fetch aus — auf ap1/ap2 per device-mode=home gesperrt. Der .npk-Weg bleibt richtig.
  • 🟡 MikroMCPs export_config liefert einen unvollständigen Export (301 B gegen 6,2 KiB bei direktem /export) — ein Rollback-Artefakt, das existiert und trotzdem leer ist. Für die Backups dieser Session wurden die MCP-Exporte verworfen und /export file=… verwendet.
  • ✅ ERLEDIGT 2026-08-31: die sechs Drifts sind ins Soll gehoben, Compliance 139/140. Entscheid Christoph: ins Template heben (nicht als Transaktion führen), und /interface veth bekommt eine eigene ComplianceRule — vorher war es das tragende Stück ohne jede Überwachung. Umgesetzt in nautobot/mikrotik_routeros.j2 (Block [7]), nautobot/ingest-ap-configcontext.py (ap.container) und neu nautobot/ingest-veth-rule.py. Kein Geräte-Write. 🔴 Der veth-Export trägt zwei gerätevergebene MACs (mac-address, container-mac-address, je AP verschieden). Ohne Vorbehandlung hätte die neue Regel auf allen drei APs Dauer-Drift erzeugt — also genau die immer-rote Zahl, die niemand mehr liest. Zwei ConfigReplace-Regeln normalisieren sie im Backup, eng auf die veth-Zeile verankert ([^\n]*? plus Lookbehind, damit VAP-/Bridge-/Ethernet-MACs nicht mitgefangen werden; Selbsttest im Skript). ⚠️ Ehrlich dazugesagt: das kostet Backup-Treue — die veth-MACs stehen in bf-golden-config nicht mehr im Klartext. Vertretbar, weil RouterOS sie beim Neuanlegen ohnehin neu vergibt; was die Regel beweist, ist die Existenz des veth mit der richtigen Adressierung. (Der ursprüngliche Befund, zur Nachvollziehbarkeit:)
  • 🟢 War: Compliance stand auf 128/135, nicht 134/135. §3.4 schreibt Direkt-Writes für veth / ip address / nat vor, während /ip address und /ip firewall unter GC-Drift-Kontrolle stehen — Runbook und Governance widersprechen sich. Folge: je AP zwei neue extra-Drifts (ip-address mit 172.17.0.1/24 interface=veth-cf, ip-firewall-nat mit dem TP31: cloudflared egress-Masquerade), also sechs. Sie sind inhaltlich richtig — die Config soll dort stehen. Der saubere Weg ist, sie ins Soll zu heben (AP-Template/ConfigContext), nicht ein Re-Baseline und nicht ein Rückbau. ⚠️ Bis dahin ist die Zahl erklärt, aber nicht in Ordnung. (Die siebte Drift, crs326-core / wifi-security / ft-over-ds, ist die bekannte und unverändert.)
  • ⚠️ WLAN-Abnahme mit Einschränkung: main (3) und iot (27) haben Clients und frische Leases; media/untrusted/guest hatten schon vor dem Umbau null Clients — für die drei gibt es keinen Lease-Nachweis, weil niemand eine zieht. Kein Rückschritt, aber auch kein Beleg.

▶️ Der grosse Schnitt — ausgefuehrt 2026-08-30 (User-Entscheid „kompletter Schnitt in einem Zug")

  • 12 Hostnames entfernt, aus Ingress und DNS, in einem Durchgang: pve · portainer · pve-mcp · ha-mcp · ds-bf · ccu · iobroker · z2m25 · pbs und die drei CF-Access-SSH-Ziele 82unkpb5 (macmini) · 3k23uow7 (NAS) · j44d6tua (pve). Ingress-Bilanz: bf-mm 6→4 · bf-nas 6→3 · bf-pve 9→2. Catch-all in allen dreien geprüft letzte.
  • pbs und die SSH-Ziele standen nicht in der Kandidatenliste — User-Entscheid 2026-08-30: beide mesh-only. PBS erreicht man über 10.40.20.135:8007, SSH direkt per IP, jeweils mit WARP.
  • ✅ Zwei Namen zunächst NICHT geschnitten: gitea-mcp und n8n-mcp — am selben Tag per F1 nachgeholt. Die TP16.3-Vorprüfung („hängt Werkzeug am öffentlichen Namen?") schlug hier an: gitea-mcp.emsenhuber.net stand in 11 Dateien (nicht 9 — beim Edit kamen zwei Worktree-Kopien dazu, s. F1) — ~/dev/{bf-network-r6,bf-network-r6.opencode,blaueblueten-n8n, dachterrasse,ha,ha-xstate,poulios-recherche,valamar}/.mcp.json plus bf-infra/opencode.jsonc — und ~/.claude.json nutzte zusätzlich n8n-mcp.emsenhuber.net. TP35 Schicht 1 hatte nur die globale ~/.config/opencode/opencode.jsonc auf LAN-IP migriert; Projekt-Ebene und ~/.claude.json waren zurückgeblieben. Regel „erst migrieren, dann schneiden" eingehalten: F1 hat migriert, danach fiel der Schnitt (Ingress bf-mm 4→2, beide DNS-CNAMEs gelöscht).
  • ✅ Abnahme (2026-08-30), beide Richtungen gemessen:
    • intern trägt weiter: AdGuard löst alle neun Namen unverändert auf 10.40.20.40 (Split-Horizon, TP31 Schritt 4), Caddy antwortet über HTTPS mit HTTP 200 (pve 0,08 s · portainer 0,03 s · ds-bf 0,91 s · z2m25 0,03 s · pbs 0,03 s).
    • öffentlich weg: pve/portainer/ha-mcp/pbs gegen 1.1.1.1 → NXDOMAIN.
    • Public-Strang unberührt: vw/hass/n8n/gitea weiterhin HTTP 200 über Cloudflare.
    • Nach TP16.3-Regel ist der gültige Nachweis das Zurücklesen der Ingress-Config, nicht die Antwort des Frontends — die Bilanz oben stammt aus dem GET nach dem PUT.
  • 📦 Rollback: docs/cf-backup/{bf-mm,bf-nas,bf-pve,bf-public}-ingress-20260830-vor-tp16.3-schnitt.json
    • docs/cf-backup/dns-emsenhuber.net-20260830-vor-tp16.3-schnitt.json (alle success:true). Ingress zurück = ein PUT je Tunnel; DNS zurück = CNAME auf <tunnel-id>.cfargotunnel.com, proxied.

🔴 Blocker: der Abbau der drei Alt-Tunnel haengt an einer Vor-Ort-Session

Der Schnitt endet hier, weil bf-public noch keinen Connector hat (0 Verbindungen, angelegt 2026-08-27) und die vier Public-Dienste weiterhin auf bf-mm/bf-nas/bf-pve liegen. User-Entscheid 2026-08-30: der Connector gehört auf ap1/ap2 (TP31-Begründung: der AP ist eine eigene Maschine, kein Docker-Host — gegenüber macvlan/ipvlan sind wir dort nicht blind).

Live gelesen am 2026-08-30 — der Preis ist nicht bezahlt:

ap1: /system package print  →  routeros 7.24, wifi-qcom 7.24   (2 Pakete, KEIN container)
ap1: /system device-mode print →  mode: home · container: no · fetch: no · scheduler: no

⇒ Es fehlt beides: das container-Paket (extra .npk, arm64) und device-mode container=yes, und Letzteres verlangt die physische Bestätigung am Gerät — ap1 Technik UG, ap2 Technik OG. fetch: no heißt zusätzlich: das Image kann der AP nicht selbst ziehen, es muss als .tar hoch und über /container add file= importiert werden. Das ist eine Vor-Ort-Session, kein Remote-Schritt. → Runbook-Bedarf als Follow-up F2; bis dahin bleiben die drei Alt-Tunnel mit je 2 Connectoren stehen.

📌 Follow-ups aus dieser Session (2026-08-30)

  • ✅ F1 — gitea-mcp/n8n-mcp FQDN→LAN-IP, DURCH 2026-08-30. Statt der geschätzten 10 waren es 12 Dateien: ~/.claude.json (beide Namen), bf-infra/opencode.jsonc, 8× dev/*/.mcp.json und zwei Worktree-Kopien in bf-network-r6{,.opencode}/.claude/worktrees/frosty-hermann-76f9cd/, die erst ein Sweep fand. Danach fiel der Schnitt (s. oben). Backups je Datei.

    • 🔑 Die zwei Clients haben verschiedene Konfigpfade — das ist die Stelle, an der man Dateien übersieht. Claude Code liest <projekt>/.mcp.json, also eine Datei pro Projekt. opencode liest eine globale ~/.config/opencode/opencode.jsonc; projekt-lokal existiert nur bf-infra/opencode.jsonc. Deshalb genügte für opencode die eine globale Migration (TP35 Schicht 1), während Claude Code acht Einzeldateien brauchte. .mcp.json ist kein opencode-Pfad.
    • 🔴 n8n-mcp lässt sich NICHT ohne Bearer betreiben. dist/http-server.js ruft process.exit(1), wenn AUTH_TOKEN fehlt („required for HTTP mode"). Damit ist er der einzige echte Zugriffs-Bearer im Setup — gitea-mcp hat gar keine Auth, alles andere (mikromcp, hostinger, portainer, github) sind Arbeits-Credentials der MCPs, kein Zugriffsschutz. Der Wunsch „Bearer für alle weg" ist also faktisch schon erfüllt, bis auf den einen, der es nicht kann.
    • 🔧 gitea-mcp auf 10.40.20.167:3000 gebunden (war 0.0.0.0:3000). Zustandslos, kein Volume, per docker run rekonstruiert; Token über die Prozessumgebung (-e VAR ohne Wert), nicht über argv — dieselbe Klasse wie die sshpass -p-Falle. ⚠️ Der Nutzen ist klein: der macmini hat nur en0, 0.0.0.0 war dort faktisch schon .167. n8n-mcp blieb deshalb auf *:3001.
    • 🔴🔴 gitea-mcp war funktionsunfähig — mit einem veralteten Gitea-Token, seit unbekannter Zeit. Der Container trug GITEA_ACCESS_TOKEN = a9317c68…, den Gitea mit 401 ablehnt; der SSoT-Store ~/.secrets/credentials.env hielt längst den gültigen (bc12ad19…, 200). Gitea selbst war die ganze Zeit erreichbar (/api/v1/version → 200). Fix: Container mit dem Store-Token neu gebaut (Token per stdin, nicht argv). Abnahme = echter API-Call user__get_my_userinfo → login=admin. ⚠️ Nicht durch den Recreate verursacht: der Token-Hash war vor und nach dem Bind-Umbau identisch. 🔑 Die eigentliche Lehre ist die Abnahme-Methode: ein initialize-Handshake beweist nur, dass der MCP-Prozess lebt — er antwortet mit serverInfo auch dann, wenn jeder echte Aufruf am Token scheitert. Die erste Verifikation dieser Session sah deshalb grün aus und war es nicht. Für MCPs mit Backend-Credential ist die gültige Abnahme ein tools/call, der das Backend wirklich anfasst — dasselbe Muster wie „frische DHCP-Lease statt Registration-Table" bei WLAN. Direkt daraus folgt F5: die Container-Envs driften vom Store, weil sie ihn nie lesen.
    • ⚪ Tote Diagnose-Permissions entfernt: blaueblueten-n8n/.claude/settings.local.json hatte 3× Bash(dig/host/curl … gitea-mcp.emsenhuber.net) in der allow-Liste (10→7 Einträge). Das waren keine MCP-Verbindungen — enabledMcpjsonServers: ["gitea-mcp"] gibt per Name frei und blieb unberührt. Projektweit geprüft: nur dieses eine Projekt betroffen.
    • 📌 Nachweis-Korrektur zur Abnahme oben: geschnittene Namen antworten öffentlich mit NOERROR + leerer Antwort, nicht mit NXDOMAIN — gegengeprüft an pve/portainer/ha-mcp/pbs aus dem großen Schnitt, die sich identisch verhalten. Gültiger Nachweis bleibt der zurückgelesene Ingress plus die leere Antwort.
    • ⚠️ Projektübergreifend → gehört ins Announcement (TP35 Schicht 4), eigene Session. Dort mitnehmen: „achte auf opencode" sowie den neuen Pfad ~/dev/bf-infra / Repo admin/bf-infra.
  • F2 — Vor-Ort-Runbook ap1/ap2: container-Paket + device-mode, cloudflared-Image als .tar, bf-public-Token, danach CNAME-Umhang der vier Public-Dienste und Abbau von bf-mm/bf-nas/bf-pve samt der sechs Container (LXC400 .126: cloudflared-bfmm+cloudflared-bfnas · macmini .167: cloudflared-bfpve+cloudflared-bfnas · NAS .10: cloudflared-bfpve+cloudflared-bfmm). ⚠️ terpene-cloudflared (LXC400) gehört zum Tunnel terpene-db und bleibt — anderes Projekt. Ebenso unberührt: Tunnel fm.emsenhuber.net (Standort FM).

  • F3 — CF-Connectoren in Nautobot abbilden (User-Wunsch 2026-08-30, „durchgehende Netzwerkdoku Stück für Stück"). Als TP37 vorgeschlagen, noch nicht angelegt — wartet auf dein Okay.

  • 🔜 F4 — n8n-mcp-Compose versöhnen (Befund aus F1, 2026-08-30; User-Entscheid: eigener Punkt, nicht nebenbei). Der laufende Container stimmt mit keiner seiner Definitionsquellen überein — ein docker compose up -d wäre deshalb destruktiv, es würde die interne n8n-Anbindung umstellen:

    Quelle N8N_API_URL Netz Port
    .env (macmini) http://10.40.41.167:5678 ← totes 41.x-Schema — 3000
    docker-compose.yml https://n8n.emsenhuber.net (hartkodiert, ignoriert die .env) web ${HOST_PORT:-3000}
    laufender Container http://n8n:5678 ← korrekt, Container-DNS n8n_default 3001

    🔴 Vorsicht — die naheliegende Lösung ist die falsche. Ein tools/call n8n_health_check (statt nur initialize) zeigt: n8n-mcp hat einen SSRF-Schutz und lehnt private Adressen ab — "SSRF protection: Private IP addresses not allowed", apiUrl: http://n8n:5678. Die Container-DNS-Adresse des laufenden Containers funktioniert also gar nicht; sie im Compose festzuschreiben würde den Zustand zementieren, nicht heilen. Umgekehrt war https://n8n.emsenhuber.net im Compose vermutlich Absicht, kein Drift — nur eben über den CF-Umweg, was der TP16.3-Stossrichtung zuwiderläuft. Ein Abschalter für den Schutz ist im Image nicht ersichtlich (nur TRUST_PROXY). F4 muss diesen Zielkonflikt lösen, nicht nur Dateien angleichen. ⚠️ Aktueller Ausfallumfang: die Doku-Tools laufen (search_nodes ✅ — das ist die Hauptfunktion von n8n-documentation-mcp), nur die n8n-Instanz-Management-Tools (n8n_*) sind tot. Zu tun: Netz als external: true auf n8n_default, N8N_API_URL erst nach Klärung des SSRF-Punkts, Image auf den laufenden Digest pinnen (sha256:2c8ff077… = v2.57.1; heute :latest + pull_policy: always, was bei jedem Recreate unkontrolliert hochzieht), Bind auf 10.40.20.167. ⚠️ N8N_API_KEY ist in allen drei Quellen verschieden; der laufende und der aus ~/.secrets/credentials.env wurden beide gegen die n8n-API getestet (HTTP 200), der .env-Wert ist ungetestet. Der Bearer dagegen ist überall identisch. Volume n8n-mcp-deploy_n8n-mcp-data überlebt einen Recreate, gitea-mcp hat gar kein Compose (reiner docker run).

  • ✅ F7 — pve-mcp-URL korrigiert (2026-08-30). Beide globalen Configs zeigten auf http://10.40.20.173:3000/**mcp**, der Server antwortet aber nur auf dem Root-Pfad (live: / → 200, /mcp → 404) — die Session meldete entsprechend ENDPOINT_NOT_FOUND. Damit widersprachen ~/.claude.json und ~/.config/opencode/opencode.jsonc der eigenen CLAUDE.md, die den Root-Pfad seit jeher korrekt nennt („Root-Pfad, NICHT /sse"). Beide auf http://10.40.20.173:3000/ gezogen; bf-infra/opencode.jsonc war bereits richtig. Merkregel: wenn ein MCP „endpoint not found" meldet, zuerst die Doku gegen die Config lesen — hier stand die Antwort seit Monaten im Repo.

  • ✅ F6 — GUI-Apps sehen exports.zsh nicht: gelöst 2026-08-30 per LaunchAgent. Aufgefallen, weil n8n-mcp beim Session-Start mit 401 abgewiesen wurde, obwohl URL und Bearer stimmten. 🔑 Ursache — und sie reicht weit über n8n-mcp hinaus: ~/.zprofile sourct exports.zsh korrekt, aber .zprofile liest nur eine Login-Shell. Claude Desktop und opencode starten gar keine Shell, sie erben die launchd-Umgebung. Damit lösen alle {env:VAR}-Referenzen zu LEER auf — live nachgewiesen: in keinem der Claude Helper-, claude- und opencode-MCP-Prozesse war N8N_MCP_BEARER gesetzt, während dieselbe Variable in einer Login-Shell mit len=64 vorhanden ist. 🔴 Das erklärt rückwirkend die Klartext-Secrets in ~/.claude.json (4 Router-PW + Hostinger-Token): dort steht Klartext, weil {env:} in einer GUI-App nicht funktioniert — vermutlich kein Schlamperei-, sondern ein Notbehelf. Die naheliegende „Hygiene"-Umstellung auf {env:} hätte mikromcp und hostinger stillgelegt. Erst jetzt ist sie gefahrlos → Voraussetzung für TP30.

    • Lösung: ~/Library/LaunchAgents/net.emsenhuber.gui-env.plist (RunAtLoad) ruft ~/dev/.bin/gui-env-sync.sh; das Skript liest exports.zsh zur Laufzeit und setzt die Variablen per launchctl setenv. Kein Secret-Duplikat — die plist enthält keine Werte, die SSoT bleibt exports.zsh, neue Variablen dort werden beim nächsten Login automatisch mitgenommen. Abnahme: 22 von 22 gesetzt, N8N_MCP_BEARER mit dem Hash des Store-Werts.
    • ⚠️ Damit ist die Konvention „niemals launchctl setenv" zu präzisieren, nicht zu brechen: verboten war der Handaufruf, weil er in keiner Datei steht und keinen Reboot überlebt. plist + Skript sind genau die Datei-Form davon. In Kauf genommen: launchctl setenv übergibt den Wert als Argument (kurz in ps sichtbar) — es gibt keine stdin-Variante.
    • 📌 Nach jeder Änderung an exports.zsh gilt: Skript erneut laufen lassen und die App neu starten — ein laufender Prozess erbt die Umgebung nicht nachträglich.
  • 🔜 F5 — Secret-Verteilung auf Container-Hosts klären (offen aus F1). ~/.secrets/credentials.env ist SSoT, liegt aber nur auf dem MacBook; auf dem macmini existiert kein ~/.secrets, und dort laufen die Container. Ein Symlink ist deshalb physisch unmöglich. Zu entscheiden: schlanke Ableitung mit nur den nötigen Keys je Host, oder Vollspiegelung. Zusammen mit TP30 lösen — dort liegt der verwandte Posten: ~/.claude.json hält 5 Secrets im Klartext (4 Router-PW, Hostinger-Token), wo die opencode-Configs an denselben Stellen {env:…} nutzen. 🔴 Falle beim Auslesen des Stores: die Zeilen tragen Inline-Kommentare (CF_MM_TUNNEL_ID=7a32… # bf-mm …). Naives cut -d= liefert einen 80-Zeichen-Wert, die CF-API antwortet dann 7003 Could not route. Ein globales set -a; source setzt die Variablen gar nicht (len=0, Grund ungeklärt). Richtig ist gezieltes Lesen mit sed -E 's/[[:space:]]+#.*$//'.

  • 📌 Ein Argument des TP ist entfallen: die HA-Degradierung, die §7 als Nebeneffekt anführte, ist am selben Tag repariert (TP16 ✅). Der Fall steht damit allein auf dem eigentlichen Argument — weniger bewegliche Teile für Dienste, die öffentlich nicht gebraucht werden.

📌 Der Schnitt (nautobot aus dem bf-nas-Ingress) ist ausformuliert inkl. Vorbedingungen, PUT-Ablauf und Rollback. Zwei Punkte stehen dort explizit, weil sie den TP sonst kippen: Mesh-Hostname-Routes sind Egress und veröffentlichen nichts (wer das verwechselt, baut den Public-Strang ab), und Nautobot ist nicht nur Web-UI, sondern die Deploy-Pipeline → zur Abnahme gehört ein Job-Lauf, nicht nur ein Browser-Aufruf. Christophs Frage (2026-08-17): „recherchiere, ob unser neuer wg-tunnel/cf mesh als datenpfad für die server/services genutzt werden kann. die containerpflicht als offizieller weg ist für mich unlogisch."

  • ✅ Die Container-„Pflicht" gibt es für den privaten Zugang tatsächlich nicht — und wir nutzen sie schon nicht mehr. CF-Docs (Mesh↔Tunnel-Vergleichstabelle) sind eindeutig: Mesh = bidirektional, L3/L4, TCP+UDP+ICMP, jeder Teilnehmer bekommt eine Mesh-IP, Connector = warp-cli oder ein nativer WireGuard-Peer (unser Fall). Tunnel = inbound zum Origin, Veröffentlichen von Hostnames, Connector = cloudflared, Protokolle HTTP/S·TCP·SSH·RDP·SMB über WebSocket geproxied. Für „Server hinter dem Node erreichbar machen, die keinen Client laufen lassen können" nennen die Docs ausdrücklich CIDR-Routes auf dem Mesh-Node als den Weg — wörtlich für „servers, databases, printers, IoT devices".
  • 🎯 Das ist bei uns seit TP16.1 bereits scharf: die vnet-Route 10.40.0.0/16 zeigt auf bf-core-mesh (§4.4, Weg A). Der gesamte private Datenpfad läuft schon über natives WireGuard auf dem Core, ohne jeden Container. Und er ist im TP5-Log-Lauf belegt, nicht vermutet: 137 Zeilen in:cf-mesh, Quelle 100.96.0.1 → Nautobot .20.30:8080 (62×), NAS .20.10:6690 (46×), macmini :22, NAS :5000/:5001/:443/:22, pve .20.100:8006, pve-mcp .20.173:3000, branch/ap1/ap2 :22. Christophs Instinkt war richtig.
  • 🔴 Wo die Container-Pflicht echt ist — und warum sie nicht willkürlich ist: für einen öffentlichen Hostname, den jemand ohne WARP im Browser aufruft, braucht es einen Reverse-Proxy an der Edge. Das ist cloudflared. Achtung Verwechslungsfalle: Mesh kennt seit 2026-07-02 ebenfalls Hostname-Routes, und die können auch public hostnames — aber das ist Egress: „route that hostname's traffic through the node and egress via the node's public IP". Es veröffentlicht nichts. Wer das verwechselt, baut den Public-Strang ab und ist überrascht.
  • ✅ Der reale Gewinn ist trotzdem groß, weil die meisten Dienste keinen öffentlichen Hostname brauchen. Kriterium: hat der Dienst einen Client, der keinen interaktiven Access-Login kann? (dieselbe Logik wie der TP16.2-Constraint bei vw)
    Muss öffentlich bleiben (cloudflared) Grund Kann auf Mesh-only
    vw Bitwarden-Clients, harter TP16.2-Constraint pve, nautobot, portainer, grafana
    hass Companion-App (400-Muster) pve-mcp, ha-mcp, gitea-mcp, ds-bf
    n8n eingehende Webhooks von Dritten ccu, iobroker, z2m25
    gitea Git-Clients / CI von außen — zu prüfen
    → Für die rechte Spalte ist der Mesh funktional gleichwertig und operativ einfacher (keine Ingress-Zeile,
    kein Connector-Paar, keine HA-Frage). Sie erreicht man ohnehin schon per privater IP.
  • 📌 Zwei Nebeneffekte, die den Fall stärken: (1) die TP16-HA-Degradierung (bf-nas/bf-pve je nur ein Connector) verliert an Gewicht, sobald nur noch die vier Public-Dienste an cloudflared hängen — weniger Dienste pro SPOF; (2) ICMP + beliebiges UDP gehen über den Mesh und über cloudflared grundsätzlich nicht (der QUIC-Proxy war genau die Lücke aus TP16.1).
  • ⚠️ Grenzen, ehrlich benannt: (a) der Mesh-Weg setzt WARP auf dem Client voraus — für Christophs eigene Geräte gegeben, für Gäste/Fremdgeräte nicht; (b) IPv6-CIDR-Routes brauchen MASQUE und funktionieren mit einem WireGuard-Device-Profil nicht — deckt sich mit dem v4-only-Entscheid aus TP16.1, wäre aber ein Blocker, falls TP12 später v6 über den Mesh wollte; (c) es bleibt eine Cloudflare-Abhängigkeit — der Mesh ersetzt cloudflared, nicht Cloudflare.
  • Vorgeschlagener Umsetzungsschnitt (klein anfangen, reversibel): einen Dienst der rechten Spalte als Probe aus dem CF-Ingress nehmen (Kandidat: nautobot — nur Christoph nutzt es, im Mesh-Log mit 62 Treffern der am häufigsten per Mesh angefasste Dienst, und ein Fehlschlag kostet nichts Produktives), eine Woche fahren, dann entscheiden. Rollback = eine Ingress-Zeile zurück.
  • 📋 Runbook: docs/tp16.3-nautobot-mesh-only-runbook.md (2026-08-19) — der Schnitt oben, ausführbar ausformuliert: Vorbedingungen (Mesh-Route, Zugriff über 10.40.20.30:8080 mit WARP von außerhalb), Ingress-PUT über CF_API_TOKEN (nicht über den CF-MCP, der ist read-only), Rollback aus der gesicherten Config.
    • 🔴 Der Abnahmepunkt, den man übersieht: Nautobot ist nicht nur Web-UI, sondern die Deploy-Pipeline. Nach dem Schnitt gehört ein Job-Lauf zur Abnahme, nicht nur ein Browser-Aufruf — der Netmiko-Deploy kommt von 10.40.20.31 (nautobot-worker), nicht von .20.30.
    • ⚠️ Beim Kürzen des ingress-Arrays muss die Catch-all-Regel die letzte bleiben, sonst greift der Ingress für alle übrigen Hostnames nicht mehr.
  • Abh.: TP16.1 ✅ (Mesh live), TP16 (Public-Strang + dessen HA-Degradierung), TP16.2 (der vw-Constraint liefert das Entscheidungskriterium). ⚠️ Berührt TP5: der Mesh-Verkehr braucht bei deny-by-default eine explizite forward-Zeile — s. TP5 Etappe 2.

🆕 Notiz 2026-08-28 — n8n-mcp Accept-Header-Strike + Auth-Sanierung

  • Live-Befund (opencode-Probing): n8n-mcp antwortet auf POST mit nur Accept: application/json mit HTTP 406 Not Acceptable: Client must accept both application/json and text/event-stream. Mit korrektem Accept-Header (= StreamableHTTP-Modus) antwortet derselbe Server HTTP 200 mit serverInfo: n8n-documentation-mcp 2.57.1. ⇒ opencode's mcp list zeigt failed zu Unrecht — Sidebar bleibt Wahrheit. Volle Diagnose im Session-Anker docs/session-20260828-decisions.md, Abschnitt „MCP-Status-Check (Cold-Start-Race + Accept-Header-Strike)".
  • Konsequenz für TP16.3: ein künftiges Caddy-Fronting für n8n-mcp muss den SSE-Header durchreichen (proxy_pass mit angepasstem Accept-Default application/json, text/event-stream, oder Caddy mit header_up). Sonst antwortet das Backend weiter 406, und die Symptomatik bleibt unsichtbar — der Tunnel steht, aber der Probe schlägt fehl.
  • Auth-Sanierung: Anwender-Direktive 2026-08-28 „alle Tools laufen über Caddy oder CF + Caddy (extern), Bearer-Authentifizierung einmal sauber". Gehört thematisch in TP16.3, nicht als neuer TP — siehe Quick-Index Zeile TP16.3. Bezug zu TP35 Schicht 2: Reihenfolge ist TP16.3 → TP35. TP35 wartet mit Schicht 2, bis das Fronting steht.

TP17.1 — Shelly Gen4 raus aus dem WLAN ▸ ❌ VERWORFEN (2026-08-23) — Weg bewiesen, Ziel damit nicht erreichbar — 📋 docs/tp17.1-shelly-gen4-funkmigration-runbook.md

❌ User-Entscheid 2026-08-23 nach dem Pilot: stoppen und zurückbauen. Auslöser war Christophs strukturelle Frage: „zigbee sollte die antwort auf die vielen radios auf -iot geben. nutzt aber wenig wenn man das wifi nicht abdrehen kann bzw. es nicht mehr aktivieren kann ohne reset."

Der Pilot hat den Weg technisch vollständig bewiesen — s9 lief auf PowerStripZB, trat ZHA bei, ein Zigbee-Befehl schaltete das physische Relais, kein Broadcast-Sturm. Und er hat gezeigt, dass das Ziel so nicht erreichbar ist:

  • Das Gerät bleibt WLAN-Client — Zigbee kommt zusätzlich, nicht statt.
  • Der Funkgewinn ist gemessen klein: die 12 Gen4 sind 12 von 26 IoT-Clients, bewegen aber je nur ~150–200 KB rx / 0,6–3 MB tx in sechs Stunden. Leerlaufende Stationen.
  • Der Preis wäre dauerhaft: die Zigbee-Firmware hat keinen AP (WiFi.GetConfig kennt nur sta/sta1/roam), Shelly macht kein Zigbee-OTA, und Shelly.InstallAlt braucht IP. WLAN aus hieße: nie wieder Firmware-Update, kein Rückweg — bei 10 Unterputz-Modulen.

✅ Rückbau durchgeführt 2026-08-23: ZHA-Knoten sauber entfernt (Leave vor dem Flash, damit kein Geisterrouter im 52-Geräte-Mesh bleibt), s9 zurück auf PowerStrip, Config aus dem Backup wiederhergestellt. Compliance des Zigbee-Netzes unauffällig: alle vier roten EZSP-Zähler blieben 0, Retry-Quote fiel im Delta auf 8,9 %. ⚠️ Beim Rückflash zog die Firmware einen OFFENEN AP hoch (ap.enable=true, is_open=true) — abgeschaltet und mit dem IoT-PSK abgesichert. Nach jedem Shelly-Firmwarewechsel den AP prüfen.

📌 Das Runbook bleibt — es enthält den kartierten Mechanismus (drei undokumentierte RPCs, Zwei-Slot-Modell), das gemessene 2,4-GHz-Bild und die ZHA-Sturmzähler mit Baseline. Der Weg ist nicht gescheitert, das Ziel war mit ihm nicht erreichbar. 🔭 Einziger Restposten: der Frühindikator — Shelly.CheckForUpdate auf einem Gen4 auf einen …MT/Thread-alt-Eintrag beobachten. Erscheint er, ändert sich die Rechnung.

Ursprünglicher Planungstext (historisch)

User-Formulierung (2026-08-23): „wir lassen das pwd vorerst wie es ist. wir werden alle shelly gen4 auf thread umstellen — neue firmware. das machen wir im ha-projekt. das sehe ich als subpunkt zu pwd ändern. der vorteil: wir müssen dann weit weniger geräte umstellen, steigern thread-stabilität und bringen zudem mehr ruhe ins wifi."

📋 Runbook: tp17.1-shelly-gen4-funkmigration-runbook.md — Bestand, die drei Wege im Vergleich, Ablauf, Abnahme, Verzahnung mit der PSK-Rotation. 📌 Wohnort ist das Netz-Projekt (User-Entscheid 2026-08-23). Eine erste Fassung lag kurz im HA-Projekt (ha-xstate); sie ist dort entfernt, damit es keine zweite Wahrheit gibt. HA ist Betroffener, nicht Eigentümer: die Entities brechen, aber die Entscheidung ist eine Funk- und Netzentscheidung. (Der Titel sagt jetzt „Thread oder Zigbee" — der ursprüngliche Thread-Zuschnitt hat den heute tatsächlich verfügbaren Weg ausgeschlossen, s. Nachtrag unten.)

  • 🔀 Die Reihenfolge ist der eigentliche Entscheid: die PSK-Rotation (Follow-up unten) wird nachgelagert, nicht vorgezogen. Begründung des Users: jedes Gerät, das vorher auf Thread wandert, muss beim PSK-Wechsel nicht mehr angefasst werden. Damit ist die Rotation Nachfolger von TP17.1, nicht mehr eigenständig. (Sie war nie dringend — Anlass war ein Passwort in einem Sitzungstranskript, kein Vorfall.)
  • 📊 Bestand live gesweept 2026-08-23 (mcp/shelly/discover.py, read-only, VLAN30): 22 von 26 erreichbar, davon 12 Gen4 (9× 2PM Gen4 S4SW-002P16EU, 1× 1PM Gen4, 2× Plug S Gen4), 8 Gen3, 2 Gen2 — alle auf Firmware 2.0.0. Nicht erreichbar: s1/s2/s3 (weihnachten_*, saisonal) und s4 kueche_mikrowelle (Metallgehäuse, RSSI −93 — der Härtefall des WLAN-Betriebs).
  • 🔴 Die Prämisse „neue Firmware" trägt so nicht, und das ändert die Rechnung: Shelly unterstützt Matter over Thread offiziell nicht und plant es nicht. Gen4 kann offiziell nur Matter over WiFi — was hier nutzlos wäre, weil das Gerät im WLAN bliebe und den PSK weiter bräuchte. Thread geht nur über Dritt-Firmware (die ESP32-C6-Hardware kann es). Das brauchbare Projekt (hencou/shelly_gen4_matter_module) deckt 1/1 Mini/1PM/2PM Gen4 ab = 10 der 12 Geräte; die zwei Plug S Gen4 fallen raus. Preis: Garantie und Shelly-Cloud weg, Rückweg nur mit UART-Vollbackup je Gerät, PM-Kalibrierung zunächst Platzhalter. ⇒ Selbst bei vollem Erfolg blieben 12 Shellys im WLAN (8 Gen3 + 2 Gen2 + 2 Plug S + die 4 ausgesteckten). Die Rotation wird kleiner, nicht überflüssig — und Gen3 kann nie folgen, dort fehlt 802.15.4. Das ist Christophs Abwägung vor dem ersten Flash, nicht die des Umsetzenden.
    • 📊 Und die Rechnung ist noch etwas ungünstiger, als „Shellys" nahelegt (live 2026-08-23): auf dirtyvader-iot hängen 26 der 27 WLAN-Clients des ganzen Netzes — darunter Drucker (P750W), Airfryer, SwitchBot, Twinkly, DTUBI und ein SONOFF-NSPanelPro, die von Thread nichts haben. Nach zehn migrierten Gen4 blieben also grob 16 Geräte auf dem IoT-SSID, nicht 12. Der WLAN-Ruhe-Effekt ist real (−38 % Clients), der PSK-Effekt ist es halb.
  • ✅ Was netzseitig schon steht und nicht nochmal gebaut werden muss: Thread-Border-Router läuft mit ~30 produktiven Matter-Knoten; der Commissioning-Weg ist erprobt — temporäre SSID dirtyvader-infra (VLAN20) nach matter-pairing-infra-ssid-runbook.md, am 2026-08-22 mit dem TIMMERFLOTTE bewiesen. cfg-infra/dp-infra/sec-infra stehen dauerhaft am Manager, das Anlernen ist ein Zweizeiler plus eine Config-Transaktion.
  • 🔴 Der Umweg bleibt nötig, weil TP12.1 offen ist (kein IPv6-Routing VLAN10↔VLAN20 → Matter- Commissioning-Etappe 2 bricht ab), und ble_proxy ist wirkungslos, seit der ESP32-BT-Proxy ausgeschlossen wurde. Je mehr Thread-Geräte kommen, desto öfter zahlt man diesen Umweg — das ist das stärkste neue Argument für TP12.1, das bisher nur an einem einzigen Fühler hing.
  • Abnahme (aus TP28 gelernt): pro Gerät ein frischer Thread-Knoten und eine funktionierende HA-Entity — nicht die Anzeige in einer Tabelle. Ein Gerät, das in Thread hängt und in HA halbtot ist, ist ein Rückschritt.
  • 🔬 Nachtrag 2026-08-23 (abends) — die Geräte selbst befragt (Shelly.CheckForUpdate, read-only). Ergebnis: es gibt heute KEIN Thread-Update, aber die offizielle Alt-Firmware-Schiene existiert — sie liefert bisher ZIGBEE.
    Was Stand am Gerät
    Stable 2.0.0 auf allen 22 — für keines ein stabiles Update verfügbar
    Beta 2.0.1-beta1 (Build 2026-08-19) für alle, auch Gen3
    Alt-Varianten (nur Gen4!) S2PMG4ZB „Shelly 2 PM Gen4 with Zigbee" · S1PMG4ZB (1 PM Gen4) · PowerStripZB (Power Strip Gen4) — je stable 2.0.0 + beta
    Gen3 (S3PL-*) kein alt-Eintrag → Alternativfunk ist eine reine Gen4-Sache, wie erwartet
    • 🎯 Das ist die wichtigste Erkenntnis für die Planung: Shelly liefert alternative Funk-Firmware für Gen4 über den normalen Update-Kanal, nicht über einen Bastelpfad. Käme eine Thread/Matter-Variante, erschiene sie mit hoher Wahrscheinlichkeit genau dort als weiterer alt-Eintrag. ⇒ Billiger Frühindikator statt Warten auf Pressemeldungen: Shelly.CheckForUpdate auf einem Gen4 pollen und auf einen …MT/Thread-Eintrag warten.
    • 📰 Presselage (recherchiert 08-23): Shellys offizielle Linie ist „wird evaluiert", nicht „ist angekündigt" — Stand Juli 2026, mit Flash-Platz als genanntem Engpass. Ein Release-Datum gibt es nicht. (Die Thread-Firmware, die im Mai 2026 als „für Gen4 verfügbar" durch die Presse ging, ist die Dritt-Firmware von automatous-io, nicht Shelly.)
    • 💡 Und daraus folgt eine Option, die vorher nicht auf dem Tisch lag: ZIGBEE statt Thread. Sie erfüllt alle drei Ziele von TP17.1 — Geräte raus aus dem WLAN (PSK-Rotation wird billiger), Mesh-Stabilität, Funkruhe im 2,4-GHz-WLAN — und zwar heute, mit Hersteller-Firmware über den offiziellen Kanal, also ohne Garantieverlust, ohne UART-Backup, ohne Test-VID. Die Infrastruktur steht bereits: Zigbee2MQTT läuft als LXC 112 (10.40.20.106, z2m25). ⚠️ Zu prüfen, bevor das zur Empfehlung wird: (a) ob die Alt-Firmware genauso zurückrollbar ist wie ein normales Update (Wechsel des Funkstacks, nicht nur der Version) — offen; (b) der Entity-Bruch ✅ entschärft (User 2026-08-23): der Umbau behält die Entity-Namen — damit fällt der teuerste Posten der Abwägung weg, für Zigbee und Thread gleichermaßen (zwei Fallen dazu im Runbook: die alte entity_id ist erst nach dem Entfernen frei, und unique_id/Statistiken sind trotzdem neu); (c) Kanalplanung ✅ erhoben 2026-08-23 — es gibt drei 802.15.4-Netze, nicht eines: Thread 15 (~30 Matter-Knoten), ZHA 20 (52 Geräte, SONOFF Dongle-M über socket://10.40.30.137:6638), Z2M 25 (Sonoff Dongle Plus). Gegen WLAN ap1 ch1 / ap2 ch11 ist das sauber — außer ZHA ch20 (2450), das direkt auf der Unterkante von WLAN ch11 (2451) sitzt, ausgerechnet 52 Geräte gegen den AP mit 26 Clients. Die Lücke 2423–2451 (ZB 16/17/18) ist unbenutzt; ⚠️ ein ZHA-Kanalwechsel müsste von 52 Geräten mitgemacht werden, schlafende und ältere Router folgen nicht zuverlässig → erst Energy-Scan, dann entscheiden. 🔴 Und der Vorbehalt, der schwerer wiegt als alle drei: Christophs Z2M-Vorfall von 2024 („Lichterdisko", Installation unsteuerbar, Neuaufbau). Fehlerklasse ist benennbar (Broadcast-/Routing-Sturm — trifft ZHA genauso), Root Cause nicht mehr rekonstruierbar. ⇒ Mitigation ohne Ursachenkenntnis = Blast Radius trennen: die zehn neuen Router komplett in ein Zigbee-Netz, und zwar in das, das nicht Licht und Fernbedienungen trägt. Details und Abbruchkriterium im Runbook.
  • 🩺 HA-Gegencheck 2026-08-23: genau EIN Shelly ist offline — s4 kueche_mikrowelle (23 Entities unavailable). Deckungsgleich mit dem Netz-Sweep, und es ist dasselbe Gerät, das bei RSSI −93 hinter Metall hängt. s1/s2/s3 (weihnachten_*) sind in HA nur DEAKTIVIERT, nicht gelöscht (Christoph 2026-08-23) — deaktivierte Entities haben keinen Zustand und tauchen deshalb in keiner Zustandssuche auf. Die Entities gibt es also zu Recht; sie kommen mit den Geräten zurück. (Meine erste Formulierung „existieren gar nicht" war falsch.) ⇒ Für die PSK-Rotation heißt das: drei Geräte, die niemand vermisst, aber die beim nächsten Einstecken mit dem alten PSK kommen. Genau dafür ist die Übergangs-access-list da.
  • 🖨️ Es gibt ZWEI Drucker — geklärt 2026-08-23, beide Angaben waren richtig: (1) P750W = Etikettendrucker, VLAN30 (10.40.30.5, Reservierung auf dhcp-v30, ARP vlan30; am 08-23 aus, last-seen 3d15h — deshalb nicht unter den 26 assoziierten Clients). (2) Brother MFC = der eigentliche Drucker, VLAN10 — live bestätigt 10.40.10.6, MAC 30:05:5C:78:86:20 (OUI Brother Industries), ARP delay = aktiv, statisch konfiguriert, keine DHCP-Reservierung. 📌 Folge für TP5: die Merkregel „Drucker fällt mit udp/161 (SNMP) aus, nicht mit 631" (ref_printer_snmp_before_ipp) gilt dem VLAN-übergreifenden Fall. Der MFC steht im selben VLAN wie die Macs — dort greift die forward-Kette gar nicht.
  • 🔀 Reihenfolge revidiert (User 2026-08-23): PSK-Rotation und Thread-Umstellung laufen PARALLEL. Sie blockieren einander nicht — der Passphrase-Override je MAC (/interface wifi access-list, s. Follow-up) macht den Flag-Day überflüssig: neuer PSK aufs Profil, die noch nicht migrierten Geräte übergangsweise per MAC mit dem alten weiter. Einzige Regel, damit die Parallelität nichts kostet: ein Gerät, das ohnehin auf Thread/Zigbee wandert, bekommt den neuen PSK gar nicht — es kommt auf die Override-Liste und verschwindet später ganz. (Die frühere Formulierung „blockt die Rotation" war zu streng; die Reihenfolge war nie technisch nötig, nur ökonomisch.)
  • Abh.: keine harten fürs Netz. Läuft parallel zur PSK-Rotation (User-Entscheid 08-23, vorher als Blocker geführt). Verwandt: TP12.1 (IPv6/Matter — profitiert direkt), TP25 (jedes Gerät weniger im 2,4-GHz-Band ist Funkruhe), TP17 im Archiv (Migrationshistorie + mcp/shelly/-Werkzeug).

TP17.3 — Shelly-Regression nach der VLAN30-Migration ▸ ✅ ERLEDIGT (2026-08-23, Folge-Session): beide Geräte repariert, Ursache eco_mode bewiesen — offen nur noch der Flotten-Entscheid (gehört TP17.1) — aus TP17 herausgelöst, User-Meldung

User-Meldung 2026-08-23: „vor einiger zeit hast du an den shellys änderungen vorgenommen. das hatte bei einigen einen bösen seiteneffekt: lüfter-shelly musste ich doppelt drücken. der übelste ist jedoch der von diele ug: seit deinen änderungen spinnt er herum und blinkt wie ein wilder. sobald AP on ist, ist die symptomatik weg. ich muss vermutlich eh alle shellys neu machen."

  • 🔴 Erste Konsequenz, sofort: der aktivierte AP auf s23 ist ein WORKAROUND, kein Rest. Ich hatte ihn beim WLAN-Scan am selben Tag als „übrig gebliebenen Anker aus TP17, gehört abgeschaltet" eingeordnet — das war falsch, und es hätte Christophs Notbehelf zerstört. Er ist bewusst an. Nicht abschalten, bevor die Ursache behoben ist. (Merkregel: ein Zustand, der nach einem Verfahren „übrig" aussieht, kann eine Reparatur sein. Vor dem Aufräumen fragen, wozu er da ist.) ✅ Aufgelöst 2026-08-23: die Ursache ist behoben, der AP auf s23 ist abgeschaltet und wird nicht mehr gebraucht. Der Workaround hat exakt das getan, was die Hypothese vorhersagte.
  • Symptome und Geräte:
    Gerät Modell Symptom
    s23 dieleug-wohnzimmer_licht .203 SPDM-002PE01EU, app ProDimmerx = Phasenanschnitt-Dimmer (Diele UG/Licht + Wohnzimmer/Seitenlicht) „blinkt wie ein wilder", AP an ⇒ weg
    s26 bad_luefter .233 S4SW-002P16EU (Gen4), switch:0 in_mode=momentary muss doppelt gedrückt werden

Was die Migration am Gerät wirklich verändert hat (TP17, 2026-07-13)

Vier Dinge, nicht nur die WLAN-Config: (1) STA-SSID/PSK · (2) statische IP statt DHCP (ipv4mode=static, gw/dns 10.40.30.1 — an allen 22 bestätigt) · (3) eco_mode nach einer RSSI-Politik · (4) AP-Anker an/aus.

🎯 ✅ BESTÄTIGTE URSACHE: eco_mode, nicht die WLAN-Config

  • eco_mode senkt den CPU-Takt und die WLAN-Sendeleistung — so steht es in TP17 selbst.
  • Ein Phasenanschnitt-Dimmer braucht präzises Nulldurchgangs-Timing. Ein gedrosselter Takt erzeugt Jitter in genau diesem Timing ⇒ Flackern. Das ist s23.
  • Warum „AP an" heilt: ein aktiver Access Point muss beaconen, das Funkteil darf nicht in die Sparzustände. Der AP hebt also faktisch den Energiesparbetrieb auf — er behandelt das Symptom genau dort, wo die Hypothese die Ursache vermutet. Diese Korrelation ist das stärkste Indiz.
  • s26: momentary-Eingang + gedrosselter Takt ⇒ die erste Flanke wird verschluckt ⇒ „doppelt drücken".
  • ✅ Gegenprobe im Bestand: s12 dieleug_steckdosen hängt am selben Ort, ist dasselbe Gen4-Modell wie s26 — und hat eco_mode=false. Es ist unauffällig. (Kein Beweis, aber die Hypothese überlebt ihn.)
  • ✅ Die Alternativerklärung ist damit erledigt: der Verdacht auf gerätespezifische Dimmer-Themen (Leading/Trailing Edge, Kalibrierung, Mindestlast/LED-Treiber) hat sich nicht bestätigt — eco_mode allein genügte. Nebenbei: die Config dieses Geräts kennt für Phasenan-/-abschnitt ohnehin keinen Parameter; Stellschrauben wären nur warmup, range_map, gamma, min_brightness_on_toggle gewesen. Der Link war mit −54 dBm die ganze Zeit sehr gut, ein reines Funkproblem war nie plausibel.

Der diskriminierende Test (klein, reversibel, ein Gerät) — ▶️ AUSGEFÜHRT 2026-08-23

  1. ✅ s23: Sys.SetConfig {"config":{"device":{"eco_mode":false}}} — AP zunächst AN gelassen. cfg_rev 91 → 92, eco_mode=false gelesen.
  2. ✅ Christoph beobachtet: „licht in diele ug ist jetzt flackerfrei". ⚠️ Das ist noch kein Nachweis — mit AP an war es vorher schon flackerfrei. Beide Hypothesen sagen an dieser Stelle dasselbe voraus. (Merkregel feedback_abnahme_wirkung_nicht_anzeige.)
  3. ✅ AP aus (WiFi.SetConfig ap.enable=false) — damit war der Test scharf: eco_mode=false, ap.enable=false, STA got ip 10.40.30.203 bei −54 dBm.
  4. 🎯 ERGEBNIS — Christoph, 2026-08-23: „beide lampenausgänge getestet. funktioniert perfekt, kein flackern." Genau die Konstellation, die vorher geflackert hat, ist ohne AP stabil. ⇒ eco_mode war die Ursache. Hypothese bestätigt, s23 repariert.
  5. ✅ s26 bad_luefter analog: eco_mode=false gesetzt (cfg_rev 18 → 19) + Reboot (s.u.). 🎯 ERGEBNIS — Christoph: „lüfter perfekt." Der Doppeldruck ist weg. ⇒ Beide gemeldeten Geräte repariert, beide Symptome durch dieselbe Ursache erklärt.

🔴 Korrektur einer Annahme aus dieser Session: ich hatte gewarnt, eco_mode brauche einen Neustart, um zu wirken, und der Test sei deshalb unvollständig. Falsch — das Gerät antwortet auf Sys.SetConfig selbst mit {"restart_required": false}. Der Test war richtig formuliert, ein Reboot des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin nicht ausgeführt. (Und daraus folgt der wichtigere Schluss: eco_mode kann das restart_required der 14 Geräte unten nicht verursacht haben.)

Weitere Funde aus dem Sweep vom 2026-08-23 (read-only, alle 22 Geräte)

  • 🔴 Die eco-Politik ist damit nicht nur stale, sondern WIDERLEGT. Regel von 2026-07-13 war „eco an bei RSSI ≥ −80, aus darunter" — sie hat an s23 realen Alltagsschaden erzeugt und war nie konsistent umgesetzt. Ist-Stand des Sweeps 2026-08-23 (22 Geräte, live): eco AUS: s7 (−90), s12 (−64), s17 (−76), s18 (−81), s19 (−88) · eco AN: alle übrigen 16, darunter s20 (−85) und s21 (−85) — die RSSI-Regel ist an keiner Kante mehr erkennbar. Die Werte haben sich seit Juli durch TP25/TP28 ohnehin verschoben. Die in TP17 vorgemerkte „2. Evaluation nach TP3.3" hat nie stattgefunden. ⇒ ✅ ENTSCHIEDEN UND AUSGEFÜHRT 2026-08-23. User: „eco-mode für alle deaktivieren. der rotz macht nur probleme." 13 Geräte umgestellt (s5, s6, s8, s10, s11, s13, s14, s15, s16, s20, s21, s22, s25), null Fehler; 9 standen bereits auf false. Stand jetzt: eco_mode=false auf allen 22 erreichbaren Shellys, einzige Ausnahme s24 (SNDC-0D4P10WW meldet eco_mode: None — nicht unterstützt). Offen nur s1/s2/s3 (weihnachten_*, saisonal stromlos). 🔧 Werkzeug: mcp/shelly/migrate.py eco <range> [--go|--on] — neu, über denselben _rpc-Pfad, mit Retry je Gerät (ein Timeout an einem schwachen Link darf den Sweep nicht abreißen) und einer Fehlerliste am Ende.

    🔬 Und die Gegenmessung widerlegt meine eigene Warnung. Ich hatte hier notiert, eco aus hebe die Sendeleistung an und sei deshalb eine Airtime-Änderung, die TP25 berühre. Gemessen stimmt das nicht: Signal der 13 geänderten Geräte in der Registration-Table des Managers, vorher/nachher — Mittel −0,9 dB, Median −1 dB, Spanne −3..+3, bei einer Kontrollgruppe von 13 unveränderten Geräten mit +0,0 dB und derselben Streuung. Das ist Rauschen, kein Effekt. ⇒ Die Aussage aus TP17, eco_mode senke „den CPU-Takt UND die WLAN-Sendeleistung", stimmt nur zur Hälfte. Der Takt-Teil ist durch das Dimmer-Flackern bewiesen; der Sendeleistungs-Teil ist an diesen Geräten nicht messbar. Kein Funkbild-Argument mehr für oder gegen eco.

  • ✅ AP-Anker-Sweep erledigt (der offene read-only-Punkt aus der Übergabe): WiFi.GetConfig → ap.enable über alle 22 Geräte. Nur s23 hatte einen AP an (= Christophs Workaround, inzwischen abgeschaltet), die übrigen 21 stehen auf false. Kein Aufräumbedarf, kein weiterer Anker im Bestand.

  • 🔵 Nebenbefund, gehört nicht hierher, aber nicht verlieren: alle Shellys stehen auf sys.sntp.server = time.cloudflare.com. Kanonisch ist seit 2026-08-18 der Core als Zeitquelle, ausgeliefert per DHCP-Option 42 — die Shellys haben aber aus TP17 statische IPs und sehen die Option nie. Funktioniert (Uhrzeit stimmt, udp/123 aus VLAN30 geht raus), ist aber ein blinder Fleck in der NTP-Vereinheitlichung. Gleiche Klasse für jedes statisch adressierte Gerät im Netz.

  • 🔵 inventory/bf.csv ist bei s12 stale: dort steht noch 10.40.41.219, live ist das Gerät auf 10.40.30.219 (migriert). Die drei weihnachten_* (s1–s3) sind saisonal stromlos, s4 kueche_mikrowelle offline — das erklärt 26 Einträge vs. 22 erreichbare.

  • 🟠 restart_required = true — nicht auf 2 Geräten, sondern auf 14 von 22 (Sweep 2026-08-23, Folge-Session): s5, s6, s7, s10, s12, s13, s16, s17, s18, s19, s20, s21, s24, s26. Uptimes 22–41 Tage. ✅ Entwarnung, empirisch: s26 wurde kontrolliert neu gestartet und die Config davor/danach verglichen — sie ist identisch. Der Flag stand für keine abweichende Konfiguration; er ist ein sticky Bit, das nur nie zurückgesetzt wurde. Die Geräte sitzen nicht auf versteckten Änderungen. Reboots können damit gefahrlos gebündelt mit TP17.1 laufen.

  • 🔍 Was restart_required gesetzt hat, ist am Gerät NICHT rekonstruierbar — drei Wege zu Ende gegangen: (a) 169 RPC-Methoden auf s26, keine gibt Auskunft über Pending-Zustand; (b) der Config-Diff über die Gruppengrenze trägt nicht — die Gen3-Familie S3PL-00112EU hat identische Configs, davon s6/s7/s10 flagged und s8/s11/s14/s15 sauber; (c) kein persistentes Gerätelog (sys.debug.file_log.enable=false, /debug/log ist ein Live-Stream ohne Ring-Puffer). Ausgeschlossen sind immerhin matter.enable, ble.rpc.enable, cloud, mqtt, ws (trennen die Gruppen alle nicht) — und eco_mode (s.o.). Nicht erneut untersuchen; der Aufwand ist gedeckelt und das Ergebnis negativ.

Scope / Abgrenzung

  • Gehört hierher: alles, was die Migration am Gerät verstellt hat (eco_mode, statische IP, AP-Anker) und was daraus an Fehlverhalten folgt.
  • Gehört nicht hierher: die Funkumstellung selbst (TP17.1, verworfen) und das Geräte-Passwort (seit 2026-08-24 TP30 Posten #3, vormals TP17.2).
  • 🔗 Aber die Verzahnung mit TP17.1 ist der Hebel: wenn ohnehin alle Shellys angefasst werden, ist das der Moment, eco_mode grundsätzlich zu entscheiden statt gerätweise zu pflegen. Christophs „ich muss vermutlich eh alle shellys neu machen" ist damit nicht Resignation, sondern der effiziente Weg — nur eben nach dem Test, nicht davor.
  • Governance: Shellys laufen nicht über Nautobot GC (kein Netzgerät im GC-Sinn). Änderungen über mcp/shelly/ bzw. direkte RPC. ⚠️ migrate.py hat kein Kommando für eco_mode — der Sweep-Weg wäre eine kleine Ergänzung über denselben Pfad.
  • Abh.: keine. Blockt nichts. Der Alltagsschaden ist seit 2026-08-23 behoben.

📌 Was noch offen ist (Stand 2026-08-23, Folge-Session)

  1. ✅ Tastentest s26 — erledigt, Doppeldruck weg.
  2. Flotten-Entscheid eco_mode aus für die übrigen ~20 Geräte — gebündelt mit TP17.1 wegen der Funkbild-/Airtime-Wirkung (s.o.). Gehört Christoph.
  3. migrate.py um ein eco-Kommando ergänzen, wenn 2. beschlossen wird — heute gibt es dafür keinen Pfad im Werkzeug, der Sweep lief ad hoc.
  4. Reboots der verbleibenden 13 restart_required-Geräte — gefahrlos (empirisch an s26 belegt), deshalb kein eigener Termin, sondern Mitnahme bei TP17.1.

TP18 — Souveräner Objektspeicher (Garage, Multi-Site, S3-Cold-Tier) ▸ 🔜 KONZEPT (2026-07-12)

  • Auslöser: 9-TB-Syno-Backup-Volume zu 2/3 voll; reine Daten ~3 TB, Tendenz steigend, „schlagartiger" Bedarf nicht ausgeschlossen. „Wenn skalieren, dann jetzt Syno-Nachfolger mitdenken."
  • Kriterien (User-priorisiert): (1) keine Fremd-Abhängigkeit / kein Cloud-Lock-in (Cloud-Opex abschreckend); (2) Multi-Site BF/FM/DW; (3) HA (Standort-Verlust überlebbar); (4) billig/teuer trennen — Variante „kalt = Objektspeicher, heiß = lokal" genügt (kein Hot/Cold innerhalb des Stores nötig); (5) horizontal + vertikal skalierbar; (6) S3-API als Branchenstandard, den jedes Backup-Tool spricht.
  • Entscheid Engine = Garage (Deuxfleurs, Rust): einziger Kandidat, der geo-verteilt über offenes Internet/WAN latenztolerant ist (CRDT-Metadaten, eventually consistent) → passt aufs 3-Keller-Modell BF/FM/DW. Ceph verworfen (will Sub-ms-LAN, Single-Cluster-über-WAN = Anti-Pattern; zu schwer, ~1 GB RAM/TB). MinIO (AGPL + Console/Feature-Abbau 2025) und SeaweedFS (kann Hot/Cold + Erasure-Coding, aber komplexer, Multi-Site async) = Fallback, falls später feingranulares Tiering / EC-Effizienz bei zweistelligen TB dominant wird.
  • Entscheid Disk-Layout = JBOD, KEIN RAID. Redundanz gehört in eine Schicht: Garage RF=3 über 3 Sites ist die Redundanz. RAID 1 darunter = Doppelt-Bezahlen (3 Sites × 2 Mirror = 6× roh für 1× nutzbar, absurd). RAID 0 verworfen (1 Platte tot = ganzer Node-Rebuild über WAN). JBOD = jede Platte eigenes Garage-Data-Dir (v1 Multi-HDD): Failure-Domain = 1 Platte, Rebuild nur deren Anteil, kein Kapazitätsverlust. Merksatz: distributed store = „just a bunch of disks", Redundanz ausschließlich über die Sites.
  • Was man dafür bezahlt: Plattentod triggert Re-Replikation über WAN (durch Upload-BW gedeckelt). Bei ~3 TB/Site tolerabel; wenn Bedarf „schlagartig" auf zweistellige TB springt → mehr/kleinere Platten (Blast-Radius) oder 4. Node, ggf. Wechsel auf SeaweedFS-EC.
  • Dimensionierung (Planungsanker 10 TB nutzbar; heute 3 TB unique / 6 TB Backup-used): RF=3 → 30 TB roh gesamt = 1 volle Kopie ~10 TB/Site (provisionieren ~16 TB roh/Site: 2 Platten = halber Blast-Radius + Headroom). Linear skalierend.
  • Kostenmodell (Annahmen: HDD neu CMR ~€18/TB · recert ~€10/TB · Strom €0,25/kWh · Lean-Node Mini-PC/pve 12 W + 2 HDD ~20–28 W):
    • Capex kumulativ (3 Sites): Platten €540 (recert) – €900 (neu) + Nodes €400 (BF=pve €0; FM/DW je Mini-PC €200) = **€600–1.300**. Vergleich: ein resilienter Syno-Nachfolger für 10 TB = €1.000–1.600 capex ohne Geo-HA → Garage-3-Node gleich teuer/billiger mit HA.
    • Opex kumulativ: ~€135–185/J Strom — aber getrennt gerechnet: FM/DW laufen über fremde Zähler, auf Christophs Zähler landet nur der BF-Node ≈ €50–60/J (Gegenleistung: er hostet deren Kopie, reziprok wie TP16).
    • JBOD-vs-RAID1-Ersparnis: Strom bloß €9–22/J (Christophs Zähler) → kein Entscheidungsgrund; der Hebel ist Capex ~€900 (nicht gekaufte Spiegelplatten) + architektonische Korrektheit. (Reicht nicht für ein BW-Upgrade €120–240/J, taugt aber als Seed für HW-Rückstellungen.)
  • Konzeptioneller Bruch (Syno-Nachfolger): Kein großes resilientes NAS mehr nötig — die kollektive 3-Site-Redundanz ist die Garantie. Syno-Rolle splittet: Hot/SMB/VM-Disks bleiben lokal (Syno oder ZFS auf pve, klein/schnell); Backup/Cold wandert in Garage über BF/FM/DW → das 9-TB-Volume wird aufgelöst statt ersetzt (aus 1 SPOF werden 3 geo-redundante Kopien). Backup-Tool (restic/Kopia) sprechen S3 nativ.
  • Impacts auf Bestand (Kurz): (a) Storage-Redundanz-Philosophie kippt — weg von SHR/RAID pro NAS, hin zu 3× JBOD; (b) Backup-Kette Hyper-Backup/SMB → S3-Bucket umstellen; (c) Container-Host-System (TP14) kriegt Garage als LXC/Container je Site, aber auf rohen Platten, nicht auf bestehenden SHR-Volumes; (d) Upload-BW wird Durchsatz-/Rebuild-Limit; (e) HW-Planung verzahnt mit TP15 (Node-HW, pve-Standort) und TP24 (Stromversorgung je Site), FM/DW brauchen je 1 Node.
  • Startpfad (inkrementell/reversibel): ① BF zuerst — Garage-Container auf pve, 2 HDDs JBOD, Backups lokal auf Bucket (sofort Nutzen, noch keine Redundanz). ② FM/DW nachziehen (Mini-PC + 2 Platten), RF→3, ab jetzt geo-redundant+HA. ③ Syno entlasten (Backup-Last abgeben).
  • Offen (Modell scharfstellen): realer Strompreis; ob FM/DW eigene Nodes stellen oder Christoph HW hinstellt; Backup-Tool-Wahl (restic vs Kopia); ob heißes Zeug künftig auf ZFS/pve statt Syno.
  • Abh.: TP10 (WG-Multi-Site = Transport, Upload-BW-Constraint), TP14 (Container-Hosts), TP15 (Node-HW) / TP24 (Stromversorgung), Muster von TP16 (reziprokes Per-Host-HA). Kein Blocker für laufende TPs. SSoT dieser Session.

TP20.1 — Vaultwarden → Platform-Postgres + Cross-Site-HA ▸ 🔜 KONZEPT (2026-07-14, ausgelagert aus TP20)

  • Ziel: vw (.20.127, heute MariaDB) auf den Precious-Tier holen und das eigentliche User-Ziel erfüllen: DB-Replikation über 2 Sites (Standort-Verlust überlebbar).
  • Phase 1 — Migration (single-site): vw MariaDB→Postgres (Export → Import in Tier-1-DB vaultwarden → DATABASE_URL umstellen → verifizieren). Danach ist vw precious + im pgBackRest/PITR-Schirm von TP20. (Alternative verworfen: vw auf MariaDB lassen + MariaDB-Repl — zugunsten EINER Replikations-/Backup-Mechanik im Precious-Tier.)
  • Phase 2 — Cross-Site-Standby: db-platform-standby-LXC am 2. Standort → pg_basebackup/Streaming-Replication über WG (TP10) → manuelles Promote-Runbook. Repliziert den ganzen Tier-1-Cluster (vw = HA-Ziel, Nautobot bekommt Gratis-DR-Kopie). Patroni + 3. Witness nur, falls später Auto-Failover statt manuellem Promote gewünscht.
  • Verwandt/Abgrenzung: vw ist auch in TP16 offen (CF-Access: bypass→WARP-geschützt) — DB-HA ≠ Access, getrennt behandeln. Reziprokes 2-Site-Muster analog TP16/TP18.
  • Abh.: TP20 (Tier-1 Postgres muss stehen), TP10 (WG-Transport für Phase-2), TP18 (Backup-Schirm). Kein Blocker für TP21.

TP23 — DNS-Policy & Self-Service-Ausnahmen ▸ ⏳ STUFE 1 DURCH, STUFE 2 VORBEREITET (2026-08-18)

  • ✅ Stufe 1 — DoH auf dem Core scharf (routeros/tp23-stufe1-doh-core.rsc), schliesst zugleich TP21.1 TODO 1. use-doh-server=https://px35ynlol3.cloudflare-gateway.com/dns-query, verify-doh-cert=yes. servers=1.1.1.1 bleibt als Bootstrap fuer den DoH-Hostnamen stehen.
    • 🔑 Die alte Begruendung „core hat kein CA-Bundle, verify also unmoeglich" war zu pessimistisch. Es braucht kein Bundle, sondern genau einen Root: die Kette ist Leaf → GTS WE1 → GTS Root R4, importiert wurde nur R4 (~1 KB, gueltig bis 2036) — bei 2,8 MB freiem Flash ist das der Unterschied zwischen geht und geht nicht. Fingerprint lokal und am Geraet gegengeprueft (349dfa40…3c7d).
    • 📌 Der Zertifikatsimport steht bewusst nicht im GC-Template: er braucht die Datei am Geraet und ist einmaliger Bootstrap, kein Sollzustand. Im Template steht nur die /ip dns set-Zeile.
    • ⚠️ Der Deploy meldete FAILURE, obwohl alles sass. /certificate import gibt failed: 0 aus, und der Job scannt den Geraete-Output stumpf nach dem Wort. Wieder ein Beleg fuer die TP22-Regel: Job-Status ist kein Wirksamkeitsnachweis, nur die Gegenpruefung am Geraet zaehlt.
  • ⏳ Stufe 2 — AdGuard eingerichtet, aber bewusst NICHT scharf geschaltet (User-Vorgabe 2026-08-18). adguard_bf1 (10.40.20.16) macht bereits DoH zum selben CF-Gateway. Ergaenzt wurden: conditional upstreams [/bf.lan/]10.40.20.1 und [/40.10.in-addr.arpa/]10.40.20.1 (ohne die zweite Zeile wuerden interne Reverse-Lookups zu Cloudflare abfliessen), sechs CIDR-Client-Gruppen (ein VLAN = eine Gruppe) und die permanente Ausnahmegruppe dns-bypass-exempt (aktuell 10.40.10.183). Abnahme per dig: core.bf.lan → 10.40.99.1, nas.bf.lan → 10.40.20.10, extern ueber DoH. Backup AdGuardHome.yaml.bak-tp23-20260817.
    • Kein DHCP-Scope wurde angefasst — alle sieben liefern weiter 10.40.X.1 aus. Das Scharfschalten ist damit spaeter ein reiner DHCP-Change; die passende Firewall-Zeile (TP5-E2: LAN->AGH dns (Vorrat)) steht bereits, damit dabei die scharfe Firewall nicht angefasst werden muss.
    • ⚠️ AGH schreibt die YAML beim Start neu (eigene Schluesselreihenfolge, Kommentare weg, time_zone: Local → UTC). Wer die Datei patcht, muss danach am laufenden Stand pruefen, nicht am eigenen Entwurf.
  • 🔜 Offen bleiben Stufe 3–6 (zweite AGH-Instanz, per-Lease Option 6, HA-Buttons, und zuletzt die Bypass-Sperre). Ausdruecklich keine HA/zweite Instanz (User 2026-08-18).
  • Auslöser: sobald gefiltertes DNS scharf ist, gehen einzelne Seiten/Dienste kaputt. Es braucht Ausnahmen ohne Admin-Eingriff — per User/Client/Gruppe/global, temporär oder permanent, mit Oberfläche. Bedarf ist belegt, nicht vermutet: der bisherige Workaround war „Client von Hand auf 1.1.1.1 stellen", regelmäßig (User 2026-08-14).
  • 🔴 Zentrale Reihenfolge-Regel: genau dieser Workaround IST der DNS-Bypass, den TP5 schließen will → erst der Ersatz, dann die Sperre. Ohne funktionierende Ausnahme-Oberfläche verliert u.a. der Entwicklungsrechner samt Agenten-Toolchain seinen Ausweg — und DNS-Blocks äußern sich dort nicht als klare Meldung, sondern als diffus kaputte Build-Umgebung.
  • Zwei Befunde, die die Architektur bestimmen:
    1. DoH ist nicht das Problem. Clients sprechen plain mit dem lokalen Resolver, der macht DoH nach oben — die empfohlene Auflösung des DoH/Split-Horizon-Konflikts, bf.lan bleibt heil. Was Seiten blockiert, ist die Policy. ➡️ Nie einen „DoH aus"-Schalter bauen, sondern Policy-Ausnahmen.
    2. Identität existiert nur dort, wo die Client-IP sichtbar ist. Router-DoH aggregiert alle Clients hinter einer Quelle — CF sieht nur den Core. Per-User-Identität bei CF verlangt einen Agent (WARP; bei NextDNS/ControlD deren CLI) → auf Shelly/Sonos/TV unmöglich. RouterOS kann keine per-Client-Ausnahme (keine DNS-Views; /ip dns static wirkt domain-weit — verifiziert). ➡️ Ein lokaler Resolver mit Client-Policy ist unvermeidbar.
  • ✅ Architektur-Entscheid: Filter zentral bei CF, Identität + Ausnahmen lokal. Die Filterliste bleibt vollständig bei Cloudflare (Core via DoH, unterwegs via WARP); lokal wird nie eine Filterliste vorgehalten, nur die Diffs. Vorteile: kein RAM-Thema auf dem Core (eine Adlist kostet ~1,9 KB/Domain, 50k ≈ 95 MB — Konkurrenz mit Conntrack/DHCP/WG), eine Policy-Quelle statt zweier, gleiche Regelbasis zuhause wie unterwegs.
  • ✅ Resolver-Wahl: AdGuard Home (läuft bereits, 10.40.20.16). Vergleich gegen die realen Anforderungen:
    • Pi-hole disqualifiziert: spricht kein DoH → bräuchte cloudflared/unbound als Sidecar (vierte QUIC-Komponente neben den drei TP16-Connectoren). Dazu belegtes Bruchrisiko v5→v6 (FTL/API/Web neu, Docker-Env-Variablen tot).
    • AGHs „keine Gruppen"-Schwäche verschwindet hier, weil Clients als CIDR eintragbar sind → 10.40.30.0/24 = die IoT-Gruppe. Die VLAN-Segmentierung ist bereits die Policy-Grenze. Dazu natives DoH, per-Device-ID auch bei wechselnder IP, „Unblock for this client", API + HA-Integration.
    • Technitium = ernsthafte Alternative (autoritativer DNS + DHCP + Apps, networkGroupMap); seine IP/Subnet-Bindung trifft uns kaum, weil die IPs per Reservierung faktisch statisch sind. Neubewertung legitim (die TP4-Ablehnung galt einer anderen Frage) — sinnvoller Anlass wäre, DNS und DHCP zusammenzuführen. Eigener TP, nicht dieser.
    • Verworfen: eigene Self-Service-UI bauen · per-Device-DoH via NextDNS/ControlD (Agent-Zwang, bricht bf.lan) · Umbrella ($30–45/User/Jahr) · Cloudflare verlassen (wirft TP16/TP16.1 weg) · Adlist auf dem Core.
  • Zuweisung ist nativ möglich: /ip/dhcp-server/lease hat die Property dhcp-option (verifiziert) → Option 6 pro Lease, bei 54 Reservierungen ein Konfigurationsschritt. (Nebenfund: Leases haben address-lists → automatische Aufnahme in eine Firewall-Liste, direkt nützlich für die TP5-JIT-Gruppe.)
  • ✅ WARP im LAN abschalten — Mechanismus: Cloudflare „Managed Networks". Der Client sucht bei jedem Netzwechsel einen selbst gehosteten TLS-Endpunkt, prüft dessen SHA-256-Fingerprint und zieht ein Device-Profil, das WARP deaktiviert. Beacon auf NAS/pve, nicht auf den Core (certs=0). Beseitigt die ICMP-Falle vom 2026-08-13 (ref_warp_local_icmp_trap) an der Wurzel.
    • ✅ Kollision aufgelöst (2026-08-14): die frühere Sorge war, dass TP16-Schritt 5 vw auf WARP-geschützt hebt und damit „WARP zuhause aus" bricht. Der User-Constraint aus TP16.2 streicht genau das — vw bleibt bypass und verlangt nie Device-Identity. Damit steht der Managed-Networks-Weg frei. Rest-Risiko nur für Dienste, die künftig doch Device-Identity verlangen sollten → dann Access-Policy zusätzlich auf die Heim-IP (bf4-home-ip existiert bereits).
    • ✅ Kein Verlust beim Fremdnetz-Zugriff: FM/DW laufen über den WG-Hub (TP10), nicht über WARP. Managed Networks schaltet nur im Heimnetz ab.
    • ⚠️ Free-Plan-Verfügbarkeit des Features nicht belegt → im Dashboard gegenprüfen. Holzhammer-Alternative (WARP-Endpunkte per Firewall sperren) nicht empfohlen: bricht still, Client probiert Transport-Fallbacks, trifft auch Gäste.
  • Umsetzung in Stufen — Sperre zuletzt:
    1. DoH auf dem Core scharf → schließt TP21.1-TODO 1 (heute plain 1.1.1.1); CA-Bundle nötig.
    2. WARP-Managed-Network (Beacon auf NAS/pve) → WARP zuhause aus.
    3. AGH in den Pfad, zweite Instanz + Config-Sync, beide per DHCP als DNS 1 + 2.
    4. Client-Gruppen per CIDR (ein VLAN = eine Gruppe); per-Lease Option 6 nur für Abweichler.
    5. HA-Buttons für temporäre Freigaben + JIT (AGH-API, RouterOS-REST, CF-API), RouterOS-scheduler als Backstop, damit Ablauf auch ohne HA greift.
    6. 🔴 Erst jetzt (in TP5): DNS-Bypass sperren — Vorbedingung: Freigabe über HA funktioniert nachweislich, und die permanente Ausnahmegruppe dns-bypass-exempt (Entwicklungsrechner + Build-/Agent-Tooling) steht. Dann: nur Resolver dürfen 53/853 nach außen (schließt Geräte, die eigene Resolver hardcoden — Smart-TV mit 8.8.8.8 ist der Klassiker).
  • Grenzen, offen benannt: ein Resolver im Datenpfad ist unvermeidbar (zwei Instanzen federn ab, ⚠️ AGH hat kein natives HA → Config-Sync ist Betriebsaufwand, sonst gilt nach Failover eine andere Policy) · keine Blockseite bei HTTPS (gilt für jede DNS-Filterlösung, auch Umbrella) · CF-Log-Retention 24 h im Free-Plan — laut User ausreichend, weil Ausnahmen JIT entstehen und ältere Fälle reproduziert werden.
  • ⚠️ 802.1X löst die IoT-Identität NICHT: Consumer-IoT (Shelly, Sonos) hat keinen 802.1X-Supplicant; der Standardweg ist MAB = wieder MAC-basiert → kein Identitätsgewinn gegenüber der DHCP-Reservierung. TP8 bleibt richtig, aber als Sicherheitsthema (dynamische VLAN-Zuweisung, kein MAC-Spoofing bei Managed-Geräten), nicht als DNS-Identitätsquelle.
  • Abh.: TP21.1 TODO 1 (DoH auf core), TP5 (Firewall/Bypass-Sperre — Reihenfolge s.o.), TP16 Schritt 5 (Access-Kollision), TP16.1 (falls WARP zuhause bleiben soll), TP8 (nur Sicherheit). Kein Blocker für TP5 → TP6, aber TP5 darf die Bypass-Sperre nicht vorziehen.

TP24 — Unterbrechungsfreie Stromversorgung (DC-USV, 48V-Bus) ▸ 🔜 KONZEPT — aus TP15 herausgelöst (User-Entscheid 2026-08-16) — docs/hw-refresh-requirements.md §9

  • Warum ein eigener TP: Die USV hing bisher als Unterpunkt in TP15 (HW-Refresh Switching/Router) mit. TP15s Kernauftrag — CRS326-Beschaffung, Bring-up, Core-Tausch — ist seit 2026-08-08 durch; die Stromversorgung ist als einziger Rest übrig geblieben und hat mit Switching sachlich nichts zu tun. Sie ist ein eigenständiges Infrastruktur-Thema mit eigener Einkaufs-, Verkabelungs- und Entscheidungslogik (Elektrik, Batterie, Standort) und blockiert nichts auf dem kritischen Pfad. Darum eigener TP.

  • Was es ist (Begriffsklärung): eine Pufferbatterie für die Netz-/Infrastruktur-Hardware — keine klassische AC-USV mit 230-V-Steckdosen. Ein 48-V-Gleichspannungsbus versorgt die Geräte direkt an ihrem DC-Eingang; die Batterie hängt am selben Bus und übernimmt bei Netzausfall ohne Umschaltlücke. Der Umweg AC→DC→AC→DC (und damit dessen Dauerverlust) entfällt.

  • Zielbild (Aufbau, unverändert gültig aus der TP15-Recherche):

    Teil Typ Preis Wirkungsgrad
    Netzteil MeanWell LRS-150-48 (150 W, 48 V) ~€30 ~88 %
    DC-USV-Modul MeanWell DDR-120-48 (120 W) ~€80 ~95 % (pass-through)
    Batterie LiFePO4 12,8 V / 20 Ah (256 Wh) ~€50 —
    DC-DC 12 V MeanWell SD-100A-12 (100 W, für NAS/pve) ~€30 ~85 %
    Summe ~€190

    230 V AC → LRS-150-48 → 48-V-Bus → Switches direkt · → DC-DC 12 V → DS923/pve · → DDR-120-48 → LiFePO4.

  • Warum 48 V: PoE-Technik arbeitet nativ mit 44–57 V (kein Wandler nötig), höhere Spannung = kleinere Ströme = weniger Leitungsverlust, und 48 V ist der Telecom-/USV-Standard.

  • Laufzeit (256 Wh): ~2,5 h bei 100 W reiner DC-Last · ~5,5 h wenn nur Switch + PoE (45 W) · ~2 h wenn ein Inverter für den Mac mini mithängt.

  • Effizienz-Argument (der eigentliche Grund für DC statt AC): eine AC-USV verheizt bei 100 W Dauerlast ~25–40 W (≈ €66–105/Jahr), die DC-Variante 5–12 W (≈ €13–32/Jahr) — bei praktisch gleicher Anschaffung (€190 vs. €150–250). Der Mehrpreis amortisiert sich über den Dauerbetrieb.

  • ✅ DC-Eingang vorhanden (geprüft): crs326-core + crs326-branch (12–57 V, DC-Jack) · ap1/ap2 hAP ax² (12–57 V, alternativ PoE-in). ⚠️ Adapter/Sonderfall: DS923+ (externes 12 V/7 A, 4-Pin Mini-Fit JR) · pve/N100 (12 V Hohlstecker) · Mac mini hat gar keinen DC-Eingang (eingebautes Netzteil → nur über Inverter).

  • Offene Entscheidungen (aus TP15 mitgenommen, unverändert offen):

    1. Mac mini auf USV? Braucht einen Inverter (~15–20 % Verlust + Komplexität). Vorfrage: läuft er überhaupt 24/7? Wenn nein, reicht ein Graceful-Shutdown-Script auf USV-Warnung.
    2. Standort/Verkabelung: Der Bus soll laut Skizze im Arbeitszimmer/Technik OG sitzen — der L3-Core steht aber in Technik UG. Entweder zweiter Zweig nach unten oder Bus dorthin, wo der Core steht. Diese Frage ist durch den Core-Umzug neu zu stellen (die Skizze stammt aus der Zeit, als oben der Verteiler geplant war).
    3. Arris-Kabelmodem: vorentschieden auf Grid (muss ohnehin am Netz bleiben, DC-Zweig nach unten wäre Aufwand für ~1–2 W).
    4. Batteriegröße: 20 Ah als Startwert; 50 Ah wenn die gepufferte Last über Switch+AP hinaus wächst.
    5. Monitoring/Shutdown: wie meldet die USV einen Netzausfall an NAS/pve (Signalkontakt vs. nichts)? Ohne das gibt es kein sauberes Herunterfahren, nur Durchhalten bis die Batterie leer ist.
  • Nutzen konkret: bei Stromausfall bleiben Core-Switch, Branch-Switch und beide APs online → Netz, WLAN und der WG-Hub überleben (der WAN-Uplink nur, solange auch der Arris Strom hat, s. Entscheidung 3). Für NAS/pve zählt vor allem das kontrollierte Herunterfahren statt des harten Abschaltens.

  • Abh.: keine harten. TP15 ✅ (die zu versorgende Hardware steht und hat DC-Eingänge). Verwandt: TP18 (Garage-Nodes brauchen je Site eine Stromversorgung), TP11 (USV-Zustand ins Monitoring). Kein Blocker für TP5/TP6.

  • Historie: war bis 2026-08-16 Unterpunkt von TP15 („🔋 DC-USV: MeanWell LRS-150-48 + DDR-120-48 + LiFePO4 20Ah (~€190), 48V-Bus; CRS326 DC-input passt"). Vollständige Recherche inkl. Schaltbild, Effizienzvergleich und Gerätetabelle: docs/hw-refresh-requirements.md §9 (+ §10 D/E/F für die offenen Fragen).

TP25 — WLAN-Stabilisierung ▸ 🔬 FUND E: Hebel 1 widerlegt (124× im upgrade-freien Fenster) und am 2026-09-01 ZURÜCKGEBAUT — Entscheidung faellt mit der Messung am 09-02; NEU: ap2 unterperformt (2 m/−80 dBm, 23:02-Befund) als eigener In-situ-Unterpunkt · Kern erledigt: 🟢 (2026-08-22) Dead Spot Terrasse repariert (43 % → 9 % Verlust, 20,4 s → 1,0 s). Fund F ✅ aufgelöst (08-24, geräteseitig — kein Netzfehler), 5-GHz-Umfeldscan ✅ (08-24), Kanalplan für AP 3 ✅ gerechnet. Offen nur noch die Umsetzung, wenn der dritte AP kommt. Nicht mehr kritischer Pfad — Sammel-TP für Christophs Beobachtungen 1–6

📚 Kondensiert am 2026-08-23 (Doku-Session, Punkt F). Dieser Abschnitt war mit 394 Zeilen 18 % der Datei. Die Langfassung steht vollständig und unverändert im Archiv: teilprojekte-archiv.md → TP25 (Langfassung) — Messchronik (A)–(G), Funde A–E im Volltext, die beantworteten Fragen (Beobachtung 5, ch149-Herkunft, 2ghz-g, Deckenmontage), Umsetzungsreihenfolge mit allen Deploy-Gotchas. Hier steht: Stand, Offenes, Merkregeln. Wer an TP25 weiterarbeitet, kommt mit diesem Abschnitt aus; wer wissen will warum etwas so entschieden wurde, liest die Langfassung.

Stand 2026-08-22, 23:30 — die Terrasse ist repariert. Erster Lauf mit beiden Ping-Strecken (tp25-runde-20260822-2330-auswertung.md): 43 % Verlust / 20,4 s längste Lücke (08-21) → 9 % / 1,0 s bei gleichem Gerät und gleicher Rolle. Wirksam war das Anheben von ap2 auf 2,5 m; 1,0 s liegt unter der Hörbarkeitsschwelle im Call. Der Dead Spot, der TP25 seit dem 08-20 getragen hat, ist damit weg.

🔎 Kernbefund, der bleibt: das Netz ist gut, die Redundanz ist es nicht. Die frei roamende Referenz hat in 17 von 21 Raumabschnitten 0 % Verlust; der Gegen-AP liefert Schlafzimmer −84,6/38 %, Bad −82,3/24 %, Küche −82,0/23 %, Terrasse −78,7/19 %. Fällt ein AP aus, ist die halbe Wohnung „verbunden, aber tot". ⇒ Ein dritter AP ist nach Christophs eigener Randbedingung derzeit NICHT begründet — er kaufte Redundanz, keine Abdeckung. Das ist eine andere Frage und Christophs.

Beide vormals offenen Punkte sind am 2026-08-24 abgearbeitet — Fund F ist aufgelöst, der 5-GHz-Umfeldscan gefahren und der Kanalplan für einen dritten AP gerechnet. Was von TP25 bleibt, ist Umsetzung bei Bedarf, keine offene Frage:

  1. ✅ Fund F — GEMESSEN UND AUFGELÖST (2026-08-24): kein netzseitiges Phänomen. Kollektor am Manager (/interface wifi registration-table, 15-s-Takt, 29 Samples über 7,7 min, 28 Clients), Rohdaten docs/messdaten/tp25-fundf-20260824.log. Von den drei Kandidaten sind zwei erledigt, einer bleibt — und die zwei fielen ohne Wartezeit:

    Kandidat Ergebnis woran es hängt
    (b) DHCP-lease-time gegen die Periode ❌ tot dhcp-v30 fährt lease-time=1d → Renew bei 12 h. Kann 5:05 nicht erzeugen. Eine Abfrage, keine Messung.
    (a) periodischer VAP-Re-Apply ❌ tot Ein Re-Apply wirft alle Clients des VAP gleichzeitig. Auf cap-wifi4-virtual1 standen zeitgleich drei Clients bei 16 h 51 min Uptime, 25 von 28 über 6 h. Mit einem 5-Minuten-Takt ist das unvereinbar.
    (c) das Gerät selbst 🟡 bleibt Der einzige Client, der im Fenster überhaupt neu verband.

    🔑 Der entscheidende Punkt an der Beweisführung: dafür braucht es die vier Zyklen nicht. Ein einziges Sample mit 16 h 51 min Uptime auf genau dem VAP, der angeblich alle 5:05 abwirft, widerlegt (a) vollständig. Die Fensterlänge begrenzt nur, was man über abwesende Geräte sagen kann.

    🔴 Und genau die sind abwesend: die beiden Fund-F-MACs gibt es gerade nicht. 38:A5:C9:C0:73:1E (10.40.30.178) und 38:A5:C9:BC:33:31 (10.40.30.179) stehen in /ip arp auf status=failed, haben keine DHCP-Lease, stehen nicht in der Registration-Table und sind auch HA unbekannt (kein device_tracker zu diesen MACs). ⇒ Das Phänomen ist an diesen Geräten derzeit nicht reproduzierbar, weil die Geräte weg sind. 📌 Merkregel — dieselbe wie beim P750W: bevor ein periodisches Trennen als Funk- oder Konfigurationsfehler untersucht wird, erst prüfen, ob die betroffenen Geräte überhaupt da sind. Beim Drucker war die Fehldiagnose „Power-Save", hier wäre sie „VAP-Re-Apply" geworden.

    ⇒ Fund F wird geschlossen: was auch immer der 5:05-Takt war, er lag geräteseitig, und die zwei betroffenen Geräte sind vom Netz. Netzseitig ist nichts zu tun. Nicht erneut aufgreifen, solange .30.178/.179 nicht wieder da sind und das Muster erneut gemessen wird.

    🟡 Nebenbefund mit Substanz — der DTU ist der einzige Wackelkandidat, den es wirklich gibt. 54:F2:9F:12:51:F0 (DTU/Wechselrichter, auf cap-wifi4-virtual1 = ap1) war der einzige Client mit einem Uptime-Rücksetzer: 43 min Sitzung, dann weg für ein Sample, dann wieder da — bei −52 dBm, also bei tadellosem Signal. Das ist derselbe Mechanismus, der schon die Transaktion TP17.1-dtu-pin-ap2 begründet hat: ein ESP hat EIN Radio, sein eigener AP und die STA-Verbindung teilen sich den Kanal. Periode hier 43 min, nicht 5:05 — es ist nicht Fund F, aber es ist echt. ⇒ Gehört zur ohnehin vorgesehenen Neubewertung des DTU-Pins, sobald der Kanalplan steht (s.o.).

    📜 Ursprüngliche Fund-F-Formulierung (2026-08-22)
    1. 🟡 Fund F — IoT-Clients trennen exakt alle ~5:05 bei gutem Signal (−59 dBm, vier Messpaare, auf 3 s reproduzierbar → ein Timer, kein Funk). Muss am Manager nachgemessen werden, die AP-Tabellen sind unter CAPsMAN leer. Kandidaten und der Test, der sie trennt: (a) periodischer VAP-Re-Apply (Fund D war die Ursache → prüfen, ob es nach dem Fix weg ist); (b) DHCP-lease-time der beiden MACs (38:A5:C9:C0:73:1E, 38:A5:C9:BC:33:31) gegen die Periode legen; (c) das Gerät selbst mit 5-min-Duty-Cycle → dann bleibt es unverändert. 🚫 Nicht die alte Power-Save-Merkregel wiederbeleben — die ist am 2026-08-13 als Fehldiagnose zurückgezogen (ref_wifi_powersave_multicast).
  2. ⚖️ Entscheidung 5 GHz auf 20 MHz (statt heute 20/40) — ✅ 5-GHz-Umfeldscan durchgeführt 2026-08-24, Empfehlung: NICHT verschmälern, Entscheidung an einen Auslöser hängen. ⚠️ 2,4 GHz NICHT vorschnell drosseln — dort hängen ~20 IoT-Geräte.

    Messung (/interface wifi scan am Manager, cap-wifi3 = ap1 / cap-wifi1 = ap2, 10–12 s):

    vom Radio auf fremdes 5-GHz-Netz Kanal Signal Abstand zum eigenen AP
    ap1 (5180/Ce) Drei_H288A_5G_acHG 5745/ac/80 MHz −86 dBm 19 dB darunter
    ap2 (5220/Ce) LiwestBB93-5G 5200/ac/80 MHz (Block 5180–5240) −89 dBm 24 dB darunter

    ⇒ Das 5-GHz-Band ist an beiden Standorten praktisch leer. Genau ein Fremdnetz je AP, beide rund 20 dB unter dem eigenen Pegel. Der Nachbar LiwestBB93-5G liegt mit 80 MHz zwar formal über beiden unserer Blöcke — bei −89 dBm ist das ohne Wirkung.

    🎯 Warum das gegen 20 MHz spricht — die zwei üblichen Gründe treffen hier nicht zu:

    • Fremdinterferenz: existiert nicht (−86/−89). Ein schmalerer Kanal hätte nichts abzuwehren.
    • Selbstüberlappung: existiert nicht. ap1 5180/Ce = ch36+40, ap2 5220/Ce = ch44+48 — überlappungsfrei (Fund C, 08-17).
    • Bleibt allein das +3 dB Link-Budget — und das ist zum guten Teil schon vorhanden: in einem 40-MHz-BSS sendet die Ratenanpassung an schwache Clients ohnehin auf dem Primärkanal in 20 MHz. Den Ceiling zahlen aber alle Clients dauerhaft. Der Nutzen geht an die, die ihn schon haben.
    • Und es löst das gemessene Problem nicht: die offenen Schwachstellen sind die Redundanz-Lücken (Schlafzimmer −84,6 / 38 %, Bad −82,3 / 24 %) am Gegen-AP. +3 dB macht aus 38 % Verlust keine 0 % — das ist eine Platzierungs-/AP-Zahl-Frage, keine Kanalbreiten-Frage.

    🔑 Der echte Auslöser ist die AP-Zahl, nicht die Interferenz — und das ist der Grund, die Entscheidung jetzt zu treffen statt sie offenzulassen. Beide Kanäle stehen hart in Band A (ch-5g-ap1=5180, ch-5g-ap2=5220, kein skip-dfs-channels), und Band A hat unter ETSI genau 4 × 20 MHz = 2 × 40 MHz. Mit zwei APs ist es damit exakt aufgebraucht:

    Szenario bei 40 MHz bei 20 MHz
    2 APs (heute) ✅ passt genau, überlappungsfrei ✅ passt, +3 dB, halber Ceiling
    3.–4. AP ❌ Band A voll ⇒ nur mit DFS (Band B ch52–64: Radar-Events, 60 s CAC) ✅ 4 saubere Kanäle, kein DFS

    ⇒ Empfehlung: 20/40 MHz beibehalten. Die Verschmälerung ist heute ein Preis ohne Gegenwert. Sie wird fällig, sobald ein dritter AP dazukommt — dann lautet die Wahl nicht mehr „20 gegen 40", sondern „20 MHz flottenweit" gegen „DFS akzeptieren", und dafür ist 20 MHz die ruhigere Antwort. Ein Zwischenweg existiert (ap1 behält 40 MHz, AP 2+3 fahren 20 MHz auf 5220/5240) und braucht ebenfalls kein DFS. 📌 Damit ist dieser Punkt entscheidungsreif und an einen Auslöser gebunden statt offen.

📐 Kanalplan für den dritten AP ▸ gerechnet 2026-08-24, DFS vom User genehmigt

User-Vorgaben (2026-08-24): 20/40 MHz bleibt · DFS ist genehmigt, soll aber auf ap1 oder ap2 liegen, nicht auf dem Neuen · der Neue ist ein MikroTik-Deckenstrahler mit ax · möglicherweise kommt er an die Stelle eines der bisherigen APs · der vorhandene Outdoor-AP bleibt konzeptionell außen vor.

Das Kanalbudget unter ETSI (Indoor), als Rechnung statt als Gefühl. Ein 40-MHz-Block darf keine Bandgrenze überschreiten — [48+52] ist deshalb unmöglich, und daran hängt die ganze Knappheit:

Band Frequenzen Kanäle 40-MHz-Blöcke DFS EIRP
A 5150–5250 36/40/44/48 2 (5180/Ce, 5220/Ce) nein 23 dBm
B 5250–5350 52/56/60/64 2 (5260/Ce, 5300/Ce) ja + TPC 23 dBm
C 5470–5725 100–140 5 ja + TPC 30 dBm, outdoor erlaubt

Heute belegen zwei APs mit 40 MHz ganz Band A. Genau das macht den dritten AP zur Bandfrage — und mit genehmigtem DFS ist sie lösbar, ohne irgendwo auf 20 MHz zu gehen.

Empfohlene Zuordnung (3 APs, alle 40 MHz):

AP Rolle Kanal 5 GHz Band DFS 2,4 GHz
cAP ax (neu) Primär, beste Position 5180/Ce (36+40) A nein 2437 (ch6)
ap1 hAP ax² Sekundär 5260/Ce (52+56) B ja 2412 (ch1)
ap2 hAP ax² Sekundär 5300/Ce (60+64) B ja 2462 (ch11)
— Ausweichspur, frei halten 5220/Ce (44+48) A — —
  • Warum Band B und nicht Band C für die zwei DFS-Radios: gleicher 23-dBm-Deckel wie Band A (kein Leistungssprung zwischen den APs) und CAC 60 s statt 600 s — die 10-Minuten-Prüfung gilt im Wetterradar-Bereich von Band C. Band C bleibt damit frei für den Outdoor-AP, wo seine 30 dBm auch hingehören.
  • Warum der Neue das nicht-DFS-Band bekommt: DFS kostet 60 s CAC bei jedem Boot und jeder Provisionierung plus einen erzwungenen Kanalwechsel bei Radar-Erkennung. Diese Kosten gehören auf die Radios, deren Fläche von einem anderen AP mitgetragen wird — nicht auf den bestplatzierten. Christophs Instinkt ist hier richtig.
  • 2,4 GHz fällt nebenbei sauber: ch1/6/11 ist mit drei APs erstmals vollständig überlappungsfrei möglich (heute 1/11 mit einer Lücke in der Mitte).

🔴 Zwei Fallen, ohne die der Plan teurer wird als er aussieht:

  1. Eine hart gepinnte Frequenz macht DFS zum Single Point of Failure. ch-5g-ap1/-ap2 tragen heute je genau einen Wert (frequency=5180 bzw. 5220). Bei einer Radar-Erkennung muss das Radio den Kanal verlassen — hat es keinen zweiten konfiguriert, bleibt das 5-GHz-Funk dieses APs unten. ⇒ Für DFS gehört in das Channel-Objekt eine Liste (z.B. frequency=5260,5280,5220), damit der AP neu wählen kann. Das ist der Unterschied zwischen „DFS ist unkritisch" und „DFS kostet einen AP".
  2. Vor dem Umzug die erreichte Sendeleistung messen — das ist Fund B, nochmal. Band B/C verlangen TPC; ohne TPC sinkt der erlaubte EIRP um 3 dB. TP25 Fund B war exakt dieses Muster: 18 dBm gesetzt, 9 dBm gesendet, weil ch149 unter ETSI leistungsbegrenzt ist. ⇒ Diskriminierender Test, bevor irgendwas gekauft oder umgezogen wird: ap2 testweise auf 5260, dann /interface wifi monitor lesen und die tatsächliche tx-power gegen die heutigen 18 dBm auf 5220 stellen. Kostet eine Config-Transaktion und zehn Minuten, und entscheidet, ob der Plan trägt.

⚖️ Zur Platzierung — die Messung widerspricht dem Aufstellen an einer bisherigen Stelle. Die Referenzmessung vom 08-22 sagt: 17 von 21 Raumabschnitten haben 0 % Verlust. Zusätzliche Abdeckung ist damit wertlos. Der ganze Wert liegt im Gegen-AP: Schlafzimmer −84,6 / 38 %, Bad −82,3 / 24 %, Küche −82,0 / 23 %.

Variante was sie bringt
Neuer AP an bisheriger Stelle besseres Deckenbild an einem Punkt, der schon funktioniert. Die Drei-Zimmer-Lücke bleibt unverändert — gleicher Standort, gleicher Schatten.
Neuer AP an neuer Stelle (Richtung Schlafzimmer/Bad/Küche) schließt die Lücke, also genau die Redundanz, die gekauft werden soll.
🎯 Beides: cAP ax an die bisherige Stelle, der verdrängte hAP ax² wandert in den Schatten bestes Deckenbild dort, wo die Clients sind, und eine zweite Option in den drei Räumen. Braucht keine vierte Antenne, nur eine Steckdose/PoE im Zielraum.

⇒ Empfehlung: Variante 3. Sie ist der einzige Weg, bei dem der Deckenstrahler an den guten Platz darf, ohne dass die gemessene Schwäche unangetastet bleibt.

  • Voraussetzung Neuer AP: PoE-in am Zielort (die cAP-ax-Reihe hat einen Gigabit-PoE-Port, kein Netzteil im Lieferumfang üblich) und ein Trunk mit VLAN 10/30/40/50/90/99 wie bei ap1/ap2. Der AP läuft auf wifi-qcom und tritt damit als regulärer CAP dem Manager bei — kein neues Modell im GC, nur ein weiteres Gerät plus zwei channel-Objekte. ⚠️ Exaktes Modell vor dem Kauf prüfen (cAP ax vs. cAP XL ax) — beide sind ax/wifi-qcom, unterscheiden sich in Abdeckung und Preis.
  • Wenn später doch der Outdoor-AP dazukommt: er gehört nach Band C (30 dBm, outdoor zulässig); Band A/B sind dann von den drei Indoor-APs belegt und die Ausweichspur 5220/Ce ist aufgebraucht.

🔴 Fund E — WIEDER GEÖFFNET 2026-08-30: die Wiedervorlage-Bedingung ist eingetreten

Fund E war „abgestuft, NICHT umsetzen" mit einer ausdrücklichen Bedingung: „Erst wieder aufgreifen, wenn ein Sticky-Fall erneut gemessen ist." Am 2026-08-30 ist genau das passiert.

Auslöser war eine unabhängige User-Meldung, nicht die Suche danach: „die letzten Tage/Wochen regelmäßig mit dem iPhone den WiFi-Link verloren obwohl bester Kontakt. Ich lande auf 5G und nicht am alten AP oder dem anderen." 🔑 Auf Mobilfunk zu fallen ist kein Roaming-Problem — bei schlechtem Roaming bliebe das Telefon im WLAN und wäre nur langsam. Es heißt: die Assoziation ist gestorben und die Neuanmeldung gescheitert.

Neue Messung (Manager-Log, 2026-08-30): dasselbe iPhone F2:62:4C:E1:AF:09 machte 16 Bandwechsel in 20 Minuten zwischen cap-wifi5 (ap3, 5 GHz) und cap-wifi6 (ap3, 2,4 GHz), zweimal im Abstand von einer Sekunde, bei −53 … −81 dBm. Kurz darauf dasselbe zwischen cap-wifi1↔cap-wifi2 — also an ap2. ⇒ Nicht ap3-spezifisch, sondern an jedem AP. Das deckt sich mit der Ursprungsmessung vom 08-20 (sechs Flaps in 22 Minuten) und mit …@wifi2 disconnected, not responding, signal strength −47 — Abbruch bei ausgezeichnetem Signal.

⚠️ Zwei Symptome, die NICHT gleichgesetzt werden dürfen: die Band-Oszillation (beide Male gemessen) und der Abbruch „not responding" bei gutem Signal. Christophs Beschreibung passt auf das zweite. Ob dieselbe Ursache dahintersteht, ist offen.

Zeitachse: am 2026-08-17 liefen TP28 (CAPsMAN-Cutover) und TP27 (7.24) gleichzeitig. Vorher waren ap1/ap2 Standalone ohne Steering und ohne Neighbor-Group, und sec-main fuhr ft=yes ft-over-ds=no. Beides änderte sich an diesem Tag. Das Beschwerdefenster deckt sich — Korrelation, kein Beweis.

Was die MikroTik-Doku dazu hergibt (recherchiert 2026-08-30):

  • transition-threshold Default −80 dBm, -time 10 s, -request-period 30 s, -count 3. Unsere Steering-Objekte setzen nichts davon. Bei −53…−74 dBm wird das iPhone also kaum Transition-Kandidat ⇒ unsolicited BTM scheidet als Treiber weitgehend aus.
  • WPA3 + 802.11r: die Doku nennt nur „802.11r geht nicht über OWE". Eine WPA3-PSK/FT-Inkompatibilität steht dort nicht — sie kursiert in Foren. Hypothese.
  • 2g-probe-delay: dokumentiertes Band-Steering (2,4-GHz-AP ignoriert die ersten 3 Probe-Requests je Client pro 60 s). Bei uns nicht gesetzt.
  • Bei dynamischen Neighbor-Groups markiert RouterOS 5 GHz als preferable. Wir nutzen die statische Gruppe bf-main über beide Bänder; ob die Präferenz dort greift, sagt die Doku nicht ⇒ offene Frage.

✅ Erledigt 2026-08-30: die Beweislage ist jetzt dauerhaft. wireless- und caps-Logs gehen in den Kollektor (10.40.20.18:514, dieselbe Senke wie TP5), und /system logging steht seit heute unter der 27. ComplianceRule system-logging — vorher war der Block unter keiner Regel und im Template gar nicht vorhanden. Vorher lief alles nur in einen 75-Zeilen-Ringpuffer.

🔴 NACHTRAG 2026-08-31 — die Daten sind da, und sie kippen die Reihenfolge. Nach einer Nacht Logging: 559 wireless-Zeilen, 439 davon (79 %) vom iPhone. Entscheidend ist die Trennschärfe — reauthenticating tritt 93 von 93 Mal bei genau einem Client auf (dem iPhone) und 93 von 93 Mal auf genau einer SSID (dirtyvader-main). Was main von den anderen fünf unterscheidet, ist ein einziges Attribut: ft=yes ft-over-ds=yes. Dieselben APs, dieselben Radios, derselbe Zeitraum — die SSIDs mit ft=no zeigen null Ereignisse. Das ist eine saubere Kontrollgruppe; die WPA3+802.11r-Hypothese ist damit durch eigene Messung gestützt, nicht mehr Forenwissen. Verlauf am 31.08.: 07:25 (10×) → 07:34 (21×), eskalierend, teils mehrere pro Sekunde, endend mit der Flucht auf einen 25 dB schlechteren AP (-89 dBm). ⇒ ft-over-ds=no ist jetzt Hebel 1, nicht 2g-probe-delay: Letzteres adressiert Band-Steering, hier liegt ein Authentifizierungsproblem vor. Details und Messrezept im Handoff vom 2026-08-31.

🚫 Kein Funk-Parameter wird geändert, solange keine Daten da sind. Vier Hebel, einzeln und je als Transaktion (gc-transaktion.py). Reihenfolge korrigiert am 2026-08-31 durch den Befund oben — die alte Liste führte 2g-probe-delay an, das aber Band-Steering adressiert, während hier ein Authentifizierungsproblem gemessen ist:

# Hebel Begründung der Reihenfolge Risiko Stand
1 ft-over-ds=no auf sec-main nimmt die Änderung vom 08-17 zurück; FT bleibt über ft=yes (over-the-air) erhalten. Kleinster Eingriff, der die verdächtige Achse trifft gering 🔬 gezogen 2026-08-31 08:09, Messung läuft
2 ft=no auf sec-main falls (1) nicht reicht: FT komplett aus. Kostet Roaming-Geschwindigkeit, nicht Roaming mittel 🔜 nur wenn (1) nicht greift
3 2g-probe-delay=yes (2,4-GHz-Configs) MikroTiks dafür gebautes Werkzeug; verzögert nur Probe-Antworten und kann keinen Client stranden lassen. Adressiert die Band-Oszillation — ein anderes Symptom sehr gering 🔜
4 signal-range-Boden per access-list (Fund E im Original) kann Clients aussperren — genau das war 08-22 das Gegenargument; nur mit gemessenem Wert hoch 🔜

🔬 Hebel 1 — gezogen am 2026-08-31 08:09, Messung läuft bis 2026-09-01

Was steht: sec-main am Core auf ft=yes ft-over-ds=no. Abnahme am Manager über alle Master-VAPs — cap-wifi3/4/5/6 (= ap2 + ap3) melden ft=true ft-over-ds=false; cap-wifi1/2 (= ap1) sind leer, weil ap1 funkstill ist. Danach remote-cap provision [find]; alle drei CAPs kamen mit state=Ok zurück, alle VAPs running=yes am CAP gelesen, und das iPhone hing 71 s später wieder auf dirtyvader-main mit auth-type=ft-wpa3-psk und frischer DHCP-Lease 10.40.10.196 — also die vorgeschriebene Abnahme (Lease, nicht Registration-Table). 30 Clients insgesamt zurück.

Governance: Transaktion TP25-fund-e-ft-over-ds (Scope wifi-security, Ablauf 2026-09-03), Geräte-Backup pre-tp25-funde-ftoverds.backup auf dem Core, Snapshot docs/transaktionen/TP25-fund-e-ft-over-ds.snapshot.rsc. Compliance danach 134/135, und der Diff ist exakt ft-over-ds=yes → no auf sec-main — kein Kollateral-Drift durch die Re-Provisionierung. Das GC-Template wird beim Commit nachgezogen, nicht vorher: sonst wäre der laufende Versuch unsichtbar und status könnte ihn nicht mehr von einem Versehen unterscheiden.

🔴 Die cap-wifiN-Karte hat sich durch die Re-Provisionierung VERSCHOBEN — wer die Logs vom 31.08. auswertet, muss den Schnitt kennen, sonst ordnet er Ereignisse dem falschen AP zu:

vor 08:09 (Log 07:xx) ab 08:09
ap1 (funkstill) — cap-wifi1 / cap-wifi2
ap2 cap-wifi1 / cap-wifi2 cap-wifi3 / cap-wifi5
ap3 cap-wifi5 / cap-wifi6 cap-wifi4 / cap-wifi6

⚠️ Korrektur am Befund: main unterscheidet sich in ZWEI Attributen, nicht einem. Neben ft=yes ft-over-ds=yes fährt sec-main als einzige SSID authentication-types=wpa3-psk allein; die fünf anderen sind wpa2-psk,wpa3-psk. Die Kontrollgruppe trennt also FT und WPA3-only nicht voneinander. Das ändert nichts an der Hebelreihenfolge (die Foren-Hypothese nennt genau die Kombination WPA3+FT), aber die Formulierung „genau ein Attribut" ist zu stark — wenn Hebel 1 und 2 beide nichts bewegen, ist wpa2-psk,wpa3-psk auf sec-main der nächste Kandidat, nicht signal-range.

Messung (Erfolgskriterium): reauthenticating für F2:62:4C:E1:AF:09 geht auf ~0, ohne dass die anderen SSIDs neue Ereignisse zeigen. Die Zahl MUSS sich bewegen.

ssh root@10.40.20.100 "pct exec 400 -- sh -c 'cat /opt/tp5-syslog/log/10.40.20.1/2026-0*-*.log |
  grep reauthenticating | grep -oE \"([0-9A-F]{2}:){5}[0-9A-F]{2}\" | sort | uniq -c'"

Baseline 93 (alle iPhone, alle main); davon 79 am 31.08., alle in Stunde 07, der letzte um 07:34:43 — also vor dem Eingriff. Zwischen 08:00 und 08:15 kein einziges. Das ist noch kein Ergebnis, nur der saubere Schnitt.

(Der Defekt war nie gegenstandslos, nur unbelegt: Fund B/C wurden am 08-20 mit ap1 auf ether15 gemessen — also in genau der Stellung, in der ap1 heute wieder hängt. Er ist platzierungsunabhängig.)

✅ Erledigte Funde — Ergebnis und Merkregel (Volltext im Archiv):

Fund Ergebnis Merkregel, die bleibt
A 2,4 GHz fuhr 40 MHz statt der profilierten 20 ✅ 08-17: Inline-channel.* auf wifi1/wifi2 geleert → 2412/2462 @ 20 MHz Inline-Overrides gewinnen gegen das verlinkte Profil — die SoT war die ganze Zeit richtig, das Gerät hat sie nicht befolgt
B ap2 sendete 5 GHz mit 9 statt 18 dBm ✅ 08-17: 5220/ax/Ce/I, 18 dBm Der Fix ist gemessen, der Mechanismus nicht — beide ersten Erklärungen (ETSI-Band C, „80 MHz kostet generell") sind widerlegt. Resthypothese + diskriminierender Test (ap2 auf 5180/Ceee) stehen im Archiv und gehören vor einen Hardwarekauf
C 80 MHz kostete Randabdeckung ✅ 08-17: ch-5g auf 20/40mhz; ap1 5180/Ce, ap2 5220/Ce, überlappungsfrei Unter ETSI gibt es im Indoor-Band nur einen 80-MHz-Block — zwei APs können dort nicht beide 80 MHz fahren. Genau deshalb war ap2 je auf 5745 gelandet
D w2-iot trug einen Channel-Override auf einem Slave-VAP ✅ 08-17: Overrides auf beiden APs entfernt, w2-iot seither BR band ist eine Eigenschaft des Radios, nicht eines Slave-VAPs. War der Mechanismus hinter „alle SSIDs da, -iot beharrlich nicht"
Drift-Lücke — genau diese Overrides lagen außerhalb jeder ComplianceRule ✅ 08-18 mit dem TP28-GC-Umbau: wifi-*-Features hängen am Core, drei neue Rules; ein CAP hat keine Inline-Overrides mehr „Compliant" heißt nur: die abgedeckten Sektionen sind sauber. Fund A und D blieben seit 08-08 unentdeckt, während die Flotte durchgehend compliant meldete

🔴 Deploy-Gotchas aus derselben Arbeit (kosten sonst zwei Deploys, Volltext im Archiv): /interface/wifi/set <if> !channel löscht die Inline-Sub-Properties NICHT — es wirft nicht, es tut nichts; richtig ist einzeln !channel.width !channel.band !channel.skip-dfs-channels. Und bei Kanalarbeit Breite VOR Frequenz ändern, sonst erzeugt man auf dem Weg eine Co-Channel-Kollision. ⚠️ steering=/Channel-Änderungen re-applien alle VAPs und werfen jeden Client kurz raus — nicht während eines Calls deployen, /system backup je AP vorher.

🔬 Messwerkzeug — vier Regeln, jede aus einem Fehlbefund geboren:

  • 🔴 Für einen Kanal-/Umfeldscan NICHT list_wifi_interfaces benutzen (Fund 2026-08-24): das Tool gibt security.passphrase aller Profile im Klartext aus — ein Read für die Kanalbreite hat so fünf WLAN-Passphrasen in ein Transkript geschrieben, den IoT-PSK inklusive. Für Kanäle reicht /interface wifi channel print detail; für die cap-wifiN-Zuordnung … print where master=yes mit Feldauswahl. Der Scan selbst (/interface wifi scan) ist unbedenklich.
  • 🔴 Die cap-wifiN-Nummern sind NICHT stabil. Der Manager vergibt sie in der Reihenfolge, in der sich die CAPs anmelden — nach dem ap1-Rückbau sind cap-wifi1/2 = ap2 und cap-wifi3/4 = ap1, umgekehrt zur früher fest verdrahteten Tabelle. ⇒ Die Zuordnung gehört zum Messlauf, nicht zur Auswertungszeit: tp25-walk-messung.sh friert sie als ap-map.txt ein, die Auswertung lässt die Namen sonst roh — unbeschriftet ist ehrlicher als falsch beschriftet. Läufe vor dem 2026-08-22 sind nicht rekonstruierbar; wer ihre AP-Namen zitiert, prüft sie an der Physik (der AP im Raum liefert dort das stärkste Signal).
  • 🔴 Ein vom Pin abgewiesener Verbindungsversuch ist in Ping-Daten nicht von einem Funkloch zu unterscheiden — und der Manager protokolliert ihn auf info-Ebene gar nicht. Android quittiert die Abweisung mit „Verbindung fehlgeschlagen" und wählt das Netz ab; danach misst der Rest des Rundgangs Stille. Die Klick-Konsole schlägt deshalb Alarm, sobald das gepinnte Gerät nirgends hängt, und schreibt nach 45 s UNERREICHBAR=apX — denn „der Gegen-AP trägt hier nicht" ist das Messergebnis.
  • 📐 Eine Runde, zwei Messpunkte (Christophs Aufbau 2026-08-22): das frei roamende iPhone zeigt, was ein normaler Client wählt — interessant ist der andere. Das S22 wird per access-list auf den Gegen-AP gezwungen (tp25-pin-s22.sh). Gute Referenz + schlechte Sonde = die Abdeckung steht, die Rückfallebene nicht. Memory ref_gegen_ap_messung.
  • ▶️ Start ohne Werkzeugkette: bash docs/tp25-runde.sh [name] — legt den Lauf unter docs/messdaten/tp25-runde-<name>/ an, friert die AP-Karte ein, startet Konsole (Port 3031) und Messung, sagt die vom Interface gelesene Adresse fürs Telefon an und löst beim Beenden den Pin. Auswertung: tp25-gehweg-auswertung.py --dir ….
  • ℹ️ Und das alles passiert TROTZ aktivem 802.11k/v und FT (st-main mit rrm=yes/wnm=yes, ft-over-ds=yes, live geprüft). Die Merkregel ref_ft_is_not_roaming („11k/v fehlen") ist historisch. Bei −48…−55 dBm kostet eine Übergabe 0–2,6 s, meist unter einer halben Sekunde — nicht die Übergabe ist teuer, sondern die schwache Zone.

User-Randbedingung (2026-08-17, wörtlich): „ich weigere mich einen anderen access point zu kaufen solange ich nicht das gefühl habe, wir haben alles aus dem ap rausgeholt." Das ist die Abnahmebedingung dieses TPs — keine Hardware-Empfehlung, bevor die Parameter stimmen. Der Boden ist gelegt (beide APs 18 dBm, überlappungsfrei, 20 MHz auf 2,4 GHz, ap2 auf 2,5 m); was übrig bleibt, ist die Redundanzfrage oben.

  • Warum eigener TP: Christoph hat am 2026-08-16/17 sechs konkrete Beobachtungen gemeldet und ausdrücklich einen TP dafür verlangt; sie hängen alle an derselben Ursache-Klasse (Funkparameter, die nicht nach Manual gesetzt sind). TP25 übernimmt den offenen Rest von TP3.3 (Re-Walk, Dead-Spot-Kartierung — mit der Raumkarte vom 08-22 faktisch geleistet). TP3.2/TP3.3 stehen als Historie.
  • Stand bei Sitzungsende 2026-08-22: Pin gelöst, Automatik aus, crs326-core unverändert und transaktionsfrei — die access-list steht unter keiner ComplianceRule, es gab nichts zu committen. (Seit 2026-08-23 gibt es die Rule wifi-access-list, s. TP22-Nachtrag — Fund E wäre ab jetzt transaktionsfähig.)
  • 📋 Übergabe: handoff-tp25-session-20260821.md · Messungen: tp25-wlan-roaming-messung-20260820.md, tp25-runde-20260822-2330-auswertung.md
  • Abh.: TP22 ✅ (GC-Deploy-Pipeline). Verwandt TP3.2 (docs/wifi-p750w-coverage-and-map.md), TP29 (nutzt dieselbe Datenquelle als Sensor). Kein Blocker für irgendetwas.

🔬 Fund E — 2026-09-01: der ap2-Teilbefund (gültig) und eine widerlegte Heilungs-Wertung

Anlass: Christoph meldete 2026-08-31 23:02 eine Störung am Schreibtisch (5G, iPhone, kein Telefonat). Analyse (alles read-only am Manager crs326-core):

  • AP-Karte (stabil seit letztem CAP-Rejoin 15:57, kein Rejoin danach — Attribution belastbar): cap-wifi5/6 = ap2, cap-wifi3/4 = ap3, cap-wifi1/2 = ap1 (funkstill, Radios da, 0 Clients).
  • 23:02-Ereignis: iPhone F2:62:4C:E1:AF:09 ping-pongt im Sekundentakt cap-wifi5 ↔ cap-wifi6 = ap2s 5G (5220) ↔ ap2s 2,4G (ch11/2462) — Band-Steering-Thrash auf demselben AP, plus disconnected, not responding um 22:37:40 (−76) und 23:01:00 (−82). Feldstärke der Roams −75…−88 dBm.
  • 🔴 Christophs Fakt kippt die Coverage-Deutung: iPhone und ap2 nur 2 m entfernt, gleicher Raum, ap2 auf 2,3 m. Bei 2 m Sicht wären −35…−45 dBm normal. −80 auf 2 m ist kein Coverage- und kein FT-Problem. Dazu: aktuell 0 Clients auf ap2, alle 29 auf ap3 — die Clients meiden ap2. ⇒ ap2s Funk selbst unterperformt (RF/Antenne/HW) und/oder die DTU-Störung auf ap2/ch11 (Transaktion TP17.1-dtu-pin-ap2, Wechselrichter 54:F2:9F:12:51:F0 mit −59 dBm auf ap2s 2,4-Radio).
  • In-situ zu messen (nachts nicht möglich — Phone idle auf ap3): Registration-Signal live prüfen, während ein Client auf ap2 hängt; Kandidaten-Eingriffe — DTU-Pin lösen/verlegen, dirtyvader-main auf ap2 temporär deaktivieren (ap3 übernimmt den Schreibtisch), ap2-Antenne/HW prüfen. Keine Änderung ohne diese Messung.

🔴 WIDERRUFEN am 2026-09-01 vormittags — der zweite Teil dieses Abschnitts war ein Messfehler. Ursprünglich stand hier: „Fund E ist im sauberen Fenster geheilt — 0× reauthenticating seit dem 14:36-Reboot (sinceMinutes=1440, total 0), also hat Hebel 1 sein Ziel erreicht."

Warum das nicht trägt: get_log liest den flüchtigen Ringpuffer des Routers, nicht den Kollektor. Live nachgemessen fasst er 1000 Zeilen und reicht bis 01.09. 00:38:58 zurück — keine 9 Stunden. Der Core-Reboot um 14:36 hatte ihn geleert; der 31.08. Vormittag war darin gar nicht enthalten. total 0 hieß „im Puffer nichts", nicht „in 24 h nichts". Das ist exakt die Falle, wegen der am 30.08. das persistente Logging gebaut wurde.

Was der Kollektor zeigt: im upgrade-freien Fenster 08:09–14:36 (Hebel 1 allein) 124× reauthenticating — 09h=1 · 11h=2 · 12h=20 · 13h=31 · 14h=70, alle vom iPhone allein, dazu Christophs zwei erlebte Ausfälle 13:16 und 14:23. Zwischen 09:00 und 14:36 lag kein Reboot. ⇒ Hebel 1 wurde am 2026-09-01 zurückgebaut; der Rückbau ist das entscheidende Experiment (s. Ist-Stand-Block oben).

✅ Der ap2-Teilbefund oben bleibt davon unberührt und gültig — er ist unabhängig gemessen und erklärt die 23:02-Störung, die weder FT noch Coverage erklärt.

Commit-Weg (wartet auf Christophs Go — git-push-Kette): Template nautobot/mikrotik_routeros.j2 Z. 220 sec-main von ft-over-ds=yes → ft-over-ds=no; dann Template-Repo nach Nautobot syncen, gc-configplan.py --refresh-compliance (Backup→Intended→Compliance), dann gc-transaktion.py commit --change-id TP25-fund-e-ft-over-ds. Danach wifi-security wieder driftfrei. 🔴 Kreuzt git-push (Backup-Guard + Commit-Hinweis-Regel) und Live-Nautobot-Jobs — nicht autonom. Handoff: handoff-20260901-tp25-ap2-verdacht.md.

TP28 — WLAN über CAPsMAN auf crs326-core ▸ ✅ ap3 LIVE (2026-08-30) — wieder geöffnet und durchgezogen

Der CAPsMAN-Kern ist und bleibt ✅ KOMPLETT (2026-08-18) — der Abschnitt darunter ist unverändert der Archivtext inkl. Post-Mortem. Wieder geöffnet wurde das TP am 2026-08-30, weil mit ap3 ein dritter CAP dazukommt; das ist dieselbe Baustelle, kein neues TP (Christoph-Direktive: ein archiviertes TP ist ein geschlossenes Ticket und darf wieder geöffnet werden). Der offene Teil steht am Ende des Abschnitts unter „Nachtrag ap3".

  • 🔴🔴 POST-MORTEM 2026-08-18 (gemeldet von Christoph: „die -iot-geräte sind offline … connect möglich, aber kein dhcp"): Seit dem Cutover hingen alle Slave-VAPs in gar keiner Bridge. /interface wifi cap slaves-datapath war auf beiden APs leer — und genau dieses Feld ist das Einzige, was die vom Manager erzeugten Slave-VAPs lokal in die Bridge haengt. Der Manager schickt datapath.bridge=bridge zwar mit, der CAP uebernimmt davon nur die vlan-id. Betroffen: dirtyvader-iot, -media, -untrusted, -guest. Nicht betroffen: dirtyvader-main — dessen Master-Radios wifi1/wifi2 sind statische Bridge-Ports und liefen deshalb weiter. Genau das machte den Ausfall unsichtbar.

    • Fix (live 2026-08-18 auf ap1 + ap2, seither Soll im Template): /interface wifi datapath add name=dp-caps-slaves bridge=bridge + /interface wifi cap set slaves-datapath=dp-caps-slaves. Das Objekt traegt bewusst keine vlan-id — die liefert der Manager pro VAP. Danach erscheinen die VAPs als dynamische Bridge-Ports mit pvid=30/40/50/90, und DHCP laeuft sofort wieder.
    • 🔴 Die Diagnose-Lehre: „Client assoziiert" beweist gar nichts. Am Manager standen 25 IOT- Registrations — das WLAN sah perfekt aus. Erst zwei Reads trennten die Faelle: torch auf vlan30 am Core (leer) und der TP5-Drop-Log (kein einziger DHCP-Eintrag). Beides leer heisst: es wird nichts verworfen, es kommt nichts. Damit war die Firewall — der naheliegende Verdaechtige nach der Haertung derselben Nacht — in zwei Minuten entlastet.
    • ⚠️ Die Abnahme „✅ Ohne jeden Aussetzer" weiter unten war falsch. Sie zaehlte Registrations (30 Clients, beide CAPs Ok, 7 VAPs je AP) — also genau die Metrik, die hier luegt. Keiner dieser Werte beruehrt die Bridge. Ein Abnahmekriterium fuer WLAN-Umbauten ist ab jetzt eine frische DHCP-Lease pro SSID, nicht die Registration-Table.
    • routeros/tp28-ap-wifi-reste-abraeumen.rsc hat den Ausfall nicht ausgeloest, aber zementiert: mit den geloeschten datapath-Objekten gab es lokal nichts mehr, worauf slaves-datapath haette zeigen koennen. Der Ein-Zeilen-Fix war damit weg. Nachtrag steht im Skript.
    • ✅ Reboot-Abnahme an ap2 (2026-08-18), drei Fragen auf einmal beantwortet: slaves-datapath ueberlebt den Neustart (statisches Objekt, steht im /export), der CAP findet seinen Manager durch die gehaertete input-Kette wieder (current-caps-man-identity=crs326-core — MNDP/CAPWAP sind also korrekt offen, das war seit TP5 Etappe 4 nie durchgespielt), und die sieben Slave-VAPs kommen von selbst als dynamische Bridge-Ports mit pvid 30/40/50/90 zurueck. AP nach 40 s wieder erreichbar. Abnahme nach neuer Regel = frische Lease: IOT-Client 10.40.30.8 last-seen=30s, MAIN-Client 10.40.10.180 last-seen=1m54s.
    • ⚠️ Reboot per MikroMCP geht auf 7.24 nicht: das reboot-Tool sendet delay, die REST-API antwortet unknown parameter delay; run_command ist fuer /system reboot* per Deny-Policy gesperrt. Weg bis das gefixt ist: sshpass + ssh admin@<ap> "/system reboot".
    • ✅ Soll nachgezogen, Compliance wieder sauber: 92/92, 0 Drifts (2026-08-18). Weg: Template in nautobot/mikrotik_routeros.j2 → routeros/push-gc-template.py (Secret-Guard) → GitRepository-Sync → Backup/Intended/Compliance. Ohne den Push meldet der Refresh die zwei neuen Zeilen als extra auf beiden APs — das Jinja lebt im separaten Gitea-Repo bf-golden-config, ein Commit hier allein erreicht Nautobot nicht.
    • ⚠️ Beim ersten Sync-Versuch: redis.exceptions.ConnectionError: broken pipe auf dem Nautobot-Stack. Ein simpler Retry ging durch; NAS-Load war unauffaellig (0,9). Der Job meldet das als FAILURE — nicht mit einem inhaltlichen Fehler verwechseln.
  • ✅ Der offene GC-Rest ist erledigt (2026-08-18). Die fuenf wifi-*-ComplianceFeatures haengen jetzt am Core, der AP-Zweig des Templates ist auf eine einzige Zeile eingedampft (/interface wifi cap set discovery-interfaces=vlanXX enabled=yes).

    • 🔴 Dabei aufgefallen: die APs trugen noch die KOMPLETTE Standalone-WLAN-Config aus TP3 — je 2 channel, 6 datapath, 6 security, 7 configuration, 1 steering. Der Cutover vom 08-17 hatte sie nie abgeraeumt. Wirkungslos (der Manager liefert alles dynamisch), aber sie im Template als Soll zu rendern hiesse, totes Zeug zur Absicht zu erklaeren — dasselbe Argument wie bei den defconf-Firewallregeln in TP26. Abgeraeumt mit routeros/tp28-ap-wifi-reste-abraeumen.rsc.
    • ✅ Ohne jeden Aussetzer: vorher/nachher 30 Clients, beide CAPs Ok, 7 dynamische VAPs je AP. Die Sorge vor einem Radio-Re-Apply (TP25/TP3.3: „wirft jeden Client kurz raus") hat sich nicht bestaetigt — die dynamischen VAPs tragen ihre Werte inline (datapath.vlan-id=…) und referenzieren kein lokales Objekt; nur wifi1/wifi2 hielten noch einen toten configuration=-Verweis, der beim Loeschen folgenlos blieb.
    • 🔴 [find !dynamic] ist auf diesen Sektionen ein SYNTAXFEHLER, nicht bloss wirkungslos: /interface wifi configuration & Co. haben gar keine dynamic-Eigenschaft (die gibt es nur auf /interface wifi selbst). Der erste Anlauf lief komplett in die on-error-Zweige, aenderte nichts und meldete trotzdem „Plan Completed". Wieder: nur die Gegenpruefung am Geraet zaehlt.
  • ✅ Drei neue ComplianceRules schliessen die letzten WLAN-Luecken: wifi-capsman (der Manager-Schalter), wifi-provisioning (die vier Regeln je radio-mac — sie entscheiden, welcher AP welche Kanaele und Slave-VAPs bekommt) und wifi-cap (die CAP-Seite; faellt discovery-interfaces weg, findet der AP seinen Manager nach dem naechsten Reboot nicht mehr). Ergebnis: 92/92 compliant, 0 Drifts.

  • ⚠️ wifi-security ist gruen, aber NICHT deploybar. Die Passphrasen stehen als <<REDACTED>> im Template (User-Entscheid 2026-08-18: alle fuenf Features umhaengen, Platzhalter in Kauf nehmen). Der gruene Haken beweist nur, dass Soll und Ist dieselbe Maske tragen — nicht, dass die Passphrasen stimmen, und schon gar nicht, dass die Sektion ausgerollt werden koennte. Das steht als Warnung im Template. Wer sie aendert, liest den Ist-Stand per REST zurueck (GET http://10.40.99.1/rest/interface/wifi/security) — nicht aus wlan-psks.env oder den drei Bring-up-Dateien, die voneinander abgeschrieben sind.

  • ⚠️ Die frueher hier notierte „bisher fehlende Rule auf /interface wifi" ist gegenstandslos — wie vorhergesagt: ein CAP hat keine Inline-Overrides mehr, die driften koennten.

Methodenwechsel auf Christophs Anstoß: „ich möchte die methode in frage stellen … eine sot ist zwar nett, aber wenn sie uns das leben zur hölle macht, verzichte ich auf diese governance." Der Entscheid vom 2026-07-22, CAPsMAN abzuschaffen, ist damit revidiert.

  • ✅ Warum es diesmal trägt: 2026-07-22 war ap1 Manager UND AP — und ein Manager kann seine eigenen Radios nicht per CAPsMAN provisionieren (MikroTik-bestätigt). Genau diese Asymmetrie war die Ursache von TP3.1. Mit dem Core als Manager sind beide APs symmetrisch. Der Core braucht dafür kein wifi-qcom: das capsman-Menü steckt im Basis-routeros, die 3,35 MB freier Flash sind irrelevant. Die zweite Begründung von damals („WLAN instabil") war ohnehin widerlegt — standalone blieb es instabil, die Ursachen waren die Funkparameter aus TP25.
  • ✅ Ist-Stand: Manager auf vlan99, 4 Kanäle · 6 Datapaths · 6 Security · 2 Steering · 9 Configurations · 4 Provisioning-Regeln je radio-mac. Beide remote-cap Ok, je 7 dynamische VAPs. Kanäle exakt wie in TP25 gemessen (ap1 5180/ax/Ce/I+2412/ax, ap2 5220/ax/Ce/I+2462/ax). 34 Clients (main 3, iot 27, media 4), Roaming im Manager-Log belegt. ft-over-ds=yes ist jetzt aktiv (unter CAPsMAN erstmals verfügbar), -guest hat eigenes st-guest mit neighbor-group=bf-guest — nicht bf-main mitbenutzen, sonst meldet 802.11k Gast-Clients BSSIDs einer SSID, auf die sie nicht assoziieren können.
  • 📌 Local-Forwarding ist keine Einstellung mehr: im neuen /interface/wifi gibt es kein zentrales Forwarding („There is no CAPsMAN-forwarding in WiFi (RouterOS 7.0+)"). Die datapath-Property existiert deshalb nicht — monitor bestätigt traffic processing on CAP.
  • 🔴 DIE MESSFALLE DIESES TPs — unter CAPsMAN führt der MANAGER die Tabellen, nicht der CAP. /interface/wifi/registration-table und die wireless-Logs sind auf ap1/ap2 leer, obwohl das WLAN einwandfrei läuft. Ich habe daraus zweimal „WLAN ist unten" geschlossen, während Christophs Geräte mit vollem Ausschlag verbunden waren und ein Speedtest gut lief. Immer am Manager zählen.
  • 🔴 DIE ZWEITE FALLE — drei übereinstimmende Dateien sind nicht drei Belege. wlan-psks.env wich für iot/untrusted/infra von ap1-prov-base.rsc, ap2-prov-base.rsc und ap1-standalone.rsc ab. Ich habe die drei als Bestätigung gelesen und die env-Datei „korrigiert" — ~20 Shellys waren 20 Minuten aus dem Netz. Die drei Dateien stammen alle aus dem Bring-up vom 2026-08-08, sind also eine Quelle in drei Kopien. ✅ Auflösung: die Passphrasen sind am Gerät per REST lesbar (GET http://10.40.99.1/rest/interface/wifi/security) — dort ausgelesen ergab sich exakt der env-Wert. wlan-psks.env ist neu aus dem Live-Stand befüllt und trägt jetzt eine Kopf-Warnung; die drei Baselines sind seit dem Cutover ohnehin nicht mehr deploybar und wurden entschärft (Passphrasen raus, HISTORISCH).
  • ✅ OFFEN — das GC-Modell beschreibt weiter Standalone-APs — ERLEDIGT am 2026-08-18. Die wifi-*-Features haengen jetzt am Core, das AP-Template ist auf die cap-Zeile eingedampft, und die toten Standalone-Objekte sind von beiden APs abgeraeumt. Details im ersten Block dieses TPs. Die hier genannte Zahl 76/76 ist damit doppelt ueberholt (aktuell 92/92 bei 23 Rules). (Der Absatz stand bis 2026-08-19 unveraendert als „OFFEN" hier, obwohl der Kopf des TPs schon „KOMPLETT" sagte — ein Selbstwiderspruch im selben Abschnitt, ueber den prompt die naechste Session gestolpert ist. Merkregel fuer kuenftige Statuswechsel: nicht nur den Header umschreiben, sondern die Punkte darunter mitziehen.)
  • 📌 Nebenbefund: HAs mikrotik-Integration pollt alle 10 s und schreibt zwei Log-Zeilen pro Poll auf jedes Gerät. Die AP-Logs sind dadurch für Diagnose praktisch unbrauchbar (get_log ohne topics-Filter liefert nur admin logged in/out via api).
  • Abh.: TP25 ✅ (die Funkparameter sind übernommen), TP15/TP21.1 ✅ (Core als L3-Router = Manager-Standort). Verwandt: TP3 (dessen CAPsMAN-Ära hier revidiert wird), TP22 (GC-Scope).

↩️ Nachtrag ap3 — dritter Access-Point (cAP ax), offen seit 2026-08-30

Gerät: MikroTik cAP ax (cAPGi-5HaxD2HaxD), Werkszustand, Uplink an crs326-core ether21. Mgmt-IP 10.40.99.13/24 (TP32: .10–.29 = physische Hosts; ap1 .11, ap2 .12), statisch am Gerät wie ap1/ap2 — keine DHCP-Reservierung.

Warum überhaupt: ap3 soll ap1 in Technik UG ablösen, ap1 wandert danach ans Sideboard in die gemessene Schwäche (Schlafzimmer/Bad/Küche). Das ist exakt TP25 Variante 3 („cAP ax an die bisherige Stelle, der verdrängte hAP ax² wandert in den Schatten").

Kanalplan = der aus TP25 (2026-08-24 gerechnet, DFS von Christoph genehmigt) — nicht neu erfunden:

AP 5 GHz Band DFS 2,4 GHz wann
ap3 5180/Ce (36+40) A nein 2437 (ch6) jetzt
ap2 5220/Ce heute → 5300/Ce (60+64) A → B → ja 2462 (ch11) beim ap1-Umzug
ap1 — (Radios aus) → 5260/Ce (52+56) → B → ja 2412 (ch1) beim ap1-Umzug
— 5220/Ce Ausweichspur frei halten A — — —

🔴 ap1s Radios gehen AUS, bevor ap3s angehen (User-Vorgabe 2026-08-30): beide stehen bis zum Umzug nebeneinander, ein zweiter AP am selben Punkt bringt nichts und stört. Das ist ein Interim, kein Endzustand — ap1 kommt auf 5260 zurück.

📌 Erratum 2026-08-30: der Port ist ether21, nicht ether22 (erste Annahme aus der Fragerunde). Der Befund lag die ganze Zeit in den Daten: in der dynamischen pvid-VLAN1-Zeile steht current-untagged=…,ether21 — ether22 taucht dort nicht auf, weil es keinen Link hat. Merkregel: bevor man einen Port konfiguriert, den behaupteten Port gegen link-ok + die Bridge-Host-Tabelle stellen. /interface bridge host print where interface=etherN nennt die MAC des Gegenübers — hier D0:EA:11:FF:16:9D, exakt die E01 vom ap3-Label.

Stand 2026-08-30 — ✅ DURCH
  • ✅ core ether21 ist Trunk (10/20/30/40/50/90/99 tagged, Muster ether15), GC-Template nachgezogen; AP-Zweig rendert Bridge-Ports aus ap.extra_ports statt hartkodiert.
  • ✅ Zugang über ap3 ether2 → core ether22 (Patchkabel). Über ether1 war ap3 grundsätzlich nicht administrierbar, s. Gotchas unten. Loop-Schutz: ether21 war während Phase 1 abgeschaltet, danach im selben Befehl getauscht (ether22 aus, ether21 an).
  • ✅ Bring-up in zwei Phasen: Phase 1 (ether1 aus der WAN-Liste, 10.40.99.13/24 auf vlan99, Flotten-PW) über das Kabel, Phase 2 (Werks-WLAN weg, CAP-Modus, slaves-datapath, vlan-filtering=yes) über vlan99.
  • ✅ RouterOS 7.20.8 → 7.24.1 + RouterBOOT 7.24.1 (zwei Reboots, TP27-Kriterium current-firmware = upgrade-firmware). 🆕 Die Angleichung der übrigen vier ist am 2026-08-31 erfolgt (Christoph, WebFig) — die Flotte ist einheitlich, TP27 erledigt.
  • ✅ ap1 funkstill (Provisioning-Regeln entfernt + remote-cap provision), gegengeprüft am CAP: nur noch wifi1/wifi2 mit running=false, keine dynamischen VAPs.
  • ✅ ap3 funkt: cap-wifi5 = 5180/ax/Ce/I, cap-wifi6 = 2437/ax, voller VAP-Satz (2 Master + 7 Slaves), alle running=true.
  • ✅ Datapath bewiesen: 20 IOT-Clients auf ap3, Ping über VLAN30 mit 0 % Loss. (Bewusst nicht über die Registration-Table abgenommen — die zeigte im 14-h-Ausfall 25 zufriedene Clients. Bei statisch reservierten Shellys erzeugt ein Roam keine neue DHCP-Lease, deshalb ist hier Datenverkehr das belastbare Signal.)
  • ✅ Nautobot: DeviceType cAP ax, Device ap3 mit 16 Interfaces, primary_ip4, secrets_group, ConfigContext (5g=ch36 2g=ch6 extra_ports=[]). Nebenkorrektur: ap1 stand auf „Wohnzimmer Sideboard", steht aber in Technik UG.
  • ✅ Compliance 130/130, 0 Drifts über 5 Geräte. Die drei Werksreste, die der erste Lauf mit ap3 gemeldet hat (defconf-DNS, 11 defconf-Firewallregeln, fehlende /ip service-Härtung), sind mit routeros/tp28-ap3-werksreste.rsc abgeräumt.
  • 🔜 Offen: TP25-Messrunde (bash docs/tp25-runde.sh ap3-bringup) — braucht einen Rundgang. Leitfrage: deckt ap3 allein ab, was ap1 abgedeckt hat?
  • 🔜 Offen: Passwort-Rotation (TP30) — das gemeinsame Admin-PW ist in dieser Session verbrannt; ap3 trägt es bereits mit, hängt also in der Rotation mit drin.
Was am cAP ax anders ist als am hAP ax² — und was daran wehtut
  • 🔴 Nur ZWEI Ethernet-Ports (ether1 PoE-in, ether2 PoE-out) — kein ether3/4/5. Der GC-Template-AP-Zweig hatte die hartkodiert; das war die stille Annahme „AP == hAP ax²". Deshalb ap.extra_ports im ConfigContext (Default = ["ether3","ether4","ether5"], ap3 = []). Merkregel: eine Literal-Liste im Template ist so lange harmlos, bis die Flotte heterogen wird.
  • crs326-core hat kein PoE-out (/interface ethernet poe existiert dort nicht). Der cAP ax bringt aber Netzteil und Gigabit-PoE-Injector mit (Produktseite 2026-08-30) — die TP25-Notiz „kein Netzteil im Lieferumfang üblich" trifft auf dieses Modell nicht zu.
  • ether2 bleibt im Bring-up als Lifeline im Bridge (untagged VLAN1) und fliegt erst raus, wenn ap3 über 10.40.99.13 steht — Zielzustand = wie ap2: ether2 kein Bridge-Port (VLAN1 bleibt leer, TP19 ist geschlossen).
  • Radio-MACs nicht raten: /interface wifi radio print am Manager listet CAP-Identity + Radio-MAC ohne jede Passphrase. Das ist der richtige Read — nicht list_wifi_interfaces, das gibt security.passphrase aller Profile im Klartext zurück (Leak 2026-08-24).
Auflagen aus TP25 für den ap1-Umzug (nicht vergessen)
  1. DFS-Channel-Objekte brauchen eine Frequenz-LISTE (z.B. frequency=5260,5280,5220). Mit genau einem Wert ist eine Radar-Erkennung ein SPOF: das Radio muss den Kanal verlassen und hat keinen zweiten → 5 GHz dieses APs bleibt unten.
  2. Vorher die erreichte tx-power messen (/interface wifi monitor), ap2 testweise auf 5260 gegen die heutigen 18 dBm auf 5220. Fund B war genau das Muster: 18 dBm gesetzt, 9 dBm gesendet.
  3. Uplink für ap1 danach: core ether3 (no-link, frei) — vorher dort die Altlast abräumen, der Port ist untagged-Member von VLAN40 bei pvid=1.

TP29 — Anwesenheitserkennung fürs Energiesparen ▸ 💡 IDEE, noch nicht auf dem kritischen Pfad (angelegt 2026-08-21)

User-Formulierung (2026-08-21): „Personen im Haus orten mit BT (Shellys), WiFi und Radar. So kommen wir dem Ziel der Energieersparnis ohne Komfortverlust groß näher."

  • Ziel: raumgenaue Anwesenheit als Eingangssignal für Heizung und Licht — sparen, ohne den typischen Komfortverlust („erst kalt, dann warm, wenn jemand da ist").
  • 🔎 Der Entwurfsgedanke sind die komplementären Schwächen der drei Quellen — keine trägt allein:
    Quelle erkennt erkennt nicht
    BLE über die Shellys grobe Zonen, billig, überall vorhanden Menschen ohne getragenes Gerät; Raumgrenzen unscharf
    WLAN (Registration-Table am Manager: Gerät, AP, Signal) wer mit Telefon da ist, inkl. AP-Zuordnung Menschen ohne Telefon; Raum nur so genau wie die AP-Abdeckung
    mmWave-Radar auch die stillsitzende Person ohne Gerät braucht einen Sensor je Raum
  • ✅ Was schon steht: Shellys vollständig auf VLAN30 (TP17 ✅), HA produktiv — und mit docs/tp6-monitor-core.sh existiert seit TP25 bereits ein Sammler, der die WLAN-Präsenzdaten am Manager mitschreibt (Gerät → AP → Signal, im Sekundentakt).
  • ❓ Offene Fragen für die erste Session: Datenpfad ins HA (MQTT oder REST?) · Firewall-Regeln für neue Sensorik (TP5 ist deny-by-default, jeder neue Dienst über eine VLAN-Grenze braucht eine Regel) · Aufbewahrung und Privatsphäre der Ortsdaten · ob Radar-Sensoren auf dirtyvader-iot laufen oder eigene Segmentierung brauchen.
  • Abgrenzung: TP25 misst den Funk, TP29 nutzt ihn als Sensor — dieselbe Datenquelle, anderes Ziel. Die Raumkarten aus TP25 sind für TP29 direkt verwertbar: sie sagen, wie genau die WLAN-Quelle einen Raum überhaupt auflösen kann.
  • Abh.: keine harten. Profitiert von TP25 (Abdeckung/AP-Zuordnung) und TP11 (Monitoring), wenn es dahin kommt. Kein Blocker für irgendetwas.

TP30 — Credentials: neu setzen & rotieren (Sammelbecken) ▸ 🔜 ANGELEGT 2026-08-24, bewusst GEBÜNDELT gegen Projektende

User-Entscheid 2026-08-24: „ich würde gerne daraus einen eigenen punkt machen. ich bin mir sicher, dass es noch öfters passwörter zu rotieren geben wird. das machen wir dann alles gegen projektende." — nachgeschärft am selben Tag: „tp30 wird unser sammelbecken für credentials, die neu gesetzt werden müssen", und TP17.2 wandert hierher.

📥 Scope ist damit breiter als „Rotation": jedes Credential, das neu gesetzt werden muss — Erstvergabe (bisher gar keins vorhanden, z.B. Shelly SetAuth) oder Rotation (Wert verbrannt/abgelaufen). Die Trennung ist für die Priorität wichtig, für die Arbeit nicht: der Kostentreiber ist in beiden Fällen derselbe — die Konsumenten, nicht das Setzen. Neue Credential-Aufgaben kommen ab jetzt hier hinein, nicht als eigenes TP.

Warum ein eigener TP und nicht vier verstreute Follow-up-Zeilen: die Rotationen teilen sich Werkzeug, Fallen und vor allem den teuren Teil — den Konsumenten (HA, Nautobot, MikroMCP, ~/.secrets). Wer sie einzeln macht, zahlt den Re-Auth-Aufwand mehrfach. Gebündelt ist es ein Durchgang je Gerät. Deshalb steht hier die Liste, nicht der Termin.

  • Auslösekriterium (statt Datum): wenn die offenen TPs abgearbeitet sind bzw. Christoph das Projekt schließt. 🔴 Harte Ausnahme, die den Termin vorzieht: ein neuer Leak, ein fremdzugängliches Gerät oder ein Personenwechsel — dann sofort und einzeln, nicht gebündelt.
# Credential Umfang Anlass Vorarbeit Preis der Rotation
1 MikroTik admin core · branch · ap1 · ap2 (ein geteilter Wert) 🔴 zweimal in ein Transkript geraten (2026-08-22 via sshpass -p; 2026-08-23 durch einen Enumerationsfehler) ✅ Etappe 1 durch (s.u.) nach Etappe 2 nur noch ~/.secrets/credentials.env + ~/.claude.json + MikroMCP-Neustart
2 IoT-WLAN-PSK (sec-iot) dirtyvader-iot, 26 Clients steht im Klartext im TP17-Text ⇒ auch in der Git-Historie; Redigieren holt ihn dort nicht raus Hebel multi-passphrase am Core verifiziert vorhanden, Tabelle leer 23 Shellys per Skript + 3 von Hand (.249 NSPanel Pro, .244 Nuki Bridge, .227 SwitchBot)
3 Shelly-Gerätepasswort (vormals TP17.2) 23 Shellys Härtung (bisher keine Auth auf den Geräten) ✅ Werkzeug fertig (migrate.py cmd_auth + Digest-Fallback) je Gerät ein HA-Re-Auth-Flow — das ist der Aufwand, nicht SetAuth
4 CF-Access-Service-Token (CF_API_TOKEN) Cloudflare-Zugang, s. TP14.4 ⏳ läuft am 2027-01-09 ohnehin ab — harter Termin, unabhängig vom Projektende Runbook liegt (tp14.4-cf-access-token-runbook.md) Token neu ausstellen + ~/.secrets nachziehen; account-owned, anderer verify-Endpoint
5 Nautobot-Tokens (NAUTOBOT_SUPERUSER_API_TOKEN, NAUTOBOT_GITEA_TOKEN) Nautobot-Superuser + Gitea-Anbindung 🔴 in der TP16.3-§2/§3-Session (2026-08-31) durch schwache sed-Maskierung ins Transkript geraten Werte in docs/nas-vlan20/stacks/210-nautobot.yml (gitignored) neu ausstellen + Stack-YML nachziehen; Superuser-Token wird von den gc-*-Skripten gelesen
6 MariaDB root (LXC 302, 10.40.20.23) root-DB-Login 🔴 am 2026-09-01 (TP39) ins Transkript geraten — die /root/.my.cnf trug password <wert> ohne =, die Feld-Maskierung ankerte auf = und griff nicht Wert in /root/.my.cnf (chmod 600) SET PASSWORD + .my.cnf nachziehen; kein externer Konsument (Dienste nutzen eigene DB-User) ⇒ billigste Rotation der Liste
7 TERPENE_ANDROID_KEYSTORE_PW Terpene-Android-TWA-Signing (android/android.keystore, alias terpene) 🔴 am 2026-09-01 (TP41) durch Regex-Suffix-Loch (_PW nicht abgedeckt) ins Transkript geraten Wert in ~/.secrets/credentials.env + Keystore-Datei Store-/Key-Passwort via keytool -storepasswd/-keypasswd ändern + credentials.env nachziehen; Konsument = Android-Build/Play-Signing (Cross-Projekt blaueblüten) — den Key selbst NICHT neu erzeugen
8 N8N_SMTP_PASS (Gmail-App-PW für johnny.claw.0x00@gmail.com) SMTP-Versand aus n8n (Portainer-Stack 174 n8n, macmini) 🔴 am 2026-09-01 (n8n-mcp-SSRF-Session) beim Ausgeben des Stack-174-Files ins Transkript geraten — Redaktionsmuster hatte PASSWORD, aber nicht bare PASS → N8N_SMTP_PASS=<wert> blieb stehen Wert in Portainer-Stack 174 (environment) Gmail-App-Passwort widerrufen (Google-Konto → App-Passwörter) + neues erzeugen, Stack-174-Env nachziehen + redeploy; Konsument = n8n-SMTP-Node(s)

✅ Der Leak-MECHANISMUS hinter #1 ist seit 2026-08-31 zu — der Wert bleibt trotzdem verbrannt. Alle sechs Skripte im Repo, die sshpass -p <PW> benutzten (nautobot/gc-transaktion.py und routeros/import-core-rsc.py sowie die vier TP15-Cutover-Skripte, die am selben Tag nach routeros/archiv-tp15-cutover/ gewandert sind), laufen jetzt über SSHPASS-Env + sshpass -e — auch die archivierten, denn Archiv-Code wird abgeschrieben. Kontrolliert gemessen, nicht behauptet: mit -e taucht das Passwort in 40 ps-Stichproben während einer laufenden Verbindung nicht auf, und die Negativkontrolle (ein Dummy-Wert über die alte -p-Form) wurde 36-mal gesehen — die Probe ist also nicht blind. 🔴 Das ändert an #1 nichts: ein Wert, der einmal in einem Transkript stand, wird nicht dadurch sauber, dass der Weg dorthin geschlossen ist. Rotation bleibt fällig.

🔴 Posten #6 ist die dritte Maskierungs-Panne derselben Klasse (nach SHELLY_DEVICE_PW 08-28 und den Nautobot-Tokens 08-31): das Muster ankerte auf ein Trennzeichen (=), das im konkreten File gar nicht stand (password <wert> mit Leerzeichen). Merkregel, jetzt zum dritten Mal: vor jedem Ausgeben einer Secret-Datei den Schlüsselnamen ankern und das ganze Feld ersetzen, nie auf ein Trennzeichen vertrauen, das evtl. fehlt — und Werte gar nicht erst suchen, um sie zu sehen. #6 ist zugleich der billigste Posten (kein externer Konsument), aber die Panne dahinter ist der teure Teil.

🔴 #7 ist die vierte Maskierungs-Panne derselben Klasse (nach SHELLY_DEVICE_PW 08-28, Nautobot-Tokens 08-31, MariaDB-root 09-01): diesmal ein Regex-Suffix-Loch — das Muster (TOKEN|PASS|PASSWORD|SECRET|KEY|CLIENT_ID) deckte den Suffix _PW nicht ab, also blieb der Wert von TERPENE_ANDROID_KEYSTORE_PW beim maskierten Lesen stehen. Dieselbe Merkregel, erweitert: die Alternation muss PW/PWD mit enthalten (…|PW|PWD|…), sonst greift die Feld-Ersetzung nicht.

🔴 #8 ist die fünfte Maskierungs-Panne derselben Klasse (SHELLY 08-28, Nautobot-Tokens 08-31, MariaDB-root #6, Keystore-_PW #7): diesmal enthielt das Muster zwar PASSWORD, aber nicht das bare PASS — N8N_SMTP_PASS endet auf PASS, nicht PASSWORD, also blieb der Wert stehen. Die Merkregel steht jetzt zum fünften Mal da und wurde zum fünften Mal gebrochen — Konsequenz ist nicht noch eine Notiz, sondern ein fixes, geteiltes Redaktions-Snippet, das man nicht jedes Mal neu (und falsch) tippt: sed -E 's/([A-Z0-9_]*(PASS|PASSWORD|PWD|PW|TOKEN|SECRET|KEY|BEARER))=[^[:space:]]*/\1=<redacted>/g' — ankert auf den Schlüsselnamen inkl. Präfix und deckt alle bisher gesehenen Suffixe ab.

ℹ️ #3 war bis 2026-08-24 TP17.2 und ist jetzt vollständig hier (s. Detailabschnitt unten) — es benutzt denselben Geräte-Durchgang wie #2, ein Reboot statt zwei. #4 gehört sachlich zu TP14.4, das als TP bestehen bleibt (dort steckt das CF-Access-Profil, nicht nur der Token).

Posten #1 im Detail — MikroTik admin: Radius, Etappe 1, Etappe 2

Am 2026-08-24 aus den Follow-ups hierher gezogen — Text unverändert übernommen.

Vorgeschichte des Entscheids (Stand bis 2026-08-23, User: „machen wir, aber erst später"): ⏭️ Nachgelagert seit 2026-08-23: erst TP17.1, dann rotieren. Revidiert noch am selben Tag (User): beides läuft PARALLEL. Der Passphrase-Override je MAC (s.u.) macht den Flag-Day überflüssig, damit war die Reihenfolge nie technisch nötig — nur ökonomisch. Regel für die Parallelität: ein Gerät, das ohnehin auf Thread/Zigbee wandert, bekommt den neuen PSK gar nicht, sondern nur einen Übergangs-Eintrag in der access-list. ⚠️ Der Entscheid betrifft beide Passwörter: der IoT-WLAN-PSK steht im Klartext im TP17-Text (Archiv) und damit auch in der Git-Historie — bleibt auf User-Entscheid 2026-08-23 vorerst unverändert, weil Redigieren ihn nicht aus der Historie holt und Rotieren heute ~22 Shellys kostet. Nach TP17.1 sind es deutlich weniger. 🔴 Hinfällig seit 2026-08-23: TP17.1 ist verworfen — die Geräte bleiben WLAN-Clients, es werden also nicht weniger. Die Rechnung „~23 Shellys + 3 Fremdgeräte" gilt unverändert. Anlass: das Core-Passwort ist bei einer Debug-Ausgabe im Klartext in ein Sitzungstranskript geraten. Ursache war sshpass -p, das es in die Kommandozeile schreibt — sichtbar in ps und in jedem argv-Trace.

🔑 Radius-Befund 2026-08-23: das Admin-Konto teilen sich FÜNF Konsumenten

Vor dem Rotieren erhoben — das ist deutlich mehr als „vier Geräte + eine Datei":

Halter Nachziehen
~/.secrets/credentials.env Datei
~/.claude.json (MikroMCP-Env, ROUTER_*/CRS326_*) JSON + MCP-Neustart
Nautobot-Stack NAUTOBOT_ROUTEROS_USERNAME/PASSWORD Stack-Datei + Portainer-Redeploy auf der DS923
HA: VIER mikrotik-Integrationen (.99.1/.2/.11/.12, 40 device_tracker) 🔴 supports_reconfigure: false — NICHT skriptbar, 4× Re-Auth von Hand
Repo-Skripte (routeros/*.py, nautobot/*.py, docs/tp*.sh) ziehen aus ~/.secrets → automatisch

⇒ User-Entscheid 2026-08-23: Konten trennen statt ein Passwort teilen. Nicht das Rotieren ist das Problem, sondern dass ein einziges admin von fünf Stellen benutzt wird — jede Rotation kostet sonst wieder vier Re-Auths und einen Stack-Redeploy.

✅ Etappe 1 DURCH (2026-08-23): dedizierte Service-Accounts auf allen vier Geräten

Rein additiv, kein laufender Dienst berührt. Vorher existierte nur admin (Gruppe full).

Gruppe Policy User Zweck
grp-ha read,api,rest-api,test ha HA-Mikrotik-Integration, lesend über API 8728
grp-nautobot ssh,read,write,test,sensitive nautobot Golden Config über SSH/Netmiko (Backup und Deploy)

Passwörter: MIKROTIK_HA_PASS / MIKROTIK_NAUTOBOT_PASS in ~/.secrets/credentials.env (32 Zeichen, je Dienst eigen, geräteübergreifend gleich). 🔒 Angelegt über ein Skript, das die Werte aus der Datei liest — bewusst nicht über manage_user, dessen password-Parameter im Sitzungstranskript landen würde.

Abnahme, auf allen vier Geräten geprüft:

  • ha liest (identity, dhcp-lease — am Core 59 Einträge, genau was das Device-Tracking braucht), Schreibversuch wird abgewiesen.
  • nautobot meldet sich per SSH an und exportiert; kein REST (bewusst kein rest-api in der Policy).
  • 🎯 Der Export von nautobot ist byte-identisch zu dem von admin (391/56/87/87 Zeilen je Gerät) ⇒ die GC-Compliance driftet beim Umstellen nicht. Das war das Hauptrisiko dieser Etappe.
  • /user wird von keiner ComplianceRule abgedeckt ⇒ die neuen Konten erzeugen selbst keinen Drift.

⏭️ Etappe 2 offen — die Konsumenten umhängen, DANN admin rotieren

  1. Nautobot auf nautobot umstellen (Stack-Env + Portainer-Redeploy), GC-Backup-Lauf als Abnahme.
  2. HA: die vier mikrotik-Einträge auf den ha-User bringen. ⚠️ Reconfigure ist nicht skriptbar; Entfernen-und-neu-Anlegen wäre skriptbar, riskiert aber die 40 device_tracker-Entity-IDs (sie leiten sich aus der MAC ab, kämen also gleich zurück — aber erst nachdem die alten Registry-Einträge weg sind, sonst hängt HA ein _2 an; dieselbe Falle wie beim Shelly-Rename).
  3. Erst danach admin rotieren — dann berührt es nur noch ~/.secrets + ~/.claude.json.

🔴 Eigener Fehler, protokolliert: beim Erheben des Radius hat ein json.dumps-Aufruf über ~/.claude.json das ap1-Passwort im Klartext ins Transkript geschrieben. Da alle vier Geräte dasselbe admin-Passwort teilen, ist es derselbe Wert, der am 2026-08-22 bereits verbrannt ist — keine neue Expositionsklasse, aber ein zweiter Beleg dafür, dass die Rotation fällig ist. Merkregel: Secrets-Enumeration gibt nur Schlüsselnamen aus, nie Werte — auch nicht abgeschnitten.

✅ Vorbedingung erfüllt, geprüft 2026-08-23: alle vier Skripte laufen auf sshpass -e (tp6-marker/server.py, tp6-monitor-core.sh, tp25-walk-messung.sh, tp25-pin-s22.sh) — die frühere Notiz, zwei davon hätten die Schwäche noch, war veraltet. Übrig blieb nur CLAUDE.md, das sshpass -p weiterhin aktiv als Reboot-Weg empfahl — am 2026-08-23 auf sshpass -e korrigiert. Damit ist die Rotation ohne Rückfallgefahr fahrbar. Weg: manage_user set-password (⚠️ /user set* ist per Deny-Policy blockiert), danach ~/.secrets/credentials.env (ROUTER_*_PASS/CRS326_*_PASS) und ~/.claude.json nachziehen. Es ist ein Passwort für alle vier Geräte — die „Überlebensversicherung" aus CLAUDE.md.

  • 🔎 Nachtrag 2026-08-23 (Recherche-Session, read-only — kein Eingriff): der PSK in der Doku ist noch der LIVE konfigurierte. Gegengeprüft am Manager crs326-core (/interface wifi security, Profil sec-iot → cfg-iot → SSID dirtyvader-iot, datapath.vlan-id=30, wpa2-psk,wpa3-psk) — und zwar per Fingerprint statt Klartext (Länge + drei Zeichenpaare), damit der Wert nicht ein zweites Mal in ein Transkript wandert. Diese Methode ist die Merkregel daraus: einen Verdacht auf Übereinstimmung prüft man mit Stichproben, nicht durch Ausgeben.

  • ✅ User-Entscheid 2026-08-23 zur Git-Historie: belassen, im Nachtrag dokumentieren — kein git filter-repo. Begründung trägt: genau eine getrackte Fundstelle (Archiv), drei Commits (e76289c Ersteintrag 2026-07-11, f0d1ff9 08-08, 7ee64b5 Doku-Split 08-23), Remote ausschließlich gitea.emsenhuber.net, ein lokaler Klon. Eine History-Rewrite hätte hier also nichts gerettet, was nicht ohnehin nur auf der eigenen Infrastruktur liegt — und den Preis (alle Hashes neu) für nichts bezahlt.

  • 📊 Warum die Rotation heute teuer ist — live gemessen 2026-08-23: 26 der 27 WLAN-Clients des gesamten Netzes hängen auf dirtyvader-iot (dirtyvader-main 1, MEDIA/UNTRUSTED/GUEST je 0). VLAN30 führt 41 Leases/Reservierungen (5 aktuell bound, der Rest sind statische Reservierungen ausgeschalteter Geräte). Neben den ~26 Shellys hängen dort P750W (Drucker), Airfryer (espressif, .180), SwitchBot floor-lamp, Twinkly .228, DTUBI .8, SONOFF-NSPanelPro und ein iPhone .177. ⇒ Ein Passwortwechsel ist heute eine Batch-Aktion über ~26 Geräte, und das ist genau die Rechnung, die TP17.1 verkleinert.

  • 🔧 Zwei Umsetzungshinweise fürs spätere Rotations-Paket: (a) mcp/shelly/migrate.py hat kein Rotate-Kommando (nur migrate/deanchor/auth/ table) — es bräuchte eine kleine Variante über denselben set_sta-Pfad. (b) Ein Flag-Day ist vermeidbar: RouterOS 7 kann in /interface wifi access-list Passphrase-Overrides je MAC. Also neuer PSK aufs Profil, Altgeräte übergangsweise per MAC mit dem alten weiter, danach die Liste leeren. 💡 Nebeneffekt: der TP25-Befund „keine access-list" bekäme damit endlich einen Zweck — und seit dem 08-23 gibt es die ComplianceRule wifi-access-list, die Einträge dort auch sichtbar macht.

  • Mitzuziehen bei der Rotation (legitime, gitignorete Ablagen desselben Werts): ~/.secrets/credentials.env (+ .bak-20260808), routeros/.secrets/wlan-psks.env, mcp/shelly/.env.

  • CF-Access-Service-Token — Rotationsposten #4, am 2026-08-24 aus den Follow-ups hierher gezogen (stand dort als „TP14.4 — CF-Access-Service-Token rotieren"). Sachlich gehört er zu TP14.4 (CF-Access-Profil für Claude, Runbook docs/tp14.4-cf-access-token-runbook.md); hier steht er, damit die Rotationsliste vollständig ist. ⚠️ Der CF_API_TOKEN läuft ohnehin 2027-01-09 ab — spätestens dann ist dieser Posten fällig, unabhängig vom Projektende.

Posten #3 im Detail — Shelly-Gerätepasswort (SetAuth), vormals TP17.2

Am 2026-08-24 als eigenes TP aufgelöst und hierher gezogen — Text unverändert übernommen.

🔴 Hochstufung 2026-08-28: SHELLY_DEVICE_PW ist im Zuge des shelly-mcp-Deploys (Session B) erneut in ein Sitzungstranskript geraten — sed 's/=.*/=<set>/' auf der Stack-YML hat den Wert nicht maskiert (Replacement-Pattern zu wenig greedy). Datei selbst ist gitignored/chmod 600, aber das Transkript ist die Expositionsklasse. Folge: Vorziehgrund, Posten #3 ist ab jetzt der dringendste offene Rotationspunkt — vor #2 (PSK, der schon länger im Klartext in der Git-Historie steht) und #1 (MikroTik admin, der schon zwei Transkript-Leaks hat, aber beide male dasselbe PW, nicht vier verschiedene Quellen wie hier). Reihenfolge: erst Werkzeug-Härtung (sed-Replacement absichern ODER tr-Stream-Filter vor jedem Read einer Secret-Datei), dann Posten #3 rotieren (alle 23 Shellys + HA-Re-Auth-Flow).

User 2026-08-23: „außerdem haben wir einen punkt, um die shellys mit einem pwd zu schützen." Damit ist der Zurückgestellt-Status von 2026-07-13 aufgehoben. Begründung der Kopplung: die PSK-Rotation fasst ohnehin jeden Shelly an und rebootet ihn — SetAuth gehört in denselben Durchgang, ein Reboot statt zwei.

✅ Werkzeugseitig ist nichts zu bauen: mcp/shelly/migrate.py hat cmd_auth (setzt Shelly.SetAuth mit SHA-256-ha1 gegen die Geräte-realm, Dry-Run als Default), und _rpc hat einen Digest-Auth-Fallback auf 401 mit SHELLY_DEVICE_PW.

🔴 Der teure Teil ist nicht SetAuth, sondern Home Assistant. Mit aktiver Auth verlangt die Shelly-Integration je Gerät einen Re-Auth-Flow — bei 23 Geräten ist das der Aufwand, und genau der Grund für die frühere Einstufung als „Kosmetik". ⇒ Erst an EINEM Gerät machen und den HA-Aufwand messen (Vorschlag s15 technik_pve, RSSI −36, unkritisch), bevor der Rest folgt. Sonst bündelt man ein ungemessenes Risiko in eine 23-Geräte-Aktion.

Ursprünglicher Text (historisch)
  • Was: je Gerät Shelly.SetAuth (User admin, realm-individueller ha1 = sha256("admin::"), einheitliches Klartext-PW) via mcp/shelly/migrate.py auth --go. SHELLY_DEVICE_PW liegt in mcp/shelly/.env (gesetzt, len 13). Runner + Digest-Auth-Fallback in _rpc fertig.
  • Warum zurückgestellt: rein kosmetisch/Härtung; bricht die HA-Shelly-Integration, bis pro Config-Entry (22×) das Credential neu eingetragen ist → disruptiv, kein Mehrwert im abgeschotteten IOT-VLAN. Erst nach dem Netz-Teardown (TP19) neu bewerten.

🔴 Neues Leak-Ereignis 2026-08-24 — der IoT-PSK ist erneut exponiert

Beim 5-GHz-Umfeldscan für TP25 wurde mcp__mikromcp__list_wifi_interfaces gegen den Manager gefahren, um die cap-wifiN-Zuordnung zu bestimmen. Das Tool gibt security.passphrase aller Profile im Klartext zurück — damit stehen alle fünf WLAN-Passphrasen (main, iot, media, untrusted, guest) in einem Sitzungstranskript. Kein Werkzeug hat versagt; es war das falsche Werkzeug für eine Kanalabfrage. Betrifft Posten #2 (IoT-PSK) und zusätzlich vier bisher unverbrannte Passphrasen. ⚖️ Nach der „harten Ausnahme" oben ist das ein Vorziehgrund — Entscheidung gehört Christoph. Gegenargument, das dieselbe Rechnung wie am 08-23 ergibt: der IoT-PSK lag ohnehin schon im Klartext in der Git-Historie, neu exponiert sind vor allem main/media/untrusted/guest — und die kosten beim Rotieren wenige Clients, nicht 26. ⇒ main und guest wären billig und einzeln machbar, unabhängig vom gebündelten Durchgang.

Fallen, die für jedes Setzen/Rotieren gelten (nicht neu erheben)

  • 🔴 Kein Read auf /interface/wifi, wenn nur Kanäle/Interfaces gebraucht werden — list_wifi_interfaces liefert alle Passphrasen im Klartext mit (Leak vom 2026-08-24, s.o.). Feldauswahl benutzen oder /interface wifi channel print detail.
  • 🔴 Niemals sshpass -p — schreibt das Passwort in die Kommandozeile, sichtbar in ps und in jedem argv-Trace. Genau so entstand Leak #1. -e hält es in der Umgebung des Kindprozesses. Der Repo-Stand ist bereits durchgängig -e.
  • 🔴 Passwörter nie per MCP-password-Parameter setzen, sondern per Skript aus ~/.secrets (manage_user set-password; /user set* ist per Deny-Policy blockiert).
  • 🔴 Keine Secrets-Enumeration im Transkript — das war Leak #2, ohne dass irgendein Werkzeug fehlerhaft war.
  • 🔒 wifi-security ist als ComplianceRule grün, aber NICHT deploybar (Passphrasen stehen als <<REDACTED>> im Template). Ist-Stand für #2 immer per REST vom Gerät lesen (GET http://10.40.99.1/rest/interface/wifi/security), nie aus wlan-psks.env.
  • ⚠️ CAPsMAN, für #2: eine Profiländerung wirkt erst nach remote-cap provision für beide APs — und die Abnahme ist eine frische DHCP-Lease je SSID, nicht die Registration-Table.
  • ⚠️ Für #2 zuerst multi-passphrase gegen WPA3/SAE testen — auf einer Wegwerf-SSID, nie an dirtyvader-iot. sec-iot fährt wpa2-psk,wpa3-psk; trägt multi-passphrase SAE nicht, sperrt man 26 IoT-Geräte auf einmal aus, inklusive Nuki und NSPanel.
  • ⚠️ s1/s2/s3 sind ausgesteckt und kommen im Dezember mit dem alten PSK zurück — der Übergangs-Eintrag muss sie überleben oder sie werden bei Rückkehr von Hand nachgezogen.
  • 🔴 HA- und Repo-Nummerierung der Shellys weichen ab (s10, s17) — immer über MAC/IP zuordnen, nie über die s-Nummer.
  • Abh.: keine harten; #2 und #3 laufen sinnvollerweise in einem Geräte-Durchgang. Blockiert nichts. Die frühere Kopplung „erst TP17.1, dann rotieren" ist gegenstandslos — TP17.1 ist am 2026-08-23 verworfen, die Geräte bleiben WLAN-Clients.

TP31 — Reverse Proxy für innen und außen; cloudflared von sechs Knoten auf einen ▸ ✅ ABGESCHLOSSEN 2026-09-01

✅ Durch — Schritt 5+6 sind über den Piloten TP16.3 miterledigt (2026-09-01): bf-public healthy, 12 Connectoren auf ap1/ap2/ap3; vw/hass/n8n/gitea laufen über CF → Caddy (https://10.40.20.40:443), die drei Alttunnel bf-nas/bf-pve/bf-mm gelöscht. Damit steht das Zielbild: eine Host→Origin-SoT im Caddyfile, öffentlich nur vier Namen. Abnahme-Tool caddy/cf-sync.py grün (2026-09-02: die vier geflippten Namen wurden vorher als Fehlalarm gemeldet — das Tool prüfte nur http://, gebaut ist https://…:443; Einzeiler-Fix in cf-sync.py). 📎 Überholt: Schritt-6-Teil „MCP-Clients auf FQDN umstellen" ist durch TP35 Schicht 1 andersherum entschieden (FQDN→LAN-IP). 🧹 Rest-Kleinkram (verwaistes grafana-Zertifikat) läuft mit dem Grafana-Wiederaufbau nach TP32. Auslöser (User 2026-08-24): „ich weigere mich, für public applications 2 cloudflare container zu installieren. … ich habe [den Core] unter der prämisse gekauft, dass über diesen dann auch die public apps laufen können. wir wollten doch den core als single point of failure platzieren."

Der Vorwurf, fair geprüft

Eine schriftliche Zusage, dass die Public-Apps auf den Core wandern, existiert nicht — TP16.1 ist durchgehend auf den privaten Zugang zugeschnitten und hält mehrfach fest, dass „der cloudflared-Public-Teil unberührt blieb". Aber der Vorwurf trifft trotzdem etwas Richtiges: dasselbe Dokument begründet die gesamte Auswahl mit Christophs Prinzip — „Router = einziger SPOF, keine Abhängigkeit zu anderen Rechnern" — und niemand hat je geprüft, ob der öffentliche Strang ohne sechs Container auskommt. Keine Täuschung, aber eine ungeprüfte Annahme.

Die Lösung ist nicht „raus aus Cloudflare", sondern „ein Knoten statt sechs"

Caddy als Reverse Proxy für innen und außen, emsenhuber.net in beiden Richtungen, plus ein cloudflared als Tunnel-Endpunkt.

  • ⚖️ Erst geprüft, dann verworfen: der Weg über dst-nat. Der Core hält 90.146.33.177 direkt ohne CGNAT, Liwest lässt eingehend alles durch (User: „hatte früher schon nginx hier laufen"), und Origin Rules (Ziel-Port, Free = 10 Regeln), die CF-HTTPS-Ports (443, 2053, 2083, 2087, 2096, 8443) sowie Authenticated Origin Pulls sind alle im Free-Tarif enthalten. Es ginge also ohne jeden Container.
  • 🎯 Christophs Einwand ist der bessere Weg: „caddy muss so oder so. nachdem dieser vorhanden ist, fahren wir natürlich über zero trust apps. noch etwas sicherer, aber kein zusätzlicher knotenpunkt, der ausfallen kann." Der Tunnel spart drei Risiken auf einmal: keine angreifbare Origin-IP, keine CF-IP-Allowlist, kein DDNS als Voraussetzung (der TP10-Updater bliebe sonst zwingend).

Zielbild

Internet ─► Cloudflare (Access, WAF) ─► Tunnel »bf-public« ─► cloudflared auf ap1/ap2
WARP ─► cf-mesh (nativ am Core, kein Container) ─┐
LAN  ─► Split-Horizon-DNS ───────────────────────┴─► Caddy (LXC 130, 10.40.20.40)
                                                      └─► alle Dienste per Host-Header

Öffentlich bleiben vier: vw (Bitwarden-Clients), hass (Companion-App), n8n (Webhooks Dritter), gitea (Access nur für die Web-UI, Git-/API-Pfade daran vorbei ⇒ Push ohne WARP bleibt). Alles andere ist intern; 19 Ingress-Zeilen über drei Tunnel werden EINE Catch-all-Regel auf Caddy.

Wo cloudflared läuft — und warum die APs (live gemessen 2026-08-24/25)

Gerät Arch Flash frei RAM frei Container?
ap1 / ap2 (hAP ax²) arm64 95 MB / 128 MB 690 MB / 1 GB ✅ ja
crs326-core arm 32-bit 2,9 MB / 16 MB 512 MB ❌ nie
  • ✅ Christophs Argument, das den Ausschlag gibt: „cf auf den aps hat den vorteil, dass wir gegenüber unseren hosts/container nicht blind sind." — Der AP ist eine eigene Maschine, kein Docker-Host; die macvlan/ipvlan-Blindheit kann dort prinzipiell nicht auftreten. Kein anderer Ort hat das. 💡 Daraus folgt eine Reserve: fällt Caddy aus, kann derselbe Connector über eine zweite Ingress-Regel direkt auf ein, zwei kritische Backends zeigen.
  • 🔴 Ehrliche Einschränkung: cloudflareds einziges Origin ist Caddy. Solange Caddy auf pve liegt, kauft ein Connector auf dem AP Hardware-Diversität, aber keine Verfügbarkeit — fällt pve aus, ist alles weg. Der echte Gewinn kommt erst mit caddy-bf2 (LXC 131).
  • Caddy oder AGH zusätzlich auf die APs? Nein. 95 MB minus RouterOS-Reserve fürs eigene .npk (~20 MB) = ein Container pro AP. Kriterium, das auch künftig trägt: auf die Infrastruktur darf, was sich selten ändert und wenig schreibt — cloudflared ja (Config liegt in Cloudflare, schreibt nichts); Caddy nein (Caddyfile ändert sich bei jedem neuen Dienst); AGH nein (Query-Log schreibt dauernd → NAND-Verschleiß).
  • ⚠️ Preis: container verlangt eine device-mode-Änderung mit physischer Bestätigung am Gerät (ap1 Technik UG, ap2 Technik OG). Und der AP bekommt eine zweite Rolle neben dem WLAN.

Umsetzung

  1. ✅ ERLEDIGT 2026-08-25 — Leichen gelöscht: 120 adguard-bf1, 121 adguard-bf2, 128 nginx-proxy-manager, 129 caddy, 130 pihole1, 131 pihole2 — alle waren stopped, onboot: 0 und hingen konfigurativ noch am toten VLAN1 (10.40.41.116/.117, GW .41.254). pct destroy --purge hat 120 und 128 zugleich aus dem Backup-Job entfernt. Alle 72 PBS-Snapshots radiert — sieben davon waren in PBS als protected markiert und mussten erst über pvesh set …/content/<volid> --protected 0 entschützt werden (User-Freigabe 2026-08-25). Gegenprobe: 0 Snapshots zu allen sechs IDs. ⚠️ Merkregel: pvesm free scheitert an geschützten Snapshots mit „cannot remove protected snapshot" — und zwar stillschweigend, wenn man den Fehler nicht ausliest. Wer nur die Erfolgszähler betrachtet, hält ein halb erledigtes Aufräumen für fertig.
  2. ✅ ERLEDIGT 2026-08-25 — LXC 130 caddy-bf1 steht: Debian 13, VLAN20 tag=20, 10.40.20.40/24, GW/DNS 10.40.20.1, onboot: 1, startup order=10 (Core-Stufe nach TP32), 4 GB / 512 MB, 2 Cores. In den Backup-Job backup-6dcf5c10-610f aufgenommen. Caddy v2.11.4 installiert, Modul dns.providers.cloudflare verifiziert vorhanden. Egress geprüft (DNS, HTTPS, CF-API) — TP5 lässt VLAN20 nach draußen.
  3. ✅ ERLEDIGT 2026-08-25 — Token da, deploy.sh durchgelaufen, Proxy verifiziert. caddy/Caddyfile im Repo (17 Sites: 4 öffentlich vw/hass/n8n/gitea, 13 intern inkl. der vier MCP-Endpunkte und drei HTTPS-Backends mit tls_insecure_skip_verify), Syntaxprüfung läuft durch. Zertifikate ausschließlich per DNS-01, damit auch rein interne Namen gültige Zertifikate bekommen. 🔑 Das eng geschnittene Token liegt — Zone:DNS:Edit, Zone-Resource auf emsenhuber.net begrenzt, nicht das account-weite CF_API_TOKEN (Fingerprint gegengeprüft). Ablage als CF_DNS_API_TOKEN in ~/.secrets/credentials.env, Ausrollen per caddy/deploy.sh — das liest den Wert von dort und schreibt ihn in die Container-Env, ohne ihn je auf stdout oder in eine Kommandozeile zu bringen (dieselbe Regel wie sshpass -e).
  4. 📋 Split-Horizon → ENTSCHIEDEN 2026-08-25: in AdGuard, zusammen mit TP23 Stufe 2. ➡️ Übergabe steht (2. Fassung, 08-25 spät): handoff-20260825-agh-dns-cutover.md — mit drei live erhobenen Funden, die den ersten Plan korrigieren: (a) der Bestands-AGH 10.40.20.16 ist ein Archiv, keine Vorlage (24 Rewrites: 8 ins tote VLAN1, 9 auf den heute zerstörten NPM, 2 FritzBox-Reste) ⇒ Upstreams ja, Rewrites neu; (b) 10.40.20.17 ist adguard_fm2, gehört FM und antwortet ebenfalls ins tote VLAN1 — BF hat heute einen AGH, keine Redundanz; (c) die TP5-Vorratsregeln zeigen hart auf 10.40.20.16 — nach dem DHCP-Flip wird DNS zu einem forward-Fluss über VLAN-Grenzen, ohne erweiterte Regeln verlieren IOT/MEDIA/UNTRUSTED/GUEST/MGMT die Auflösung, während VLAN20 unauffällig weiterläuft. Dazu: lease-time=1d macht Flip und Rollback träge, Rückfallebene bleibt der Core. Begruendung des Users: nichts zweimal bauen. Konsequenz, die man kennen muss: AdGuard wird damit vom Nebendarsteller zum Resolver des Standorts — heute liefert der DHCP in allen sieben VLANs dns-server = <VLAN>.1, also den Core; AGH auf 10.40.20.16 bekommt niemand. ⇒ Der Schritt ist groesser als „ein paar Eintraege": er verschiebt die DNS-Aufloesung des ganzen Hauses vom grundsoliden Core in einen Container. 🔑 Deshalb als Paar bauen — TP32 reserviert 110/111 fuer DNS, und die geloeschten Altlasten hiessen nicht zufaellig adguard-bf1/adguard-bf2: die Redundanz war urspruenglich schon einmal geplant. Ein einzelner AGH waere ein DNS-SPOF auf pve.
  5. ✅ ERLEDIGT via TP16.3 §5 (2026-09-01) — Tunnel bf-public + Connectoren auf ap1/ap2/ap3 (12 conns healthy), CNAMEs geflippt (n8n/gitea/hass/vw → CF → Caddy https://10.40.20.40:443). Je Name einzeln geflippt, dazwischen gemessen; Antwortzeiten vorher/nachher unverändert.
  6. ✅ ERLEDIGT via TP16.3 §6 (2026-09-01) — Abbau: bf-nas/bf-pve/bf-mm gelöscht, 7 cloudflared-Container weg. 🔴 Ausnahme, bewusst umgekehrt: der Teil „MCP-Clients auf FQDN umstellen" ist durch TP35 Schicht 1 andersherum entschieden — gitea-mcp/n8n-mcp sprechen heute die LAN-IP (10.40.20.167:3000/:3001), nicht den FQDN. Der ursprüngliche Plan ist damit gegenstandslos, nicht offen.

✅ Stand 2026-08-25 nach dem Bau — Caddy laeuft, zwei Backends fielen dabei auf

17 von 17 Zertifikaten per DNS-01 von Let's Encrypt geholt, Dienst active+enabled, Proxy-Funktion gegen den Host-Header verifiziert: nautobot 302 · gitea 200 · pve 200 · portainer 200. caddy/deploy.sh laeuft End-to-End (Token wird aus ~/.secrets gelesen, per Fingerprint gegengeprueft: eigenes Token, nicht das account-weite).

🔴 Fund: grafana war seit Langem tot — und Cloudflare Access hat es maskiert. LXC 104 alpine-grafana stand auf stopped und konfigurativ noch auf VLAN1 (10.40.41.109); die CF-Ingress-Zeile zeigte auf 10.40.20.109:3000 — eine Adresse, die es fuer Grafana nie gab. Im FQDN-Smoke-Test lieferte grafana.emsenhuber.net trotzdem ein sauberes 302 — das war der Access-Login, nicht der Dienst. ⚠️ Das ist exakt die Falle aus TP16.3 §8, diesmal in freier Wildbahn: ein Statuscode vom oeffentlichen FQDN sagt nichts ueber das Origin, solange Access davor steht. Ein toter Dienst sieht monatelang gesund aus. ⇒ Caddy-Zeile entfernt (16 Sites), LXC 104 zerstoert und aus dem Backup-Job genommen. Die 14 PBS-Snapshots bleiben bewusst stehen. Am 2026-08-25 auf User-Entscheid ebenfalls weg — zusammen mit den vier gestoppten Gaesten 103 minarca, 109 node-red, 115 hms-mqtt-publisher und 201 openproject (28 Snapshots). Damit laeuft auf pve jeder existierende Gast, 11 LXC + 2 VMs, kein stopped mehr. ⚠️ Snapshot-Liste vor dem destroy einsammeln — pvesm list --vmid meldet danach 0, obwohl die Backups noch liegen** — direkt gegen den Datastore gegengeprueft (ct/104 = 0), nicht nur gegen pvesm list --vmid, dessen Filter nach dem destroy nicht mehr aussagekraeftig ist. Wiederaufbau spaeter als eigener Punkt, dann nach TP32-Systematik.

✅ Abnahme 2026-08-25 (spaet) — 16 von 16 Sites gegen den Host-Header gemessen

Dienst active+enabled, Probe von einem zweiten Host (pve, VLAN20) mit curl --resolve <fqdn>:443:10.40.20.40, also ueber den echten Namen statt ueber die IP: 11x 200 (vw n8n gitea ds-bf ccu iobroker z2m25 n8n-mcp pve portainer pbs), nautobot 302 (Login-Redirect, erwartet), Antwortzeiten 0,02–0,11 s.

  • ✅ Die drei „Fehlercodes" der MCP-Endpunkte sind KEINE: pve-mcp 400, ha-mcp 403, gitea-mcp 404 kommen identisch auch direkt vom Origin (gegengemessen ohne Caddy). Es sind die Antworten der MCP-Server auf ein nacktes GET / — der Proxy reicht sie unveraendert durch. ⇒ Merkregel: einen Statuscode nie ohne Gegenprobe am Origin als Proxy-Fehler lesen — sonst jagt man einen Fehler, der keiner ist (dieselbe Klasse wie der grafana-Fall eine Zeile hoeher, nur andersherum).
  • 🧹 Rest: ein verwaistes Zertifikat. Der Store haelt 17 Zertifikate, die Config nur 16 Sites — grafana.emsenhuber.net liegt noch da. Harmlos (Caddy erneuert es nicht mehr), beim Grafana-Wiederaufbau nach TP32 mit aufraeumen.

✅ hass liefert 200 — gelöst 2026-08-26, und die Ursache ist eine Falle für jeden Reverse Proxy. Der http:-Block in configuration.yaml war wirkungslos: Home Assistant hat die http-Konfiguration nach .storage/http migriert ("yaml_migration_done": true, created_at 2026-08-05). Seither wird der YAML-Block ignoriert — ohne Fehlermeldung.

  • 🔎 Warum das so schwer zu sehen war: die Logzeile lautet Received X-Forwarded-For header from an untrusted proxy 10.40.20.40 und nicht „not set-up for reverse proxies". Laut Quelltext fällt die erste nur bei aktivem use_x_forwarded_for — was zur Annahme führt, die Option greife und nur die Liste sei falsch. Tatsächlich kam beides aus .storage: use_x_forwarded_for: true stand dort längst, die Liste enthielt .20.10, .20.126, .20.167 (die drei cloudflared-Origins) — nur Caddy fehlte.
  • 🧪 Was den Verdacht in die Irre führte, der Reihe nach ausgeschlossen: Datei-Identität (ein Testskript in python_scripts/ tauchte sofort als Service auf ⇒ HA liest dieses Verzeichnis), Blockposition (nach ganz oben verschoben — keine Änderung), Kommentare/Formatierung (minimaler Block — keine Änderung), Prozesszeitpunkt (.ha_run.lock start_ts gegen die Datei-mtime gestellt) und Schema-Validierung (ein absichtlich ungültiger Schlüssel im http:-Block erzeugte keinen Config-Fehler — im Nachhinein der eigentliche Beweis).
  • ✅ Der vorgesehene Weg ist ein WS-Zweischritt mit Auto-Revert: http/config lesen → http/config/configure mit der vollständigen Config (setzt pending und startet HA neu) → innerhalb von 5 Minuten http/config/promote, sonst fällt HA automatisch auf stable zurück. ⚠️ Der Promote ist knapp: HA braucht hier ~2 min bis zur Erreichbarkeit, und der WS-Weg über das MCP-Add-on antwortet währenddessen mit 502. Erst promoten, wenn die Wirkung gemessen ist — genau dafür ist das Fenster da.
  • 🧹 In configuration.yaml steht jetzt ein Hinweis-Kommentar statt eines wirkungslosen Blocks.
  • ✅ Trusted-Liste aufgeräumt (User, 2026-08-26): 10.40.41.10/32 — die alte VLAN1-NAS — ist entfernt und der Stand als stable bestätigt. Vertraut wird jetzt genau dem, was auch wirklich proxyt: 10.40.20.40 (Caddy) sowie .20.10/.20.126/.20.167 (die cloudflared-Origins, die mit Schritt 6 wegfallen), dazu Loopback und 172.16.0.123. Abnahme: hass/vw/gitea/n8n je 200.

🧭 Wo liegt die Source of Truth? — beantwortet 2026-08-26

User-Frage: „schaffen wir es mit überschaubarem aufwand cf und caddy synchron zu halten? wo liegt die sot?"

Antwort: gar nicht synchron halten — die Doppelpflege fällt mit Schritt 5/6 von selbst weg. Heute steht dieselbe Zuordnung Name → Origin an zwei Orten: im CF-Ingress (19 Regeln über drei bf-Tunnel) und im caddy/Caddyfile (16 Sites). Der Vergleich am 08-26 zeigt: sie stimmen fachlich überein — jeder gemeinsame Name zeigt auf dasselbe Origin. Es gibt also nichts zu versöhnen, nur etwas abzuschaffen.

Im Zielbild hat der Tunnel bf-public eine einzige Catch-all-Regel auf 10.40.20.40. Damit hält Cloudflare keine Host→Origin-Zuordnung mehr, und die SoT ist eindeutig:

Was SoT wo gepflegt
Name → Origin (alle Dienste) caddy/Caddyfile (Git) dieses Repo
Welcher Name ist öffentlich CNAME + Access-App (4 Namen) Cloudflare
Name → IP intern AdGuard-Rewrites (TP23 Stufe 2) AGH-Paar

🔴 Standortsuffix als Regel (User-Entscheid 2026-08-26). Hosts, die es an mehreren Standorten gibt, heißen nie ohne Suffix (nas.emsenhuber.net ist die Falle) — Schema ist <host>.<site>.emsenhuber.net (nas.bf.…, nas.fm.…, nas.dw.…). Caddy bleibt pro Standort eigenständig (BF-Caddy kennt ds-bf, FM-Caddy kennt ds-fm); die AdGuard-Instanzen je Site halten die lokale <host>.<site>-Auflösung, und ein gemeinsamer Name ohne Suffix existiert nicht. Konsequenz für nas.emsenhuber.net: gelöscht — weder AGH-Rewrite, noch CF-CNAME (gab es nie), noch Caddyfile-Eintrag (das hatte von Anfang an ds-bf). Der Rewrite war eine AGH-Insel ohne Auflöser dahinter, der Cutover ändert für niemanden etwas an der heutigen Erreichbarkeit. Andere namensgleiche Hosts (macmini/mfc/tv) stehen heute noch ohne Suffix in AGH — Bewertung einzeln in der TP23-Stufe-2-Session; hier nur festgehalten, damit das Muster ab jetzt nicht mehr ohne Suffix entsteht.

⇒ Zu pflegen bleiben vier öffentliche Namen statt neunzehn Ingress-Regeln. Und die zwölf internen Namen brauchen dann gar keinen öffentlichen DNS-Eintrag mehr — heute ist z. B. pve.emsenhuber.net öffentlich auflösbar (hinter Access), danach ist es von außen schlicht unbekannt. Das ist ein Sicherheitsgewinn, kein Nebeneffekt.

🛠️ Werkzeug dafür: caddy/cf-sync.py — read-only, liest CF-Ingress, Zonen-DNS und Access-Apps und stellt sie gegen den Caddyfile; Exit 1 bei Drift (cron-/CI-tauglich). Erster Lauf 2026-08-26 fand 7 echte Abweichungen, alle in Cloudflare, keine in Caddy:

  • grafana steht noch im Ingress (bf-nas → 10.40.20.109:3000) — ein Origin, das es nie gab.
  • nautobot hat noch seinen CNAME, aber seit TP16.3 keine Ingress-Regel ⇒ läuft in die Catch-all; das ist die Quelle des 302, das man leicht für „lebt noch" hält. Dasselbe bei gateway-dw.
  • Drei *.bf-CNAMEs zeigen auf nginx.bf — den heute zerstörten NPM (phpmyadmin, speedtest, z2m25).
  • Eine Wildcard-Access-App *.emsenhuber.net deckt die ganze Zone ab — sie ist der Grund, warum jeder FQDN ein sauberes 302 liefert, auch wenn dahinter nichts läuft. 🔑 Merkregel daraus: ein Eintrag ohne Gegenstück ist unsichtbar, solange etwas davor antwortet. Der Drift-Check muss deshalb beide Richtungen prüfen — Ingress ohne Site und CNAME ohne Ingress.

HA-Ausblick (nicht Teil der Umsetzung)

Es bleibt bei Caddy — zustandslos bis auf den Zertifikatsspeicher, identisches Caddyfile je Instanz, versionierbar; NPM hätte eine Datenbank, die man mitreplizieren müsste. 🔴 Prinzip für vw: der Eingang darf nicht klüger sein als die Datenbank. TP20.1 sieht manuelles Promote vor. Zwei Connectoren auf verschiedene Caddys an verschiedenen Standorten würden die Hälfte der Schreibzugriffe auf einen read-only Standby schicken — das #1033-Muster, diesmal mit Datenverlust statt 530ern. Automatisches Ingress-Failover erst, wenn der DB-Teil es trägt (User: vertagt bis TP20.1 Phase 1+2 laufen).

  • Abh.: TP32 (Nummern/IPs — vergibt 130/.40), TP16.1 ✅ (Mesh), TP23 Stufe 2 (Split-Horizon-Ort). Löst ab: TP16 (reziprokes HA) und TP16.2 (der vw-bypass-Strang bekommt mit Caddy + CF davor einen anderen Schutz). TP16.3 ist der Pilot dieses TP.
  • 📋 Vollständiger Plan mit allen Abwägungen: ~/.claude/plans/expressive-pondering-clarke.md

TP32 — Nummern- und Adress-Systematik ▸ 🟢 KONZEPT STEHT (2026-08-25), Startreihenfolge bereits repariert

User-Auftrag 2026-08-24: „du/ich sollst in der lage sein, selbstständig nach einem vorgegebenen system freie ips vergeben zu können … und diese systematik findet sich in anderen netzen künftig auch wieder" · „ich meine 100 sollte künftig für die core services stehen."

Eigener TP, weil es eine Konvention ist, kein Bauteil — TP31 ist ihr erster Kunde. Regel: Bestand bleibt, wo er ist; jeder NEUE Gast folgt der Systematik.

🔑 Ein gemeinsamer VMID-Raum für VMs und LXCs. 100 haos und 330 RaspberryMatic sind VMs — die Klasse bestimmt die Nummer, nicht die Virtualisierungsart.

Kreis Klasse heute dort (VM kursiv)
100–199 Core / Infrastruktur 108 mcast-relay
200–299 Daten / DBs (Precious-Tier) (105, 106, 107, 302 → später hierher)
300–399 Security / Identity —
400–499 Platform / Runtime ✅ 400 docker passt bereits
500–599 App-Services / Smart Home 101, 102, 112, 150, 100, 330 → später hierher
600–699 Business / ERP 201 openproject
700–799 KI / ML —
800–899 Dev / R&D / Test —
900–999 Ephemeral / Scratch wird ungefragt gelöscht

Zehnerschritte innerhalb eines Kreises („Respektabstand", User), damit Instanzen und Nachbarn Platz haben: 100–109 netznah · 110–119 DNS · 130–139 Reverse Proxy · 150–159 Monitoring · 170–179 PKI/Zeit. ⇒ AGH 110, Caddy 130.

Letztes Oktett je /24 — gilt in jedem VLAN, an jedem Standort:

Oktett Rolle Anker heute
.1–.9 Gateways / L3 .1 je VLAN ✅ · 99.1 core, 99.2 branch ✅
.10–.29 physische Hosts (Storage, Hypervisor, APs) NAS 20.10 ✅ · APs 99.11/.12 ✅
.30–.99 virtuelle Core-Services, Zehnergruppen wie oben DNS .3x · Ingress .4x · DBs .5x · Security .6x · Monitoring .7x
.100–.199 App-Services
.200–.249 DHCP-Pool
.250–.254 VIPs / Relays

🔑 Der Merk-Trick: die Zehnergruppe im Oktett spiegelt die der VMID. 110 agh → .3x, 130 caddy → .4x. Wer eine Zahl kennt, kennt die Klasse der anderen.

Warum der Bestand anders aussieht — er war nicht falsch, nur auf einer anderen Achse

User 2026-08-25: „zuerst war haos, welches mit 100 den nummernkreis für iot eröffnet hat. und die anderen … gehörten zu ha, daher 1xx. erst später habe ich erfahren, dass die nummer die startreihenfolge definiert." ⇒ Der Bestand gruppiert nach Zugehörigkeit, die neue Systematik nach Klasse. Beide Achsen sind legitim; die zweite ist die, die das System auswertet. Der alte 1xx-Block ist der Smart-Home-Block und wandert später als Gruppe nach 5xx.

✅ ERLEDIGT 2026-08-25: die Startreihenfolge war nachweislich falsch

🔴 Und der Hebel ist NICHT die VMID. Drei Gäste hatten ein explizites startup: order= (101 iob-master 110, 102 mqtt5 120, 100 haos 125), alle übrigen keines. Proxmox startet erst alle Gäste mit order=, dann den Rest nach VMID — real lief also iob-master → mqtt5 → haos und erst danach platform-pg, mariadb, z2m25, docker. ⇒ Home Assistant startete vor seiner eigenen Datenbank.

Repariert (wirkt beim nächsten pve-Neustart):

order= Stufe Gäste
10 Core 108 mcast-relay
20 Daten 105 platform-pg (up=15), 302 mariadb (up=15), 106 influxdb2, 107 redis
30 Platform 400 docker (up=20)
40 Broker / Funk 102 mqtt5 (up=10), 112 z2m25, 330 RaspberryMatic
50 / 51 Anwendungen 101 iob-master, 100 haos, 150 iob-exposed

🎯 Die wichtigste Erkenntnis des TP: VMID = Klassifikation, startup: order= = Abhängigkeitsreihenfolge. Die Umnummerierung muss die Startreihenfolge gar nicht reparieren — das ist bereits erledigt, ohne eine einzige ID zu ändern. Die Nummernkreise sind damit reine Ordnung und Lesbarkeit, und der Bestand kann in Ruhe nachziehen.

  • Offen: Migration des Bestands (Tabelle oben) — am Ende der Reise, nicht jetzt. Und die Kollision AGH will .30, dort sitzt Nautobot (.31 = nautobot-worker) ⇒ Vorschlag .36 als Interim, .30 erst nach dem Nautobot-Umzug in den App-Bereich. Fällt vor dem AGH-Neubau an.
  • Abh.: keine. Blockiert nichts, aber TP31 vergibt bereits danach.

TP33 — Sonos aus der Ferne bedienen (VPN/WARP) ▸ 💡 IDEE, bewusst zurückgestellt (angelegt 2026-08-25, User-Entscheid „später entscheiden")

  • Auslöser: beim Sonos-Fall vom 08-25 kam die Frage auf, ob die Player auch von unterwegs auffindbar sein sollen. Antwort für jetzt: nein, später entscheiden — hier nur festgehalten, damit die Begründung nicht verloren geht.
  • 🔑 Der Kernsatz: SSDP und mDNS queren einen Tunnel nicht von selbst. Beides ist link-lokal (TTL 1 bzw. Multicast auf 224.0.0.251), ein WireGuard-/WARP-Tunnel ist ein Punkt-zu-Punkt-Interface ohne Broadcast-Domäne. Es bräuchte also dieselbe Relay-Bauart wie TP7.1, nur ein zweites Mal — ein Bein im Tunnel, eines in VLAN40 — plus die passende forward-Regel (TP5 ist deny-by-default).
  • ⚖️ Warum zurückgestellt: echter Zusatzaufwand für einen Randfall. Steuern aus der Ferne geht heute schon über die Sonos-Cloud-App und über HA (cf-mesh/CF-Tunnel), beides ohne Discovery. Fehlt nur die Desktop-App von außen — dafür lohnt der zweite Relay-Strang derzeit nicht.
  • ⚠️ Nicht mit dem Gegenteil verwechseln: mit aktivem WARP findet die App die Player auch lokal nicht — erwartbares Verhalten, kein Fehler (ref_warp_local_icmp_trap, Gotcha in CLAUDE.md). Wer den Fernzugriff baut, muss genau diesen Fall mit auflösen.
  • Abh.: TP7.1 (Relay-Muster) ✅, TP16.1 (cf-mesh) ✅. Blockiert nichts.

TP34 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) ▸ 🔜 STUB

Konzept + Architektur-Skizze: docs/handoff-20260827-inventur-und-architektur.md — bitte dort weiterlesen (Architektur-Abschnitt).

  • Scope: Reverse-Proxy-Hosts werden im Nautobot-Datenmodell gepflegt (Site, Host, Backend-Spec). Golden Config rendert den Caddyfile aus den Daten, Diff-Check gegen die Live-Version auf caddy-bf1, Compliance-Checks + Transaktionen identisch zum bestehenden Workflow für Router-Configs.
  • Auslöser (Inventur 2026-08-27): der Caddyfile (bf-infra/caddy/Caddyfile) ist heute manuell gepflegt — kein Drift-Check, keine Transaktionen, andere Projekte können keine Sites anlegen ohne Christoph. Lösung: Nautobot/GC, kein Caddy-API-Sidecar (kein zweites SSoT).
  • Erster Anwendungsfall: terpene.teilzeitladen.xyz → http://10.40.20.167:8890 (Mac-mini-Backend, dort PostgREST + sync + web via lokalem Caddy :8890). Vor TP34: manueller Quick-Win-Caddy-Block.
  • Abh.: TP22 (GC-Pipeline) ✅ vorhanden · TP23 (DNS-Policy + Caddy-Voraussetzungen) ✅ Stufe 1. Hängt an nichts, eigene Implementierung im Runbook nach dem Quick-Win.
  • Status: 🔜 Stub; Konzept + Architektur-Skizze vollständig in Handoff-Datei. Implementierung in eigenem Runbook nach terpene-Caddy-Quick-Win.

TP35 — MCP-Schichtung global/projekt/user + 3-Schicht-CLAUDE.md + Announcement-Mechanismus + Backup-Guard ▸ ⏳ ANGELEGT 2026-08-28, Schicht 1 (FQDN→IP) + Schicht 3 (globale Regeln) + Schicht 6 (Skill) ✅

  • Auslöser: Bestandsaufnahme Session 2026-08-28 (Handoff docs/handoff-20260828-pre-opencode-migration.md). Erkenntnis: MCP-Configs sind heute über ~/.config/opencode/opencode.jsonc UND ~/.claude.json verstreut, ohne klares Schema.
  • Befund (geprüft): 11/12 Backups im Verzeichnis ~/dev/.pre-opencode-migration-backups/20260828-150335/ gzip-valid; uwe-job-bewerbung.tar.gz korrupt (Source unverändert da). Repos und globale Configs md5-/HEAD-identisch zum Vor-Session-Stand. Synology-Drive-Sync aktiv (Anwender, manuell).
  • Scope:
    1. MCP-Schichtung (global/projekt/user) — wo lebt welche MCP-Definition, wie wird sie aufgelöst, wie bleibt sie synchron, wenn ein Projekt physisch auf einen anderen Host umzieht. ✅ 2026-08-29 (Teilschritt): gitea-mcp + n8n-mcp in der globalen opencode.jsonc von CF-FQDN auf LAN-IP migriert (s. Ist-Stand-Block). cloudflare-MCP bleibt FQDN (User-Direktive).
    2. Credentials-Schichtung — alle Secrets in ~/.secrets/exports.zsh, MCP-Configs ausschließlich {env:...}. Schließt TP30 Posten #1 ab.
    3. 3-Schicht-CLAUDE.md — CLAUDE.md (öffentlich) / CLAUDE.local.md (gitignored) / ~/.claude/CLAUDE.md (global). ✅ 2026-08-29 (globale Schicht): ~/.config/opencode/AGENTS.md als SSoT angelegt (Secrets + VLAN-Schema + 5 Lessons + SoC + Übergabe-Mechanik + Mid-Action + Commit-Hinweis + Backup-Guard); ~/.claude/CLAUDE.md hält denselben Block als Spiegel. Offen: die pro-Projekt-Schicht (CLAUDE.md/CLAUDE.local.md + Handover-Posts) — eigene Sessions.
    4. Announcement-Mechanismus (Hybrid) — zentraler Ort (voraussichtlich bf-infra/docs/announcements.md) + Skill /news oder /announce zum Einlesen beim Session-Start. Initial: Breaking News über die MCP-Schichtung selbst.
    5. Backup-Guard — ~/dev/.bin/backup-guard.sh, Pre-Commit-Hook pro Projekt, „3 Tage aktiv"-Definition: 5 Commits oder 1000 LOC diff.
    6. /projektstatus-Skill global — als Erstes, vor den anderen Schichten. Alle Projekte verwenden dasselbe Skill. ✅ 2026-08-28: /projektstatus-Skill nach ~/.config/opencode/skills/projektstatus/SKILL.md kopiert (cp, nicht ln -s) — opencode scannt diesen Default-Pfad, kein skills.paths-Eintrag in der JSONC nötig. Lehre im CLAUDE.md-Lessons-Block.
  • Abgrenzung: TP35 ist Konzept + Schichtung, nicht die Umsetzung. Pro Projekt eigene Session, eigener Handover-Post, eigene Mini-Anpassung.
  • Verzahnt mit: TP30 (Credentials), TP34 (Repository-Pattern), TP22 (Nautobot-GC als Vorbild für SSoT + Compliance), TP11 (Monitoring).
  • Erste Session für Scope-Ausarbeitung: Schichten 1–5 + Skill-Setup; Handover-Posts pro Projekt (Schicht 3 + 4 ergeben die Posts).
  • Referenzen: Handoff 2026-08-28 (docs/handoff-20260828-pre-opencode-migration.md) · Backups ~/dev/.pre-opencode-migration-backups/20260828-150335/ · MCP-Inventur bf-infra/mcp/INVENTORY.md · Lessons bf-infra/CLAUDE.md Abschnitt „Lessons for Sessions" · docs/handoff-20260828-mcp-bestand-und-performance.md (MCP-Bestand-Korrektur + Mac-mini-Restriktion).

TP36 — SSoT via Nautobot für teilprojekte.md-Live-Daten + Datei-Aufteilung ▸ 🔜 ANGELEGT 2026-08-28, Scope-Ausarbeitung eigene Session

  • Auslöser: Skill-Run 2026-08-28 (docs/handoff-20260828-session-a-projektstatus.md) lieferte „Nächster Schritt (2026-08-26): TP31 Schritt 5" als Output, obwohl seit dem 26. drei Folge-Sessions parallel offen waren. Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, sobald sie lang wird.
  • Befund: Drei Stellen pro Edit berücksichtigen müssen — Quick-Index (Tabelle), TP-Abschnitt, Ist-Stand-Block. Keine schlägt die andere. Wer eine ändert, muss die anderen mitlesen. Die Datei hat aktuell 1940 Zeilen, der Ist-Stand-Block steht bei Z. 28, das Archiv-Cover bei Z. 1771. Strukturelles Risiko, kein Einzelfall.
  • Scope (Skizze, eigene Session):
    1. Nautobot Custom Fields für Live-Daten — RouterOS-Version, Compliance-Zahl (104/104, 26 Rules × 4 Geräte), DHCP-Server-Anzahl, MGMT-IPs, DNS-Upstream-Modus. Script nautobot/gc-snapshot.py rendert docs/teilprojekte.md (oder einen schmaleren Ersatz) als read-only Snapshot.
    2. Datei-Aufteilung — docs/teilprojekte-index.md (1 Seite: nur aktive TPs, Titel + Status + Datum), docs/teilprojekte-aktuell.md (Live-Snapshot aus Nautobot), docs/teilprojekte-archiv.md (bleibt Volltext). /projektstatus-Skill liest Index + Archiv, nicht die Snapshot-Datei.
    3. Archiv-Anzeige im Quick-Index — die heute 34 Namen stehen heute in einer Zeile am Ende. Sichtbarkeit = „Geschichte zählt". Quick-Index-Block bekommt eine zweite Tabelle mit „abgeschlossen seit …" und Link aufs Archiv.
    4. Migrationspfad — kompatibel mit bestehendem Workflow (/projektstatus-Skill konsumieren, niemand außer mir editiert Live-Markdown). Testphase = beide Dateien parallel führen.
  • Abgrenzung: TP36 ist Querschnitt, nicht Kern-BF. Eigene Session, eigener Handoff. Nicht heute.
  • Verzahnt mit: TP22 (Nautobot-GC als Vorbild), TP35 (geht Hand in Hand — beide sind „SSoT + Konsolidierung"), bf-infra/CLAUDE.md-Lessons.
  • Referenzen: Lessons-Block-Eintrag „Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität" · Handoff Session A 2026-08-28.

TP38 — Out-of-Band-Management: ein untagged-VLAN99-Port je Switch ▸ 🔜 ANGELEGT 2026-08-31

(Nummer 2026-08-31 von 37 auf 38 korrigiert und von Christoph bestätigt: TP37 war zeitgleich in einem zweiten Branch für „Nautobot als Netzwerkdoku" vergeben worden.)

Auslöser (User 2026-08-31, aus eigener Anschauung): „ich durfte beim Neustart vom core gerade erleben, dass ich nicht mehr auf den branch zum verbinden kam. weil das eben alles über den core läuft."

Der Befund, live nachgemessen (2026-08-31)

Das Management des Standorts hat einen einzigen Weg, und der führt über den Core:

crs326-core crs326-branch
Mgmt-IP 10.40.99.1 (VLAN99-SVI) 10.40.99.2 (vlan99 auf der Bridge)
VLAN99 auf der Bridge SVI + Trunks tagged=ether1,ether24,bridge, untagged=""
Weg dorthin direkt nur über ether1 → core ether8

⇒ crs326-branch ist ausschließlich durch den Core hindurch erreichbar. Rebootet der Core, ist der Branch für die Dauer weg — nicht weil er ausfällt, sondern weil der einzige Pfad zu ihm fehlt. Dasselbe gilt für jeden Fehler, der den Core-Trunk oder dessen VLAN99-Konfiguration betrifft. Wer den Core zerkonfiguriert, verliert den Branch gleich mit — und damit die Möglichkeit, es vom Branch aus zu reparieren.

🔴 Der zweite Befund ist der unangenehmere: crs326-branch hat KEINE input-Firewallregeln (Kette leer, live gelesen). Sein gesamter Schutz besteht heute darin, dass VLAN99 nur über den Core erreichbar ist und dessen forward-Kette seit TP5 deny-by-default fährt. Die Erreichbarkeitsbeschränkung IST die Zugangskontrolle. Das ist für die Lösung unten kein Detail, sondern die Hauptauflage.

Zielbild

Je Switch ein Port, untagged in VLAN99 — ein Laptop direkt daran erreicht das Gerät ohne jeden Umweg über den Core, ohne Trunk, ohne funktionierendes Routing.

Laptop ──(statische IP 10.40.99.x/24)── ether23 (untagged VLAN99) ── crs326-branch  10.40.99.2
                                                                     ^ funktioniert auch, wenn der Core steht

Kandidat ether23 auf beiden Geräten — auf core und branch heute running=false, also frei, und symmetrisch (ein Port, eine Nummer, an beiden Standorten dieselbe). (Core hat reichlich freie Ports; auf dem Branch sind ether23 und die Reihe darunter frei, ether24 ist ap2.)

Umsetzung (Skizze — eigene Session)

  1. Port aus dem Trunk-Verhalten nehmen und in VLAN99 untaggen: bridge port mit pvid=99 + frame-types=admit-only-untagged-and-priority-tagged, dazu ether23 als untagged-Member in die bridge vlan-Zeile für VLAN99.
  2. Kein DHCP dort — der VLAN99-DHCP läuft auf dem Core, und genau der soll ja nicht Voraussetzung sein. ⇒ statische IP am Laptop (10.40.99.50/24), im Runbook mitschreiben.
  3. Beschriftung. Ein OOB-Port, den niemand erkennt, wird im Zweifel für „ist ja frei" gehalten.
  4. Abnahme, die den Zweck wirklich prüft: Laptop an ether23 des Branch, Core-Trunk ether1 gezogen (oder Core im Reboot) → 10.40.99.2 muss antworten. Alles andere misst nur den Normalfall.

🔴 Auflage, ohne die das TP nicht gebaut werden darf

Ein untagged-MGMT-Port ist ein physischer Generalschlüssel: wer ihn steckt, steht ungefiltert im Management-VLAN eines Geräts, das (Branch) keinerlei input-Filter hat. Deshalb gehört eine der folgenden Absicherungen dazu, und die Entscheidung darüber in die Umsetzungs-Session:

  • Port disabled=yes im Normalbetrieb, im Bedarfsfall am anderen Gerät freigeschaltet — schützt, kostet aber genau dann Zugriff, wenn das andere Gerät weg ist (halbierter Nutzen, ehrlich benennen);
  • oder input-Regeln auf beiden Switches nachziehen (der Branch braucht sie ohnehin — das ist die eigentliche Lücke, die dieses TP nebenbei aufgedeckt hat);
  • oder rein physisch: abschließbarer Schrank, Port beschriftet und dokumentiert.

⚖️ Meine Einschätzung: die input-Regeln sind der richtige Weg, weil sie eine Lücke schließen, die unabhängig von diesem TP besteht. Der OOB-Port macht sie nur sichtbar.

Abgrenzung

  • Nicht Teil von TP16.3 — dort wird nur festgestellt, dass dreimal Geräte rebooten, deren Mgmt am Core hängt. TP37 entschärft das für künftige Sessions, ist aber keine Voraussetzung dafür.
  • Nicht VLAN-Schema-Änderung (TP32 bleibt unberührt), nicht Konsolen-/Serial-Zugang (das wäre echtes OOB im engeren Sinn; hier geht es um ein zweites Netz-Bein).
  • Abh.: TP5 (Firewall-Systematik, liefert das Muster für die input-Regeln), TP15 (Portbelegung beider CRS326), TP22 (die Umsetzung läuft über Golden Config, ip-firewall-filter und bridge-Sektionen stehen unter Drift-Kontrolle).

TP40 — Home-Assistant-Recorder-Retention: die 41-GB-states-Tabelle beschneiden ▸ 🔜 ANGELEGT 2026-09-01 (User-Okay erteilt)

Auslöser: LXC 302 (MariaDB) lief am 2026-09-01 auf 100 % voll. Sofortmassnahme war +30G (TP39 Punkt 3), aber 59 der 65 GB sind die homeassistant-Datenbank, und die wächst weiter — +30G verschiebt das Problem um ~4 Wochen, es löst es nicht.

Befund (live gemessen)

Tabelle Größe
homeassistant.states 41 GB
homeassistant.state_attributes 8,6 GB
homeassistant.statistics_short_term 8,1 GB
homeassistant.statistics / events je ~0,7 GB

Der HA-Recorder schreibt unbegrenzt — es ist keine purge_keep_days-Grenze wirksam, sonst wäre states nicht auf 41 GB gewachsen. Das ist ein Konfigurations-, kein Plattenproblem.

Scope

  1. HA-recorder-Retention setzen (configuration.yaml bzw. .storage): purge_keep_days auf einen bewussten Wert (Vorschlag 30) — Christoph entscheidet die Zahl, das ist eine Aufbewahrungs-Policy, keine rein technische Größe.
  2. Rauschende Entities ausschließen (recorder: exclude) — die üblichen Verdächtigen sind hochfrequente Sensoren (Leistung, Netz-Durchsatz, sensor.time/date). Vorher per SELECT COUNT(*) … GROUP BY entity_id die Top-Schreiber ermitteln, nicht raten.
  3. recorder.purge mit repack: true — erst nach 1+2, sonst wächst es sofort nach. repack gibt den Plattenplatz auf InnoDB-Ebene real frei (ohne repack bleibt die .ibd-Datei gross). 🔴 states hängt per FK an state_attributes und events — purge räumt beide mit; die Reihenfolge macht HA selbst, nicht von Hand einzelne Tabellen truncaten.
  4. Danach die TP39-Vergrösserung bewerten: schrumpft states deutlich, waren die +30G Vorhalt und keine Dauerlösung — die Platte lässt sich zwar nicht online verkleinern, aber der Druck ist weg.

Abgrenzung

  • Nicht TP39: das ist LXC-/DB-Hygiene (Resolver, networking, skip-name-resolve). Der Resize tauchte dort nur auf; die Ursache (HA-Retention) ist HA-seitig und darum ein eigenes TP.
  • 🔴 Kein eigenmächtiger Datenlöschvorgang — purge wirft echte Historie weg; die Aufbewahrungsdauer ist eine Nutzerentscheidung. Diese Session hat nur vergrössert und gemessen.

TP41 — CF-Access-Reparatur + Zero-Trust-Konsolidierung ▸ ⏸️ ZURÜCKGESTELLT 2026-09-01 (User-Entscheid) — Auth-Kern durch; offen nur Gelis WARP-Client (nicht aus der Ferne machbar), User gibt ggf. später Info

Auslöser: CF-Access zwang Christoph und Geli täglich zum OTP-Login (soll: 30 Tage); GitHub/Google „funktionieren nicht". Root-Causes: Team-Domain-Rename bf4→emsenhuber (Provider-Callback veraltet), App-Session 24h, diverse Ordnungs-/Sicherheitslecks. Vollständiger Verlauf + offene Schritte: docs/handoff-20260901-tp41.md. Branch tp41-cf-access-repair.

✅ Live erledigt: Session 720h (Wildcard/vw/Warp-Login) · login_method-Loch zu (reusable Policy bf-users = nur email:christoph+email:geli) · 8e44f5d1 von 4 Apps auf nur Terpenfinder entkoppelt · Access-Gruppen zt-humans/zt-home/zt-automation, Policies zeigen darauf · vw voll Bypass (Lebensader offen) + vw/admin per WAF-Regel öffentlich geblockt (CF-Edge + Mobilfunk verifiziert) · API-Token auf das vollständige …4148 konsolidiert (User hat Service-Tokens- + Zero-Trust- Write ergänzt), altes …2174 revoken.

✅ T4 DURCH (2026-09-01, Variante B): ein Access-Service-Token zt-svc (65f1821a, rotiert) über Gruppe zt-automation; Terpene-Worker per wrangler secret put TERPENE_SVC_CLIENT_ID auf zt-svc umgestellt (Worker-Pin index.js:44); terpene-verify+-v2 gelöscht; test_whoami grün. Token-Altlast geklärt: b00722 = LIVE Caddy-DNS-Token (LXC130 caddy.env, SSoT), d47435 = stale Kopie (kein Consumer) → User revoket d47435; credentials.env-Kopie auf b00722 realignt.

✅ T3 APPLIED (2026-09-01): Default-Profil (0197d1b3; das custom „Cloudflare Mesh" matcht nur den warp_connector) fallback_domains += emsenhuber.net → [10.40.20.36, 10.40.20.37]; Tunnel-Route 10.40.0.0/16 (d1da7c8a) da, kein 10/8-Exclude → routbar. ⏳ Client-Verify: WARP neu verbinden. ✅ T2 DURCH (2026-09-01): WAF-Regel f26744eb gelöscht; Caddy vw-Block respond @cf_admin 404 bei Header CF-Connecting-IP (LXC130). Verify: Edge /admin=404, lokal/WARP /admin=200. ✅

✅ T5 DURCH (2026-09-01, User-bestätigt): GitHub-Login läuft; Google-Login läuft nach dem Consent-Screen-Fix (§A des Follow-up-Handoffs: User Type Internal→External/veröffentlicht). Callback für beide https://emsenhuber.cloudflareaccess.com/cdn-cgi/access/callback. ❌ Facebook/Meta für TP41 abgesagt (2026-09-01, User-Entscheid): optional, kein Bedarf — Geli ist über OTP angelikahy@outlook.de + Google pillepalle670@gmail.com voll gedeckt. Keine Facebook-App, kein CF-Facebook-IdP angelegt → nichts aufzuräumen. Instagram hat ohnehin keinen CF-IdP. ✅ App-Launcher (D1): je eine Bookmark-Kachel vw/hass/terpenfinder, Wildcard-Kachel raus, Launcher gestylt (Follow-up-Handoff §D, Policies unverändert).

✅ Geli in WARP — CF-seitig nichts zu tun (2026-09-01 live geprüft): Device-Enrollment läuft über die App „Warp Login App" (6c32fe23…, type warp) → Policy bf-users → Gruppe zt-humans, die Gelis beide Mails schon enthält → sie darf bereits enrollen (deshalb war „Device enrollment permissions" nicht als fehlendes Setting zu finden). Terpenfinder-geli-Policy lässt sie per E-Mail durch. ⇒ verbleibt nur ein Handgriff auf Gelis Gerät (WARP installieren, Team emsenhuber, Login OTP angelikahy@outlook.de / Google pillepalle670@gmail.com) — nicht aus der Ferne machbar. 🔑 Permission-Frage beantwortet: CF-seitig keine nötig. Für generelle WARP/Enrollment/User-Verwaltung per Account-Token: Access: Apps and Policies → Edit + Access: Organizations, IdPs, and Groups → Edit (User erteilt 2026-09-01). Seat-Verwaltung (/access/seats) bleibt per MCP gesperrt — 10405 Method not allowed for this authentication scheme (Endpoint will User-Token, nicht Account-Token; kein Scope-Fix, für Enroll irrelevant).

🔜 Offen — nur noch außerhalb CF: Gelis WARP-Client auf ihrem Gerät (s.o.) · T6 optional Rate-Limit · 🔴 TERPENE_ANDROID_KEYSTORE_PW-Rotation erwägen (TP30 Posten #7, Transkript-Leak). d47435-Revoke ✅ · T3-WARP-Client-Verify ✅ (User, 2026-09-01).

🔻 Optionaler Security-Cleanup (eigener Punkt, nicht in dieser Runde): die geli-Terpenfinder-Policy hat OR-Semantik und enthält den breiten CGNAT-Bereich 94.154.138.0/24 als IP-Include → jeder aus dieser geteilten Range käme ohne Login rein. Über WARP ist der Egress ohnehin Cloudflare (nicht diese IP) → der IP-Include wäre dann überflüssig und entfernbar.

Abgrenzung: hass/gitea bleiben (diese Runde) öffentlich; Terpenfinder-Human-Policies nicht angefasst; Gitea-Least-Privilege = eigener künftiger TP (jedes Projekt repo-scoped Login, Admin nur Claude/Christoph).

  • Bezug TP11 (Monitoring): eine Plattenfüllstand-Warnung hätte das Wochen früher gezeigt.

TP42 — Namens-Vereinheitlichung bf.lan → emsenhuber.net ▸ 🔜 ANGELEGT 2026-09-01 (User-Okay erteilt)

Auslöser (2026-09-01, TP41-Randbefund): live gemessen liefern die realen Client-Resolver 10.40.20.36/.37 *.emsenhuber.net → 10.40.20.40 (Caddy) — korrekt. Daneben existiert aber ein zweiter, gedrifteter interner Namensraum: der Core trägt in /ip dns static 11 Einträge auf *.bf.lan, der Caddyfile doppelt jeden internen Namen als *.bf.emsenhuber.net, und die alten AdGuard .16/.17 antworten noch mit nginx*.bf.emsenhuber.net bzw. totem VLAN1 10.40.41.123. Christoph: „aus emsenhuber.net wurde schon wieder bf.lan — das müssen wir ein für alle mal aus allen Geräten rausprügeln und in Nautobot festhalten."

Ziel: genau ein interner Namensraum emsenhuber.net (Split-Horizon .36/.37 → .40). .bf.lan und .bf.emsenhuber.net werden entfernt; Sollzustand in Nautobot dokumentiert.

Scope (Reihenfolge = eigene Session):

  1. Bestandsaufnahme aller Fundstellen: Core /ip dns static (*.bf.lan), DHCP-Option domain/Search-Domain je Netz, Caddyfile-Aliase (*.bf.emsenhuber.net), AdGuard-Rewrites .36/.37, stale .16/.17, Geräte-identity.
  2. Entscheidung Zielschema (nur emsenhuber.net, ggf. bf.emsenhuber.net als einzige interne Zone — Christoph-Entscheid) und Migrationsreihenfolge ohne DNS-Loch.
  3. Umsetzen: Core-DNS + DHCP + Caddyfile angleichen; stale .16/.17 klären/abbauen.
  4. Nautobot: interne Namen/Records als Sollzustand erfassen (Bezug TP37 Datenmodell).

Abgrenzung: kein CF-Public-DNS-Umbau (Zone emsenhuber.net öffentlich bleibt); kein Resolver-Swap (.36/.37 bleiben); .1-als-DNS-Reste auf Clients sind separat/tolerierbar (User: „bis jetzt kein Problem"). Abh.: TP37 (Nautobot-Datenmodell) für Punkt 4; sonst nichts Offenes.

Status: 🔜 angelegt 2026-09-01, Ausarbeitung eigene Session. Handoff: docs/handoff-20260901-tp42-naming.md.

Auslöser (2026-09-01, TP41-Follow-up): Geli soll 420.teilzeitladen.xyz am dw-Standort erreichen. Split-Horizon am dw scheitert an der FritzBox (10.95.10.1): kein Per-Host-A-Override im Standard (nur DNS-Rebind-Ausnahmen ohne IP). Christoph: „keine Chance im FritzBox-Standard? … lieber ein TP als uns verfranzen. Geli kommt rein, das zählt."

Ziel: dem dw-Standort einen eigenen Resolver/DNS-Uplink geben, der site-spezifische Overrides hält (erster Fall 420.teilzeitladen.xyz → BF-Caddy über WG), als DNS der FritzBox/Clients gesetzt.

Kernfrage (explizit): Verhalten bei WG-Disconnect. Ein Override auf eine BF-interne IP (10.40.20.40) fällt aus, wenn der Tunnel unten ist. Für einen Login-gegateten Dienst gibt es kein „kein Login und funktioniert bei WG-down" — Optionen: (a) Override nur bei WG-up (Failover-Resolver), (b) öffentlicher Pfad (CF, mit Login) als Fallback. Zu entwerfen.

Sofortlösung (nicht Teil von TP43): Geli per WARP (Session statt täglichem Login, CGNAT egal, degradiert sauber: WG-up = nahtlos, WG-down = öffentliche URL + Login).

BF-seitige Prereqs: Caddy-vhost 420.teilzeitladen.xyz → macmini-Worker (10.40.20.167:8890) mit Cert (DNS-01, …4148 hat teilzeitladen.xyz-DNS) · WG-Route dw→10.40.20.40 · Split-Horizon für 420.teilzeitladen.xyz (heute liefern auch .36/.37 public). Vorher verifizieren: FritzOS-Version + Dauerläufer am dw. Cross-Projekt (teilzeitladen.xyz). Handoff: docs/handoff-20260901-tp41-followup.md.

Status: 🔜 angelegt 2026-09-01, eigene Session.

TP37 — Nautobot als durchgehende Netzwerkdoku: Hosts/Container/Dienste, erster Anwendungsfall CF-Connectoren ▸ 🔜 ANGELEGT 2026-08-30 (User-Okay erteilt)

Christophs Formulierung (2026-08-30): „können wir das setup mit den cf-connectoren dann auch in nautobot abbilden. quasi eine durchgehende netzwerkdoku stück für stück. muss eh fast sein, da du auf den mikrotiks ja nur via gc/nautobot deployst."

  • Auslöser: der TP16.3-Schnitt am 2026-08-30. Um ihn zu planen, mussten sechs cloudflared-Container über drei Hosts hinweg per SSH und Portainer-API zusammengesucht werden (LXC400 .126, macmini .167, NAS .10), und die Zuordnung Tunnel→Connector→Host existierte nirgends als Datensatz — nur verstreut in Prosa. Dieselbe Suche fällt bei jedem weiteren Eingriff erneut an.
  • 🔴 Befund (live gegen die API gelesen 2026-08-30) — das Fundament fehlt, nicht nur die Connector-Zeilen:
    Devices: 8   (ap1, ap2, crs326-core, crs326-branch + cudy1/cudy2/sw1/sw2 decommissioned)
    Apps: golden-config          Platform: 1 (RouterOS)
    ipam.Service: 0 · CustomField: 0 · Relationship: 0 · VirtualMachine: 0 · Tag: 0
    
    Nautobot kennt heute ausschließlich die vier MikroTik. Kein Host, kein Container, kein Dienst ist erfasst — weder Caddy (caddy-bf1), noch AdGuard (agh-bf1/bf2), noch pve/NAS/macmini, noch Nautobot selbst. ⇒ TP37 ist zuerst eine Datenmodell-Frage, erst danach eine Connector-Frage.
  • Scope (Vorschlag, Reihenfolge ist der Punkt):
    1. Substrat abbilden — pve/NAS/macmini als Hosts, die LXCs und Docker-Container als virtualization.VirtualMachine (Cluster je Host). Damit existiert erstmals ein Ort, an dem „wo läuft was" steht.
    2. Dienste abbilden — ipam.Service je exponiertem Port (Caddy :80/:443, AdGuard :53, Nautobot :8080, n8n :5678, …). Das ist Nautobot-Kernmodell, keine eigene App nötig.
    3. CF-Connectoren als erster Anwendungsfall — Tunnel (bf-public, historisch bf-mm/bf-nas/bf-pve) als Objekt, Connector als Relation Tunnel↔Host, Ingress-Regel als Hostname→Backend-Paar. Modellwahl offen: Custom Fields + Relationships auf Bestandsmodellen oder eine kleine eigene App. 🔴 Erst Punkt 1–2 entscheiden, dann diese Frage — umgekehrt entsteht ein Modell für einen Sonderfall.
    4. Drift-Check — Skript liest CF-API (cfd_tunnel + configurations) und vergleicht gegen Nautobot, analog nautobot/gc-configplan.py --refresh-compliance. Read-only zuerst; ob Nautobot je nach Cloudflare schreibt, ist eine spätere, eigene Entscheidung.
  • Abgrenzung (wichtig, sonst überlappt es mit zwei offenen TPs):
    • TP34 (Caddyfile via GC) rendert Config aus Daten. TP37 liefert die Daten, auf denen TP34 aufsetzt — Punkt 1+2 sind TP34s fehlende Voraussetzung. TP37 vor TP34.
    • TP36 (SSoT für teilprojekte.md) rendert Doku aus Daten und braucht dieselben Custom Fields. Läuft parallel, gleiches Fundament.
    • Nicht in TP37: Golden Config für Nicht-Netzgeräte (Caddy/AdGuard deployen) — das ist TP34. Nicht in TP37: Cloudflare-Writes aus Nautobot heraus.
  • Abh.: TP21/TP22 ✅ (Nautobot + GC-Pipeline stehen) · TP32 ✅ (Nummern-Systematik liefert die Klassifikation für Punkt 1) · TP16.3 (liefert den Anwendungsfall und den Beleg, warum es fehlt). Hängt an nichts Offenem.
  • ⚠️ Ehrliche Einschränkung: Nautobot ist bis heute bewusst schlank (8 Devices, 0 Custom Fields) — die Pipeline ist stabil und 104/104 compliant. TP37 vergrößert die Angriffsfläche dieser Pipeline. Deshalb Punkt 1–2 als eigener, abnehmbarer Schritt mit Compliance-Gegenprobe danach, nicht als Nebeneffekt von Punkt 3.
  • Status: 🔜 angelegt 2026-08-30, Scope-Ausarbeitung in eigener Session. Kein Runbook.

Abgeschlossene Teilprojekte ▸ teilprojekte-archiv.md

34 abgeschlossene TPs liegen dort im Volltext, unverändert und mit eigenem Inhaltsverzeichnis. 📊 Für einen Statusbaum reicht diese Datei: die Tabelle unten nennt jedes archivierte TP, und die Zeile „✅ Durch (Fundament)" weiter unten führt sie noch einmal mit Abschlussdatum. Das Archiv muss dafür nicht geöffnet werden — es trägt die Begründungen, nicht den Status.

Kurzorientierung, wo was zu finden ist:

Thema TPs im Archiv
Fundament / Fabric TP1 ALLNET-Core (Gerät ausgebaut) · TP2/TP2.1 Omada (ausgebaut) · TP15 HW-Refresh CRS326 · TP21.1 Core-Cutover-Recovery · TP4 Router-Cutover (Rolle seit 08-08 auf dem Core)
WLAN TP3 (historisch) · TP3.1 · TP3.2 · TP3.3 (in TP25 aufgegangen) · TP28 CAPsMAN auf dem Core · TP25-Langfassung
Firewall / QoS TP5 deny-by-default · TP26 ip-firewall-filter unter Drift-Kontrolle · TP6 CAKE (geschlossen statt gebaut) · TP6.1 Upload-Priorisierung
Discovery / Media TP7 mDNS · TP7.1 SSDP-Relay · TP7.2 HA↔Sonos
Hosts / Migration TP14 (+.1/.2/.2a/.3/.5) VLAN20-Cutover · TP17 Shelly→VLAN30 · TP19 VLAN1-Retirement
Control-Plane TP20 Platform-Postgres · TP21 Nautobot GC · TP22 Adoption + Compliance-Push · TP27 RouterBOOT 7.24 · TP-R Capabilities-Sweep
Zugang TP10 Multi-Site-VPN (offen nur der DDNS-Updater, s. Follow-ups) · TP16.1 natives CF-Mesh
Verworfen TP9 Gateway-HA (User-Entscheid 2026-08-08)

Reihenfolge / Abhängigkeiten

🎯 Kritischer Pfad (Stand 2026-08-23) — LEER

TP5 E1 ✅ ─► E2 ✅ ─► E2-Nachtrag ✅ ─► E3 Drop ✅ ─► E4 input+MGMT ✅ ─► E5 MAIN→IOT ✅ ─► TP6 ✅ GESCHLOSSEN

                        🎯 DER KRITISCHE PFAD IST LEER (2026-08-23).
              Das Projekt ist in seinem Kern fertig: deny-by-default steht, Golden Config
              steht, WLAN steht, VLAN1 ist weg, und TP6 ist beantwortet statt offengelassen.
              Was jetzt kommt, entscheidet Christoph — nichts blockiert mehr etwas.

➡️ Update 2026-08-22 (Sitzungsende): TP25 hat den Pfad seit dem 08-20 blockiert und ist im Kern erledigt — die Terrasse ist repariert, der Dead Spot weg, Fund E abgestuft. Damit ist TP6 wieder der einzige Punkt auf dem kritischen Pfad, und seine Prämisse ist inzwischen gemessen (tp6-praemisse-messrunbook.md §5): formal Fall B (+28…47 ms in der Senderichtung), aber durch sechs Lastphasen mit laufendem Videocall kein einziger Problem-Klick. Die nächste Sitzung entscheidet daher bauen oder begründet schließen — sie misst nicht neu. Übergabe mit den Sitzungszuschnitten: handoff-20260822-naechste-schritte.md.

🟢 Update 2026-08-23 (Endstand): TP6 ist beantwortet — und der kritische Pfad damit leer. Gebaut wurde, wie entschieden. Der erste Anlauf (/queue tree parent=vlan100) war wirkungslos, aber nicht aus dem zuerst notierten Grund: nicht der Switch-Chip, sondern die MikroTik-dokumentierte Regel, dass Interface-HTB bei hw-offloadetem out-interface nicht greift. Ueber parent=global + Mangle-Mark funktioniert CAKE einwandfrei (zaehlt 6 MB, drosselt 9,3 → 7,78 Mbit). Und genau deshalb ist das Ergebnis belastbar: die Latenz unter Last bessert sich trotzdem nicht — bei keiner Bandbreite, auch nicht bei 52 % der Linerate (42,4 / 43,3 / 44,0 / 44,6 ms). Der Puffer sitzt im DOCSIS-Uplink, nicht im Router. ⇒ TP6 schliessen: nicht mangels Nutzen, sondern weil das Werkzeug nicht an die Ursache reicht. Details im TP6-Abschnitt oben, Merkregeln in ref_queue_htb_platzierung_docsis.

🔴 Korrektur wenige Stunden spaeter (2026-08-23, TP6.1): die DOCSIS-Erklaerung oben ist FALSCH. Der Puffer sitzt nicht unabaenderlich im Uplink — er loest sich auf, sobald der Verkehr in getrennte Klassen faellt: NAS-Bulk priority=8 gegen interaktiv priority=1 bringt dieselbe Strecke unter derselben Saettigung von 40,8 auf 11,0 ms (−73 %). Der Fehler steckte in der Messmethode — Last und Messung liefen vom selben Host, ein Shaper kann eine Queue nicht priorisieren, in der nur eine Klasse liegt. ⇒ TP6 bleibt geschlossen (CAKE war das falsche Werkzeug), aber nicht aus dem dort notierten Grund. Der Rest steht in TP6.1 (Archiv) und in ref_queue_htb_platzierung_docsis.

📜 Die folgende Fassung stammt vom 2026-08-18 und ist erledigt — die Praemisse ist am 08-20/21 gemessen, TP6 am 08-23 geschlossen. Bewusst erhalten, weil sie zeigt, warum nicht gebaut, sondern zuerst gemessen wurde:

TP5 ist am 2026-08-18 abgeschlossen. Damit steht rechnerisch nur noch TP6 auf dem kritischen Pfad — 🟠 aber dessen Prämisse ist offen: eine Messung von 2026-06-16 (docs/call-quality-diagnosis.md) bescheinigt dem Netz bereits A/A+ und hält CAKE für „marginal". Der nächste Schritt ist deshalb ein Bufferbloat-Test gegen den heutigen Edge, nicht der Bau einer Queue. Fällt er gut aus, ist der kritische Pfad leer und das Projekt in seinem Kern fertig — dann entscheidet Christoph, was das neue Ziel ist. Alles andere läuft nebenher und blockiert nichts.

  1. ✅ TP5 Etappe 1–4 — komplett durch (2026-08-16 bis 2026-08-18). Details im TP5-Abschnitt oben. Die Wartezeit-Position, die hier bis 2026-08-17 als eigener Schritt stand, ist eingeloest: statt eines mehrtaegigen Fensters wurde der Nachtrag aus einem dreieinhalbstuendigen Fenster abgeleitet und der Drop mit Auto-Revert plus weiterlaufender Log-Regel abgesichert. Die Vorbedingung D2 (iPhone .30.201) ist gegenstandslos: das Geraet hing dort bewusst zum Anlernen eines IoT-Geraets (User 2026-08-17). Wer im IOT-SSID sitzt, ist im selben VLAN wie das Geraet und beruehrt die forward-Kette gar nicht; fuer Ad-hoc-Zugriffe aus MAIN gibt es jetzt die selbstverfallende Gruppe jit-admin.
  2. ✅ TP6 — QoS/CAKE ▸ GESCHLOSSEN 2026-08-23. Gebaut, gemessen, wieder ausgebaut. CAKE ist auf diesem Geraet baubar (Global-HTB + Mangle) und wirkt technisch — verbessert die Latenz unter Last aber bei keiner Bandbreite, auch nicht bei 52 % der Linerate. Der Puffer sitzt im DOCSIS-Uplink, nicht im Router (⚠️ widerlegt von TP6.1 am selben Tag — s. Korrektur oben). Queue und Template-Block sind entfernt, die ComplianceRule queue bleibt und bewacht den Abschnitt als leer. Damit ist der kritische Pfad leer. 💡 Aus dem Abschluss ist TP6.1 entstanden (Saettigung vermeiden statt Bufferbloat bekaempfen) — Christophs Gedanke, den NAS-Sync zurueckzuhalten. Steht als Vorschlag, nicht auf dem Pfad. ✅ Am selben Tag gebaut und gemessen: 40,8 → 11,0 ms. Das eigentliche Projektziel; keine offenen Blocker mehr. Korrigiert 2026-08-19: kein Blocker, aber eine offene Prämisse — der Satz oben und dieser Punkt widersprachen sich seit dem 18. Richtig ist: der nächste Schritt ist eine Messung, kein Bau. Runbook liegt bereit: docs/tp6-praemisse-messrunbook.md. Fällt sie A/A+ aus, wird TP6 geschlossen statt gebaut — dann ist der kritische Pfad leer und das Projekt in seinem Kern fertig.

Eine harte Reihenfolge-Regel dazwischen: TP5 darf die DNS-Bypass-Sperre erst schließen, wenn TP23s Ausnahme-Oberfläche steht — der bisherige Workaround „Client von Hand auf 1.1.1.1" ist genau dieser Bypass. Alles andere aus TP5 ist davon unabhängig und kann vorher laufen.

⚠️ Und eine zweite, aus TP16.1/TP16.3: der Drop braucht cf-mesh explizit — steht seit Etappe 2 als TP5-E2: mesh->LAN / LAN->mesh drin. Ohne diese zwei Zeilen sperrt Etappe 3 den Fernzugang aus, also genau den Weg, über den man den Fehler zurücknehmen wollte.

Follow-ups nebenher (blockieren nichts)

  • TP25 — WLAN-Stabilisierung (NEU 2026-08-17): Audit durch, 6 Funde (2,4 GHz fährt 40 MHz statt der profilierten 20; ap2 sendet auf 5 GHz mit 9 statt 18 dBm, weil ch149 unter ETSI leistungsbegrenzt ist; 80 MHz kostet Randabdeckung; unzulässiger Channel-Override auf w2-iot; keine access-list; 5-Minuten-Takt bei IoT-Trennungen offen). Umsetzung in 6 Schritten, alles via GC. Übernimmt den offenen Rest von TP3.3.
  • TP16.3 — cloudflared-Reduktion (NEU 2026-08-17): Recherche durch. Der Mesh ist schon der private Datenpfad (im TP5-Log mit 137 Zeilen belegt); Container braucht nur noch der öffentliche Ingress. Vorschlag: nautobot als Probe aus dem CF-Ingress nehmen.
  • DoH-Cloudflare auf crs326-core ✅ erledigt 2026-08-18 (TP23 Stufe 1 = TP21.1 TODO 1).
  • TP6.1 — Saettigung vermeiden ✅ LIVE 2026-08-23: 40,8 → 11,0 ms unter Last (−73 %). NAS-Sync weicht bei Last zurueck, ComplianceRule ip-firewall-mangle angelegt, 104/104.
  • TP24 — DC-USV (seit 2026-08-16 eigener TP, aus TP15 herausgelöst).
  • TP19 — VLAN1-Client-Retirement / cudy-Teardown ✅ geschlossen 2026-08-18 (VLAN1-ARP = 0). Die zwei Reste sind dorthin verschoben, wo sie hingehören: tote CF-Tunnel → TP16, HA-Portainer setup_retry → TP11.
  • TP3.3 — WLAN-Coverage-Kur → in TP25 aufgegangen (2026-08-17). 802.11k/v ist repariert (08-13); Re-Walk + Dead-Spot-Kartierung laufen als Schritt 5 von TP25 — und zwar erst nach den Funkparameter-Fixes, weil vorher gemessene Werte wertlos sind.
  • TP10 — DDNS-Auto-Updater für bf4.emsenhuber.net (auf dem Core möglich, device-mode erlaubt scheduler).
  • 🔑 Alle Passwort-/Token-Rotationen laufen seit 2026-08-24 unter TP30 — Sammel-TP, gebündelt gegen Projektende (User-Entscheid). Dort steht die Rotationsliste (MikroTik admin, IoT-PSK, Shelly-Gerätepasswort, CF-Access-Token), der Radius der fünf Konsumenten, der Etappe-1-Abschluss und die Fallenliste. Etappe 1 ist durch, Etappe 2 wartet bewusst.
  • Doku-Aufräumen (docs/doku-cleanup-session.md) ✅ abgeschlossen 2026-08-23. Punkte A–D (08-16), E (08-18/19) und F — der Split (08-23) sind durch: abgeschlossene TPs liegen jetzt in teilprojekte-archiv.md, TP25 ist von 394 auf ~100 Zeilen kondensiert, diese Datei ist von 2217 auf ~760 Zeilen. Die Regel dahinter bleibt gültig: ⚠️ während einer Doku-Session teilprojekte.md nicht parallel bearbeiten (in der Nacht auf den 18. viermal committet, das kollidiert sonst) — und ein TP wandert erst dann ins Archiv, wenn nichts mehr daran zu tun ist. (Ergänzt 2026-08-30: und zurück, wenn doch wieder etwas zu tun ist — das Archiv ist eine Ablage, kein Endlager.)

Nachgelagert / research-getrieben

TP8 (Identity/802.1X) · TP11 (Monitoring) · TP12 (IPv6) · TP13 (Botschafts-WLANs) · TP16.2 (Härtung bypass-Dienste) · TP18 (Garage-Objektspeicher) · TP20.1 (Vaultwarden → Platform-PG) · TP23 (DNS-Policy, s. Regel oben) · TP30 (Credential-Rotation — nicht research-, sondern termin-gated: gebündelt gegen Projektende, User-Entscheid 2026-08-24). (TP17.1 — Shelly Gen4 → Thread, aktiviert 2026-08-23 — läuft nicht hier, sondern im HA-Projekt ~/dev/ha-xstate; netzseitig ist nichts zu bauen. Es blockt bewusst die PSK-Rotation.) (TP16.1 stand hier bis 2026-08-17 fälschlich als offen — es ist seit demselben Tag ✅ abgeschlossen und steht unten unter „Durch" (Volltext im Archiv). Nachfolger ist TP16.3, das oben unter Follow-ups läuft.)

✅ Durch (Fundament — nicht mehr planen, nur nachschlagen)

📚 Alle folgenden TPs stehen seit 2026-08-23 im Archiv — im Volltext, mit Inhaltsverzeichnis. Diese Zeile bleibt als Landkarte: sie sagt, was fertig ist; das Archiv sagt, wie es dazu kam.

TP1 · TP2 · TP2.1 · TP3 (Rebuild 2026-08-08) · TP3.1 · TP4 (Rolle seit 08-08 auf dem Core) · TP7 · TP7.2 · TP10 (Kern) · TP14 (+14.1/14.2/14.2a/14.3/14.5) · TP15 · TP16 (Kern) · TP17 · TP19.1 · TP20 · TP21 · TP21.1 · TP22 · TP-R · TP3.2 (08-17) · TP5 (08-18) · TP7.1 (08-18) · TP19 (08-18) · TP23 Stufe 1 (08-18) · TP6 (08-23, geschlossen statt gebaut) · TP6.1 (08-23, Upload-Prio live) · TP26 (08-17) · TP27 (08-17/18) · TP28 (08-17, GC-Modell 08-18). Verworfen: TP9 (Gateway-HA, User-Entscheid 2026-08-08). Obsolet: Omada-PVID-Fix (User-Entscheid 2026-07-22 — „nie wieder aufgreifen").

📜 Historische Fassung dieses Abschnitts (bis 2026-08-14 fortgeschrieben, hier bewusst erhalten)

🎯 AKTUELLER KRITISCHER PFAD (Stand 2026-07-16, überschreibt die Reihenfolge unten): TP20 (Platform-Postgres) → TP21 (Nautobot) — Ziel: fertig vor CRS326-Produktivsetzung, spätestens Montag 2026-07-20. TP18 (Garage-S3) und die Komodo-Abhängigkeit wurden am selben Tag als Blocker entfernt/entkoppelt (s. TP20), um diese Deadline zu halten — beide werden später nachgezogen, nicht mehr vorher abgewartet. ➡️ Stand-Update 2026-08-08: TP20/TP21-Plattform steht, Core-Cutover (TP21.1) + WLAN-Rebuild (TP3) + VLAN20-Migration (TP14) sind durch. Neuer Rest-Pfad: TP22 (Device-Adoption + Compliance-Push) → TP5 (Deny-by-default) → TP6 (CAKE, das Ziel). Kleine Follow-ups davor: DoH-Cloudflare auf core, USV (TP24). ➡️ Stand-Update 2026-08-13 (Abschluss): TP22 ist DURCH (alle 5 Schritte; Fleet adoptiert, Deploy-Pipeline scharf, Datenmodell as-built, Secret-Handling entschieden) → kritischer Pfad jetzt: TP5 (Deny-by-default) → TP6 (CAKE). Kleine Follow-ups davor: DoH-Cloudflare auf core, DC-USV (TP24), Doku-Aufräumen (docs/doku-cleanup-session.md, vorbereitet 2026-08-14 — dieses Dokument beschreibt an mehreren Stellen noch die Topologie vor dem Core-Cutover). ➡️ Neu 2026-08-14: TP23 (DNS-Policy & Self-Service) hängt an TP5 — und zwar in beide Richtungen: TP23 braucht TP5s Firewall, TP5 darf aber die DNS-Bypass-Sperre erst schließen, wenn TP23s Ausnahme-Oberfläche steht (der bisherige Workaround „Client auf 1.1.1.1" ist dieser Bypass). Ebenfalls in TP5 gezogen: TP7.1 (SSDP-Reflector für die macOS-Sonos-App). Kabel core ether4→ether2 (Sideboard/TV) entfällt (2026-08-14): gelöst wurde am Port, nicht am Kabel (User-Vorgabe „Patchfeld-Nummerierung wird nicht wegen einer Config geändert") — ether4 steht seit 08-13 auf untagged VLAN40, End-to-End-Beweis am 08-14 erbracht, s. TP21.1 Punkt 4. Historisch: TP22 war das nächste Arbeitspaket — TP17 (Shelly-Migration) ist parallel dazu vollständig abgeschlossen (alle Shellys auf VLAN30, User-bestätigt). Grund für den Nautobot-First-Ansatz: Governance-Entscheid „Config exklusiv über Nautobot" (s. oben) macht Nautobot zur Voraussetzung für praktisch jede weitere Router-/Switch-Änderung. Die Punkte 3-4 unten (Omada-PVID-Fix etc.) sind obsolet (User-Entscheid 2026-07-22 — Omada spielt seit TP15-Korrektur keine Rolle mehr, nie wieder aufgreifen) — ganze Liste braucht mal eine Aufräum-Session, hier nur der aktuelle Fokus vorangestellt. ✅ TP16 (CF-Tunnel Round-Robin/macvlan-Host-Isolation → reziprokes 3-Tunnel-HA) KERN UMGESETZT 2026-07-10 (#1033 strukturell weg). Offen nur Access-Strang (WARP/vw) = user-gated. As-Built docs/cf-tunnel-mapping.md §10.

  1. TP1 (GE8) → TP2 (Omada Controller). ✅ erledigt 2026-06-15.
  2. TP4 (Router-Cutover L3-Infra + WAN-Edge). ✅ erledigt 2026-06-15 (L3) + 2026-07-01 (Edge-Cutover, FB physisch weg).
  3. Jetzt: Omada-PVID-Fix — Omada Access-Profile korrigieren (PVID 10/20, untagNetworkIds bereinigen, sicher P3/P5/P6 zuerst), dann IP-Migration NAS/macmini/pve auf VLAN-Subnetze + DNS-Einträge updaten. IP-Migration NAS/pve = TP14 (NAS ✅ 2026-07-10, pve 🔜 Session B).
  4. Dann: TP5 (Deny-by-default fertigziehen — Sonos-/HA-/mDNS-Holes + WAN-Input-Seal stehen bereits; TP5.1 = DNS-Redirect + Live-Torch für Low-Trust-Geräte/Airfryer hängt dran) → TP6 (QoS/CAKE). TP7 (mDNS) ✅ 2026-07-02 (inkl. HA/vlan20), TP3 (WLAN/CAPsMAN) ✅ 2026-06-18, TP3.1 (ap2-VLAN-Trunk) ✅ 2026-07-01, TP3.2 (P750W/Discovery) ✅ 2026-07-02, Sonos-Migration ✅ 2026-07-02 (alle 10 auf VLAN 40). ⏳ TP3.3 (WLAN-Coverage-Kur) offen — Fortsetzung neue Session (docs/wifi-p750w-coverage-and-map.md).
  5. TP10 (VPN) ✅ Kern durch (2026-06-27) + Edge-Cutover (2026-07-01) → DURCH. Offen nur: DDNS-Auto-Updater.
  6. Nachgelagert/Research-getrieben: TP8 (Access), TP9 (HA), TP11 (Monitoring + CF-Tunnel-Track-B/C), TP12 (IPv6), TP13 (Spaß).
  7. Foundation (2026-07-14): TP15 (MikroTik-Switching / Omada-WLAN, 2× CRS326 bestellt) ∥ TP20 (Platform-Postgres, single-site replication-ready) → TP21 (Nautobot Golden Config) ∥ TP20.1 (Vaultwarden→PG + 2-Site-HA). TP20 hängt an TP18 ① (Garage-Bucket für pgBackRest).