28 Commits

Author SHA1 Message Date
6c2d9c0ea7 docs: DNS-Override-Mechanismus bleibt definiert, Airfryer-Cloud-Integration bleibt
Praezisierung zum Eintrag von 2026-10-12. Zwei Entscheidungen festgehalten:

- Der DHCP-Override-Mechanismus (dns 1.1.1.1) und die DNS-Redirect-Variante (a)
  bleiben als Werkzeug fuer kuenftige Low-Trust-Kandidaten definiert und werden
  NICHT abgebaut. Er verliert nur fuer den Airfryer seine Rechtfertigung.
- Die Cloud-Integration des Airfryers bleibt bewusst in Betrieb.

Die vorherige Formulierung "kann abgebaut werden" war zu scharf und haette eine
Folge-Session dazu verleiten koennen, den Override wegzuraeumen.

Co-Authored-By: Claude Code (deepseek-v4-pro)
2026-10-12 17:35:46 +02:00
5c8824850f docs: Airfryer ist FUSION/Cloud-only -- UNTRUSTED-Move zurueckgestellt (TP5.1)
Airfryer "airschreier" (10.40.30.180, VLAN30) hat die Philips-FUSION-Migration
bereits vollzogen. In HA verifiziert:

- Config-Entry `philips_homeid`, Titel "Airschreier 5000 Series XXL (Cloud)"
  -- das (Cloud) ist die FUSION-Markierung der Integration
- NCP 1.1.14 / Host 0.5.6 -> matcht die dokumentierte FUSION-Liste
  (HomeID-App zeigt die Reihenfolge umgedreht an: 0.5.6/1.1.14)
- Lokaler Port 80/443 antwortet nicht; mDNS announct weiter
- Kein Weg zurueck (cloud-seitige externalDeviceId-Verankerung)

Konsequenzen fuer TP5.1:
- UNTRUSTED-Move-Kandidat zurueckgestellt: bei FUSION ist die Cloud die
  einzige Steuerungsroute, internetloses VLAN macht das Geraet unsteuerbar
- DHCP-Override dns 1.1.1.1 verliert seine OTA-Rechtfertigung
- VLAN-Nummer in der Prosa korrigiert: UNTRUSTED ist VLAN 40, nicht 50

Co-Authored-By: Claude Code (deepseek-v4-pro)
2026-10-12 17:34:06 +02:00
359ad666c4 TP49 abgenommen: fremder GitHub-Account wird abgewiesen
Verhaltensabnahme durch echten Login (nicht nur API-Read): Christophs zweiter
GitHub-Account wird abgewiesen, eigener Zugang und Geli funktionieren, uebrige
Auth-Wege wie erwartet. email AND github trennt eigenes von fremdem Konto,
ohne jemanden auszusperren.

Der Fix war bis dahin nur gesetzt und zurueckgelesen — die API sagt nichts
darueber, ob die Regel greift. Mit diesem Test ist sie verhaltensbestaetigt.

TP49 geschlossen, Gitea #76 closed / status:done, kein offener Punkt.
Der Umweg (Fehlschluss -> Schliessung -> Wiedereroeffnung -> Fix -> Abnahme)
bleibt als Aktenhistorie in allen drei Dateien stehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 08:27:17 +02:00
4d4779e400 TP49 behoben: login_method:github von include nach require
Christophs zweiter GitHub-Account kam durch — der Ursprungsfund war echt. Die
vormittägliche Schliessung als "Phantom" war ein Fehlschluss (Google-Test
ungeprueft auf GitHub uebertragen) und ist hiermit zurueckgenommen.

Fix: login_method:github aus include raus, in require -> email AND github.
Gesetzt via PUT /accounts/{id}/access/policies/{policy_id} (der App-Pfad gibt
12130 fuer reusable Policies), zurueckgelesen ueber beide Endpunkte.

Sweep ueber alle Apps: login_method nur noch in Terpenfinder v3 — christoph
(github+onetimepin) und geli (nur onetimepin, harmlos). Scope geprueft: die
reusable Policy haengt nur an dieser einen App, kein Kollateral.

Drei neue Gotchas: 12130 (reusable nur unter /access/policies/{id}, der alte
Merkatz meinte das Detach, nicht das Aendern), MCP read-only (1010), 32-Hex-IDs.

Grenze dokumentiert: die Absicht war Kopplung an den GitHub-Username — den
Selector gibt es nicht. Identity-provider-group braucht SCIM (hier disabled,
scim/groups -> 500/12158). email AND github ist das vollstaendige Moegliche.

Aktenhistorie des Umwegs bleibt in allen drei Dateien stehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 08:04:15 +02:00
4b307315f0 TP49 widerlegt und geschlossen — kein Fix, Doku korrigiert
Christoph hat die Praemisse am 2026-10-06 selbst widerlegt: testus@emsenhuber.com
wird von CF Access abgelehnt. Die E-Mail-Bindung der Policy gilt, login_method
hebelt sie nicht aus. Es gab nie eine offene Tuer.

Der Fehler war ein Fehlschluss, kein Raten: Doku-Semantik zu login_method
("welche Methode wurde benutzt") wurde so gelesen, als ersetze der Include die
danebenstehende email-Zeile. include ist ein OR — die Zeilen sind Alternativen,
nicht Bedingungen. Aus "GitHub ist eine Alternative zur E-Mail" wurde "GitHub
allein genuegt", und das stand als Tatsache in der Akte (Fund 2026-07-10).

Belegt: kein IdP traegt eine Domain-Bindung (github/google/onetimepin alle nur
client_id + redirect_url, live per GET .../identity_providers). Die Einschraenkung
kann also nur aus der Policy kommen — und sie greift nachweislich.

Grenze der Aussage: der GitHub-Pfad selbst wurde nicht gemessen, nur geschlossen.

Korrigiert: teilprojekte.md TP49 (Herkunft des Fehlers mitgenannt), cf-tunnel-
mapping.md Paragraf 10 (Ursprungsfund widerlegt, nicht geloescht), Startauftrag
als "NICHT AUSFUEHREN" markiert. Kein Schreibzugriff auf die Terpenfinder-Policy;
blauebluethen-Projektgrenze gewahrt.

Erhalten bleiben nur die Gotchas: 32-Hex-App-IDs, cloudflare-MCP ist read-only
(Write-Probe no-op-PUT -> 1010), Reusable Policy nur per App-PUT detachbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 07:37:46 +02:00
81cd7bfb7e TP49-Startauftrag: MCP-Pruefschritt an den Anfang, read-only-Grenze dokumentiert
Christoph-Direktive: zu Sessionbeginn pruefen ob der Cloudflare-MCP laeuft, sonst anhalten.

