# 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](teilprojekte-archiv.md)** — 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.)* ## Status & Index → Gitea-Board (SSoT seit 2026-09-07, TP36-Cutover) > 🧭 **Ist-Stand, kritischer Pfad und Quick-Index leben ab jetzt als Gitea-Issues**, nicht mehr in > dieser Datei: **[admin/bf-infra · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues)**. > **TP = Issue · Status = Label `status:*`** (todo/wip/paused/idea/concept/done/dropped) · > **Gruppierung = Label `thema:*`**. Filtern über die Label-Chips der Issue-Liste, z.B. > [nur offene](https://gitea.emsenhuber.net/admin/bf-infra/issues?type=issue&state=open). > > **Diese Datei führt nur noch die per-TP-Prosa** — die `### TPnn`-Abschnitte unten; Abgeschlossenes im > **[Archiv](teilprojekte-archiv.md)**. **Live-Gerätedaten** (Compliance-Zahl, RouterOS-Version …) kommen > aus **Nautobot**, nicht von hier. Sync-Werkzeug Datei/Archiv → Gitea (idempotent, Dry-Run default): > `bin/gitea-tp-sync.py`. > > *(TP36-Cutover 2026-09-07: die frühere Quick-Index-Tabelle und der chronologische „Ist-Stand (Kurz)"-Block > sind aus dieser Datei entfernt, damit TP-Branches nicht mehr auf der Steuerungs-Doku kollidieren — Status- > Updates gehen nach Gitea statt in diese Datei. Die alte Ist-Stand-Historie steht in der Git-Historie.)* ## Governance & genehmigte Ausnahmen (stehende Regeln — kein Status) > 🎯 **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 `<>` 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](teilprojekte-archiv.md)** (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). > > 📊 **Index/Statustabelle → [Gitea-Board](https://gitea.emsenhuber.net/admin/bf-infra/issues)** (siehe Kopf dieser Datei). Hier unten folgt nur noch die per-TP-Prosa. ### TP5.1 · Gitea #1 — DNS-Visibility & Flow-Observability für Low-Trust-Geräte (Airfryer-Fall) - **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. - ⚠️ **Nachtrag 2026-10-12 — Gerät ist FUSION/Cloud-only, UNTRUSTED-Move zurückgestellt:** Der Airfryer hat die Philips-FUSION-Migration bereits vollzogen (HA-Config-Entry `philips_homeid`, Titel „Airschreier 5000 Series XXL **[Cloud]**"; NCP `1.1.14` / Host `0.5.6`). Damit ist die **Cloud-Verbindung die einzige Steuerungsroute** — ein Move ins internetlose UNTRUSTED-VLAN würde das Gerät unsteuerbar machen (auch aus HA). Der oben genannte UNTRUSTED-Kandidat ist damit **vom Tisch**, nicht nur verschoben. Der DHCP-Override `dns 1.1.1.1` verliert seine Rechtfertigung (OTA-Problem erledigt) und kann abgebaut werden. Details: `docs/announcements.md`, Eintrag 2026-10-12. **Nebenbei:** die VLAN-Nummer im obigen Text („VLAN50") war falsch — UNTRUSTED ist laut `docs/vlan-plan.md` **VLAN 40**. ### TP8 · Gitea #2 — Identity & Access-Control - **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 · Gitea #3 — Monitoring & Mgmt-Control-Plane - 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 · Gitea #4 — IPv6 (intern + extern, 3 Standorte) - Konzept für FM/BF/DW. → TP-R. #### TP12.1 · Gitea #5 — IPv6 im LAN (ULA) + v6-Firewall > **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`](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 · Gitea #6 — „Botschafts-WLANs" — `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 · Gitea #7 — Session D: CF-Access-Profil für Claude — `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`](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 · Gitea #8 — CF-Tunnel: Round-Robin killen + reziprokes HA — 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 · Gitea #9 — Härtung der öffentlich erreichbaren Dienste (bypass-Strang) — 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 · Gitea #10 — cloudflared-Reduktion: was der Mesh als Datenpfad kann (und was nicht) — 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`](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:///`**, 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.154:8080/api` (TP45 Schicht A 2026-09-21; zum Messzeitpunkt war es `.20.30`), 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.154:8080` (TP45 Schicht A 2026-09-21) 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 `.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 `/.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.154: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.155` (`nautobot-worker`), nicht von `.154` (Werte nach TP45 Schicht A 2026-09-21; zum Messzeitpunkt `.31` / `.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`](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 · Gitea #11 — Shelly Gen4 raus aus dem WLAN — 📋 `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`](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`](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](teilprojekte-archiv.md#tp17) (Migrationshistorie + `mcp/shelly/`-Werkzeug).
### TP17.3 · Gitea #12 — Shelly-Regression nach der VLAN30-Migration — 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 [--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 · Gitea #13 — Souveräner Objektspeicher (Garage, Multi-Site, S3-Cold-Tier) - **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 · Gitea #14 — Vaultwarden → Platform-Postgres + Cross-Site-HA — 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 · Gitea #15 — DNS-Policy & Self-Service-Ausnahmen > 🔴 **Redesign 2026-09-12 — Stufe 5 reopened.** Bei „Stufe 6 angehen" fiel auf: die 09-04-Stufe-5 > (single-domain, temp-only 15m/1h/4h, **eine** Ausnahme gleichzeitig, **HA-Neustart bei neuer > Domain**, `allow-domain` global statt pro Person) deckt die Nutzer-Requirements **nicht** ab > („funktional ein kastriertes Pi-hole", User). Die harte Reihenfolge-Regel „erst der Ersatz, dann > die Sperre" ist damit real **nicht** erfüllt → **Stufe 6 darf nicht laufen**, bis der neue > Self-Service nachweislich trägt. > - **Neue Requirements (User 2026-09-12):** (1) **pro Person** (Login), Sicht über alle Geräte; > (2) **„zuletzt geblockt" → 1 Klick**; (3) temporär **und** dauerhaft, Modus umschaltbar > (self-permanent ⇄ Anfrage, Start self-permanent); (4) **scoped HA-Dashboard** je Person — > nichts von anderen/Admin/Power-User, AGH-Web-UI bleibt reines Admin-Werkzeug. > - **Design/Bau:** Personen-Client je AGH (SSoT Person→Gerät), Ausnahmen via `$client=`-Modifier, > `agh-exception.py` um scoped `allow/unallow` + `list-exceptions` + `recent-blocked` erweitert > (reine Logik netzunabhängig getestet, `selftest` ✅). HA-Dashboard/Sensoren/Modus-Switch als > Bau-Spec in [`adguard/ha-self-service.md`](../adguard/ha-self-service.md) (on-network zu bauen). > - 🔴 **Load-bearing, on-network zu prüfen:** wirkt AGH `$client=` auf **Allow**-Regeln (`@@`)? > Fallback = IP-Liste statt Client-Name (Code kann beides). Plan: > `~/.claude/plans/tp23-stufe-6-angehen-soft-sunrise.md`. > - **Neue echte Stufe-6-Vorbedingung:** End-to-End-Abnahme mit echtem Nicht-Admin-Flow. > - **Stufe-6-Runbook-Korrektur:** Core `10.40.X.1` in der Allowlist überdenken (Client-Gateway → > DoH direkt zu CF = interner Bypass der AGH-Personen-Policy). > 🟢 **Update 2026-09-04 — Stufe 4 + 5 gebaut, live verifiziert. Der `.16`-Text unten ist überholt** > (Stufe 2 lebt seit 08-26 auf dem Paar `agh-bf1`/`agh-bf2` = `.36`/`.37`, LXC 110/111 — s. Ist-Stand-Block): > - **Stufe 4 (CIDR-Substrat):** 7 VLAN-Gruppen + `dns-bypass-exempt` in `clients.persistent` auf beiden > Instanzen; Policy-Matrix (INFRA/MEDIA/MGMT `filtering=aus`, GUEST safe-search+parental an). Neuer > AGH-API-User `automation` (PW in `~/.secrets` → TP30). Sync-Werkzeug `adguard/agh-sync.py` > (`check`/`push`), Repo-SSoT `adguard/AdGuardHome.yaml` mit **redigierten** Hashes. Invariante `.36==.37`. > - **Stufe 5 (HA-Self-Service):** `agh-exception.py` (RMW-CLI, `allow-domain`/`unallow`/`unfilter`/`refilter`) > auf HAOS deployt; `shell_command` + 5 Helper + Timer + 2 Automations (`ha-mcp`). Button → Domain für > 15 min/1 h/4 h entsperren, nativer `timer` revertiert; RouterOS-`jit-dns-bypass`-Backstop erst mit Stufe 6. > Doku: `adguard/README.md` + `adguard/ha-self-service.md`. End-to-End getestet (Start + Ablauf). > - **Offen:** nur **Stufe 6** (DNS-Bypass-Sperre) — bewusst NICHT geschaltet. Runbook für eine > **separate Session**: [`docs/tp23-stufe6-bypass-sperre-runbook.md`](tp23-stufe6-bypass-sperre-runbook.md). > 🔴🔴 **HARTES VOR-ORT-GATE:** macht AGH `.36/.37` zum harten DNS-SPOF, aus der Ferne nicht > reparierbar → **NUR vor Ort, NICHT während Christoph abwesend ist** (2026-09-06: ~1 Woche weg → > in dem Fenster gar nicht anfangen). Methodik: log-only-Vorstufe zuerst, Drop mit Auto-Revert zuletzt. > - **WARP-Managed-Network (Stufe 2 im alten Plan)** bleibt offen (TP41-nah). Der `.16`-basierte > Abschnitt darunter ist die historische Fassung (2026-08-18), bewusst erhalten. **[Historische Fassung 2026-08-18 — `.16`-basiert, durch den Block oben überholt:]** - ✅ **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 · Gitea #16 — Unterbrechungsfreie Stromversorgung (DC-USV, 48V-Bus) — 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 · Gitea #17 — WLAN-Stabilisierung — → [Archiv](teilprojekte-archiv.md#tp25) > **Abgeschlossen:** Platzierung final (ap1 Sideboard · ap2 Technik UG · ap3 Technik OG), Fund E > geschlossen (`7.24.1` war die Ursache, nicht `ft-over-ds`), alle vier Handoff-Punkte aufgelöst, > Gitea #17 closed. Volltext (Kondensat + Langfassung) im Archiv. ### TP28 · Gitea #18 — WLAN über CAPsMAN auf `crs326-core` — → [Archiv](teilprojekte-archiv.md#tp28) > **Abgeschlossen:** ap1 Sideboard · ap2 Technik UG · ap3 Technik OG (Platzierung final), > Kanalplan committed (ap1 `5260/5280` · ap2 `5300/5320` · ap3 `5180`), `wifi-cap`-Rest auf dem > Core bereinigt (`enabled=no`), Compliance 29/29 über 5 Geräte. Gitea #18 closed. Volltext im Archiv. ### TP29 · Gitea #19 — Anwesenheitserkennung fürs Energiesparen > **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`](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 · Gitea #20 — Credentials: neu setzen & rotieren (Sammelbecken) > **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. *(2026-09-21: eine frühere „harte Ausnahme für neue Leaks" wurde **gestrichen** — sie widersprach dem Bündelungszweck dieses TPs und der Praxis: auch die leak-getriebenen Posten #1 und #5–#9 stehen unverändert im Bündel. Ein Transkript-Leak zieht **keinen** Posten vor.)* | # | 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 | 🔴 **2026-09-21 erneut ins Transkript geraten** (Session-`grep` unmaskiert). Der bisherige harte Termin ist **entfallen**: ⏳ „2027-01-09" galt für das längst revokte Alt-Token `9984078812bc…`; das **live** `CF_API_TOKEN` (id `493ed8bd…`) hat **kein** Ablaufdatum — verifiziert 2026-09-21 via `/accounts/{id}/tokens/verify`, `expires_on: null` | 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 ` **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=` 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) | | 9 | **`GITEA_ACCESS_TOKEN`** (API-Zugang `gitea.emsenhuber.net`) | Session-Tooling (`bin/gitea-tp-sync.py`, Ad-hoc-API-Calls) | 🔴 am 2026-09-14 (TP25-In-situ-Session) **ins Transkript** geraten: Env-Parse-Fehler schob den Token in die URL-Konstruktion, `urllib` warf `ValueError: unknown url type` und druckte ihn im Traceback | Mechanismus zu: Token gehört **nur in den `Authorization`-Header, nie in eine URL** — und der Fehlerpfad, der ihn enthält, darf ihn nicht ausgeben (gleiche Familie wie #1/#5/#6/#7/#8, diesmal Unterklasse „Fehlerausgabe statt Maskierung") | neu anlegen (Gitea → Settings → Applications), `~/.hermes/.env` nachziehen, altes widerrufen; Konsumenten = `gitea-tp-sync.py` + Session-Ad-hoc-Calls | | 10 | **`N8N_MCP_BEARER`** (n8n-Public-API) | n8n-MCP `macmini:3001` — in **7 Projekten** referenziert (`bf-infra`, `blaueblueten-n8n`, `travelmate`, `poulios-recherche`, `ha-xstate`, `valamar`, `uwe-job-bewerbung`) | 🔴 am 2026-09-22 (Env-/OpenRouter-Session) **ins Transkript** geraten: die Redaktions-Alternation führte `BEARER` nicht, und der Schlüssel endet **ohne `=`** auf den Bezeichner — die Feld-Ersetzung hatte damit keine Ankerstelle | Ablage nur `~/.secrets/exports.zsh`; alle Konsumenten sind `${…}`-Referenzen (kein Klartext in den Repos, geprüft) | Token in n8n neu erzeugen, `~/.secrets` nachziehen, **App-Neustart** (MCP-Env wird beim Start gelesen) — danach 7 Projekte ohne Edit, weil sie nur referenzieren | | 11 | **`TERPENE_RO_URL`** (PG-User `terpene_ro`, DB `terpene`) | Terpenfinder-DB read-only `10.40.20.20:5432` — **Cross-Projekt (blaueblüten)**; einziger Konsument ist `~/.secrets/exports.zsh` (+ `.bak`) | 🔴 am 2026-09-22 (Env-/OpenRouter-Session) **ins Transkript** geraten — **neue Unterklasse derselben Familie:** das Passwort steckt in der **URL** (`…://user:PASS@host`), die Feld-Maskierung ankert auf `=` und findet dort keine Zeile zum Ersetzen (dieselbe Lücke wie #9: was nicht als Feld auftritt, wird nicht maskiert — das geteilte Redaktions-Snippet deckt diesen Fall **nicht** ab) | Wert nur in `~/.secrets/exports.zsh`; **kein** Konsument in Code oder Configs | `ALTER ROLE terpene_ro … PASSWORD ''` am NAS (`10.40.20.20`), dann `exports.zsh` nachziehen; **kein** Geräte-Roundtrip ⇒ billigste Rotation nach #6. ⚠️ Cross-Projekt: nur mit Christophs Okay anfassen | | 12 | **OpenRouter-API-Key** (`sk-or-v1-cb6…258`) | Claude-Desktop-3P-Gateway: CCR-Provider in `~/.claude-code-router/config.sqlite` (`Providers[].api_key`) **+** 3P-Config `e1219ea4` (`inferenceGatewayApiKey`) | 🔴 am 2026-09-23 (Claude-Desktop/OpenRouter-Session) ins Transkript geraten — das `sed` ankerte auf den **geratenen** Namen `apiKey`, der echte Schlüssel heißt `inferenceGatewayApiKey` ⇒ kein Match (gleiche Familie wie #5/#10/#11, neue Unterklasse s. Merkregel unten). Zweiter Fund derselben Session: `head -c 400` auf `~/.claude-code-router/bin/ccr-claude-code-api-key-claude-code` gab den **CCR-Profil-Token** aus (local-only `127.0.0.1`, geringe Reichweite — nicht in der Zählung) | ⚠️ **kein `~/.secrets`-Eintrag** — der Wert lebt nur in `config.sqlite` + 3P-Config, und er ist **noch gültig** (200, verifiziert 2026-09-23 via `/api/v1/key`) | neu ausstellen (openrouter.ai/keys); danach **zwei** Konsumenten: `config.sqlite` (`Providers[].api_key`, mit CCR-Neustart) + 3P-Config (`inferenceGatewayApiKey`) — beide müssen mit, sonst steht die App still (3P ist alles-oder-nichts). **Nachtrag 2026-09-24 (Konsumenten-Inventur korrigiert, live geprüft):** die **angewandte** 3P-Config `8f69f2f1` (= „Claude Code Router", `appliedId` seit 2026-09-24) trägt **nur den CCR-Gateway-Token** (len 50, `x-api-key`, gegen CCR mit 200 verifiziert) — **nicht** den OR-Key. Der OR-Key lebt real in **`config.sqlite`** (live, von CCR benutzt) **und** in der nur *gespeicherten*, nicht angewandten `e1219ea4` (len 73, sha `f23aaebdaa4c`). Rotation also: `config.sqlite` → `Providers[0].api_key`, **CCR-App neu starten**, `e1219ea4` löschen/ersetzen; `8f69f2f1` bleibt **unangetastet** (sonst steht die App still) | > ✅ **Der Leak-MECHANISMUS hinter #1 ist seit 2026-08-31 zu — der Wert bleibt trotzdem verbrannt.** > Alle sechs Skripte im Repo, die `sshpass -p ` 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 ` 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=/g'` > — ankert auf den **Schlüsselnamen inkl. Präfix** und deckt alle bisher gesehenen Suffixe ab. > > 🔴 **#9 ist der sechste Vorfall derselben Familie, aber eine neue Unterklasse** (SHELLY 08-28, > Nautobot-Tokens 08-31, MariaDB-root 09-01, Keystore 09-01, SMTP 09-01): diesmal kein > Maskierungsmuster, sondern ein **Fehlerpfad** — ein Env-Parse-Fehler schob den Token in die > URL-Konstruktion, und `urllib` druckte ihn im `ValueError: unknown url type`-Traceback. > **Merkregel: ein Secret gehört in keine Datenstruktur, die eine Exception ausgeben kann** > (URL-Konstruktion validieren, bevor das Secret hineinwandert; Fehlerpfade dürfen den > fehlerhaften Wert nicht mitschleppen). Das Redaktions-Snippet oben hilft hier nicht — es > kann nichts maskieren, was als Fehlermeldung und nicht als Dateiinhalt kommt. > > 🔴 **#12 ist die siebte Panne derselben Familie — und wieder eine neue Unterklasse** (SHELLY 08-28, > Nautobot-Tokens 08-31, MariaDB-root #6, Keystore-`_PW` #7, SMTP-`PASS` #8, Gitea-Traceback #9): > diesmal war die Quelle **JSON**, nicht Env. Das geteilte Snippet oben ist auf **`KEY=VALUE`-Zeilen** > gebaut (Anker `([A-Z0-9_]*(…|KEY|…))=[^[:space:]]*`) — es kann `"inferenceGatewayApiKey": "…"` > gar nicht treffen: **camelCase**-Schlüssel, kein `=`, kein Zeilenanfang. Wer JSON mit dem Env-Snippet > redigiert, maskiert **nichts** — und genau das ist passiert; das danach handgetippte `sed` ankerte > auf den **geratenen** Namen `apiKey` statt auf den echten. **Merkregel: für JSON einen JSON-bewussten > Masker benutzen** — Objekt laden, über **Schlüsselnamen** filtern (case-insensitiv, Teilstring ok), > jeden Treffer durch `` ersetzen; nie das Env-Snippet, nie ein geratenes Muster. > Und: **Schlüsselnamen erst auflisten, dann maskieren** — „ich weiß schon, wie der Key heißt" ist > genau die Annahme, die hier gerissen ist. > > ℹ️ **#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](teilprojekte-archiv.md#tp17)) 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](teilprojekte-archiv.md#tp17)), 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~~ — **korrigiert 2026-09-21:** dieses Datum gehörte zum revokten Alt-Token `9984078812bc…`; das **live** `CF_API_TOKEN` (id `493ed8bd…`) hat **kein** Ablaufdatum (`expires_on: null`, via `/accounts/{id}/tokens/verify`). Der harte Termin ist damit weg — **Anlass** ist jetzt der **Leak vom 2026-09-21** (🔴 siehe Posten #4 oben). Der Posten bleibt **gebündelt** und wird nicht vorgezogen (Auslösekriterium oben). #### 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/=.*/=/'` 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 `<>` 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 · Gitea #21 — Reverse Proxy für innen und außen; cloudflared von sechs Knoten auf einen > ✅ **Durch — Schritt 5+6 sind über den Piloten [TP16.3](#tp163) 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`](../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](teilprojekte-archiv.md#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/ --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`](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 = .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](#tp163) §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](#tp163) §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 :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 `..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 `.`-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`](../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](#tp20-1) sieht **manuelles Promote** vor. Zwei Connectoren auf **verschiedene** Caddys an verschiedenen Standorten würden die Hälfte der **Schreib**zugriffe 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](#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 · Gitea #22 — Nummern- und Adress-Systematik > **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 | Ableitung aus TP32 | Anker | |---|---|---|---| | `.1–.9` | Gateways / L3 | Bestand | `.1` je VLAN ✅ · `99.1` core, `99.2` branch ✅ | | `.10–.19` | physische Netzhardware im VLAN | APs/Switches, sobald sie im VLAN liegen | heute nur VLAN99: `99.11/.12/.13` ✅ · VLAN30: Nuki-Bridge, NSPanelPro, P750W, DTUBI | | `.20–.29` | Core / netznah | Kreis 100–199, Stufe `10x` | mcast-relay (heute `10.21`/`40.21` — **kein VLAN20-Bein**) | | `.30–.39` | DNS / Resolver | Stufe `11x` | AGH-Paar — **Soll** `.30`/`.31`, Ist `.36`/`.37` (bis Nautobot dort weg ist) | | `.40–.49` | Ingress / Reverse-Proxy | Stufe `13x` | caddy `20.40` ✅ | | `.50–.59` | Datenbanken | Kreis 200–299 | Soll `platform-pg .50`, `mariadb .51`, `influxdb2 .52`, `redis .53` | | `.60–.69` | Security / Identity | Kreis 300–399 | Soll `vaultwarden .60` | | `.70–.79` | Monitoring | Stufe `15x` | frei | | `.80–.89` | Platform / Runtime | Kreis 400–499 | Soll `docker .80`, `coolify .81` | | `.90–.99` | Broker / Funk | **Ordnungsgruppe 40** (s. u.) — *nicht* aus dem VMID-Kreis abgeleitet | Soll `mqtt5 .90`, `z2m25 .91` | | `.100–.149` | **Server-Hosts** (physische Bleche) | `.10x` Reserve · `.11x` Storage · `.12x` Hypervisor · `.13x` Bleche/Workstation/Backup | Soll `nas .110`, `pve2 .120` (reserviert), `pve .121`, `macmini .130`; Ist `pbs 20.135` ✅ | | `.150–.199` | App-Services / Gäste | Kreis 500–599 | `gitea 20.157` ✅, `pve-mcp .173` ✅, `shelly-mcp .180` ✅ | | `.200–.249` | DHCP-Pool | Bestand | VLAN20 soll `.200–.249` (Ist `.100–.200`, `used=0`) | | `.250–.254` | VIPs / Relays | Bestand | | 🔑 **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. ⚠️ **Kein Rechenweg — eine Nachschlagetabelle.** Die Zeile `.90–.99` (Broker/Funk) ist die **Ausnahme**: sie stammt nicht aus dem VMID-Kreis, sondern aus der Ordnungsgruppen-Tabelle unten (Stufe 40). **Wo die beiden Achsen auseinanderlaufen, gilt die Gruppe, nicht die Ziffer** (Christoph 2026-09-21). Dasselbe Muster bei `.100–.149`: die Server-Hosts werden über den **Gerätetyp** einsortiert (Blech), nicht über eine VMID. ⚠️ **Solange eine Alt-IP als Zweitadresse weiterläuft, ist sie reserviert** — `.10` (nas), `.100` (pve), `.167` (macmini) dürfen bis zum Rückbau **nicht** neu vergeben werden. Deshalb bekommt z. B. `z2m25` **nicht** `.167`, sondern `.91`. #### 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` | | 50 / 51 | Anwendungen | `101 iob-master`, **`100 haos`**, `150 iob-exposed`, `330 RaspberryMatic` | 🔴 **Korrektur 2026-09-21 (Christoph):** die CCU gehörte **nie** in die Funk-Gruppe. „Funk" ist `102`/`112` — die CCU läuft in der **Anwendungs-Gruppe**, in derselben wie `haos`. Live stand sie noch auf `order=40`; der Geräte-Write ist Teil desselben Zuges (nicht nur diese Doku-Zeile — eine Doku, die dem Gerät vorauseilt, ist genau der Drift, den TP32 vermeiden soll). 🎯 **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) — **läuft jetzt an**, nicht mehr „am Ende der Reise": siehe **TP45** (VLAN20 + VLAN30 ins Schema, danach DNS/FQDN). Die alte Kollision **AGH will `.30`, dort sitzt Nautobot** (`.31` = nautobot-worker, `.32` = scheduler, `.34` = redis) ist dort **Schritt 1**: Nautobot zieht in den App-Block (`20.154–.156`), **erst danach** greift AGH `.30`/`.31`. Reihenfolge bewusst so — der GC-Pfad läuft über Nautobot, ein Umzug von AGH zuerst würde die Werkzeugkette gegen sich selbst laufen lassen. - **Abh.:** keine. **Blockiert nichts**, aber TP31 vergibt bereits danach. #### 🔎 Bestands-Fehler, die beim Umsetzen auffielen (2026-09-21, live gemessen) Die frühere Fassung dieser Tabelle war an vier Stellen **nicht gemessen, sondern angenommen**: `mcast-relay` liegt **nicht in VLAN20** (Beine `10.40.10.21` + `10.40.40.21`), der Anker der Zeile `.20–.29` war damit falsch; `nautobot` sind **vier** Gäste (`.30`–`.32`, `.34`), nicht drei; und der **DHCP-Pool in VLAN30 enthält ~25 Statics** (`.202–.249`) — die VLAN30-Nummerierung war ebenfalls gewachsen, nicht geplant. Merkregel: **„Bestand bleibt, wo er ist" gilt nur, solange niemand behauptet, wo er ist, ohne nachzusehen.** ### TP33 · Gitea #23 — Sonos aus der Ferne bedienen (VPN/WARP) — 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.** --- ### TP35 · Gitea #25 — MCP-Schichtung global/projekt/user + 3-Schicht-CLAUDE.md + Announcement-Mechanismus + Backup-Guard — 5/6 Schichten durch (MCP-Schichtung FQDN→IP, Credentials-`{env:}`-Flip angewandt+verifiziert, globale CLAUDE.md-Schicht, `/news`, Backup-Guard, `/projektstatus`-Skill). Schicht-3-**pro-Projekt**-Rollout laut Abgrenzung eigene Session je Projekt (kein Blocker). Einziger Rest = On-Network-mikromcp-Smoketest (off-site nicht machbar, VLAN99:80 timeout, non-blocking) → als Merkposten nach TP30. Gitea #25 closed/`status:done`. - **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. **✅ 2026-09-12 angewandt:** `exports.zsh` um die 3 fehlenden `MCP_MIKROMCP_{ROUTER_AP3,CRS326_CORE,CRS326_BRANCH}_PASS` ergänzt (jetzt 25 Vars, die opencode schon referenzierte — vorher liefen die dort leer), `gui-env-sync` gelaufen (25/25 in launchd-Env). `~/.claude.json` via `~/dev/.bin/schicht2-claude-json-migrate.py --apply-claude` (bei beendeter App, Backup `.bak-20260912-*`) geflippt — **alle 7 Secrets** (`mikromcp` 5× PASS, `hostinger-api` Token, `n8n-mcp` Bearer) jetzt `{env:}`, verifiziert; App neu gestartet, MCP-Server laden sauber. 🔴 **Merkregel:** `~/.claude.json` nie aus laufender Session editieren (Claude Code schreibt dort State → überschreibt). Restcheck offen: **authentifizierter** mikromcp-Read **on-network** (am 09-12 Connect-Timeout VLAN99:80 = Erreichbarkeit vom Standort, kein Auth-Fehler). Rollback = `.bak` zurück. 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 + Skill zum Einlesen beim Session-Start. **✅ 2026-09-12:** `docs/announcements.md` angelegt (Format: `## YYYY-MM-DD — Titel` + `Betrifft:`/`Aktion:`, neueste oben) + globaler Skill **`/news`** (in beiden Skill-Pfaden `~/.claude/skills/news/` **und** `~/.config/opencode/skills/news/`, `cp` kein Symlink — Schicht-6-Lehre). `/news` zeigt Einträge, die seit dem letzten Lauf dazukamen (Seen-State pro Repo unter `~/.news-seen/`). Erste Einträge: SSoT-Cutover (TP36) + TP36-Live. 5. **Backup-Guard** — `~/dev/.bin/backup-guard.sh`, Pre-Commit-Hook pro Projekt, „3 Tage aktiv"-Definition: 5 Commits oder 1000 LOC diff. **✅ 2026-09-12 abgenommen:** Guard (112 Z., `check`/`mark`/`status`) + `.git/hooks/pre-commit` in `bf-infra` live verdrahtet; der Hook hat beim TP36-Live-Commit real gegriffen („Projekt aktiv 12 Commits/198 LOC, Backup markiert"). ⚠️ **Bekannte Grenze:** `check` prüft nur *Existenz* der Guard-Datei, nicht deren Alter — ein alter `mark` (hier 2026-08-29) lässt den Commit durch. Das ist als Push-Erinnerung gewollt, keine Frische-Garantie. 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.2 (Daten→Config-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 · Gitea #26 — Index/Status als SSoT nach Gitea (Steuerung aus der branchbaren Datei herauslösen) — Index/Status = Gitea-Issues, Prosa bleibt Datei - **Auslöser:** Skill-Run 2026-08-28 (`docs/handoff-20260828-session-a-projektstatus.md`) lieferte einen überholten „Nächster Schritt"-Output; **manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, sobald sie lang wird.** Verschärft 2026-09-06: `docs/teilprojekte.md` wurde in **40/40** letzten Commits angefasst — jeder TP editierte Quick-Index **und** Ist-Stand-Block → **Merge-Konflikt-Magnet**, der Branch-pro-TP verhinderte (ein Parallel-Branch hatte so die TP37-Doppelvergabe verursacht). - 🔄 **Reframe 2026-09-06 (Christoph):** von „SSoT via **Nautobot**" auf „SSoT via **Gitea**". Grund: Index+Status sind **kuratierte Steuerung** (Reihenfolge, Marker, Abhängigkeiten), kein Geräte-Live-Zustand — die gehören auf ein **Issue-Board**, nicht in Custom Fields. **Nautobot bleibt nur für Live-Gerätedaten** relevant (Compliance-Zahl, RouterOS-Version …) → als optionaler Rest unten, entkoppelt von diesem TP. - **Modell (umgesetzt):** TP = **Gitea-Issue** in [`admin/bf-infra`](https://gitea.emsenhuber.net/admin/bf-infra/issues) · Status = Label `status:*` (todo/wip/paused/idea/concept/done/dropped) · Gruppierung = Label `thema:*`. Seeder `bin/gitea-tp-sync.py` (idempotent, Dry-Run default, `--commit` schreibt; Token aus `~/.hermes/.env`, **nie ausgeben**). Erstlauf: **68 Issues (25 open/43 closed) + 15 Labels**. - ✅ **Erledigt (A1–A3):** Labels/Modell · 68 TPs → Issues · Board = Index (Label-Filter statt Handtabelle). - ⏳ **Cutover 2026-09-07 (diese Session):** - **A4** — Quick-Index-Tabelle + „Ist-Stand (Kurz)"-Block aus dieser Datei entfernt; Header zeigt aufs Gitea-Board; die vier **Genehmigten Ausnahmen** unter eigenem Governance-Header behalten. Alle `### TPnn`-Prosa bleibt. - **A5** — `/projektstatus` liest Baum+Status aus der **Gitea-API** (Issues + `status:*`/`thema:*`), Transaktionen weiter aus `nautobot/gc-transaktion.py status`, Prosa-Detail bei Bedarf aus dem Repo. Beide Skill-Kopien (`~/.claude/skills/…` **und** `~/.config/opencode/skills/…`, `cp`, kein Symlink). - **A6** — Projekt-`CLAUDE.md`: Pflichtlektüre = Gitea-Board (Status) + Prosa-Datei (Detail); Branch-Konvention (`tpNN-kurz` jederzeit, Merge nach `main` wenn Code/Config steht, Steuerungs-Updates nach Gitea); SSoT-Notiz. - 🔜 **Optionaler Rest (entkoppelt, „TP36-Live"):** Live-Daten-CustomFields auf den **4 `dcim.Device`** (`ros_version`, `compliance_pct`, `dhcp_server_count`, `mgmt_ip`, `dns_upstream_mode`) + Snapshot-Skript `nautobot/gc-snapshot.py` (existiert noch nicht). Unabhängig von diesem Cutover; nur relevant, wenn die Doku Live-Zahlen tragen soll. ⚠️ **Namensfalle bereinigt:** `nautobot/tp36-cf-estate-seed.py` gehörte **nie** zu TP36 → seit 2026-09-06 `tp44-cf-estate-seed.py`. - **Verzahnt mit:** TP35 (SSoT + Konsolidierung), TP44 (native Nautobot-Modelle), `bf-infra/CLAUDE.md`-Lessons. Plan: `~/.claude/plans/geht-das-los-auch-reflective-lark.md`. ### TP38 · Gitea #28 — Out-of-Band-Management: ein untagged-VLAN99-Port je Switch *(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 · Gitea #29 — Home-Assistant-Recorder-Retention: die 41-GB-`states`-Tabelle beschneiden **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. ### TP37 · Gitea #27 — Nautobot als durchgehende Netzwerkdoku: Hosts/Container/Dienste, erster Anwendungsfall CF-Connectoren > **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 — ERLEDIGT via [TP44](teilprojekte-archiv.md#tp44) (Etappe 1, 2026-09-02).** Die Modellwahl ist damit entschieden: **native `nautobot.cloud`-Modelle** (Tunnel/WARP = `CloudNetwork`, VPC/Hyperdrive/Worker/ Access-App = `CloudService`, IDs/Ports/Origins in `extra_config`), **keine eigene App**. Offen nur noch TP44 Etappe 2 (CF-API→Nautobot-Sync). ⇒ **Punkt 1+2 unten sind der echte Rest von TP37.** 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.** ⚠️ **Nachtrag 2026-09-22:** TP34 ist **❌ verworfen** (09-08, #24 — der Caddyfile bleibt manuell); der *Daten → gerenderte Config*-Gedanke lebt als **[TP34.2](#tp342)** (interne DNS-Namen) weiter. - **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.2** (das Caddy-Teilstück davon bleibt ❌ verworfen). **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:** ✅ **Punkt 1+2 (Substrat + Dienste) ERLEDIGT** — die frühere Zeile „offen" war Doku-Drift. Live gegen die API gelesen **2026-09-21**: **28 VMs · 3 Cluster · 26 `ipam.Service` · 38 `ipam.IPAddress` · 9 Devices · 5 CustomFields**. Beide Seeder sind also gelaufen (`tp37-substrat-seed.py`, `tp37-dienste-seed.py` — die 26 Services sind genau die 26 Zeilen der Dienste-Liste). Datenmodell **entschieden 2026-09-06 (native Modelle: `virtualization.Cluster/VirtualMachine` + `ipam.Service`, keine App)**; Runbook [`handoff-20260906-tp37-34-36-plan.md`](handoff-20260906-tp37-34-36-plan.md). **Offen bleibt Punkt 4** (Drift-Check gegen die CF-API) — und der Weg **Daten → gerenderte Config** ist nicht mehr TP37, sondern **TP34.2 / TP45 Schicht C**. ⚠️ **Merkregel:** die Zeile stand seit dem 09-06 auf „offen", obwohl die Seeder längst gelaufen waren. Ein Status, der nur in einer Datei lebt, altert still — der Abgleich gegen die API kostet eine Minute. --- ### TP34.2 · Gitea #73 — Interne DNS-Namen als Daten (FQDN aus Nautobot) — **gemeinsame Arbeit** > **Auslöser:** beim Schärfen von **TP45 Schicht C (C4)** am 2026-09-22 stand die Frage, was aus dem > verworfenen **TP34** wird. Christoph: *„C4 unter TP45 schärfen, TP34 wird gemeinsame Arbeit. wir beide!"* > → **eigener Träger im TP34-Nummernkreis**. Parent **#24 (TP34) bleibt `status:dropped`** — kein Reopen: > dort wurde *Caddyfile-Rendering* verworfen, hier geht es um *interne DNS-Namen als Daten*. **Kein > Zwillings-TP mit neuer Nummer**, aber auch kein Wiederbeleben eines anderen Gegenstands. - **Scope:** Die **27 internen `A`-Statics** auf `crs326-core` (`*.bf.emsenhuber.net`, live geprüft 2026-09-22) werden **Daten in Nautobot**; ein Renderer schreibt sie als GC-Soll. **Abnahme:** ein Golden-Config-Recovery von null erzeugt einen Core, in dem die internen Namen **wieder auflösen**. - **Ausdrücklich NICHT Scope:** Caddyfile-/Reverse-Proxy-Rendering (der verworfene TP34-/`.1`-Ansatz) — der `caddy/Caddyfile` bleibt **manuell**. - 🔴 **Warum es das braucht:** die Namen existieren heute **nur am Gerät** — Handarbeit mit Freitext-Kommentaren (`"caddy -> nautobot :8080"`), und GC rendert sie **nirgends**. Ein Recovery von null erzeugt damit einen Core **ohne interne Namensauflösung**: dieselbe **Recovery-Lücke** wie die DHCP-Reservierungen und die `nautobot`-Adressliste (beide inzwischen als Renderer nachgezogen, TP45 Schicht B). ⚠️ **Compliance meldet trotzdem grün — die Zahl prüft nur, was gerendert wird.** - **Datenlage / offener Modell-Entscheid:** `ipam.Service` (**26**) und `ipam.IPAddress` (**38**) stehen (TP37 P1+2 ✅). Für DNS braucht es voraussichtlich `ipam.IPAddress` + `dns_name` je Gerät/Dienst, mit **zwei Zielarten**: Geräte-Statics zeigen auf die echte IP, Proxy-Namen auf **`10.40.20.40`** (Caddy) — dazu je Name ein Kommentar-Feld. Der eigentliche Modell-Entscheid steht noch aus (**nicht raten**). - **Abh.:** TP37 ✅ (Datenmodell) · TP45 **Schicht A** ✅ / **E** offen (die Namen müssen auf die **neuen** Gast-IPs zeigen) · TP21/22 ✅ (GC-Pipeline). **Blockiert:** nichts. - **Abgrenzung:** die `coolify.bf`-Frage bleibt in **TP45 Schicht C** (Port unbekannt — nicht raten); Token-**Rotation** ist TP30. --- ### TP45 · Gitea #71 — VLAN20/VLAN30 ins TP32-Schema + DNS/FQDN aus Nautobot > **Auslöser (Christoph, 2026-09-21):** *„schau dir mal die fqdns von `*.emsenhuber.net` an. viele > verschwunden. sowohl caddy als auch cloudflare."* — und, als der Befund stand: > *„wenn wir schon reverse proxy & co korrigieren müssen, wäre es vl jetzt ein guter zeitpunkt, > unsere infrastruktur vlan20 an das ausgedachte ip-schema anzupassen. danach dns/fqdn."* #### Befund (live, 2026-09-21) — **weder Caddy noch Cloudflare haben etwas verloren** | # | Befund | Beleg | |---|---|---| | 1 | **CF ist gewollt geschrumpft** — 24 Records, 8 proxied; `bf-public` trägt 5 (`vw`,`hass`,`n8n`,`gitea`,`ds-bf`), `fm` 2 | CF-DNS-API | | 2 | **Caddy hat nichts verloren** — 16 Sites, live **byte-gleich** zum Repo (`diff` identisch, 2977 B) | LXC 130 | | 3 | 🔴 **Internes Split-Horizon tot** | s. u. | | 4 | 🔴 **`ds-bf` öffentlich kaputt** | s. u. | 🔴 **Root Cause #3 (bewiesen).** AGH wertet DNS-Rewrites **im Filter-Pfad** aus; für Clients mit `filtering_enabled: false` werden sie **übersprungen**. Genau die Gruppen, von denen gebrowst wird, sind unfiltriert (`dns-bypass-exempt` = beide Mac-NICs, `grp-infra`, `grp-media`, `grp-mgmt`). Diese Clients fallen auf den conditional forward `[/bf.emsenhuber.net/] → 10.40.20.1` (Core) zurück — der Core hält aber nur **10** Host-Records; die **11 Dienstnamen** (`nautobot`, `portainer`, `pbs`, `ccu`, `iobroker`, `z2m25`, `tv`, `mfc`, `ha-mcp`, `gitea-mcp`, `n8n-mcp`) existieren **nur** als AGH-Rewrite → **NXDOMAIN**. Aus dem AGH-Container selbst (`127.0.0.1`, kein Client-Match) liefern dieselben Rewrites korrekt — **deshalb war jede Handoff-Abnahme grün: sie wurde nie vom Browser aus gemessen.** Zweitdefekt: `pve.bf`/`pve-mcp.bf` **doppelt** definiert (Core → Host-IP, AGH → Caddy `.40`). 🔴 **Root Cause #4 (bewiesen, Firewall).** Der Origin antwortet aus VLAN20 in 0,08 s mit `200`, öffentlich `000` nach 20 s — die vier anderen Namen desselben Tunnels sind in ~0,2 s da. `vw`/`hass`/ `n8n`/`gitea` gehen auf `https://10.40.20.40:443` (**Caddy**); `ds-bf` geht **direkt** auf `http://10.40.20.10:8080` (**NAS**) und ist damit der **einzige Ingress-Eintrag, der Caddy umgeht** → die AP-Connectoren (VLAN99) dürfen per Regel `*42` **ausschließlich** Caddy:443, alles andere fällt unter `*1A TP5-WOULD-DROP` / `*2D TP5: deny-by-default` (621 Pakete). **Fix ohne Firewall-Änderung: `ds-bf` über Caddy führen.** #### Schichten (Reihenfolge = Abhängigkeit) | Schicht | Inhalt | Stand | |---|---|---| | **A** | VLAN20 virtuelle Gäste ins Schema — **Nautobot zuerst** (`.30–.34` → `.154–.156`/`.158`), dann AGH | 🔄 **Nautobot ✅ 2026-09-21** (s. u.) · AGH + übrige Gäste offen | | **B** | VLAN30 — **nur die Pool-Grenze**, Geräte bleiben (Christoph 2026-09-21) | offen | | **C** | DNS/FQDN: `ds-bf` über Caddy, `coolify.bf` neu, `.bf` vollständig auf den Core, AGH nur Filtering, TP34.2 | ✅ **C1–C3 durch 2026-09-22** (s. u.) · offen: **C4 → TP34.2** (Gitea #73) + `coolify.bf` | | **D** | Tunnel-Inventar + GC + generische Namen, TP30 #10, Leak-Ursachen | offen | | **E** | nas/pve/macmini — **neue IPs zuerst am Core** (Sekundär-IP + 1:1-NAT), dann messen und umstellen; nas zuletzt | offen | | **F** | CF-/Tunnel-SSoT + Recovery → **TP46** | offen | ##### Schicht E — Mechanik (Christoph-Entscheid 2026-09-21, ersetzt „Doppeladresse am Host zuerst") **Neue IPs zuerst am Core, nicht am Gerät:** Sekundär-IPs `.110` (nas) · `.121` (pve) · `.130` (macmini) auf der Core-`vlan20` **plus 1:1-NAT** auf das Gerät (`dst-nat` + `src-nat`, platziert **vor** der `no-NAT inter-VLAN`-Regel). 🔴 **Routing allein reicht nicht** — im selben Subnetz antwortet das Gerät dem Client direkt per L2, der Client verwirft eine Antwort von `.10` auf eine Verbindung zu `.110`; der Rückweg braucht `src-nat`, und die Adresse muss **am Core liegen**, sonst beantwortet er kein ARP dafür (**nicht** `proxy-arp` — das antwortet für *jede* freie Adresse im /24). Heute ist die Core-NAT-Kette leer (nur `no-NAT inter-VLAN` + WAN-Masquerade) ⇒ Neubau. **Ab dann gemessen:** zwei Zähler je Host — auf der **neuen** IP (NAT-Regel: „wer ist schon umgestellt") und auf der **Alt-IP** (`action=passthrough`, `comment="dualaddr-watch: …"`: „ist noch was los"), beide ohne `log`. **Erst wenn der Alt-IP-Zähler über N Tage stillsteht**, wird das Gerät **zum Siedeln vorgemerkt**: neue IP aufs Gerät → NAT + Sekundär-IP + Zähler raus → Alt-IP entfernen → Doku/Nautobot nachziehen. Alles **ins Soll**, nicht als Transaktion (Christoph: *„wir haben einen Ist- und einen Sollzustand; die Regel kann uns nicht vorschreiben, wie lange wir für die Überführung brauchen"*). **Reihenfolge:** `nas` siedelt **zuletzt und gemeinsam** (die meisten Abhängigkeiten: SMB, Time Machine, Caddy-Backend `ds-bf`), `pve`/`macmini` vorher; **`pve2` nur reservieren** (`.120`). **Vorabcheck ✅ 2026-09-21:** NAS-NFS (`/etc/exports`, 9 Shares) und iSCSI (`.10`/`.11`:3260) sind IP-ACL'd, haben aber **keine lebenden Nutzer** — pve hat **null** NAS-Mounts (einzige Remote-Storage ist `pbs 10.40.20.135`), kein LXC hängt am NAS, die Synology-Firewall hat keine Regeln ⇒ der src-nat-Umweg (gleich-subnetzige Clients sehen den Core als Quelle) **bricht nichts**. 🔴 **Eigener Aufräumposten:** dieselben Exports stehen komplett auf totem **`10.40.41.x`** plus drei `10.0.0.0/8`-Freigaben (offen für jeden 10/8-Client). ⚠️ Der NAS trägt **zwei** VLAN20-Adressen (`.10` `ovs_eth0`, `.11` `ovs_eth1`) — laut Christoph haben die beiden nichts miteinander zu tun; `.11` gehört **nicht** in die Umzugslogik. 🔴 **Warum Nautobot zuerst.** AGH will `.30`/`.31` — dort saß Nautobot (`.30`/`.31`/`.32`/`.34`). **28 Skript-/Config-Dateien** referenzierten `10.40.20.30` (alle `gc-*.py`, 14× `ingest-*.py`, `tp44-*`, `routeros/fetch-core-cutover-config.py`) — alle am 2026-09-21 mitgezogen. Und: **der GC-Pfad läuft über Nautobot** — AGH zuerst zu ziehen hieße, die Werkzeugkette gegen sich selbst laufen zu lassen. ##### ✅ Nautobot-Umzug durch (2026-09-21) — `.30/.31/.32/.34` → `.154/.155/.156/.158` `.157` ist gitea, deshalb nimmt redis `.158`. Reihenfolge und Belege: 1. **Core-Adressliste `nautobot` additiv erweitert** (`.154/.155/.156` **dazu**, Alt-Einträge bleiben). Das ist die wirksame Ebene: der Mgmt-Zugang des Cores läuft über `src-address-list=nautobot` (Regel `*28`, 113 Pakete), **nicht** über den Literal `*24`. 2. **Nautobot-Datenmigration** über das neue [`nautobot/tp45-ip-move.py`](../nautobot/tp45-ip-move.py). Nautobot **verbietet** das Ändern einer IP nach dem Anlegen (`Host address cannot be changed once created`) → Umzug ist Dreischritt *anlegen → `primary_ip4` umhängen → Altobjekt löschen*. 3. **Stack-YML + Redeploy** (`PUT /api/stacks/203?endpointId=14`), Container auf den neuen IPs, `http://10.40.20.154:8080/health/` = **200**, alle vier Alt-IPs **000** (keine Doppeladresse — so entschieden). 4. **Caddy** `nautobot.bf` → `.154`, deployt und **verifiziert**. 5. **Core-Literal `*24` auf `.154`** über GC (`gc-manual-plan.py --deploy`, Plan `51a90e25`, Completed) mit Wirksamkeitsprüfung am Gerät. 6. **`python3 nautobot/gc-configplan.py --refresh-compliance` → 140/140, 0 Drifts.** ##### 🔴 Zwei Werkzeug-Fehler, die genau dabei Zeit gekostet haben (beide neu) **(a) `pct push` meldet Exit 0, auch wenn es die Quelle nicht lesen kann.** Gemessen: `pct push 130 /etc/caddy/Caddyfile` → `failed to open … for reading`, **`rc=0`**. Folge in `caddy/deploy.sh`: die Push-Zeile war als `push || push`-Fallback gebaut (wegen einer `$$`-Escaping-Verwechslung); der Fallback griff **nie**, weil der kaputte Push „Erfolg" meldete. Die anschließende `caddy validate` prüfte damit die **alte** Datei und war grün — **grüne Abnahme bei unverändertem Zustand**. Symptom war `nautobot.bf` **502** bei gleichzeitig gesundem Backend. **Fix:** `push_verified()` in `caddy/deploy.sh` — Push **und** md5-Vergleich in *einem* Remote-Aufruf, harter Abbruch bei Abweichung. Merkregel: **ein Push-Werkzeug, dessen Exit-Code nicht stimmt, wird geprüft, nicht geglaubt** — dieselbe Klasse wie `nc -z` (09-01) und das `sed`-Muster (08-31). **(b) 🔴 Die Firewall-Adressliste `nautobot` wird von Golden Config NIRGENDS gerendert.** Der Core-Zugang für Nautobot hängt an `src-address-list=nautobot` — die Liste stammt aber aus dem **von Hand gelaufenen** `routeros/tp5-etappe2-whitelists.rsc` und existiert im Template nicht (`grep` auf `address-list` findet nur Kommentare). **Ein Golden-Config-Recovery von null erzeugt damit einen Core, in den Nautobot nicht mehr per SSH kommt** — die Deploy-Pipeline wäre tot und würde sich selbst nicht reparieren können. Gleiche Klasse wie die fehlenden DHCP-Reservierungen. Der Renderer gehört in dieselbe Baustelle wie Punkt „DHCP-Reservierungen" oben. **(c) Nebenfund — Regel `*24`/`*28` sind Dubletten.** Dieselbe Erlaubnis existiert zweimal: `src-address=10.40.20.30` (Literal, **0 Pakete** = tot) und `src-address-list=nautobot`. Nur die Liste ist wartbar. Der Literal wurde auf `.154` gezogen (Soll-Konsistenz), die **Zusammenlegung** bleibt als Aufräumposten offen. **(d) Nebenfund — `gc-manual-plan.py` Diagnosepfad ist tot.** Die Log-Abfrage `/api/extras/job-log-entries/?job_result=…` liefert **HTTP 404** (Nautobot 3.x hat den Endpunkt verschoben). Bei einem **erfolgreichen** Deploy ist das kosmetisch, bei einem **gescheiterten** nimmt es die Fehlermeldung weg — genau dann, wenn man sie braucht. #### ✅ Schicht C durch (2026-09-22) — FQDN-Wiederherstellung, vorgezogen (C1–C3, Commit `6d1d56e`) Auslöser: die IP-Umstellung (A/B/E) dauert länger; die internen `*.bf.emsenhuber.net`-Namen mussten **sofort** zurück. Drei Züge, jeder am **Gerät** *und* am **Client** belegt — nicht am Plan-Status. **C1 — 17 Namen in den Core** (`nautobot/mikrotik_routeros.j2`, Deploy über den neuen GC-Job `tp45-c-dns`). Der Core hält jetzt **27** `/ip dns static` (vorher 10): die 9 Caddy-Frontends (`nautobot` `pbs` `portainer` `ccu` `iobroker` `z2m25` `ha-mcp` `n8n-mcp` `gitea-mcp`) → `.40` · `tv.bf`→`10.40.40.11` · `mfc.bf`→`10.40.10.6` · **`ap3.bf`→`.99.13`** (existierte in *keiner* Quelle) · `pve.bf`/`pve-mcp.bf` von Host-IP auf `.40` · dazu die **5 bare Namen** (`vw` `hass` `n8n` `gitea` `ds-bf`) → `.40` (Entscheid 09-22: lokaler Split-Horizon ist **gewollt**, weil `/admin` nur header-frei geht). 🔑 Der Job liest die Zeilen **aus dem Template selbst** und macht daraus einen **idempotenten Upsert** (`:if ([:len [/ip dns static find where name=X]] = 0) do={ … }`) — `add` allein ist nicht wiederholbar. Abnahme: Geräte-Read **27 Statics** · `--refresh-compliance` → **145/145, 0 Drifts**. **C2 — AGH entdoppelt** (`adguard/AdGuardHome.yaml`, Weg `python3 adguard/agh-sync.py push`). `upstream_dns` = **nur** `10.40.20.1`; CF-DoH-Zeile und conditional `[/bf.emsenhuber.net/]`-Forward entfallen; **alle 26 Rewrites raus**. Der Core ist damit die **einzige** Quelle — auch für die bare Namen, die vorher über DoH **öffentlich** aufgelöst wurden und den Client an den CF-Edge schickten (dort setzt der Tunnel `CF-Connecting-IP` → `@cf_admin`-Guard → 404). Vorher/Nachher aller 26 Rewrites geprüft: **verhaltensneutral**, identisches Ziel, eine Quelle statt zwei. Abnahme: `agh-sync.py check` → `.36 == .37` · `/admin` **header-frei 200**, mit `CF-Connecting-IP` weiter **404** · Filter intakt (`doubleclick` → `0.0.0.0`) · öffentliche Auflösung intakt. **C3 — `ds-bf` über Caddy** (CF-Write am Tunnel `bf-public` `48d3b0b6`): Ingress-Origin von `http://10.40.20.10:8080` auf `https://10.40.20.40:443` + `originRequest.originServerName`. Öffentlich **200 in 0,24 s** (vorher `000` nach 20 s); `vw`/`hass`/`n8n`/`gitea` unverändert. ⚠️ Der **CF-MCP darf lesen, aber nicht schreiben** (`1001: Not authorized` auf `PUT …/configurations`) — der Write lief über `CF_API_TOKEN` aus `mcp/cloudflare/.env` (Wert nie ausgegeben); Original-Ingress vorher gesichert, nachher gegengeprüft. **WARP-Abnahme (Christophs Bedingung) ✅.** Unter verbundenem WARP löst `127.0.2.2/.3` den Namen auf **`.40`** auf, die Route geht über `utun8` (Mesh), `/admin` = **200**. Der zuvor gemeldete 404 des Users kam aus dem **Browser-/mDNSResponder-Cache** (dort lag noch die CF-Anycast-A); nach TTL-Ablauf sauber. **Hebel (a) (Zero-Trust Local Domain Fallback) war nicht nötig.** 🔴 **Befund, bewusst offen — AAAA bleibt öffentlich.** Der Core hält nur **A**-Statics und ist kein Zonen-Eigentümer: eine AAAA-Abfrage **reicht er weiter** und liefert für die bare Namen die **CF-IPv6** (`2a06:98c1::`). **Heute wirkungslos** — im Netz läuft kein globales IPv6 (der Mac hat keine globale Adresse; `curl -6` landet über `::ffff:10.40.20.40`). **Latent:** sobald IPv6 kommt, geht der `/admin`-Weg wieder über die Edge und die Tür ist zu. Christoph-Entscheid 2026-09-22: **dokumentieren, nicht ändern** — `aaaa_disabled: true` in AGH wäre die Ein-Zeilen-Abhilfe, ist aber netzweit „IPv4-only" und passt nicht zu „AGH filtert nur". 🔴 **Zweiter Befund — AGH ist doppelt adressiert.** LXC 110/111 tragen im Container **zwei** VLAN20-Adressen: `eth0` `.36`/`.37` (aus `net0`) **plus** `eth1` `.30`/`.31`. Schicht A ist dort **halb** umgestellt — die neue IP ist live und wird per DHCP verteilt (`dns-server=10.40.20.30,10.40.20.31`), die Alt-IP lebt noch. Gehört in die „siedeln"-Messung, nicht in C. ℹ️ **Korrektur einer eigenen Fehlannahme:** `adguard/agh-sync.py` nennt `.36`/`.37` — das ist **kein** Drift, sondern die `net0`-Adresse; nur die Konstante kennt die zweite Adresse nicht. 🧪 **Messfalle, die fast ein Fehlalarm wurde:** `dig nautobot.bf` **ohne** `.emsenhuber.net` fragt den Namen `nautobot.bf.` im **Root** ab und liefert leer — sieht wie ein nicht gefixter Name aus, war aber die eigene Abfrage. Dieselbe Klasse wie `nc -z` (09-01) und `pct push` (09-21): **erst das eigene Werkzeug verdächtigen.** **Offen in C:** `coolify.bf` (nicht ausgerollt, Port unbekannt — **nicht raten**) · **C4 = die internen Namen als Daten** → eigener Träger **[TP34.2](#tp342)** (Gitea #73, angelegt 2026-09-22, gemeinsame Arbeit). 🔴 **Was C4 genau ist:** die **27** `A`-Statics auf `crs326-core` sind heute Handarbeit (live geprüft 2026-09-22: 27 Einträge, Freitext-Kommentare, `coolify.bf` fehlt) und werden **von GC nirgends gerendert** ⇒ ein Recovery von null erzeugt einen Core **ohne** interne Namensauflösung — dieselbe **Recovery-Lücke** wie die DHCP-Reservierungen und die `nautobot`-Adressliste. **Nicht** C4 ist Caddyfile-Rendering: das war TP34 und bleibt ❌ verworfen (09-08). Kosmetisch zurückgestellt: die `ds-bf`-Site steht im `caddy/Caddyfile` weiter im INTERN-Block (nur Beschriftung; ein Caddy-Redeploy allein dafür wäre unverhältnismäßig). #### Soll-Oktette (Nachschlagetabelle, s. TP32 für die Herleitung) | Oktett | Rolle | Soll | |---|---|---| | `.20–.29` | Core / netznah | mcast-relay | | `.30–.39` | DNS | agh-bf1 `.30` · agh-bf2 `.31` | | `.50–.59` | DBs | platform-pg `.50` · mariadb `.51` · influxdb2 `.52` · redis `.53` | | `.60–.69` | Security | vaultwarden `.60` | | `.80–.89` | Platform | docker `.80` · coolify `.81` | | `.90–.99` | Broker/Funk | mqtt5 `.90` · z2m25 `.91` | | `.150–.199` | App | haos `.150` · iob-master `.151` · CCU `.152` · nautobot `.154–.156`+`.158` · gitea `.157` · iob_slave `.159` | 🔴 **DHCP-Reservierungen sind die Lücke:** Golden Config rendert sie **nirgends** (pools/DNS-statics/ NAT/mangle/WG sind Template-Literale, der Firewall-Filter kommt als ConfigContext-**Daten** — aber `/ip dhcp-server lease` existiert im Template nicht). Die 26 VLAN30- + 1 VLAN20-Reservierung sind **Handarbeit auf dem Gerät** aus der ap1-Ära. Christoph-Entscheid 2026-09-21: **Renderer bauen** (Template + Nautobot-Datenmodell), statt Handarbeit zu dokumentieren. #### Live-Befunde, die den ursprünglichen Plan widerlegten (2026-09-21) - `mcast-relay` liegt **nicht in VLAN20** (Beine `10.40.10.21` + `10.40.40.21`) → der Anker der Zeile `.20–.29` war falsch gesetzt. - `nautobot` sind **vier** Gäste (`.30`–`.32`, `.34`), der Plan sah **drei** Slots vor. - Die VLAN30-Soll-Spalte war **durchgehend erfunden**: Ender3V3KE liegt live auf `.150` (Plan `.180`), die 26 Shelly-Reservierungen auf `.202–.226`+`.233` (Plan `.150–.169`), Twinkly `.228`, Lichterketten `.236`/`.237`, Withings `.230`/`.231` … → Christoph: **„VLAN30 nur Pool-Grenze"**. - Pool-Fund: `pool-v20` `.100–.200` hat **`used=0`** (gratis verschiebbar); `pool-v30` `.100–.250` enthält **~25 Statics** → Pool und Statics tauschen praktisch die Plätze. - ⚠️ **`ap3.bf` existierte nirgends** — weder Core-Static noch AGH-Rewrite, obwohl ap3 seit 08-30 live ist. ✅ **Behoben in C1 (09-22):** `ap3.bf` → `.99.13` als Core-Static. - **Abh.:** TP32 (Systematik) · TP37 ✅ (Nautobot-Datenmodell) · TP21 ✅. **Blockiert:** TP34.2 (rendert die FQDNs, die hier Daten werden). **Abgrenzung:** VMID-Umnummerierung bleibt draußen; Token-**Rotation** läuft in TP30; Tunnel-GC-Namensgebung hier, die **SSoT/Recovery** in TP46. ### TP48 · Gitea #75 — Recovery-Lücke: Nicht-GC-Objekte + Evaluation Ansible als Nautobot-Ablösung > **Anlass:** beim Abschluss von **TP10** (2026-10-05) fiel auf, dass der DDNS-Updater als > `/system script` + DHCP-Trigger auf `crs326-core` lebt und in **keinem** Template steht — ein > Recovery von null hätte ihn nicht. Das ist eine **Klasse**, kein Einzelfund. Christophs Frage > daraus: *„wäre ansible in der lage, nautobot vollständig als ssot abzulösen. 1 tool für > doku/deployment und nicht mehrere … keine indiv-anpassungen falls einfach vermeidbar."* #### Teil 1 — Die Lücke (live geprüft 2026-10-05, `crs326-core`) | Objekt | In einem Template? | |---|---| | `tp10-ddns-bf4`, DHCP-Client `client1` `script=` | ❌ | | `/system script environment cfDdnsToken` (**Secret**) | ❌ und darf nie (Secret-Gebot) | | 8 Alt-Skripte aus TP21.1 (`tp21-dhcp-restore`, `tp21-wan-wg-restore`, …) | ❌ | | `/system scheduler` | — (keiner vorhanden) | 🔴 **Nebenbefund:** `tp21-wan-wg-restore` trägt den **WG-Hub-Privkey im Klartext** im Skript-Körper. Das Skript liegt **nicht im Git** (nur auf dem Gerät), es ist also ein undokumentierter Recovery-Pfad mit einem Secret darin. **Aufnehmen, nicht sofort anfassen** (Christoph 2026-10-05). **Warum unsichtbar:** GC prüft **29 ComplianceFeatures** — **keines** deckt `system script`, `system scheduler` oder `script environment`. Die Compliance-Zahl prüft nur, **was gerendert wird**; was nicht gerendert wird, ist per Definition nicht „drift". #### Teil 2 — Die Evaluation 🔴 **Der Befund, der die Fragestellung verschiebt:** Nautobot ist **heute schon nur teilweise SSoT.** Live gezählt im Template: **datengetrieben** sind nur Device-Felder, `interfaces.all()`, `ip_addresses.all()` und zwei ConfigContexts (`dhcp_leases`, `firewall_filter`). **Literal im Jinja** stehen: 7 VLAN-Definitionen, 37 `bridge-port`- und 19 `bridge-vlan`-Zeilen, die Core-Firewall, NAT/Mangle/Queue/Routen. Der Ablöse-Vergleich lautet also nicht „Ansible vs. Nautobot-SSoT", sondern „Ansible vs. **Jinja-im-Repo + Gitea + ein paar Nautobot-Felder**". | Rolle | Träger heute | Ansible-Äquivalent | |---|---|---| | TP-Index/Status | **Gitea** (seit TP36 — nicht Nautobot) | ❌ bleibt Gitea | | Live-Gerätedaten (Compliance) | Nautobot DCIM + GC | ⚠️ Facts + `--check` | | Soll-Config | GC-Templates (Jinja) | ✅ Templates/Inventar | | Ist-Config | GC-Backup | ⚠️ `netcommon` backup | | Doku/Inventar (133 Interfaces, 38 IPs, 54 Leases) | Nautobot DB | ❓ offen — Ansible hat keine DB | **Offene Punkte:** (1) was leistet Nautobot *unersetzlich* (Compliance-Historie, ConfigPlan-Audit, GitRepository-Sync, Secret-Maskierung)? (2) Kann Ansible Inventar+Soll+Drift aus *einer* Git-Quelle bedienen? (3) Migrationskosten: 29 Features + 7 `config_replace`-Regeln nachbauen? (4) **Wo bleibt Doku**, wenn Ansible nur deployt? (5) Werden die heutigen Sonderfälle (Literal-Blöcke wegen bewusster Schiefstände wie `ether2` in VLAN1) mit Ansible sauberer oder chaotischer? **Abgrenzung:** nicht TP21/TP22 (bauen GC auf), nicht TP46 (CF-SSoT). Berührt die Governance-Regel „Config nur über Nautobot" — bei Trägerwechsel neu zu fassen. Verwandt: TP30 (der WG-Privkey ist ein Rotations-Kandidat). **Ergebnis = Empfehlung mit Zahlen, keine Migration.** --- ## Abgeschlossene Teilprojekte ▸ [`teilprojekte-archiv.md`](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](teilprojekte-archiv.md#tp1) ALLNET-Core *(Gerät ausgebaut)* · [TP2](teilprojekte-archiv.md#tp2)/[TP2.1](teilprojekte-archiv.md#tp2-1) Omada *(ausgebaut)* · [TP15](teilprojekte-archiv.md#tp15) HW-Refresh CRS326 · [TP21.1](teilprojekte-archiv.md#tp21-1) Core-Cutover-Recovery · [TP4](teilprojekte-archiv.md#tp4) Router-Cutover *(Rolle seit 08-08 auf dem Core)* | | **WLAN** | [TP3](teilprojekte-archiv.md#tp3) *(historisch)* · [TP3.1](teilprojekte-archiv.md#tp3-1) · [TP3.2](teilprojekte-archiv.md#tp3-2) · [TP3.3](teilprojekte-archiv.md#tp3-3) *(in TP25 aufgegangen)* · [TP28](teilprojekte-archiv.md#tp28) CAPsMAN auf dem Core · [TP25-Langfassung](teilprojekte-archiv.md#tp25-lang) | | **Firewall / QoS** | [TP5](teilprojekte-archiv.md#tp5) deny-by-default · [TP26](teilprojekte-archiv.md#tp26) `ip-firewall-filter` unter Drift-Kontrolle · [TP6](teilprojekte-archiv.md#tp6) CAKE *(geschlossen statt gebaut)* · [TP6.1](teilprojekte-archiv.md#tp6-1) Upload-Priorisierung | | **Discovery / Media** | [TP7](teilprojekte-archiv.md#tp7) mDNS · [TP7.1](teilprojekte-archiv.md#tp7-1) SSDP-Relay · [TP7.2](teilprojekte-archiv.md#tp7-2) HA↔Sonos | | **Hosts / Migration** | [TP14](teilprojekte-archiv.md#tp14) (+[.1](teilprojekte-archiv.md#tp14-1)/[.2](teilprojekte-archiv.md#tp14-2)/[.2a](teilprojekte-archiv.md#tp14-2a)/[.3](teilprojekte-archiv.md#tp14-3)/[.5](teilprojekte-archiv.md#tp14-5)) VLAN20-Cutover · [TP17](teilprojekte-archiv.md#tp17) Shelly→VLAN30 · [TP19](teilprojekte-archiv.md#tp19) VLAN1-Retirement | | **Control-Plane** | [TP20](teilprojekte-archiv.md#tp20) Platform-Postgres · [TP21](teilprojekte-archiv.md#tp21) Nautobot GC · [TP22](teilprojekte-archiv.md#tp22) Adoption + Compliance-Push · [TP27](teilprojekte-archiv.md#tp27) RouterBOOT 7.24 · [TP-R](teilprojekte-archiv.md#tp-r) Capabilities-Sweep · [TP44](teilprojekte-archiv.md#tp44) CF-Estate Doku-SSoT | | **Zugang** | [TP10](teilprojekte-archiv.md#tp10-ddns) Multi-Site-VPN *(✅ abgeschlossen 2026-10-05 — DDNS-Updater live + Mesh-Quell-NAT)* · [TP16.1](teilprojekte-archiv.md#tp16-1) natives CF-Mesh | | **Verworfen** | [TP9](teilprojekte-archiv.md#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`](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](teilprojekte-archiv.md#tp6-1)) 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) - **🔴 TP45 — VLAN20/VLAN30 ins TP32-Schema + DNS/FQDN (NEU 2026-09-21, Gitea #71):** der aktuell **dringendste** Posten — die internen `*.bf.emsenhuber.net`-Namen lösen im Browser nicht auf (Ursache: AGH-Rewrites im Filter-Pfad für unfiltrierte Clients übersprungen), und `ds-bf` ist öffentlich kaputt (einziger Ingress-Eintrag, der Caddy umgeht). Danach ist **TP34.2** erst baubar. - **TP46 — CF-/Tunnel-SSoT + Recovery (NEU 2026-09-21, Gitea #72):** Push-Weg + Ingress-Origins + DNS + Access-Policies nach Nautobot, Recovery-Runbook. Eigene Baustelle, unabhängig von TP45. - **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](#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`](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](teilprojekte-archiv.md#tp16-1)). 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](teilprojekte-archiv.md)** — 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).
### TP43 · Gitea #32 — dw-Standort: interne BF-DNS-Namen über WG auflösbar machen — dw-FritzBox: Upstream `.36`+`.37`, DNS-Rebind-Ausnahme `bf.emsenhuber.net`, Fallback-Haken „öffentliche NS bei Unreachbarkeit" **gesetzt** (Ideal-Variante: `.37`-Redundanz bleibt, BF-down → public). (verworfene 420-Historie am Ende dieses Abschnitts) **Live-Auslöser (2026-09-11):** Christoph am dw-Standort, WG-Tunnel zu BF up, kann `portainer.bf.emsenhuber.net` nicht erreichen. Direkte IP `https://10.40.20.126:9443` geht ⇒ **reines DNS-, kein Routing-Problem.** Der dw-Client nutzt die FritzBox (`10.95.10.1`) als Resolver; die kennt die interne Zone `bf.emsenhuber.net` (Split-Horizon `.36/.37 → 10.40.20.40` Caddy) nicht. **Korrektur gegenüber dem verworfenen Scope:** Der ursprüngliche Fall (`420.teilzeitladen.xyz`) ist ein **öffentlicher** CF-Worker und war nie das Problem — der ist via TP41/WARP + CF Access gelöst. Das echte, offene Problem sind **interne** BF-Namen (`*.bf.emsenhuber.net`) über den WG-Tunnel. **Live verifiziert (2026-09-11, `dig @10.40.20.36` über den Tunnel):** `portainer.bf.emsenhuber.net → 10.40.20.40` (Caddy), `macmini.bf.emsenhuber.net → 10.40.20.167` — alle internen bf-Namen landen in `10.40.0.0/16`, über WG erreichbar. `ds-fm`/`gateway-fm.emsenhuber.net → 188.114.96.10/.97.10` = **Cloudflare-Anycast** (public), **nie `10.101.0.x`**. ⇒ **Der fm-Einwand (10.101.x intern ohne Pfad von dw) trifft für `.36/.37` nicht zu** — fm läuft über den public `fm.emsenhuber.net`-Tunnel, nicht über interne Records. (WG-Topologie TP10: dw-Peer AllowedIPs BF-seitig = `10.40.0.0/16`, dw routet **nicht** nach `10.101.0.0/24`.) **Constraint am dw:** die 24/7-Geräte `10.95.10.246/.247` sind **Cudy-APs (kein DNS-Host)**; die FritzBox kann **kein Per-Zone-Forwarding** (nur einen globalen Upstream) **und per DHCP nur EINE DNS-IP ausliefern** (⇒ „mehrere NS an die Clients" scheidet aus). Ein echter conditional forwarder (dnsmasq `server=/bf.emsenhuber.net/.36`) bräuchte einen Zusatz-Dauerläufer — abgelehnt. **Entscheidung (Christoph, 2026-09-11): FB-Upstream + Rebind-Ausnahme.** Clients bekommen per DHCP `10.95.10.1` (die FB selbst, Default); die **FB forwardet an die BF-Resolver**: *DNS-Server → „Andere DNSv4-Server"* Bevorzugt **`10.40.20.36`**, Alternativ **`10.40.20.37`** (beide BF, beide Voll-Resolver → Redundanz bleibt). Sauberer als der zwischenzeitliche DHCP-Multi-Gedanke: die FB ist der **einzige** Client-Resolver und `.36` löst **intern und public** auf → alles geht zuerst an `.36`, public wird nur als Unreachable-Fallback konsultiert, **nie „zuerst"** → kein NXDOMAIN-Leak im Normalbetrieb. 🔴 **DNS-Rebind-Schutz-Ausnahme `bf.emsenhuber.net`** (*Heimnetz → Netzwerk → Netzwerkeinstellungen → DNS-Rebind-Schutz*) ist **zwingend**: sonst filtert die FB die Privat-IP-Antworten (`10.40.x`) als Rebind-Angriff weg → bf-Namen liefern nichts, obwohl `.36` korrekt antwortet. Die Ausnahme ist ein reines Namens-Whitelist (keine IP). **BF-down-Fallback (Verzweigung am FRITZ!OS-Stand):** Christoph erinnert ein Setting „öffentliche NS verwenden, wenn die primären nicht erreichbar sind". Vorhanden → aktivieren, Upstream bleibt `.36`+`.37`, BF-down geht auf public. Fehlt → Alternativ-Slot statt `.37` auf **public `9.9.9.9`** (kostet `.37`-Redundanz); dann zusätzlich prüfen, dass die FB **strikt primär** fragt (nicht round-robbt), sonst NXDOMAIN-Leak für `bf.*` bei BF-up. - **BF-up:** FB → `.36` → interne `bf.*` (→ `.40`, dank Rebind-Ausnahme durchgereicht) **und** public. - **BF-down:** `.36`/`.37` unreachable → public-Fallback → public lebt, `bf.*` = NXDOMAIN (erwartet). **Bewusst akzeptierter Tradeoff:** bei BF-up läuft Geli's **gesamter** Haushalt-DNS über Linz (`.36`/AdGuard-Filter) — vermeidbar nur mit conditional forwarder (Zusatz-HW, abgelehnt). **On-site-Verifikationen (Christoph, am dw):** (1) Rebind-Ausnahme greift — `nslookup portainer.bf.emsenhuber.net` über die FB → stabil `10.40.20.40`; (2) BF-down-Fallback vorhanden (Setting) oder via public-Alternativ-Slot gesetzt; (3) nur falls BF+public gemischt: FB fragt strikt primär. Rückfall pro Gerät: `/etc/hosts` `10.40.20.40 ` oder WARP `fallback_domains bf.emsenhuber.net → [.36,.37]` (TP41-Präzedenz). **Sofort-Workaround (läuft unabhängig):** `https://10.40.20.126:9443` direkt bzw. `/etc/hosts`-Zeile `10.40.20.40 portainer.bf.emsenhuber.net`. **Abhängigkeiten:** WG-Route dw→`10.40.0.0/16` (steht). Grenzt an TP42 (interner Namensraum `bf.emsenhuber.net`) und TP10 (Multi-Site-VPN). **Umsetzung = eigene On-site-Session** (FritzBox-DHCP ist Christophs manueller Schritt, kein Tool-Zugriff). --- **Historie — verworfener 420-Scope (angelegt 2026-09-01, verworfen 2026-09-11):** **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."* — Diese Prämisse fiel weg: `420.teilzeitladen.xyz` ist ein public CF-Worker hinter CF Access (löst auf CF-Anycast), kein BF-Origin ⇒ dafür braucht es kein Split-Horizon; Zugang via TP41-**WARP** gelöst (Geli 2026-09-10 via Android). Der Override-Bedarf für **interne** Namen blieb dabei übersehen — deshalb reopened, s.o. ### TP49 · Gitea #76 — CF-Access: `login_method:github` im include der Terpenfinder-Policy — ✅ behoben (require statt include) *(Angelegt 2026-10-06. Fund vom 2026-07-10 (Memory `project_cf_tunnel_1033`). **Am 2026-10-06 zunächst fälschlich als Phantom geschlossen, nach einem echten Test wieder geöffnet und behoben.** Die Irrfahrt steht unten unter „Aktenhistorie" — sie ist lehrreich genug, um sie zu behalten.)* #### Befund (verifiziert, 2026-10-06) Policy `christoph` auf App `420.teilzeitladen.xyz` (Terpenfinder v3, `e6a7991f…`, **öffentlich**) trug `login_method:github` im **`include`**. `include` ist ein **OR** — die Zeile „email christoph@emsenhuber.com" daneben war eine **Alternative**, keine Bedingung. Damit kam **jeder GitHub-Account** hinein, unabhängig von seiner E-Mail. **Belegt durch echten Test (Christoph, 2026-10-06):** ein **zweiter GitHub-Account** mit fremder Adresse kam durch. Vorher war mit `testus@emsenhuber.com` über den **Google**-IdP getestet worden — und **abgelehnt**; daraus war falsch geschlossen worden, GitHub verhalte sich genauso. #### Fix (ausgeführt 2026-10-06) `login_method:github` von `include` nach **`require`** verschoben ⇒ **`email AND github`**. | | vorher | jetzt | |---|---|---| | `include` | email · **github** · onetimepin | email · onetimepin | | `require` | — | **github** | **Zurückgelesen** über **beide** Endpunkte (App-Pfad und reusable-Pfad) — identisch, Fix sitzt. Fremder GitHub-Account fällt durch (hat die Adresse nicht); Christop's GitHub-Account hat sie als primäre Adresse und kommt durch. `onetimepin` bleibt als Include — **auf GitHub-Login-Seite ist der Einmalcode allein aber nicht mehr ausreichend** (`require` verlangt beides). **Sweep über alle Apps:** `login_method` in `include`/`require`/`exclude` — **genau zwei Treffer, beide in `Terpenfinder v3`** (`christoph`: github+onetimepin, `geli`: nur onetimepin=harmlos). Der interne Wildcard und alle anderen Apps: **kein `login_method` mehr** (TP41). Der Sweep prüfte **alle drei** Regel-Arrays, nicht nur `include` — sonst wäre der Fix hinterher als neuer Treffer aufgetaucht. **Scope-Warnung geprüft:** die Policy `christoph` ist `reusable: true`. Vor dem Schreiben wurde verifiziert, dass sie **nur an diese eine App gebunden** ist ⇒ Fix bleibt lokal, kein Kollateral. #### ⚠️ Was `login_method` NICHT kann (Christophs eigentliche Absicht) Die Absicht war, die Berechtigung **an den GitHub-Username** zu koppeln. **Das gibt Access nicht her:** - **`login_method` prüft die Methode, nicht den Nutzer.** „hat sich via GitHub angemeldet" — kein Username, keine Org-Zugehörigkeit. - **Es gibt keinen GitHub-Username-Selector** im Selector-Katalog der Doku. - Der **einzige** GitHub-bezogene Identitäts-Selector ist **„Identity provider group"** — der zieht aus **SCIM-Provisionierung**. Am hiesigen GitHub-IdP ist `scim_config.enabled = false`; `GET …/identity_providers/{id}/scim/groups` → **`500 (12158)`**. Der Weg ist daher **zu**. (Zusätzlich ist der Selector für Infrastructure-Apps laut Doku auf *GitHub **organization*** begrenzt.) - **Konsequenz:** `email AND github` ist keine Notlösung, sondern die **vollständige** Umsetzung dessen, was Access hier hergibt. Eine echte Username-/Org-Kopplung bräuchte **SCIM aktiviert** (Org-basiert, kostenpflichtig) — **eigener TP, nicht Teil von TP49.** #### Gotchas (live bestätigt — alle drei neu am 2026-10-06) - 🔴 **Reusable Policies lassen sich NICHT über den App-Pfad ändern.** `PUT /accounts/{id}/access/apps/{app}/policies/{pol}` → **`12130 can not update reusable policies through this endpoint`**. Korrekt ist **`PUT /accounts/{id}/access/policies/{policy_id}`** (eigener Endpunkt für reusable Policies). Lesen geht über beide Pfade. ⚠️ Der bisherige Merksatz „Reusable Policy nur per App-PUT detachbar" beschreibt eine andere Operation (Detach vom App-`policies`-Array) — **nicht** das Ändern ihres Inhalts. - 🔴 **Der `cloudflare`-MCP ist strikt read-only.** Write-Probe 2026-10-06: no-op-PUT (byte-identischer Body) → **`1010: undefined`**. Writes nur via `CF_API_TOKEN`/curl. - 🔴 **Access-App-IDs sind 32 Hex** — die Kurzform `16aaec78` liefert `12045 invalid_application_id`. Volle ID zuerst per `GET /accounts/{id}/access/apps` holen. #### Aktenhistorie (dokumentierter Fehlschlag — nicht löschen) Die erste Korrektur dieses Abschnitts (2026-10-06, vormittags) erklärte TP49 zum **Phantom** und schloss es — auf Basis eines **Google**-Tests plus eines **Schlusses** auf GitHub. Zwei Fehler: 1. **Lese-Schluss als Befund verkauft.** Aus „`include` ist ein OR" + „Google bindet die Adresse" wurde „also bindet GitHub auch" — nie gemessen. Das stand als Tatsache in Akte und Issue. 2. **Der Gegenbeweis war keiner.** Der Google-Test zeigt nur, was der Google-IdP liefert — nicht, was GitHub liefert. Die IdP-Configs sind konfigurationsgleich; das Feld, in dem der Unterschied steckt, steht nicht in der Config. **Lehre:** bei Aussagen über fremde Infrastruktur **pro IdP einzeln messen**, nicht den ersten Test auf den zweiten IdP übertragen. Der echte Test dauerte Minuten und hätte den Umweg erspart. Details in Memory `feedback_no_guessing`. #### Abgrenzung **Nicht** TP41 (hat den internen Wildcard saniert), **nicht** TP44 (Dokumentation). ⚠️ `420.teilzeitladen.xyz` gehört zum Projekt **blaueblüten** — Änderung erfolgte mit Christophs ausdrücklicher Freigabe, das Nachbar-Repo wurde **nicht** angefasst. #### Abnahme (2026-10-06, durch echten Login — nicht nur API-Read) Christoph hat nach dem Fix **alle Auth-Pfade neu getestet**: - ✅ **Fremder GitHub-Account (2. Konto) → wird abgewiesen.** Der Fix greift real, nicht nur in der API. - ✅ **Eigener Zugang (GitHub, primäre Adresse `christoph@emsenhuber.com`) → funktioniert.** - ✅ **Geli → funktioniert** (unverändert über Einmalcode/IP; ihr `require` blieb leer). - ✅ **Alle übrigen Auth-Wege verhalten sich wie erwartet.** Damit ist `email AND github` **verhaltensbestätigt**: es trennt das eigene Konto vom fremden, ohne jemanden auszusperren. Kein Regress an den Nachbar-Policies (`geli`, `terpene-svc`) und kein Kollateral am internen Wildcard. **→ Abgeschlossen 2026-10-06: Fix gesetzt, zurückgelesen und per Login verifiziert. Gitea #76 `closed` / `status:done`.**