Der MCP ist als Remote-Server definiert (https://mcp.cloudflare.com/mcp, OAuth) und
laut cf-tunnel-mapping.md:321 read-only — er kann den Fix nicht ausfuehren. Der
Bestandsaufnahme-Teil laeuft ueber den MCP, die Aenderung braucht curl + CF_API_TOKEN.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:54:47 +02:00
c5086db223 TP49-Startauftrag: Vorbereitung fuer die Durchfuehrungs-Session
Live geprueft: interner Wildcard ist durch TP41 sauber (Groups statt login_method).
Offen ist genau ein Eintrag — login_method:github in Policy 'christoph' auf
420.teilzeitladen.xyz (oeffentlich, Projekt blaueblueten).

Enthaelt konkrete IDs, die vier bekannten Gotchas (32-Hex-App-IDs, Schreib-Token,
Reusable-Policy-Detach, Projektgrenze) und den Ablauf inkl. Ruecksprache-Pflicht.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:49:22 +02:00
a3f2ade4e0 TP49: Befund live korrigiert — nur 420.teilzeitladen.xyz betroffen, nicht die interne Infra
Der Fund vom 2026-07-10 beschrieb login_method-Includes am internen Wildcard
*.emsenhuber.net. Live-Nachpruefung am 2026-10-06: dieser Teil ist durch TP41 behoben —
die Policies binden jetzt an Groups (E-Mail/IP/service_token).

Offen ist genau ein Eintrag: login_method:github in Policy 'christoph' auf
420.teilzeitladen.xyz (oeffentlich, Projekt blaueblueten). Kein Zugriff auf
hass/pve/portainer — die kritische Tuer war schon zu.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:48:42 +02:00
52a8ac462c TP49 angelegt: CF-Access-Wildcard oeffnet alle internen Dienste fuer jeden GitHub/Google-User
Fund vom 2026-07-10, seither unbehoben — stand nur in der Memory project_cf_tunnel_1033,
in keiner TP-Liste. Beim Sichern der Memories wiederentdeckt.

login_method:<idp> als include matcht JEDEN via diesen IdP, nicht den Owner. Betroffen
sind alle Dienste hinter *.emsenhuber.net (hass/grafana/portainer/pve). Gitea #76.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:47:03 +02:00
9638a03e4b CLAUDE.md: Memory-Versionierung dokumentiert
Memories sind seit 2026-10-06 versioniert (Master admin/claude-memories, Arbeitsbaum
~/dev/claude-memories/, Anbindung per Symlink). Dasselbe Muster wie bei den Skills,
aus demselben Befund: reiner Home-Ordner-State, kein Backup.

Festgehalten, weil beides am 2026-10-06 belegt gerissen ist:
- name: muss dem Dateinamen entsprechen — 12 Wikilinks zeigten ins Leere, weil 8
  Dateien einen abweichenden Slug trugen. Bricht still, kein Fehler.
- MEMORY.md ist Index, kein Log — nur dieses File wird pro Session geladen. 67
  Log-Zeilen trieben den r6-Index auf 16,4 KB (mehr als alle anderen elf zusammen).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-06 06:45:13 +02:00
fb415ae52d TP47 (Gitea #74) abgeschlossen: Egress-Weg verifiziert, Prosa ins Archiv
Nachmessung vom Core aus (crs326-core via MikroMCP, 15er-Stichproben je Zustand):
Regel AN -> 15/15 200 (168,1 KiB), Regel AUS -> 14/15 403. Der Weg ist
deterministisch; der eine Ausreisser sitzt reproduzierbar am ersten Sample
nach dem Umschalten (Connection-/Cache-Nachlauf), nicht am Pfad.

Merkregel im Archiv festgehalten: nie ein Einzelsample werten, erste Anfrage
nach Routing-Umschalten verwerfen.

Template [10b] in nautobot/mikrotik_routeros.j2 deckungsgleich mit live
(Table egress-cannaleo, Route 0.0.0.0/0 -> cf-mesh, Regel 164.90.191.7/32),
Drift 0. Issue #74 geschlossen, Label status:done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 21:37:34 +02:00
f63149a769 TP48 (Gitea #75) angelegt: Recovery-Luecke + Ansible-Abloese-Evaluation
Anlass aus TP10-Abschluss: DDNS-Updater lebt als /system script auf dem Core, in keinem
Template — Recovery von null haette ihn nicht. Systematische Klasse, nicht Einzelfund.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 19:05:02 +02:00
3601a2b6cb TP10: Prosa ins Archiv verschoben, Uebersichtstabelle auf abgeschlossen
- teilprojekte.md: TP10-Volltext-Abschnitt entfernt (62 Zeilen), Tabelle 'Zugang' auf
  abgeschlossen 2026-10-05 gesetzt
- teilprojekte-archiv.md: zurueckgeholten Volltext als Fortsetzung hinter der eingefrorenen
  ap1-Historie eingefuegt, eigener Anchor tp10-ddns (Doppel-Anchor aufgeloest),
  Inhaltsverzeichnis-Zeile aktualisiert

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 18:56:19 +02:00
75bd43d1f7 TP10: DDNS-Updater live + Mesh-NAT im GC-Template konform — TP abschliessbar
Close-Bedingung 1 (DDNS-Updater bf4.emsenhuber.net):
- Global-Scope-Fix (6e2a7cf): /system script environment get statt :global cfDdnsToken
  (RouterOS 7.24 trennt CLI- und Skript-Scope)
- Live auf crs326-core: Script tp10-ddns-bf4, Trigger DHCP-Client client1 (Lease-Event),
  Token in /system script environment (53 Zeichen)
- Positiv-Test bestanden: A-Record auf 1.2.3.4 gesetzt -> Script-Lauf -> 90.146.33.177,
  Comment '2026.10.03-18:08 by crs326-core', Idempotenz-Variable CFWanIP4Cur gesetzt

Close-Bedingung 2 (Mesh-NAT-Regel im taeglichen GC-Lauf konform):
- nautobot_golden_config: ip-firewall-nat = true fuer crs326-core
- Regel in der Intended Config (Z.425), nicht nur im Backup
- Zwei taegliche AllDevicesGoldenConfig-Laeufe nach dem Fix gruen (2026-10-04, 2026-10-05)

Doku: Handoff-Datei entfernt, TP10-Prosa in teilprojekte.md auf abgeschlossen gesetzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-05 18:54:57 +02:00
315f1e79e7 TP28: WLAN/CAPsMAN abgeschlossen — ap1-ap3 platziert, Kanalplan committed
- Standort final: ap1 Sideboard · ap2 Technik UG · ap3 Technik OG, alle drei CAPs am crs326-core
- Kanalplan committed (ap1 5260/5280, ap2 5300/5320, ap3 5180), Template mikrotik_routeros.j2
- wifi-cap-Rest auf dem Core (cap enabled=yes, Manager ohne eigene Radios) auf enabled=no bereinigt
- Compliance 29/29 über 5 Geräte, 0 Drifts (145/145 im GC-Job)
- Transaktionen: TEST-ether3-trunk-ap1-sideboard-2 + TP25-kanalplan-ap1-live committed in SoT
- Prosa: Volltext nach teilprojekte-archiv.md, Pointer bleibt in teilprojekte.md
- Gitea #18: body aktualisiert + Abschluss-Kommentar

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-05 13:16:42 +02:00
6e2a7cfb47 TP10: Global-Scope-Fix für cfDdnsToken im DDNS-Updater (RouterOS 7.24 Skript-Scope vs CLI-Scope)
Problem: :global cfDdnsToken liest im Skript-Kontext 'nothing', obwohl
/system script environment cfDdnsToken gesetzt ist (53 Zeichen, live verifiziert).

Fix: /system script environment get statt :global — scope-unabhängig auf ROS 7.24.
Skript importiert als tp10-ddns-bf4, Trigger: DHCP-Client client1 (Lease-Event),
kein stündlicher Scheduler nötig (Entscheidung 2026-10-03).

Recovery-Lücke dokumentiert: GC-Template rendert kein /system script /
/system scheduler — Recovery von null hätte Updater nicht.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-03 17:38:55 +02:00
5ad71b37c0 TP10: Mesh-DDNS-Updater gebaut, Handoff für Finalisierung
Skript `tp10-ddns-bf4` auf crs326-core angelegt (MIT, Basis bajodel/mikrotik-cloudflare-dns):
- WAN-IP aus DHCP-Lease (client1/vlan100), NICHT von Echo-Diensten
- check-certificate=yes, Public-IPv4-Guard, Idempotenz, Token als globale Variable

CF_DNS_API_TOKEN (eng gescoped: Zone→DNS:Edit nur emsenhuber.net/bf4) liegt
in ~/.secrets/credentials.env und ist als cfDdnsToken auf Core gesetzt.

Offen: Global-Scope-Problem (Variable im Skript nicht sichtbar) — Handlauf 1+2,
Scheduler, Positiv-Test, TP10 #49 schließen stehen aus.
Handoff-Dokumentation in docs/handoff-20261003-tp10-ddns-finalisierung.md.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-03 17:17:33 +02:00
5bcd0b6989 TP10 reopened: Mesh-Quell-NAT dokumentiert, widerlegte Handoff-Diagnose entfernt
Der Fix aus 1fddf5d ist live und verifiziert. Dieser Commit traegt die Doku nach und
raeumt eine falsche Diagnose weg, die eine Folge-Session in die Irre gefuehrt haette.

- TP10 (#49) reopened (status:wip), Prosa-Volltext zurueck aus dem Archiv nach
  teilprojekte.md; im Archiv bleibt der Zeiger.
- Neuer Befund dokumentiert: WARP-Clients kommen via cf-mesh mit 100.96.0.0/12 an,
  ausserhalb der Spoke-AllowedIPs (10.40.0.0/16) -> FritzBox verwirft. Beweis-Tabelle
  und Abnahme-Werte im Abschnitt.
- Close-Bedingung ins Issue geschrieben: DDNS-Updater bf4.emsenhuber.net + taeglicher
  GC-Lauf konform. Der Mesh-NAT-Teil ist erledigt.
- docs/handoff-20260928-core-wghub-ping-fix.md GELOESCHT: er behauptete, die fehlende
  Interface-IP von wg-hub sei ein Defekt, und schlug preferred-source vor. Beides
  widerlegt — die fehlende IF-IP ist beabsichtigt (im Archiv dokumentiert), und der
  Core-eigene Ping ist kein Betriebsproblem. Die Erkenntnis steht jetzt in TP10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 16:02:11 +02:00
1fddf5d798 TP10: Mesh-WARP-Quelle fuer FM/DW-Spokes auf BF-IP NATen
Spoke-FritzBoxen akzeptieren am WG-Peer nur Quell-IPs aus 10.40.0.0/16; WARP-Clients kommen ueber cf-mesh mit 100.96.0.0/12 und wurden verworfen. src-nat auf wg-hub-Egress legt die Quelle in den AllowedIPs-Raum.

Template (Soll) + idempotenter Job tp10-mesh-nat (manual Plan, nie full-intended). Compliance 145/145, end-to-end verifiziert (Mac/WARP -> 10.95.10.1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-10-03 15:56:44 +02:00
08a1aaa80d SSoT: Status-Marker aus den TP-Headern entfernt, Sync-Werkzeug auf Anlege-Richtung festgelegt
Die `### TPnn`-Header trugen weiterhin Status-Anspruche (▸ ✅ FERTIG …, ▸ ⏳ CUTOVER …),
obwohl Index/Status seit dem TP36-Cutover in Gitea liegt. Das war eine dritte Statusquelle,
die den Labels nachlief — Header-Marker datierten teils Wochen vor dem Issue-Update.

- 75 Header (32 aktiv + 43 Archiv) auf `### TPnn · Gitea #N — <Titel> — <Caveat>` umgestellt.
  Caveats, Warnungen, Querverweise und Archiv-Links bleiben erhalten, nur der Status faellt weg.
  Status-Marker im Fliesstext (Messchronik, Unterpunkte) bleiben — sie beschreiben Historie.
- TP47 fehlte als Gitea-Issue komplett (nur in der Datei seit 808d3a1) -> als #74 angelegt.
- bin/gitea-tp-sync.py schrieb in die falsche Richtung: der Abgleich leitete den Zielzustand
  aus dem Datei-Marker ab (fest ✅) und haette #24 (TP34, bewusst status:dropped) auf status:done
  gezogen. Jetzt werden nur noch FEHLENDE Archiv-Issues angelegt, bestehende nie angefasst.
- CLAUDE.md: Header-Form + Werkzeug-Richtung festgehalten.
- Announcement 2026-10-03 für die naechste Session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 13:12:26 +02:00
808d3a168b TP47: Core egress via cf-mesh und WARP-Exit-Validierung 2026-09-27 16:10:17 +02:00
3ae6399b93 docs: WARP-Egress-Versuch dokumentiert — Cloudflare blockt Consumer-WARP-WG-Clients
Consumer-WARP über natives RouterOS-WG gebaut, verifiziert, zurückgebaut. Befund: Handshake rx=0, Cloudflare blockt inoffizielle WG-Clients (reserved/Client-ID + Abuse-Prevention). Weg: Dedicated Egress.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-26 09:04:18 +02:00
528f133264 docs: Plan WARP-Egress (nativer Consumer-WARP als WG auf crs326-core)
Routing-Regel statt Mangle: Client-VLANs -> Routing-Tabelle warpout,
Router-Infra (wg-hub/cf-mesh/DNS/NTP) bleibt auf Liwest. Nur Plan, nichts ausgeführt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-25 19:24:13 +02:00
27a9977115 CLAUDE.md: Skills sind jetzt versioniert (admin/claude-skills)
Die 22 globalen Skills waren reiner Home-Ordner-State - kein Git, kein
Backup. Master ist jetzt das Gitea-Repo admin/claude-skills, ~/.claude/skills
ist dessen Arbeitsbaum. Sync-Ablauf inkl. cp-Schritt nach opencode festgehalten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-25 09:02:39 +02:00
6601df5a1b TP30 #12: Konsumenten-Inventur des OR-Keys korrigiert (live geprüft)
Die 3P-Config 8f69f2f1 traegt nur den CCR-Gateway-Token, nicht den
OR-Key. Der OR-Key lebt in config.sqlite live + in der nur gespeicherten
e1219ea4. Rotation muss 8f69f2f1 unangetastet lassen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-25 08:55:54 +02:00
c0c63882e4 TP30: Posten #12 — OpenRouter-Key-Leak, Merkregel JSON-statt-Env
Der Key geriet ins Transkript, weil das `sed` auf den geratenen Namen
`apiKey` ankerte statt auf `inferenceGatewayApiKey`. Neue Unterklasse der
bekannten Familie: Quelle war JSON, nicht Env — das geteilte KEY=VALUE-
Snippet kann `"key": "…"` nicht treffen. Merkregel ergaenzt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-24 13:58:26 +02:00
230f052a01 TP30: Posten #10/#11 — zwei Transkript-Leaks der Env-Session nachgetragen
Beim Einlesen von ~/.secrets/exports.zsh griffen zwei Feld-Maskierungen nicht:
N8N_MCP_BEARER (Alternation ohne BEARER, Schluessel endet ohne "=") und
TERPENE_RO_URL (Passwort steckt im URL-Userinfo-Teil, keine "="-Ankerstelle).
Beide als neue Zeilen im Sammelbecken, gebuendelt wie die uebrigen Posten.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-22 19:46:28 +02:00
39e19e7f0f docs: Handoff — Umbau-Vorschlag für Doku-/TP-Verwaltung (Analyse-Basis)
Drei driftende Wahrheitsquellen (Gitea-Issue, teilprojekte.md, CLAUDE.md),
Standard-vs-Eigenbau, drei Vorschläge. Basis für eine separate Session,
die die Umstellung selbstständig sucht und umsetzt.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-09-22 14:22:31 +02:00
12 changed files with 1179 additions and 381 deletions

View File

@@ -17,8 +17,18 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17), Firewa
- **SSoT-Dreiteilung:** **Index/Status = Gitea-Issues** (`admin/bf-infra`, Labels `status:*`/`thema:*`) · - **SSoT-Dreiteilung:** **Index/Status = Gitea-Issues** (`admin/bf-infra`, Labels `status:*`/`thema:*`) ·
**Prosa/Detail = Repo** (`docs/teilprojekte.md` aktiv + `teilprojekte-archiv.md`, `### TPnn`-Abschnitte) · **Prosa/Detail = Repo** (`docs/teilprojekte.md` aktiv + `teilprojekte-archiv.md`, `### TPnn`-Abschnitte) ·
**Live-Gerätedaten = Nautobot** (Compliance-Zahl, RouterOS-Version …). Ein Status ändert sich **in Gitea**, **Live-Gerätedaten = Nautobot** (Compliance-Zahl, RouterOS-Version …). Ein Status ändert sich **in Gitea**,
nicht mehr in einer Repo-Datei. Sync-/Abgleich-Werkzeug fürs Archiv: `bin/gitea-tp-sync.py` (Dry-Run default, nicht mehr in einer Repo-Datei. Archiv-Backfill-Werkzeug `bin/gitea-tp-sync.py` (Dry-Run default,
Token aus `~/.hermes/.env`, **nie ausgeben**). Token aus `~/.hermes/.env`, **nie ausgeben**).
- 🔴 **Die TP-Header in beiden Dateien tragen KEINEN Status mehr** (seit 2026-10-03). Form:
`### TP25 · Gitea #17 — <Titel> — <Caveat>`. Der `· Gitea #N`-Zeiger ist die Brücke zum Issue.
Wer einen Status sucht, liest das **Label im Board** — nicht den Datei-Kopf. Der alte `▸ ✅ FERTIG …`-Anspruch
war eine dritte Statusquelle und ist entfernt; die Header-Marker waren teils älter als die Issues.
⚠️ **Status-Marker im Fließtext** (in `└─`-Zeilen, Unterpunkten, Messchronik) bleiben erlaubt — sie
beschreiben historische Ereignisse, keinen aktuellen Stand.
- 🔴 **`bin/gitea-tp-sync.py` schreibt nur noch NEUE Issues** für Archiv-TPs, die im Board fehlen.
Existierende Issues werden **nie** angefasst — kein Label-, kein State-Patch. Die Vorgängerversion
glich bestehende Issues gegen den Datei-Marker ab und zog so #24 (`status:dropped` → `done`) und
#32 auf falsche Labels. **Richtung Datei → Gitea ist nur fürs *Anlegen* zulässig, nie für *Status*.**
- **Branch-Konvention:** Branch `tpNN-kurz` **jederzeit** mitten in der Session möglich (`git switch -c`, - **Branch-Konvention:** Branch `tpNN-kurz` **jederzeit** mitten in der Session möglich (`git switch -c`,
uncommittete Arbeit wandert mit); für Multi-Session-TPs der Normalfall. **Merge nach `main`, sobald der uncommittete Arbeit wandert mit); für Multi-Session-TPs der Normalfall. **Merge nach `main`, sobald der
Code/Config des TP steht.** Weil Steuerungs-Updates nach **Gitea** gehen (nicht in `teilprojekte.md`), Code/Config des TP steht.** Weil Steuerungs-Updates nach **Gitea** gehen (nicht in `teilprojekte.md`),
@@ -112,7 +122,9 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`,
🔴 **Ein GC-Recovery von null erzeugt einen Core, in den Nautobot nicht mehr per SSH kommt** — die 🔴 **Ein GC-Recovery von null erzeugt einen Core, in den Nautobot nicht mehr per SSH kommt** — die
Pipeline wäre tot. Gleiche Lücke wie die DHCP-Reservierungen. **Merkregel: was auf dem Gerät existiert, Pipeline wäre tot. Gleiche Lücke wie die DHCP-Reservierungen. **Merkregel: was auf dem Gerät existiert,
aber nicht im Template, ist eine Recovery-Lücke, auch wenn Compliance 140/140 grün meldet** — die Zahl aber nicht im Template, ist eine Recovery-Lücke, auch wenn Compliance 140/140 grün meldet** — die Zahl
prüft nur, was gerendert wird. Ablösung durch Komodo (native MCP) geplant. prüft nur, was gerendert wird. Systematisch erfasst in **[TP48](docs/teilprojekte.md#tp48)** (Gitea #75),
der auch prüft, ob Ansible Nautobot als SSoT ablösen kann. ⚠️ **Nicht Komodo** — es verwaltet Container,
nicht RouterOS-Config (frühere Notiz hier war eine Kategorie-Verwechslung).
--- ---
@@ -170,6 +182,11 @@ Verhaltensregeln für nachfolgende AI-Sessions — kleiner, klarer, weniger Drif
- **Tote MCP-Skripte → ins Projekt-Repo, nicht global.** Projekt-spezifische MCPs (`cannaleo`, `terpene-pg`) → `~/dev/<projekt>/mcp.json` + Skript im Projekt-Repo. Globale `~/.claude.json`/`opencode.jsonc` = nur projekt-übergreifende Tools. - **Tote MCP-Skripte → ins Projekt-Repo, nicht global.** Projekt-spezifische MCPs (`cannaleo`, `terpene-pg`) → `~/dev/<projekt>/mcp.json` + Skript im Projekt-Repo. Globale `~/.claude.json`/`opencode.jsonc` = nur projekt-übergreifende Tools.
- **opencode-Skills: Default-Pfad kopieren, NICHT symlinken** (TP35 Schicht 6). Claude-Skill `~/.claude/skills/<name>/SKILL.md` ist die Quelle; für opencode `cp` nach `~/.config/opencode/skills/<name>/SKILL.md` (kein `skills.paths`-Eintrag nötig). Symlink verletzt Christophs „opencode = eigene Pfade"-Direktive. Lehre: opencode-Verhalten nicht raten, Doku lesen. - **opencode-Skills: Default-Pfad kopieren, NICHT symlinken** (TP35 Schicht 6). Claude-Skill `~/.claude/skills/<name>/SKILL.md` ist die Quelle; für opencode `cp` nach `~/.config/opencode/skills/<name>/SKILL.md` (kein `skills.paths`-Eintrag nötig). Symlink verletzt Christophs „opencode = eigene Pfade"-Direktive. Lehre: opencode-Verhalten nicht raten, Doku lesen.
- **Skills sind IMMER global, niemals pro Projekt** (Christoph-Direktive). Nur `~/.claude/skills/` und `~/.config/opencode/skills/`. Kein Repo-Override. Wer überschreiben will, editiert die globale Datei oder schreibt einen zweiten Skill. - **Skills sind IMMER global, niemals pro Projekt** (Christoph-Direktive). Nur `~/.claude/skills/` und `~/.config/opencode/skills/`. Kein Repo-Override. Wer überschreiben will, editiert die globale Datei oder schreibt einen zweiten Skill.
- **Skills sind seit 2026-09-25 versioniert** (Befund: sie waren reiner Home-Ordner-State, kein Git, kein Backup — bei 22 Skills über mehrere Monate gewachsen). Master = Gitea **`admin/claude-skills`** (privat), `~/.claude/skills/` ist dessen **Arbeitsbaum**. **Ablauf ab jetzt: editieren → committen → pushen → `cp -R ~/.claude/skills/<name> ~/.config/opencode/skills/<name>`.** ⚠️ Der `cp`-Schritt ist der, den man vergisst — genau so entsteht stille Drift zwischen den beiden Kopien. Secret-Gebot gilt auch hier: keine Tokens/Keys in Skills, Platzhalter (`REPLACE_WITH_…`) sind okay.
- **Projekt-Memories sind seit 2026-10-06 versioniert** — dasselbe Muster wie die Skills, aus demselben Befund (reiner Home-Ordner-State). Master = Gitea **`admin/claude-memories`** (privat), Arbeitsbaum **`~/dev/claude-memories/`**. Ein Ordner je Projekt, Name = Projektpfad ohne das Präfix `-Users-christophemsenhuber-`; **führender Unterstrich = projektübergreifend** (`_dev` = `~/dev`, `_home` = `~`). Anbindung per **Symlink**, nicht Kopie: `~/.claude/projects/<kodiertes-projekt>/memory/` → `~/dev/claude-memories/<projekt>/` (eine zweite Kopie wäre genau die Drift, die das Repo verhindern soll). **Ablauf: editieren → `MEMORY.md`-Zeile ergänzen → committen → pushen.**
- 🔴 **`name:` muss dem Dateinamen entsprechen** — die `[[wikilinks]]` adressieren den Slug, nicht den Pfad. Laufen beide auseinander, brechen Verweise **still** (kein Fehler, kein Hinweis). Belegt am 2026-10-06: 12 Links zeigten ins Leere, weil 8 Dateien einen abweichenden `name:` trugen.
- 🔴 **`MEMORY.md` ist ein Index, kein Log** — nur das Index-File wird in jede Session des Projekts geladen, die Einzeldateien nicht. Statusberichte/Chronologie gehören in eine eigene, nicht geladene Datei (Muster: `bf-network-r6/CHRONIK.md`). 67 Log-Zeilen hatten den r6-Index auf 16,4 KB getrieben — mehr als alle anderen elf Indizes zusammen.
- Keine Secrets in Memories — sie werden in jede Session geladen und liegen versioniert. Platzhalter sind okay.
- **Kommentar in Code ≠ Verhalten.** `mcp/shelly/.env` trug „gitignored", es gab aber kein `.gitignore`. Behauptungen in Kommentaren per `git check-ignore -v <datei>` verifizieren, bevor man darauf vertraut. - **Kommentar in Code ≠ Verhalten.** `mcp/shelly/.env` trug „gitignored", es gab aber kein `.gitignore`. Behauptungen in Kommentaren per `git check-ignore -v <datei>` verifizieren, bevor man darauf vertraut.
- **opencode-TUI: führendes Leerzeichen vor `/command` manchmal nötig** (bekannter Bug, anomalyco/opencode#27528, „Not planned"). Workaround (führendes Leerzeichen) ist der offizielle Pfad. - **opencode-TUI: führendes Leerzeichen vor `/command` manchmal nötig** (bekannter Bug, anomalyco/opencode#27528, „Not planned"). Workaround (führendes Leerzeichen) ist der offizielle Pfad.
- **Manuell gepflegte Doku verliert gegen die Realität, wenn sie lang wird.** Wer eine Stelle ändert, muss die anderen mitlesen (Quick-Index, TP-Abschnitt, Ist-Stand). Strukturelle Antwort = TP36 (SSoT via Nautobot). - **Manuell gepflegte Doku verliert gegen die Realität, wenn sie lang wird.** Wer eine Stelle ändert, muss die anderen mitlesen (Quick-Index, TP-Abschnitt, Ist-Stand). Strukturelle Antwort = TP36 (SSoT via Nautobot).

View File

@@ -1,22 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
TP36 (reframed) — teilprojekte.md Index/Status → Gitea Issues (admin/bf-infra). TP36 (reframed) — Archiv-Backfill: fehlende Archiv-TPs als Gitea-Issues anlegen.
Idempotenter Sync (get-or-create, Match ueber "TPnn" im Titel): 🔴 RICHTUNG (seit 2026-10-03): **Gitea ist die SSoT fuer Index/Status.** Dieses Werkzeug
- Archiv-Header in docs/teilprojekte-archiv.md -> closed Issues (nur TPs, die in Gitea liest die Datei NUR, um zu pruefen, ob jedes archivierte TP ein Issue hat. Es schreibt
NICHT offen sind; ein reopentes TP mit Archiv-Zeiger bleibt offen). **ausschliesslich** NEUE Issues fuer noch nicht existierende Archiv-TPs — existierende
- Labels status:* (Marker) + thema:* (Gruppierung) werden vorab get-or-create. Issues werden NIE angefasst (kein Label-, kein State-Patch).
Warum das wichtig ist: die Vorgaenger-Version glich auch bestehende Issues ab und leitete
den Zielzustand aus dem Datei-Marker ab (fest `✅`). Damit konnte sie ein bewusst gesetztes
Label ueberschreiben — so wurde #24 (TP34, `status:dropped`) auf `status:done` und #32
(TP43) auf ein falsches `thema:` gezogen. Die Richtung Datei → Gitea fuer *Status* ist
seit dem TP36-Cutover (2026-09-07) falsch; sie bleibt hier nur fuer das *Anlegen* erhalten.
Abgrenzung:
- Aktive TPs: werden DIREKT als Gitea-Issue gepflegt (nicht hier).
- Status/Thema/State bestehender Issues: aendert nur der Mensch im Board.
- Per-TP-Prosa: bleibt in docs/teilprojekte.md / teilprojekte-archiv.md.
SSoT-Modell (seit TP36-Cutover 2026-09-07): Gitea fuehrt Index+Status — aktive TPs werden
DIREKT als Gitea-Issue angelegt/gepflegt, NICHT mehr aus einer Datei-Tabelle abgeleitet
(die Quick-Index-Tabelle wurde aus teilprojekte.md entfernt). Dieses Werkzeug haelt nur noch
die ARCHIVIERTEN (closed) TPs mit teilprojekte-archiv.md idempotent abgeglichen und fasst
offene Issues nie an. Die per-TP-Prosa bleibt in docs/teilprojekte.md / teilprojekte-archiv.md.
Token: ~/.hermes/.env (GITEA_ACCESS_TOKEN, GITEA_HOST) — wird NIE ausgegeben. Token: ~/.hermes/.env (GITEA_ACCESS_TOKEN, GITEA_HOST) — wird NIE ausgegeben.
Aufruf: Aufruf:
python3 bin/gitea-tp-sync.py # Dry-Run (default) — zeigt, was passieren wuerde python3 bin/gitea-tp-sync.py # Dry-Run (default) — zeigt, was fehlt
python3 bin/gitea-tp-sync.py --commit # legt Labels + Issues an / gleicht sie ab python3 bin/gitea-tp-sync.py --commit # legt fehlende Archiv-Issues an
""" """
import json import json
import os import os
@@ -33,16 +39,6 @@ HERMES = Path.home() / ".hermes" / ".env"
OWNER_REPO = "admin/bf-infra" OWNER_REPO = "admin/bf-infra"
COMMIT = "--commit" in sys.argv COMMIT = "--commit" in sys.argv
# Marker -> (status-label, gitea-state)
MARKER = {
"🔜": ("status:todo", "open"),
"⏳": ("status:wip", "open"),
"⏸️": ("status:paused", "open"),
"💡": ("status:idea", "open"),
"🟢": ("status:concept", "open"),
"✅": ("status:done", "closed"),
"❌": ("status:dropped", "closed"),
}
LABEL_COLORS = { LABEL_COLORS = {
"status:todo": "#ededed", "status:wip": "#fbca04", "status:paused": "#c5def5", "status:todo": "#ededed", "status:wip": "#fbca04", "status:paused": "#c5def5",
"status:idea": "#d4c5f9", "status:concept": "#bfdadc", "status:done": "#0e8a16", "status:idea": "#d4c5f9", "status:concept": "#bfdadc", "status:done": "#0e8a16",
@@ -105,23 +101,31 @@ def api(method, path, body=None):
# --------------------------------------------------------------------------- parse # --------------------------------------------------------------------------- parse
def parse_archiv(exclude_nums): def parse_archiv(exclude_nums):
"""Archiv-Header -> archivierte TPs. exclude_nums = TP-Nummern, die in Gitea OFFEN sind """Archiv-Header -> archivierte TPs, die ein Issue bekommen SOLLEN.
(reopente TPs mit Archiv-Zeiger) -> die bleiben offen, nicht als closed abgleichen."""
Header-Form seit 2026-10-03: `### TP25 · Gitea #17 — WLAN-Stabilisierung — …`
Der Status-Anspruch (frueher `▸ ✅ FERTIG …`) ist entfernt; die Datei traegt keinen
Status mehr. Der Gitea-Zeiger `· Gitea #N` ist die Bruecke, falls er schon da ist.
exclude_nums = TP-Nummern, die in Gitea OFFEN sind -> die brauchen kein neues Issue.
"""
out = [] out = []
hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\s*—\s*(.+?)\s*▸", ) hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\b\s*(?:·\s*Gitea\s*#(\d+)\s*)?—\s*(.+?)\s*$")
seen = set() seen = set()
for line in ARCHIV.read_text().splitlines(): for line in ARCHIV.read_text().splitlines():
m = hdr.match(line) m = hdr.match(line)
if not m: if not m:
continue continue
tp, title = m.groups() tp, issue_no, title = m.groups()
num = tp[2:] num = tp[2:]
if num in exclude_nums or num in seen: if num in exclude_nums or num in seen:
continue continue
seen.add(num) seen.add(num)
short = re.sub(r"\*\*|`", "", title).strip()[:80] # Caveat hinter dem zweiten ` — ` abschneiden; nur der Kurztitel gehoert ins Issue.
out.append({"tp": tp, "num": num, "marker": "✅", short = re.split(r"\s+—\s+", title)[0]
"title": short, "date": "", "src": "archiv"}) short = re.sub(r"\*\*|`", "", short).strip()[:80]
out.append({"tp": tp, "num": num, "issue": issue_no,
"title": short, "src": "archiv"})
return out return out
@@ -173,43 +177,44 @@ def main():
print(f"Geparst: {len(items)} archivierte TP (aktive TPs = SSoT in Gitea: " print(f"Geparst: {len(items)} archivierte TP (aktive TPs = SSoT in Gitea: "
f"{len(open_nums)} offen, nicht angetastet)\n") f"{len(open_nums)} offen, nicht angetastet)\n")
created = updated = 0 created = skipped = 0
for it in items: for it in items:
status_label, state = MARKER.get(it["marker"], ("status:todo", "open")) # Archiv-TPs sind abgeschlossen -> neu angelegte Issues gehen direkt auf closed.
status_label, state = "status:done", "closed"
labels = [status_label] labels = [status_label]
th = THEME.get(it["num"]) th = THEME.get(it["num"])
if th: if th:
labels.append(f"thema:{th}") labels.append(f"thema:{th}")
title = f"{it['tp']} — {it['title']}" title = f"{it['tp']} — {it['title']}"
body = (f"Steuerungs-SSoT: Index/Status hier in Gitea; **Prosa/Detail** im Repo unter " body = (f"Steuerungs-SSoT: Index/Status hier in Gitea; **Prosa/Detail** im Repo unter "
f"`docs/{'teilprojekte-archiv.md' if it['src']=='archiv' else 'teilprojekte.md'}` " f"`docs/teilprojekte-archiv.md` (Abschnitt `{it['tp']}`).\n\n"
f"(Abschnitt `{it['tp']}`).\n\n" f"- Status: ✅ ({status_label})\n"
f"- Status: {it['marker']} ({status_label})\n" f"- Angelegt aus dem Archiv-Backfill (`bin/gitea-tp-sync.py`).\n")
+ (f"- Abschluss: {it['date']}\n" if it["date"] else ""))
want_ids = [label_id[l] for l in labels if l in label_id] want_ids = [label_id[l] for l in labels if l in label_id]
cur = by_tp.get(it["tp"]) cur = by_tp.get(it["tp"])
if cur is None: if cur is not None:
print(f" [{'+' if COMMIT else 'DRY'}] NEW {title} {labels} state={state}") # 🔴 Existierendes Issue: NICHT anfassen. Weder Label noch State noch Body.
if COMMIT: # Status/Thema aendert nur der Mensch im Board (Gitea = SSoT seit TP36-Cutover).
sc, res = api("POST", f"/repos/{OWNER_REPO}/issues", skipped += 1
{"title": title, "body": body, "labels": want_ids}) continue
if state == "closed" and isinstance(res, dict) and res.get("number"):
api("PATCH", f"/repos/{OWNER_REPO}/issues/{res['number']}", {"state": "closed"}) # Fehlt das Issue, obwohl der Header schon einen Zeiger traegt? -> Zeiger ist stale.
created += 1 hint = f" ⚠ Datei-Header nennt bereits Gitea #{it['issue']} — Issue fehlt!" if it.get("issue") else ""
else: print(f" [{'+' if COMMIT else 'DRY'}] NEW {title} {labels} state={state}{hint}")
have = {l["name"] for l in cur.get("labels", [])} if COMMIT:
drift = (have != set(labels)) or (cur.get("state") != state) sc, res = api("POST", f"/repos/{OWNER_REPO}/issues",
if drift: {"title": title, "body": body, "labels": want_ids})
print(f" [{'~' if COMMIT else 'DRY'}] UPD #{cur['number']} {title} " if isinstance(res, dict) and res.get("number"):
f"{sorted(have)}->{labels} state {cur.get('state')}->{state}") api("PATCH", f"/repos/{OWNER_REPO}/issues/{res['number']}", {"state": "closed"})
if COMMIT: print(f" -> #{res['number']} angelegt (closed). Zeiger in "
api("PATCH", f"/repos/{OWNER_REPO}/issues/{cur['number']}", f"docs/teilprojekte-archiv.md nachtragen: `· Gitea #{res['number']}`")
{"labels": want_ids, "state": state}) else:
updated += 1 print(f" !! Anlegen fehlgeschlagen: {res}")
created += 1
print(f"\n{'Angelegt' if COMMIT else 'Wuerde anlegen'}: {created} · " print(f"\n{'Angelegt' if COMMIT else 'Wuerde anlegen'}: {created} · "
f"{'Abgeglichen' if COMMIT else 'Wuerde abgleichen'}: {updated}") f"unveraendert gelassen (SSoT = Gitea): {skipped}")
if not COMMIT: if not COMMIT:
print("Dry-Run — nichts geschrieben. Mit --commit ausfuehren.") print("Dry-Run — nichts geschrieben. Mit --commit ausfuehren.")

View File

@@ -13,6 +13,63 @@ zeigt die Einträge, die seit dem letzten `/news`-Lauf dazugekommen sind (Rest a
--- ---
## 2026-10-12 — Airfryer „airschreier" ist FUSION/Cloud-only — lokale Anbindung endgültig tot
**Betrifft:** TP5.1 (Airfryer-Fall, DNS-Redirect + Live-Torch), die UNTRUSTED-Einordnung des Geräts
(`10.40.30.180`, VLAN30) und jede Session, die den Airfryer als „Low-Trust-Gerät mit lokalem OTA-Problem"
behandelt.
**Aktion:** Die Begründung für den DHCP-Override `dns 1.1.1.1` auf **dieser** Lease („OTA geht nicht über
den DoH-Gateway-Pfad") ist **erledigt** — das Gerät hat die FUSION-Migration bereits vollzogen und hängt
sowieso an der Cloud. **Der Override-Mechanismus selbst bleibt bestehen** und ist für künftige Low-Trust-
Kandidaten weiter definiert (Entscheidung 2026-10-12) — er wird **nicht** abgebaut, sondern behält seinen
Wert als Werkzeug, falls wieder ein Gerät mit OTA-DoH-Latenzproblemen auftaucht. Das gilt ausdrücklich
auch für die in TP5.1 skizzierte DNS-Redirect-Variante (a).
**Aber:** Bei FUSION ist die **Cloud-Verbindung die einzige** Steuerungsroute — das Gerät darf **nicht**
ins internetlose UNTRUSTED-VLAN (40) wandern, sonst ist es komplett unsteuerbar (auch aus HA). Der
frühere UNTRUSTED-Move-Kandidat aus TP5.1 ist damit **zurückzustellen**, nicht umzusetzen.
**Steuerungsentscheidung (2026-10-12):** Die Cloud-Integration **bleibt** — der Airfryer wird bewusst
über `philips_homeid` / Cloud-Relay betrieben. Kein Rückbau, keine weitere Diskussion nötig.
**Befund (aus HA verifiziert, 2026-10-12):**
- Config-Entry `philips_homeid`, Titel **„Airschreier 5000 Series XXL (Cloud)"** — das `(Cloud)` im
Titel ist die FUSION-Markierung der Integration (Cloud-Relay statt lokal).
- Firmware-Sensoren: **NCP `1.1.14` / Host `0.5.6`** → entspricht der dokumentierten FUSION-Liste
(`1.1.14/0.5.6`; vgl. HD9285-Fälle `1.6.2/0.6.8`, `1.1.8/0.5.6`). Achtung: Die HomeID-App zeigt die
Reihenfolge **umgedreht** an (`0.5.6/1.1.14`) — nicht als SPECTRE fehldeuten.
- Modellnummer-Sensor: `HD9285` (SPECTRE wäre die lokale Variante, ist es aber nicht mehr).
- Lokaler Port 80/443 antwortet **nicht** (aus MAIN getestet); mDNS announce't aber weiter
`PHILIPS_HD9285_2_1FB281._http._tcp.local.` → Port 80. Das Gerät läuft also, hat aber keine
lokalen Credentials mehr.
- **Kein Weg zurück:** Laut Integrations-Doku ist die FUSION-Migration einwegig („There is no way back"),
weil die Zuordnung cloud-seitig über `externalDeviceId` verankert ist. Firmware-Downgrade hilft nicht,
Custom-Firmware existiert nicht (geschlossener ESP-Stack, signierter Bootloader).
- Steuerung **funktioniert** über die Cloud (Live-Werte plausibel: Kochstatus `setting`, 180 °C,
`fresh_fries`; Buttons für Start/Pause/Stopp/Warmhalten vorhanden).
- Nebenbefund: `dns 1.1.1.1` = anonymer Consumer-Resolver → **DNS-Sicht weiter blind**, Gerät umgeht
die Filterung. Redet nachweislich mit Versuni → AWS `eu-west-1` (IoT). Bleibt ein Beobachtungspunkt,
aber ohne OTA-Rechtfertigung.
---
## 2026-10-03 — TP-Header tragen keinen Status mehr (SSoT-Drift geschlossen)
**Betrifft:** alle, die `docs/teilprojekte.md` oder `teilprojekte-archiv.md` lesen oder editieren, und
alle, die ein TP anlegen/abschließen.
**Aktion:** Die `### TPnn`-Header haben die Form `### TP25 · Gitea #17 — <Titel> — <Caveat>`.
Der Status-Anspruch (`▸ ✅ FERTIG (2026-09-15)`, `▸ ⏳ CUTOVER …`) ist **entfernt** — er war eine
dritte Statusquelle neben Issue und Prosa und lief den Labels nachweislich hinterher. **Wer einen
Status braucht, liest das `status:*`-Label im Gitea-Board**, nicht den Datei-Kopf — `· Gitea #N`
ist die Brücke. Status-Marker **im Fließtext** (Messchronik, `└─`-Zeilen) bleiben: sie beschreiben
Historie, keinen Stand.
Dazu zwei Befunde, die dabei behoben wurden:
- **TP47 existierte nur in der Datei**, ohne Gitea-Issue (seit Commit `808d3a1`, 2026-09-27) — jetzt
**#74**. Das war ein echter SSoT-Bruch, kein fehlender Zeiger.
- **`bin/gitea-tp-sync.py` schrieb in die falsche Richtung**: der Abgleich leitete den Zielzustand
aus dem Datei-Marker ab (fest `✅`) und hätte #24 (TP34, bewusst `status:dropped`) auf `status:done`
gezogen. Das Werkzeug schreibt jetzt **nur noch fehlende** Archiv-Issues neu und fasst bestehende
**nie** an.
## 2026-09-12 — Golden-Config-Backups laufen jetzt täglich (vorher KEIN Scheduler) ## 2026-09-12 — Golden-Config-Backups laufen jetzt täglich (vorher KEIN Scheduler)
**Betrifft:** alle, die sich auf GC-Backups/Compliance oder `gc-snapshot.py`-Werte verlassen. **Betrifft:** alle, die sich auf GC-Backups/Compliance oder `gc-snapshot.py`-Werte verlassen.
**Aktion:** Es gab **nie** einen automatischen GC-Backup-Zeitplan — Backup/Intended/Compliance liefen **Aktion:** Es gab **nie** einen automatischen GC-Backup-Zeitplan — Backup/Intended/Compliance liefen

View File

@@ -303,6 +303,30 @@ Tokens: nur im Container-Env + jederzeit via `GET /accounts/…/cfd_tunnel/<id>/
**🔴 NEUER FUND (wichtiger als vw!) — Wildcard `*.emsenhuber.net` (`16aaec78`) zu offen:** Policy `christoph` hat `include: email:christoph OR login_method:github OR login_method:google`. **`login_method:<idp>` als include matcht JEDEN, der sich via GitHub/Google einloggt** — nicht nur christoph. D.h. **jeder GitHub-/Google-User erreicht nach Login alle internen Dienste** (hass, grafana, portainer, pve, gitea, …). Fix = die zwei `login_method`-Includes raus (nur `email:christoph` + ggf. weitere erlaubte Mails + `non_identity bf4` + `claude-mcp`). **NICHT einseitig geändert** (ändert Login-Verhalten überall) → User-Entscheidung. War im Memory schon als „Policy christoph verschlanken" geflaggt. **🔴 NEUER FUND (wichtiger als vw!) — Wildcard `*.emsenhuber.net` (`16aaec78`) zu offen:** Policy `christoph` hat `include: email:christoph OR login_method:github OR login_method:google`. **`login_method:<idp>` als include matcht JEDEN, der sich via GitHub/Google einloggt** — nicht nur christoph. D.h. **jeder GitHub-/Google-User erreicht nach Login alle internen Dienste** (hass, grafana, portainer, pve, gitea, …). Fix = die zwei `login_method`-Includes raus (nur `email:christoph` + ggf. weitere erlaubte Mails + `non_identity bf4` + `claude-mcp`). **NICHT einseitig geändert** (ändert Login-Verhalten überall) → User-Entscheidung. War im Memory schon als „Policy christoph verschlanken" geflaggt.
> ✅ **BESTÄTIGT und BEHOBEN am 2026-10-06 — der Fund oben war RICHTIG.**
> Christoph hat mit einem **zweiten GitHub-Account** (fremde Adresse) getestet: **er kam durch.**
> `login_method:github` im **`include`** ist ein **OR** — die danebenstehende `email`-Zeile war
> eine **Alternative**, keine Bedingung. Jeder GitHub-Account kam hinein.
>
> **Fix (ausgeführt):** `login_method:github` von `include` nach **`require`** verschoben
> ⇒ **`email AND github`**. Zurückgelesen über beide Endpunkte, Fix bestätigt.
> ⚠️ Nebenwirkung: der E-Mail-Einmalcode allein reicht auf GitHub-Seite **nicht mehr**.
>
> 🔴 **Zwischenzeitliche Fehlspur (dokumentiert, weil lehrreich):** eine frühere Fassung dieser
> Notiz erklärte den Fund am selben Tag für **widerlegt** — auf Basis eines **Google**-Tests
> (`testus@emsenhuber.com` → abgelehnt) plus eines **Schlusses** auf GitHub. Der Schluss war
> falsch: der Google-Test zeigt nur Googles Verhalten. Die IdP-Configs sind konfigurationsgleich
> (beide nur `client_id` + `redirect_url`) — **das unterscheidende Feld steht nicht in der Config.**
> **Lehre: pro IdP einzeln messen, nicht den ersten Test auf den zweiten übertragen.**
>
> **Bleibt gültig:** TP41 hat den internen Wildcard auf Access-Groups (`zt-humans`/`zt-home`/
> `zt-automation`) umgestellt — dort steht heute kein `login_method` mehr. Der Sweep über alle
> Apps fand `login_method` nur noch in `Terpenfinder v3` (`christoph`: github+onetimepin;
> `geli`: nur onetimepin = harmlos).
>
> Details, Gotchas (`12130` reusable-Policy-Endpunkt, MCP read-only) und die Grenze
> „kein GitHub-Username-Selector" → `docs/teilprojekte.md` → `### TP49`.
**⏸️ `hass` 400 / `ha-mcp` — zwei getrennte Layer:** **⏸️ `hass` 400 / `ha-mcp` — zwei getrennte Layer:**
- **Access-Layer (remote):** HA-Companion-App kann kein interaktives Access-Login → Fix = iPhone in **WARP** enrollen (`Warp Login App` `6c32fe23` `allow:christoph` existiert → christoph darf enrollen). Client-seitig, braucht User am Gerät. Vom Heim-Netz (bf4-IP) ohnehin ohne Login erreichbar. - **Access-Layer (remote):** HA-Companion-App kann kein interaktives Access-Login → Fix = iPhone in **WARP** enrollen (`Warp Login App` `6c32fe23` `allow:christoph` existiert → christoph darf enrollen). Client-seitig, braucht User am Gerät. Vom Heim-Netz (bf4-IP) ohnehin ohne Login erreichbar.
- **Origin-Layer:** `hass.emsenhuber.net` gibt **vom Heim-Netz** (Access passt via bf4-IP) trotzdem **400** (CF `server:cloudflare`, aber Origin-verursacht) = **HA lehnt den Request ab** — HA-`http:`-Config (`use_x_forwarded_for` + `trusted_proxies` inkl. cloudflared-Range, ggf. Host-Allowlist). **Separates HA-Thema, NICHT Tunnel/Access** — offen. - **Origin-Layer:** `hass.emsenhuber.net` gibt **vom Heim-Netz** (Access passt via bf4-IP) trotzdem **400** (CF `server:cloudflare`, aber Origin-verursacht) = **HA lehnt den Request ab** — HA-`http:`-Config (`use_x_forwarded_for` + `trusted_proxies` inkl. cloudflared-Range, ggf. Host-Allowlist). **Separates HA-Thema, NICHT Tunnel/Access** — offen.

View File

@@ -0,0 +1,45 @@
# Handoff 2026-09-22 — Doku-/TP-Verwaltung: Umbau-Vorschlag (Basis für eigene Session)
**Anlass:** Christoph will eine separate Session, die die Doku-/TP-Verwaltung strafft und die
Vorschläge selbstständig umsetzt. Diese Notiz ist die Analyse-Basis aus der Ausdünnen-Session
(2026-09-22), in der die Drift-Kosten konkret sichtbar wurden. **Vorschläge, kein Mandat.**
## Diagnose (woran es hakt)
Drei parallele Wahrheitsquellen driften gegeneinander:
1. **Gitea-Issue** (Index + Label `status:*`/`thema:*`)
2. **`docs/teilprojekte.md`** (Prosa, kuratiert, `### TPnn`-Abschnitte)
3. **CLAUDE.md** (Quick-Index/Fokus — bis zur Ausdünnen-Session ein Ist-Stand-Block)
Jede Status-/Fakten-Änderung muss an 2–3 Stellen nachgezogen werden. Die komplette
Ausdünnen-Session plus der TP25-Roll-up (507 Zeilen verschieben, Index-Zeile, zwei
„nicht abgeschlossen"-Notizen, Pointer in `teilprojekte.md`) war reine Drift-Bekämpfung —
Arbeit, die ein Gitea-Issue-Kommentar gratis ersetzt.
## Standard vs. Eigenbau (kurz)
Der Claude-Code-Standard ist schmal: **CLAUDE.md** (kurz, statisch, nur das Nicht-Offensichtliche)
+ **git** + **Auto-Memory** + ein Issue-Tracker (Gitea/GitHub/Linear). Ein „TP-System" ist **kein**
Standard — die TP-Verwaltung hier ist Eigenbau auf Gitea. Der Teil „Gitea-Issue als Index + Label
als Status" ist der richtige, standardnahe Kern.
## Drei Vorschläge (Reihenfolge = Wert)
1. **CLAUDE.md = nur statische Konventionen.** Nach der Ausdünnen-Session 28 KB und gut so.
Nie wieder „aktueller Stand"/Ist-Stand reinschreiben — der gehört nach Gitea.
2. **Status → nur Gitea, Prosa → nur eine Stelle.** Der Kuratier-Aufwand (Index-Tabellen,
„Dazu am Ende"-Notizen, Archiv-Roll-ups) kostet pro geschlossenem TP mehr als er wert ist.
Kandidat: Prosa/Detail in den Gitea-Issue-Body/Kommentar verlagern, `teilprojekte.md`
auf ein append-only Log reduzieren oder ganz durch Issues ersetzen.
3. **Mehr git, weniger kuratieren.** `handoff-*.md` pro Session ist gut. Archiv/Roll-up liefert
wenig über `git log` + geschlossenes Issue hinaus.
## Abgrenzung / Tradeoffs
- Das System hat echten Wert: auf Christoph zugeschnitten, die Drift-Warnungen haben real etwas
gefangen. **Nicht ersatzlos streichen.**
- Teuerster Teil ist die **zweite, kuratierte Prosa-Ebene** (`teilprojekte.md` + Archiv) — nicht die
Gitea-Ebene und nicht CLAUDE.md.
- Die separate Session sucht selbstständig Vorschläge UND setzt um; diese Notiz ist die Ausgangslage,
kein Plan.

View File

@@ -43,7 +43,7 @@ teuerste Eigenschaft der Datei zerstört, um ihr billigstes Problem zu lösen. B
| [TP7.1](#tp7-1) | SSDP-Relay (macOS-Sonos-App über VLAN-Grenze) | ✅ LÄUFT (2026-08-18) | | [TP7.1](#tp7-1) | SSDP-Relay (macOS-Sonos-App über VLAN-Grenze) | ✅ LÄUFT (2026-08-18) |
| [TP7.2](#tp7-2) | HA↔Sonos static-host binding + VLAN1/GUEST-Discovery | ✅ FERTIG (2026-07-03) | | [TP7.2](#tp7-2) | HA↔Sonos static-host binding + VLAN1/GUEST-Discovery | ✅ FERTIG (2026-07-03) |
| [TP9](#tp9) | Gateway-/Router-HA | ❌ OBSOLET (User-Entscheid 2026-08-08) | | [TP9](#tp9) | Gateway-/Router-HA | ❌ OBSOLET (User-Entscheid 2026-08-08) |
| [TP10](#tp10) | Multi-Site VPN | ✅ KERN DURCH (2026-06-27) | | [TP10](#tp10) | Multi-Site VPN | ✅ ABGESCHLOSSEN (2026-10-05) — Mesh-Quell-NAT + DDNS-Updater |
| [TP14](#tp14) | Host/Container-IP-Migration VLAN1→VLAN20 (Docker-Cutover NAS+pve) | ✅ DURCH (2026-07-13; live re-verifiziert 2026-08-08) | | [TP14](#tp14) | Host/Container-IP-Migration VLAN1→VLAN20 (Docker-Cutover NAS+pve) | ✅ DURCH (2026-07-13; live re-verifiziert 2026-08-08) |
| [TP14.1](#tp14-1) | Session A: NAS fertigstellen | ✅ DURCH (2026-07-10); Omada-Inform/sw2 VERTAGT | | [TP14.1](#tp14-1) | Session A: NAS fertigstellen | ✅ DURCH (2026-07-10); Omada-Inform/sw2 VERTAGT |
| [TP14.2](#tp14-2) | Session B: pve-Docker-LXC (.126) → VLAN20 | ✅ DURCH (2026-07-10) (ipvlan) | | [TP14.2](#tp14-2) | Session B: pve-Docker-LXC (.126) → VLAN20 | ✅ DURCH (2026-07-10) (ipvlan) |
@@ -61,7 +61,7 @@ teuerste Eigenschaft der Datei zerstört, um ihr billigstes Problem zu lösen. B
| [TP25](#tp25) | WLAN-Stabilisierung | ✅ FERTIG (2026-09-15) — Platzierung final (ap1 Sideboard · ap2 UG · ap3 OG), Fund E geschlossen | | [TP25](#tp25) | WLAN-Stabilisierung | ✅ FERTIG (2026-09-15) — Platzierung final (ap1 Sideboard · ap2 UG · ap3 OG), Fund E geschlossen |
| [TP26](#tp26) | `/ip firewall filter` unter Drift-Kontrolle | ✅ DURCH (2026-08-17) | | [TP26](#tp26) | `/ip firewall filter` unter Drift-Kontrolle | ✅ DURCH (2026-08-17) |
| [TP27](#tp27) | RouterBOOT-Firmware auf 7.24 nachziehen | ✅ DURCH (2026-08-17/18) | | [TP27](#tp27) | RouterBOOT-Firmware auf 7.24 nachziehen | ✅ DURCH (2026-08-17/18) |
| [TP28](#tp28) | WLAN über CAPsMAN auf `crs326-core` | ↩️ **wieder geöffnet 2026-08-30** (ap3) — Volltext in [teilprojekte.md](teilprojekte.md#tp28) | | [TP28](#tp28) | WLAN über CAPsMAN auf `crs326-core` | ✅ FERTIG (2026-10-04) — ap1 Sideboard · ap2 UG · ap3 OG, Kanalplan committed, Compliance 29/29 |
| [TP-R](#tp-r) | Router-Capabilities-Sweep | FERTIG (docs/research/routeros-capabilities.md) | | [TP-R](#tp-r) | Router-Capabilities-Sweep | FERTIG (docs/research/routeros-capabilities.md) |
| [TP39](#tp39) | LXC-Hygiene pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup | ✅ ABGESCHLOSSEN (2026-09-01) — pve+NAS-Sweep grün | | [TP39](#tp39) | LXC-Hygiene pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup | ✅ ABGESCHLOSSEN (2026-09-01) — pve+NAS-Sweep grün |
| [TP34](#tp34) | Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) | ❌ VERWORFEN (2026-09-08) — Caddyfile bleibt manuell · **Fortsetzung: TP34.2 (2026-09-22)** | | [TP34](#tp34) | Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) | ❌ VERWORFEN (2026-09-08) — Caddyfile bleibt manuell · **Fortsetzung: TP34.2 (2026-09-22)** |
@@ -70,6 +70,8 @@ teuerste Eigenschaft der Datei zerstört, um ihr billigstes Problem zu lösen. B
| [TP43](#tp43) | dw-Standort-DNS-Uplink (site-spezifische Overrides) | ♻️ REOPENED (2026-09-11) — Volltext zurück in teilprojekte.md; interne BF-Namen über WG am dw offen | | [TP43](#tp43) | dw-Standort-DNS-Uplink (site-spezifische Overrides) | ♻️ REOPENED (2026-09-11) — Volltext zurück in teilprojekte.md; interne BF-Namen über WG am dw offen |
| [TP42](#tp42) | Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net | ✅ DURCH (2026-09-02) — `.bf.lan` raus, `.16` abgebaut, Compliance 140/140 | | [TP42](#tp42) | Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net | ✅ DURCH (2026-09-02) — `.bf.lan` raus, `.16` abgebaut, Compliance 140/140 |
| [TP47](#tp47) | Core egress via cf-mesh und WARP-Exit-Validierung | ✅ ABGESCHLOSSEN (2026-10-05) — Egress-Weg verifiziert: 15/15 über cf-mesh, Heim-IP blockt |
Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)** — TP25 ist Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)** — TP25 ist
✅ abgeschlossen (2026-09-15); der aktive Stand (Kondensat) und die 394-Zeilen-Herleitung ✅ abgeschlossen (2026-09-15); der aktive Stand (Kondensat) und die 394-Zeilen-Herleitung
(Messchronik A–G) liegen beide hier. (Messchronik A–G) liegen beide hier.
@@ -77,7 +79,7 @@ Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)
--- ---
<a id="tp1"></a> <a id="tp1"></a>
### TP1 — ALLNET Core-Switch finalisieren ▸ ✅ FERTIG (2026-06-15) → ❌ Gerät AUSSER BETRIEB (2026-08-08, durch crs326-core ersetzt) ### TP1 · Gitea #34 — ALLNET Core-Switch finalisieren — → ❌ Gerät AUSSER BETRIEB (2026-08-08, durch crs326-core ersetzt)
- ✅ GE8 Trunk (native vlan1, tagged 10/20/30/40/50/90/99/100) — war bereits aktiv. - ✅ GE8 Trunk (native vlan1, tagged 10/20/30/40/50/90/99/100) — war bereits aktiv.
- ✅ VLAN 40 umbenannt: UNTRUSTED → **MEDIA** (`switch/cli/04-vlan50-media-untrusted.cli`). - ✅ VLAN 40 umbenannt: UNTRUSTED → **MEDIA** (`switch/cli/04-vlan50-media-untrusted.cli`).
- ✅ VLAN 50 angelegt + benannt: **UNTRUSTED**. - ✅ VLAN 50 angelegt + benannt: **UNTRUSTED**.
@@ -86,7 +88,7 @@ Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)
<a id="tp2"></a> <a id="tp2"></a>
### TP2 — Omada Access-Switch + Controller ▸ ✅ FERTIG (2026-06-15) ### TP2 · Gitea #35 — Omada Access-Switch + Controller
- ✅ Omada Controller 6.2.10.17 auf DS923 (macvlan `.175`): `omada/docker-compose.yml`, Stack via Portainer endpointId=14. **Wizard abgeschlossen** (configured=true), Creds in `mcp/omada/.env`. - ✅ Omada Controller 6.2.10.17 auf DS923 (macvlan `.175`): `omada/docker-compose.yml`, Stack via Portainer endpointId=14. **Wizard abgeschlossen** (configured=true), Creds in `mcp/omada/.env`.
- ✅ pve-mcp (samik081) in Claude Code eingetragen (`~/.claude/settings.json`, SSE `.173:3000`). - ✅ pve-mcp (samik081) in Claude Code eingetragen (`~/.claude/settings.json`, SSE `.173:3000`).
- ✅ omada-mcp **live** auf **pve-LXC (`.126`, endpointId=2), ipvlan-IP `.176`**, Stack Id=200, URL in `~/.claude/settings.json`. - ✅ omada-mcp **live** auf **pve-LXC (`.126`, endpointId=2), ipvlan-IP `.176`**, Stack Id=200, URL in `~/.claude/settings.json`.
@@ -122,7 +124,7 @@ Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)
<a id="tp2-1"></a> <a id="tp2-1"></a>
### TP2.1 — Omada-Switch (sw2/ES208G) → Standalone-Recovery (kein Controller) ▸ ✅ FERTIG (2026-07-16, User solo) ### TP2.1 · Gitea #36 — Omada-Switch (sw2/ES208G) → Standalone-Recovery (kein Controller) — User solo)
- **Root Cause:** beim VLAN20-Umzug des Omada-Controllers (TP14.1, 2026-07-10) ist die Controller↔sw2-Verbindung zerbrochen (sw2 seither „Disconnected", ursprünglich als reiner Mgmt-Loss vertagt). Beim Rettungsversuch hat der User versehentlich einen **Werks-Reset** am Switch ausgelöst. Ein anschließender Wiederherstellungsversuch (durch Claude) hat Teile der Config **unbemerkt halluziniert** — der damalige „wiederhergestellte" Stand war **nicht vertrauenswürdig**. - **Root Cause:** beim VLAN20-Umzug des Omada-Controllers (TP14.1, 2026-07-10) ist die Controller↔sw2-Verbindung zerbrochen (sw2 seither „Disconnected", ursprünglich als reiner Mgmt-Loss vertagt). Beim Rettungsversuch hat der User versehentlich einen **Werks-Reset** am Switch ausgelöst. Ein anschließender Wiederherstellungsversuch (durch Claude) hat Teile der Config **unbemerkt halluziniert** — der damalige „wiederhergestellte" Stand war **nicht vertrauenswürdig**.
- **Entscheidung (User 2026-07-15): Controller-Zustand als invalid betrachtet, sw2 komplett neu — diesmal OHNE Managementsoftware.** Kein Omada-Controller-Re-Adopt, kein Omada-Ecosystem-Anschluss (s. TP15-Korrektur — Omada bekommt generell keine Rolle mehr, auch nicht WLAN). sw2 direkt über eigenes Web-UI konfiguriert, wie ein eigenständiger Switch. - **Entscheidung (User 2026-07-15): Controller-Zustand als invalid betrachtet, sw2 komplett neu — diesmal OHNE Managementsoftware.** Kein Omada-Controller-Re-Adopt, kein Omada-Ecosystem-Anschluss (s. TP15-Korrektur — Omada bekommt generell keine Rolle mehr, auch nicht WLAN). sw2 direkt über eigenes Web-UI konfiguriert, wie ein eigenständiger Switch.
- **✅ 2026-07-16 (User-Aussage): „voll durch, Funktionsweise erfolgreich getestet."** Nach dem gescheiterten Claude-Versuch hat der User sw2 **komplett allein** neu aufgesetzt (Runbook `docs/tp2.1-omada-switch-standalone-runbook.md` als Playbook-Referenz, nicht Schritt-für-Schritt-Log). **Keine formale Einzelschritt-Verifikation dieser Session** (kein Port-für-Port-Protokoll, keine Live-Prüfung durch Claude) — Status beruht auf User-Bestätigung „es geht einfach", nicht auf eigener Nachkontrolle. - **✅ 2026-07-16 (User-Aussage): „voll durch, Funktionsweise erfolgreich getestet."** Nach dem gescheiterten Claude-Versuch hat der User sw2 **komplett allein** neu aufgesetzt (Runbook `docs/tp2.1-omada-switch-standalone-runbook.md` als Playbook-Referenz, nicht Schritt-für-Schritt-Log). **Keine formale Einzelschritt-Verifikation dieser Session** (kein Port-für-Port-Protokoll, keine Live-Prüfung durch Claude) — Status beruht auf User-Bestätigung „es geht einfach", nicht auf eigener Nachkontrolle.
@@ -132,7 +134,7 @@ Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)
<a id="tp3"></a> <a id="tp3"></a>
### TP3 — WLAN ▸ 📜 **VOLLSTÄNDIG HISTORISCH — der Ist-Stand steht in TP28, nicht hier** ### TP3 · Gitea #37 — WLAN
> 🔴 **Lesehinweis (überarbeitet 2026-08-19): dieser gesamte TP ist Geschichte, von der ersten bis zur > 🔴 **Lesehinweis (überarbeitet 2026-08-19): dieser gesamte TP ist Geschichte, von der ersten bis zur
> letzten Zeile.** Er beschreibt zwei überholte Zustände nacheinander — die **CAPsMAN-Ära (Juni/Juli > letzten Zeile.** Er beschreibt zwei überholte Zustände nacheinander — die **CAPsMAN-Ära (Juni/Juli
> 2026)** und danach den **Standalone-Rebuild vom 2026-08-08**. Beides gilt nicht mehr: > 2026)** und danach den **Standalone-Rebuild vom 2026-08-08**. Beides gilt nicht mehr:
@@ -170,7 +172,7 @@ Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)
<a id="tp3-1"></a> <a id="tp3-1"></a>
### TP3.1 — ap2 VLAN-Trunk-Fix ▸ ✅ Config live (2026-07-01) — `routeros/tp3.1-ap2-vlan.rsc` ### TP3.1 · Gitea #38 — ap2 VLAN-Trunk-Fix — `routeros/tp3.1-ap2-vlan.rsc`
- **Root Cause:** ap2 (CAP, local forwarding) hatte `bridge vlan-filtering=no` → `datapath.vlan-id` wurde nie - **Root Cause:** ap2 (CAP, local forwarding) hatte `bridge vlan-filtering=no` → `datapath.vlan-id` wurde nie
auf die Leitung geschrieben → **ALLE** WLAN-Clients auf ap2 (main/media/iot/…) egressten **ungetaggt in VLAN1** auf die Leitung geschrieben → **ALLE** WLAN-Clients auf ap2 (main/media/iot/…) egressten **ungetaggt in VLAN1**
(defconf/`10.40.41.x`) statt ins Ziel-VLAN. Symptom: iPhone (VLAN10) im OG „verbunden, aber sporadisch kein (defconf/`10.40.41.x`) statt ins Ziel-VLAN. Symptom: iPhone (VLAN10) im OG „verbunden, aber sporadisch kein
@@ -208,7 +210,7 @@ Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)
<a id="tp3-2"></a> <a id="tp3-2"></a>
### TP3.2 — WLAN Coverage / P750W-Discovery ▸ ✅ **ABGESCHLOSSEN (2026-08-17, User-Test erfolgreich)** — `docs/wifi-p750w-coverage-and-map.md` ### TP3.2 · Gitea #39 — WLAN Coverage / P750W-Discovery — `docs/wifi-p750w-coverage-and-map.md`
- ✅ **Brother P750W (VLAN30) gelöst:** DHCP-`client-id`-Fix (matcht über MAC → feste `10.40.30.5`); **an ap1 gepinnt** - ✅ **Brother P750W (VLAN30) gelöst:** DHCP-`client-id`-Fix (matcht über MAC → feste `10.40.30.5`); **an ap1 gepinnt**
(geordnete wifi-access-list `accept w2-iot` → blanket `reject`, überlebt ap2-Re-Provision) — dozende Legacy-Clients (geordnete wifi-access-list `accept w2-iot` → blanket `reject`, überlebt ap2-Re-Provision) — dozende Legacy-Clients
nur am co-located-GW ap1 per ARP erreichbar, CAP ap2 nicht; mDNS `vlan30` in `mdns-repeat-ifaces`+5353 → in MAIN nur am co-located-GW ap1 per ARP erreichbar, CAP ap2 nicht; mDNS `vlan30` in `mdns-repeat-ifaces`+5353 → in MAIN
@@ -221,7 +223,7 @@ Dazu am Ende: **[TP25 (Kondensat)](#tp25)** + **[TP25 (Langfassung)](#tp25-lang)
<a id="tp3-3"></a> <a id="tp3-3"></a>
### TP3.3 — WLAN Coverage-Kur ▸ 📜 **IN TP25 AUFGEGANGEN (2026-08-17)** — offene Arbeit steht dort, nicht hier ### TP3.3 · Gitea #40 — WLAN Coverage-Kur — offene Arbeit steht dort, nicht hier
> ⚠️ Der Header sagte bis 2026-08-19 „🔜 TODO (Fortsetzung, neue Session)" — dieselbe Falle wie bei TP3: > ⚠️ Der Header sagte bis 2026-08-19 „🔜 TODO (Fortsetzung, neue Session)" — dieselbe Falle wie bei TP3:
> ein Status, der nicht mitgezogen wurde. Der Re-Walk und die Dead-Spot-Kartierung sind **Schritt 5 von > ein Status, der nicht mitgezogen wurde. Der Re-Walk und die Dead-Spot-Kartierung sind **Schritt 5 von
> TP25** und zwar bewusst **nach** den Funkparameter-Fixes; die 802.11k/v-Reparatur ist am 2026-08-13 > TP25** und zwar bewusst **nach** den Funkparameter-Fixes; die 802.11k/v-Reparatur ist am 2026-08-13
@@ -260,7 +262,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp4"></a> <a id="tp4"></a>
### TP4 — Router-Cutover (hAP#1 = Router + WAN-Edge) ▸ ✅ FERTIG (2026-07-01) — ⚠️ **Rolle seit 2026-08-08 auf `crs326-core`** (TP21.1); alles unten beschreibt den damaligen ap1-Aufbau und gilt als **Historie/Referenz**, nicht als Ist ### TP4 · Gitea #41 — Router-Cutover (hAP#1 = Router + WAN-Edge) — ⚠️ **Rolle seit 2026-08-08 auf `crs326-core`** (TP21.1); alles unten beschreibt den damaligen ap1-Aufbau und gilt als **Historie/Referenz**, nicht als Ist
- ✅ **Bridge VLAN-Filtering aktiviert** (`vlan-filtering=yes`); VLAN-Tabelle: VLAN1 untagged bridge+ether2, VLANs 10/20/30/40/50/90/99/100 tagged bridge+ether2. - ✅ **Bridge VLAN-Filtering aktiviert** (`vlan-filtering=yes`); VLAN-Tabelle: VLAN1 untagged bridge+ether2, VLANs 10/20/30/40/50/90/99/100 tagged bridge+ether2.
- ✅ **VLAN-Subinterfaces:** vlan10/20/30/40/50/90/99 auf bridge; je GW `.1` (10.40.10.1 … 10.40.99.1). - ✅ **VLAN-Subinterfaces:** vlan10/20/30/40/50/90/99 auf bridge; je GW `.1` (10.40.10.1 … 10.40.99.1).
- ✅ **DHCP:** dhcp-v10…dhcp-v99 je auf eigenem VLAN-Interface, pools .100-.200, GW+DNS = VLAN-GW. - ✅ **DHCP:** dhcp-v10…dhcp-v99 je auf eigenem VLAN-Interface, pools .100-.200, GW+DNS = VLAN-GW.
@@ -275,7 +277,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp5"></a> <a id="tp5"></a>
### TP5 — Firewall & Inter-VLAN-Policy ▸ ✅ **DURCH (2026-08-18): deny-by-default in BEIDEN Ketten scharf** · **+ E5 (2026-08-19)** — 📋 `docs/tp5-deny-by-default-runbook.md` ### TP5 · Gitea #42 — Firewall & Inter-VLAN-Policy — + E5 (2026-08-19)** — 📋 `docs/tp5-deny-by-default-runbook.md`
> 📌 **„DURCH" heißt nicht „abgeschlossen".** E5 (MAIN→IOT) kam einen Tag nach dem Drop — unter deny-by-default > 📌 **„DURCH" heißt nicht „abgeschlossen".** E5 (MAIN→IOT) kam einen Tag nach dem Drop — unter deny-by-default
> braucht **jeder** neue Dienst über eine VLAN-Grenze seine Regel. Weitere Etappen sind normaler Betrieb, > braucht **jeder** neue Dienst über eine VLAN-Grenze seine Regel. Weitere Etappen sind normaler Betrieb,
> kein Rückschritt. Der Weg dahin ist immer derselbe: **erst den Drop-Log lesen**, dann die Regel schreiben. > kein Rückschritt. Der Weg dahin ist immer derselbe: **erst den Drop-Log lesen**, dann die Regel schreiben.
@@ -411,7 +413,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp6"></a> <a id="tp6"></a>
### TP6 — QoS/CAKE + Call-Quality-A/B-Test ▸ ✅ **GESCHLOSSEN 2026-08-23 — CAKE gebaut, gemessen, wieder ausgebaut.** ⚠️ **Die Erklaerung darin ist von TP6.1 ueberholt: nicht DOCSIS, sondern fehlende Klassentrennung.** ### TP6 · Gitea #43 — QoS/CAKE + Call-Quality-A/B-Test — ⚠️ **Die Erklaerung darin ist von TP6.1 ueberholt: nicht DOCSIS, sondern fehlende Klassentrennung.**
> 🔴 **ZUERST LESEN — diese Analyse ist teilweise widerlegt (2026-08-23, durch TP6.1).** > 🔴 **ZUERST LESEN — diese Analyse ist teilweise widerlegt (2026-08-23, durch TP6.1).**
> Unten steht, die ~30 ms entstuenden im DOCSIS-Uplink und seien unabaenderlich. **Sie sind es > Unten steht, die ~30 ms entstuenden im DOCSIS-Uplink und seien unabaenderlich. **Sie sind es
> nicht.** TP6.1 holt dieselbe Strecke unter derselben Saettigung auf **11,0 ms** — allein durch > nicht.** TP6.1 holt dieselbe Strecke unter derselben Saettigung auf **11,0 ms** — allein durch
@@ -550,7 +552,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp6-1"></a> <a id="tp6-1"></a>
### TP6.1 — Saettigung vermeiden statt Bufferbloat bekaempfen ▸ ✅ **LIVE UND GEMESSEN (2026-08-23): 40,8 → 11,0 ms unter Last** ### TP6.1 · Gitea #44 — Saettigung vermeiden statt Bufferbloat bekaempfen
> **Der Gedanke kam von Christoph** beim TP6-Abschluss („dann koennten wir zumindest den Sync > **Der Gedanke kam von Christoph** beim TP6-Abschluss („dann koennten wir zumindest den Sync
> von der NAS zurueckhalten“) — und er hat mein TP6-Urteil zu Recht angezweifelt. > von der NAS zurueckhalten“) — und er hat mein TP6-Urteil zu Recht angezweifelt.
> >
@@ -601,7 +603,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp7"></a> <a id="tp7"></a>
### TP7 — Media-Discovery (mDNS) für VLAN 40 ▸ ✅ FERTIG (2026-07-02) — `docs/tp7-mdns.md` ### TP7 · Gitea #45 — Media-Discovery (mDNS) für VLAN 40 — `docs/tp7-mdns.md`
- ✅ **Built-in mDNS-Repeater** aktiv — **Live-Stand `crs326-core` (verifiziert 2026-08-14):** - ✅ **Built-in mDNS-Repeater** aktiv — **Live-Stand `crs326-core` (verifiziert 2026-08-14):**
`mdns-repeat-ifaces=vlan10,vlan20,vlan30,vlan40,vlan90` → MAIN/INFRA/IOT/GUEST ↔ MEDIA. Reversibel (Feld leeren). `mdns-repeat-ifaces=vlan10,vlan20,vlan30,vlan40,vlan90` → MAIN/INFRA/IOT/GUEST ↔ MEDIA. Reversibel (Feld leeren).
📌 **`bridge` (=VLAN1) ist nicht mehr dabei** — mit dem VLAN1-Retirement erwartbar und korrekt, war aber nie nachgezogen. 📌 **`bridge` (=VLAN1) ist nicht mehr dabei** — mit dem VLAN1-Retirement erwartbar und korrekt, war aber nie nachgezogen.
@@ -618,7 +620,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp7-1"></a> <a id="tp7-1"></a>
### TP7.1 — SSDP-Relay (macOS-Sonos-App über VLAN-Grenze) ▸ ✅ **LÄUFT (2026-08-18)** — User-bestätigt: „sonos ist da!" ### TP7.1 · Gitea #46 — SSDP-Relay (macOS-Sonos-App über VLAN-Grenze) — User-bestätigt: „sonos ist da!"
- ✅ **Umsetzung:** eigener LXC **108 `mcast-relay`** auf pve (Ubuntu 24.04, 1 Kern, 256 MB, `onboot`), - ✅ **Umsetzung:** eigener LXC **108 `mcast-relay`** auf pve (Ubuntu 24.04, 1 Kern, 256 MB, `onboot`),
zweibeinig: `eth0` = **10.40.10.21** (VLAN10), `eth1` = **10.40.40.21** (VLAN40), Default-Route nur zweibeinig: `eth0` = **10.40.10.21** (VLAN10), `eth1` = **10.40.40.21** (VLAN40), Default-Route nur
über VLAN10, **kein IP-Forwarding**. Dazu `core ether13` (pve-Port) um `tagged vlan10, vlan40` über VLAN10, **kein IP-Forwarding**. Dazu `core ether13` (pve-Port) um `tagged vlan10, vlan40`
@@ -659,7 +661,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp7-2"></a> <a id="tp7-2"></a>
### TP7.2 — HA↔Sonos static-host binding + VLAN1/GUEST-Discovery ▸ ✅ FERTIG (2026-07-03) — `docs/sonos-inventar.md` ### TP7.2 · Gitea #47 — HA↔Sonos static-host binding + VLAN1/GUEST-Discovery — `docs/sonos-inventar.md`
- **Root Cause (widerlegt frühere Doku):** HA-`media_player.*`-Sonos alle `unavailable`. HA sitzt **nicht** auf - **Root Cause (widerlegt frühere Doku):** HA-`media_player.*`-Sonos alle `unavailable`. HA sitzt **nicht** auf
VLAN 20, sondern noch auf `10.40.41.105` (VLAN-1-Transfernetz, IP-Migration steht aus). Der mDNS-Repeater VLAN 20, sondern noch auf `10.40.41.105` (VLAN-1-Transfernetz, IP-Migration steht aus). Der mDNS-Repeater
deckte VLAN 1 **nicht** ab, und die HA-Sonos-Integration ist **SSDP-abhängig** (`manifest dependencies:[ssdp]`, deckte VLAN 1 **nicht** ab, und die HA-Sonos-Integration ist **SSDP-abhängig** (`manifest dependencies:[ssdp]`,
@@ -682,7 +684,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp9"></a> <a id="tp9"></a>
### TP9 — Gateway-/Router-HA ▸ ❌ OBSOLET (User-Entscheid 2026-08-08) ### TP9 · Gitea #48 — Gateway-/Router-HA
- ❌ **Verworfen (User-Begründung 2026-08-08, „ganz klar nein"):** am `crs326-core` führt ohnehin **nie ein Weg vorbei** — er ist der unumgängliche L3-Durchleitungspunkt. Ein zweites Gerät / VRRP **beseitigt diesen SPOF nicht**, sondern baut nur **zusätzliche Sollbruchstellen** ein (2× Config-Sync für DHCP/DNS/Firewall/NAT, Split-Brain-Risiko, VRRP-Eigenfehler) → netto ein Zuverlässigkeits-**Verlust**, kein -Gewinn. Die Frage ist nicht „Aufwand vs. Nutzen", sondern „mehr Fehlerquellen einbauen?" → nein. DR-Weg = Config-Backup-Restore (+ ggf. AP-Reprovisionierung). VRRP-Skizze unten = nur noch historische Recherche. - ❌ **Verworfen (User-Begründung 2026-08-08, „ganz klar nein"):** am `crs326-core` führt ohnehin **nie ein Weg vorbei** — er ist der unumgängliche L3-Durchleitungspunkt. Ein zweites Gerät / VRRP **beseitigt diesen SPOF nicht**, sondern baut nur **zusätzliche Sollbruchstellen** ein (2× Config-Sync für DHCP/DNS/Firewall/NAT, Split-Brain-Risiko, VRRP-Eigenfehler) → netto ein Zuverlässigkeits-**Verlust**, kein -Gewinn. Die Frage ist nicht „Aufwand vs. Nutzen", sondern „mehr Fehlerquellen einbauen?" → nein. DR-Weg = Config-Backup-Restore (+ ggf. AP-Reprovisionierung). VRRP-Skizze unten = nur noch historische Recherche.
- **„Was, wenn ap1 ausfällt?"** → **VRRP** löst Gateway-Failover je VLAN ~gratis (kreuzweise Master/Backup). - **„Was, wenn ap1 ausfällt?"** → **VRRP** löst Gateway-Failover je VLAN ~gratis (kreuzweise Master/Backup).
⚠️ **Haken (nicht Last):** DHCP/Firewall/NAT/DNS müssen auf **BEIDEN** APs **identisch** gepflegt sein, sonst „Gateway up, Internet tot". ⚠️ **Haken (nicht Last):** DHCP/Firewall/NAT/DNS müssen auf **BEIDEN** APs **identisch** gepflegt sein, sonst „Gateway up, Internet tot".
@@ -691,7 +693,8 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp10"></a> <a id="tp10"></a>
### TP10 — Multi-Site VPN ▸ ✅ KERN DURCH (2026-06-27) — beide WG live auf ap1-Hub (`docs/tp10-vpn-migration.md`) ### TP10 · Gitea #49 — Multi-Site VPN — beide WG live auf ap1-Hub (`docs/tp10-vpn-migration.md`)
> ♻️ **REOPENED 2026-10-03 → Volltext zurück in [`teilprojekte.md`](teilprojekte.md#tp10)** (neuer Befund: Mesh-Quell-NAT `100.96.0.0/12` → `10.40.10.1`). Dies ist die eingefrorene Historie.
> **Details/Cutover/Gotchas/Eckdaten → `docs/tp10-vpn-migration.md` (Single Source of Truth).** Research: `docs/research/vpn-comparison.md`. > **Details/Cutover/Gotchas/Eckdaten → `docs/tp10-vpn-migration.md` (Single Source of Truth).** Research: `docs/research/vpn-comparison.md`.
- **✅ KERN DURCH (2026-06-27) + EDGE-CUTOVER (2026-07-01):** beide AVM-Tunnel → **2× WireGuard mit Hub auf ap1** migriert. Live: `peer fm` - **✅ KERN DURCH (2026-06-27) + EDGE-CUTOVER (2026-07-01):** beide AVM-Tunnel → **2× WireGuard mit Hub auf ap1** migriert. Live: `peer fm`
(`10.101.0.0/24`, bidirektional, myfritz-Endpoint) **und** `peer dw` (`10.95.10.0/24`, CGNAT-Dial-in) mit frischem (`10.101.0.0/24`, bidirektional, myfritz-Endpoint) **und** `peer dw` (`10.95.10.0/24`, CGNAT-Dial-in) mit frischem
@@ -709,9 +712,87 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
- **Offen:** DDNS-Auto-Updater für `bf4.emsenhuber.net` auf NAS/pve, damit der A-Record der Public-IP folgt - **Offen:** DDNS-Auto-Updater für `bf4.emsenhuber.net` auf NAS/pve, damit der A-Record der Public-IP folgt
(falls Liwest sie tauscht). Aktuell manuell in Cloudflare gepflegt. (falls Liwest sie tauscht). Aktuell manuell in Cloudflare gepflegt.
<a id="tp10-ddns"></a>
### TP10 · Gitea #49 — Multi-Site VPN (WG-Hub FM/DW) — ✅ ABGESCHLOSSEN 2026-10-05 (Mesh-Quell-NAT + DDNS-Updater)
> **Details/Cutover/Gotchas/Eckdaten → `docs/tp10-vpn-migration.md` (Single Source of Truth).**
> Research: `docs/research/vpn-comparison.md`. Prosa-Historie: `teilprojekte-archiv.md#tp10`.
> **Zurück ins Archiv am 2026-10-05** — beide Close-Bedingungen erfüllt (DDNS-Updater live,
> Mesh-NAT-Regel im täglichen GC-Lauf konform). Erneut geöffnet war es am 2026-10-03 für den
> Mesh-Quell-NAT-Befund (Konvention: geschlossenes Ticket, kein versiegeltes).
**Bestand (aus dem Archiv, unverändert gültig):** beide WG-Tunnel (fm `10.101.0.0/24`, dw
`10.95.10.0/24`) laufen mit Hub auf `crs326-core`; `wg-hub` UDP/51821, MTU 1412, **keine
Interface-IP** (bewusst so, s.u.). Public-Edge direkt, kein Double-NAT.
#### 🔴 Neuer Befund (2026-10-03) — Mesh-Quelle außerhalb der Spoke-AllowedIPs
WARP-Clients erreichen die Spoke-Netze über **`cf-mesh`** (TP16.1) mit Quell-IP **`100.96.0.0/12`**.
Die Spoke-FritzBoxen führen am WG-Peer aber nur **`10.40.0.0/16`** als AllowedIPs → sie **verwerfen**
den Verkehr. Live bewiesen am Core:
| Quelle | → Spoke | Ergebnis |
|---|---|---|
| `src=100.96.0.6` (cf-mesh) | fm/dw | ❌ 100 % Loss — **das ist der Fehler** |
| `src=10.40.20.1` (BF-IP) | fm/dw | ✅ 0 % Loss |
**Fix (live seit 2026-10-03, Commit `1fddf5d`):** eine `src-nat`-Regel auf dem `wg-hub`-**Egress**
legt die Quelle in den AllowedIPs-Raum:
`/ip firewall nat add action=src-nat chain=srcnat comment="TP10: NAT mesh source to WG spokes (AllowedIPs)" out-interface=wg-hub src-address=100.96.0.0/12 to-addresses=10.40.10.1`
Soll im GC-Template (`nautobot/mikrotik_routeros.j2` Abschnitt `[7] NAT`), Deploy idempotent über den
Job **`tp10-mesh-nat`** (`nautobot/gc-configplan.py`, manueller Plan, **nie** full-intended).
**Warum `src-nat` und nicht `masquerade`:** `wg-hub` hat **keine Interface-IP** — masquerade hätte
keine Quelle zum Einsetzen. Die feste IP `10.40.10.1` hält zudem das Connection-Tracking: die Antwort
an `10.40.10.1` wird im prerouting auf die Mesh-IP zurückübersetzt und via `cf-mesh` geroutet.
**Abnahme (live 2026-10-03):** Regel-Zähler steigt (6 Pakete/456 Bytes bei der ersten Prüfung),
Core → `10.95.10.246` ✅ 0 % Loss (vorher 100 %), Mac/WARP → `10.95.10.1` ✅.
#### ⚠️ Bewusste Grenze
Alle Mesh-Clients erscheinen an der FritzBox als **eine** IP (`10.40.10.1`) — unterscheidbar nur
über Connection-Tracking am Core. Christoph-Entscheid 2026-10-03: **bewusst so**, Erreichbarkeit war
das Ziel. Eine Unterscheidbarkeit pro Client ist **nicht** Teil dieses TP.
#### ❌ Verworfene Diagnose — nicht wieder aufgreifen
Ein Handoff vom 2026-09-28 behauptete, `wg-hub`s fehlende Interface-IP sei ein **Defekt**, der
Core-eigene Traffic unerreichbar mache, und schlug `preferred-source` auf den zwei TP10-Routen vor.
**Beides ist falsch:** (a) die fehlende IF-IP ist **beabsichtigt** und im Archiv dokumentiert;
(b) der Core-eigene Ping ist **kein** Betriebsproblem — normaler Client-Traffic liegt mit seiner
Quell-IP ohnehin im erlaubten Raum. Die Datei wurde gelöscht; die Erkenntnis steht hier.
#### Offen / Close-Bedingung
- [x] **DDNS-Updater für `bf4.emsenhuber.net`** — **LIVE seit 2026-10-03** auf `crs326-core`:
Script `tp10-ddns-bf4` installiert, Trigger: DHCP-Client `client1` (Lease-Event),
Token `cfDdnsToken` in `/system script environment` (53 Zeichen, SSoT = `CF_DNS_API_TOKEN`).
Positiv-Test bestanden: Test-IP 1.2.3.4 → Script-Lauf → Record zurück auf 90.146.33.177,
Comment `2026.10.03-18:08 by crs326-core`, Idempotenz-Variable `CFWanIP4Cur` gesetzt.
- [x] **Mesh-NAT-Regel im täglichen GC-Lauf** — `tp10-mesh-nat` manueller ConfigPlan,
im Template `nautobot/mikrotik_routeros.j2` Abschnitt `[7] NAT` abgebildet, wird beim
nächsten `backup → intended → compliance` Zyklus (täglich 03:30 UTC) deployed.
- [x] **Zweiter Spoke prüfen** — Fix an fm/dw gemeinsam verifiziert (fm & dw gleicher
AllowedIPs-Raum, dieselbe src-nat-Regel greift für beide).
#### ✅ Abschluss-Nachweis (2026-10-05)
- **DDNS Positiv-Test:** A-Record auf `1.2.3.4` gesetzt → `/system script run tp10-ddns-bf4` →
Record zurück auf `90.146.33.177`, Comment `2026.10.03-18:08 by crs326-core`, `CFWanIP4Cur` gesetzt.
- **GC-Compliance:** `nautobot_golden_config` → `ip-firewall-nat` = **true** für crs326-core;
Regel steht in der **Intended Config** (Z.425), nicht nur im Backup; zwei tägliche
`AllDevicesGoldenConfig`-Läufe nach dem Fix grün (2026-10-04, 2026-10-05).
- **Commits:** `6e2a7cf` (Global-Scope-Fix) + `75bd43d` (Abschluss/Doku) → `main` gepusht.
- Gitea **#49 geschlossen** (Label `status:done`).
### ⚠️ Offener Restpunkt (bewusst, eigener TP)
**Recovery-Lücke:** das GC-Template rendert **kein `/system script` / `/system scheduler`**.
Ein Recovery von null hätte den DDNS-Updater nicht. Nicht Teil von TP10 —
**übernommen von [TP48](teilprojekte.md#tp48)** (Gitea #75, 2026-10-05), der die ganze Klasse
inventarisiert und zugleich prüft, ob Ansible Nautobot als SSoT ablösen kann.
*(Frühere Notiz „Ablösung durch Komodo geplant" ist damit korrigiert: Komodo verwaltet Container,
nicht RouterOS-Config — es kann diese Lücke nicht schließen.)*
<a id="tp14"></a> <a id="tp14"></a>
### TP14 — Host/Container-IP-Migration VLAN1→VLAN20 (Docker-Cutover NAS+pve) ▸ ✅ DURCH (2026-07-13; live re-verifiziert 2026-08-08) — `docs/nas-vlan20/RUNBOOK.md`, Handoff `docs/nas-vlan20/handoff/` ### TP14 · Gitea #50 — Host/Container-IP-Migration VLAN1→VLAN20 (Docker-Cutover NAS+pve) — live re-verifiziert 2026-08-08) — `docs/nas-vlan20/RUNBOOK.md`, Handoff `docs/nas-vlan20/handoff/`
- ✅ **ABGESCHLOSSEN — Migration komplett** (Live-Check 2026-08-08 vom core: NAS `.20.10`, pve `.20.100`, Docker-LXC `.20.126`, platform-pg `.20.20`, Nautobot `.20.30` alle 0% Loss). Alle Hosts + Container auf VLAN20. **TP14.4** (CF-Access-Token-Rotation) = kleiner, nicht-blockierender Standalone-Follow-up (CF/TP11-Umfeld); **TP14.5** war nie ein eigener Change (nur ein Reihenfolge-Gate) → erfüllt, TP5 damit entblockt. - ✅ **ABGESCHLOSSEN — Migration komplett** (Live-Check 2026-08-08 vom core: NAS `.20.10`, pve `.20.100`, Docker-LXC `.20.126`, platform-pg `.20.20`, Nautobot `.20.30` alle 0% Loss). Alle Hosts + Container auf VLAN20. **TP14.4** (CF-Access-Token-Rotation) = kleiner, nicht-blockierender Standalone-Follow-up (CF/TP11-Umfeld); **TP14.5** war nie ein eigener Change (nur ein Reihenfolge-Gate) → erfüllt, TP5 damit entblockt.
- ✅ **NAS (DS923, ep14) migriert (2026-07-09/10):** Docker von `default_external` (macvlan .41) auf **ein** `vlan20`-Macvlan (10.40.20.0/24, gw .1, parent `ovs_eth0`); DSM-Cutover `.20.10`; Portainer-Endpoint `.20.10:9001`. **Root-Cause der Dauerfehler** = Portainer **Config-Only-Macvlan-Falle** (`Driver:null`, kein Container-Attach) + Macvlan-Parent-Exklusivität (`default_external` muss weg für `vlan20`) + `default_internal` (172.18) weg. Container laufen auf `.20.x`. - ✅ **NAS (DS923, ep14) migriert (2026-07-09/10):** Docker von `default_external` (macvlan .41) auf **ein** `vlan20`-Macvlan (10.40.20.0/24, gw .1, parent `ovs_eth0`); DSM-Cutover `.20.10`; Portainer-Endpoint `.20.10:9001`. **Root-Cause der Dauerfehler** = Portainer **Config-Only-Macvlan-Falle** (`Driver:null`, kein Container-Attach) + Macvlan-Parent-Exklusivität (`default_external` muss weg für `vlan20`) + `default_internal` (172.18) weg. Container laufen auf `.20.x`.
- ✅ **CF-Ingress v116 (2026-07-10):** 6 NAS-Origins .41→.20 (SSoT `docs/cf-tunnel-mapping.md` §8); `vw` 200 via Tunnel = pve→VLAN20-Routing steht (502 direkt nach Ingress-PUT = transient). - ✅ **CF-Ingress v116 (2026-07-10):** 6 NAS-Origins .41→.20 (SSoT `docs/cf-tunnel-mapping.md` §8); `vw` 200 via Tunnel = pve→VLAN20-Routing steht (502 direkt nach Ingress-PUT = transient).
@@ -723,7 +804,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14-1"></a> <a id="tp14-1"></a>
### TP14.1 — Session A: NAS fertigstellen ▸ ✅ DURCH (2026-07-10); Omada-Inform/sw2 VERTAGT ### TP14.1 · Gitea #51 — Session A: NAS fertigstellen — Omada-Inform/sw2 VERTAGT
- ✅ **Alle ep14-Container auf vlan20 redeployed + verifiziert running/.20.x** (via Portainer-Stack-Redeploy, Env preserved, Volumes intakt): oc-stack(112 → prometheus `.151`/node-exporter `.154`/grafana `.109`), default(72 → hms-mqtt `.124`), nginx(101 → dashboard-web `.148`), oc-central(160 → qdrant `.159`), oc-charlie(158 → `.162`+cf), oc-johnny2(150 → `.161`+cf), gitea(111 → `.157`, DB `.20.23:3306` erreichbar ✓), drawio(107 → `.139`, intern; CF-CNAME war eh gelöscht). - ✅ **Alle ep14-Container auf vlan20 redeployed + verifiziert running/.20.x** (via Portainer-Stack-Redeploy, Env preserved, Volumes intakt): oc-stack(112 → prometheus `.151`/node-exporter `.154`/grafana `.109`), default(72 → hms-mqtt `.124`), nginx(101 → dashboard-web `.148`), oc-central(160 → qdrant `.159`), oc-charlie(158 → `.162`+cf), oc-johnny2(150 → `.161`+cf), gitea(111 → `.157`, DB `.20.23:3306` erreichbar ✓), drawio(107 → `.139`, intern; CF-CNAME war eh gelöscht).
- ✅ **pve-mcp NEU** als Stack (id 202, vlan20-only `.20.173`), verbunden mit Proxmox `.41.100` (v9.0.11) — ⚠️ **StreamableHTTP am Root-Pfad `/`, NICHT `/sse`** (mcp-pve v0.4.8, `MCP_TRANSPORT=http`). `~/.claude.json` `mcpServers.pve-mcp` NEU angelegt `{type:http, url:http://10.40.20.173:3000/}` (war vorher gar nicht drin) → wirkt nach Claude-Code-Restart. - ✅ **pve-mcp NEU** als Stack (id 202, vlan20-only `.20.173`), verbunden mit Proxmox `.41.100` (v9.0.11) — ⚠️ **StreamableHTTP am Root-Pfad `/`, NICHT `/sse`** (mcp-pve v0.4.8, `MCP_TRANSPORT=http`). `~/.claude.json` `mcpServers.pve-mcp` NEU angelegt `{type:http, url:http://10.40.20.173:3000/}` (war vorher gar nicht drin) → wirkt nach Claude-Code-Restart.
- ✅ **Tunnel-Smoke** (CF-Ingress v116 zeigte schon auf `.20`): gitea 200, grafana 302, vw 200 — Origins existieren jetzt real. - ✅ **Tunnel-Smoke** (CF-Ingress v116 zeigte schon auf `.20`): gitea 200, grafana 302, vw 200 — Origins existieren jetzt real.
@@ -734,7 +815,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14-2"></a> <a id="tp14-2"></a>
### TP14.2 — Session B: pve-Docker-LXC (.126) → VLAN20 ▸ ✅ DURCH (2026-07-10) (ipvlan) ### TP14.2 · Gitea #52 — Session B: pve-Docker-LXC (.126) → VLAN20
- ✅ ep2-Container auf `vlan20`: **omada-mcp `.20.176`** (OMADA_BASE_URL→`.20.175`), **adguard_bf1 `.20.16`**, **adguard_fm2 `.20.17`** — alle running/reachable (health 200, adguard 302). Transf. Files `docs/nas-vlan20/stacks/{195-adguard,200-omada-mcp}.yml`. - ✅ ep2-Container auf `vlan20`: **omada-mcp `.20.176`** (OMADA_BASE_URL→`.20.175`), **adguard_bf1 `.20.16`**, **adguard_fm2 `.20.17`** — alle running/reachable (health 200, adguard 302). Transf. Files `docs/nas-vlan20/stacks/{195-adguard,200-omada-mcp}.yml`.
- ✅ LXC 400 (`docker`) net0 → `tag=20, ip=10.40.20.126/24, gw=10.40.20.1` (⚠️ **Retag brauchte Christoph** — Token `claude@pam!claude` hat kein `VM.Config.Network`; UI-Edit, dann Reboot via API mit `VM.PowerMgmt`). Portainer wandert auf `https://10.40.20.126:9443`. - ✅ LXC 400 (`docker`) net0 → `tag=20, ip=10.40.20.126/24, gw=10.40.20.1` (⚠️ **Retag brauchte Christoph** — Token `claude@pam!claude` hat kein `VM.Config.Network`; UI-Edit, dann Reboot via API mit `VM.PowerMgmt`). Portainer wandert auf `https://10.40.20.126:9443`.
- 🐞 **Gotcha: in-Container-Iface heißt jetzt `veth20`, NICHT `eth0`** (UI setzte Name analog net1=veth16) → Docker `vlan20`=ipvlan **`parent=veth20`** (nicht eth0 wie im Runbook). cloudflared(host)/portainer(bridge) unberührt. - 🐞 **Gotcha: in-Container-Iface heißt jetzt `veth20`, NICHT `eth0`** (UI setzte Name analog net1=veth16) → Docker `vlan20`=ipvlan **`parent=veth20`** (nicht eth0 wie im Runbook). cloudflared(host)/portainer(bridge) unberührt.
@@ -744,13 +825,13 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14-2a"></a> <a id="tp14-2a"></a>
### TP14.2a — pve-mcp Single-Session-Fix + VLAN20-Host-DNS ▸ ✅ (2026-07-10) ### TP14.2a · Gitea #53 — pve-mcp Single-Session-Fix + VLAN20-Host-DNS
- **pve-mcp „Failed to connect":** Ursache = **mcp-pve v0.4.8** kann im HTTP-Transport nur **eine** MCP-Session (ein globaler `StreamableHTTPServerTransport`, `sessionIdGenerator` gesetzt) → erster Client nach Boot belegt den Slot, jeder weitere Init (neue Claude-Code-Session, `claude mcp list`-Healthcheck, Reconnect) = `400 "Server already initialized"`. **omada-mcp NICHT betroffen** (frische Session-ID pro Init → Per-Session-Transport). **Fix = Upgrade auf 0.6.1** (Per-Session-Map, `serverFactory()` je Session, gleiche Env-Vars = Drop-in); Stack 202 neu deployed, Container `.20.173` = 0.6.1, `Connected to Proxmox VE 9.0.11`, `tools/list` liefert `pve_*`, **Claude Code ✔ Connected**. 3× init hintereinander = 3× 200 mit verschiedenen session-ids (verifiziert). - **pve-mcp „Failed to connect":** Ursache = **mcp-pve v0.4.8** kann im HTTP-Transport nur **eine** MCP-Session (ein globaler `StreamableHTTPServerTransport`, `sessionIdGenerator` gesetzt) → erster Client nach Boot belegt den Slot, jeder weitere Init (neue Claude-Code-Session, `claude mcp list`-Healthcheck, Reconnect) = `400 "Server already initialized"`. **omada-mcp NICHT betroffen** (frische Session-ID pro Init → Per-Session-Transport). **Fix = Upgrade auf 0.6.1** (Per-Session-Map, `serverFactory()` je Session, gleiche Env-Vars = Drop-in); Stack 202 neu deployed, Container `.20.173` = 0.6.1, `Connected to Proxmox VE 9.0.11`, `tools/list` liefert `pve_*`, **Claude Code ✔ Connected**. 3× init hintereinander = 3× 200 mit verschiedenen session-ids (verifiziert).
- **Kollateral-Fund — VLAN20-Host-DNS kaputt (beide Docker-Hosts):** nach dem VLAN20-Umzug zeigten **NAS** *und* **pve-LXC** `/etc/resolv.conf` noch auf totes `.41.254` → `docker pull` ghcr.io scheitert (`EAI_AGAIN` / 5s-Resolver-Timeout; Egress zu ghcr-IP `140.82.x:443` = OK 31 ms, also **nur Resolver tot**, nicht Egress). Fixes: **LXC** `.20.126` → `pct set 400 --nameserver 10.40.20.1` (Christoph). **NAS** → DSM-„Allgemein"-DNS war `.20.1`, ABER die **Netzwerkschnittstelle zog DNS per DHCP** und überschrieb `resolv.conf`; Fix = Schnittstelle „DNS manuell" `10.40.20.1` (Christoph) → NAS pullt wieder direkt. **Merkregel Synology:** GUI-DNS ≠ was `dockerd` liest (Quelle `/etc/resolv.conf`, per DHCP überschreibbar). **Workaround solange NAS-DNS tot:** `docker save`(LXC, hat DNS) → **gzip** (191-MB-tar reißt am Portainer-Agent-Socket-Proxy „context canceled"; ~70 MB gz geht) → Portainer `POST /endpoints/14/docker/images/load` → Stack-Redeploy `pullImage:false`. Gotcha: laufende Container frieren den `127.0.0.11`-Upstream zum Erstellzeitpunkt ein (Recreate nötig; für pve-mcp egal — Proxmox per IP `.41.100`). Memory `ref_pve_mcp_singlesession_and_vlan20_dns`. - **Kollateral-Fund — VLAN20-Host-DNS kaputt (beide Docker-Hosts):** nach dem VLAN20-Umzug zeigten **NAS** *und* **pve-LXC** `/etc/resolv.conf` noch auf totes `.41.254` → `docker pull` ghcr.io scheitert (`EAI_AGAIN` / 5s-Resolver-Timeout; Egress zu ghcr-IP `140.82.x:443` = OK 31 ms, also **nur Resolver tot**, nicht Egress). Fixes: **LXC** `.20.126` → `pct set 400 --nameserver 10.40.20.1` (Christoph). **NAS** → DSM-„Allgemein"-DNS war `.20.1`, ABER die **Netzwerkschnittstelle zog DNS per DHCP** und überschrieb `resolv.conf`; Fix = Schnittstelle „DNS manuell" `10.40.20.1` (Christoph) → NAS pullt wieder direkt. **Merkregel Synology:** GUI-DNS ≠ was `dockerd` liest (Quelle `/etc/resolv.conf`, per DHCP überschreibbar). **Workaround solange NAS-DNS tot:** `docker save`(LXC, hat DNS) → **gzip** (191-MB-tar reißt am Portainer-Agent-Socket-Proxy „context canceled"; ~70 MB gz geht) → Portainer `POST /endpoints/14/docker/images/load` → Stack-Redeploy `pullImage:false`. Gotcha: laufende Container frieren den `127.0.0.11`-Upstream zum Erstellzeitpunkt ein (Recreate nötig; für pve-mcp egal — Proxmox per IP `.41.100`). Memory `ref_pve_mcp_singlesession_and_vlan20_dns`.
<a id="tp14-3"></a> <a id="tp14-3"></a>
### TP14.3 — Session C: Dormante Stacks vlan20-tauglich + Referenz-Sweep ▸ ✅ Referenz-Sweep durch (2026-07-16); dormante Stacks weiter bewusst nicht deployt ### TP14.3 · Gitea #54 — Session C: Dormante Stacks vlan20-tauglich + Referenz-Sweep — dormante Stacks weiter bewusst nicht deployt
- ✅ **Dormante Stacks bereitgestellt (2026-07-11):** `transform.py` auf core(43)/openclaw(92)/openclaw2(97)/oc20260307(119)/openspeedtest(74)/speedtest_tracker(80)/technikscheiss(42) → **`.new.yml` + `.orig.yml`** in `docs/nas-vlan20/stacks/` (gitignored). **Nicht deployt** (User: „im Bedarfsfall später", Container-Zukunft ungewiss). 42/43 waren `default_internal` → jetzt vlan20-only (DHCP-.20.x). ⚠️ **Deploy-Caveats:** externe Refs bewusst NICHT umgeschrieben (`DB_HOST=.41.122` [pre-existing tote openclaw-DB, in 80+119], `OLLAMA_BASE_URL=.41.129`, `REDIS_URL=.41.111` in 119); **Kollision** `43-core` will `.20.23` = gitea-DB-IP (nicht gleichzeitig deploybar). - ✅ **Dormante Stacks bereitgestellt (2026-07-11):** `transform.py` auf core(43)/openclaw(92)/openclaw2(97)/oc20260307(119)/openspeedtest(74)/speedtest_tracker(80)/technikscheiss(42) → **`.new.yml` + `.orig.yml`** in `docs/nas-vlan20/stacks/` (gitignored). **Nicht deployt** (User: „im Bedarfsfall später", Container-Zukunft ungewiss). 42/43 waren `default_internal` → jetzt vlan20-only (DHCP-.20.x). ⚠️ **Deploy-Caveats:** externe Refs bewusst NICHT umgeschrieben (`DB_HOST=.41.122` [pre-existing tote openclaw-DB, in 80+119], `OLLAMA_BASE_URL=.41.129`, `REDIS_URL=.41.111` in 119); **Kollision** `43-core` will `.20.23` = gitea-DB-IP (nicht gleichzeitig deploybar).
- ✅ **Referenz-Sweep `.41→.20` (2026-07-16, im Rahmen der TP19-Backup-Vorbereitung live gefunden + gefixt):** ap1 `/ip dns static` hatte noch `nas.bf.lan→.41.10` und `docker.bf.lan→.41.126` **stale** — korrigiert auf `.20.10`/`.20.126` (MikroMCP `manage_dns_entry`, dry-run+apply). Zusätzlich per User-Entscheid **Omada-Controller + omada-mcp komplett gelöscht** (overkill für 1 Switch auf der Abschussliste) → DNS-Einträge `omada.bf.lan`/`omada-mcp.bf.lan` entfernt statt migriert. `fritzbox.bf.lan` (totes Gerät, physisch weg seit 2026-07-01) ebenfalls entfernt. `docs/ip-plan.md` + CLAUDE.md nachgezogen. - ✅ **Referenz-Sweep `.41→.20` (2026-07-16, im Rahmen der TP19-Backup-Vorbereitung live gefunden + gefixt):** ap1 `/ip dns static` hatte noch `nas.bf.lan→.41.10` und `docker.bf.lan→.41.126` **stale** — korrigiert auf `.20.10`/`.20.126` (MikroMCP `manage_dns_entry`, dry-run+apply). Zusätzlich per User-Entscheid **Omada-Controller + omada-mcp komplett gelöscht** (overkill für 1 Switch auf der Abschussliste) → DNS-Einträge `omada.bf.lan`/`omada-mcp.bf.lan` entfernt statt migriert. `fritzbox.bf.lan` (totes Gerät, physisch weg seit 2026-07-01) ebenfalls entfernt. `docs/ip-plan.md` + CLAUDE.md nachgezogen.
- ✅ **ERLEDIGT/GEGENSTANDSLOS (live geprüft 2026-08-16):** ~~`~/.claude.json` (omada-mcp-Eintrag verwaist), CF-Ingress-Rest (`omada`/`omada-mcp`-Hostnames, ungeprüft)~~ - ✅ **ERLEDIGT/GEGENSTANDSLOS (live geprüft 2026-08-16):** ~~`~/.claude.json` (omada-mcp-Eintrag verwaist), CF-Ingress-Rest (`omada`/`omada-mcp`-Hostnames, ungeprüft)~~
@@ -760,7 +841,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp14-5"></a> <a id="tp14-5"></a>
### TP14.5 — Session E: Freigabe-Gate → TP5-Firewall ▸ ✅ ERFÜLLT (2026-08-08) — kein eigener Change ### TP14.5 · Gitea #55 — Session E: Freigabe-Gate → TP5-Firewall — kein eigener Change
- Gate „TP14.1–14.3 durch (alle Origins/Connectoren in VLAN20)" ist erfüllt → **TP5 (Deny-by-default) ist entblockt.** Reines Reihenfolge-Gate, kein eigener Change. - Gate „TP14.1–14.3 durch (alle Origins/Connectoren in VLAN20)" ist erfüllt → **TP5 (Deny-by-default) ist entblockt.** Reines Reihenfolge-Gate, kein eigener Change.
<a id="tp15"></a> <a id="tp15"></a>
@@ -771,7 +852,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
> auf tote Adressen — `backup-ap1.py` allerdings auf eine **lebende falsche** (`10.40.10.1` ist die > auf tote Adressen — `backup-ap1.py` allerdings auf eine **lebende falsche** (`10.40.10.1` ist die
> VLAN10-SVI des Core, nicht ap1) und hätte still das falsche Gerät gesichert. > VLAN10-SVI des Core, nicht ap1) und hätte still das falsche Gerät gesichert.
> >
### TP15 — HW-Refresh: Switching + Router ▸ ✅ **ABGESCHLOSSEN (2026-08-08)** — Bring-up beider CRS326 + Branch-Tausch (2026-07-23) **und** Core-Tausch/L3-Umzug (2026-08-08, Durchführung s. TP21.1) sind durch — `docs/hw-refresh-requirements.md`, Memory `project_mikrotik_commit_nautobot_gc` ### TP15 · Gitea #56 — HW-Refresh: Switching + Router — Bring-up beider CRS326 + Branch-Tausch (2026-07-23) **und** Core-Tausch/L3-Umzug (2026-08-08, Durchführung s. TP21.1) sind durch — `docs/hw-refresh-requirements.md`, Memory `project_mikrotik_commit_nautobot_gc`
> 🔌 **Scope-Änderung 2026-08-16 (User-Entscheid): die DC-USV ist aus diesem TP herausgelöst und heißt jetzt **TP24**.** Sie war der letzte offene Rest hier, hat mit Switching/Routing sachlich nichts zu tun und trägt eigene Entscheidungen (Elektrik, Batterie, Standort). TP15 ist damit inhaltlich zu. > 🔌 **Scope-Änderung 2026-08-16 (User-Entscheid): die DC-USV ist aus diesem TP herausgelöst und heißt jetzt **TP24**.** Sie war der letzte offene Rest hier, hat mit Switching/Routing sachlich nichts zu tun und trägt eigene Entscheidungen (Elektrik, Batterie, Standort). TP15 ist damit inhaltlich zu.
> *Vorherige Header-Fassung (Stand 2026-08-05, überholt): „🟢 CRS326-Bring-up + Branch-Tausch DURCH (2026-07-23); Core-Tausch/L3-Umzug **vorbereitet, Cutover-Config materialisiert (2026-08-05)**, Cutover = nächstes Wartungsfenster."* > *Vorherige Header-Fassung (Stand 2026-08-05, überholt): „🟢 CRS326-Bring-up + Branch-Tausch DURCH (2026-07-23); Core-Tausch/L3-Umzug **vorbereitet, Cutover-Config materialisiert (2026-08-05)**, Cutover = nächstes Wartungsfenster."*
- ✅ **BRING-UP BEIDER CRS326 + BRANCH-TAUSCH gegen sw2 — DURCH (2026-07-23, Claude via REST unter Governance-Ausnahme #2).** Ablauf/As-built: - ✅ **BRING-UP BEIDER CRS326 + BRANCH-TAUSCH gegen sw2 — DURCH (2026-07-23, Claude via REST unter Governance-Ausnahme #2).** Ablauf/As-built:
@@ -825,7 +906,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp16-1"></a> <a id="tp16-1"></a>
### TP16.1 — Private Access via native Cloudflare Mesh (WireGuard) auf `crs326-core` ▸ ✅ **ABGESCHLOSSEN (2026-08-17)** — `docs/tp16.1-warp-mesh-native-crs326.md` ### TP16.1 · Gitea #57 — Private Access via native Cloudflare Mesh (WireGuard) auf `crs326-core` — `docs/tp16.1-warp-mesh-native-crs326.md`
- ✅ **Kernergebnis: ICMP ist repariert.** Privater Zugang unterwegs läuft über **natives WireGuard auf `crs326-core`** statt cloudflared-QUIC — **0 % Ping-Loss auf alle BF-VLANs, vorher 100 %**. Genau das Symptom aus dem Incident 2026-07-16, das dieses TP ausgelöst hat. Öffentliche FQDNs unberührt auf dem 3-Tunnel-HA (TP16). Compliance **68/68**. - ✅ **Kernergebnis: ICMP ist repariert.** Privater Zugang unterwegs läuft über **natives WireGuard auf `crs326-core`** statt cloudflared-QUIC — **0 % Ping-Loss auf alle BF-VLANs, vorher 100 %**. Genau das Symptom aus dem Incident 2026-07-16, das dieses TP ausgelöst hat. Öffentliche FQDNs unberührt auf dem 3-Tunnel-HA (TP16). Compliance **68/68**.
- ✅ **§4.0 erledigt 2026-08-17: Mesh-Node angelegt.** `bf-core-mesh` = **`d1da7c8a-2736-4cbb-8af1-32db5393fb9c`**, Register-Token in `mcp/cloudflare/.env` (`CF_MESH_REGISTER_TOKEN`, gitignored). ➡️ **Die seit 2026-07-17 offene Free-Plan-Frage ist beantwortet: der Free-ZT-Plan erlaubt Cloudflare Mesh — der LXC-Fallback ist vom Tisch.** - ✅ **§4.0 erledigt 2026-08-17: Mesh-Node angelegt.** `bf-core-mesh` = **`d1da7c8a-2736-4cbb-8af1-32db5393fb9c`**, Register-Token in `mcp/cloudflare/.env` (`CF_MESH_REGISTER_TOKEN`, gitignored). ➡️ **Die seit 2026-07-17 offene Free-Plan-Frage ist beantwortet: der Free-ZT-Plan erlaubt Cloudflare Mesh — der LXC-Fallback ist vom Tisch.**
- 🔑 **Der Weg dahin war ein Token-Problem, kein Plan-Problem (kostete 2 Anläufe).** Der alte User-Token hatte **zwei** fehlende Permissions (§4.0 *und* §4.1 — `devices/settings` war schon lesend `403`). Ersetzt durch einen **account-owned** Token (`9984078812bc…`, ⏰ **gültig bis 2027-01-09**). Zwei Fallen, beide sehen aus wie „Token kaputt": (1) Account-Tokens verifizieren an `/accounts/{id}/tokens/verify` — am alten `/user/tokens/verify` melden sie `1000 "Invalid API Token"`, **während alle echten Endpoints 200 liefern**; (2) Dashboard-Fehler `"Account tag in access policy must match tag in request uri"` = eine **Zone**-Permission (`DNS`) steckt in der Policy-Zeile mit **Account**-Ressourcen → braucht eine eigene Zeile. Verifiziertes Soll-Set + Permission-IDs in `docs/tp16.1-…md` §4.0. **Merke: Dashboard sagt „Edit", API sagt „Write".** - 🔑 **Der Weg dahin war ein Token-Problem, kein Plan-Problem (kostete 2 Anläufe).** Der alte User-Token hatte **zwei** fehlende Permissions (§4.0 *und* §4.1 — `devices/settings` war schon lesend `403`). Ersetzt durch einen **account-owned** Token (`9984078812bc…`, ⏰ **gültig bis 2027-01-09**). Zwei Fallen, beide sehen aus wie „Token kaputt": (1) Account-Tokens verifizieren an `/accounts/{id}/tokens/verify` — am alten `/user/tokens/verify` melden sie `1000 "Invalid API Token"`, **während alle echten Endpoints 200 liefern**; (2) Dashboard-Fehler `"Account tag in access policy must match tag in request uri"` = eine **Zone**-Permission (`DNS`) steckt in der Policy-Zeile mit **Account**-Ressourcen → braucht eine eigene Zeile. Verifiziertes Soll-Set + Permission-IDs in `docs/tp16.1-…md` §4.0. **Merke: Dashboard sagt „Edit", API sagt „Write".**
@@ -865,7 +946,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp17"></a> <a id="tp17"></a>
### TP17 — Shelly-Übersiedelung VLAN1 → VLAN30 (IOT) + `shelly-mcp` ▸ ✅ **KOMPLETT (User-Bestätigt 2026-08-13): alle Shellys auf VLAN30** — Geräte-PW *(war TP17.2, seit 2026-08-24 **TP30** Posten #3)* — `mcp/shelly/`, Runbook `docs/tp17-shelly-go-runbook.md` ### TP17 · Gitea #58 — Shelly-Übersiedelung VLAN1 → VLAN30 (IOT) + `shelly-mcp` — Geräte-PW *(war TP17.2, seit 2026-08-24 **TP30** Posten #3)* — `mcp/shelly/`, Runbook `docs/tp17-shelly-go-runbook.md`
- ✅ **Abschluss 2026-08-13 (User-Bestätigt):** auch die zuletzt offenen saisonalen Shellys (s1/s2/s3/s12) sind inzwischen umgeklemmt — **alle Shellys auf `dirtyvader-iot`/VLAN30**, kein VLAN1-Shelly-Rest mehr. Migration-Teil von TP17 damit vollständig abgeschlossen. - ✅ **Abschluss 2026-08-13 (User-Bestätigt):** auch die zuletzt offenen saisonalen Shellys (s1/s2/s3/s12) sind inzwischen umgeklemmt — **alle Shellys auf `dirtyvader-iot`/VLAN30**, kein VLAN1-Shelly-Rest mehr. Migration-Teil von TP17 damit vollständig abgeschlossen.
- ✅ **GO durchgezogen 2026-07-13 (16 Shellys VLAN1→VLAN30, 0 Fehler):** Pilot s26 + Welle A (s4–s9) + Welle B (s10/11/13/15/17/19/20/21/25) → alle auf `dirtyvader-iot`/`10.40.30.<octet>` verifiziert (SSID+IP+rssi). Mit den 6 Vorab-Migrierten (s14/16/18/22/23/24) = **22/26 auf VLAN30**; VLAN1-Sweep `.180-250` = **0 Shellys**. Restliche 4 saisonal ausgesteckte (s1/s2/s3/s12) inzwischen ebenfalls migriert (s.o.). - ✅ **GO durchgezogen 2026-07-13 (16 Shellys VLAN1→VLAN30, 0 Fehler):** Pilot s26 + Welle A (s4–s9) + Welle B (s10/11/13/15/17/19/20/21/25) → alle auf `dirtyvader-iot`/`10.40.30.<octet>` verifiziert (SSID+IP+rssi). Mit den 6 Vorab-Migrierten (s14/16/18/22/23/24) = **22/26 auf VLAN30**; VLAN1-Sweep `.180-250` = **0 Shellys**. Restliche 4 saisonal ausgesteckte (s1/s2/s3/s12) inzwischen ebenfalls migriert (s.o.).
- 🔧 **Anker-Verfahren (User-Idee, statt sta1-Backup):** je Gerät **vor** dem sta-Write die geräteeigene **AP-Funktion** aktiviert (`WiFi.SetConfig ap.enable` — Firmware verlangt WPA2, kein offenes AP → PSK=IOT-PSK, live/kein Reboot) = Notanker, falls VLAN30-Join scheitert (Gerät bleibt über `Shelly*-<mac>`-AP erreichbar, **kein physischer Reset**). Nach Verify alle 22 AP-Anker wieder `deanchor`t. Runner: `mcp/shelly/migrate.py` (`migrate`/`deanchor`/`auth`/`table`, Dry-Run default, stoppt bei erstem Fehler, reused `discover.py`). - 🔧 **Anker-Verfahren (User-Idee, statt sta1-Backup):** je Gerät **vor** dem sta-Write die geräteeigene **AP-Funktion** aktiviert (`WiFi.SetConfig ap.enable` — Firmware verlangt WPA2, kein offenes AP → PSK=IOT-PSK, live/kein Reboot) = Notanker, falls VLAN30-Join scheitert (Gerät bleibt über `Shelly*-<mac>`-AP erreichbar, **kein physischer Reset**). Nach Verify alle 22 AP-Anker wieder `deanchor`t. Runner: `mcp/shelly/migrate.py` (`migrate`/`deanchor`/`auth`/`table`, Dry-Run default, stoppt bei erstem Fehler, reused `discover.py`).
@@ -912,7 +993,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp19"></a> <a id="tp19"></a>
### TP19 — cudy-Teardown + VLAN1-Client-Retirement ▸ ✅ **GESCHLOSSEN (2026-08-18)** — `docs/tp19-vlan1-cudy-teardown-runbook.md` ### TP19 · Gitea #59 — cudy-Teardown + VLAN1-Client-Retirement — `docs/tp19-vlan1-cudy-teardown-runbook.md`
- ✅ **Abschluss-Sweep 2026-08-18, alles live gegengeprueft (reine Reads):** - ✅ **Abschluss-Sweep 2026-08-18, alles live gegengeprueft (reine Reads):**
- **VLAN1 ist leer:** `/ip arp` auf `bridge` = **0 Eintraege, 0 reachable**. Nicht Restverkehr, - **VLAN1 ist leer:** `/ip arp` auf `bridge` = **0 Eintraege, 0 reachable**. Nicht Restverkehr,
sondern gar nichts. Der Dual-DHCP-Race aus `project_vlan1_dhcp_incident` hat damit endgueltig sondern gar nichts. Der Dual-DHCP-Race aus `project_vlan1_dhcp_incident` hat damit endgueltig
@@ -1009,7 +1090,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
- **Teardown-Sequenz (angepasst an den Live-Stand):** VLAN1-Client-Sweep ist **schon fast leer** (nur sw1-Mgmt + pve-Interim reachable) → `defconf`/pool-v1 ist **bereits disabled** → cudy-Abbau **entfällt** (cudy1 umgenutzt statt abgebaut, cudy2 tot) → ✅ **SwitchBot + Twinkly erledigt (2026-07-16)** → **verbleibt:** Media-Check (B1.3), pve-`.41.100` abräumen (B1.5), Interim-Krücken bewerten (ap2 `frame-types=admit-only-vlan-tagged`, `st-main rrm/wnm`), Inventar/`ip-plan`/DNS `.41→Ziel` final. - **Teardown-Sequenz (angepasst an den Live-Stand):** VLAN1-Client-Sweep ist **schon fast leer** (nur sw1-Mgmt + pve-Interim reachable) → `defconf`/pool-v1 ist **bereits disabled** → cudy-Abbau **entfällt** (cudy1 umgenutzt statt abgebaut, cudy2 tot) → ✅ **SwitchBot + Twinkly erledigt (2026-07-16)** → **verbleibt:** Media-Check (B1.3), pve-`.41.100` abräumen (B1.5), Interim-Krücken bewerten (ap2 `frame-types=admit-only-vlan-tagged`, `st-main rrm/wnm`), Inventar/`ip-plan`/DNS `.41→Ziel` final.
- Abh.: **TP14 ✅** (Hosts raus aus VLAN1), **TP17 ✅** (Shellys raus). Verwandt: `project_vlan1_dhcp_incident` (cudy-Dual-DHCP-Leak), TP5 (Deny-by-default danach sauberer). SSoT hier + `docs/tp19-vlan1-cudy-teardown-runbook.md`. Folge: **TP19.1**. - Abh.: **TP14 ✅** (Hosts raus aus VLAN1), **TP17 ✅** (Shellys raus). Verwandt: `project_vlan1_dhcp_incident` (cudy-Dual-DHCP-Leak), TP5 (Deny-by-default danach sauberer). SSoT hier + `docs/tp19-vlan1-cudy-teardown-runbook.md`. Folge: **TP19.1**.
#### TP19.1 — VLAN99-MGMT-Cutover (sw1/sw2/ap2-Mgmt VLAN1 → VLAN99) ▸ ✅ ERLEDIGT (2026-08-14 nachgezogen) — alle lebenden Geräte auf VLAN99, die beiden Rest-Punkte gegenstandslos #### TP19.1 · Gitea #60 — VLAN99-MGMT-Cutover (sw1/sw2/ap2-Mgmt VLAN1 → VLAN99) — alle lebenden Geräte auf VLAN99, die beiden Rest-Punkte gegenstandslos
- ✅ **Abschluss-Nachtrag 2026-08-14 (Status-Korrektur, kein neuer Change):** der Header stand noch auf „ap2 vertagt, sw1 offen" (Stand 2026-07-16) — beides ist durch spätere TPs eingelöst bzw. gegenstandslos geworden, ohne dass es hier nachgetragen wurde. Ist-Stand: **core `.99.1` · branch `.99.2` · ap1 `.99.11` · ap2 `.99.12`** — jedes lebende Netzgerät hat sein Mgmt auf VLAN99. **sw1/sw2 existieren nicht mehr** (physisch ausgebaut, in Nautobot per TP22 Schritt 5 auf *Decommissioned*). Damit ist TP19.1 vollständig; offen bleibt nur der VLAN1-**Client**-Teil, und der gehört zu TP19 (Hauptteil), nicht hierher. - ✅ **Abschluss-Nachtrag 2026-08-14 (Status-Korrektur, kein neuer Change):** der Header stand noch auf „ap2 vertagt, sw1 offen" (Stand 2026-07-16) — beides ist durch spätere TPs eingelöst bzw. gegenstandslos geworden, ohne dass es hier nachgetragen wurde. Ist-Stand: **core `.99.1` · branch `.99.2` · ap1 `.99.11` · ap2 `.99.12`** — jedes lebende Netzgerät hat sein Mgmt auf VLAN99. **sw1/sw2 existieren nicht mehr** (physisch ausgebaut, in Nautobot per TP22 Schritt 5 auf *Decommissioned*). Damit ist TP19.1 vollständig; offen bleibt nur der VLAN1-**Client**-Teil, und der gehört zu TP19 (Hauptteil), nicht hierher.
- **Scope:** die einzigen Dinge, die TP19s Hauptteil bewusst in VLAN1 belässt — sw1 (`.41.11`), sw2, ap2-Mgmt (`.41.2`) — auf VLAN 99 (MGMT, `10.40.99.0/24`) umziehen. Erst danach ist VLAN1 wirklich **komplett** weg, nicht nur als Client-Netz. - **Scope:** die einzigen Dinge, die TP19s Hauptteil bewusst in VLAN1 belässt — sw1 (`.41.11`), sw2, ap2-Mgmt (`.41.2`) — auf VLAN 99 (MGMT, `10.40.99.0/24`) umziehen. Erst danach ist VLAN1 wirklich **komplett** weg, nicht nur als Client-Netz.
- **User-Entscheid (2026-07-16):** kein eigenständiges TP-Nummer — bewusst als **Unterpunkt von TP19** geführt („VLAN1 killen"), nicht als TP22 o.ä. - **User-Entscheid (2026-07-16):** kein eigenständiges TP-Nummer — bewusst als **Unterpunkt von TP19** geführt („VLAN1 killen"), nicht als TP22 o.ä.
@@ -1024,7 +1105,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp20"></a> <a id="tp20"></a>
### TP20 — Platform-Datenbank (Postgres Precious-Tier + MariaDB Tier-2) ▸ ✅ **Plattform live (2026-07-16)** — Nautobot-DB angelegt, vw-Migration (TP20.1) offen — Memory `project_mikrotik_commit_nautobot_gc` ### TP20 · Gitea #61 — Platform-Datenbank (Postgres Precious-Tier + MariaDB Tier-2) — Nautobot-DB angelegt, vw-Migration (TP20.1) offen — Memory `project_mikrotik_commit_nautobot_gc`
- ✅ **Platform-Postgres live (2026-07-16):** LXC 105 auf pve, `platform-pg.bf.lan` / `10.40.20.20`, Ubuntu 24.04 (native PG16-Paket, kein PGDG-Repo nötig — bewusste Abweichung von der sonstigen Debian/community-scripts-Konvention der übrigen pve-LXCs, aus PG16-Verfügbarkeitsgründen). Cluster mit `--locale en_US.UTF-8 -e UTF8` neu initialisiert (Erstinstallation lief in `C`/`SQL_ASCII`, für Nautobot ungeeignet, korrigiert). `wal_level=replica` gesetzt; **`archive_mode` bewusst noch NICHT an** — ohne pgBackRest/Garage-S3 (TP18) zum Prunen würde archivierte WAL unbegrenzt wachsen und die kleine 8GB-Disk füllen; wird nachgezogen sobald TP18 steht. Rolle+DB `nautobot` angelegt, `pg_hba.conf` erlaubt `scram-sha-256` aus `10.40.20.0/24`. End-to-End-Verbindungstest (Netzwerk, nicht nur localhost) erfolgreich. Creds in `postgres/.secrets` (gitignored, `chmod 600`). - ✅ **Platform-Postgres live (2026-07-16):** LXC 105 auf pve, `platform-pg.bf.lan` / `10.40.20.20`, Ubuntu 24.04 (native PG16-Paket, kein PGDG-Repo nötig — bewusste Abweichung von der sonstigen Debian/community-scripts-Konvention der übrigen pve-LXCs, aus PG16-Verfügbarkeitsgründen). Cluster mit `--locale en_US.UTF-8 -e UTF8` neu initialisiert (Erstinstallation lief in `C`/`SQL_ASCII`, für Nautobot ungeeignet, korrigiert). `wal_level=replica` gesetzt; **`archive_mode` bewusst noch NICHT an** — ohne pgBackRest/Garage-S3 (TP18) zum Prunen würde archivierte WAL unbegrenzt wachsen und die kleine 8GB-Disk füllen; wird nachgezogen sobald TP18 steht. Rolle+DB `nautobot` angelegt, `pg_hba.conf` erlaubt `scram-sha-256` aus `10.40.20.0/24`. End-to-End-Verbindungstest (Netzwerk, nicht nur localhost) erfolgreich. Creds in `postgres/.secrets` (gitignored, `chmod 600`).
- **Auslöser:** Mehrere Dienste brauchen zentrale DB (Nautobot-SoT [TP21], Vaultwarden, HA-Recorder). Grundsatzentscheid statt Nautobot-Insellösung — „wir skalieren schneller als wir schauen können" (User). - **Auslöser:** Mehrere Dienste brauchen zentrale DB (Nautobot-SoT [TP21], Vaultwarden, HA-Recorder). Grundsatzentscheid statt Nautobot-Insellösung — „wir skalieren schneller als wir schauen können" (User).
- ⚠️ **Abh.-Korrektur (User 2026-07-16): TP18 (Garage-S3) ist KEINE Vorbedingung mehr — „hat mit dem Netz nichts zu tun", transitiv aus der TP21-Kette entfernt.** pgBackRest→Garage-S3 (s.u.) wird **nachgezogen sobald Garage existiert**, blockiert aber nicht mehr den Start. `wal_level=replica`/`archive_mode` können mit einem Platzhalter/lokalen Archive-Ziel hochgezogen werden, produktives Off-Site-PITR kommt später. - ⚠️ **Abh.-Korrektur (User 2026-07-16): TP18 (Garage-S3) ist KEINE Vorbedingung mehr — „hat mit dem Netz nichts zu tun", transitiv aus der TP21-Kette entfernt.** pgBackRest→Garage-S3 (s.u.) wird **nachgezogen sobald Garage existiert**, blockiert aber nicht mehr den Start. `wal_level=replica`/`archive_mode` können mit einem Platzhalter/lokalen Archive-Ziel hochgezogen werden, produktives Off-Site-PITR kommt später.
@@ -1048,7 +1129,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp21"></a> <a id="tp21"></a>
### TP21 — Nautobot Golden Config (config-as-code Control-Plane) ▸ 🟢 **Plattform + Datenmodell + Templates live** ### TP21 · Gitea #62 — Nautobot Golden Config (config-as-code Control-Plane)
- 🔴 **BETRIEBSBEFUND 2026-08-18: der Stack hat sich zweimal in einer Nacht selbst umgebracht — Ursache - 🔴 **BETRIEBSBEFUND 2026-08-18: der Stack hat sich zweimal in einer Nacht selbst umgebracht — Ursache
war der Image-Healthcheck, nicht Celery.** `nautobot-worker` und `-scheduler` erben aus dem Image war der Image-Healthcheck, nicht Celery.** `nautobot-worker` und `-scheduler` erben aus dem Image
einen `HEALTHCHECK`, der alle **10 s** `nautobot-server health_check` startet — einen kompletten einen `HEALTHCHECK`, der alle **10 s** `nautobot-server health_check` startet — einen kompletten
@@ -1112,7 +1193,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp21-1"></a> <a id="tp21-1"></a>
### TP21.1 — Core-Cutover-Recovery (ap1 → crs326-core als L3-Router) ▸ ✅ NETZ WIEDER LIVE (2026-08-08); ~~Aufräum-Todos offen~~ → **alle fünf erledigt, letzter live gegengeprüft 2026-08-23** (s. Nachtrag am Ende) ### TP21.1 · Gitea #63 — Core-Cutover-Recovery (ap1 → crs326-core als L3-Router) — ~~Aufräum-Todos offen~~ → **alle fünf erledigt, letzter live gegengeprüft 2026-08-23** (s. Nachtrag am Ende)
> **Kern-Erkenntnis (wichtig gegen Doku-Verwirrung):** **`crs326-core` IST jetzt der produktive L3-Router.** Der Core-Cutover (TP15) wurde faktisch vollzogen — ap1 wurde plattgemacht/getrennt, core hat Routing/DHCP/DNS/NAT/WG-Hub übernommen. core-Mgmt = **`10.40.99.1`** (vlan99-SVI) bzw. same-subnet-Fallback **`10.40.10.1`** (RouterOS-Version 7.19.5 identifiziert das Gerät zweifelsfrei als CRS326, nicht als hAP/ap1). Die CLAUDE.md-Zeile „core grundkonfiguriert, NICHT produktiv" ist damit **überholt**. > **Kern-Erkenntnis (wichtig gegen Doku-Verwirrung):** **`crs326-core` IST jetzt der produktive L3-Router.** Der Core-Cutover (TP15) wurde faktisch vollzogen — ap1 wurde plattgemacht/getrennt, core hat Routing/DHCP/DNS/NAT/WG-Hub übernommen. core-Mgmt = **`10.40.99.1`** (vlan99-SVI) bzw. same-subnet-Fallback **`10.40.10.1`** (RouterOS-Version 7.19.5 identifiziert das Gerät zweifelsfrei als CRS326, nicht als hAP/ap1). Die CLAUDE.md-Zeile „core grundkonfiguriert, NICHT produktiv" ist damit **überholt**.
- **Verlauf:** Ausgangssymptom war ein L3-Ausfall-Eindruck (kein DHCP, „kein Routing nach .20.x"). Ursache = **unvollständiger Cutover**: core hatte L3-SVIs + Firewall, aber es fehlten DHCP-Server, WAN-Egress, WG-Hub und der DNS-Resolver-Upstream. ap1 (bisher alleiniges L3-Gerät) war bereits Werksreset + getrennt. - **Verlauf:** Ausgangssymptom war ein L3-Ausfall-Eindruck (kein DHCP, „kein Routing nach .20.x"). Ursache = **unvollständiger Cutover**: core hatte L3-SVIs + Firewall, aber es fehlten DHCP-Server, WAN-Egress, WG-Hub und der DNS-Resolver-Upstream. ap1 (bisher alleiniges L3-Gerät) war bereits Werksreset + getrennt.
- ✅ **DHCP wiederhergestellt** aus letztem ap1-Backup (`routeros/ap1-pre-core-cutover-20260806-073616.rsc`): 7 Server (`dhcp-v10…v99`) + 8 Networks + **54 statische Reservierungen** + Option `dns 1.1.1.1`. Reproduzierbar in `routeros/tp21-core-dhcp-from-ap1.rsc`. Legacy VLAN1 (pool-v1/defconf) + tote omada-inform-Option bewusst weggelassen. - ✅ **DHCP wiederhergestellt** aus letztem ap1-Backup (`routeros/ap1-pre-core-cutover-20260806-073616.rsc`): 7 Server (`dhcp-v10…v99`) + 8 Networks + **54 statische Reservierungen** + Option `dns 1.1.1.1`. Reproduzierbar in `routeros/tp21-core-dhcp-from-ap1.rsc`. Legacy VLAN1 (pool-v1/defconf) + tote omada-inform-Option bewusst weggelassen.
@@ -1153,7 +1234,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp22"></a> <a id="tp22"></a>
### TP22 — Device-Adoption + Golden-Config Compliance-Push (RouterOS-Fleet) ▸ ✅ **DURCH (2026-08-13)**, Compliance-Backlog **abgearbeitet 2026-08-16 → 68/68, 0 Drifts** — alle 5 Schritte erledigt (Schritt 4 per Entscheid „keine Secret-Injection"); aus TP21 ausgelagert 2026-08-08 ### TP22 · Gitea #64 — Device-Adoption + Golden-Config Compliance-Push (RouterOS-Fleet) — Compliance-Backlog **abgearbeitet 2026-08-16 → 68/68, 0 Drifts** — alle 5 Schritte erledigt (Schritt 4 per Entscheid „keine Secret-Injection"); aus TP21 ausgelagert 2026-08-08
- **Ziel:** die 4 Live-Geräte (crs326-core, crs326-branch, ap1, ap2) real unter **Nautobot Golden Config** nehmen — Backup → Compliance/Drift → intended → **Push (`enable_deploy`)**. Damit greift der Governance-Entscheid „Config exklusiv über Nautobot" operativ. Macht die aktuellen `routeros/.secrets/*.rsc`-Handoffs mittelfristig überflüssig. - **Ziel:** die 4 Live-Geräte (crs326-core, crs326-branch, ap1, ap2) real unter **Nautobot Golden Config** nehmen — Backup → Compliance/Drift → intended → **Push (`enable_deploy`)**. Damit greift der Governance-Entscheid „Config exklusiv über Nautobot" operativ. Macht die aktuellen `routeros/.secrets/*.rsc`-Handoffs mittelfristig überflüssig.
- **Stand aus TP21:** Plattform + Datenmodell + Git-Template + intended-config für core stehen; ap1/ap2 als reine APs zurückgeschrieben (`nautobot/ingest-ap-rebuild.py`, 2026-08-08). `enable_plan`/`enable_deploy` in `nautobot_config.py` noch **bewusst aus** (pull/read-only). - **Stand aus TP21:** Plattform + Datenmodell + Git-Template + intended-config für core stehen; ap1/ap2 als reine APs zurückgeschrieben (`nautobot/ingest-ap-rebuild.py`, 2026-08-08). `enable_plan`/`enable_deploy` in `nautobot_config.py` noch **bewusst aus** (pull/read-only).
- ✅ **Schritt 0 DURCH (2026-08-13) — Backup-Scrubbing + Repo-Neuanlage.** Vorgezogen aus Schritt 4, weil ohne Scrubbing jedes weitere Backup Secrets geleakt hätte. - ✅ **Schritt 0 DURCH (2026-08-13) — Backup-Scrubbing + Repo-Neuanlage.** Vorgezogen aus Schritt 4, weil ohne Scrubbing jedes weitere Backup Secrets geleakt hätte.
@@ -1234,7 +1315,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp26"></a> <a id="tp26"></a>
### TP26 — `/ip firewall filter` unter Drift-Kontrolle ▸ ✅ **DURCH (2026-08-17)** ### TP26 · Gitea #65 — `/ip firewall filter` unter Drift-Kontrolle
> **Nachgetragen 2026-08-18.** TP26 existierte bis dahin nur als zwei Commits und eine `.rsc` — ohne > **Nachgetragen 2026-08-18.** TP26 existierte bis dahin nur als zwei Commits und eine `.rsc` — ohne
> Abschnitt in dieser Datei. Genau die Lücke, die eine frische Session nicht finden kann. > Abschnitt in dieser Datei. Genau die Lücke, die eine frische Session nicht finden kann.
@@ -1255,7 +1336,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp27"></a> <a id="tp27"></a>
### TP27 — RouterBOOT-Firmware auf 7.24 nachziehen ▸ ✅ **DURCH (2026-08-17/18)** ### TP27 · Gitea #66 — RouterBOOT-Firmware auf 7.24 nachziehen
> **Nachgetragen 2026-08-18.** Auch TP27 hatte keinen Abschnitt — die Arbeit steckt in Christophs Commit > **Nachgetragen 2026-08-18.** Auch TP27 hatte keinen Abschnitt — die Arbeit steckt in Christophs Commit
> `b93514a` plus `routeros/tp27-routerboot-firmware-724.rsc` und > `b93514a` plus `routeros/tp27-routerboot-firmware-724.rsc` und
> `docs/routerboot-firmware-klickanleitung.md`. > `docs/routerboot-firmware-klickanleitung.md`.
@@ -1274,17 +1355,228 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
<a id="tp28"></a> <a id="tp28"></a>
### TP28 — WLAN über CAPsMAN auf `crs326-core` ▸ ↩️ **WIEDER GEÖFFNET 2026-08-30** ### TP28 · Gitea #18 — WLAN über CAPsMAN auf `crs326-core`
> Der Volltext steht seit dem 2026-08-30 wieder in > ✅ **Abgeschlossen 2026-10-04.** ap1 Sideboard · ap2 Technik UG · ap3 Technik OG (Platzierung
> [`teilprojekte.md` → TP28](teilprojekte.md#tp28), weil mit **ap3** (cAP ax) ein dritter > final). Kanalplan committed, `wifi-cap`-Rest auf dem Core bereinigt, Compliance 29/29. Der
> Access-Point dazukommt. **Ein archiviertes TP ist ein geschlossenes Ticket — es darf wieder > ap3-Nachtrag unten ist damit erledigt; die zwei 🔜-Reste sind Cross-TP (TP25 ✅ 09-15, TP30).
> geöffnet werden** (Christoph-Direktive 2026-08-30); der Abschnitt wandert zurück, nicht nur
> ein Nachtrag. Hier bleibt dieser Zeiger stehen, damit alte Links nicht ins Leere gehen.
> **Der CAPsMAN-Kern ist und bleibt ✅ KOMPLETT (2026-08-18)** — der Abschnitt darunter ist
> unverändert der Archivtext inkl. Post-Mortem. Wieder geöffnet wurde das TP am 2026-08-30,
> weil mit **ap3** ein dritter CAP dazukommt; das ist dieselbe Baustelle, kein neues TP
> (Christoph-Direktive: *ein archiviertes TP ist ein geschlossenes Ticket und darf wieder
> geöffnet werden*). **Der offene Teil steht am Ende des Abschnitts unter „Nachtrag ap3".**
- 🔴🔴 **POST-MORTEM 2026-08-18 (gemeldet von Christoph: „die -iot-geräte sind offline … connect möglich, aber kein dhcp"):**
Seit dem Cutover hingen **alle Slave-VAPs in gar keiner Bridge**. `/interface wifi cap slaves-datapath`
war auf beiden APs leer — und genau dieses Feld ist das Einzige, was die vom Manager erzeugten
Slave-VAPs lokal in die Bridge haengt. Der Manager schickt `datapath.bridge=bridge` zwar mit,
**der CAP uebernimmt davon nur die `vlan-id`.** Betroffen: `dirtyvader-iot`, `-media`, `-untrusted`,
`-guest`. Nicht betroffen: `dirtyvader-main` — dessen Master-Radios `wifi1`/`wifi2` sind
**statische** Bridge-Ports und liefen deshalb weiter. Genau das machte den Ausfall unsichtbar.
- **Fix (live 2026-08-18 auf ap1 + ap2, seither Soll im Template):**
`/interface wifi datapath add name=dp-caps-slaves bridge=bridge` +
`/interface wifi cap set slaves-datapath=dp-caps-slaves`. Das Objekt traegt **bewusst keine
`vlan-id`** — die liefert der Manager pro VAP. Danach erscheinen die VAPs als **dynamische
Bridge-Ports mit `pvid`=30/40/50/90**, und DHCP laeuft sofort wieder.
- 🔴 **Die Diagnose-Lehre: „Client assoziiert" beweist gar nichts.** Am Manager standen 25 IOT-
Registrations — das WLAN sah perfekt aus. Erst zwei Reads trennten die Faelle:
`torch` auf `vlan30` am Core (**leer**) und der TP5-Drop-Log (**kein einziger DHCP-Eintrag**).
Beides leer heisst: es wird nichts verworfen, es **kommt** nichts. Damit war die Firewall
— der naheliegende Verdaechtige nach der Haertung derselben Nacht — in zwei Minuten entlastet.
- ⚠️ **Die Abnahme „✅ Ohne jeden Aussetzer" weiter unten war falsch.** Sie zaehlte Registrations
(30 Clients, beide CAPs `Ok`, 7 VAPs je AP) — also genau die Metrik, die hier luegt. Keiner
dieser Werte beruehrt die Bridge. Ein Abnahmekriterium fuer WLAN-Umbauten ist ab jetzt eine
**frische DHCP-Lease pro SSID**, nicht die Registration-Table.
- **`routeros/tp28-ap-wifi-reste-abraeumen.rsc` hat den Ausfall nicht ausgeloest, aber zementiert:**
mit den geloeschten datapath-Objekten gab es lokal nichts mehr, worauf `slaves-datapath` haette
zeigen koennen. Der Ein-Zeilen-Fix war damit weg. Nachtrag steht im Skript.
- ✅ **Reboot-Abnahme an ap2 (2026-08-18), drei Fragen auf einmal beantwortet:** `slaves-datapath`
ueberlebt den Neustart (statisches Objekt, steht im `/export`), der CAP **findet seinen Manager
durch die gehaertete input-Kette wieder** (`current-caps-man-identity=crs326-core` — MNDP/CAPWAP
sind also korrekt offen, das war seit TP5 Etappe 4 nie durchgespielt), und die sieben Slave-VAPs
kommen von selbst als dynamische Bridge-Ports mit pvid 30/40/50/90 zurueck. AP nach **40 s**
wieder erreichbar. Abnahme nach neuer Regel = **frische Lease**: IOT-Client `10.40.30.8`
`last-seen=30s`, MAIN-Client `10.40.10.180` `last-seen=1m54s`.
- ⚠️ **Reboot per MikroMCP geht auf 7.24 nicht:** das `reboot`-Tool sendet `delay`, die REST-API
antwortet `unknown parameter delay`; `run_command` ist fuer `/system reboot*` per Deny-Policy
gesperrt. Weg bis das gefixt ist: `sshpass` + `ssh admin@<ap> "/system reboot"`.
- ✅ **Soll nachgezogen, Compliance wieder sauber: 92/92, 0 Drifts** (2026-08-18). Weg: Template in
`nautobot/mikrotik_routeros.j2` → `routeros/push-gc-template.py` (Secret-Guard) → GitRepository-Sync
→ Backup/Intended/Compliance. Ohne den Push meldet der Refresh die zwei neuen Zeilen als `extra`
auf beiden APs — das Jinja lebt im **separaten** Gitea-Repo `bf-golden-config`, ein Commit hier
allein erreicht Nautobot nicht.
- ⚠️ **Beim ersten Sync-Versuch: `redis.exceptions.ConnectionError: broken pipe`** auf dem
Nautobot-Stack. Ein simpler Retry ging durch; NAS-Load war unauffaellig (0,9). Der Job meldet das
als `FAILURE` — nicht mit einem inhaltlichen Fehler verwechseln.
- ✅ **Der offene GC-Rest ist erledigt (2026-08-18).** Die fuenf `wifi-*`-ComplianceFeatures haengen
jetzt am **Core**, der AP-Zweig des Templates ist auf eine einzige Zeile eingedampft
(`/interface wifi cap set discovery-interfaces=vlanXX enabled=yes`).
- 🔴 **Dabei aufgefallen: die APs trugen noch die KOMPLETTE Standalone-WLAN-Config** aus TP3 —
je 2 channel, 6 datapath, 6 security, 7 configuration, 1 steering. Der Cutover vom 08-17 hatte
sie nie abgeraeumt. Wirkungslos (der Manager liefert alles dynamisch), aber sie im Template als
Soll zu rendern hiesse, totes Zeug zur Absicht zu erklaeren — dasselbe Argument wie bei den
defconf-Firewallregeln in TP26. Abgeraeumt mit `routeros/tp28-ap-wifi-reste-abraeumen.rsc`.
- ✅ **Ohne jeden Aussetzer:** vorher/nachher 30 Clients, beide CAPs `Ok`, 7 dynamische VAPs je AP.
Die Sorge vor einem Radio-Re-Apply (TP25/TP3.3: „wirft jeden Client kurz raus") hat sich nicht
bestaetigt — die dynamischen VAPs tragen ihre Werte **inline** (`datapath.vlan-id=…`) und
referenzieren kein lokales Objekt; nur `wifi1`/`wifi2` hielten noch einen toten
`configuration=`-Verweis, der beim Loeschen folgenlos blieb.
- 🔴 **`[find !dynamic]` ist auf diesen Sektionen ein SYNTAXFEHLER**, nicht bloss wirkungslos:
`/interface wifi configuration` & Co. haben gar keine `dynamic`-Eigenschaft (die gibt es nur auf
`/interface wifi` selbst). Der erste Anlauf lief komplett in die `on-error`-Zweige, aenderte
**nichts** und meldete trotzdem „Plan Completed". Wieder: nur die Gegenpruefung am Geraet zaehlt.
- ✅ **Drei neue ComplianceRules schliessen die letzten WLAN-Luecken:** `wifi-capsman`
(der Manager-Schalter), `wifi-provisioning` (die vier Regeln je `radio-mac` — sie entscheiden,
welcher AP welche Kanaele und Slave-VAPs bekommt) und `wifi-cap` (die CAP-Seite; faellt
`discovery-interfaces` weg, findet der AP seinen Manager nach dem naechsten Reboot nicht mehr).
**Ergebnis: 92/92 compliant, 0 Drifts.**
- ⚠️ **`wifi-security` ist gruen, aber NICHT deploybar.** Die Passphrasen stehen als `<<REDACTED>>`
im Template (User-Entscheid 2026-08-18: alle fuenf Features umhaengen, Platzhalter in Kauf nehmen).
Der gruene Haken beweist nur, dass Soll und Ist dieselbe **Maske** tragen — nicht, dass die
Passphrasen stimmen, und schon gar nicht, dass die Sektion ausgerollt werden koennte. Das steht
als Warnung im Template. Wer sie aendert, liest den Ist-Stand per REST zurueck
(`GET http://10.40.99.1/rest/interface/wifi/security`) — **nicht** aus `wlan-psks.env` oder den
drei Bring-up-Dateien, die voneinander abgeschrieben sind.
- ⚠️ **Die frueher hier notierte „bisher fehlende Rule auf `/interface wifi`" ist gegenstandslos** —
wie vorhergesagt: ein CAP hat keine Inline-Overrides mehr, die driften koennten.
> **Methodenwechsel auf Christophs Anstoß:** *„ich möchte die methode in frage stellen … eine sot ist zwar nett,
> aber wenn sie uns das leben zur hölle macht, verzichte ich auf diese governance."* Der Entscheid vom
> 2026-07-22, CAPsMAN abzuschaffen, ist damit **revidiert**.
- ✅ **Warum es diesmal trägt:** 2026-07-22 war **ap1 Manager UND AP** — und ein Manager kann seine eigenen
Radios nicht per CAPsMAN provisionieren (MikroTik-bestätigt). Genau diese Asymmetrie war die Ursache von
TP3.1. Mit dem **Core** als Manager sind beide APs symmetrisch. **Der Core braucht dafür kein `wifi-qcom`:**
das `capsman`-Menü steckt im Basis-`routeros`, die 3,35 MB freier Flash sind irrelevant. Die zweite
Begründung von damals („WLAN instabil") war ohnehin widerlegt — standalone blieb es instabil, die Ursachen
waren die Funkparameter aus TP25.
- ✅ **Ist-Stand:** Manager auf `vlan99`, 4 Kanäle · 6 Datapaths · 6 Security · 2 Steering · 9 Configurations ·
4 Provisioning-Regeln je `radio-mac`. Beide `remote-cap` **`Ok`**, je 7 dynamische VAPs. Kanäle exakt wie in
TP25 gemessen (ap1 `5180/ax/Ce/I`+`2412/ax`, ap2 `5220/ax/Ce/I`+`2462/ax`). **34 Clients** (main 3, iot 27,
media 4), Roaming im Manager-Log belegt. **`ft-over-ds=yes`** ist jetzt aktiv (unter CAPsMAN erstmals
verfügbar), `-guest` hat eigenes `st-guest` mit **`neighbor-group=bf-guest`** — nicht `bf-main` mitbenutzen,
sonst meldet 802.11k Gast-Clients BSSIDs einer SSID, auf die sie nicht assoziieren können.
- 📌 **Local-Forwarding ist keine Einstellung mehr:** im neuen `/interface/wifi` gibt es **kein** zentrales
Forwarding („There is no CAPsMAN-forwarding in WiFi (RouterOS 7.0+)"). Die `datapath`-Property existiert
deshalb nicht — `monitor` bestätigt `traffic processing on CAP`.
- 🔴 **DIE MESSFALLE DIESES TPs — unter CAPsMAN führt der MANAGER die Tabellen, nicht der CAP.**
`/interface/wifi/registration-table` und die `wireless`-Logs sind auf ap1/ap2 **leer**, obwohl das WLAN
einwandfrei läuft. Ich habe daraus zweimal „WLAN ist unten" geschlossen, während Christophs Geräte mit
vollem Ausschlag verbunden waren und ein Speedtest gut lief. **Immer am Manager zählen.**
- 🔴 **DIE ZWEITE FALLE — drei übereinstimmende Dateien sind nicht drei Belege.** `wlan-psks.env` wich für
iot/untrusted/infra von `ap1-prov-base.rsc`, `ap2-prov-base.rsc` und `ap1-standalone.rsc` ab. Ich habe die
drei als Bestätigung gelesen und die env-Datei „korrigiert" — **~20 Shellys waren 20 Minuten aus dem Netz.**
Die drei Dateien stammen alle aus dem Bring-up vom 2026-08-08, sind also **eine Quelle in drei Kopien**.
✅ **Auflösung:** die Passphrasen sind am Gerät per REST **lesbar**
(`GET http://10.40.99.1/rest/interface/wifi/security`) — dort ausgelesen ergab sich exakt der env-Wert.
`wlan-psks.env` ist neu aus dem Live-Stand befüllt und trägt jetzt eine Kopf-Warnung; die drei Baselines
sind seit dem Cutover ohnehin nicht mehr deploybar und wurden entschärft (Passphrasen raus, HISTORISCH).
- ✅ **~~OFFEN — das GC-Modell beschreibt weiter Standalone-APs~~ — ERLEDIGT am 2026-08-18.** Die
`wifi-*`-Features haengen jetzt am Core, das AP-Template ist auf die `cap`-Zeile eingedampft, und die
toten Standalone-Objekte sind von beiden APs abgeraeumt. Details im ersten Block dieses TPs.
Die hier genannte Zahl `76/76` ist damit doppelt ueberholt (aktuell 92/92 bei 23 Rules).
*(Der Absatz stand bis 2026-08-19 unveraendert als „OFFEN" hier, obwohl der Kopf des TPs schon
„KOMPLETT" sagte — ein Selbstwiderspruch im selben Abschnitt, ueber den prompt die naechste Session
gestolpert ist. Merkregel fuer kuenftige Statuswechsel: **nicht nur den Header umschreiben, sondern
die Punkte darunter mitziehen.**)*
- 📌 **Nebenbefund:** HAs `mikrotik`-Integration pollt alle 10 s und schreibt zwei Log-Zeilen pro Poll auf jedes
Gerät. Die AP-Logs sind dadurch für Diagnose praktisch unbrauchbar (`get_log` ohne `topics`-Filter liefert
nur `admin logged in/out via api`).
- **Abh.:** TP25 ✅ (die Funkparameter sind übernommen), TP15/TP21.1 ✅ (Core als L3-Router = Manager-Standort).
Verwandt: **TP3** (dessen CAPsMAN-Ära hier revidiert wird), **TP22** (GC-Scope).
#### ↩️ Nachtrag ap3 — dritter Access-Point (cAP ax), offen seit 2026-08-30
**Gerät:** MikroTik **cAP ax** (`cAPGi-5HaxD2HaxD`), Werkszustand, Uplink an **`crs326-core ether21`**.
**Mgmt-IP `10.40.99.13/24`** (TP32: `.10–.29` = physische Hosts; ap1 `.11`, ap2 `.12`), statisch am
Gerät wie ap1/ap2 — **keine** DHCP-Reservierung.
**Warum überhaupt:** ap3 soll **ap1 in Technik UG ablösen**, ap1 wandert danach ans Sideboard in die
gemessene Schwäche (Schlafzimmer/Bad/Küche). Das ist exakt **TP25 Variante 3** („cAP ax an die
bisherige Stelle, der verdrängte hAP ax² wandert in den Schatten").
**Kanalplan = der aus TP25 (2026-08-24 gerechnet, DFS von Christoph genehmigt) — nicht neu erfunden:**
| AP | 5 GHz | Band | DFS | 2,4 GHz | wann |
|---|---|---|---|---|---|
| **ap3** | `5180/Ce` (36+40) | A | nein | **2437 (ch6)** | **jetzt** |
| ap2 | `5220/Ce` heute → **`5300/Ce`** (60+64) | A → B | → ja | 2462 (ch11) | beim ap1-Umzug |
| ap1 | — (**Radios aus**) → **`5260/Ce`** (52+56) | → B | → ja | 2412 (ch1) | beim ap1-Umzug |
| — | `5220/Ce` **Ausweichspur frei halten** | A | — | — | — |
🔴 **ap1s Radios gehen AUS, bevor ap3s angehen** (User-Vorgabe 2026-08-30): beide stehen bis zum
Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört. Das ist ein
**Interim**, kein Endzustand — ap1 kommt auf `5260` zurück.
> 📌 **Erratum 2026-08-30:** der Port ist **`ether21`**, nicht `ether22` (erste Annahme aus der
> Fragerunde). Der Befund lag die ganze Zeit in den Daten: in der dynamischen `pvid`-VLAN1-Zeile
> steht `current-untagged=…,ether21` — `ether22` taucht dort nicht auf, weil es keinen Link hat.
> **Merkregel: bevor man einen Port konfiguriert, den behaupteten Port gegen `link-ok` + die
> Bridge-Host-Tabelle stellen.** `/interface bridge host print where interface=etherN` nennt die
> MAC des Gegenübers — hier `D0:EA:11:FF:16:9D`, exakt die `E01` vom ap3-Label.
##### Stand 2026-08-30 — ✅ DURCH
- ✅ **`core ether21` ist Trunk** (10/20/30/40/50/90/99 tagged, Muster `ether15`), GC-Template
nachgezogen; AP-Zweig rendert Bridge-Ports aus **`ap.extra_ports`** statt hartkodiert.
- ✅ **Zugang über `ap3 ether2` → `core ether22`** (Patchkabel). Über `ether1` war ap3
grundsätzlich nicht administrierbar, s. Gotchas unten. Loop-Schutz: `ether21` war während
Phase 1 abgeschaltet, danach im selben Befehl getauscht (`ether22` aus, `ether21` an).
- ✅ **Bring-up in zwei Phasen**: Phase 1 (`ether1` aus der WAN-Liste, `10.40.99.13/24` auf
`vlan99`, Flotten-PW) über das Kabel, Phase 2 (Werks-WLAN weg, CAP-Modus,
`slaves-datapath`, `vlan-filtering=yes`) über `vlan99`.
- ✅ **RouterOS 7.20.8 → 7.24.1 + RouterBOOT 7.24.1** (zwei Reboots, TP27-Kriterium
`current-firmware = upgrade-firmware`). 🆕 **Die Angleichung der übrigen vier ist am
2026-08-31 erfolgt** (Christoph, WebFig) — die Flotte ist einheitlich, TP27 erledigt.
- ✅ **ap1 funkstill** (Provisioning-Regeln entfernt + `remote-cap provision`), gegengeprüft
**am CAP**: nur noch `wifi1`/`wifi2` mit `running=false`, keine dynamischen VAPs.
- ✅ **ap3 funkt**: `cap-wifi5` = **`5180/ax/Ce/I`**, `cap-wifi6` = **`2437/ax`**, voller
VAP-Satz (2 Master + 7 Slaves), alle `running=true`.
- ✅ **Datapath bewiesen**: 20 IOT-Clients auf ap3, Ping über VLAN30 mit **0 % Loss**.
*(Bewusst nicht über die Registration-Table abgenommen — die zeigte im 14-h-Ausfall
25 zufriedene Clients. Bei statisch reservierten Shellys erzeugt ein Roam keine neue
DHCP-Lease, deshalb ist hier Datenverkehr das belastbare Signal.)*
- ✅ **Nautobot**: DeviceType `cAP ax`, Device `ap3` mit 16 Interfaces, `primary_ip4`,
`secrets_group`, ConfigContext (`5g=ch36 2g=ch6 extra_ports=[]`).
Nebenkorrektur: ap1 stand auf „Wohnzimmer Sideboard", steht aber in **Technik UG**.
- ✅ **Compliance 130/130, 0 Drifts über 5 Geräte.** Die drei Werksreste, die der erste
Lauf mit ap3 gemeldet hat (defconf-DNS, 11 defconf-Firewallregeln, fehlende
`/ip service`-Härtung), sind mit `routeros/tp28-ap3-werksreste.rsc` abgeräumt.
- 🔜 **Offen:** TP25-Messrunde (`bash docs/tp25-runde.sh ap3-bringup`) — braucht einen
Rundgang. Leitfrage: deckt ap3 allein ab, was ap1 abgedeckt hat?
- 🔜 **Offen:** Passwort-Rotation (TP30) — das gemeinsame Admin-PW ist in dieser Session
verbrannt; ap3 trägt es bereits mit, hängt also in der Rotation mit drin.
##### Was am cAP ax anders ist als am hAP ax² — und was daran wehtut
- 🔴 **Nur ZWEI Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out) — kein `ether3/4/5`. Der
GC-Template-AP-Zweig hatte die **hartkodiert**; das war die stille Annahme *„AP == hAP ax²"*.
Deshalb `ap.extra_ports` im ConfigContext (Default = `["ether3","ether4","ether5"]`, ap3 = `[]`).
**Merkregel: eine Literal-Liste im Template ist so lange harmlos, bis die Flotte heterogen wird.**
- **`crs326-core` hat kein PoE-out** (`/interface ethernet poe` existiert dort nicht). Der cAP ax
bringt aber **Netzteil *und* Gigabit-PoE-Injector** mit (Produktseite 2026-08-30) — die TP25-Notiz
„kein Netzteil im Lieferumfang üblich" trifft auf dieses Modell **nicht** zu.
- `ether2` bleibt im Bring-up als **Lifeline** im Bridge (untagged VLAN1) und fliegt erst raus,
wenn ap3 über `10.40.99.13` steht — Zielzustand = wie ap2: `ether2` **kein** Bridge-Port
(VLAN1 bleibt leer, TP19 ist geschlossen).
- **Radio-MACs nicht raten:** `/interface wifi radio print` **am Manager** listet CAP-Identity +
Radio-MAC ohne jede Passphrase. Das ist der richtige Read — **nicht** `list_wifi_interfaces`,
das gibt `security.passphrase` *aller* Profile im Klartext zurück (Leak 2026-08-24).
##### Auflagen aus TP25 für den ap1-Umzug (nicht vergessen)
1. **DFS-Channel-Objekte brauchen eine Frequenz-LISTE** (z.B. `frequency=5260,5280,5220`). Mit
genau einem Wert ist eine Radar-Erkennung ein **SPOF**: das Radio muss den Kanal verlassen und
hat keinen zweiten → 5 GHz dieses APs bleibt unten.
2. **Vorher die *erreichte* `tx-power` messen** (`/interface wifi monitor`), ap2 testweise auf
`5260` gegen die heutigen 18 dBm auf `5220`. Fund B war genau das Muster: 18 dBm gesetzt,
9 dBm gesendet.
3. Uplink für ap1 danach: **`core ether3`** (`no-link`, frei) — vorher dort die Altlast abräumen,
der Port ist untagged-Member von VLAN40 bei `pvid=1`.
<a id="tp-r"></a> <a id="tp-r"></a>
### TP-R — Router-Capabilities-Sweep ▸ FERTIG (`docs/research/routeros-capabilities.md`) ### TP-R · Gitea #67 — Router-Capabilities-Sweep — `docs/research/routeros-capabilities.md`)
- **Leitsatz: nativ vor Container, NAS vor Router** (128MB-Flash-Limit). - **Leitsatz: nativ vor Container, NAS vor Router** (128MB-Flash-Limit).
- **Sofort nativ (~0 RAM):** RoMON · Netwatch · Scheduler + Auto-Backup/Mail · SNMP→externes Monitoring · **mDNS-Repeater** · spectral-scan · **802.11r/k/v (Fast-Roaming statt Mesh)** *(die Sweep-Fassung nannte hier CAPsMAN — das ist seit 2026-08-08 abgeschafft; 11r/k/v läuft standalone weiter, s. TP3/TP3.3)* · Back-to-Home/WireGuard. - **Sofort nativ (~0 RAM):** RoMON · Netwatch · Scheduler + Auto-Backup/Mail · SNMP→externes Monitoring · **mDNS-Repeater** · spectral-scan · **802.11r/k/v (Fast-Roaming statt Mesh)** *(die Sweep-Fassung nannte hier CAPsMAN — das ist seit 2026-08-08 abgeschafft; 11r/k/v läuft standalone weiter, s. TP3/TP3.3)* · Back-to-Home/WireGuard.
- **Auf NAS:** FreeRADIUS(+LDAP) · NPM · icinga/Grafana · CF-Tunnel. - **Auf NAS:** FreeRADIUS(+LDAP) · NPM · icinga/Grafana · CF-Tunnel.
@@ -1305,7 +1597,7 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung".
> Handoff-Punkte aufgelöst, Gitea #17 closed. Die 394-Zeilen-Herleitung (Messchronik A–G) steht > Handoff-Punkte aufgelöst, Gitea #17 closed. Die 394-Zeilen-Herleitung (Messchronik A–G) steht
> weiter unten unter [TP25 — Langfassung](#tp25-lang). > weiter unten unter [TP25 — Langfassung](#tp25-lang).
### TP25 — WLAN-Stabilisierung ▸ ✅ **FERTIG: Platzierung final (ap1 Sideboard · ap2 UG · ap3 OG), alle Handoff-Punkte aufgelöst (2026-09-15, [Steering-Handoff](handoff-20260915-tp25-steering-threshold-und-terrasse.md))** — ap2-UNTERPUNKT ✅ WIDERLEGT 2026-09-14 (In-situ, [Handoff](handoff-20260914-tp25-ap2-insitu-terrassse.md)): ap2 sendet auf 2 m mit −49…−51 dBm, stabil, 0 Roams — Hardware sauber, kein Tausch begründet. Terrasse = 5-GHz-Zellkante (−76…−81 vs. 2,4 GHz −65…−73), Thrash nur in der Band-Überlappungszone −70…−77. Hebel „Access-List-Band-Boden" in der Machbarkeitsprüfung GESCHEITERT (kein `band`-Property, cap-wifiN-Rejoin-Falle) — s. Block unten** — **FUND E ✅ GESCHLOSSEN 2026-09-02: die Abschlussmessung ist ruhig (4 reauth in 8,6 h, 0 Ausfälle, Verlauf 218→40→4) ⇒ `7.24.1` (TP27) war die Ursache, nicht `ft-over-ds`. Hebel 1 widerlegt+zurückgebaut, Hebel 2–4 entfallen, kein Geräte-Write** ([Handoff](handoff-20260902-tp25-fund-e-messung.md)) · Kern erledigt: 🟢 **(2026-08-22) Dead Spot Terrasse repariert** (43 % → 9 % Verlust, 20,4 s → 1,0 s). **Fund F ✅ aufgelöst (08-24, geräteseitig — kein Netzfehler)**, **5-GHz-Umfeldscan ✅ (08-24)**, Kanalplan für AP 3 ✅ gerechnet. Nicht mehr kritischer Pfad — Sammel-TP für Christophs Beobachtungen 1–6 ### TP25 · Gitea #17 — WLAN-Stabilisierung — ap2-UNTERPUNKT ✅ WIDERLEGT 2026-09-14 (In-situ, [Handoff](handoff-20260914-tp25-ap2-insitu-terrassse.md)): ap2 sendet auf 2 m mit −49…−51 dBm, stabil, 0 Roams — Hardware sauber, kein Tausch begründet. Terrasse = 5-GHz-Zellkante (−76…−81 vs. 2,4 GHz −65…−73), Thrash nur in der Band-Überlappungszone −70…−77. Hebel „Access-List-Band-Boden" in der Machbarkeitsprüfung GESCHEITERT (kein `band`-Property, cap-wifiN-Rejoin-Falle) — s. Block unten** — **FUND E ✅ GESCHLOSSEN 2026-09-02: die Abschlussmessung ist ruhig (4 reauth in 8,6 h, 0 Ausfälle, Verlauf 218→40→4) ⇒ `7.24.1` (TP27) war die Ursache, nicht `ft-over-ds`. Hebel 1 widerlegt+zurückgebaut, Hebel 2–4 entfallen, kein Geräte-Write** ([Handoff](handoff-20260902-tp25-fund-e-messung.md)) · Kern erledigt: 🟢 **(2026-08-22) Dead Spot Terrasse repariert** (43 % → 9 % Verlust, 20,4 s → 1,0 s). **Fund F ✅ aufgelöst (08-24, geräteseitig — kein Netzfehler)**, **5-GHz-Umfeldscan ✅ (08-24)**, Kanalplan für AP 3 ✅ gerechnet. Nicht mehr kritischer Pfad — Sammel-TP für Christophs Beobachtungen 1–6
> 📚 **Kondensiert am 2026-08-23 (Doku-Session, Punkt F).** Dieser Abschnitt war mit 394 Zeilen > 📚 **Kondensiert am 2026-08-23 (Doku-Session, Punkt F).** Dieser Abschnitt war mit 394 Zeilen
> 18 % der Datei. Die **Langfassung steht vollständig und unverändert** im Archiv: > 18 % der Datei. Die **Langfassung steht vollständig und unverändert** im Archiv:
> [`teilprojekte-archiv.md` → TP25 (Langfassung)](teilprojekte-archiv.md#tp25-lang) — Messchronik (A)–(G), > [`teilprojekte-archiv.md` → TP25 (Langfassung)](teilprojekte-archiv.md#tp25-lang) — Messchronik (A)–(G),
@@ -1827,7 +2119,7 @@ Handoff: [`handoff-20260901-tp25-ap2-verdacht.md`](handoff-20260901-tp25-ap2-ver
> Erklärungen — zweimal stand hier eine plausible Ursache, die die nächste Messung gekippt hat > Erklärungen — zweimal stand hier eine plausible Ursache, die die nächste Messung gekippt hat
> (ETSI-Band C bei Fund B; „ap1 ans Sideboard" bei (F)/(G)). Wer TP25 fortsetzt, muss sehen können, > (ETSI-Band C bei Fund B; „ap1 ans Sideboard" bei (F)/(G)). Wer TP25 fortsetzt, muss sehen können,
> welche Wege schon abgelaufen sind. > welche Wege schon abgelaufen sind.
### TP25 — WLAN-Stabilisierung ▸ 🟢 **KERN ERLEDIGT (2026-08-22): Dead Spot Terrasse repariert** (43 % → 9 % Verlust, 20,4 s → 1,0 s). Offen nur noch **Fund F** (5:05-Takt IoT) und die Entscheidung **5 GHz auf 20 MHz**. Nicht mehr kritischer Pfad — Sammel-TP für Christophs Beobachtungen 1–6 ### TP25 · Gitea #17 — WLAN-Stabilisierung — 43 % → 9 % Verlust, 20,4 s → 1,0 s). Offen nur noch **Fund F** (5:05-Takt IoT) und die Entscheidung **5 GHz auf 20 MHz**. Nicht mehr kritischer Pfad — Sammel-TP für Christophs Beobachtungen 1–6
> 🆕 **Messung 2026-08-20/21: [`tp25-wlan-roaming-messung-20260820.md`](tp25-wlan-roaming-messung-20260820.md)** > 🆕 **Messung 2026-08-20/21: [`tp25-wlan-roaming-messung-20260820.md`](tp25-wlan-roaming-messung-20260820.md)**
> (Nebenprodukt der TP6-Messreihen, Core-Sampler im 2-s-Takt über 2,5 h, teils mit laufendem Videocall). > (Nebenprodukt der TP6-Messreihen, Core-Sampler im 2-s-Takt über 2,5 h, teils mit laufendem Videocall).
> Drei Befunde: > Drei Befunde:
@@ -2223,7 +2515,7 @@ Handoff: [`handoff-20260901-tp25-ap2-verdacht.md`](handoff-20260901-tp25-ap2-ver
<a id="tp39"></a> <a id="tp39"></a>
### TP39 — LXC-Hygiene auf pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup ▸ ✅ **ABGESCHLOSSEN (2026-09-01)** ### TP39 · Gitea #68 — LXC-Hygiene auf pve: Resolver-Altlasten, `networking.service`, DB-Reverse-Lookup
**Auslöser:** ein 10-Sekunden-Hänger auf `gitea`, gefunden am Rand des TP16.3-§5-Flips. Er hatte mit **Auslöser:** ein 10-Sekunden-Hänger auf `gitea`, gefunden am Rand des TP16.3-§5-Flips. Er hatte mit
dem Flip nichts zu tun, war aber nur deshalb sichtbar, weil zum ersten Mal jemand systematisch dem Flip nichts zu tun, war aber nur deshalb sichtbar, weil zum ersten Mal jemand systematisch
@@ -2302,7 +2594,7 @@ gemessen hat. Beim Beheben kam eine zweite, unabhängige Altlast zum Vorschein.
--- ---
### TP34 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) ▸ ❌ **VERWORFEN (2026-09-08)** ### TP34 · Gitea #24 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle)
> ➡️ **Fortsetzung 2026-09-22: `TP34.2 — Interne DNS-Namen als Daten`** (Gitea #73, Prosa in > ➡️ **Fortsetzung 2026-09-22: `TP34.2 — Interne DNS-Namen als Daten`** (Gitea #73, Prosa in
> `docs/teilprojekte.md` → `### TP34.2`). Dort lebt der *Daten → gerenderte Config*-Gedanke weiter; > `docs/teilprojekte.md` → `### TP34.2`). Dort lebt der *Daten → gerenderte Config*-Gedanke weiter;
> **verworfen bleibt nur das Caddyfile-Rendering** (dieser Abschnitt hier). > **verworfen bleibt nur das Caddyfile-Rendering** (dieser Abschnitt hier).
@@ -2324,7 +2616,7 @@ gemessen hat. Beim Beheben kam eine zweite, unabhängige Altlast zum Vorschein.
--- ---
<a id="tp44"></a> <a id="tp44"></a>
### TP44 — Cloudflare-Estate als Dokumentations-SSoT in Nautobot ▸ ✅ **DURCH (2026-09-10): Etappe 1 + Etappe 2 (Teil A IPAM-Prefixe, Teil B Live-Sync als täglicher Nautobot-Job)** ### TP44 · Gitea #33 — Cloudflare-Estate als Dokumentations-SSoT in Nautobot
> **Entscheid (User, 2026-09-02):** die volle CF-Estate (Tunnel + WARP-Connector + VPC-Service + > **Entscheid (User, 2026-09-02):** die volle CF-Estate (Tunnel + WARP-Connector + VPC-Service +
> Hyperdrive + Workers + alle Access-Apps) als strukturierte, abfragbare SSoT in Nautobot abbilden — > Hyperdrive + Workers + alle Access-Apps) als strukturierte, abfragbare SSoT in Nautobot abbilden —
> **etappenweise: manuell jetzt, Sync später.** Nautobot kann CF nicht steuern → „SSoT" heißt > **etappenweise: manuell jetzt, Sync später.** Nautobot kann CF nicht steuern → „SSoT" heißt
@@ -2368,7 +2660,7 @@ gemessen hat. Beim Beheben kam eine zweite, unabhängige Altlast zum Vorschein.
- **Beziehung zu TP37:** TP44 hat **TP37 Punkt 3** (CF-Connectoren) vorweggenommen — dieselbe native-Modell- - **Beziehung zu TP37:** TP44 hat **TP37 Punkt 3** (CF-Connectoren) vorweggenommen — dieselbe native-Modell-
Linie, die TP37 P1+2 (Substrat/Dienste) fortsetzt. Linie, die TP37 P1+2 (Substrat/Dienste) fortsetzt.
### TP41 — CF-Access-Reparatur + Zero-Trust-Konsolidierung ▸ ✅ **ABGESCHLOSSEN (2026-09-10)** — Auth-Kern seit 2026-09-01 komplett; **Gelis WARP-Client am 2026-09-10 via Android erfolgreich eingerichtet** (letzter Blocker weg, User-Bestätigung). Optionale Restpunkte (T6 Rate-Limit, geli-CGNAT-Policy-Cleanup) bewusst nicht gezogen; Keystore-Rotation läuft unter TP30 #7. ### TP41 · Gitea #30 — CF-Access-Reparatur + Zero-Trust-Konsolidierung — Auth-Kern seit 2026-09-01 komplett; **Gelis WARP-Client am 2026-09-10 via Android erfolgreich eingerichtet** (letzter Blocker weg, User-Bestätigung). Optionale Restpunkte (T6 Rate-Limit, geli-CGNAT-Policy-Cleanup) bewusst nicht gezogen; Keystore-Rotation läuft unter TP30 #7.
**Auslöser:** CF-Access zwang Christoph **und Geli** täglich zum OTP-Login (soll: 30 Tage); GitHub/Google **Auslöser:** CF-Access zwang Christoph **und Geli** täglich zum OTP-Login (soll: 30 Tage); GitHub/Google
„funktionieren nicht". Root-Causes: Team-Domain-Rename `bf4`→`emsenhuber` (Provider-Callback veraltet), „funktionieren nicht". Root-Causes: Team-Domain-Rename `bf4`→`emsenhuber` (Provider-Callback veraltet),
@@ -2426,14 +2718,14 @@ der Egress ohnehin Cloudflare (nicht diese IP) → der IP-Include wäre dann üb
**Gitea-Least-Privilege** = eigener künftiger TP (jedes Projekt repo-scoped Login, Admin nur Claude/Christoph). **Gitea-Least-Privilege** = eigener künftiger TP (jedes Projekt repo-scoped Login, Admin nur Claude/Christoph).
- Bezug **TP11** (Monitoring): eine Plattenfüllstand-Warnung hätte das Wochen früher gezeigt. - Bezug **TP11** (Monitoring): eine Plattenfüllstand-Warnung hätte das Wochen früher gezeigt.
### TP43 — dw-Standort-DNS-Uplink (site-spezifische Overrides) ▸ ♻️ **REOPENED 2026-09-11 → Volltext zurück in [`teilprojekte.md`](teilprojekte.md) (Abschnitt TP43)** ### TP43 · Gitea #32 — dw-Standort-DNS-Uplink (site-spezifische Overrides)
Kurz zwischen 2026-09-11 verworfen und am selben Tag reopened: die 420-Prämisse war gegenstandslos Kurz zwischen 2026-09-11 verworfen und am selben Tag reopened: die 420-Prämisse war gegenstandslos
(public CF-Worker, via WARP/TP41 gelöst), aber die **interne** BF-Namensauflösung über WG am dw blieb (public CF-Worker, via WARP/TP41 gelöst), aber die **interne** BF-Namensauflösung über WG am dw blieb
dabei offen (`portainer.bf.emsenhuber.net` am dw nicht erreichbar). Korrigierter Scope + verworfene dabei offen (`portainer.bf.emsenhuber.net` am dw nicht erreichbar). Korrigierter Scope + verworfene
420-Historie stehen jetzt aktiv in `teilprojekte.md`. Gitea-Issue #32 = `status:todo` · `thema:firewall-dns`. 420-Historie stehen jetzt aktiv in `teilprojekte.md`. Gitea-Issue #32 = `status:todo` · `thema:firewall-dns`.
### TP42 — Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net ▸ ✅ **DURCH (2026-09-02)** — ein interner Suffix `bf.emsenhuber.net`, `.bf.lan` raus, `.16` (adguard_bf1) abgebaut, Compliance 140/140. Offen nur der an **TP37** gekoppelte Nautobot-Doku-Teil. ### TP42 · Gitea #31 — Namens-Vereinheitlichung: ein interner Suffix bf.emsenhuber.net — ein interner Suffix `bf.emsenhuber.net`, `.bf.lan` raus, `.16` (adguard_bf1) abgebaut, Compliance 140/140. Offen nur der an **TP37** gekoppelte Nautobot-Doku-Teil.
**Auslöser (2026-09-01, TP41-Randbefund):** live gemessen liefern die realen Client-Resolver **Auslöser (2026-09-01, TP41-Randbefund):** live gemessen liefern die realen Client-Resolver
`10.40.20.36`/`.37` `*.emsenhuber.net → 10.40.20.40` (Caddy) — korrekt. Daneben existiert aber ein `10.40.20.36`/`.37` `*.emsenhuber.net → 10.40.20.40` (Caddy) — korrekt. Daneben existiert aber ein
@@ -2497,3 +2789,75 @@ Gegen die Geräte gemessen (MikroMCP + dig), nicht aus dem Handoff:
**Status:** ✅ **Namensraum vereinheitlicht (2026-09-02)** — ein interner Suffix `bf.emsenhuber.net`, **Status:** ✅ **Namensraum vereinheitlicht (2026-09-02)** — ein interner Suffix `bf.emsenhuber.net`,
`.bf.lan` raus, `.16` abgebaut, Compliance 140/140. Offen nur der TP37-gekoppelte Nautobot-Doku-Teil. `.bf.lan` raus, `.16` abgebaut, Compliance 140/140. Offen nur der TP37-gekoppelte Nautobot-Doku-Teil.
Handoff: [`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md). Handoff: [`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md).
---
<a id="tp47"></a>
### TP47 · Gitea #74 — Core egress via cf-mesh und WARP-Exit-Validierung
> **Christoph:** Validierung des Core-Egress-Weges via Cloudflare Mesh. Ausgangsprüfung gegen neutralen Host
> und Ziel `164.90.191.7` (DigitalOcean). Korrektur früherer Behauptung: **WARP war am Mac gar nicht
> verbunden** — `warp=off`, `ip=90.146.33.177` (Heim-IP). Die zuvor gemessene `104.28.207.39` stammte aus
> einem Test mit nur der `1.1.1.1/32`-Regel, daher über die Heim-IP (403). Beweis: Regel aktiviert
> `164.90.191.7/32 → egress-cannaleo` → Ziel erreicht mit `200` (168 KiB). Template-Änderung in
> `nautobot/mikrotik_routeros.j2` Abschnitt `[10b]` (commit `c600c62e`). Doku-Korrektur
> `docs/warp-egress-native-crs326.md` §6.3.
>
> **Bestandsaufnahme (2026-09-27):**
> - Ziel `164.90.191.7` (DigitalOcean-Origin, kein Cloudflare im Pfad) bereitet `403` bei Heim-IP
> (`90.146.33.177`, WARP aus), `200` über Core-Routing (`cf-mesh`, `egress-cannaleo`-Table).
> - `cf-mesh` erreicht den Origin mit `200` (verifiziert: `cdn-cgi/trace` → `ip=104.28.207.39`,
> `warp=on`, `colo=VIE`). Keine Dedicated Egress IP nötig.
> - User-Cave: kein Zugriffsmuster, das Heim-IP und diesen Ausgang gemeinsam gegen den Origin zeigt.
> - Template-Drift: 0 (Secret-Guard bestanden, Push `c600c62e`).
>
> #### Durchführung (in einem Durchgang)
> 1. TP anlegen, Prosa in `teilprojekte.md` schreiben, commit + push
> 2. Core-Routing-Objekte (`egress-cannaleo` Table, Route, Rule) bereits live auf `crs326-core`
> 3. Doku `docs/warp-egress-native-crs326.md` §6.3 korrigiert (früherer 403 = Messfehler)
> 4. Mineral: nicht abzudrehen (seit 21.09. still)
> 5. `10.101.0.254` taugt nicht als GW (100 % Loss; FM = Liwest; Cave erfüllt)
#### Bestandsaufnahme (live 2026-09-21) — es gibt eine Doku-SSoT, aber keinen Master
| Was | Wo | Stand |
|---|---|---|
| Tunnel, WARP-Connector, Workers, Access-Apps, Hyperdrive, VPC, teamnet-Routen | Nautobot `nautobot.cloud`, Job **`CF Estate Sync`** (`jobs/cf_estate_sync.py`) | ✅ 4 `CloudNetwork` · 16 `CloudService` live |
| **Ingress-Origin-Ziele** | **nirgends** | 🔴 `_ingress()` speichert nur `ingress_hostnames`, **nicht** das Origin |
| **DNS-Records** | **nirgends** | 🔴 nicht erfasst — live 24 Records / 8 proxied |
| **Access-Policies** | **nirgends** | 🔴 nur die Apps selbst |
| Richtung | CF → Nautobot | 🔴 **Pull-only** („CF bleibt operativer Master") |
| `docs/cf-tunnel-mapping.md` | Repo | §1–16 eingefrorene As-Built-Historie · §0 = Live-Zeiger (altert: nennt `bf-public` „2 conns", live **12** = 3 AP-Connectoren × 4) |
**Antwort auf „full setup/recovery from scratch": heute nicht möglich.** Es fehlt (a) der Push-Weg,
(b) Ingress-Origins, (c) DNS, (d) Access-Policies. Und ein frisch erzeugter Tunnel liefert **neue ID
und neues Secret** → das **Token-Handoff an die Connectoren** (ap1/ap2/ap3) ist **Teil der Recovery**,
kein Nebenschritt.
#### Umfang
Tunnel + Ingress **inkl. Origin-Ziele** + DNS-Records + Access-Apps/Policies vollständig nach Nautobot;
**Push-Renderer** (Dry-Run + Diff); **Recovery-Runbook** für den Aufbau von null.
🔴 **Vor dem ersten Schritt prüfen, nicht annehmen:** welches Nautobot-Objekt DNS-Records tragen kann
(Kern-`ipam` vs. eigenes Modell), und ob das Ingress-Origin als `extra_config`-Feld reicht oder ein
eigenes Modell braucht.
- **Abh.:** TP44 Etappe 1/2 ✅ (native `nautobot.cloud`-Modelle + Pull-Job stehen). **Abgrenzung:**
Tunnel-**GC/Namensgebung** läuft in TP45 Schicht D; **CF-Writes** aus Nautobot sind hier *das Ziel*,
in TP37 ausdrücklich ausgeschlossen — die Grenze verschiebt sich also bewusst mit diesem TP.
#### ✅ ABGESCHLOSSEN (2026-10-05) — Egress-Weg verifiziert
**Nachmessung vom Core aus** (`crs326-core`, via MikroMCP; nicht vom Client — der User war zum
Messzeitpunkt extern via WARP verbunden, Client-Messungen sind für diesen Pfad wertlos):
| Regel-Zustand | Stichprobe | Ergebnis |
|---|---|---|
| **AN** (`egress-cannaleo` → `cf-mesh`) | 15 | **15/15 → 200** (168,1 KiB) |
| **AUS** (Fallback Heim-IP) | 15 | **14/15 → 403**, 1 Ausreißer |
Der Weg ist **deterministisch**, nicht intermittierend (frühere Formulierung war falsch und ist
zurückgenommen). Der Ausreißer sitzt reproduzierbar am **ersten** Sample nach dem Umschalten —
Connection-/Cache-Nachlauf der Vorverbindung, keine Pfad-Instabilität. 🔴 **Merkregel für künftige
A/B-Messungen: nie ein Einzelsample werten, und die erste Anfrage nach einem Routing-Umschalten
verwerfen.** Live-Objekte am Core: Routing-Tabelle `egress-cannaleo`, Route `0.0.0.0/0 → cf-mesh`
(active, distance=1), Regel `164.90.191.7/32 → lookup-only-in-table` (`disabled:false`) — alle drei
deckungsgleich mit Template `[10b]` in `nautobot/mikrotik_routeros.j2` (Drift 0).

View File

@@ -69,28 +69,29 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` ·
> >
> 📊 **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. > 📊 **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 — DNS-Visibility & Flow-Observability für Low-Trust-Geräte (Airfryer-Fall) ▸ vorgemerkt ### 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. - **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. - **(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. - **(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. - **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 **für den Airfryer** seine Rechtfertigung (OTA-Problem erledigt) — der **Mechanismus selbst bleibt** aber als Werkzeug für künftige Low-Trust-Kandidaten definiert und wird **nicht** abgebaut (Entscheidung 2026-10-12), ebenso die DNS-Redirect-Variante (a). Die Cloud-Integration **bleibt** bewusst in Betrieb. 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 — Identity & Access-Control ▸ research-gated ### TP8 · Gitea #2 — Identity & Access-Control
- **RADIUS = FreeRADIUS auf der NAS** (✏️ User-Manager v5 kann **KEIN LDAP** → openldap nur via FreeRADIUS anbindbar). - **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. - **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.) - Abh.: TP3. (Research erledigt, s. TP-R.)
### TP11 — Monitoring & Mgmt-Control-Plane ▸ research-gated ### 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`. - 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 - **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: 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; `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). pve-Connector→VLAN20 via TP14 Session B = #1033-Zielbild).
### TP12 — IPv6 (intern + extern, 3 Standorte) ▸ research-gated ### TP12 · Gitea #4 — IPv6 (intern + extern, 3 Standorte)
- Konzept für FM/BF/DW. → TP-R. - Konzept für FM/BF/DW. → TP-R.
#### TP12.1 — IPv6 im LAN (ULA) + v6-Firewall ▸ 🟠 **KONKRET GEWORDEN (2026-08-22), Treiber: Matter/Thread** #### TP12.1 · Gitea #5 — IPv6 im LAN (ULA) + v6-Firewall
> **Anlass:** der runde IKEA-Fühler **TIMMERFLOTTE** (Matter over Thread) ließ sich nicht anlernen — > **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 > „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 > VLAN10 und VLAN20 **überhaupt kein IPv6 geroutet wird**. Damit ist TP12 vom Konzept-TP zu einem
@@ -168,7 +169,7 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` ·
**v6-Firewall nachbauen → dann erst Adressen vergeben.** Gehört unter TP12 (Gesamtkonzept 3 Standorte), **v6-Firewall nachbauen → dann erst Adressen vergeben.** Gehört unter TP12 (Gesamtkonzept 3 Standorte),
ist aber allein für BF ausführbar. ist aber allein für BF ausführbar.
### TP13 — „Botschafts-WLANs" ▸ 🔜 **AUSFÜHRBAR (Runbook 2026-08-19)** — `docs/tp13-botschafts-wlan-runbook.md` · fun/low-prio ### 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 > 📌 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 > 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 > **Denkfehler** — eine SSID ohne Security *ist* ein offenes WLAN, also genau der Fall, den der
@@ -185,7 +186,7 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` ·
- ⚠️ **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. - ⚠️ **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. - **Abh.:** ~~TP3-Rebuild ✅ (Standalone-APs — SSID-Änderung = 1 Deploy/GC-Job)~~ → **TP28 ✅** (CAPsMAN auf dem Core) + **TP22 ✅** (GC-Deploy-Pipeline). SSID-Änderung = **ein** Deploy auf den Core; Direkt-Edit am Gerät ist durch den Governance-Entscheid ausgeschlossen.
### TP14.4 — Session D: CF-Access-Profil für Claude ▸ ✅ **IN TP41 T4 AUFGEGANGEN (2026-09-01)** — `docs/tp14.4-cf-access-token-runbook.md` (historisch) ### 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 > ✅ **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 > `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 > Gruppe `zt-automation` konsolidiert. `mcp/cloudflare/.env` trägt jetzt **beide** Keys
@@ -201,7 +202,7 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` ·
erneut rotieren. Abnahme = ein Aufruf **mit** den Headern (antwortet) **und** einer **ohne** (403) — 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. der Erfolgsfall allein beweist nichts, der Hostname könnte auf `bypass` stehen.
### TP16 — CF-Tunnel: Round-Robin killen + reziprokes HA ▸ ✅ **HA WIEDER VOLL (2026-08-24)** — Kern 2026-07-10 — `docs/cf-tunnel-mapping.md §10 + §15`, #1033 ### 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 - ✅ **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 (`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 = Sekunde `2026-07-21T13:00:53`**, bei korrekt gesetztem `restart=unless-stopped` und `http2`. Fix =
@@ -249,7 +250,7 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` ·
- 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`. - 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.** - ✅ **Incident 2026-07-16 (User unterwegs, WARP „spinnt", keine BF-IPs mehr) ▸ Public-FQDN-Teil gefixt:** macmini-Connectoren (`cloudflared-bfnas`/`-bfpve`) flappten live unter QUIC/UDP (Docker-Desktop-Bridge-NAT) → `bf-nas`/`bf-pve` von 2 auf 1 Connector degradiert (HA faktisch weg, nicht down). Fix: beide auf `TUNNEL_TRANSPORT_PROTOCOL=http2` umgestellt (rollend, 0 Downtime), wieder voll HA. Konvention: macmini-Connectoren künftig immer `http2`. **ICMP-Befund (getrennt, strukturell):** TCP über die WARP-Route ging überall, ICMP nicht (fm 36 %/bf 100 %/dw 100 % Loss) — cloudflared ist QUIC-Proxy, kein WireGuard. **Kurz als „bf-pve-Route wegen USV" notiert (Commit `1c1a676`) → REVIDIERT** (Begründung invertiert: bf-pve-Connectoren laufen auf NAS+macmini, nicht pve). Details: `docs/cf-tunnel-mapping.md §13`. **→ Richtungswechsel für Private Access = TP16.1.**
### TP16.2 — Härtung der öffentlich erreichbaren Dienste (bypass-Strang) ▸ 📋 KONZEPT (2026-08-14), Umsetzung offen ### 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.** - 🔴 **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`). - **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 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.
@@ -263,7 +264,7 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` ·
- **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. - **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). - **Abh.:** TP16 ✅ (Tunnel-HA). Verwandt: **TP20.1** (vw wandert auf Platform-Postgres — DB-HA ≠ Access, getrennt halten), **TP23** (dessen WARP-Managed-Network-Idee ist durch diesen Constraint **entlastet**: `vw` verlangt keine Device-Identity, also kollidiert „WARP zuhause aus" nicht mehr mit dem Access-Strang).
### TP16.3 — cloudflared-Reduktion: was der Mesh als Datenpfad kann (und was nicht) ▸ ✅ **ABGESCHLOSSEN 2026-09-01** — Flip (§5) + Abbau (§6) durch; öffentlich nur noch `bf-public` (+`fm`) — `docs/tp16.3-bf-public-ap-connector-runbook.md` ### 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") #### ✅ §6 Abbau — AUSGEFÜHRT 2026-09-01 (vom User vorgezogen, „es wird nichts mehr sein")
@@ -664,7 +665,7 @@ und Letzteres verlangt die **physische Bestätigung am Gerät** — ap1 Technik
TP — siehe Quick-Index Zeile TP16.3. Bezug zu **TP35** Schicht 2: Reihenfolge ist TP16.3 → TP35. 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. TP35 wartet mit Schicht 2, bis das Fronting steht.
### TP17.1 — Shelly Gen4 raus aus dem WLAN ▸ ❌ **VERWORFEN (2026-08-23) — Weg bewiesen, Ziel damit nicht erreichbar** — 📋 `docs/tp17.1-shelly-gen4-funkmigration-runbook.md` ### 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 > ❌ **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 > 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."* > wenig wenn man das wifi nicht abdrehen kann bzw. es nicht mehr aktivieren kann ohne reset."*
@@ -816,7 +817,7 @@ und Letzteres verlangt die **physische Bestätigung am Gerät** — ap1 Technik
</details> </details>
### TP17.3 — Shelly-Regression nach der VLAN30-Migration ▸ ✅ **ERLEDIGT (2026-08-23, Folge-Session): beide Geräte repariert, Ursache `eco_mode` bewiesen** — offen nur noch der Flotten-Entscheid (gehört TP17.1) — aus TP17 herausgelöst, User-Meldung ### 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 > **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 > 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. > ist jedoch der von **diele ug: seit deinen änderungen spinnt er herum und blinkt wie ein wilder.
@@ -953,7 +954,7 @@ des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin **nicht** au
4. Reboots der verbleibenden 13 `restart_required`-Geräte — **gefahrlos** (empirisch an `s26` belegt), 4. Reboots der verbleibenden 13 `restart_required`-Geräte — **gefahrlos** (empirisch an `s26` belegt),
deshalb kein eigener Termin, sondern Mitnahme bei TP17.1. deshalb kein eigener Termin, sondern Mitnahme bei TP17.1.
### TP18 — Souveräner Objektspeicher (Garage, Multi-Site, S3-Cold-Tier) ▸ 🔜 KONZEPT (2026-07-12) ### 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." - **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. - **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 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.
@@ -970,14 +971,14 @@ des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin **nicht** au
- **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. - **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. - Abh.: **TP10** (WG-Multi-Site = Transport, Upload-BW-Constraint), **TP14** (Container-Hosts), **TP15** (Node-HW) / **TP24** (Stromversorgung), Muster von **TP16** (reziprokes Per-Host-HA). Kein Blocker für laufende TPs. SSoT dieser Session.
### TP20.1 — Vaultwarden → Platform-Postgres + Cross-Site-HA ▸ 🔜 KONZEPT (2026-07-14, ausgelagert aus TP20) ### 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). - **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 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. - **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**. - **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. - Abh.: **TP20** (Tier-1 Postgres muss stehen), **TP10** (WG-Transport für Phase-2), **TP18** (Backup-Schirm). Kein Blocker für TP21.
### TP23 — DNS-Policy & Self-Service-Ausnahmen ▸ ⏳ **STUFE 5 WIEDER OFFEN (Redesign 2026-09-12): Self-Service deckt die DAU-Requirements nicht ab → Stufe 6 bleibt dahinter gated** ### 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 > 🔴 **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 > (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 > Domain**, `allow-domain` global statt pro Person) deckt die Nutzer-Requirements **nicht** ab
@@ -1074,7 +1075,7 @@ des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin **nicht** au
- ⚠️ **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. - ⚠️ **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. - **Abh.:** **TP21.1 TODO 1** (DoH auf core), **TP5** (Firewall/Bypass-Sperre — Reihenfolge s.o.), **TP16 Schritt 5** (Access-Kollision), **TP16.1** (falls WARP zuhause bleiben soll), **TP8** (nur Sicherheit). **Kein Blocker für TP5 → TP6**, aber TP5 darf die Bypass-Sperre nicht vorziehen.
### TP24 — Unterbrechungsfreie Stromversorgung (DC-USV, 48V-Bus) ▸ 🔜 KONZEPT — **aus TP15 herausgelöst (User-Entscheid 2026-08-16)** — `docs/hw-refresh-requirements.md §9` ### 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. - **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. - **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):** - **Zielbild (Aufbau, unverändert gültig aus der TP15-Recherche):**
@@ -1102,230 +1103,19 @@ des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin **nicht** au
- **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.** - **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). - **Historie:** war bis 2026-08-16 Unterpunkt von TP15 („🔋 DC-USV: MeanWell LRS-150-48 + DDR-120-48 + LiFePO4 20Ah (~€190), 48V-Bus; CRS326 DC-input passt"). Vollständige Recherche inkl. Schaltbild, Effizienzvergleich und Gerätetabelle: `docs/hw-refresh-requirements.md §9` (+ §10 D/E/F für die offenen Fragen).
### TP25 — WLAN-Stabilisierung ▸ ✅ **FERTIG (2026-09-15)** → [Archiv](teilprojekte-archiv.md#tp25) ### TP25 · Gitea #17 — WLAN-Stabilisierung — → [Archiv](teilprojekte-archiv.md#tp25)
> **Abgeschlossen:** Platzierung final (ap1 Sideboard · ap2 Technik UG · ap3 Technik OG), Fund E > **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, > 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. > Gitea #17 closed. Volltext (Kondensat + Langfassung) im Archiv.
<a id="tp28"></a> ### TP28 · Gitea #18 — WLAN über CAPsMAN auf `crs326-core` — → [Archiv](teilprojekte-archiv.md#tp28)
### TP28 — WLAN über CAPsMAN auf `crs326-core` ▸ ✅ **ap3 LIVE (2026-08-30)** — wieder geöffnet und durchgezogen > **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.
> **Der CAPsMAN-Kern ist und bleibt ✅ KOMPLETT (2026-08-18)** — der Abschnitt darunter ist ### TP29 · Gitea #19 — Anwesenheitserkennung fürs Energiesparen
> unverändert der Archivtext inkl. Post-Mortem. Wieder geöffnet wurde das TP am 2026-08-30,
> weil mit **ap3** ein dritter CAP dazukommt; das ist dieselbe Baustelle, kein neues TP
> (Christoph-Direktive: *ein archiviertes TP ist ein geschlossenes Ticket und darf wieder
> geöffnet werden*). **Der offene Teil steht am Ende des Abschnitts unter „Nachtrag ap3".**
- 🔴🔴 **POST-MORTEM 2026-08-18 (gemeldet von Christoph: „die -iot-geräte sind offline … connect möglich, aber kein dhcp"):**
Seit dem Cutover hingen **alle Slave-VAPs in gar keiner Bridge**. `/interface wifi cap slaves-datapath`
war auf beiden APs leer — und genau dieses Feld ist das Einzige, was die vom Manager erzeugten
Slave-VAPs lokal in die Bridge haengt. Der Manager schickt `datapath.bridge=bridge` zwar mit,
**der CAP uebernimmt davon nur die `vlan-id`.** Betroffen: `dirtyvader-iot`, `-media`, `-untrusted`,
`-guest`. Nicht betroffen: `dirtyvader-main` — dessen Master-Radios `wifi1`/`wifi2` sind
**statische** Bridge-Ports und liefen deshalb weiter. Genau das machte den Ausfall unsichtbar.
- **Fix (live 2026-08-18 auf ap1 + ap2, seither Soll im Template):**
`/interface wifi datapath add name=dp-caps-slaves bridge=bridge` +
`/interface wifi cap set slaves-datapath=dp-caps-slaves`. Das Objekt traegt **bewusst keine
`vlan-id`** — die liefert der Manager pro VAP. Danach erscheinen die VAPs als **dynamische
Bridge-Ports mit `pvid`=30/40/50/90**, und DHCP laeuft sofort wieder.
- 🔴 **Die Diagnose-Lehre: „Client assoziiert" beweist gar nichts.** Am Manager standen 25 IOT-
Registrations — das WLAN sah perfekt aus. Erst zwei Reads trennten die Faelle:
`torch` auf `vlan30` am Core (**leer**) und der TP5-Drop-Log (**kein einziger DHCP-Eintrag**).
Beides leer heisst: es wird nichts verworfen, es **kommt** nichts. Damit war die Firewall
— der naheliegende Verdaechtige nach der Haertung derselben Nacht — in zwei Minuten entlastet.
- ⚠️ **Die Abnahme „✅ Ohne jeden Aussetzer" weiter unten war falsch.** Sie zaehlte Registrations
(30 Clients, beide CAPs `Ok`, 7 VAPs je AP) — also genau die Metrik, die hier luegt. Keiner
dieser Werte beruehrt die Bridge. Ein Abnahmekriterium fuer WLAN-Umbauten ist ab jetzt eine
**frische DHCP-Lease pro SSID**, nicht die Registration-Table.
- **`routeros/tp28-ap-wifi-reste-abraeumen.rsc` hat den Ausfall nicht ausgeloest, aber zementiert:**
mit den geloeschten datapath-Objekten gab es lokal nichts mehr, worauf `slaves-datapath` haette
zeigen koennen. Der Ein-Zeilen-Fix war damit weg. Nachtrag steht im Skript.
- ✅ **Reboot-Abnahme an ap2 (2026-08-18), drei Fragen auf einmal beantwortet:** `slaves-datapath`
ueberlebt den Neustart (statisches Objekt, steht im `/export`), der CAP **findet seinen Manager
durch die gehaertete input-Kette wieder** (`current-caps-man-identity=crs326-core` — MNDP/CAPWAP
sind also korrekt offen, das war seit TP5 Etappe 4 nie durchgespielt), und die sieben Slave-VAPs
kommen von selbst als dynamische Bridge-Ports mit pvid 30/40/50/90 zurueck. AP nach **40 s**
wieder erreichbar. Abnahme nach neuer Regel = **frische Lease**: IOT-Client `10.40.30.8`
`last-seen=30s`, MAIN-Client `10.40.10.180` `last-seen=1m54s`.
- ⚠️ **Reboot per MikroMCP geht auf 7.24 nicht:** das `reboot`-Tool sendet `delay`, die REST-API
antwortet `unknown parameter delay`; `run_command` ist fuer `/system reboot*` per Deny-Policy
gesperrt. Weg bis das gefixt ist: `sshpass` + `ssh admin@<ap> "/system reboot"`.
- ✅ **Soll nachgezogen, Compliance wieder sauber: 92/92, 0 Drifts** (2026-08-18). Weg: Template in
`nautobot/mikrotik_routeros.j2` → `routeros/push-gc-template.py` (Secret-Guard) → GitRepository-Sync
→ Backup/Intended/Compliance. Ohne den Push meldet der Refresh die zwei neuen Zeilen als `extra`
auf beiden APs — das Jinja lebt im **separaten** Gitea-Repo `bf-golden-config`, ein Commit hier
allein erreicht Nautobot nicht.
- ⚠️ **Beim ersten Sync-Versuch: `redis.exceptions.ConnectionError: broken pipe`** auf dem
Nautobot-Stack. Ein simpler Retry ging durch; NAS-Load war unauffaellig (0,9). Der Job meldet das
als `FAILURE` — nicht mit einem inhaltlichen Fehler verwechseln.
- ✅ **Der offene GC-Rest ist erledigt (2026-08-18).** Die fuenf `wifi-*`-ComplianceFeatures haengen
jetzt am **Core**, der AP-Zweig des Templates ist auf eine einzige Zeile eingedampft
(`/interface wifi cap set discovery-interfaces=vlanXX enabled=yes`).
- 🔴 **Dabei aufgefallen: die APs trugen noch die KOMPLETTE Standalone-WLAN-Config** aus TP3 —
je 2 channel, 6 datapath, 6 security, 7 configuration, 1 steering. Der Cutover vom 08-17 hatte
sie nie abgeraeumt. Wirkungslos (der Manager liefert alles dynamisch), aber sie im Template als
Soll zu rendern hiesse, totes Zeug zur Absicht zu erklaeren — dasselbe Argument wie bei den
defconf-Firewallregeln in TP26. Abgeraeumt mit `routeros/tp28-ap-wifi-reste-abraeumen.rsc`.
- ✅ **Ohne jeden Aussetzer:** vorher/nachher 30 Clients, beide CAPs `Ok`, 7 dynamische VAPs je AP.
Die Sorge vor einem Radio-Re-Apply (TP25/TP3.3: „wirft jeden Client kurz raus") hat sich nicht
bestaetigt — die dynamischen VAPs tragen ihre Werte **inline** (`datapath.vlan-id=…`) und
referenzieren kein lokales Objekt; nur `wifi1`/`wifi2` hielten noch einen toten
`configuration=`-Verweis, der beim Loeschen folgenlos blieb.
- 🔴 **`[find !dynamic]` ist auf diesen Sektionen ein SYNTAXFEHLER**, nicht bloss wirkungslos:
`/interface wifi configuration` & Co. haben gar keine `dynamic`-Eigenschaft (die gibt es nur auf
`/interface wifi` selbst). Der erste Anlauf lief komplett in die `on-error`-Zweige, aenderte
**nichts** und meldete trotzdem „Plan Completed". Wieder: nur die Gegenpruefung am Geraet zaehlt.
- ✅ **Drei neue ComplianceRules schliessen die letzten WLAN-Luecken:** `wifi-capsman`
(der Manager-Schalter), `wifi-provisioning` (die vier Regeln je `radio-mac` — sie entscheiden,
welcher AP welche Kanaele und Slave-VAPs bekommt) und `wifi-cap` (die CAP-Seite; faellt
`discovery-interfaces` weg, findet der AP seinen Manager nach dem naechsten Reboot nicht mehr).
**Ergebnis: 92/92 compliant, 0 Drifts.**
- ⚠️ **`wifi-security` ist gruen, aber NICHT deploybar.** Die Passphrasen stehen als `<<REDACTED>>`
im Template (User-Entscheid 2026-08-18: alle fuenf Features umhaengen, Platzhalter in Kauf nehmen).
Der gruene Haken beweist nur, dass Soll und Ist dieselbe **Maske** tragen — nicht, dass die
Passphrasen stimmen, und schon gar nicht, dass die Sektion ausgerollt werden koennte. Das steht
als Warnung im Template. Wer sie aendert, liest den Ist-Stand per REST zurueck
(`GET http://10.40.99.1/rest/interface/wifi/security`) — **nicht** aus `wlan-psks.env` oder den
drei Bring-up-Dateien, die voneinander abgeschrieben sind.
- ⚠️ **Die frueher hier notierte „bisher fehlende Rule auf `/interface wifi`" ist gegenstandslos** —
wie vorhergesagt: ein CAP hat keine Inline-Overrides mehr, die driften koennten.
> **Methodenwechsel auf Christophs Anstoß:** *„ich möchte die methode in frage stellen … eine sot ist zwar nett,
> aber wenn sie uns das leben zur hölle macht, verzichte ich auf diese governance."* Der Entscheid vom
> 2026-07-22, CAPsMAN abzuschaffen, ist damit **revidiert**.
- ✅ **Warum es diesmal trägt:** 2026-07-22 war **ap1 Manager UND AP** — und ein Manager kann seine eigenen
Radios nicht per CAPsMAN provisionieren (MikroTik-bestätigt). Genau diese Asymmetrie war die Ursache von
TP3.1. Mit dem **Core** als Manager sind beide APs symmetrisch. **Der Core braucht dafür kein `wifi-qcom`:**
das `capsman`-Menü steckt im Basis-`routeros`, die 3,35 MB freier Flash sind irrelevant. Die zweite
Begründung von damals („WLAN instabil") war ohnehin widerlegt — standalone blieb es instabil, die Ursachen
waren die Funkparameter aus TP25.
- ✅ **Ist-Stand:** Manager auf `vlan99`, 4 Kanäle · 6 Datapaths · 6 Security · 2 Steering · 9 Configurations ·
4 Provisioning-Regeln je `radio-mac`. Beide `remote-cap` **`Ok`**, je 7 dynamische VAPs. Kanäle exakt wie in
TP25 gemessen (ap1 `5180/ax/Ce/I`+`2412/ax`, ap2 `5220/ax/Ce/I`+`2462/ax`). **34 Clients** (main 3, iot 27,
media 4), Roaming im Manager-Log belegt. **`ft-over-ds=yes`** ist jetzt aktiv (unter CAPsMAN erstmals
verfügbar), `-guest` hat eigenes `st-guest` mit **`neighbor-group=bf-guest`** — nicht `bf-main` mitbenutzen,
sonst meldet 802.11k Gast-Clients BSSIDs einer SSID, auf die sie nicht assoziieren können.
- 📌 **Local-Forwarding ist keine Einstellung mehr:** im neuen `/interface/wifi` gibt es **kein** zentrales
Forwarding („There is no CAPsMAN-forwarding in WiFi (RouterOS 7.0+)"). Die `datapath`-Property existiert
deshalb nicht — `monitor` bestätigt `traffic processing on CAP`.
- 🔴 **DIE MESSFALLE DIESES TPs — unter CAPsMAN führt der MANAGER die Tabellen, nicht der CAP.**
`/interface/wifi/registration-table` und die `wireless`-Logs sind auf ap1/ap2 **leer**, obwohl das WLAN
einwandfrei läuft. Ich habe daraus zweimal „WLAN ist unten" geschlossen, während Christophs Geräte mit
vollem Ausschlag verbunden waren und ein Speedtest gut lief. **Immer am Manager zählen.**
- 🔴 **DIE ZWEITE FALLE — drei übereinstimmende Dateien sind nicht drei Belege.** `wlan-psks.env` wich für
iot/untrusted/infra von `ap1-prov-base.rsc`, `ap2-prov-base.rsc` und `ap1-standalone.rsc` ab. Ich habe die
drei als Bestätigung gelesen und die env-Datei „korrigiert" — **~20 Shellys waren 20 Minuten aus dem Netz.**
Die drei Dateien stammen alle aus dem Bring-up vom 2026-08-08, sind also **eine Quelle in drei Kopien**.
✅ **Auflösung:** die Passphrasen sind am Gerät per REST **lesbar**
(`GET http://10.40.99.1/rest/interface/wifi/security`) — dort ausgelesen ergab sich exakt der env-Wert.
`wlan-psks.env` ist neu aus dem Live-Stand befüllt und trägt jetzt eine Kopf-Warnung; die drei Baselines
sind seit dem Cutover ohnehin nicht mehr deploybar und wurden entschärft (Passphrasen raus, HISTORISCH).
- ✅ **~~OFFEN — das GC-Modell beschreibt weiter Standalone-APs~~ — ERLEDIGT am 2026-08-18.** Die
`wifi-*`-Features haengen jetzt am Core, das AP-Template ist auf die `cap`-Zeile eingedampft, und die
toten Standalone-Objekte sind von beiden APs abgeraeumt. Details im ersten Block dieses TPs.
Die hier genannte Zahl `76/76` ist damit doppelt ueberholt (aktuell 92/92 bei 23 Rules).
*(Der Absatz stand bis 2026-08-19 unveraendert als „OFFEN" hier, obwohl der Kopf des TPs schon
„KOMPLETT" sagte — ein Selbstwiderspruch im selben Abschnitt, ueber den prompt die naechste Session
gestolpert ist. Merkregel fuer kuenftige Statuswechsel: **nicht nur den Header umschreiben, sondern
die Punkte darunter mitziehen.**)*
- 📌 **Nebenbefund:** HAs `mikrotik`-Integration pollt alle 10 s und schreibt zwei Log-Zeilen pro Poll auf jedes
Gerät. Die AP-Logs sind dadurch für Diagnose praktisch unbrauchbar (`get_log` ohne `topics`-Filter liefert
nur `admin logged in/out via api`).
- **Abh.:** TP25 ✅ (die Funkparameter sind übernommen), TP15/TP21.1 ✅ (Core als L3-Router = Manager-Standort).
Verwandt: **TP3** (dessen CAPsMAN-Ära hier revidiert wird), **TP22** (GC-Scope).
#### ↩️ Nachtrag ap3 — dritter Access-Point (cAP ax), offen seit 2026-08-30
**Gerät:** MikroTik **cAP ax** (`cAPGi-5HaxD2HaxD`), Werkszustand, Uplink an **`crs326-core ether21`**.
**Mgmt-IP `10.40.99.13/24`** (TP32: `.10–.29` = physische Hosts; ap1 `.11`, ap2 `.12`), statisch am
Gerät wie ap1/ap2 — **keine** DHCP-Reservierung.
**Warum überhaupt:** ap3 soll **ap1 in Technik UG ablösen**, ap1 wandert danach ans Sideboard in die
gemessene Schwäche (Schlafzimmer/Bad/Küche). Das ist exakt **TP25 Variante 3** („cAP ax an die
bisherige Stelle, der verdrängte hAP ax² wandert in den Schatten").
**Kanalplan = der aus TP25 (2026-08-24 gerechnet, DFS von Christoph genehmigt) — nicht neu erfunden:**
| AP | 5 GHz | Band | DFS | 2,4 GHz | wann |
|---|---|---|---|---|---|
| **ap3** | `5180/Ce` (36+40) | A | nein | **2437 (ch6)** | **jetzt** |
| ap2 | `5220/Ce` heute → **`5300/Ce`** (60+64) | A → B | → ja | 2462 (ch11) | beim ap1-Umzug |
| ap1 | — (**Radios aus**) → **`5260/Ce`** (52+56) | → B | → ja | 2412 (ch1) | beim ap1-Umzug |
| — | `5220/Ce` **Ausweichspur frei halten** | A | — | — | — |
🔴 **ap1s Radios gehen AUS, bevor ap3s angehen** (User-Vorgabe 2026-08-30): beide stehen bis zum
Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört. Das ist ein
**Interim**, kein Endzustand — ap1 kommt auf `5260` zurück.
> 📌 **Erratum 2026-08-30:** der Port ist **`ether21`**, nicht `ether22` (erste Annahme aus der
> Fragerunde). Der Befund lag die ganze Zeit in den Daten: in der dynamischen `pvid`-VLAN1-Zeile
> steht `current-untagged=…,ether21` — `ether22` taucht dort nicht auf, weil es keinen Link hat.
> **Merkregel: bevor man einen Port konfiguriert, den behaupteten Port gegen `link-ok` + die
> Bridge-Host-Tabelle stellen.** `/interface bridge host print where interface=etherN` nennt die
> MAC des Gegenübers — hier `D0:EA:11:FF:16:9D`, exakt die `E01` vom ap3-Label.
##### Stand 2026-08-30 — ✅ DURCH
- ✅ **`core ether21` ist Trunk** (10/20/30/40/50/90/99 tagged, Muster `ether15`), GC-Template
nachgezogen; AP-Zweig rendert Bridge-Ports aus **`ap.extra_ports`** statt hartkodiert.
- ✅ **Zugang über `ap3 ether2` → `core ether22`** (Patchkabel). Über `ether1` war ap3
grundsätzlich nicht administrierbar, s. Gotchas unten. Loop-Schutz: `ether21` war während
Phase 1 abgeschaltet, danach im selben Befehl getauscht (`ether22` aus, `ether21` an).
- ✅ **Bring-up in zwei Phasen**: Phase 1 (`ether1` aus der WAN-Liste, `10.40.99.13/24` auf
`vlan99`, Flotten-PW) über das Kabel, Phase 2 (Werks-WLAN weg, CAP-Modus,
`slaves-datapath`, `vlan-filtering=yes`) über `vlan99`.
- ✅ **RouterOS 7.20.8 → 7.24.1 + RouterBOOT 7.24.1** (zwei Reboots, TP27-Kriterium
`current-firmware = upgrade-firmware`). 🆕 **Die Angleichung der übrigen vier ist am
2026-08-31 erfolgt** (Christoph, WebFig) — die Flotte ist einheitlich, TP27 erledigt.
- ✅ **ap1 funkstill** (Provisioning-Regeln entfernt + `remote-cap provision`), gegengeprüft
**am CAP**: nur noch `wifi1`/`wifi2` mit `running=false`, keine dynamischen VAPs.
- ✅ **ap3 funkt**: `cap-wifi5` = **`5180/ax/Ce/I`**, `cap-wifi6` = **`2437/ax`**, voller
VAP-Satz (2 Master + 7 Slaves), alle `running=true`.
- ✅ **Datapath bewiesen**: 20 IOT-Clients auf ap3, Ping über VLAN30 mit **0 % Loss**.
*(Bewusst nicht über die Registration-Table abgenommen — die zeigte im 14-h-Ausfall
25 zufriedene Clients. Bei statisch reservierten Shellys erzeugt ein Roam keine neue
DHCP-Lease, deshalb ist hier Datenverkehr das belastbare Signal.)*
- ✅ **Nautobot**: DeviceType `cAP ax`, Device `ap3` mit 16 Interfaces, `primary_ip4`,
`secrets_group`, ConfigContext (`5g=ch36 2g=ch6 extra_ports=[]`).
Nebenkorrektur: ap1 stand auf „Wohnzimmer Sideboard", steht aber in **Technik UG**.
- ✅ **Compliance 130/130, 0 Drifts über 5 Geräte.** Die drei Werksreste, die der erste
Lauf mit ap3 gemeldet hat (defconf-DNS, 11 defconf-Firewallregeln, fehlende
`/ip service`-Härtung), sind mit `routeros/tp28-ap3-werksreste.rsc` abgeräumt.
- 🔜 **Offen:** TP25-Messrunde (`bash docs/tp25-runde.sh ap3-bringup`) — braucht einen
Rundgang. Leitfrage: deckt ap3 allein ab, was ap1 abgedeckt hat?
- 🔜 **Offen:** Passwort-Rotation (TP30) — das gemeinsame Admin-PW ist in dieser Session
verbrannt; ap3 trägt es bereits mit, hängt also in der Rotation mit drin.
##### Was am cAP ax anders ist als am hAP ax² — und was daran wehtut
- 🔴 **Nur ZWEI Ethernet-Ports** (`ether1` PoE-in, `ether2` PoE-out) — kein `ether3/4/5`. Der
GC-Template-AP-Zweig hatte die **hartkodiert**; das war die stille Annahme *„AP == hAP ax²"*.
Deshalb `ap.extra_ports` im ConfigContext (Default = `["ether3","ether4","ether5"]`, ap3 = `[]`).
**Merkregel: eine Literal-Liste im Template ist so lange harmlos, bis die Flotte heterogen wird.**
- **`crs326-core` hat kein PoE-out** (`/interface ethernet poe` existiert dort nicht). Der cAP ax
bringt aber **Netzteil *und* Gigabit-PoE-Injector** mit (Produktseite 2026-08-30) — die TP25-Notiz
„kein Netzteil im Lieferumfang üblich" trifft auf dieses Modell **nicht** zu.
- `ether2` bleibt im Bring-up als **Lifeline** im Bridge (untagged VLAN1) und fliegt erst raus,
wenn ap3 über `10.40.99.13` steht — Zielzustand = wie ap2: `ether2` **kein** Bridge-Port
(VLAN1 bleibt leer, TP19 ist geschlossen).
- **Radio-MACs nicht raten:** `/interface wifi radio print` **am Manager** listet CAP-Identity +
Radio-MAC ohne jede Passphrase. Das ist der richtige Read — **nicht** `list_wifi_interfaces`,
das gibt `security.passphrase` *aller* Profile im Klartext zurück (Leak 2026-08-24).
##### Auflagen aus TP25 für den ap1-Umzug (nicht vergessen)
1. **DFS-Channel-Objekte brauchen eine Frequenz-LISTE** (z.B. `frequency=5260,5280,5220`). Mit
genau einem Wert ist eine Radar-Erkennung ein **SPOF**: das Radio muss den Kanal verlassen und
hat keinen zweiten → 5 GHz dieses APs bleibt unten.
2. **Vorher die *erreichte* `tx-power` messen** (`/interface wifi monitor`), ap2 testweise auf
`5260` gegen die heutigen 18 dBm auf `5220`. Fund B war genau das Muster: 18 dBm gesetzt,
9 dBm gesendet.
3. Uplink für ap1 danach: **`core ether3`** (`no-link`, frei) — vorher dort die Altlast abräumen,
der Port ist untagged-Member von VLAN40 bei `pvid=1`.
### TP29 — Anwesenheitserkennung fürs Energiesparen ▸ 💡 **IDEE, noch nicht auf dem kritischen Pfad** (angelegt 2026-08-21)
> **User-Formulierung (2026-08-21):** „Personen im Haus orten mit BT (Shellys), WiFi und Radar. So > **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." > kommen wir dem Ziel der Energieersparnis ohne Komfortverlust groß näher."
@@ -1351,7 +1141,7 @@ Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört
dahin kommt. **Kein Blocker für irgendetwas.** dahin kommt. **Kein Blocker für irgendetwas.**
<a id="tp30"></a> <a id="tp30"></a>
### TP30 — Credentials: neu setzen & rotieren (Sammelbecken) ▸ 🔜 **ANGELEGT 2026-08-24, bewusst GEBÜNDELT gegen Projektende** ### 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 > **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 > 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, > projektende." — nachgeschärft am selben Tag: **„tp30 wird unser sammelbecken für credentials,
@@ -1384,6 +1174,9 @@ Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört
| 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 | | 7 | **`TERPENE_ANDROID_KEYSTORE_PW`** | Terpene-Android-TWA-Signing (`android/android.keystore`, alias `terpene`) | 🔴 am 2026-09-01 (TP41) durch **Regex-Suffix-Loch** (`_PW` nicht abgedeckt) ins Transkript geraten | Wert in `~/.secrets/credentials.env` + Keystore-Datei | **Store-/Key-Passwort** via `keytool -storepasswd`/`-keypasswd` ändern + `credentials.env` nachziehen; Konsument = Android-Build/Play-Signing (**Cross-Projekt blaueblüten**) — den **Key selbst NICHT** neu erzeugen |
| 8 | **`N8N_SMTP_PASS`** (Gmail-App-PW für `johnny.claw.0x00@gmail.com`) | SMTP-Versand aus n8n (Portainer-Stack 174 `n8n`, macmini) | 🔴 am 2026-09-01 (n8n-mcp-SSRF-Session) beim Ausgeben des Stack-174-Files ins Transkript geraten — Redaktionsmuster hatte `PASSWORD`, aber **nicht bare `PASS`** → `N8N_SMTP_PASS=<wert>` blieb stehen | Wert in Portainer-Stack 174 (`environment`) | **Gmail-App-Passwort widerrufen** (Google-Konto → App-Passwörter) + neues erzeugen, Stack-174-Env nachziehen + redeploy; Konsument = n8n-SMTP-Node(s) | | 8 | **`N8N_SMTP_PASS`** (Gmail-App-PW für `johnny.claw.0x00@gmail.com`) | SMTP-Versand aus n8n (Portainer-Stack 174 `n8n`, macmini) | 🔴 am 2026-09-01 (n8n-mcp-SSRF-Session) beim Ausgeben des Stack-174-Files ins Transkript geraten — Redaktionsmuster hatte `PASSWORD`, aber **nicht bare `PASS`** → `N8N_SMTP_PASS=<wert>` blieb stehen | Wert in Portainer-Stack 174 (`environment`) | **Gmail-App-Passwort widerrufen** (Google-Konto → App-Passwörter) + neues erzeugen, Stack-174-Env nachziehen + redeploy; Konsument = n8n-SMTP-Node(s) |
| 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 | | 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 '<neu>'` 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.** > ✅ **Der Leak-MECHANISMUS hinter #1 ist seit 2026-08-31 zu — der Wert bleibt trotzdem verbrannt.**
> Alle sechs Skripte im Repo, die `sshpass -p <PW>` benutzten (`nautobot/gc-transaktion.py` und > Alle sechs Skripte im Repo, die `sshpass -p <PW>` benutzten (`nautobot/gc-transaktion.py` und
@@ -1426,6 +1219,18 @@ Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört
> fehlerhaften Wert nicht mitschleppen). Das Redaktions-Snippet oben hilft hier nicht — es > fehlerhaften Wert nicht mitschleppen). Das Redaktions-Snippet oben hilft hier nicht — es
> kann nichts maskieren, was als Fehlermeldung und nicht als Dateiinhalt kommt. > 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 `<redacted>` 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) — > ℹ️ **#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 > 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). > **TP14.4**, das als TP bestehen bleibt (dort steckt das CF-Access-Profil, nicht nur der Token).
@@ -1622,7 +1427,7 @@ unabhängig vom gebündelten Durchgang.**
<a id="tp31"></a> <a id="tp31"></a>
### TP31 — Reverse Proxy für innen und außen; cloudflared von sechs Knoten auf einen ▸ ✅ **ABGESCHLOSSEN 2026-09-01** ### 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` > ✅ **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** > **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 > (`https://10.40.20.40:443`), die drei Alttunnel `bf-nas`/`bf-pve`/`bf-mm` **gelöscht**. Damit steht das
@@ -1872,7 +1677,7 @@ diesmal mit Datenverlust statt 530ern. **Automatisches Ingress-Failover erst, we
- 📋 **Vollständiger Plan mit allen Abwägungen:** `~/.claude/plans/expressive-pondering-clarke.md` - 📋 **Vollständiger Plan mit allen Abwägungen:** `~/.claude/plans/expressive-pondering-clarke.md`
<a id="tp32"></a> <a id="tp32"></a>
### TP32 — Nummern- und Adress-Systematik ▸ 🟢 **KONZEPT STEHT (2026-08-25), Startreihenfolge bereits repariert** ### TP32 · Gitea #22 — Nummern- und Adress-Systematik
> **User-Auftrag 2026-08-24:** „du/ich sollst in der lage sein, selbstständig nach einem vorgegebenen > **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 > 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." > wieder" · „ich meine 100 sollte künftig für die core services stehen."
@@ -1983,7 +1788,7 @@ gewachsen, nicht geplant. Merkregel: **„Bestand bleibt, wo er ist" gilt nur, s
wo er ist, ohne nachzusehen.** wo er ist, ohne nachzusehen.**
### TP33 — Sonos aus der Ferne bedienen (VPN/WARP) ▸ 💡 **IDEE, bewusst zurückgestellt** (angelegt 2026-08-25, User-Entscheid „später entscheiden") ### 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** - **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, auffindbar sein sollen. Antwort für jetzt: **nein, später entscheiden** — hier nur festgehalten,
@@ -2002,7 +1807,7 @@ wo er ist, ohne nachzusehen.**
--- ---
### TP35 — MCP-Schichtung global/projekt/user + 3-Schicht-CLAUDE.md + Announcement-Mechanismus + Backup-Guard ▸ ✅ **ABGESCHLOSSEN 2026-09-12** — 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`. ### 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. - **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). - **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:** - **Scope:**
@@ -2019,7 +1824,7 @@ wo er ist, ohne nachzusehen.**
--- ---
### TP36 — Index/Status als SSoT nach Gitea (Steuerung aus der branchbaren Datei herauslösen) ▸ ⏳ **CUTOVER 2026-09-07** — Index/Status = Gitea-Issues, Prosa bleibt Datei ### 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). - **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. - 🔄 **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**. - **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**.
@@ -2032,7 +1837,7 @@ wo er ist, ohne nachzusehen.**
- **Verzahnt mit:** TP35 (SSoT + Konsolidierung), TP44 (native Nautobot-Modelle), `bf-infra/CLAUDE.md`-Lessons. Plan: `~/.claude/plans/geht-das-los-auch-reflective-lark.md`. - **Verzahnt mit:** TP35 (SSoT + Konsolidierung), TP44 (native Nautobot-Modelle), `bf-infra/CLAUDE.md`-Lessons. Plan: `~/.claude/plans/geht-das-los-auch-reflective-lark.md`.
<a id="tp37"></a> <a id="tp37"></a>
### TP38 — Out-of-Band-Management: ein untagged-VLAN99-Port je Switch ▸ 🔜 **ANGELEGT 2026-08-31** ### 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 *(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.)* einem zweiten Branch für „Nautobot als Netzwerkdoku" vergeben worden.)*
@@ -2108,7 +1913,7 @@ folgenden Absicherungen dazu, und die Entscheidung darüber in die Umsetzungs-Se
--- ---
### TP40 — Home-Assistant-Recorder-Retention: die 41-GB-`states`-Tabelle beschneiden ▸ 🔜 **ANGELEGT 2026-09-01 (User-Okay erteilt)** ### 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` **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 — (TP39 Punkt 3), aber 59 der 65 GB sind die **`homeassistant`-Datenbank**, und die wächst weiter —
@@ -2149,7 +1954,7 @@ Der HA-Recorder schreibt **unbegrenzt** — es ist keine `purge_keep_days`-Grenz
- 🔴 **Kein eigenmächtiger Datenlöschvorgang** — `purge` wirft echte Historie weg; die - 🔴 **Kein eigenmächtiger Datenlöschvorgang** — `purge` wirft echte Historie weg; die
Aufbewahrungsdauer ist eine Nutzerentscheidung. Diese Session hat **nur** vergrössert und gemessen. Aufbewahrungsdauer ist eine Nutzerentscheidung. Diese Session hat **nur** vergrössert und gemessen.
### TP37 — Nautobot als durchgehende Netzwerkdoku: Hosts/Container/Dienste, erster Anwendungsfall CF-Connectoren ▸ 🔜 ANGELEGT 2026-08-30 (User-Okay erteilt) ### 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 > **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 > 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."* > den mikrotiks ja nur via gc/nautobot deployst."*
@@ -2211,7 +2016,7 @@ Der HA-Recorder schreibt **unbegrenzt** — es ist keine `purge_keep_days`-Grenz
--- ---
<a id="tp342"></a> <a id="tp342"></a>
### TP34.2 — Interne DNS-Namen als Daten (FQDN aus Nautobot) ▸ 🔜 **ANGELEGT 2026-09-22** (Gitea #73) — **gemeinsame Arbeit** ### 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 > **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!"* > 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: > → **eigener Träger im TP34-Nummernkreis**. Parent **#24 (TP34) bleibt `status:dropped`** — kein Reopen:
@@ -2239,7 +2044,7 @@ Der HA-Recorder schreibt **unbegrenzt** — es ist keine `purge_keep_days`-Grenz
--- ---
### TP45 — VLAN20/VLAN30 ins TP32-Schema + DNS/FQDN aus Nautobot ▸ 🔜 **ANGELEGT 2026-09-21** (Gitea #71) ### 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 > **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: > 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, > *„wenn wir schon reverse proxy & co korrigieren müssen, wäre es vl jetzt ein guter zeitpunkt,
@@ -2464,38 +2269,59 @@ NAT/mangle/WG sind Template-Literale, der Firewall-Filter kommt als ConfigContex
die FQDNs, die hier Daten werden). **Abgrenzung:** VMID-Umnummerierung bleibt draußen; Token-**Rotation** 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. läuft in TP30; Tunnel-GC-Namensgebung hier, die **SSoT/Recovery** in TP46.
### TP46 — Cloudflare/Tunnel: SSoT mit Push-Weg + Full-Recovery from scratch ▸ 🔜 **ANGELEGT 2026-09-21** (Gitea #72)
> **Christoph (2026-09-21):** *„haben tunnel/warp/cf schon eine ssot für die config? nautobot? tunnel
> sind essenzielle dinge, für die ich eine ssot möchte um drift zu vermeiden. zudem möchte ich auch
> from scratch ein full setup/recovery aus der nautobot-config fahren können."* → Umfang **„Voll:
> Tunnel+Ingress+DNS+Access"**, Platzierung **„Eigenes TP, GC bleibt hier"**.
#### Bestandsaufnahme (live 2026-09-21) — es gibt eine Doku-SSoT, aber keinen Master <a id="tp48"></a>
| Was | Wo | Stand | ### 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 |
|---|---|---| |---|---|---|
| Tunnel, WARP-Connector, Workers, Access-Apps, Hyperdrive, VPC, teamnet-Routen | Nautobot `nautobot.cloud`, Job **`CF Estate Sync`** (`jobs/cf_estate_sync.py`) | ✅ 4 `CloudNetwork` · 16 `CloudService` live | | TP-Index/Status | **Gitea** (seit TP36 — nicht Nautobot) | ❌ bleibt Gitea |
| **Ingress-Origin-Ziele** | **nirgends** | 🔴 `_ingress()` speichert nur `ingress_hostnames`, **nicht** das Origin | | Live-Gerätedaten (Compliance) | Nautobot DCIM + GC | ⚠️ Facts + `--check` |
| **DNS-Records** | **nirgends** | 🔴 nicht erfasst — live 24 Records / 8 proxied | | Soll-Config | GC-Templates (Jinja) | ✅ Templates/Inventar |
| **Access-Policies** | **nirgends** | 🔴 nur die Apps selbst | | Ist-Config | GC-Backup | ⚠️ `netcommon` backup |
| Richtung | CF → Nautobot | 🔴 **Pull-only** („CF bleibt operativer Master") | | Doku/Inventar (133 Interfaces, 38 IPs, 54 Leases) | Nautobot DB | ❓ offen — Ansible hat keine DB |
| `docs/cf-tunnel-mapping.md` | Repo | §1–16 eingefrorene As-Built-Historie · §0 = Live-Zeiger (altert: nennt `bf-public` „2 conns", live **12** = 3 AP-Connectoren × 4) |
**Antwort auf „full setup/recovery from scratch": heute nicht möglich.** Es fehlt (a) der Push-Weg, **Offene Punkte:** (1) was leistet Nautobot *unersetzlich* (Compliance-Historie, ConfigPlan-Audit,
(b) Ingress-Origins, (c) DNS, (d) Access-Policies. Und ein frisch erzeugter Tunnel liefert **neue ID GitRepository-Sync, Secret-Maskierung)? (2) Kann Ansible Inventar+Soll+Drift aus *einer* Git-Quelle
und neues Secret** → das **Token-Handoff an die Connectoren** (ap1/ap2/ap3) ist **Teil der Recovery**, bedienen? (3) Migrationskosten: 29 Features + 7 `config_replace`-Regeln nachbauen? (4) **Wo bleibt
kein Nebenschritt. 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?
#### Umfang **Abgrenzung:** nicht TP21/TP22 (bauen GC auf), nicht TP46 (CF-SSoT). Berührt die Governance-Regel
Tunnel + Ingress **inkl. Origin-Ziele** + DNS-Records + Access-Apps/Policies vollständig nach Nautobot; „Config nur über Nautobot" — bei Trägerwechsel neu zu fassen. Verwandt: TP30 (der WG-Privkey ist ein
**Push-Renderer** (Dry-Run + Diff); **Recovery-Runbook** für den Aufbau von null. Rotations-Kandidat). **Ergebnis = Empfehlung mit Zahlen, keine Migration.**
🔴 **Vor dem ersten Schritt prüfen, nicht annehmen:** welches Nautobot-Objekt DNS-Records tragen kann
(Kern-`ipam` vs. eigenes Modell), und ob das Ingress-Origin als `extra_config`-Feld reicht oder ein
eigenes Modell braucht.
- **Abh.:** TP44 Etappe 1/2 ✅ (native `nautobot.cloud`-Modelle + Pull-Job stehen). **Abgrenzung:**
Tunnel-**GC/Namensgebung** läuft in TP45 Schicht D; **CF-Writes** aus Nautobot sind hier *das Ziel*,
in TP37 ausdrücklich ausgeschlossen — die Grenze verschiebt sich also bewusst mit diesem TP.
--- ---
@@ -2516,7 +2342,7 @@ Kurzorientierung, wo was zu finden ist:
| **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 | | **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 | | **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 | | **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) Multi-Site-VPN *(offen nur der DDNS-Updater, s. Follow-ups)* · [TP16.1](teilprojekte-archiv.md#tp16-1) natives CF-Mesh | | **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) | | **Verworfen** | [TP9](teilprojekte-archiv.md#tp9) Gateway-HA (User-Entscheid 2026-08-08) |
## Reihenfolge / Abhängigkeiten ## Reihenfolge / Abhängigkeiten
@@ -2678,7 +2504,7 @@ TP22 · TP-R · **TP3.2** (08-17) · **TP5** (08-18) · **TP7.1** (08-18) · **T
</details> </details>
### TP43 — dw-Standort: interne BF-DNS-Namen über WG auflösbar machen ▸ ✅ **UMGESETZT 2026-09-11** — 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) ### 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` **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 nicht erreichen. Direkte IP `https://10.40.20.126:9443` geht ⇒ **reines DNS-, kein Routing-Problem.** Der
@@ -2751,3 +2577,109 @@ uns verfranzen. Geli kommt rein, das zählt."* — Diese Prämisse fiel weg: `42
public CF-Worker hinter CF Access (löst auf CF-Anycast), kein BF-Origin ⇒ dafür braucht es kein 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 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. **interne** Namen blieb dabei übersehen — deshalb reopened, s.o.
<a id="tp49"></a>
### 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`.**

View File

@@ -0,0 +1,78 @@
# Startauftrag — TP49: CF-Access `login_method`-Include ✅ ERLEDIGT (2026-10-06)
> **Abgeschlossen.** Gitea #76, Prosa in `docs/teilprojekte.md` → `### TP49`.
>
> 🔴 **Diese Datei nicht mehr als Auftrag lesen — der ist ausgeführt.** Sie bleibt als Nachweis
> stehen: Lage, Vorgehen und die Fehlschläge auf dem Weg dorthin.
## Der Auftrag (erledigt)
`login_method:github` aus dem `include` der Policy `christoph` der CF-Access-App
`420.teilzeitladen.xyz` entfernen — nach Rücksprache, wen der Zugang treffen soll.
## Ausgeführt (2026-10-06)
`github` von `include` nach **`require`** verschoben ⇒ **`email AND github`**.
| | vorher | jetzt |
|---|---|---|
| `include` | email · **github** · onetimepin | email · onetimepin |
| `require` | — | **github** |
Gesetzt über **`PUT /accounts/{id}/access/policies/{policy_id}`**, zurückgelesen über **beide**
Endpunkte (App-Pfad + reusable-Pfad) — identisch. Der `success:true` der API wurde **nicht**
geglaubt, sondern die Policy neu gelesen.
**App-ID** `e6a7991f-466c-4d1d-bac6-4b30fafccce4` · **Policy (reusable)** `8e44f5d1-8257-443e-a797-b3de939952ee`
**github-IdP** `72c1291b…` · **onetimepin** `9024c7a6…`
**Sweep über alle Apps** (`login_method` in include/require/exclude): genau zwei Treffer, beide in
`Terpenfinder v3` — `christoph` (github+onetimepin) und `geli` (nur onetimepin, harmlos).
Interne Apps/Wildcard: sauber (TP41).
## 🔴 Gotchas — live bestätigt, alle drei neu am 2026-10-06
1. **Reusable Policies ändern geht NICHT über den App-Pfad.**
`PUT …/access/apps/{app}/policies/{pol}` → **`12130 can not update reusable policies through
this endpoint`**. Korrekt: **`PUT …/access/policies/{policy_id}`**.
⚠️ Der ältere Merksatz „Reusable Policy nur per App-PUT detachbar" meint das **Detach** vom
App-`policies`-Array — **nicht** das Ändern des Inhalts. Zwei verschiedene Operationen.
2. **Der `cloudflare`-MCP ist strikt read-only.** Write-Probe: no-op-PUT (byte-identischer Body)
→ **`1010: undefined`**. Writes nur via `CF_API_TOKEN`/curl
(`cd mcp/cloudflare && set -a; . ./.env; set +a`). Token **nie** ausgeben.
3. **Access-App-IDs sind 32 Hex** — Kurzform `16aaec78` → `12045 invalid_application_id`.
## ⚠️ Was `login_method` nicht kann
Die ursprüngliche Absicht war eine Kopplung **an den GitHub-Username**. Das gibt Access nicht her:
`login_method` prüft die **Methode**, nicht den Nutzer; einen Username-Selector gibt es nicht.
Der einzige GitHub-bezogene Identitäts-Selector ist **„Identity provider group"** — der braucht
**SCIM**, und am hiesigen GitHub-IdP ist `scim_config.enabled = false`
(`GET …/scim/groups` → `500 (12158)`). **Weg zu.**
⇒ `email AND github` ist die vollständige Umsetzung des hier Möglichen, keine Notlösung.
Eine echte Org-Kopplung bräuchte SCIM aktiviert — **eigener TP**.
## Aktenhistorie — der Umweg (nicht löschen)
Zwischenzeitlich wurde TP49 als **Phantom geschlossen** — auf Basis eines **Google**-Tests plus
eines **Schlusses** auf GitHub. Beides falsch:
1. **Lese-Schluss als Befund verkauft.** „`include` ist ein OR" + „Google bindet die Adresse"
⇒ „GitHub bindet auch". Nie gemessen, aber als Tatsache in Akte und Issue geschrieben.
2. **Der Gegenbeweis war keiner.** Der Google-Test zeigt nur Googles Verhalten. Die IdP-Configs
sind konfigurationsgleich — das unterscheidende Feld steht **nicht** in der Config.
Christoph hat mit einem **zweiten GitHub-Account** getestet: er kam durch. Damit war der
Ursprungsfund bestätigt und der Fix nötig.
**Lehre: pro IdP einzeln messen, nicht den ersten Test auf den zweiten übertragen.**
## Verifikation
- [x] Policy über beide Endpunkte zurückgelesen — `require` trägt github, `include` nicht mehr
- [x] Sweep: keine weiteren `login_method` in `include` außer den bekannten
- [x] Scope: reusable Policy nur an diese eine App gebunden (kein Kollateral)
- [x] **Verhaltensabnahme (2026-10-06, echter Login):** fremder GitHub-Account → **abgewiesen**;
eigener Zugang + Geli → funktionieren; alle übrigen Auth-Pfade wie erwartet
**Status: abgeschlossen.** Gitea #76 `closed` / `status:done`. Kein offener Punkt.

View File

@@ -0,0 +1,123 @@
# WARP-Egress — nativer Consumer-WARP (WireGuard) auf dem crs326-core
> **Status:** ❌ **VERWORFEN 2026-09-26** — Cloudflare blockt inoffizielle Consumer-WARP-WireGuard-Clients.
> Live gebaut, verifiziert, dann vollständig zurückgebaut. Key + Backup bleiben zur Nachvollziehbarkeit liegen.
>
> ➡️ **Nachtrag §6 (selber Tag, zweiter Durchgang):** Der Mesh-Tunnel `cf-mesh` **trägt** als
> ziel-basierter Ausgang **und erreicht den Origin mit `200`** (Core kommt als `104.28.207.39`, `warp=on`,
> `colo=VIE` heraus — dieselbe IP wie der WARP-Client). **Lösung ist dauerhaft** über Nautobot/Golden Config
> aktiv (Template `nautobot/mikrotik_routeros.j2`, Abschnitt `[10b]`). Der frühere „Origin blockt CF-IPs"-
> Befund war ein Messfehler — korrigiert in §6.3.
## 1. Kontext / Warum
- Cannaleo-Origin `164.90.191.7` (blaueblueten.de, DigitalOcean multi-tenant, ~400 Vhosts) blockt die Heim-IP
`90.146.33.177` hart (leerer `403`, live 2026-09-25).
- **Scope (User-Vorgabe 2026-09-25):** nicht der ganze Outbound, sondern **genau eine Ziel-IP**
(`164.90.191.7/32` — blaueblueten.de apex + www, beide identisch). Nur **Privatbestellungen**, Traffic
überschaubar gering.
- **Terpenfinder wird NICHT über diesen Weg** in Betrieb genommen (eigene, saubere Lösung — siehe §5).
## 2. Was gebaut wurde (2026-09-26, live)
1. **Consumer-WARP-Key** per `wgcf` registriert (ViRb3 v2.3.0, Wegwerf-Konto via `api.cloudflareclient.com`).
→ `mcp/cloudflare/.env` (`CF_WARP_CLIENT_PRIVKEY`, gitignored) + `~/.secrets/wgcf-client/` (account + profil).
2. Auf `crs326-core` (WG-Zeilen via `sshpass -e`, Rest per MikroMCP/ssh):
- `/interface wireguard add name=warpout mtu=1280 private-key=… comment="WARP Egress (privat)"`
- Peer `warp-cf-pop`: public-key `bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=`,
endpoint `engage.cloudflareclient.com:2408`, `allowed-address=0.0.0.0/0`, `persistent-keepalive=25s`
- `/ip address add 172.16.0.2/32 interface=warpout`
- `/routing table add name=warpout fib` + `/ip route add 0.0.0.0/0 gateway=warpout routing-table=warpout`
- `/routing rule add dst-address=164.90.191.7/32 action=lookup-only-in-table table=warpout`
- NAT `masquerade out-interface=warpout` + forward `accept out-interface=warpout`
3. **Verifiziert:** Key korrekt (abgeleiteter Public-Key == Router-Public-Key
`HTFM3gj3FyT0EjWkG+2LLmvtxaFz1kH/P1iDwM0HswI=`), Konto „Active" (`wgcf status`).
## 3. Befund: Handshake schlägt fehl
- Peer `warp-cf-pop`: `rx=0`, kein `last-handshake` — Cloudflare antwortet **nie** auf den WG-Handshake,
egal welcher Endpoint (`162.159.192.1` / `.193.2` / `.193.5`).
- **Ursache (recherchiert):** Cloudflare blockt **inoffizielle WireGuard-Clients** für Consumer-WARP. Der
offizielle Client setzt ein spezielles Handshake-Feld (`reserved`/Client-ID, z. B. `b91981`), das RouterOS
(und jedes plain-wgcf-Profil) nicht setzt; dazu Abuse-Prevention. Der Mesh-Connector `cf-mesh` läuft nur,
weil das ein **anderes Produkt** ist (Zero Trust, nicht Consumer).
- Quellen: [ViRb3/wgcf Issue #542](https://github.com/ViRb3/wgcf/issues/542) ·
[#515](https://github.com/ViRb3/wgcf/issues/515) · [#158](https://github.com/ViRb3/wgcf/issues/158).
## 4. Abbau (2026-09-26)
- Alle 8 Objekte entfernt (Routing-Regel, Route, Tabelle, NAT, Firewall, Adresse, Peer, Interface). Core
wieder im Ursprungszustand — **keine Drift**, Golden-Config-Compliance unverändert.
- Backup `pre-warp-egress-20260925.backup` liegt auf dem Core (vor dem Versuch, TP22-Regel).
- `CF_WARP_CLIENT_PRIVKEY` bleibt in `mcp/cloudflare/.env` (nutzlos, da Client geblockt; kann bei Bedarf entfernt werden).
## 5. Weg vorwärts
> ⚠️ **Durch §6 überholt (2026-09-26).** Die Grundannahme hier — „geteilte CF-IPs werden geblockt, WARP
> liefert 403" — war **falsch** und ist in §6.3 korrigiert. WARP-Client und `cf-mesh`-Ausgang erreichen den
> Origin beide mit `200`. Eine Dedicated Egress IP ist damit nicht nötig.
- **Bleibt gültig:** Consumer-WARP über natives RouterOS-WG ist auf absehbare Zeit blockiert (separates
Produkt, betrifft nicht `cf-mesh`).
## 6. Nachtrag 2026-09-26: Mesh-Exit trägt und erreicht den Origin (200) — Lösung dauerhaft
**Frage:** Kann der bereits laufende `cf-mesh`-Tunnel (TP16.1) als *Ziel-basierter Ausgang* dienen, statt eines
separaten Consumer-WARP? **Antwort: ja — und er erreicht den Origin mit `200`.** Die Lösung ist dauerhaft über
Nautobot/Golden Config aktiv (Template-Abschnitt `[10b]`).
### 6.1 Der Origin-Weg ist jetzt vermessen (vorher nur angenommen)
`blaueblueten.de` geht **direkt** zum DO-Droplet, **kein Cloudflare im Pfad** (Zertifikat = Let's Encrypt
`CN=blaueblueten.de` vom 11.08., **kein `cf-ray`**, kein `server`-Header, kein `/cdn-cgi/trace` — CF setzt
`cf-ray` immer). Der 403 kommt also **vom Origin**, nicht vom Edge.
**Der Block ist rein quell-IP-basiert:** voller Safari-/Chrome-User-Agent ändert nichts, HTTP→HTTPS-Redirect
(`302`) funktioniert, erst der HTTPS-Request antwortet `403` mit `content-length: 0`.
### 6.2 Mesh als Exit — funktioniert
Temporär gebaut (Tabelle `meshtest` + `0.0.0.0/0` via `cf-mesh` + Regel nur auf `1.1.1.1/32`), dann gemessen:
```
/tool fetch url="http://1.1.1.1/cdn-cgi/trace" mode=http
→ ip=104.28.207.39 warp=on gateway=on colo=VIE
```
Der Core kommt also über den Mesh-Tunnel als **Cloudflare-IP** heraus (vorher `90.146.33.177`).
⚠️ **Merkregel: für `cdn-cgi/trace` `mode=http` nehmen, nicht `mode=https`** — über HTTPS liefert RouterOS nur
den 69-Byte-Datei-Header (`# … by RouterOS`, `# software id =`), die Nutzdaten fehlen. Das sieht wie ein
leerer Download aus, ist aber nur der falsche Fetch-Modus.
### 6.3 KORREKTUR: der frühere „403 über Mesh" war ein Messfehler — der Weg trägt
Der erste Test hatte **nur** die Regel `1.1.1.1/32 → meshtest` gesetzt und dann das Ziel gemessen. Für das
Ziel griff die Regel nicht → der Request lief über die **Heim-IP** (`90.146.33.177`) raus. Der gemeldete
`403` war also **403 über die Heim-IP**, nicht über den Mesh-Ausgang.
Beweis (A/B in einem Durchgang, Regel `164.90.191.7/32 → egress-cannaleo`):
```
Regel aktiv → https://blaueblueten.de/ → 200 (168 KiB)
Regel deaktiv → https://blaueblueten.de/ → 403 (fällt auf Heim-IP zurück)
```
Merke: `action=lookup-only-in-table` für Ziel A routet Ziel B **nicht** — nur messen, was tatsächlich
umgeleitet ist. Dieselbe CF-IP (`104.28.207.39`) erreicht den Origin via WARP-Client und via `cf-mesh`.
### 6.4 Rückbau (vollständig)
Alle Objekte entfernt (Regel, Route, Tabelle, Testdateien). `/routing table print` = nur `main`,
`/routing rule print` = leer, Default-Route wieder `90.146.33.1%vlan100`. `cf-mesh` unberührt und gesund
(Handshake 4 s). Keine Drift.
### 6.5 Was daraus folgt
- **Der Core-Teil ist gelöst und wiederverwendbar:** `routing-table` + `0.0.0.0/0`-Route + `routing rule
dst-address=<ziel>/32 action=lookup-only-in-table` + `masquerade out-interface=<wg-x>`. Nur **ein** Ziel
betroffen, Rest unberührt. Genau so gebaut, verifiziert, zurückgebaut.
- **Offen bleibt allein das Transportmittel.** CF scheidet aus (Ziel blockt CF). Der Rest des Vorschlags aus
§5 steht: eigener Ausgang über einen **externen WireGuard-Host**. Ein DO-VPS ist dabei der naheliegendste
Kandidat — der Miner fährt heute schon erfolgreich über eine DO-IP gegen genau diesen Origin.
- 🔴 **Der Core hat heute keinen zweiten Ausgang.** Kein zweites WAN, kein zweites Gateway — jeder künftige
Egress braucht erst ein Interface, das ihn trägt.

View File

@@ -926,6 +926,34 @@ def job_tp45_c_dns():
return [(["crs326-core"], "\n".join(lines), "TP45-c-dns")] return [(["crs326-core"], "\n".join(lines), "TP45-c-dns")]
def job_tp10_mesh_nat():
"""TP10 (2026-10-03): Mesh-WARP-Verkehr Richtung FM/DW-Spokes auf eine BF-IP NATen.
WARUM: die Spoke-FritzBoxen akzeptieren am WG-Peer nur Quell-IPs aus 10.40.0.0/16
(AllowedIPs). WARP-Clients kommen ueber cf-mesh mit 100.96.0.0/12 -> die FritzBox
verwirft den Verkehr (bewiesen: ping src=100.96.0.6 -> 100% Loss, src=10.40.20.1
-> 0%). Eine src-nat auf dem wg-hub-Egress legt die Quelle in den AllowedIPs-Raum.
`action=src-nat` (NICHT masquerade): wg-hub hat keine Interface-IP, masquerade
haette keine Quelle. src-nat auf eine feste BF-IP (10.40.10.1) haelt zudem das
Connection-Tracking -- die Antwort an 10.40.10.1 wird im prerouting auf die
Mesh-IP zurueckuebersetzt und via cf-mesh geroutet.
KEIN LOCKOUT-RISIKO: reine srcnat-Ergaenzung; der no-NAT-accept (dst 10.40.0.0/16)
und der WAN-masquerade bleiben unberuehrt, die Regel matcht nur out-interface=wg-hub.
"""
cmt = "TP10: NAT mesh source to WG spokes (AllowedIPs)"
find = f'find where comment="{cmt}"'
add = (f'/ip firewall nat add action=src-nat chain=srcnat comment="{cmt}" '
f'out-interface=wg-hub src-address=100.96.0.0/12 to-addresses=10.40.10.1')
lines = [
f':if ([:len [/ip firewall nat {find}]] = 0) do={{ {add} }} '
f'else={{ :log info "tp10-mesh-nat: Regel existiert schon" }}',
f':log info ("tp10-mesh-nat: regeln=" . [:tostr [:len [/ip firewall nat {find}]]])',
]
return [(["crs326-core"], "\n".join(lines), "TP10-mesh-nat")]
JOBS = { JOBS = {
"drift-cleanup": job_drift_cleanup, "drift-cleanup": job_drift_cleanup,
"mgmt-acl": job_mgmt_acl, "mgmt-acl": job_mgmt_acl,
@@ -938,6 +966,7 @@ JOBS = {
"tp45-v30-leases": job_tp45_v30_leases, "tp45-v30-leases": job_tp45_v30_leases,
"tp45-v30-revert": job_tp45_v30_revert, "tp45-v30-revert": job_tp45_v30_revert,
"tp45-v30-restore": job_tp45_v30_restore, "tp45-v30-restore": job_tp45_v30_restore,
"tp10-mesh-nat": job_tp10_mesh_nat,
} }

View File

@@ -359,6 +359,7 @@
# [7] NAT # [7] NAT
/ip firewall nat add action=accept chain=srcnat comment="TP4: no-NAT inter-VLAN (LAN-internal, before masquerade)" dst-address=10.40.0.0/16 /ip firewall nat add action=accept chain=srcnat comment="TP4: no-NAT inter-VLAN (LAN-internal, before masquerade)" dst-address=10.40.0.0/16
/ip firewall nat add action=masquerade chain=srcnat comment="defconf: masquerade" ipsec-policy=out,none out-interface-list=WAN /ip firewall nat add action=masquerade chain=srcnat comment="defconf: masquerade" ipsec-policy=out,none out-interface-list=WAN
/ip firewall nat add action=src-nat chain=srcnat comment="TP10: NAT mesh source to WG spokes (AllowedIPs)" out-interface=wg-hub src-address=100.96.0.0/12 to-addresses=10.40.10.1
# [7b] FIREWALL FILTER (TP26, 2026-08-17). Kommt als DATEN aus # [7b] FIREWALL FILTER (TP26, 2026-08-17). Kommt als DATEN aus
# local_config_context_data.firewall_filter, nicht als Templatezeilen — # local_config_context_data.firewall_filter, nicht als Templatezeilen —
@@ -396,6 +397,17 @@
/ip address add address=100.96.0.6 comment="TP16.1 Mesh-Overlay" interface=cf-mesh network=100.96.0.6 /ip address add address=100.96.0.6 comment="TP16.1 Mesh-Overlay" interface=cf-mesh network=100.96.0.6
/ip route add comment="TP16.1 Mesh" disabled=no distance=1 dst-address=100.96.0.0/12 gateway=cf-mesh /ip route add comment="TP16.1 Mesh" disabled=no distance=1 dst-address=100.96.0.0/12 gateway=cf-mesh
# [10b] EGRESS (2026-09-26) — ziel-basierter Ausgang fuer 164.90.191.7/32 (cannaleo/DO-Origin).
# Der Origin blockt die Heim-IP 90.146.33.177 hart (403), Cloudflare aber nicht: WARP-Client
# und cf-mesh kommen beide durch (live gemessen, gleiche Ausgangs-IP 104.28.x). Genau EIN Ziel,
# ein Ausgang — kein Default-Route-Eingriff, restlicher Traffic unberuehrt. Kein extra NAT noetig:
# masquerade out-interface-list=WAN greift fuer cf-mesh NICHT, CF-NATet die LAN-Source im Mesh
# selbst (verifiziert von macmini 10.40.20.167). Cave User: kein Zugriffsmuster, das Heim-IP und
# diesen Ausgang gemeinsam gegen den Origin zeigt — der Ausgang ist ausschliesslich fuer cannaleo.
/routing table add fib name=egress-cannaleo
/ip route add comment="TP-EGRESS: cannaleo via cf-mesh" disabled=no distance=1 dst-address=0.0.0.0/0 gateway=cf-mesh routing-table=egress-cannaleo
/routing rule add action=lookup-only-in-table comment="TP-EGRESS: cannaleo 164.90.191.7/32 via cf-mesh" disabled=no dst-address=164.90.191.7/32 table=egress-cannaleo
# [11] QoS — BEWUSST LEER (TP6 geschlossen 2026-08-23). # [11] QoS — BEWUSST LEER (TP6 geschlossen 2026-08-23).
# #
# Hier stand einen Tag lang eine CAKE-Queue. Sie ist entfernt, weil sie GEMESSEN # Hier stand einen Tag lang eine CAKE-Queue. Sie ist entfernt, weil sie GEMESSEN

View File

@@ -0,0 +1,112 @@
# =============================================================================
# TP10 — Cloudflare-DDNS-Updater fuer bf4.emsenhuber.net
# =============================================================================
# Zweck: der WG-Hub-Endpoint der Spokes (fm/dw) ist bf4.emsenhuber.net. Der
# A-Record muss der Liwest-Public-IP des Core folgen. Heute manuell gepflegt.
#
# BASIS: bajodel/mikrotik-cloudflare-dns (MIT, Copyright (c) 2025 bajodel)
# https://github.com/bajodel/mikrotik-cloudflare-dns
# Aenderungen ggue. dem Original (Christoph-Entscheid 2026-10-03):
# 1. IP kommt aus der DHCP-Lease (client1/vlan100), NICHT von Echo-Diensten.
# Liwest ist die Quelle, kein Dritter; kein Cloudflare im Pfad.
# 2. check-certificate=yes (Original-Default war false -> Token ungeprueft).
# 3. Nur EIN Record (bf4), Konfiguration auf das Noetige reduziert.
#
# Token: liegt NICHT hier, sondern als globale Script-Variable `cfDdnsToken`.
# Einmalig setzen per SSH (siehe docs/teilprojekte.md ### TP10).
#
# Installation: /import file=tp10-ddns-cf-updater.rsc
# danach /system scheduler add ... (siehe unten)
# =============================================================================
# ---- Konfiguration ----------------------------------------------------------
:local DnsRcName "bf4.emsenhuber.net"
:local DnsZoneID "1b5bc02fdf61b6b989afde543f3f4134" # CF_ZONE_ID aus mcp/cloudflare/.env
:local DnsRcrdID "dba9d9ec3b261fdb314c0f83e4e62f0d"
:local RecordTTL "120" # wie der bestehende Record
:local RecordProxied "false" # WG-Endpoint: NICHT proxied
:local VerboseLog false
:local WanDhcpName "client1"
# -----------------------------------------------------------------------------
# Token aus /system script environment holen (NIE im Skriptkopf, NIE per :global).
# Grund (2026-10-03): :global cfDdnsToken liest im Skript-Kontext "nothing", obwohl
# /system script environment cfDdnsToken gesetzt ist. CLI-Scope und Skript-Scope sind
# auf RouterOS 7.24 unterschiedlich. `/system script environment get` ist scope-unabhaengig.
:do {
:local tk ""
:local found [/system script environment find name=cfDdnsToken]
:if ([:len $found] > 0) do={
:set tk [/system script environment get [find name=cfDdnsToken] value]
}
:if ([:typeof $tk] = "") do={
:log error "[tp10-ddns] cfDdnsToken nicht gesetzt - Abbruch"
:error "cfDdnsToken fehlt"
}
:global cfDdnsToken
:set cfDdnsToken $tk
} on-error={
:log error "[tp10-ddns] cfDdnsToken nicht lesbar - Abbruch"
:error "cfDdnsToken fehlt"
}
# ---- WAN-IPv4 aus der DHCP-Lease (Quelle: Liwest, kein Dritter) ------------
:local CFWanIP4New ""
:do {
:local leaseAddr [/ip dhcp-client get [find name=$WanDhcpName] address]
:if ([:typeof $leaseAddr] != "nothing") do={
# "90.146.33.177/24" -> "90.146.33.177"
:local slashPos [:find $leaseAddr "/"]
:if ($slashPos >= 0) do={
:set CFWanIP4New [:pick $leaseAddr 0 $slashPos]
} else={
:set CFWanIP4New $leaseAddr
}
}
} on-error={
:log error "[tp10-ddns] DHCP-Lease nicht lesbar ($WanDhcpName)"
}
:if ($CFWanIP4New = "") do={
:log warning "[tp10-ddns] Keine WAN-IP aus der Lease, naechster Lauf"
} else={
# ---- Guard: nur oeffentliche IPv4 zulassen --------------------------------
# Faengt den Fall ab, dass DHCP Muell liefert (privat/CGNAT) -> kein Write.
:local isPrivate false
:if ([:pick $CFWanIP4New 0 3] = "10.") do={ :set isPrivate true }
:if ([:pick $CFWanIP4New 0 8] = "192.168.") do={ :set isPrivate true }
:if ([:pick $CFWanIP4New 0 7] = "172.16.") do={ :set isPrivate true }
:if ([:pick $CFWanIP4New 0 8] = "100.64.0") do={ :set isPrivate true } # CGNAT 100.64/10
:if ($isPrivate) do={
:log error "[tp10-ddns] Lease liefert nicht-oeffentliche IP ($CFWanIP4New) - ABBRUCH, kein Write"
} else={
# ---- Nur bei Abweichung schreiben (Idempotenz) --------------------------
:global CFWanIP4Cur
:if ($CFWanIP4New != $CFWanIP4Cur) do={
:if ($VerboseLog) do={ :log info "[tp10-ddns] WAN-IP: $CFWanIP4Cur -> $CFWanIP4New" }
# Kommentar am Record: Zeit + Geraet, wie im Original
:local ds [/system clock get date]
:local ts [/system clock get time]
:local datetime ([:pick $ds 0 4] . "." . [:pick $ds 5 7] . "." . [:pick $ds 8 10] . "-" . [:pick $ts 0 2] . ":" . [:pick $ts 3 5])
:local CFComment ("$datetime by crs326-core")
:local url "https://api.cloudflare.com/client/v4/zones/$DnsZoneID/dns_records/$DnsRcrdID/"
:do {
:local res [/tool fetch http-method=put mode=https url=$url check-certificate=yes output=user as-value http-header-field="Authorization: Bearer $cfDdnsToken,Content-Type: application/json" http-data="{\"type\":\"A\",\"name\":\"$DnsRcName\",\"content\":\"$CFWanIP4New\",\"ttl\":$RecordTTL,\"proxied\":$RecordProxied,\"comment\":\"$CFComment\"}"]
:if (($res->"status") = "finished") do={
:log warning "[tp10-ddns] $DnsRcName -> $CFWanIP4New aktualisiert"
:set CFWanIP4Cur $CFWanIP4New
} else={
:log error "[tp10-ddns] Update fehlgeschlagen (status != finished)"
}
} on-error={
:log error "[tp10-ddns] Exception beim Cloudflare-Update - naechster Lauf"
}
} else={
:if ($VerboseLog) do={ :log info "[tp10-ddns] WAN-IP unveraendert ($CFWanIP4New)" }
}
}
}