Compare commits
33 Commits
7edf01b021
...
claude/pri
| Author | SHA1 | Date | |
|---|---|---|---|
| 909e604ea6 | |||
| 614025eafb | |||
| 677dade184 | |||
| b20edd6b62 | |||
| a38ed0fe1d | |||
| 5a29c482bd | |||
| 4eba3673b7 | |||
| 2b856d3d42 | |||
| f1d565bc88 | |||
| 43c592e5d4 | |||
| 33b18a08db | |||
| 799b693ffa | |||
| 4aeced30b5 | |||
| 89d94dbd45 | |||
| 6f60e03679 | |||
| 2f4866ca9a | |||
| 8e41268b4b | |||
| b915d02a82 | |||
| efb03979a0 | |||
| 268fd859ab | |||
| 7a20a1db4f | |||
| bca8f6e27e | |||
| d63dc7bb78 | |||
| b200401c5a | |||
| 384f356290 | |||
| e96fca5a52 | |||
| 1625c30fd7 | |||
| 83fd69ef8d | |||
| 56746cb24d | |||
| a0ba87d555 | |||
| 674e5fff0d | |||
| c024f85579 | |||
| 9caf145341 |
32
CLAUDE.md
32
CLAUDE.md
@@ -6,7 +6,7 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
|
|||||||
|
|
||||||
## Pflichtlektüre pro Sitzung (Reihenfolge)
|
## Pflichtlektüre pro Sitzung (Reihenfolge)
|
||||||
|
|
||||||
1. **`docs/teilprojekte.md`** (Quick-Index-Block) — kanonischer Stand, Scope, Abhängigkeiten, **offene** TPs. **1 Read reicht.**
|
1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Für den **Detail/Kontext** eines TP dann den `### TPnn`-Prosa-Abschnitt in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`** lesen. *(Die frühere „Quick-Index-Block, 1 Read reicht"-Zeile ist überholt — Index/Status stehen nicht mehr in der Datei.)*
|
||||||
2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat.
|
2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat.
|
||||||
3. Bei Live-Writes oder Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/teilprojekte-archiv.md`**.
|
3. Bei Live-Writes oder Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/teilprojekte-archiv.md`**.
|
||||||
4. ✅ **TP16.3 §5 durch (2026-09-01):** [`docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md`](docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md)
|
4. ✅ **TP16.3 §5 durch (2026-09-01):** [`docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md`](docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md)
|
||||||
@@ -34,6 +34,21 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
|
|||||||
6. ap3-Bring-up 2026-08-30: [`docs/handoff-20260830-ap3.md`](docs/handoff-20260830-ap3.md) — sechs Funde zu Werks-MikroTiks, CAPsMAN und Golden-Config-Lücken.
|
6. ap3-Bring-up 2026-08-30: [`docs/handoff-20260830-ap3.md`](docs/handoff-20260830-ap3.md) — sechs Funde zu Werks-MikroTiks, CAPsMAN und Golden-Config-Lücken.
|
||||||
7. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift.
|
7. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift.
|
||||||
|
|
||||||
|
## SSoT & Branch-Konvention (TP36-Cutover 2026-09-07)
|
||||||
|
|
||||||
|
- **SSoT-Dreiteilung:** **Index/Status = Gitea-Issues** (`admin/bf-infra`, Labels `status:*`/`thema:*`) ·
|
||||||
|
**Prosa/Detail = Repo** (`docs/teilprojekte.md` aktiv + `teilprojekte-archiv.md`, `### TPnn`-Abschnitte) ·
|
||||||
|
**Live-Gerätedaten = Nautobot** (Compliance-Zahl, RouterOS-Version …). Ein Status ändert sich **in Gitea**,
|
||||||
|
nicht mehr in einer Repo-Datei. Sync-/Abgleich-Werkzeug fürs Archiv: `bin/gitea-tp-sync.py` (Dry-Run default,
|
||||||
|
Token aus `~/.hermes/.env`, **nie ausgeben**).
|
||||||
|
- **Branch-Konvention:** Branch `tpNN-kurz` **jederzeit** mitten in der Session möglich (`git switch -c`,
|
||||||
|
uncommittete Arbeit wandert mit); für Multi-Session-TPs der Normalfall. **Merge nach `main`, sobald der
|
||||||
|
Code/Config des TP steht.** Weil Steuerungs-Updates nach **Gitea** gehen (nicht in `teilprojekte.md`),
|
||||||
|
kollidieren TP-Branches nicht mehr auf der Steuerungs-Doku — nur noch der eigene `### TPnn`-Prosa-Abschnitt
|
||||||
|
wird angefasst.
|
||||||
|
- **TP anlegen/schließen:** neues aktives TP = **direkt als Gitea-Issue** (Labels setzen), Prosa-Abschnitt
|
||||||
|
im Repo dazu; abschließen = Issue `closed`, Prosa wandert bei Bedarf ins Archiv. **Keine Handtabelle mehr.**
|
||||||
|
|
||||||
## Was du sofort wissen musst (Inventur 2026-08-27)
|
## Was du sofort wissen musst (Inventur 2026-08-27)
|
||||||
|
|
||||||
- **`bf-infra`** ist aktiver Master **mit Gitea-Remote**: `origin` = `https://gitea.emsenhuber.net/admin/bf-infra.git` (live verifiziert 2026-08-30). ⚠️ **Die frühere Zeile „lokal, kein Gitea-Remote → verloren bei Festplattencrash" ist überholt** — sie stammt aus der Inventur vom 2026-08-27 und war damals richtig; das Remote existiert seit spätestens 2026-08-28 (`origin/main` = `b917691`). **Die Backup-Lücke ist damit keine Struktur-, sondern eine Disziplinfrage: `git push`.** Am 2026-08-30 lagen vier Commits nur lokal. 🔴 **Das Remote ist nur so viel wert wie der letzte Push** — der Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5) bricht Commits ab, solange kein Backup markiert ist; **das ist die Erinnerung ans Pushen, kein Hindernis.** Nicht mit `--no-verify` umgehen. 🔴 **Merkregel:** diese Zeile hat eine Session dazu gebracht, „kein Remote" zu behaupten, statt `git remote -v` zu tippen — vor jeder Aussage über Remotes/Backups **live prüfen**.
|
- **`bf-infra`** ist aktiver Master **mit Gitea-Remote**: `origin` = `https://gitea.emsenhuber.net/admin/bf-infra.git` (live verifiziert 2026-08-30). ⚠️ **Die frühere Zeile „lokal, kein Gitea-Remote → verloren bei Festplattencrash" ist überholt** — sie stammt aus der Inventur vom 2026-08-27 und war damals richtig; das Remote existiert seit spätestens 2026-08-28 (`origin/main` = `b917691`). **Die Backup-Lücke ist damit keine Struktur-, sondern eine Disziplinfrage: `git push`.** Am 2026-08-30 lagen vier Commits nur lokal. 🔴 **Das Remote ist nur so viel wert wie der letzte Push** — der Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5) bricht Commits ab, solange kein Backup markiert ist; **das ist die Erinnerung ans Pushen, kein Hindernis.** Nicht mit `--no-verify` umgehen. 🔴 **Merkregel:** diese Zeile hat eine Session dazu gebracht, „kein Remote" zu behaupten, statt `git remote -v` zu tippen — vor jeder Aussage über Remotes/Backups **live prüfen**.
|
||||||
@@ -42,7 +57,7 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
|
|||||||
- **`admin/blaueblueten-n8n`** (Gitea) ist Master für terpene-Repo.
|
- **`admin/blaueblueten-n8n`** (Gitea) ist Master für terpene-Repo.
|
||||||
- **`inventory/` ist tot** (Pipeline seit 2025-06 stale). Live-Daten aus MikroMCP/CF/Portainer.
|
- **`inventory/` ist tot** (Pipeline seit 2025-06 stale). Live-Daten aus MikroMCP/CF/Portainer.
|
||||||
- **`mcp/INVENTORY.md`** ist BF-Netz-SSoT; `~/.claude.json`/`opencode.jsonc` sind globalere Werkzeugkästen.
|
- **`mcp/INVENTORY.md`** ist BF-Netz-SSoT; `~/.claude.json`/`opencode.jsonc` sind globalere Werkzeugkästen.
|
||||||
- **terpene.teilzeitladen.xyz**: Worker läuft auf Mac mini (`10.40.20.167:8890`), **kein Caddy-Block, kein CF-Tunnel** — Backfill via DB-Migration blockiert (Handoff 2026-08-27).
|
- **Terpenfinder = `420.teilzeitladen.xyz`** (CF-Worker, Repo `admin/blaueblueten-n8n`, hinter CF Access), **kein Caddy, kein Mac mini im Pfad**. `terpene.*` **existiert nicht und soll es nicht** (live geprüft 2026-09-10). Einziger terpene-benannter CF-Gegenstand: der **interne** Tunnel `terpene-db` (`f2d2fc8a`, ohne öffentlichen Hostname, Hyperdrive→VPC→Postgres, s. `docs/cf-tunnel-mapping.md` §17). ⚠️ **Grenze:** blaueblüten ist ein eigenes Projekt — nicht anfassen. *(Die frühere „terpene.teilzeitladen.xyz:8890 via Tunnel"-Zeile war veraltet — bf-mm ist gelöscht.)*
|
||||||
- **shelly-mcp / portainer-mcp bewusst NICHT als MCP-Client** (User-Direktive 2026-08-28): shelly-mcp-Container auf DS923 wird via **Portainer-API** (Skript-Aufrufe, `mcp/portainer/.env` als Token-Quelle) deployt, nicht via MCP-Protokoll. Inventar-Eintrag spiegelt das. `mcp-host-policy.md` ist SSoT für die Host-/IP-Konvention.
|
- **shelly-mcp / portainer-mcp bewusst NICHT als MCP-Client** (User-Direktive 2026-08-28): shelly-mcp-Container auf DS923 wird via **Portainer-API** (Skript-Aufrufe, `mcp/portainer/.env` als Token-Quelle) deployt, nicht via MCP-Protokoll. Inventar-Eintrag spiegelt das. `mcp-host-policy.md` ist SSoT für die Host-/IP-Konvention.
|
||||||
- **Mac mini `10.40.20.167`** ist der **n8n-Host** (n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — **NICHT** der zentrale MCP-Host. Performance-Überlegung: MCP-Clients würden auf dem macmini (User-Workstation) laufen und die Box zusätzlich belasten; deshalb liegt der MCP-Host auf der **DS923** (`nas-bf`, Portainer Endpoint 14). gitea-mcp läuft aktuell noch auf dem macmini (historisch, eigene Container-Definition dort) — gehört **bei Gelegenheit** auf die DS923 umgezogen (separater Punkt, **nicht** Teil dieser Session). Container-Definitionen für n8n leben auf dem macmini (Docker Desktop); FQDN→Backend-Mapping im `caddy/Caddyfile`.
|
- **Mac mini `10.40.20.167`** ist der **n8n-Host** (n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — **NICHT** der zentrale MCP-Host. Performance-Überlegung: MCP-Clients würden auf dem macmini (User-Workstation) laufen und die Box zusätzlich belasten; deshalb liegt der MCP-Host auf der **DS923** (`nas-bf`, Portainer Endpoint 14). gitea-mcp läuft aktuell noch auf dem macmini (historisch, eigene Container-Definition dort) — gehört **bei Gelegenheit** auf die DS923 umgezogen (separater Punkt, **nicht** Teil dieser Session). Container-Definitionen für n8n leben auf dem macmini (Docker Desktop); FQDN→Backend-Mapping im `caddy/Caddyfile`.
|
||||||
|
|
||||||
@@ -50,12 +65,15 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
|
|||||||
|
|
||||||
Wenn während einer Session ein neues Teilprojekt, eine Abhängigkeit oder ein Querschnittsthema sichtbar wird, das bisher in keinem TP abgebildet ist:
|
Wenn während einer Session ein neues Teilprojekt, eine Abhängigkeit oder ein Querschnittsthema sichtbar wird, das bisher in keinem TP abgebildet ist:
|
||||||
- Vorschlagen und mit dir abstimmen (nicht eigenmächtig anlegen).
|
- Vorschlagen und mit dir abstimmen (nicht eigenmächtig anlegen).
|
||||||
- Auf dein Okay hin anlegen: Quick-Index-Zeile + `### TP<n>`-Abschnitt mit Status `🔜`, Scope,
|
- Auf dein Okay hin anlegen: **Gitea-Issue** `TPnn — <Kurztitel>` mit Labels `status:🔜`(todo) +
|
||||||
Abhängigkeiten, Abgrenzung. Nächste freie Nummer vergeben (höchste vergebene + 1; Lücken aus
|
passendem `thema:*` + Body (Anriss + Verweis auf den Prosa-Abschnitt) **und** einen
|
||||||
verworfenen TPs bleiben frei).
|
`### TP<n>`-Prosa-Abschnitt in `docs/teilprojekte.md` (Scope, Abhängigkeiten, Abgrenzung).
|
||||||
- Ist-Stand-Block mitziehen (Datum, betroffene Zeilen).
|
Nächste freie Nummer vergeben (höchste vergebene + 1; Lücken aus verworfenen TPs bleiben frei).
|
||||||
|
- **Status/Reihenfolge/„was lief zuletzt" gehen ins Gitea-Issue** (Label ändern, Kommentar), **nicht**
|
||||||
|
in eine Quick-Index-Tabelle oder einen Ist-Stand-Block — die gibt es seit dem TP36-Cutover (2026-09-07)
|
||||||
|
nicht mehr in der Datei.
|
||||||
|
|
||||||
**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt.** Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`) — und der **Ist-Stand-Block wird bei jeder Änderung mitgezogen**, nicht nur der betroffene TP-Abschnitt.
|
**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis) und ihr Gitea-Issue ist `closed` (Label `status:✅`/`❌`). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt** (Issue dann `reopen`). Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`) — nur noch der betroffene `### TPnn`-Prosa-Abschnitt wird angefasst.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
18
__init__.py
Normal file
18
__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""Macht die bf-infra-Repo-Wurzel zu einem regulären Python-Paket.
|
||||||
|
|
||||||
|
🔴 LOAD-BEARING — NICHT löschen (sonst laden die Nautobot-Jobs nicht mehr).
|
||||||
|
|
||||||
|
Nautobot klont dieses Repo nach GIT_ROOT/bf_infra/ und lädt Jobs über
|
||||||
|
`import_modules_privately()` → `pkgutil.walk_packages([GIT_ROOT])`. walk_packages
|
||||||
|
entdeckt ausschließlich *reguläre* Pakete (mit __init__.py), niemals
|
||||||
|
Namespace-Packages (Verzeichnisse ohne __init__.py). Ohne diese Datei ist
|
||||||
|
`bf_infra` für den Loader unsichtbar und `jobs/` wird nie importiert — Symptom:
|
||||||
|
Git-Sync SUCCESS, aber Log "No jobs were registered on loading the
|
||||||
|
`bf_infra.jobs` submodule" und kein Job-DB-Eintrag.
|
||||||
|
|
||||||
|
Der module_path-Filter des Loaders (["bf_infra","jobs"]) importiert nur
|
||||||
|
`bf_infra` + `bf_infra.jobs.*`; die übrigen Verzeichnisse (nautobot/, routeros/,
|
||||||
|
bin/, …) bleiben unangetastet und laufen weiter als Standalone-Skripte.
|
||||||
|
|
||||||
|
Verifiziert 2026-09-10 (Nautobot 3.1.7, Python 3.14) — TP44 Teil B.
|
||||||
|
"""
|
||||||
218
bin/gitea-tp-sync.py
Normal file
218
bin/gitea-tp-sync.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TP36 (reframed) — teilprojekte.md Index/Status → Gitea Issues (admin/bf-infra).
|
||||||
|
|
||||||
|
Idempotenter Sync (get-or-create, Match ueber "TPnn" im Titel):
|
||||||
|
- Archiv-Header in docs/teilprojekte-archiv.md -> closed Issues (nur TPs, die in Gitea
|
||||||
|
NICHT offen sind; ein reopentes TP mit Archiv-Zeiger bleibt offen).
|
||||||
|
- Labels status:* (Marker) + thema:* (Gruppierung) werden vorab get-or-create.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
python3 bin/gitea-tp-sync.py # Dry-Run (default) — zeigt, was passieren wuerde
|
||||||
|
python3 bin/gitea-tp-sync.py --commit # legt Labels + Issues an / gleicht sie ab
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
ACTIVE = REPO / "docs" / "teilprojekte.md"
|
||||||
|
ARCHIV = REPO / "docs" / "teilprojekte-archiv.md"
|
||||||
|
HERMES = Path.home() / ".hermes" / ".env"
|
||||||
|
OWNER_REPO = "admin/bf-infra"
|
||||||
|
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 = {
|
||||||
|
"status:todo": "#ededed", "status:wip": "#fbca04", "status:paused": "#c5def5",
|
||||||
|
"status:idea": "#d4c5f9", "status:concept": "#bfdadc", "status:done": "#0e8a16",
|
||||||
|
"status:dropped": "#e11d21",
|
||||||
|
"thema:firewall-dns": "#b60205", "thema:wlan": "#1d76db", "thema:cloudflare-proxy": "#f9a825",
|
||||||
|
"thema:nautobot-doku": "#5319e7", "thema:infra": "#0e8a16", "thema:ipv6-matter": "#006b75",
|
||||||
|
"thema:identity": "#e99695", "thema:backlog": "#cccccc",
|
||||||
|
}
|
||||||
|
# TP-Nummer -> Themen-Label (aus dem Statusbaum-Gruppierung; nur aktive TPs brauchen ein Board-Thema)
|
||||||
|
THEME = {
|
||||||
|
"5.1": "firewall-dns", "23": "firewall-dns",
|
||||||
|
"25": "wlan", "28": "wlan", "13": "wlan", "17.1": "wlan", "17.3": "wlan",
|
||||||
|
"16": "cloudflare-proxy", "16.2": "cloudflare-proxy", "16.3": "cloudflare-proxy",
|
||||||
|
"31": "cloudflare-proxy", "41": "cloudflare-proxy", "14.4": "cloudflare-proxy",
|
||||||
|
"42": "cloudflare-proxy", "43": "cloudflare-proxy", "44": "cloudflare-proxy",
|
||||||
|
"32": "nautobot-doku", "34": "nautobot-doku", "35": "nautobot-doku",
|
||||||
|
"36": "nautobot-doku", "37": "nautobot-doku",
|
||||||
|
"30": "infra", "40": "infra", "20.1": "infra", "18": "infra",
|
||||||
|
"24": "infra", "38": "infra", "11": "infra",
|
||||||
|
"12": "ipv6-matter", "12.1": "ipv6-matter",
|
||||||
|
"8": "identity",
|
||||||
|
"29": "backlog", "33": "backlog",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def hermes_env():
|
||||||
|
tok = host = None
|
||||||
|
for line in HERMES.read_text().splitlines():
|
||||||
|
m = re.match(r"\s*(GITEA_ACCESS_TOKEN|GITEA_HOST)\s*=\s*(.+?)\s*$", line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
if m.group(1) == "GITEA_ACCESS_TOKEN":
|
||||||
|
tok = m.group(2).strip().strip('"').strip("'")
|
||||||
|
else:
|
||||||
|
host = m.group(2).strip().strip('"').strip("'")
|
||||||
|
if not tok:
|
||||||
|
sys.exit("GITEA_ACCESS_TOKEN nicht in %s gefunden." % HERMES)
|
||||||
|
host = (host or "https://gitea.emsenhuber.net").rstrip("/")
|
||||||
|
if not host.startswith("http"):
|
||||||
|
host = "https://" + host
|
||||||
|
return tok, host
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN, HOST = hermes_env()
|
||||||
|
API = HOST + "/api/v1"
|
||||||
|
|
||||||
|
|
||||||
|
def api(method, path, body=None):
|
||||||
|
url = API + path
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
req = urllib.request.Request(url, data=data, method=method, headers={
|
||||||
|
"Authorization": "token " + TOKEN,
|
||||||
|
"Accept": "application/json", "Content-Type": "application/json"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return r.status, json.load(r)
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, (json.load(e) if e.headers.get("content-type", "").startswith("application/json") else {})
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- parse
|
||||||
|
def parse_archiv(exclude_nums):
|
||||||
|
"""Archiv-Header -> archivierte TPs. exclude_nums = TP-Nummern, die in Gitea OFFEN sind
|
||||||
|
(reopente TPs mit Archiv-Zeiger) -> die bleiben offen, nicht als closed abgleichen."""
|
||||||
|
out = []
|
||||||
|
hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\s*—\s*(.+?)\s*▸", )
|
||||||
|
seen = set()
|
||||||
|
for line in ARCHIV.read_text().splitlines():
|
||||||
|
m = hdr.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
tp, title = m.groups()
|
||||||
|
num = tp[2:]
|
||||||
|
if num in exclude_nums or num in seen:
|
||||||
|
continue
|
||||||
|
seen.add(num)
|
||||||
|
short = re.sub(r"\*\*|`", "", title).strip()[:80]
|
||||||
|
out.append({"tp": tp, "num": num, "marker": "✅",
|
||||||
|
"title": short, "date": "", "src": "archiv"})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- gitea sync
|
||||||
|
def ensure_labels(existing):
|
||||||
|
by_name = {l["name"]: l for l in existing}
|
||||||
|
for name, color in LABEL_COLORS.items():
|
||||||
|
if name in by_name:
|
||||||
|
continue
|
||||||
|
if COMMIT:
|
||||||
|
api("POST", f"/repos/{OWNER_REPO}/labels",
|
||||||
|
{"name": name, "color": color.lstrip("#")})
|
||||||
|
print(f" [{'+' if COMMIT else 'DRY'}] label {name}")
|
||||||
|
st, cur = api("GET", f"/repos/{OWNER_REPO}/labels?limit=100")
|
||||||
|
return {l["name"]: l["id"] for l in (cur if isinstance(cur, list) else [])}
|
||||||
|
|
||||||
|
|
||||||
|
def all_issues():
|
||||||
|
out, page = [], 1
|
||||||
|
while True:
|
||||||
|
st, d = api("GET", f"/repos/{OWNER_REPO}/issues?state=all&type=issues&limit=50&page={page}")
|
||||||
|
if not isinstance(d, list) or not d:
|
||||||
|
break
|
||||||
|
out += d
|
||||||
|
page += 1
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"Gitea {HOST} repo {OWNER_REPO} mode={'COMMIT' if COMMIT else 'DRY-RUN'}")
|
||||||
|
|
||||||
|
st, existing_labels = api("GET", f"/repos/{OWNER_REPO}/labels?limit=100")
|
||||||
|
label_id = ensure_labels(existing_labels if isinstance(existing_labels, list) else [])
|
||||||
|
|
||||||
|
issues = all_issues()
|
||||||
|
by_tp = {}
|
||||||
|
open_nums = set()
|
||||||
|
for iss in issues:
|
||||||
|
# Titel ist "TPxx — …"; volle ID inkl. Buchstaben/Bindestrich (TP14.2a, TP-R) bis zum " — "
|
||||||
|
m = re.match(r"^(TP[\w.\-]*?)\s+—", iss["title"])
|
||||||
|
if m:
|
||||||
|
by_tp[m.group(1)] = iss
|
||||||
|
if iss.get("state") == "open":
|
||||||
|
open_nums.add(m.group(1)[2:])
|
||||||
|
|
||||||
|
# Aktive TPs sind seit dem TP36-Cutover SSoT direkt in Gitea -> nur das Archiv abgleichen,
|
||||||
|
# und offene (reopente) TPs davon ausschliessen, damit sie nicht faelschlich geschlossen werden.
|
||||||
|
items = parse_archiv(open_nums)
|
||||||
|
print(f"Geparst: {len(items)} archivierte TP (aktive TPs = SSoT in Gitea: "
|
||||||
|
f"{len(open_nums)} offen, nicht angetastet)\n")
|
||||||
|
|
||||||
|
created = updated = 0
|
||||||
|
for it in items:
|
||||||
|
status_label, state = MARKER.get(it["marker"], ("status:todo", "open"))
|
||||||
|
labels = [status_label]
|
||||||
|
th = THEME.get(it["num"])
|
||||||
|
if th:
|
||||||
|
labels.append(f"thema:{th}")
|
||||||
|
title = f"{it['tp']} — {it['title']}"
|
||||||
|
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"(Abschnitt `{it['tp']}`).\n\n"
|
||||||
|
f"- Status: {it['marker']} ({status_label})\n"
|
||||||
|
+ (f"- Abschluss: {it['date']}\n" if it["date"] else ""))
|
||||||
|
want_ids = [label_id[l] for l in labels if l in label_id]
|
||||||
|
|
||||||
|
cur = by_tp.get(it["tp"])
|
||||||
|
if cur is None:
|
||||||
|
print(f" [{'+' if COMMIT else 'DRY'}] NEW {title} {labels} state={state}")
|
||||||
|
if COMMIT:
|
||||||
|
sc, res = api("POST", f"/repos/{OWNER_REPO}/issues",
|
||||||
|
{"title": title, "body": body, "labels": want_ids})
|
||||||
|
if state == "closed" and isinstance(res, dict) and res.get("number"):
|
||||||
|
api("PATCH", f"/repos/{OWNER_REPO}/issues/{res['number']}", {"state": "closed"})
|
||||||
|
created += 1
|
||||||
|
else:
|
||||||
|
have = {l["name"] for l in cur.get("labels", [])}
|
||||||
|
drift = (have != set(labels)) or (cur.get("state") != state)
|
||||||
|
if drift:
|
||||||
|
print(f" [{'~' if COMMIT else 'DRY'}] UPD #{cur['number']} {title} "
|
||||||
|
f"{sorted(have)}->{labels} state {cur.get('state')}->{state}")
|
||||||
|
if COMMIT:
|
||||||
|
api("PATCH", f"/repos/{OWNER_REPO}/issues/{cur['number']}",
|
||||||
|
{"labels": want_ids, "state": state})
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
print(f"\n{'Angelegt' if COMMIT else 'Wuerde anlegen'}: {created} · "
|
||||||
|
f"{'Abgeglichen' if COMMIT else 'Wuerde abgleichen'}: {updated}")
|
||||||
|
if not COMMIT:
|
||||||
|
print("Dry-Run — nichts geschrieben. Mit --commit ausfuehren.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -3,14 +3,38 @@
|
|||||||
> 🆕 **SSoT-Wechsel im Aufbau (TP44, seit 2026-09-02):** Die CF-Estate (Tunnel, WARP-Connector,
|
> 🆕 **SSoT-Wechsel im Aufbau (TP44, seit 2026-09-02):** Die CF-Estate (Tunnel, WARP-Connector,
|
||||||
> VPC-Services, Hyperdrive, Workers, Access-Apps) wird als **native `nautobot.cloud`-Objekte** in
|
> VPC-Services, Hyperdrive, Workers, Access-Apps) wird als **native `nautobot.cloud`-Objekte** in
|
||||||
> Nautobot (`10.40.20.30`) geführt — Etappe 1 (manuelle Erstbefüllung) ✅. Seeder:
|
> Nautobot (`10.40.20.30`) geführt — Etappe 1 (manuelle Erstbefüllung) ✅. Seeder:
|
||||||
> `nautobot/tp36-cf-estate-seed.py`, Detail: `docs/handoff-20260902-cf-estate-nautobot-ssot.md`.
|
> `nautobot/tp44-cf-estate-seed.py`, Detail: `docs/handoff-20260902-cf-estate-nautobot-ssot.md`.
|
||||||
> **CF bleibt operativer Master, Nautobot = Dokumentations-SSoT.** Dieses File bleibt gültige Historie
|
> **CF bleibt operativer Master, Nautobot = Dokumentations-SSoT.** Dieses File bleibt gültige Historie
|
||||||
> und As-Built-Log; die *strukturierte* Estate liest man ab jetzt in Nautobot. Etappe 2 (CF-API→Nautobot-Sync)
|
> und As-Built-Log; die *strukturierte* Estate liest man ab jetzt in Nautobot. Etappe 2 (CF-API→Nautobot-Sync)
|
||||||
> ist offen.
|
> ist offen.
|
||||||
|
|
||||||
> **Aktueller Stand: → §10 (2026-07-10, TP16 UMGESETZT ✅).** §9 = Analyse/Zielbild, §10 = As-Built.
|
> **Aktueller Stand: → §0 (unten).** §1–16 sind Historie/As-Built-Log früherer Sessions und beschreiben
|
||||||
> Abschnitte 1–8 sind Historie (Track A, 2026-06-30); die „3 Connectors gut/Zielbild"-Aussagen dort sind
|
> Topologien, die **nicht mehr live** sind (v.a. das 3-Tunnel-HA `bf-mm`/`bf-nas`/`bf-pve`). §9 = Analyse,
|
||||||
> durch §9/§10 **überholt** (Round-Robin = macvlan-Host-Isolation → 1/N-Ausfälle). Track A aus `docs/handoff-cf-tunnel-1033.md`.
|
> §10–15 = As-Built des inzwischen abgelösten 3-Tunnel-Designs. Track A aus `docs/handoff-cf-tunnel-1033.md`.
|
||||||
|
|
||||||
|
## §0 — Live-Ist-Stand (2026-09-08, maßgeblich)
|
||||||
|
|
||||||
|
> Der einzige aktuelle Topologie-Stand. **§1–16 nicht als aktuelle Konfiguration lesen** — insbesondere das
|
||||||
|
> 3-Tunnel-HA `bf-mm`/`bf-nas`/`bf-pve` (§10–15) existiert seit dem TP16.3-Cutover (bf-public/Caddy,
|
||||||
|
> 2026-08-27) **nicht mehr**. Strukturierte Estate = Nautobot (TP44, deckt sich mit diesem Stand);
|
||||||
|
> operativer Master = Cloudflare. Read-only gemessen gegen die CF-API (`cfd_tunnel`, `warp_connector`,
|
||||||
|
> `teamnet/routes`) am 2026-09-08.
|
||||||
|
|
||||||
|
**cloudflared-Tunnel (live) — es gibt genau drei:**
|
||||||
|
|
||||||
|
| Tunnel | ID | Conns | Zweck |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **bf-public** | `48d3b0b6-7233-4781-a76a-1de4a5e957ce` | 2, healthy | **einziger öffentlicher BF-Tunnel** (TP31/TP16.3), erstellt 2026-08-27 |
|
||||||
|
| **fm.emsenhuber.net** | `b697d124-53bf-4a33-8fcf-384cced4db1a` | 1, healthy | Standort FM (`ds-fm`, `gateway-fm`), separat |
|
||||||
|
| **terpene-db** | `f2d2fc8a-54ae-41df-b09a-97312cdb9fe2` | 1, healthy | DB-Pfad (Hyperdrive→VPC-Service), Rebuild 2026-09-01 — s. §17 |
|
||||||
|
|
||||||
|
**bf-public-Ingress** → alle vier auf **Caddy `10.40.20.40:443`** (Caddy macht das Fan-out), catch-all 404:
|
||||||
|
`vw` · `hass` · `n8n` · `gitea` (`.emsenhuber.net`).
|
||||||
|
|
||||||
|
**WARP / Private Access — nativer Mesh, KEIN cloudflared:**
|
||||||
|
- warp_connector **bf-core-mesh** `d1da7c8a-2736-4cbb-8af1-32db5393fb9c`, healthy — nativer
|
||||||
|
WireGuard-Mesh-Node auf dem Core (TP16.1). **Live und produktiv** (nicht mehr „gated/Konzept" wie in §14).
|
||||||
|
- Routen: `10.40.0.0/16` → **bf-core-mesh** · `10.101.0.0/24` → **fm**. Ein vnet `vnetemsenhuber` (default).
|
||||||
|
|
||||||
## 1. Alle Tunnel im Account (`393c…66c36`)
|
## 1. Alle Tunnel im Account (`393c…66c36`)
|
||||||
| Tunnel | ID | Status | Conns | Verwendung |
|
| Tunnel | ID | Status | Conns | Verwendung |
|
||||||
@@ -234,6 +258,9 @@ Der macmini ist vollwertiger dritter Knoten (nicht „vergessen").
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🗄️ <b>ARCHIV (§10–16)</b> — überholtes 3-Tunnel-HA <code>bf-mm</code>/<code>bf-nas</code>/<code>bf-pve</code>. Seit dem TP16.3-Cutover (2026-08-27) nicht mehr live — siehe §0. Aufklappen nur für die Historie.</summary>
|
||||||
|
|
||||||
## 10. AS-BUILT 2026-07-10 (TP16 umgesetzt) — reziprokes 3-Tunnel-HA ✅
|
## 10. AS-BUILT 2026-07-10 (TP16 umgesetzt) — reziprokes 3-Tunnel-HA ✅
|
||||||
> Gebaut & verifiziert in eigener Session. Round-Robin-#1033 **strukturell weg** (Loop-Gegentest 0×530 über alle 19 Live-FQDNs, HA per Host-Ausfall-Sim bewiesen). Schritte 1–4+HA durch; **Schritt 5 (Access/WARP + `vw`) offen** (user-gated).
|
> Gebaut & verifiziert in eigener Session. Round-Robin-#1033 **strukturell weg** (Loop-Gegentest 0×530 über alle 19 Live-FQDNs, HA per Host-Ausfall-Sim bewiesen). Schritte 1–4+HA durch; **Schritt 5 (Access/WARP + `vw`) offen** (user-gated).
|
||||||
|
|
||||||
@@ -337,6 +364,10 @@ Letzte in §9.2/§8 als „bleibt `.41`" geführte Origin-Klasse (`pve .41.100`,
|
|||||||
|
|
||||||
## 14. Richtungsentscheid 2026-07-16 — Private Access via native Cloudflare Mesh (WARP Connector) auf dem CRS326 Core-Switch (TP16.1)
|
## 14. Richtungsentscheid 2026-07-16 — Private Access via native Cloudflare Mesh (WARP Connector) auf dem CRS326 Core-Switch (TP16.1)
|
||||||
|
|
||||||
|
> ✅ **Nachtrag 2026-09-08 (live):** umgesetzt. warp_connector **bf-core-mesh** (`d1da7c8a-…`) ist healthy
|
||||||
|
> und trägt die Route `10.40.0.0/16` — der „Scharfschalten = eigene Session, gated"-Status unten ist
|
||||||
|
> überholt. Siehe §0.
|
||||||
|
|
||||||
> ✅ **Update 2026-07-22 (User-Entscheid):** Ziel-Device retargeted von **ap1** auf den **CRS326 Core-Switch** — konsistent mit TP15-Endstand (kein RB5009, Router bleibt ap1/ap2) und TP21s Kabel-Fabric-Modell. Reine Ziel-Doku-Änderung, Scharfschalten bleibt gated bis TP21 den CRS326 abdeckt. Der Rest dieser Sektion ist entsprechend auf den neuen Zielhost umgeschrieben; CRS326 ist aktuell noch Werkszustand, Management-IP/Interface TBD.
|
> ✅ **Update 2026-07-22 (User-Entscheid):** Ziel-Device retargeted von **ap1** auf den **CRS326 Core-Switch** — konsistent mit TP15-Endstand (kein RB5009, Router bleibt ap1/ap2) und TP21s Kabel-Fabric-Modell. Reine Ziel-Doku-Änderung, Scharfschalten bleibt gated bis TP21 den CRS326 abdeckt. Der Rest dieser Sektion ist entsprechend auf den neuen Zielhost umgeschrieben; CRS326 ist aktuell noch Werkszustand, Management-IP/Interface TBD.
|
||||||
|
|
||||||
**Trennung der Concerns (wichtig):**
|
**Trennung der Concerns (wichtig):**
|
||||||
@@ -452,6 +483,8 @@ Netzwerk-Namespace, sieht ohnehin alles), und die cloudflared-Connectoren laufen
|
|||||||
eigene Maschinen, auf denen die Blindheit prinzipiell nicht auftreten kann. Der Shim bleibt als Option
|
eigene Maschinen, auf denen die Blindheit prinzipiell nicht auftreten kann. Der Shim bleibt als Option
|
||||||
für den Fall, dass später doch ein Dienst neben macvlan-Nachbarn einziehen soll.
|
für den Fall, dass später doch ein Dienst neben macvlan-Nachbarn einziehen soll.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## 17. Terpenfinder-DB-Pfad neu aufgebaut 2026-09-02 (Fremdprojekt-Handout blaueblüten)
|
## 17. Terpenfinder-DB-Pfad neu aufgebaut 2026-09-02 (Fremdprojekt-Handout blaueblüten)
|
||||||
|
|
||||||
**Auslöser:** Handout `blaueblueten-n8n/docs/handout-bf-infra-db-pfad-20260902.md` — der Cloudflare-Worker
|
**Auslöser:** Handout `blaueblueten-n8n/docs/handout-bf-infra-db-pfad-20260902.md` — der Cloudflare-Worker
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ Manufacturer "Cloudflare"
|
|||||||
|
|
||||||
## Werkzeug
|
## Werkzeug
|
||||||
|
|
||||||
`nautobot/tp36-cf-estate-seed.py` — **idempotenter** Seeder (get-or-create), Datenstand als Inline-Struktur.
|
`nautobot/tp44-cf-estate-seed.py` — **idempotenter** Seeder (get-or-create), Datenstand als Inline-Struktur.
|
||||||
Token aus `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored). Aufruf: Dry-Run default,
|
Token aus `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored). Aufruf: Dry-Run default,
|
||||||
`--commit` schreibt. Re-Run = 26/26 „existiert".
|
`--commit` schreibt. Re-Run = 26/26 „existiert".
|
||||||
*(Dateiname trägt noch `tp36`-Präfix aus dem ersten Arbeitsstand; inhaltlich TP44. Umbenennen bei Gelegenheit.)*
|
*(Datei am 2026-09-06 von `tp36-…` auf `tp44-cf-estate-seed.py` umbenannt — Präfix jetzt korrekt.)*
|
||||||
|
|
||||||
## Gotchas (für Etappe 2 / Folgesessions)
|
## Gotchas (für Etappe 2 / Folgesessions)
|
||||||
|
|
||||||
@@ -54,15 +54,17 @@ Token aus `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored). Aufruf: Dry-Ru
|
|||||||
- VPC-Service-API liegt unter `connectivity/directory/services` (Update = **PUT**, voller Body), nicht `/vpc/`.
|
- VPC-Service-API liegt unter `connectivity/directory/services` (Update = **PUT**, voller Body), nicht `/vpc/`.
|
||||||
- Der Repo-`CF_API_TOKEN` darf `cfd_tunnel`, aber **nicht** Workers-VPC (`connectivity/directory`) — dort
|
- Der Repo-`CF_API_TOKEN` darf `cfd_tunnel`, aber **nicht** Workers-VPC (`connectivity/directory`) — dort
|
||||||
über den `cloudflare`-MCP-Credential gehen.
|
über den `cloudflare`-MCP-Credential gehen.
|
||||||
|
> ⚠️ **ÜBERHOLT (2026-09-11):** falsch. Live getestet liest der `CF_API_TOKEN` aus
|
||||||
|
> `~/.secrets/credentials.env` Hyperdrive/VPC/Workers (alle 200) — der schmale ist `CF_DNS_API_TOKEN`.
|
||||||
|
> Der Teil-B-Sync deckt damit 14/14 ab. Details: `handoff-20260910-tp44-teilb-sync-live.md`.
|
||||||
|
|
||||||
## Offen (Etappe 2, eigene Session)
|
## Offen (Etappe 2, eigene Session)
|
||||||
|
|
||||||
1. **CF-API→Nautobot-Sync** (Python/n8n): Inline-Daten im Seeder durch Live-Pull ersetzen, periodisch
|
1. **CF-API→Nautobot-Sync** (Python/n8n): Inline-Daten im Seeder durch Live-Pull ersetzen, periodisch
|
||||||
(analog golden-config-bot). Dann ist Nautobot driftfrei.
|
(analog golden-config-bot). Dann ist Nautobot driftfrei.
|
||||||
2. **TP44 in `teilprojekte.md`** eintragen (Quick-Index + Abschnitt + Ist-Stand), sobald die dortigen
|
2. ✅ **TP44 in `teilprojekte.md`** eingetragen (Quick-Index + Abschnitt + Ist-Stand) — 2026-09-06.
|
||||||
fremden Änderungen committet/geklärt sind.
|
|
||||||
3. IPAM-Prefixe `10.40.0.0/16` + `10.101.0.0/24` anlegen → dann greifen die Prefix-Assignments automatisch.
|
3. IPAM-Prefixe `10.40.0.0/16` + `10.101.0.0/24` anlegen → dann greifen die Prefix-Assignments automatisch.
|
||||||
4. Seeder-Datei nach `tp44-…` umbenennen.
|
4. ✅ Seeder-Datei nach `tp44-cf-estate-seed.py` umbenannt — 2026-09-06.
|
||||||
|
|
||||||
## Verifikation (2026-09-02)
|
## Verifikation (2026-09-02)
|
||||||
|
|
||||||
|
|||||||
127
docs/handoff-20260906-tp37-34-36-plan.md
Normal file
127
docs/handoff-20260906-tp37-34-36-plan.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Handoff 2026-09-06 — Plan TP37 (P1+2) → TP34, TP36 parallel
|
||||||
|
|
||||||
|
> **Sessiontyp:** „erst planen, nichts bauen" (User-Entscheid 2026-09-06). **Kein Nautobot-Write.**
|
||||||
|
> Dieses Dokument ist der verzahnte Runbook für die drei nachfolgenden Build-Sessions.
|
||||||
|
> Datenmodell-Entscheid: **native Nautobot-Modelle, keine eigene App** (konsistent zu TP44).
|
||||||
|
|
||||||
|
## 1. Ausgangslage — live gegen die API gelesen (2026-09-06)
|
||||||
|
|
||||||
|
| | Wert | Bedeutung |
|
||||||
|
|---|---|---|
|
||||||
|
| Nautobot | `http://10.40.20.30:8080`, v3.1.7 | Apps: **Golden Config 3.0.7 + Nornir** (cloud-Modelle sind Core) |
|
||||||
|
| Devices | 9 | 4 aktive MikroTik + decommissioned/Rest — in Build-Schritt 0 verifizieren |
|
||||||
|
| **VirtualMachines** | **0** | TP37 P1 offen |
|
||||||
|
| **Clusters** | **0** | TP37 P1 offen |
|
||||||
|
| **ipam.Service** | **0** | TP37 P2 offen |
|
||||||
|
| **CustomFields** | **0** | TP36 offen |
|
||||||
|
| Relationships / Tags | 0 / 0 | — |
|
||||||
|
| `cloud.*` (Account/ResType/Network/Service) | 1 / 6 / 4 / 14 | ✅ **TP44 Etappe 1** — CF-Estate steht |
|
||||||
|
|
||||||
|
**Kernbefund:** TP37 **Punkt 3 (CF-Connectoren) ist durch TP44 erledigt** (native cloud-Modelle). Echt
|
||||||
|
offen bleibt das **Substrat** (P1) und die **Dienste** (P2) — und genau darauf sitzt TP34.
|
||||||
|
|
||||||
|
## 2. Entscheidungen (User, 2026-09-06)
|
||||||
|
|
||||||
|
1. **Erst planen, nichts bauen** — diese Session schreibt nichts nach Nautobot.
|
||||||
|
2. **Native Modelle** — `virtualization.Cluster/VirtualMachine` + `ipam.Service`, **keine eigene App**.
|
||||||
|
3. **TP44-Doku aufgeräumt** — Quick-Index-Zeile + Abschnitt in `teilprojekte.md`, Seeder
|
||||||
|
`nautobot/tp36-…` → `tp44-cf-estate-seed.py` umbenannt.
|
||||||
|
|
||||||
|
## 3. Reihenfolge / Abhängigkeit (korrigiert)
|
||||||
|
|
||||||
|
```
|
||||||
|
TP37 P3 (CF-Connectoren) ─── ✅ ERLEDIGT via TP44 ───┐
|
||||||
|
│ gleiche native-Modell-Linie
|
||||||
|
TP37 P1 (Hosts/Container → VirtualMachine) │
|
||||||
|
└─► TP37 P2 (Dienste → ipam.Service) ─► TP34 (Caddyfile via GC)
|
||||||
|
|
||||||
|
TP36 (CustomFields auf den 4 Devices + Datei-Split) ── ∥ unabhängig, jederzeit parallel
|
||||||
|
```
|
||||||
|
|
||||||
|
**Grund für die Unabhängigkeit von TP36:** seine Live-Daten-CustomFields hängen an den **bestehenden
|
||||||
|
`dcim.Device`**, nicht an neuen VM/Service-Objekten. TP34 dagegen kann ohne die Backend-`ipam.Service`
|
||||||
|
aus P2 nichts rendern → **TP37 vor TP34** bleibt zwingend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. TP37 Punkt 1 — Substrat: Hosts + Container als VMs
|
||||||
|
|
||||||
|
**Modell-Mapping (nativ):**
|
||||||
|
- `virtualization.ClusterType`: `Proxmox VE`, `Synology Docker`, `Docker Desktop`.
|
||||||
|
- `virtualization.Cluster`: `pve`, `nas-bf`, `macmini` (je einer, passender ClusterType).
|
||||||
|
*(Die physischen Hosts als eigene `dcim.Device` sind **optional** und kommen frühestens in einer
|
||||||
|
Etappe 2 — für den Doku-SSoT reicht der Cluster als Gruppierung. Empfehlung: Etappe 1 ohne Host-Devices.)*
|
||||||
|
- `virtualization.VirtualMachine` je Workload, mit `cluster` + `primary_ip4`:
|
||||||
|
- **pve:** alle LXCs (u.a. `platform-pg` 105/`.20.20`, Docker-LXC 400/`.20.126`, `mcast-relay` 108,
|
||||||
|
influxdb, MariaDB 302/`.20.23`, …).
|
||||||
|
- **nas-bf (DS923, Portainer ep14):** Nautobot-Stack-Container, ggf. Caddy/weitere.
|
||||||
|
- **macmini (Docker Desktop):** `n8n`/`n8n-redis`/`n8n-mcp`/`gitea-mcp` + terpene-Worker `:8890`.
|
||||||
|
|
||||||
|
**Build-Schritt 0 (read-only Inventur, BEVOR geseedet wird):** je Host die Workloads mit Name, Rolle,
|
||||||
|
primärer IP enumerieren — `pct list` + `pct config <id>` über **pve-mcp**, Portainer-API (ep14) für die
|
||||||
|
NAS, `docker ps` auf dem macmini. Ergebnis in eine **Inline-Datenstruktur** (Muster: `tp44-cf-estate-seed.py`)
|
||||||
|
→ idempotenter Seeder `nautobot/tp37-substrat-seed.py` (get-or-create, Dry-Run default, `--commit`).
|
||||||
|
|
||||||
|
## 5. TP37 Punkt 2 — Dienste als `ipam.Service`
|
||||||
|
|
||||||
|
- Je exponiertem Port ein `ipam.Service`, an die VM (bzw. später das Device) gebunden, das ihn betreibt:
|
||||||
|
Caddy `:80/:443`, AdGuard `:53` (bf1/bf2), Nautobot `:8080`, n8n `:5678`, HA `:8123`, Portainer `:9443`,
|
||||||
|
`pve-mcp :3000`, gitea, Vaultwarden, …
|
||||||
|
- **Quelle ohne Raten:** die Ports stehen in `CLAUDE.md` (Hardware/Tooling) **und** in den
|
||||||
|
`extra_config.origin/port`-Feldern der TP44-`CloudService`-Objekte — gegeneinander abgleichen.
|
||||||
|
- Idempotenter Seeder `nautobot/tp37-dienste-seed.py`.
|
||||||
|
|
||||||
|
## 6. 🔴 Auflage: Compliance-Gegenprobe (aus TP37 selbst)
|
||||||
|
|
||||||
|
Nach P1+2 **muss** `python3 nautobot/gc-transaktion.py status` bzw. `gc-configplan.py --refresh-compliance`
|
||||||
|
zeigen, dass die Geräte-Compliance **unverändert 140/140** ist. VMs/Services hinzuzufügen darf die
|
||||||
|
Golden-Config-Pipeline der 4 MikroTik **nicht** berühren. P1 und P2 sind je ein **abnehmbarer** Schritt
|
||||||
|
mit dieser Gegenprobe danach — nicht als Nebeneffekt von etwas anderem.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. TP34 — Caddyfile via Golden Config (nach P2)
|
||||||
|
|
||||||
|
- **Vorbedingung:** Backend-Dienste existieren als `ipam.Service` (P2).
|
||||||
|
- **Modell (nativ):** Proxy-Route = **Relationship** (oder CustomField) Frontend `caddy`-Service ↔ Backend-
|
||||||
|
Service, plus den öffentlichen Hostname am Backend. GC-Job + Jinja rendert den Caddyfile, Diff gegen
|
||||||
|
live `caddy-bf1`, neue ComplianceRule `caddyfile`.
|
||||||
|
- **Schritt 0 / Quick-Win (sobald gebaut wird):** `terpene.teilzeitladen.xyz → http://10.40.20.167:8890`
|
||||||
|
als **manueller** Caddy-Block, damit terpene läuft, bevor die GC-Pipeline steht.
|
||||||
|
- ⚠️ **Offener Design-Punkt:** Caddy ist **kein RouterOS-Gerät** — GC an ein Nicht-Netzgerät zu deployen
|
||||||
|
(Caddy-Reload) ist neu (die „nicht in TP37"-Grenze). Deploy-Pfad in der TP34-Build-Session entscheiden.
|
||||||
|
|
||||||
|
## 8. TP36 — Live-Daten-CustomFields + Datei-Split (parallel)
|
||||||
|
|
||||||
|
- **CustomFields auf `dcim.Device`:** `ros_version`, `compliance_pct`, `dhcp_server_count`, `mgmt_ip`,
|
||||||
|
`dns_upstream_mode`. Compliance-Zahl kommt aus **GC selbst**, der Rest aus MikroMCP/REST.
|
||||||
|
- **Skript `nautobot/gc-snapshot.py`** (neu — existiert noch nicht): liest live, setzt die CFs, rendert
|
||||||
|
`docs/teilprojekte-aktuell.md` als **read-only Snapshot**.
|
||||||
|
- **Datei-Split:** `teilprojekte-index.md` (nur aktive TPs, eine Zeile) · `teilprojekte-aktuell.md`
|
||||||
|
(Live-Snapshot) · `teilprojekte-archiv.md` (unverändert). `/projektstatus` liest **Index + Archiv**.
|
||||||
|
- **Unabhängig von TP37** → kann sofort/parallel starten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Empfohlene Reihenfolge der Build-Sessions
|
||||||
|
|
||||||
|
1. **TP37 P1** (Substrat) — Inventur → Seeder → Compliance-Gegenprobe.
|
||||||
|
2. **TP37 P2** (Dienste) — schließt an P1 an, gleiche Gegenprobe.
|
||||||
|
3. **TP34** — nach P2 (Quick-Win terpene als Schritt 0).
|
||||||
|
4. **TP36** — jederzeit parallel zu 1–3.
|
||||||
|
|
||||||
|
## 10. Offene Entscheidungen je Build-Session (nicht heute)
|
||||||
|
|
||||||
|
- **TP34:** Proxy-Route-Modell (Relationship vs. CustomField) + Caddy-Deploy-Pfad (GC an Nicht-Netzgerät).
|
||||||
|
- **TP37:** Hosts als eigene `dcim.Device` anlegen oder nur Cluster? (Empfehlung: Etappe 1 nur Cluster.)
|
||||||
|
- **TP36:** genaues Feld-Set + wie oft `gc-snapshot.py` läuft (Bot analog golden-config vs. on-demand).
|
||||||
|
|
||||||
|
## 11. Was diese Session geändert hat (nur Doku)
|
||||||
|
|
||||||
|
- `nautobot/tp36-cf-estate-seed.py` → **`nautobot/tp44-cf-estate-seed.py`** umbenannt (+ 4 Referenzen,
|
||||||
|
+ Docstring-Titel TP36→TP44).
|
||||||
|
- `teilprojekte.md`: Quick-Index-Zeile TP44; Ist-Stand-Bullet 2026-09-06; TP34/TP36/TP37-Abschnitte auf
|
||||||
|
den korrigierten Stand gezogen; **neuer TP44-Abschnitt** mit Anker.
|
||||||
|
- `handoff-20260902-cf-estate-nautobot-ssot.md` + `cf-tunnel-mapping.md`: Dateiname-Referenzen + erledigte
|
||||||
|
TODOs nachgezogen.
|
||||||
|
- **TP23-Quick-Index-Zeile** (fremde, korrekte Fremd-Änderung ✅→⏳) auf User-Okay mit übernommen.
|
||||||
57
docs/handoff-20260910-tp44-teilb-sync-live.md
Normal file
57
docs/handoff-20260910-tp44-teilb-sync-live.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Handoff 2026-09-10 — TP44 Teil B: CF-Estate-Sync als Nautobot-Job LIVE
|
||||||
|
|
||||||
|
> **Ergebnis:** Der Cloudflare-Estate→Nautobot-Sync läuft als **täglicher Nautobot-Job**
|
||||||
|
> (`CFEstateSync`, grouping `TP44 / Cloudflare`). Dry-Run + Commit-Run sauber: **4 CloudNetworks /
|
||||||
|
> 14 CloudServices, alle CF-Endpoints lesbar, 0 Drift.** CF bleibt operativer Master, Nautobot ist
|
||||||
|
> read-only Dokumentations-SSoT. **TP44 ist damit vollständig durch.**
|
||||||
|
|
||||||
|
## Governance (User, 2026-09-09)
|
||||||
|
Periodische externe→Nautobot-Syncs leben als **Nautobot-Jobs** in `bf-infra/jobs/` und werden **in
|
||||||
|
Nautobot geplant** — kein per-Task-Container, kein Cron. Künftige Syncs = weitere Jobs in `jobs/`.
|
||||||
|
|
||||||
|
## Was live ist
|
||||||
|
- **Runtime:** `jobs/cf_estate_sync.py` (`CFEstateSync`, ORM). Pull der CF-API (urllib+Bearer),
|
||||||
|
Upsert per `extra_config`-Match (`tunnel_id`/`script`/`id`/`service_id`), Verwaisung = `stale`-Flag
|
||||||
|
statt Löschen. 403-tolerant (fehlender Scope kippt den Sync nicht; Verwaisung wird dann übersprungen).
|
||||||
|
- **Registrierung:** `jobs/__init__.py` ruft `register_jobs(CFEstateSync)`; **Repo-Wurzel-`__init__.py`**
|
||||||
|
macht `bf_infra` als reguläres Paket sichtbar.
|
||||||
|
- **Token:** Nautobot-Secret `cf-api-token` (Provider `environment-variable`) → `CF_API_TOKEN` am
|
||||||
|
`nautobot-worker`, verdrahtet im Anchor-`environment` von `docs/nas-vlan20/stacks/210-nautobot.yml`
|
||||||
|
(gitignored). Wert = der breite `CF_API_TOKEN` aus `~/.secrets/credentials.env` (liest 14/14).
|
||||||
|
- **Schedule:** `ScheduledJob "CF Estate Sync – taeglich"`, `interval=daily`, `start_time=2026-09-11T03:00:00Z`
|
||||||
|
(= 05:00 Europe/Vienna, vor den Golden-Config-Läufen), `enabled=True`, `dryrun=False`.
|
||||||
|
- **GitRepository:** `bf-infra` (`provided_contents=['extras.job']`) — war schon verdrahtet.
|
||||||
|
|
||||||
|
## Drei load-bearing Funde (sonst lädt/plant der Job nicht)
|
||||||
|
1. **Repo-Wurzel `__init__.py` ist Pflicht.** Nautobot 3.1.7 lädt Git-Repo-Jobs über
|
||||||
|
`import_modules_privately()` → `pkgutil.walk_packages([GIT_ROOT])`. walk_packages entdeckt **nur**
|
||||||
|
reguläre Pakete (mit `__init__.py`), **keine** Namespace-Packages. Ohne die Datei ist der geklonte
|
||||||
|
`GIT_ROOT/bf_infra/` unsichtbar → `jobs/` wird nie importiert. Symptom: Git-Sync **SUCCESS**, aber
|
||||||
|
Log „No jobs were registered on loading the `bf_infra.jobs` submodule" und **kein** Job-DB-Eintrag.
|
||||||
|
bf-infra ist das **erste** Git-Repo mit `extras.job` — daher nie zuvor aufgetreten. Der `module_path`-
|
||||||
|
Filter des Loaders importiert nur `bf_infra` + `bf_infra.jobs.*`; `nautobot/`, `routeros/` etc. bleiben
|
||||||
|
unberührt.
|
||||||
|
2. **`register_jobs()` gehört ins Paket-`__init__`**, nicht nur ins Submodul (Doku bestätigt).
|
||||||
|
3. **`Meta.has_sensitive_variables = False`** nötig — Default `True` ⇒ Nautobot verweigert das Scheduling
|
||||||
|
(„Job may have sensitive input variables"). Der Job nimmt nur `dryrun` (Boolean); das Token kommt aus
|
||||||
|
dem Secret, nicht aus einer Job-Variable.
|
||||||
|
|
||||||
|
## Zwei Daten-/Code-Funde beim Dry-Run
|
||||||
|
- **VPC-Bug:** VPC-Directory-Services tragen ihren Identifier in **`service_id`**, nicht `id`. Der Job las
|
||||||
|
`id` (=None) → per `.get(mk)==None` matchte er ein Fremdobjekt (App Launcher) und ließ den echten
|
||||||
|
`terpene-db` auf stale laufen. Fix: VPC nutzt `service_id`; `_upsert` überspringt `mv=None` generell.
|
||||||
|
- **Seed-Artefakt:** der geseedete CloudService `terpene-db` hatte `extra_config.service_id: null`
|
||||||
|
(der echte `019fa3d5-…` stand beim Seed nur in den Hyperdrive-Einträgen als `vpc_service_id`).
|
||||||
|
**Einmalig reconciled** (PATCH `service_id=019fa3d5-…`) → seither matcht der Sync sauber, 0 Drift.
|
||||||
|
|
||||||
|
## Drift-Korrektur (nicht stillschweigend)
|
||||||
|
Die frühere Notiz „Repo-`CF_API_TOKEN` darf kein Workers-VPC → über den `cloudflare`-MCP-Credential"
|
||||||
|
(TP44-Prosa + `handoff-20260902-…`) war **falsch**. Live getestet liest der `CF_API_TOKEN` aus
|
||||||
|
`~/.secrets/credentials.env` Hyperdrive/VPC/Workers (alle 200). Der schmale Token ist `CF_DNS_API_TOKEN`
|
||||||
|
(403). Der `cloudflare`-MCP ist der **remote OAuth-MCP** (kein API-Token) und für einen serverseitigen
|
||||||
|
Job ohnehin nicht nutzbar.
|
||||||
|
|
||||||
|
## Nächster CF-Sync = neuer Job im selben Muster
|
||||||
|
Modul in `jobs/` anlegen, Klasse importieren + `register_jobs(...)` im `jobs/__init__.py` ergänzen,
|
||||||
|
`Meta.has_sensitive_variables=False` setzen, Git-Sync, enablen, Dry-Run, dann Schedule. Die Wurzel-
|
||||||
|
`__init__.py` gilt schon.
|
||||||
@@ -63,6 +63,11 @@ teuerste Eigenschaft der Datei zerstört, um ihr billigstes Problem zu lösen. B
|
|||||||
| [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` | ↩️ **wieder geöffnet 2026-08-30** (ap3) — Volltext in [teilprojekte.md](teilprojekte.md#tp28) |
|
||||||
| [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 |
|
||||||
|
| [TP44](#tp44) | Cloudflare-Estate als Dokumentations-SSoT in Nautobot | ✅ DURCH (2026-09-10) |
|
||||||
|
| [TP41](#tp41) | CF-Access-Reparatur + Zero-Trust-Konsolidierung | ✅ ABGESCHLOSSEN (2026-09-10) — Gelis WARP via Android eingerichtet, letzter Blocker weg |
|
||||||
|
| [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 |
|
||||||
|
|
||||||
Dazu am Ende: **[TP25 (Langfassung)](#tp25-lang)** — die Messchronik und die Funde A–E im Volltext.
|
Dazu am Ende: **[TP25 (Langfassung)](#tp25-lang)** — die Messchronik und die Funde A–E im Volltext.
|
||||||
TP25 selbst ist **nicht** abgeschlossen und steht weiter in `teilprojekte.md`; nur seine 394 Zeilen
|
TP25 selbst ist **nicht** abgeschlossen und steht weiter in `teilprojekte.md`; nur seine 394 Zeilen
|
||||||
@@ -1777,3 +1782,198 @@ gemessen hat. Beim Beheben kam eine zweite, unabhängige Altlast zum Vorschein.
|
|||||||
- **Nicht** TP31/TP34 (Caddy) und nicht TP11 (Monitoring) — obwohl gilt: ein Monitoring, das
|
- **Nicht** TP31/TP34 (Caddy) und nicht TP11 (Monitoring) — obwohl gilt: ein Monitoring, das
|
||||||
DB-Verbindungszeiten kennt, hätte diesen Fall Monate früher gezeigt.
|
DB-Verbindungszeiten kennt, hätte diesen Fall Monate früher gezeigt.
|
||||||
- **Nicht** TP40: der HA-Recorder-Wildwuchs (Punkt 3) ist HA-seitig und ein eigenes TP.
|
- **Nicht** TP40: der HA-Recorder-Wildwuchs (Punkt 3) ist HA-seitig und ein eigenes TP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TP34 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) ▸ ❌ **VERWORFEN (2026-09-08)**
|
||||||
|
> 🔴 **Nachtrag 2026-09-08 (verworfen):** Der Ansatz „Caddyfile via Golden Config rendern" ist
|
||||||
|
> zurückgezogen (Commits `d63dc7b`, `bca8f6e`; Gitea-Issue #24 → `status:dropped`, closed). Der
|
||||||
|
> `420.teilzeitladen.xyz`-Block wurde wieder aus dem GC-gerenderten Caddyfile entfernt; **der
|
||||||
|
> Caddyfile bleibt manuell** in `caddy/Caddyfile`. Gründe: kein zweites SSoT-Modell für Proxy-Routen
|
||||||
|
> nötig, TP37 deckt die Dienst-Doku als `ipam.Service` bereits ab, und der einzige akute Anwendungsfall
|
||||||
|
> ließ sich mit einem manuellen Block lösen. Text unten unverändert als Konzept-Historie.
|
||||||
|
|
||||||
|
> **Konzept + Architektur-Skizze:** [`docs/handoff-20260827-inventur-und-architektur.md`](handoff-20260827-inventur-und-architektur.md) — bitte dort weiterlesen (Architektur-Abschnitt).
|
||||||
|
- **Scope:** Reverse-Proxy-Hosts werden im **Nautobot-Datenmodell** gepflegt (Site, Host, Backend-Spec). **Golden Config** rendert den Caddyfile aus den Daten, Diff-Check gegen die Live-Version auf `caddy-bf1`, Compliance-Checks + Transaktionen identisch zum bestehenden Workflow für Router-Configs.
|
||||||
|
- **Auslöser (Inventur 2026-08-27):** der Caddyfile (`bf-infra/caddy/Caddyfile`) ist heute **manuell gepflegt** — kein Drift-Check, keine Transaktionen, andere Projekte können keine Sites anlegen ohne Christoph. Lösung: **Nautobot/GC**, kein Caddy-API-Sidecar (kein zweites SSoT).
|
||||||
|
- **Erster Anwendungsfall:** `terpene.teilzeitladen.xyz → http://10.40.20.167:8890` (Mac-mini-Backend, dort PostgREST + sync + web via lokalem Caddy `:8890`). Vor TP34: manueller Quick-Win-Caddy-Block.
|
||||||
|
- **Abh.:** TP22 (GC-Pipeline) ✅ · TP23 (DNS/Caddy-Voraussetzungen) ✅ Stufe 1 · 🔴 **NEU 2026-09-06: TP37 Punkt 1+2** — Caddy rendert *Config aus Daten*, die Backend-Dienste müssen als `ipam.Service` in Nautobot existieren, sonst gibt es nichts zu rendern. **TP37 vor TP34.**
|
||||||
|
- **Datenmodell (entschieden 2026-09-06):** native Modelle — Backend = `ipam.Service`, Proxy-Route = Relationship/CustomField Frontend↔Backend, GC-Job rendert den Caddyfile. Keine eigene App (konsistent zu TP44).
|
||||||
|
- **Status:** 🔜 Stub; wartet auf TP37 P1+2. Quick-Win `terpene.teilzeitladen.xyz → 10.40.20.167:8890` als manueller Caddy-Block ist **Schritt 0** des Runbooks. Plan: [`handoff-20260906-tp37-34-36-plan.md`](handoff-20260906-tp37-34-36-plan.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<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)**
|
||||||
|
> **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 —
|
||||||
|
> **etappenweise: manuell jetzt, Sync später.** Nautobot kann CF nicht steuern → „SSoT" heißt
|
||||||
|
> **Dokumentations-SSoT; CF bleibt operativer Master.** Detail-Handoff:
|
||||||
|
> [`handoff-20260902-cf-estate-nautobot-ssot.md`](handoff-20260902-cf-estate-nautobot-ssot.md).
|
||||||
|
|
||||||
|
- **Warum eigenes TP, nicht TP36:** TP36 ist eng auf `teilprojekte.md`-Live-Daten geschnitten; gleiche
|
||||||
|
Idee (Nautobot als Doku-SSoT), andere Dimension. TP44 = nächste freie Nummer nach TP43.
|
||||||
|
- **Modell (native `nautobot.cloud`, Nautobot 3.1.7 — keine eigene App):** `Manufacturer "Cloudflare"` →
|
||||||
|
`CloudAccount` (`393c051f…`) → `CloudResourceType ×6` → `CloudNetwork ×4` (`bf-public`/`fm`/`terpene-db`/
|
||||||
|
`bf-core-mesh`) + `CloudService ×14`; Service↔Tunnel über das Through-Modell
|
||||||
|
`cloud-service-network-assignments` (3 Rows). IDs/Ports/Hostnames/Origins je Objekt in `extra_config`.
|
||||||
|
- **Werkzeug:** `nautobot/tp44-cf-estate-seed.py` — idempotenter Seeder (Re-Run = 26/26 „existiert"),
|
||||||
|
Datenstand inline (Live-CF-Inventur 2026-09-02). Token aus `docs/nas-vlan20/stacks/210-nautobot.yml`.
|
||||||
|
- ✅ **Verifikation 2026-09-06 (live gegen die API):** CloudAccount 1 · ResourceType 6 · CloudNetwork 4 ·
|
||||||
|
CloudService 14 — unverändert seit 09-02.
|
||||||
|
- 🔴 **Gotchas:** `CloudService.cloud_networks` ist `read_only` (nur Through-Rows schreiben/lesen) ·
|
||||||
|
VPC-Service-API unter `connectivity/directory/services` (PUT, voller Body) · **VPC-Objekte tragen ihren
|
||||||
|
Identifier in `service_id`, NICHT `id`** (kostete im Sync einen Fehl-Match) · ⚠️ **DRIFT-KORREKTUR
|
||||||
|
2026-09-10:** die frühere Zeile „Repo-`CF_API_TOKEN` darf kein Workers-VPC → über den `cloudflare`-MCP"
|
||||||
|
war **falsch**. Live getestet liest der `CF_API_TOKEN` aus `~/.secrets/credentials.env` Hyperdrive **und**
|
||||||
|
VPC **und** Workers (alle 200). Der schmale Token ist `CF_DNS_API_TOKEN` (403). Der `cloudflare`-MCP ist
|
||||||
|
der **remote OAuth-MCP** (kein API-Token) und für einen serverseitigen Job ohnehin nicht nutzbar.
|
||||||
|
- ✅ **Etappe 2 Teil A (2026-09-08): IPAM-Prefixe live.** `10.40.0.0/16` (container, Route `bf-core-mesh`) +
|
||||||
|
`10.101.0.0/24` (network, Route `fm`) angelegt via `nautobot/tp44-ipam-prefixes.py`; der Seeder-Re-Run
|
||||||
|
hat die zwei `cloud-network-prefix-assignments` eingehängt (count=2, Idempotenz-Gegenprobe grün).
|
||||||
|
- ✅ **Etappe 2 Teil B (2026-09-10): Live-Sync als Nautobot-Job — DURCH.** Governance (User 2026-09-09):
|
||||||
|
periodische externe→Nautobot-Syncs leben als **Nautobot-Jobs** in `bf-infra/jobs/`, in Nautobot geplant —
|
||||||
|
**kein per-Task-Container/-Cron.** Runtime `jobs/cf_estate_sync.py` (`CFEstateSync`, ORM, read-only Pull,
|
||||||
|
403-tolerant, Verwaisung = `extra_config.stale` statt Löschen). Token = Nautobot-Secret `cf-api-token`
|
||||||
|
(Provider env → `CF_API_TOKEN` am Worker, in `210-nautobot.yml` verdrahtet). **Schedule: täglich 03:00Z**
|
||||||
|
(`ScheduledJob "CF Estate Sync – taeglich"`). Verifiziert: Dry-Run + Commit-Run sauber, **4 Networks /
|
||||||
|
14 Services, alle Endpoints lesbar, 0 Drift**. Der REST-Einmalproof `nautobot/tp44-cf-sync.py` bleibt als
|
||||||
|
workstation-seitiges Diagnose-Werkzeug liegen (nicht die kanonische Runtime).
|
||||||
|
- 🔴 **Drei load-bearing Funde (Detail-Handoff `handoff-20260910-tp44-teilb-sync-live.md`):**
|
||||||
|
(1) **Wurzel-`__init__.py` im Repo ist Pflicht** — Nautobot lädt Git-Repo-Jobs via
|
||||||
|
`pkgutil.walk_packages([GIT_ROOT])`, das Namespace-Packages (ohne `__init__.py`) NICHT entdeckt; ohne die
|
||||||
|
Datei bleibt `bf_infra.jobs` unsichtbar (Symptom: Sync SUCCESS + „No jobs were registered"). bf-infra ist
|
||||||
|
das erste Git-Repo mit `extras.job`. (2) `register_jobs()` gehört ins **Paket-`__init__`**, nicht nur ins
|
||||||
|
Submodul. (3) `Meta.has_sensitive_variables=False` nötig, sonst verweigert Nautobot das Scheduling.
|
||||||
|
- **Beziehung zu TP37:** TP44 hat **TP37 Punkt 3** (CF-Connectoren) vorweggenommen — dieselbe native-Modell-
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Auslöser:** CF-Access zwang Christoph **und Geli** täglich zum OTP-Login (soll: 30 Tage); GitHub/Google
|
||||||
|
„funktionieren nicht". Root-Causes: Team-Domain-Rename `bf4`→`emsenhuber` (Provider-Callback veraltet),
|
||||||
|
App-Session 24h, diverse Ordnungs-/Sicherheitslecks. Vollständiger Verlauf + offene Schritte:
|
||||||
|
**[`docs/handoff-20260901-tp41.md`](handoff-20260901-tp41.md)**. Branch `tp41-cf-access-repair`.
|
||||||
|
|
||||||
|
**✅ Live erledigt:** Session 720h (Wildcard/vw/Warp-Login) · login_method-Loch zu (reusable Policy
|
||||||
|
`bf-users` = nur `email:christoph`+`email:geli`) · `8e44f5d1` von 4 Apps auf **nur Terpenfinder**
|
||||||
|
entkoppelt · Access-**Gruppen** `zt-humans`/`zt-home`/`zt-automation`, Policies zeigen darauf · **vw voll
|
||||||
|
Bypass** (Lebensader offen) + **`vw/admin` per WAF-Regel öffentlich geblockt** (CF-Edge + Mobilfunk
|
||||||
|
verifiziert) · API-Token auf das vollständige `…4148` konsolidiert (User hat Service-Tokens- + Zero-Trust-
|
||||||
|
Write ergänzt), altes `…2174` revoken.
|
||||||
|
|
||||||
|
**✅ T4 DURCH (2026-09-01, Variante B):** ein Access-Service-Token **`zt-svc`** (`65f1821a`, rotiert) über
|
||||||
|
Gruppe `zt-automation`; Terpene-Worker per `wrangler secret put TERPENE_SVC_CLIENT_ID` auf zt-svc
|
||||||
|
umgestellt (Worker-Pin `index.js:44`); `terpene-verify`+`-v2` **gelöscht**; `test_whoami` grün.
|
||||||
|
**Token-Altlast geklärt:** `b00722` = LIVE Caddy-DNS-Token (LXC130 caddy.env, SSoT), `d47435` = stale
|
||||||
|
Kopie (kein Consumer) → **User revoket `d47435`**; `credentials.env`-Kopie auf b00722 realignt.
|
||||||
|
|
||||||
|
**✅ T3 APPLIED (2026-09-01):** Default-Profil (`0197d1b3`; das custom „Cloudflare Mesh" matcht nur den
|
||||||
|
`warp_connector`) `fallback_domains += emsenhuber.net → [10.40.20.36, 10.40.20.37]`; Tunnel-Route
|
||||||
|
`10.40.0.0/16` (`d1da7c8a`) da, kein `10/8`-Exclude → routbar. ⏳ Client-Verify: WARP neu verbinden.
|
||||||
|
**✅ T2 DURCH (2026-09-01):** WAF-Regel `f26744eb` gelöscht; Caddy `vw`-Block `respond @cf_admin 404` bei
|
||||||
|
Header `CF-Connecting-IP` (LXC130). Verify: Edge `/admin`=404, lokal/WARP `/admin`=200. ✅
|
||||||
|
|
||||||
|
**✅ T5 DURCH (2026-09-01, User-bestätigt):** GitHub-Login läuft; Google-Login läuft nach dem
|
||||||
|
Consent-Screen-Fix (§A des Follow-up-Handoffs: User Type *Internal*→*External*/veröffentlicht).
|
||||||
|
Callback für beide `https://emsenhuber.cloudflareaccess.com/cdn-cgi/access/callback`.
|
||||||
|
**❌ Facebook/Meta für TP41 abgesagt (2026-09-01, User-Entscheid):** optional, kein Bedarf — Geli ist
|
||||||
|
über OTP `angelikahy@outlook.de` + Google `pillepalle670@gmail.com` voll gedeckt. **Keine Facebook-App,
|
||||||
|
kein CF-Facebook-IdP angelegt → nichts aufzuräumen.** Instagram hat ohnehin keinen CF-IdP.
|
||||||
|
**✅ App-Launcher (D1):** je eine Bookmark-Kachel vw/hass/terpenfinder, Wildcard-Kachel raus, Launcher
|
||||||
|
gestylt (Follow-up-Handoff §D, Policies unverändert).
|
||||||
|
|
||||||
|
**✅ Geli in WARP — CF-seitig nichts zu tun (2026-09-01 live geprüft):** Device-Enrollment läuft über
|
||||||
|
die App **„Warp Login App"** (`6c32fe23…`, type `warp`) → Policy `bf-users` → Gruppe **`zt-humans`**, die
|
||||||
|
Gelis beide Mails **schon enthält** → sie **darf bereits enrollen** (deshalb war „Device enrollment
|
||||||
|
permissions" nicht als fehlendes Setting zu finden). Terpenfinder-`geli`-Policy lässt sie per E-Mail durch.
|
||||||
|
✅ **Client am 2026-09-10 via Android erfolgreich eingerichtet** (WARP installiert, Team `emsenhuber`,
|
||||||
|
Login OTP `angelikahy@outlook.de` — User-Bestätigung); damit ist der einzige verbliebene Handgriff erledigt.
|
||||||
|
🔑 **Permission-Frage beantwortet:** CF-seitig **keine** nötig. Für generelle WARP/Enrollment/User-Verwaltung
|
||||||
|
per Account-Token: *Access: Apps and Policies → Edit* + *Access: Organizations, IdPs, and Groups → Edit*
|
||||||
|
(User erteilt 2026-09-01). **Seat-Verwaltung (`/access/seats`) bleibt per MCP gesperrt** — `10405 Method
|
||||||
|
not allowed for this authentication scheme` (Endpoint will User-Token, nicht Account-Token; kein Scope-Fix,
|
||||||
|
für Enroll irrelevant).
|
||||||
|
|
||||||
|
**🔻 Optionale Restpunkte (bewusst nicht gezogen, kein Blocker):** **T6** optional Rate-Limit ·
|
||||||
|
🔴 `TERPENE_ANDROID_KEYSTORE_PW`-Rotation (→ **TP30 Posten #7**, Transkript-Leak) · **Security-Cleanup:**
|
||||||
|
die `geli`-Terpenfinder-Policy hat **OR-Semantik** und enthält den breiten CGNAT-Bereich
|
||||||
|
`94.154.138.0/24` als IP-Include → jeder aus dieser geteilten Range käme **ohne Login** rein. Über WARP ist
|
||||||
|
der Egress ohnehin Cloudflare (nicht diese IP) → der IP-Include wäre dann überflüssig und entfernbar.
|
||||||
|
**d47435-Revoke ✅ · T3-WARP-Client-Verify ✅** (User, 2026-09-01).
|
||||||
|
|
||||||
|
**Abgrenzung:** hass/gitea bleiben (diese Runde) öffentlich; Terpenfinder-Human-Policies nicht angefasst;
|
||||||
|
**Gitea-Least-Privilege** = eigener künftiger TP (jedes Projekt repo-scoped Login, Admin nur Claude/Christoph).
|
||||||
|
- Bezug **TP11** (Monitoring): eine Plattenfüllstand-Warnung hätte das Wochen früher gezeigt.
|
||||||
|
|
||||||
|
### TP43 — dw-Standort-DNS-Uplink (site-spezifische Overrides) ▸ ♻️ **REOPENED 2026-09-11 → Volltext zurück in [`teilprojekte.md`](teilprojekte.md) (Abschnitt TP43)**
|
||||||
|
|
||||||
|
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
|
||||||
|
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`.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
**Auslöser (2026-09-01, TP41-Randbefund):** live gemessen liefern die realen Client-Resolver
|
||||||
|
`10.40.20.36`/`.37` `*.emsenhuber.net → 10.40.20.40` (Caddy) — korrekt. Daneben existiert aber ein
|
||||||
|
zweiter, gedrifteter interner Namensraum: der **Core** trägt in `/ip dns static` 11 Einträge auf
|
||||||
|
`*.bf.lan`, der **Caddyfile** doppelt jeden internen Namen als `*.bf.emsenhuber.net`, und die alten
|
||||||
|
AdGuard `.16`/`.17` antworten noch mit `nginx*.bf.emsenhuber.net` bzw. totem VLAN1 `10.40.41.123`.
|
||||||
|
Christoph: „aus emsenhuber.net wurde schon wieder bf.lan — das müssen wir ein für alle mal aus allen
|
||||||
|
Geräten rausprügeln und in Nautobot festhalten."
|
||||||
|
|
||||||
|
**Ziel:** genau **ein** interner Namensraum `emsenhuber.net` (Split-Horizon `.36/.37 → .40`). `.bf.lan`
|
||||||
|
und `.bf.emsenhuber.net` werden entfernt; Sollzustand in Nautobot dokumentiert.
|
||||||
|
|
||||||
|
**Scope (Reihenfolge = eigene Session):**
|
||||||
|
1. **Bestandsaufnahme** aller Fundstellen: Core `/ip dns static` (`*.bf.lan`), DHCP-Option
|
||||||
|
`domain`/Search-Domain je Netz, Caddyfile-Aliase (`*.bf.emsenhuber.net`), AdGuard-Rewrites `.36/.37`,
|
||||||
|
stale `.16/.17`, Geräte-`identity`.
|
||||||
|
2. **Entscheidung Zielschema** (nur `emsenhuber.net`, ggf. `bf.emsenhuber.net` als *einzige* interne
|
||||||
|
Zone — Christoph-Entscheid) und Migrationsreihenfolge ohne DNS-Loch.
|
||||||
|
3. **Umsetzen:** Core-DNS + DHCP + Caddyfile angleichen; stale `.16/.17` klären/abbauen.
|
||||||
|
4. **Nautobot:** interne Namen/Records als Sollzustand erfassen (Bezug TP37 Datenmodell).
|
||||||
|
|
||||||
|
**Abgrenzung:** kein CF-Public-DNS-Umbau (Zone `emsenhuber.net` öffentlich bleibt); kein Resolver-Swap
|
||||||
|
(`.36/.37` bleiben); `.1`-als-DNS-Reste auf Clients sind separat/tolerierbar (User: „bis jetzt kein
|
||||||
|
Problem"). **Abh.:** TP37 (Nautobot-Datenmodell) für Punkt 4; sonst nichts Offenes.
|
||||||
|
|
||||||
|
#### ✅ Bestandsaufnahme LIVE (2026-09-02) — Drift hat 80 % schon erledigt
|
||||||
|
Gegen die Geräte gemessen (MikroMCP + dig), nicht aus dem Handoff:
|
||||||
|
|
||||||
|
| Fundstelle | Handoff 09-01 | Live 09-02 |
|
||||||
|
|---|---|---|
|
||||||
|
| Core `/ip dns static` | 11× `*.bf.lan` | ✅ **0× `.bf.lan`** — 10× `*.bf.emsenhuber.net` (deckt sich mit GC-Template Z.284–293) |
|
||||||
|
| DHCP `domain` (7 Netze) | „prüfen" | `emsenhuber.net` (GC-Template Z.133 = gleich ⇒ **keine Drift**), DNS `.36/.37` |
|
||||||
|
| `.36/.37` (Soll) | aktiv | `vw.emsenhuber.net→.40` ✅ · `macmini.bf.emsenhuber.net→.20.167` ✅ · **`macmini.emsenhuber.net→leer`** ❌ |
|
||||||
|
| Caddyfile | `*.bf.emsenhuber.net`-Aliase | unverändert, **konsistent** mit Core-Static |
|
||||||
|
| `.16`/`.17` | tot | laufen (:53), liefern totes VLAN1; **kein Core-Objekt** (Firewall/NAT/DNS-Upstream/Addr-List) hängt an `.16` |
|
||||||
|
|
||||||
|
**Kernkonflikt:** DHCP verteilt `emsenhuber.net` (bare), Records leben unter `bf.emsenhuber.net` ⇒ bare `macmini` löst nicht.
|
||||||
|
|
||||||
|
#### ✅ Entscheidungen (User, 2026-09-02)
|
||||||
|
1. **Zielschema = `bf.emsenhuber.net`** (der wenig-Arbeit-Weg): DHCP-domain → `bf.emsenhuber.net`, dann
|
||||||
|
löst `macmini` bare korrekt; Core-Static + Caddyfile bleiben (tragen schon `.bf.`). Öffentliche Zone
|
||||||
|
`emsenhuber.net` bleibt getrennt.
|
||||||
|
2. **`.16` (adguard_bf1) abbauen**, **`.17` (adguard_fm2 = Standort FM) NICHT anfassen.**
|
||||||
|
|
||||||
|
#### ✅ Umsetzung DURCH (2026-09-02)
|
||||||
|
- **(A) DHCP-domain `emsenhuber.net → bf.emsenhuber.net` über GC ✅** — kein Direkt-Write:
|
||||||
|
`mikrotik_routeros.j2` Z.133 gehoben → `push-gc-template.py` (Commit `7a4068c4`, **Nebenbefund:** das
|
||||||
|
Remote-Template hing dem lokalen hinterher — es trug noch `*.bf.lan`+`router.lan`, obwohl Gerät+lokal
|
||||||
|
längst `.bf.emsenhuber.net` fuhren; der Push hat das GC-Repo an den Live-Stand angeglichen) →
|
||||||
|
GitRepo-Sync (SUCCESS) → manueller ConfigPlan **`tp42-dhcp-domain`** (`set [find] domain=…`, weil
|
||||||
|
`add` das Netz doppelt anlegen würde) → Deploy an **crs326-core** → Live **7/7 = bf.emsenhuber.net**,
|
||||||
|
Compliance **140/140, keine Drifts**. Der Job steht dauerhaft in `nautobot/gc-configplan.py`.
|
||||||
|
- **(B) `.16`-Teardown ✅** — `adguard_bf1` lag im geteilten Portainer-Stack **195 `adguard`** (Docker-LXC
|
||||||
|
`.126`, **ep2** — *nicht* ep14) **zusammen mit `adguard_fm2` (`.17`, Standort FM!)**. Deshalb **nicht**
|
||||||
|
Stack löschen, sondern compose auf nur `.17` getrimmt + Stack redeployt (Prune): `.16`-Container weg,
|
||||||
|
`.16:53` tot; **`.17` läuft** (⚠️ ~40 s mit-neugestartet durch den Redeploy — kurzer FM-DNS-Blip,
|
||||||
|
danach sauber). Rollback-compose gesichert. 🧹 Daten-Dirs `/opt/docker/adguard_bf1/*` bleiben als
|
||||||
|
harmlose Leichen auf dem LXC.
|
||||||
|
- **(C) Nautobot-Soll** (Scope-Punkt 4) bleibt an **TP37** gekoppelt (Datenmodell), separat — kein Blocker.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
Handoff: [`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md).
|
||||||
|
|||||||
@@ -25,470 +25,24 @@ IP-Blöcke: `.1–10` Router/Gateways · `.11–20` Switches/APs. **GW je VLAN =
|
|||||||
Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` · ap2 `.99.12`.
|
Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` · ap2 `.99.12`.
|
||||||
*(Historisch, überholt: „ap1=.1, ap2=.2 · ALLNET=.11, Omada=.12" — aus der Zeit vor dem Core-Cutover.)*
|
*(Historisch, überholt: „ap1=.1, ap2=.2 · ALLNET=.11, Omada=.12" — aus der Zeit vor dem Core-Cutover.)*
|
||||||
|
|
||||||
## Ist-Stand (Kurz)
|
## Status & Index → Gitea-Board (SSoT seit 2026-09-07, TP36-Cutover)
|
||||||
|
|
||||||
**Stand 2026-09-04 — live gegen die Geräte geprüft (MikroMCP/REST/SSH), nicht aus der Doku abgeschrieben.**
|
> 🧭 **Ist-Stand, kritischer Pfad und Quick-Index leben ab jetzt als Gitea-Issues**, nicht mehr in
|
||||||
**Compliance 140/140, 0 Drifts** (2026-09-04 nachmittags). Der TP25-Kanalplan ist **committed**:
|
> dieser Datei: **[admin/bf-infra · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues)**.
|
||||||
Template auf DFS-Frequenz-Listen umgestellt (`ch-5g-ap1=5260,5280`, `ch-5g-ap2=5300,5320`) + die
|
> **TP = Issue · Status = Label `status:*`** (todo/wip/paused/idea/concept/done/dropped) ·
|
||||||
ap1-Provisioning-Zeilen wieder aufgenommen → Transaktion `TP25-kanalplan-ap1-live` geschlossen und der
|
> **Gruppierung = Label `thema:*`**. Filtern über die Label-Chips der Issue-Liste, z.B.
|
||||||
`wifi-provisioning`-Drift weg. `wifi-security` gruen (`ft=no` auf `sec-main`).
|
> [nur offene](https://gitea.emsenhuber.net/admin/bf-infra/issues?type=issue&state=open).
|
||||||
🔴 **Zusätzlich live behoben (nicht compliance-sichtbar):** ap3 trug auf `wifi1/wifi2` einen Werks-Inline
|
|
||||||
`security.ft=yes` — gemischtes FT-ESS, das die 3/3-Strich-Abbrüche vom 09-04 erklärt. Unset → uniform
|
|
||||||
`ft=no` on-air; Bring-up-Quelle `tp28-ap3-phase2-cap.rsc` um `!security.ft` ergänzt (s. Fund-E-Block).
|
|
||||||
*(Die Fassung vom 2026-08-16 stand bis zum 18. hier und war an vier Stellen überholt. Wer diesen Block
|
|
||||||
liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)*
|
|
||||||
- ✅ **TP25 Multi-Pin-Konsole (2026-09-04):** die Klick-Konsole (`docs/tp6-marker/server.py`) ist auf
|
|
||||||
**N Clients** generalisiert und hat eine **Pin-UI** bekommen. Sie liest die Client-Liste aus
|
|
||||||
`geraete.txt` (schreibt `tp25-walk-messung.sh`), zeigt je Client AP/Signal/Ping-Verlust aus
|
|
||||||
`sampler-<name>.csv`/`walk-ping-<name>.csv` (kein eigenes SSH mehr) und pinnt N Clients band-genau
|
|
||||||
über `tp25-pin-multi.sh` direkt in der Konsole. Die alte 2-Client-Gegen-AP-Automatik ist entfernt.
|
|
||||||
Handoff: [`docs/handoff-20260904-tp25-konsole-n-clients.md`](handoff-20260904-tp25-konsole-n-clients.md).
|
|
||||||
- ✅ **TP25 Fund E GESCHLOSSEN (2026-09-02):** die vorgemerkte Abschlussmessung ist gelaufen —
|
|
||||||
`reauthenticating` am 09-02 nur noch **4 in 8,6 h** (alle iPhone `F2:62`, isolierte Einzel, 0 Ausfälle)
|
|
||||||
gegen Baseline 93 bzw. Hebel-1 **19/h**. Verlauf **218 → 40 → 4**; Vorbedingung geprüft (Core-Uptime
|
|
||||||
1d18h, `caps joined=0` → keine Reboot-Artefakte). ⇒ **7.24.1 (TP27) war die Ursache/der Fix, nicht
|
|
||||||
`ft-over-ds`.** Hebel 1 widerlegt+zurückgebaut, Hebel 2–4 entfallen, **kein Geräte-Write**. **Am
|
|
||||||
2026-09-04 hat Christoph `ft=no` auf `sec-main` als Dauerzustand entschieden** (Hebel 2 doch gezogen,
|
|
||||||
Template + bf-golden-config + Nautobot-Sync, `wifi-security` gruen). 🔴 **Nachtrag 2026-09-04:** „ohne FT"
|
|
||||||
galt bis dahin **nur für Manager/ap1/ap2** — **ap3** trug einen Werks-Inline `security.ft=yes` auf
|
|
||||||
`wifi1/wifi2` (Bring-up-Skript unset `!ft-over-ds`, aber nicht `!ft`; die Abnahme zählt nur benannte
|
|
||||||
security-Objekte und sah den Inline-Wert nicht). Das ist ein **gemischtes FT-ESS** und erklärt den
|
|
||||||
Abbruch bei 3/3 Strichen: Log zeigte wiederkehrende Strong-Signal-Disconnects (−43/−45 dBm) **nur auf
|
|
||||||
ap3** (`cap-wifi2/4`, `dirtyvader-main`). Fix: `!security.ft` auf ap3 → uniform `ft=no`; Quelle
|
|
||||||
korrigiert. **Merkregel: nach jeder `sec-main`-FT-Änderung jeden CAP einzeln auf `security.ft` prüfen —
|
|
||||||
CAP-lokale Inline-Overrides sind compliance-unsichtbar.** Offen in
|
|
||||||
TP25 bleibt nur der separate Punkt **ap2 unterperformt** (2 m/−80 dBm). Handoff:
|
|
||||||
[`docs/handoff-20260902-tp25-fund-e-messung.md`](handoff-20260902-tp25-fund-e-messung.md).
|
|
||||||
- ⏸️ **TP41 CF-Access-Reparatur — ZURÜCKGESTELLT 2026-09-01 (User-Entscheid; Rest wartet auf Info):** Session 720h,
|
|
||||||
login_method-Loch zu (`bf-users` email-only), `zt-`-Gruppen, vw voll Bypass + `vw/admin` per WAF
|
|
||||||
geblockt (Mobilfunk-verifiziert), API-Token auf vollständiges `…4148` konsolidiert. **T4 ✅**
|
|
||||||
(ein Access-Service-Token `zt-svc`, Terpene-Worker per wrangler auf zt-svc umgestellt, Alt-Tokens
|
|
||||||
gelöscht). **T3 ✅** (WARP Default-Profil fallback_domains `emsenhuber.net → .36/.37`). **T2 ✅**
|
|
||||||
(WAF-Regel gelöscht, Caddy-Origin-404 für `/admin` via CF-Header; Edge=404, lokal=200).
|
|
||||||
**T5 ✅** (GitHub- **und** Google-Login laufen, User-bestätigt 2026-09-01; Google via Consent-Fix §A).
|
|
||||||
**Facebook/Meta für TP41 abgesagt** (optional, kein CF-IdP angelegt) — Geli über OTP
|
|
||||||
`angelikahy@outlook.de` + Google `pillepalle670@gmail.com` voll gedeckt.
|
|
||||||
**Token-Altlast geklärt:** `b00722`=live Caddy-DNS-Token, `d47435`=stale Kopie → **✅ d47435 revoked**,
|
|
||||||
**✅ T3-WARP-Client-Verify** (User). **Geli CF-seitig fertig** (Enrollment via `zt-humans` + `geli`-Policy,
|
|
||||||
2026-09-01 live geprüft) → verbleibt nur ein **Device-Handgriff bei Geli** (WARP-Client, Team `emsenhuber`).
|
|
||||||
**Claude-/CF-Seite damit vollständig.** Handoff: [`docs/handoff-20260901-tp41.md`](handoff-20260901-tp41.md) · [Follow-up](handoff-20260901-tp41-followup.md).
|
|
||||||
- ✅ **TP14.4 als in TP41 T4 aufgegangen markiert (2026-09-02):** kein eigenständiger `claude-mcp`-Token
|
|
||||||
mehr — er wurde in TP41 T4 zu `zt-svc` konsolidiert, `mcp/cloudflare/.env` trägt jetzt
|
|
||||||
`CF_ACCESS_CLIENT_ID`/`_SECRET`. Die TP14.4-Ist-Stand-Notiz („.env leer") war seit 09-01 stale;
|
|
||||||
Quick-Index + TP-Abschnitt nachgezogen.
|
|
||||||
- ✅ **TP31 abgeschlossen markiert (2026-09-02):** Schritt 5+6 sind über den Piloten TP16.3 (09-01)
|
|
||||||
miterledigt — `bf-public` 12 conns, vier öffentliche Namen über Caddy, Alttunnel weg. Abnahme-Tool
|
|
||||||
`caddy/cf-sync.py` von einem Fehlalarm befreit (prüfte nur `http://`, gebaut ist `https://…:443`) und
|
|
||||||
**grün** (nur `ds-bf` als bewusst intern). Schritt-6-Teil „MCP auf FQDN" durch TP35 Schicht 1
|
|
||||||
umgekehrt (LAN-IP), damit gegenstandslos.
|
|
||||||
- ✅ **TP42 durch (2026-09-02):** interner Namensraum auf **einen** Suffix `bf.emsenhuber.net` vereinheitlicht.
|
|
||||||
(A) DHCP-domain über die GC-Pipeline geflippt (Template + `push-gc-template.py` `7a4068c4` + neuer Job
|
|
||||||
`tp42-dhcp-domain` + Deploy an core) → **7/7 live, Compliance 140/140**. Nebenbefund: Remote-GC-Template
|
|
||||||
hing hinterher (`.bf.lan`), mit angeglichen. (B) `.16` (`adguard_bf1`) aus dem **mit FM geteilten** Stack
|
|
||||||
195 getrimmt-redeployt — `.16` weg, `.17` (FM) läuft (kurzer ~40 s Blip). Nautobot-Doku-Teil (Scope 4)
|
|
||||||
an TP37 gekoppelt.
|
|
||||||
- 🔜 **TP42 Namens-Vereinheitlichung (2026-09-01, angelegt):** internes `.bf.lan` (Core-DNS, DHCP-Search-
|
|
||||||
Domain) und die `.bf.emsenhuber.net`-Aliase (Caddyfile) auf **einen** Namensraum `emsenhuber.net`
|
|
||||||
ziehen; stale `.16/.17`-Rewrites abbauen; Sollzustand in Nautobot. Handoff:
|
|
||||||
[`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md).
|
|
||||||
- 🔜 **TP43 dw-Standort-DNS-Uplink (2026-09-01, angelegt):** dem dw-Standort einen eigenen Resolver/
|
|
||||||
DNS-Uplink mit **site-spezifischen Overrides** geben (erster Fall `420.teilzeitladen.xyz` → BF-Caddy
|
|
||||||
über WG), **inkl. Verhalten bei WG-Disconnect** (Override auf BF-interne IP fällt sonst aus). FritzBox
|
|
||||||
standard kann keinen Per-Host-Override. Geli-Sofortlösung = **WARP** (nicht Teil von TP43). Cross-Projekt.
|
|
||||||
- ✅ **TP16.3 ABGESCHLOSSEN (2026-09-01):** Flip **und** Abbau durch. Öffentlich laufen nur noch
|
|
||||||
`bf-public` (12 Connectoren) und `fm` — die drei Alttunnel `bf-mm`/`bf-nas`/`bf-pve` **und** der
|
|
||||||
projektfremde `terpene-db` sind gelöscht, **7 cloudflared-Container** entfernt (inkl.
|
|
||||||
`terpene-cloudflared`, weil `420.teilzeitladen.xyz` nun über Caddy/CF läuft — vor der Löschung mit
|
|
||||||
echtem Origin-Inhalt belegt). §6 wurde auf User-Entscheid vorgezogen. **TP wandert ins Archiv.**
|
|
||||||
- 🆕 **TP16.3 §5 DURCH (2026-09-01): `n8n`/`gitea`/`hass`/`vw` laufen über `bf-public`** — der
|
|
||||||
öffentliche Verkehr geht damit erstmals über **Caddy** statt an ihm vorbei, und die
|
|
||||||
Host→Origin-Zuordnung lebt nur noch im `Caddyfile`. Je Name einzeln geflippt, dazwischen gemessen;
|
|
||||||
Antwortzeiten vorher/nachher **unverändert** (Caddy im Pfad kostet nichts Messbares).
|
|
||||||
Nachweise: `gitea` echte Gitea-Antwort · `hass` **WebSocket `101 Switching Protocols`** (= der
|
|
||||||
Companion-App-Pfad, den das Runbook als „nur vom User prüfbar" führte) · `vw` `/alive` + `/api/config`
|
|
||||||
200, WS-Hub `401` von Vaultwarden selbst · `n8n` von Christoph im Browser bestätigt.
|
|
||||||
🚫 **§6 (Abbau) bewusst NICHT** — die drei Alttunnel bleiben als Rückfallebene (healthy/8).
|
|
||||||
🔴 **Christoph hat den Flip ausdrücklich vorgezogen freigegeben** („aus Gründen"); die 7-Tage-Regel
|
|
||||||
betrifft ohnehin §6, nicht den Flip.
|
|
||||||
- 🆕 **TP39 ABGESCHLOSSEN + archiviert (2026-09-01):** letzter offener Punkt — der Resolver-Sweep —
|
|
||||||
ist durch: **pve alle 13 LXCs** auf `10.40.20.1`, und die **DS923-Container via Portainer** (ep14)
|
|
||||||
gegengeprüft — kein `.41.254`-Override, alle laufenden lösen in **66–110 ms** auf (inkl. Grenzfall
|
|
||||||
`gitea`, erstellt 2026-07-10), kein eingefrorener Dead-Resolver, kein Recreate nötig. Die
|
|
||||||
Abnahme-Regel („vor jedem LXC-Reboot `systemctl --failed` + Port-Vergleich") steht als
|
|
||||||
CLAUDE.md-Gotcha, der Sweep ist als wiederkehrende Kontrolle dort verankert. **Volltext → Archiv.**
|
|
||||||
🔴 Rest offen nur in **anderen** TPs: root-DB-PW-Rotation (→ TP30), HA-Recorder-Retention (→ TP40).
|
|
||||||
- 🆕 **TP39 fast durch (2026-09-01):** `skip-name-resolve` auf der MariaDB **aktiviert** (mysql.user
|
|
||||||
vorher geprüft — kein Hostname-Grant, gefahrlos; Wirkung: gitea/vw/hass 0 von 36 frischen Anfragen
|
|
||||||
langsam). LXC 302 war **100 % voll** → `+30G` online (95 GB/69 %). 🔴 **root-DB-Passwort dabei ins
|
|
||||||
Transkript geraten** (`.my.cnf` ohne `=`), zu rotieren (→ TP30).
|
|
||||||
- 🆕 **TP40 angelegt (2026-09-01, User-Okay):** HA-Recorder-Retention — die `homeassistant`-DB frisst
|
|
||||||
59 GB (`states.ibd` **41 GB**), unbegrenzt gewachsen. Das `+30G` aus TP39 ist ein Puffer; die
|
|
||||||
dauerhafte Lösung (`purge_keep_days`, `exclude`, `purge repack`) ist HA-seitig und **Christophs
|
|
||||||
Policy-Entscheidung** — kein eigenmächtiges Löschen.
|
|
||||||
- 🆕 **TP39: `networking.service` in vier Containern repariert (2026-09-01).** Ursache war dieselbe
|
|
||||||
Altlast wie beim Resolver: tote `auto`-Stanzas in `/etc/network/interfaces` aus dem VLAN1-Schema
|
|
||||||
(`eth0` → `10.40.41.x`) und alten Versuchen — `ifup -a` scheiterte an Interfaces, die es nicht gibt,
|
|
||||||
während `veth20` lief. **Abnahme war ein echter Reboot:** influxdb kam danach **von selbst** hoch
|
|
||||||
(vorher gar kein Startversuch). Alle 13 LXCs stehen jetzt auf `dns=10.40.20.1`, `networking` überall
|
|
||||||
sauber. **Offen in TP39: nur noch `skip-name-resolve` und der wiederkehrende Sweep.**
|
|
||||||
- 🆕 **TP39 angelegt (2026-09-01, User-Okay):** LXC-Hygiene auf pve — die zwei Restpunkte aus dem
|
|
||||||
Wartungsfenster unten (`skip-name-resolve` auf der MariaDB, kaputtes `networking.service` in vier
|
|
||||||
Containern) plus Resolver-Sweep als wiederkehrende Kontrolle. **Ein** TP, weil beides dieselbe Ecke
|
|
||||||
betrifft und in einem Fenster erledigt wird.
|
|
||||||
- ✅ **GELÖST 2026-09-01: der 10-Sekunden-Zoll auf `gitea`/`vw`/`hass` — es war Reverse-DNS auf einen
|
|
||||||
toten Resolver.** MariaDB (LXC **302**, `10.40.20.23`) hatte `skip-name-resolve` **auskommentiert**
|
|
||||||
und machte damit bei **jeder neuen Verbindung** einen Reverse-Lookup; ihr `/etc/resolv.conf` zeigte
|
|
||||||
auf **`10.40.41.254`** — den toten VLAN1-Resolver aus dem alten Schema (`nc` bestätigt tot).
|
|
||||||
**Gemessen im Container: 10.010 ms je Lookup.** Bestehende Pool-Verbindungen waren schnell, neue
|
|
||||||
kosteten 10 s — daher wirkte es sporadisch und unerklärlich.
|
|
||||||
🔑 **Die Blast-Radius war größer als gedacht:** an derselben DB hängen **Vaultwarden (`.127`),
|
|
||||||
Home Assistant (`.105`) und gitea (`.157`)** — belegt über die Verbindungstabelle, nicht vermutet.
|
|
||||||
*(Die erste Zuschreibung „vaultwarden nutzt SQLite, also nicht betroffen" war eine **Annahme** und
|
|
||||||
falsch — Christoph hat sie korrigiert, die Verbindungsliste hat es bewiesen.)*
|
|
||||||
**Fix (Wartungsfenster, User-Freigabe):** `pct set <id> --nameserver 10.40.20.1` + Reboot für
|
|
||||||
**fünf** LXCs — `302 mariadb`, `102 mqtt5`, `106 influxdb2`, `107 redis`, `150 iob-exposed`.
|
|
||||||
**Nachher: Lookup 21–24 ms statt 10.010 ms; gitea 0 von 25 Proben langsam** (vorher ~10 % bei
|
|
||||||
10,1 s), langsamste Probe 75 ms. Alle drei Dienste wieder verbunden.
|
|
||||||
⚠️ **Strukturell offen:** `skip-name-resolve` ist weiterhin aus — der Lookup ist jetzt nur schnell,
|
|
||||||
nicht abgeschafft. Ihn zu setzen bricht alle Grants, die auf **Hostnamen** statt IPs lauten; das
|
|
||||||
gehört vorher gegen `mysql.user` geprüft (DB-Zugang fehlt: `root@localhost` ohne Passwort verweigert).
|
|
||||||
- 🔴 **Latenter Fund aus demselben Wartungsfenster: `networking.service` und
|
|
||||||
`ifupdown-wait-online.service` sind in `101`, `102`, `106`, `107` fehlgeschlagen.** Folge:
|
|
||||||
Dienste mit `After=network-online.target` starten nach einem Reboot **nicht von selbst** —
|
|
||||||
**influxdb2 kam nach dem Neustart nicht hoch**, Unit `enabled`, aber **kein einziger Startversuch
|
|
||||||
im Journal**. Von Hand gestartet lief es sofort.
|
|
||||||
🔑 **Nicht durch diese Session verursacht:** `101 iob-master` wurde nie angefasst und zeigt dieselben
|
|
||||||
fehlgeschlagenen Units — die Container liefen nur 13+ Tage ohne Neustart, deshalb fiel es niemandem
|
|
||||||
auf. **Merkregel: diese vier Container sind einen Reboot davon entfernt, still Dienste zu verlieren.**
|
|
||||||
- 🔴 **Befund am Rand des Flips, NICHT von ihm verursacht: `gitea` hängt sporadisch exakt ~10,2 s.**
|
|
||||||
~10 % der Anfragen, quer über Web-UI, **`/api/healthz`** und den **Git-Pfad** (`info/refs`) — also
|
|
||||||
kein Rendering-Problem. `vw` und `hass` über **dieselbe** Strecke: **0 von 30**. Damit sind CF-Edge,
|
|
||||||
AP-Connector, Firewall und Caddy entlastet; übrig bleibt **Caddy → `10.40.20.157:3000`** oder gitea
|
|
||||||
selbst. **Immer ~10,15–10,43 s** = Timeout-Signatur (klassisch: ein toter Resolver, s. den
|
|
||||||
VLAN20-Docker-DNS-Gotcha in der CLAUDE.md), kein Lastproblem. Weiter nur mit LAN-Zugriff.
|
|
||||||
- 📎 **Drei Messfallen, die diese Session gekostet haben** (alle drei sind Standort-, keine Netzfehler):
|
|
||||||
1. **Split-Horizon macht Tests von zuhause wertlos:** der Haus-Resolver liefert für alle vier Namen
|
|
||||||
direkt `10.40.20.40`, die Anfrage sieht Cloudflare **nie**. Prüfen geht nur mit festgenagelter
|
|
||||||
CF-Edge-IP (`curl --resolve <name>:443:188.114.96.10`) oder von außerhalb.
|
|
||||||
2. **Access antwortet je nach Standpunkt verschieden:** `n8n` gibt aus dem Haus **200** (die
|
|
||||||
`bf4`-Policy trägt die WAN-IP), von außen **302** zum Login. Beides gesund. Für die drei
|
|
||||||
`bypass`-Namen (`hass`/`vw`/`gitea`) gibt es gar keinen Login — die Access-App heisst zu Recht
|
|
||||||
`hass, vw, gitea`, sie führt alle drei in `self_hosted_domains` (das Feld `domain` zeigt nur den
|
|
||||||
ersten). **Ein Rename wäre eine Verschlechterung gewesen.**
|
|
||||||
3. 🔴 **„Der Host ist ausgefallen" war der eigene Standortwechsel.** Ping/SSH/Caddy waren tot,
|
|
||||||
weil die Session-Maschine auf einen **Hotspot** gewandert war (Default-GW `172.20.10.1`) —
|
|
||||||
dieselbe Klasse wie die WARP-Falle von 2026-08-13, nur ohne WARP. Es wurde daraufhin ein bereits
|
|
||||||
erfolgreicher `n8n`-Flip **grundlos zurückgerollt**. **Merkregel: bevor ein Ausfall gemeldet wird,
|
|
||||||
`route -n get default` lesen** — ein Standortwechsel sieht exakt aus wie ein Totalausfall.
|
|
||||||
- ⚠️ **Nebenbefund: Caddy führt gar kein Access-Log.** Im `Caddyfile` steht keine `log`-Direktive, im
|
|
||||||
Journal landen nur `info`/`warn`. Der Abnahmepunkt „Caddy sieht den Request" (Runbook §5.3) **kann
|
|
||||||
nicht funktionieren** — ein erfolgreicher Request hinterlässt dort nichts, und leere Ausgabe liest
|
|
||||||
sich wie „kam nie an". Dass der öffentliche Reverse Proxy keine Requests protokolliert, ist für sich
|
|
||||||
eine Lücke (gehört zu TP31/TP34).
|
|
||||||
- 🆕 **TP16.3: die sechs AP-Drifts sind ins Soll gehoben (2026-08-31) — Compliance 139/140.**
|
|
||||||
`veth` / `ip address` / `ip firewall nat` der drei cloudflared-Connectoren stehen jetzt im
|
|
||||||
**AP-Template** (Block `[7]`) und im ConfigContext `ap-common` (`ap.container`), **nicht** in einer
|
|
||||||
`gc-transaktion`: ein Dauerzustand gehört ins Soll, eine Transaktion ist ein befristeter Versuch.
|
|
||||||
**Kein Re-Baseline, kein Rückbau, keine Zeile aufs Gerät** — nur die SoT nachgezogen.
|
|
||||||
🆕 **28. ComplianceRule `interface-veth`** (vorher gab es für `/interface veth` keine ⇒ ein
|
|
||||||
gelöschtes veth hätte **nichts** gemeldet). 🔴 **Zwei Funde dabei:** (a) der veth-Export trägt
|
|
||||||
**zwei gerätevergebene MACs** — eine nackte Regel hätte Dauer-Drift auf allen drei APs erzeugt;
|
|
||||||
gelöst mit zwei eng auf die veth-Zeile verankerten `ConfigReplace`-Regeln (`60-`/`61-veth-*`,
|
|
||||||
`nautobot/ingest-veth-rule.py`), bewusst mit der vorhandenen Maske `<<REDACTED>>`, weil die im
|
|
||||||
Repo zugleich die **Push-Sperre** ist. (b) **Der erste Compliance-Lauf meldete `SUCCESS` und war
|
|
||||||
trotzdem unvollständig** (139 statt 140 Einträge, `crs326-core`/`interface-veth` fehlte;
|
|
||||||
Job-Log: `RENDER COMPLIANCE TASK GROUP failed: Broken pipe`). Zweiter Lauf sauber.
|
|
||||||
⚠️ **Merkregel: nach einem Refresh nicht nur die Konformitätsquote lesen, sondern die ANZAHL der
|
|
||||||
Einträge** — sie muss `Regeln × Geräte` sein. Eine fehlende Kombination sieht aus wie „die neue
|
|
||||||
Regel greift nicht" und ist in Wahrheit ein halb gelaufener Job.
|
|
||||||
*(Die damals übrige `crs326-core`/`wifi-security`-Drift = Transaktion `TP25-fund-e-ft-over-ds` ist mit
|
|
||||||
dem Rückbau 2026-09-01 gegenstandslos; Fund E am 2026-09-02 geschlossen, Compliance 140/140.)*
|
|
||||||
- 🆕 **MCP-Bestand 2026-08-28 korrigiert:** **9 MCPs identisch** in `~/.claude.json` und `~/.config/opencode/opencode.jsonc` (vorher Asymmetrie 7 vs. 11 — `portainer-mcp`/`shelly-mcp` raus, `gitea-mcp`/`n8n-mcp` rein). **Mac mini = n8n-Host, NICHT zentraler MCP-Host** (Performance-Restriktion); zentraler MCP-Host = **DS923** (`nas-bf`, Portainer Endpoint 14). Inventur-Stand `mcp/INVENTORY.md` + Policy `docs/mcp-host-policy.md`.
|
|
||||||
- 🆕 **MCP-Migration FQDN→IP (2026-08-29, TP35 Schicht 1):** `gitea-mcp` und `n8n-mcp` in `~/.config/opencode/opencode.jsonc` von CF-FQDN auf LAN-IP umgestellt — `gitea-mcp` → `http://10.40.20.167:3000/mcp` (macmini, **ohne** Bearer, intern), `n8n-mcp` → `http://10.40.20.167:3001/mcp` (mit `Bearer {env:N8N_MCP_BEARER}`). **Befund dabei:** der Handoff 1.2 nannte für gitea-mcp die falsche IP `.157:3000` (das ist die Gitea-Web-UI, `400 Invalid CSRF`); der echte gitea-mcp läuft auf `.167:3000` (`serverInfo: gitea-mcp 0.0.10`). `cloudflare`-MCP bleibt auf dem offiziellen FQDN (User-Direktive).
|
|
||||||
- 🆕 **TP16.3 §2/§3 durchgezogen (2026-08-31, Vor-Ort):** je ein `cloudflared`-Container auf
|
|
||||||
**ap1/ap2/ap3**, `bf-public` **healthy mit 12 Connectoren** — der Tunnel hat damit zum ersten Mal
|
|
||||||
überhaupt Connectoren. 🔴 **Zwei Runbook-Fehler dabei aufgedeckt:** die Flash-Rechnung war um Faktor
|
|
||||||
2,5 zu optimistisch (entpacktes cloudflared = **69,5 MiB**, nicht 28 MB ⇒ das offizielle Image passt
|
|
||||||
auf keinen der drei APs; Ersatz ist ein selbst gebautes `FROM scratch`-Image mit 35,3 MiB, Binary-SHA
|
|
||||||
`e93824f6…` gepinnt), und `/container/envs` heisst `list=`, nicht `name=`.
|
|
||||||
🔴 **Offen: Compliance 128/135.** §3.4 verlangt Direkt-Writes für `ip address`/`nat`, die unter
|
|
||||||
GC-Kontrolle stehen ⇒ **sechs neue `extra`-Drifts** (zwei je AP), inhaltlich richtig, aber nicht im
|
|
||||||
Soll. Braucht eine Entscheidung (ins Template heben), **kein Re-Baseline**. Details im TP16.3-Abschnitt.
|
|
||||||
- 🆕 **TP37 angelegt (2026-08-30, User-Okay):** Nautobot soll durchgehende Netzwerkdoku werden, erster
|
|
||||||
Anwendungsfall die CF-Connectoren. 🔴 **Befund dabei, live gegen die API:** Nautobot kennt heute
|
|
||||||
**nur die 8 Netzgeräte** (4 aktiv, 4 decommissioned) — `ipam.Service`, `CustomField`, `Relationship`,
|
|
||||||
`VirtualMachine` und `Tag` sind **alle 0**, installiert ist nur `golden-config`. Kein Host, kein
|
|
||||||
Container, kein Dienst ist erfasst. **TP37 ist damit zuerst eine Datenmodell-Frage** und liefert das
|
|
||||||
Fundament, das **TP34** (Caddyfile via GC) und **TP36** (SSoT für diese Datei) ebenfalls brauchen —
|
|
||||||
Reihenfolge: **TP37 vor TP34**.
|
|
||||||
- 🆕 **CF-Public-Strang eingedampft (2026-08-30, TP16.3 grosser Schnitt):** **12 Hostnames** aus Ingress
|
|
||||||
**und** DNS entfernt — `pve`/`portainer`/`pve-mcp`/`ha-mcp`/`ds-bf`/`ccu`/`iobroker`/`z2m25`/`pbs`
|
|
||||||
plus die drei CF-Access-SSH-Ziele. Ingress: `bf-mm` 6→4, `bf-nas` 6→3, `bf-pve` 9→2. **Öffentlich
|
|
||||||
bleiben nur `vw`/`hass`/`n8n`/`gitea`** (weiterhin HTTP 200). Intern unverändert über AdGuard→Caddy
|
|
||||||
(9× HTTP 200). ✅ **Nachtrag 2026-08-30 (F1 durch): `gitea-mcp`/`n8n-mcp` sind jetzt AUCH
|
|
||||||
geschnitten** — damit **14 Hostnames** gesamt und `bf-mm` **4→2** (nur noch `n8n` + Catch-all).
|
|
||||||
Möglich wurde das, weil zuvor **12 MCP-Client-Configs** auf LAN-IP migriert wurden (nicht 10 wie
|
|
||||||
geschätzt: zwei Worktree-Kopien in `bf-network-r6{,.opencode}` kamen dazu). 🔴 **`bf-public` hat weiterhin 0 Connectoren** — er gehört
|
|
||||||
laut Entscheid auf **ap1/ap2**, dort fehlen aber `container`-Paket **und** `device-mode container=yes`
|
|
||||||
(physische Bestätigung nötig). Bis dahin stehen `bf-mm`/`bf-nas`/`bf-pve` mit **6 Containern**.
|
|
||||||
Backups: `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json`.
|
|
||||||
- 🆕 **TP16.3/TP31 Schritt 5 vorbereitet (2026-08-30):** Runbook für die Vor-Ort-Session geschrieben
|
|
||||||
([`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)), CF-Rollback-Snapshots
|
|
||||||
unter `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json` **neu angelegt** (es gab sie vorher nicht).
|
|
||||||
`docs/tp31-schritt5-runbook.md` ist damit **abgelöst** (read-only-Header): es kannte ap3 nicht,
|
|
||||||
unterstellte einen Registry-Pull (`fetch: no` auf ap1/ap2) und enthielt eine **falsche**
|
|
||||||
Firewall-Freigabe-Behauptung. **CF-Stand:** `bf-public` `inactive` / 0 Connectoren; die vier
|
|
||||||
öffentlichen Namen laufen noch über bf-mm/bf-nas/bf-pve **an Caddy vorbei** direkt aufs Backend.
|
|
||||||
- 🆕 **TP38 angelegt (2026-08-31):** OOB-Management — je ein untagged-VLAN99-Port auf beiden Switches.
|
|
||||||
Auslöser: beim Core-Neustart war `crs326-branch` nicht erreichbar, weil sein Mgmt **ausschließlich**
|
|
||||||
über den Core läuft (`vlan99` dort `tagged=ether1,ether24,bridge`, kein untagged-Member).
|
|
||||||
🔴 **Nebenbefund mit eigenem Gewicht: `crs326-branch` hat gar keine `input`-Firewallregeln** — sein
|
|
||||||
Schutz ist heute allein die Erreichbarkeit über den gehärteten Core. Das ist die Hauptauflage des TPs.
|
|
||||||
- 🆕 **ROS-Upgrade 7.24.1 auf allen fünf (2026-08-31, User).** In Nautobot nachgezogen: **ap1** stand
|
|
||||||
als einziges Gerät noch auf `7.24` (die anderen vier waren schon richtig) ⇒ auf `7.24.1` gesetzt.
|
|
||||||
⚠️ **Korrektur (2026-08-31, nachträglich):** die dabei gesehene `wifi-security`-Drift auf
|
|
||||||
`crs326-core` (`sec-main` `ft-over-ds=no` statt `yes`) hatte **nichts mit dem Upgrade zu tun** — sie
|
|
||||||
ist **TP25 Fund E, Hebel 1**, um 08:09 bewusst gesetzt (Core-Upgrade war erst 14:36) und als
|
|
||||||
**offene Transaktion `TP25-fund-e-ft-over-ds`** geführt (Scope `wifi-security`, Ablauf 2026-09-03).
|
|
||||||
**Die 134/135 sind Absicht**, nicht Schaden: grün machen würde den laufenden Versuch unsichtbar
|
|
||||||
machen, und `gc-transaktion.py` verweigert den Commit ohnehin, solange der Scope driftet.
|
|
||||||
🔑 **Lehre:** vor jeder Drift-Bewertung `nautobot/gc-transaktion.py status` lesen — Drift aus einem
|
|
||||||
laufenden Versuch sieht identisch aus wie ein Versehen, und genau dafür gibt es das Werkzeug.
|
|
||||||
- 🆕 **TP16.3 §1-Blocker remote erledigt (2026-08-31):** Ingress von `bf-public` auf
|
|
||||||
`https://10.40.20.40:443` + `originServerName`; Firewall-Freigabe `vlan99 → vlan20:443` **über
|
|
||||||
Nautobot GC** (Plan `Completed`, Re-Baseline `ingest-core-firewall.py --apply`, danach
|
|
||||||
**135/135 compliant / 0 Drifts**); `ROUTER_AP3_PASS` in `~/.secrets`. **Offen bleibt nur die
|
|
||||||
Vor-Ort-Session** (§2/§3: `container.npk`, `device-mode`, Container) und danach Flip + Abbau.
|
|
||||||
- 🆕 **Globale Verhaltensregeln konsolidiert (2026-08-29, TP35 Schicht 3):** `~/.config/opencode/AGENTS.md` ist jetzt die **SSoT** für globale Verhaltensregeln (Secrets, VLAN-Schema, 5 Lessons, SoC, Übergabe-Mechanik, Mid-Action-Checkpoint, Commit-Hinweis, Backup-Guard). `~/.claude/CLAUDE.md` hält denselben Block als Claude-Code-Spiegelung mit Verweis auf die SSoT. opencode liest AGENTS.md mit Vorrang vor `~/.claude/CLAUDE.md`.
|
|
||||||
|
|
||||||
- **Alle fünf MikroTik laufen auf RouterOS `7.24.1`** (🆕 **Angleichung am 2026-08-31 von Christoph
|
|
||||||
selbst durchgezogen, TP27 damit erledigt** — ap2 08:56, core 14:36, ap1 14:50, branch 14:52;
|
|
||||||
ap3 kam bereits so. RouterBOOT jeweils = `upgrade-firmware` = `7.24.1`, live gegengelesen).
|
|
||||||
Vorher: vier auf 7.24 (08-17), nur ap3 auf 7.24.1. Neu in der Flotte **`ap3`**
|
|
||||||
(cAP ax `cAPGi-5HaxD2HaxD`, **`10.40.99.13`**, Uplink core `ether21`).
|
|
||||||
✅ **Folge für TP16.3:** die versionsgebundene `container.npk` ist damit **eine** Datei für alle drei
|
|
||||||
APs (alle `arm64`) — die Zwei-Dateien-Regel vom 08-30 war rein versionsbedingt und ist erledigt.
|
|
||||||
*(Hier stand bis 08-29 „Alle vier MikroTik … 7.24" — durch ap3 und das Upgrade zweifach überholt.)*
|
|
||||||
- **L3 + WAN-Edge = `crs326-core`** (CRS326-24G-2S+, Technik UG): Routing, DHCP (7 Server,
|
|
||||||
54 Reservierungen), DNS, NAT, **NTP-Server für den Standort** und der WG-Hub (TP10, Peers `fm`/`dw`)
|
|
||||||
laufen hier. Mgmt-SVI **`10.40.99.1`** (vlan99), Gateway `.1` je VLAN. **WAN = `vlan100`** — DHCP-Client
|
|
||||||
hält die öffentliche Liwest-IP **`90.146.33.177/24`** direkt vom Arris (Bridge-Mode) → kein Double-NAT.
|
|
||||||
**DNS-Upstream = DoH zum CF-Gateway mit `verify-doh-cert=yes`** (TP23 Stufe 1 ✅ 08-18);
|
|
||||||
`servers=1.1.1.1` steht nur noch als Bootstrap für den DoH-Hostnamen.
|
|
||||||
- 🔥 **Die Firewall ist deny-by-default — in BEIDEN Ketten** (TP5 E3/E4 ✅ 08-18). `TP10-seal-lan` ist
|
|
||||||
`disabled`; Mgmt-Input kommt nur noch aus VLAN10, von HA (`8728`), von Nautobot (`22`) und über
|
|
||||||
`cf-mesh`. Ein neuer Dienst über eine VLAN-Grenze braucht ab jetzt eine explizite Regel.
|
|
||||||
- **L2-Branch = `crs326-branch`** (Technik OG): reiner L2-Switch, Mgmt **`10.40.99.2`**,
|
|
||||||
Uplink `ether1` → core `ether8`.
|
|
||||||
- **WLAN = CAPsMAN auf dem Core** (TP28 ✅ 08-17, GC-Modell nachgezogen 08-18): ap1 **`10.40.99.11`**
|
|
||||||
(an core `ether15`), ap2 **`10.40.99.12`** (an branch `ether24`), beide **reine CAPs** —
|
|
||||||
`cap enabled=yes`, `discovery-interfaces=vlan99`. Die APs halten **keine eigene WLAN-Config** mehr;
|
|
||||||
Kanäle, SSIDs, Security und Steering kommen vom Manager. 802.11k/v aktiv; **802.11r (FT) aus** —
|
|
||||||
`ft=no` auf allen SSIDs (sec-main seit 2026-09-04, User-Entscheid).
|
|
||||||
🆕 **Dritter AP `ap3` (cAP ax) ✅ LIVE seit 2026-08-30** (TP28 dafür wieder geöffnet):
|
|
||||||
`10.40.99.13`, Uplink `ether1` → **core `ether21`** (Trunk), reiner CAP, **RouterOS 7.24.1**
|
|
||||||
(🆕 **seit 2026-08-31 die ganze Flotte** — die frühere Versionsdifferenz ist aufgelöst). Kanäle
|
|
||||||
**`5180/ax/Ce/I`** + **`2437/ax`** (ch6) nach dem TP25-Plan vom 2026-08-24, am Gerät nachgemessen.
|
|
||||||
🔴 **ap1 ist funkstill** — es steht bis zu seinem Umzug ans Sideboard direkt neben ap3 und würde
|
|
||||||
auf 5180 kollidieren; es hat deshalb bewusst **keine Provisioning-Regel** am Manager, bleibt aber
|
|
||||||
erreichbar. Beim Umzug: ap1 → `5260/Ce` (Band B, DFS, **Frequenz-Liste statt Einzelwert**),
|
|
||||||
ap2 → `5300/Ce`, Uplink ap1 → `core ether3`.
|
|
||||||
**Compliance jetzt 130/130 / 0 Drifts über 5 Geräte** (vorher 104/104 über 4).
|
|
||||||
- **SSDP-Relay** (TP7.1, **nachgebessert 08-25**): LXC **108 `mcast-relay`** auf pve, zweibeinig
|
|
||||||
`10.40.10.21` (VLAN10) + `10.40.40.21` (VLAN40) — dafür trägt `core ether13` beide VLANs; mDNS macht
|
|
||||||
weiterhin der Core. **Läuft seit 08-25 ohne `--ssdpUnicastAddr`** — die Flagge verwarf die M-SEARCH
|
|
||||||
der App auf dem Weg nach VLAN40 (Messung: **0 Pakete `via eth1` gegen 308 `via eth0`** in 20 min,
|
|
||||||
**0×** `Received SSDP Unicast` im ganzen Journal seit 08-18). Der Rückweg läuft jetzt **nativ geroutet**
|
|
||||||
über die längst offene Regel `TP5: MEDIA->MAIN`; nachgemessen kommen `HTTP/1.1 200 OK` von
|
|
||||||
`10.40.40.193/.195` an beiden MacBook-Beinen an. Unit liegt im Repo: **`mcast-relay/multicast-relay.service`**,
|
|
||||||
Details als Gotcha in `CLAUDE.md`. ⚠️ **Der Fall hatte zwei unabhängige Ursachen** — nach der
|
|
||||||
Relay-Reparatur suchte die **App** immer noch nicht (macOS-Local-Network-Gate stand auf „an" und
|
|
||||||
griff nicht; Fix war Schalter aus/ein + Neustart, User-bestätigt 08-25). **Beide sind erledigt.**
|
|
||||||
- **Reverse Proxy = `caddy-bf1`** (LXC 130 auf pve, **`10.40.20.40`**, TP31 ✅ Schritt 1–3 seit
|
|
||||||
2026-08-25, **`hass` 400 behoben 2026-08-26, Split-Horizon seit 2026-08-26**): Caddy 2.11.4,
|
|
||||||
**16 Sites** (4 public, 12 intern mit `.bf.`-Suffix), Zertifikate ausschliesslich
|
|
||||||
per **DNS-01** mit einem eng geschnittenen `Zone:DNS:Edit`-Token (`CF_DNS_API_TOKEN`, **nicht** das
|
|
||||||
account-weite). Abnahme über den Host-Header von einem zweiten Host: 11× 200, `nautobot` 302.
|
|
||||||
Offen: **Schritt 5–6** (Connector für `bf-public`, CNAME-Flip, Abbau der sechs Container) —
|
|
||||||
**Runbook steht: [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)**,
|
|
||||||
Vor-Ort-Session an ap1/ap2/ap3. ✅ **Die drei §1-Blocker sind am 2026-08-31 remote erledigt** —
|
|
||||||
**(a)** Ingress auf `https://10.40.20.40:443` + `originServerName` je Regel umgestellt (die alte
|
|
||||||
`:80`-Zeile hätte über CF eine Redirect-Schleife gebaut, weil Caddy dort 308 auf https antwortet);
|
|
||||||
**(b)** Freigabe `vlan99 → vlan20:443` **über Nautobot GC** deployt (`ip-firewall-filter` steht unter
|
|
||||||
Drift-Kontrolle, TP26 ⇒ kein Direkt-Write), Position vor `TP5: log-only` verifiziert, **135/135
|
|
||||||
compliant, 0 Drifts**; **(c)** `ROUTER_AP3_PASS` in `~/.secrets` nachgetragen (REST-Auth 200).
|
|
||||||
⇒ **Vor Ort startet die Arbeit bei §2/§3** (Pakete, `device-mode`, Container).
|
|
||||||
- **DNS/Resolver = AdGuard-Paar `agh-bf1`/`agh-bf2`** (LXC 110/111 auf pve, **`10.40.20.36`/`.37`**,
|
|
||||||
TP23 Stufe 2 + TP31 Schritt 4 ✅ seit 2026-08-26): Upstream DoH zum CF-Gateway, conditional
|
|
||||||
`[/bf.emsenhuber.net/] → 10.40.20.1`, 47 Rewrites (16 Service → Caddy `.20.40` + 10 Host-Namen
|
|
||||||
mit `.bf.`-Suffix + alte suffixlose parallel). DHCP liefert `.36`/`.37` als DNS in allen 7 VLANs
|
|
||||||
(`domain=emsenhuber.net`), Firewall forward UDP+TCP/53 auf beide. Der Core-Resolver (`10.40.X.1`)
|
|
||||||
bleibt als Bootstrap unverändert (DoH, `allow-remote-requests=yes`, `mdns-repeat-ifaces`).
|
|
||||||
🆕 **TP23 Stufe 4+5 ✅ 2026-09-04:** `clients.persistent` = 7 VLAN-CIDR-Gruppen + `dns-bypass-exempt`
|
|
||||||
(Policy-Matrix, INFRA/MEDIA/MGMT `filtering=aus`), API-User `automation`, Sync-Tool + redigierte
|
|
||||||
Repo-SSoT unter `adguard/`. HA-Self-Service (Button → JIT-Domain-Freigabe, nativer Timer-Revert) live.
|
|
||||||
Offen nur **Stufe 6** (Bypass-Sperre, TP5). Werkzeuge/Doku: `adguard/{agh-sync,agh-exception}.py`,
|
|
||||||
`adguard/{README,ha-self-service}.md`.
|
|
||||||
- **Ausgebaut / gelöscht:** ALLNET-`sw1` und Omada-`sw2` sind **physisch weg** (2026-08-08 bzw. 2026-07-23,
|
|
||||||
in Nautobot *Decommissioned*); Omada-Controller + `omada-mcp` gelöscht (2026-07-16); FritzBox 6660 raus
|
|
||||||
seit 2026-07-01 (Liwest-Tausch auf Arris). **VLAN1 ist leer** (`/ip arp` auf `bridge` = 0 Einträge,
|
|
||||||
08-18) — TP19 ist geschlossen; `cudy1` hängt stromlos an core `ether11`, physischer Ausbau ist hands-on.
|
|
||||||
- 🆕 **WLAN-Logging persistent seit 2026-08-30 (TP25 Fund E):** `wireless` + `caps` gehen vom Core
|
|
||||||
in den TP5-Kollektor (`10.40.20.18:514`); `/system logging` steht unter der **27. ComplianceRule**
|
|
||||||
`system-logging`. Vorher nur Ringpuffer → ein seit Wochen gemeldeter iPhone-Abbruch war rückwirkend
|
|
||||||
nicht diagnostizierbar.
|
|
||||||
- ⚖️ **TP25 Fund E — Hebel 1 ZURÜCKGEBAUT, Entscheidung faellt am 2026-09-02 (Stand 09-01 09:2x):**
|
|
||||||
Zwei Sessions haben denselben Fall bewertet und kamen zu entgegengesetzten Schluessen. **Aufgeloest
|
|
||||||
zugunsten der Kollektor-Messung:**
|
|
||||||
- Die Nacht-Session las `get_log … sinceMinutes=1440` → `total 0` und schloss auf „geheilt". Das ist
|
|
||||||
der **fluechtige Ringpuffer des Routers**: live nachgemessen **1000 Zeilen, aeltester Eintrag
|
|
||||||
01.09. 00:38:58** — keine 9 h Tiefe. Der Core-Reboot 14:36 hatte ihn geleert, der 31.08. Vormittag
|
|
||||||
war darin **nicht sichtbar**. 🔴 Genau die Falle, wegen der am 30.08. der persistente Kollektor
|
|
||||||
eingerichtet wurde.
|
|
||||||
- Im **Kollektor** stehen fuer 08:09–14:36 (Hebel 1 **allein**, vor dem Upgrade) **124×
|
|
||||||
`reauthenticating`**, Stundenverteilung 09h=1 · 11h=2 · 12h=20 · 13h=31 · 14h=70, **alle vom
|
|
||||||
iPhone allein** (= Client-Befund nach der eigenen Merkregel „viele MACs = Infrastruktur"), dazu
|
|
||||||
Christophs **zwei erlebte Ausfaelle** um 13:16 und 14:23. Zwischen 09:00 und 14:36 gab es
|
|
||||||
**keinen Reboot**, der sie erklaeren koennte.
|
|
||||||
- ⇒ Hebel 1 hat im upgrade-freien Fenster **nichts** bewirkt; der Einbruch faellt exakt auf 7.24.1.
|
|
||||||
**Deshalb am 2026-09-01 zurueckgebaut** (`ft-over-ds=yes`, Provisionierung nachgezogen, frische
|
|
||||||
DHCP-Lease abgenommen, Transaktion **`zurueckgebaut`**, **Compliance 140/140**). Der Rueckbau **ist
|
|
||||||
das Experiment**: bleibt es ruhig, war 7.24.1 die Ursache; kehrt das Haemmern zurueck, hatte die
|
|
||||||
Nacht-Session recht und Hebel 1 wird als neue Transaktion erneut gezogen. **Bis zur Messung am
|
|
||||||
2026-09-02 frueh committet niemand.** Handoff:
|
|
||||||
[`handoff-20260901-fund-e-hebel1-zurueckgebaut.md`](handoff-20260901-fund-e-hebel1-zurueckgebaut.md).
|
|
||||||
- 🔴 **ABER: neuer Teilbefund am 23:02 (2026-08-31) — `ap2` unterperformt, NICHT FT.** Christoph am
|
|
||||||
Schreibtisch, 5G, kein Telefonat. Log: iPhone `F2:62:4C:E1:AF:09` ping-pongt im Sekundentakt
|
|
||||||
`cap-wifi5 ↔ cap-wifi6` = **beide Radios von ap2** (5G 5220 ↔ 2,4G ch11/2462, Band-Steering-Thrash
|
|
||||||
auf demselben AP), `disconnected, not responding` um 22:37:40 und 23:01:00. Feldstärke **−75…−88 dBm
|
|
||||||
— bei laut Christoph 2 m Abstand** (ap2 auf 2,3 m, gleicher Raum). Bei 2 m wären −35…−45 normal.
|
|
||||||
Aktuell **0 Clients auf ap2**, alle 29 auf ap3. ⇒ kein Coverage-/FT-Problem, sondern **ap2s Funk
|
|
||||||
selbst** (RF/Antenne/HW) und/oder die **DTU-Störung auf ap2/ch11** (Transaktion `TP17.1-dtu-pin-ap2`,
|
|
||||||
−59 dBm auf ap2s 2,4-Radio). Als eigener 🔬-Unterpunkt geführt, In-situ zu messen. Handoff:
|
|
||||||
[`handoff-20260901-tp25-ap2-verdacht.md`](handoff-20260901-tp25-ap2-verdacht.md).
|
|
||||||
- 🔴 **TP25 Fund E — Hebel 1 ist GESCHEITERT (Stand 2026-08-31 nachmittags):** `sec-main` steht am
|
|
||||||
Core auf **`ft=yes ft-over-ds=no`** (seit 08:09), aber das Symptom blieb: **124× `reauthenticating`
|
|
||||||
nach dem Eingriff** und **zwei von Christoph erlebte Ausfälle** — 13:16 (Videocall bricht ab,
|
|
||||||
7 min ohne WLAN) und 14:23 (5,5 min, nur durch WLAN-Toggle beendet). Stundenvergleich auf
|
|
||||||
gleicher Firmware: **07:00–08:00 = 79 · 14:00–15:00 = 70**. Erfolgskriterium war „~0" ⇒ nicht
|
|
||||||
eingetreten. Gewertet wird bewusst nur **08:09–14:36** (danach kommt das TP27-Upgrade als zweite
|
|
||||||
Variable dazu; Christoph-Entscheid: „Vormittag als Ergebnis werten").
|
|
||||||
⇒ Transaktion **`TP25-fund-e-ft-over-ds` bleibt OFFEN** (Ablauf **2026-09-03**), **Compliance
|
|
||||||
weiter bewusst 134/135**; Rollback oder Commit ist eine Entscheidung, kein Aufräumen.
|
|
||||||
Geräte-Backup `pre-tp25-funde-ftoverds.backup` liegt auf dem Core.
|
|
||||||
**Nächste Entscheidung:** Hebel 2 (`ft=no`) **oder** `authentication-types=wpa2-psk,wpa3-psk` —
|
|
||||||
`sec-main` ist auch die einzige SSID ohne `wpa2-psk`, und `association rejected, no cached PMK`
|
|
||||||
(157× am 31.08., 0× am 30.08.) ist eine SAE/PMKSA-Meldung. **Vorher prüfen, ob 7.24.1 auf dem
|
|
||||||
Manager das Verhalten schon verändert hat** — zwischen 14:37 und 15:12 stand nur **1**
|
|
||||||
`reauthenticating` im Log, keines vom iPhone (35 min = noch keine Aussage).
|
|
||||||
Details: [`handoff-20260831-tp27-upgrade-und-hebel1-befund.md`](handoff-20260831-tp27-upgrade-und-hebel1-befund.md).
|
|
||||||
- ⚠️ **Messfalle, die diese Session fast erwischt hat:** die 13 CAP-`joined`/`activity timeout` vom
|
|
||||||
31.08. sahen nach CAPWAP-Instabilität aus — es waren die **Reboots des TP27-Upgrades**. Und die
|
|
||||||
**`cap-wifiN`-Karte hat sich an einem Tag zweimal verschoben** (08:09, 14:36), womit die im
|
|
||||||
Vorgänger-Handoff festgehaltene Tabelle nach sechs Stunden falsch war. Interface-Namen in
|
|
||||||
Logzeilen sind ohne zeitnahen Karten-Snapshot **nicht zuschreibbar**.
|
|
||||||
- **Kontrolle / Governance:** MikroMCP für Reads/Debug (alle 5 Geräte in der Registry),
|
|
||||||
**Config-Änderungen ausschließlich über Nautobot Golden Config** (TP21/TP22 — Deploy-Pipeline scharf,
|
|
||||||
**135/135 Compliance-Checks grün, 0 Drifts** bei **27 Rules × 5 Geräten** (Stand 2026-08-30;
|
|
||||||
**seit 2026-08-31 vorübergehend 134/135 — der eine Drift ist die offene Transaktion
|
|
||||||
`TP25-fund-e-ft-over-ds`, s. oben, kein Fehler**) — live gegen die API
|
|
||||||
geprüft am **2026-08-23**; zuletzt dazugekommen: **`wifi-access-list`** (08-22), **`queue`**
|
|
||||||
(08-23, TP6) und **`ip-firewall-mangle`** (08-23, TP6.1), s. TP22-Nachtrag im Archiv).
|
|
||||||
*(Hier stand bis zum 08-23 „100/100 bei 25 Rules" — eine Zählung, die TP6.1 am selben Tag überholt
|
|
||||||
hat. Die Merkregel gilt: der Ist-Stand-Block wird bei jeder Änderung mitgezogen, nicht nur der TP.)*
|
|
||||||
|
|
||||||
<details><summary>📜 Historische Fassung (Stand bis 2026-08-08, Topologie <b>vor</b> dem Core-Cutover — bewusst erhalten, nicht als Ist lesen)</summary>
|
|
||||||
|
|
||||||
> ALLNET-Core `.11` (FW2.2.1, VLANs 10/20/30/40/50/90/99/100; GE1/GE2/GE8 Trunk; **TP1 ✅ 2026-06-15**). Omada-Switch `.12` (adopted, **TP2 ✅ 2026-06-15**; Trunk-ALL auf P1+P2, Access-Profile auf P3-P8; Profile noch PVID=1 bis Omada-PVID-Fix).
|
|
||||||
> Omada-Controller **`.175`** (DS923/macvlan, mbentley 6.2, 🟢 live). omada-mcp **`.176`** (pve-LXC, StreamableHTTP, 🟢 live, 84 read-Tools; Controller-API v2 intern via christoph@emsenhuber.com).
|
|
||||||
> 2× hAP ax² (7.23.1): ap1 `.1` (ALLNET P1) — **TP4 ✅ 2026-07-01: Router + WAN-Edge** (VLAN-Subinterfaces, DHCP, DNS, NAT, Public-Edge). ap2 `.2` (Omada P2 — Trunk).
|
|
||||||
> **Edge-Cutover ✅ 2026-07-01:** FB6660 physisch entfernt (Liwest-Kabelmodemtausch, kein Zurück), Arris-Kabelmodem im Bridge-Mode; WAN = `sw1 gi16 Access VLAN100 → gi15 Trunk → ap1 ether2 → vlan100 auf bridge` (**NICHT** ether1); ap1 hält Public-IP direkt (Liwest, kein CGNAT), Double-NAT weg. Telefonie war auf der FB und wird nicht weitergeführt. Vorarbeit: TP10-Kern durch (2026-06-27, WG-Hub live) → Bridge-Gate vorher gelöst. Runbook: `routeros/wan-cutover-runbook.md`.
|
|
||||||
> Kontrolle: MikroMCP (MikroTik) live · ALLNET = Klickanleitungen · Omada → Controller + omada-mcp 🟢.
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
**Nächster Schritt (Stand 2026-08-30): Vor-Ort-Session ap1/ap2** — sie ist der einzige Weg, den
|
|
||||||
Public-Strang auf `bf-public` zu heben und die drei Alt-Tunnel samt sechs Containern abzubauen
|
|
||||||
(TP16.3 Follow-up F2). ✅ **F1 ist am 2026-08-30 durch** (12 Client-Configs auf LAN-IP, danach
|
|
||||||
`gitea-mcp`/`n8n-mcp` aus Ingress+DNS geschnitten) — davor unabhängig machbar bleibt nur noch die
|
|
||||||
**Announcement-Doku (TP35 Schicht 4)**, die den anderen Projekten die neuen MCP-URLs mitteilt.
|
|
||||||
|
|
||||||
**Vorherige Fassung (Stand 2026-08-28): drei Folge-Sessions parallel** (siehe `docs/handoff-20260828-mcp-bestand-und-performance.md`):
|
|
||||||
> - ✅ **Session A** — `/projektstatus`-Skill global für opencode (TP35 Schicht 6). Funktionsbeweis erbracht 2026-08-28.
|
|
||||||
> - 🔜 **Session B** — shelly-mcp-Container auf DS923 (`10.40.20.10`) deployen (live, ssh). Repo-Materialien committed (`mcp/shelly/{Dockerfile,BUILD.md,requirements.txt,.gitignore}`); `.env` und Tunnel-Secret lokal — kein MCP-Client (User-Direktive).
|
|
||||||
> - 🔜 **Session C** — `caddy/cf-sync.py`-Drift-Check + Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5).
|
|
||||||
>
|
>
|
||||||
> 🆕 **Dazu seit 2026-08-30: TP16.3/TP31 Schritt 5 = Vor-Ort-Session** an ap1/ap2/**ap3**
|
> **Diese Datei führt nur noch die per-TP-Prosa** — die `### TPnn`-Abschnitte unten; Abgeschlossenes im
|
||||||
> ([Runbook](tp16.3-bf-public-ap-connector-runbook.md)). **Reihenfolge beachten:** die drei Blocker in
|
> **[Archiv](teilprojekte-archiv.md)**. **Live-Gerätedaten** (Compliance-Zahl, RouterOS-Version …) kommen
|
||||||
> §1 sind **remote** zu erledigen, *bevor* jemand in den Keller geht — sonst steht man vor einem
|
> aus **Nautobot**, nicht von hier. Sync-Werkzeug Datei/Archiv → Gitea (idempotent, Dry-Run default):
|
||||||
> Connector, der `healthy` meldet, während die Seite eine Redirect-Schleife dreht.
|
> `bin/gitea-tp-sync.py`.
|
||||||
>
|
>
|
||||||
> 🆕 **Strukturelle Folge = TP36:** SSoT für `teilprojekte.md`-Live-Daten via Nautobot (eigene Session, nicht heute). Befund 2026-08-28: manuell gepflegte Datei verliert gegen die Realität, sobald sie lang wird. Quickref-Index, TP-Abschnitt und Ist-Stand-Block müssen bei jedem Edit mitgezogen werden — keine schlägt die andere. **Merkregel im Lessons-Block.**
|
> *(TP36-Cutover 2026-09-07: die frühere Quick-Index-Tabelle und der chronologische „Ist-Stand (Kurz)"-Block
|
||||||
|
> sind aus dieser Datei entfernt, damit TP-Branches nicht mehr auf der Steuerungs-Doku kollidieren — Status-
|
||||||
|
> Updates gehen nach Gitea statt in diese Datei. Die alte Ist-Stand-Historie steht in der Git-Historie.)*
|
||||||
|
|
||||||
> 🆕 **Neu am 2026-08-24/25:** [TP31](#tp31) löst **TP16** und **TP16.2** ab und dampft cloudflared
|
## Governance & genehmigte Ausnahmen (stehende Regeln — kein Status)
|
||||||
> von **sechs Containern auf einen** ein; [TP32](#tp32) gibt die Nummern- und Adress-Systematik vor
|
|
||||||
> (und hat nebenbei die **falsche Startreihenfolge auf pve repariert** — HA startete vor seiner
|
|
||||||
> Datenbank). **TP16 ✅, TP25 ✅ und TP16.3 (Pilot) ✅** sind am 24./25. durchgezogen worden.
|
|
||||||
|
|
||||||
**Vorherige Fassung (Stand 2026-08-23): Christoph entscheidet — der kritische Pfad ist LEER.**
|
|
||||||
> 🎯 **Das Projekt ist in seinem Kern fertig.** deny-by-default steht (TP5), Golden Config steht und
|
|
||||||
> meldet **104/104 compliant bei 0 Drifts** (live geprüft 2026-08-23), WLAN steht über CAPsMAN auf dem
|
|
||||||
> Core (TP28), VLAN1 ist leer (TP19) — und **TP6 ist beantwortet statt offengelassen**: CAKE gebaut,
|
|
||||||
> gemessen, wieder ausgebaut. **Nichts blockiert mehr etwas.**
|
|
||||||
> 🟢 **Was zuletzt wirklich etwas gebracht hat, war TP6.1** (2026-08-23): nicht Shapen, sondern
|
|
||||||
> **Klassentrennung** — NAS-Bulk `priority=8` gegen interaktiv `priority=1` drückt dieselbe Strecke
|
|
||||||
> unter derselben Sättigung von **40,8 auf 11,0 ms (−73 %)**. Die zuerst notierte DOCSIS-Erklärung aus
|
|
||||||
> TP6 ist damit **widerlegt**; der Fehler lag in der Messmethode (Last und Messung vom selben Host).
|
|
||||||
> 📋 **Offen und ohne Blockadewirkung:** **TP25** (Fund F = 5:05-Takt bei IoT, plus die Entscheidung
|
|
||||||
> „5 GHz auf 20 MHz") · **TP16** (CF-Tunnel-HA degradiert, vier tote Alt-Tunnel) · **TP16.2/16.3** ·
|
|
||||||
> **TP23 Stufe 2** · **TP24** (DC-USV) · **TP13/TP14.4** (beide mit fertigem Runbook) ·
|
|
||||||
> ❌ **TP17.1 VERWORFEN 2026-08-23** (Zigbee-Weg bewiesen, Ziel damit nicht erreichbar — das Gerät
|
|
||||||
> bleibt WLAN-Client; `s9` zurückgebaut) · ✅ **TP17.3 ERLEDIGT** (Ursache **`eco_mode`** bewiesen;
|
|
||||||
> `s23` + `s26` repariert; **`eco_mode` flottenweit aus**, 22/22 außer `s24`) ·
|
|
||||||
> ➡️ **TP17.2 ist am 2026-08-24 in TP30 aufgegangen** (Shelly-Gerätepasswort = Erstvergabe eines
|
|
||||||
> Credentials; TP30 ist das Sammelbecken dafür) ·
|
|
||||||
> 🔑 **Alle Passwort-/Token-Rotationen sind seit 2026-08-24 TP30** (Sammel-TP) — **bewusst
|
|
||||||
> gebündelt gegen Projektende**, User-Entscheid. Damit ist die ältere Einreihung „hinter TP17.1"
|
|
||||||
> gegenstandslos (TP17.1 ist verworfen). **Etappe 1 — die Service-Accounts — ist durch;**
|
|
||||||
> das `admin`-Passwort selbst ist **nicht rotiert** und bleibt es bis dahin bewusst.
|
|
||||||
> 📌 **Einstieg für die nächste Session:
|
|
||||||
> [`handoff-20260825-agh-dns-cutover.md`](handoff-20260825-agh-dns-cutover.md)** — AdGuard als
|
|
||||||
> Resolver + Split-Horizon (TP23 Stufe 2 ∥ TP31 Schritt 4). **Bewusst eigene Session:** der Schritt
|
|
||||||
> verschiebt die DNS-Auflösung des ganzen Standorts in einen Container.
|
|
||||||
> 📜 Älter: [`handoff-20260824-session.md`](handoff-20260824-session.md)
|
|
||||||
> — ⚠️ dessen Empfehlung „Start bei der Passwort-Rotation Etappe 2" ist durch den TP30-Entscheid
|
|
||||||
> **überholt**; der Rest der Übergabe gilt. Ältere Sitzungsvorschläge:
|
|
||||||
> [`handoff-20260822-naechste-schritte.md`](handoff-20260822-naechste-schritte.md).
|
|
||||||
> ✅ **`crs326-core` ist transaktionsfrei** (seit 2026-08-22, geprüft): ein gemeldeter Drift ist ab
|
|
||||||
> jetzt **echt**, nicht beabsichtigt — `python3 nautobot/gc-transaktion.py status`.
|
|
||||||
|
|
||||||
<details><summary>📜 Vorherige Fassungen dieses Absatzes (2026-07-22 → 2026-08-21, bewusst erhalten)</summary>
|
|
||||||
|
|
||||||
> *Fassung 2026-08-21 abends (erledigt — TP25s Dead Spot ist am 08-22 repariert, TP6 am 08-23 geschlossen):*
|
|
||||||
> **Nächster Schritt: TP25 — Steering-Test in den Schlafräumen.**
|
|
||||||
> 📋 **Einstieg: [`handoff-tp25-session-20260821.md`](handoff-tp25-session-20260821.md)** — dort steht
|
|
||||||
> der verifizierte Ist-Stand, die Messfallen und der nächste Test.
|
|
||||||
> ✅ **`crs326-core` ist transaktionsfrei (2026-08-22).** `TEST-ether3-trunk-ap1-sideboard` und
|
|
||||||
> `TEST-infra-ssid-matter-pairing` sind beide zurückgerollt und geschlossen; ap1 hängt wieder auf
|
|
||||||
> `ether15`, `ether3` ist wieder untagged VLAN40. Ein gemeldeter Drift ist ab jetzt **echt**, nicht
|
|
||||||
> beabsichtigt — `python3 nautobot/gc-transaktion.py status`.
|
|
||||||
> 🆕 **TP29** (Anwesenheitserkennung fürs Energiesparen) ist als Idee angelegt, nicht auf dem Pfad.
|
|
||||||
> 📌 **Die TP6-Prämisse ist am 2026-08-20/21 gemessen** (Runbook §5): formal Fall B (+28…+47 ms unter
|
|
||||||
> Dauer-Upload), aber im Videocall **nicht wahrnehmbar** — kein einziger Problem-Klick über sechs
|
|
||||||
> Lastphasen. Laut war stattdessen der Funk: **Sticky Client + Band-Ping-Pong, zwölf Wechsel in neun
|
|
||||||
> Minuten**, je Wechsel bis zu 4 s ohne Durchsatz →
|
|
||||||
> [`tp25-wlan-roaming-messung-20260820.md`](tp25-wlan-roaming-messung-20260820.md). Deshalb steht jetzt
|
|
||||||
> **TP25 vor TP6**; TP6 bleibt offen und begründet, aber nachrangig.
|
|
||||||
>
|
|
||||||
> *Vorherige Fassung (Stand 2026-08-18, erledigt):*
|
|
||||||
TP6 wird hier seit Monaten als „DAS Ziel" geführt, aber `docs/call-quality-diagnosis.md` hat den
|
|
||||||
Bufferbloat 2026-06-16 bereits gemessen (**A/A+**, „CAKE wäre marginal, nicht deployt"). Die Messung
|
|
||||||
stammt aus der FritzBox-Ära, der Edge ist seither getauscht — also **einmal gegen den heutigen Edge
|
|
||||||
nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Nacht auf den 18. abgeschlossen worden (Etappe 3 + 4: deny-by-default in `forward` **und** `input`, MGMT isoliert, `seal-lan` abgeschaltet), TP23 Stufe 1 (DoH auf dem Core) ebenfalls. Nebenher offen, blockiert nichts: **TP25** (Signal-Boden + Re-Walk), **TP28-Rest** (die fünf `wifi-*`-Features vom AP-Scope auf den Core umhängen — die einzigen verbliebenen Compliance-Drifts), **TP7.1** (SSDP-Relay für die macOS-Sonos-App), **TP16.3**, **TP24**.
|
|
||||||
> *Vorherige Fassung (Stand 2026-08-14, überholt): „TP5 — Deny-by-default am Ende von `forward`." Alles Davorliegende ist durch:* CRS326-Bring-up + L3-Umzug ap1→`crs326-core` (TP15/TP21.1 ✅ 2026-08-08), WLAN-Neuprovisionierung ohne CAPsMAN (TP3 ✅ 2026-08-08), Nautobot-Plattform + Fleet-Adoption + Deploy-Pipeline (TP21/TP22 ✅ 2026-08-13). Danach **TP6 (CAKE) = das Projektziel**. Kleine Follow-ups nebenher: DoH-Cloudflare auf core, DC-USV (**TP24**, seit 2026-08-16 eigener TP). ⚪ **Omada-PVID-Fix ist obsolet** (User-Entscheid 2026-07-22 — Omada spielt seit der TP15-Korrektur keine Rolle mehr im Netz, nie mehr aufgreifen).
|
|
||||||
> *Vorherige Fassung (Stand 2026-07-22, erledigt): „TP15 CRS326-Bring-up als neuer L3-Core, danach WLAN-Neuprovisionierung ohne CAPsMAN; TP5 folgt nach TP21/CRS326-Adoption."*
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
> 🎯 **Ziel: sauberes System „from scratch", keine Leichen/Altlasten.** Migrations-Session-Endstand = **VLAN1 + altes cudy-WLAN komplett weg** (VLAN1 nur noch Mgmt, `defconf`-Client-DHCP zurückgezogen, cudy-Uplinks aufs Ziel-Client-VLAN getaggt oder cudy abgebaut). **Unterpunkte/Aufräum-Liste bei Migration:** (a) die Interim-Härtung ap2 `wifi1/wifi2 frame-types=admit-only-vlan-tagged` (TP3.1, 2026-07-06) bleibt gültig — verschwindet automatisch mit VLAN1; (b) ~~`st-main rrm/wnm` = reines Roaming-Provisorium~~ → **kein Provisorium mehr (2026-08-13, TP3.3): Steering ist fester Bestandteil des Golden-Config-Templates + ComplianceFeature `wifi-steering`** — es war die Neuprovisionierung, die es stillschweigend verloren hatte; (c) ~~**cudy1 = verbleibender aktiver VLAN1-Leak**~~ → **ERLEDIGT (User-bestätigt + verifiziert 2026-08-14): cudy1 ist offline.** `core ether11` trägt **kein Link** (kein `R`-Flag), Nautobot-Status *Offline* (TP22 Schritt 5). Damit existiert kein aktiver VLAN1-Leak mehr; der Dual-DHCP-Race aus `project_vlan1_dhcp_incident` ist Historie. Der formale Teardown (Gerät ausbauen, Doku/Inventar) bleibt TP19.
|
> 🎯 **Ziel: sauberes System „from scratch", keine Leichen/Altlasten.** Migrations-Session-Endstand = **VLAN1 + altes cudy-WLAN komplett weg** (VLAN1 nur noch Mgmt, `defconf`-Client-DHCP zurückgezogen, cudy-Uplinks aufs Ziel-Client-VLAN getaggt oder cudy abgebaut). **Unterpunkte/Aufräum-Liste bei Migration:** (a) die Interim-Härtung ap2 `wifi1/wifi2 frame-types=admit-only-vlan-tagged` (TP3.1, 2026-07-06) bleibt gültig — verschwindet automatisch mit VLAN1; (b) ~~`st-main rrm/wnm` = reines Roaming-Provisorium~~ → **kein Provisorium mehr (2026-08-13, TP3.3): Steering ist fester Bestandteil des Golden-Config-Templates + ComplianceFeature `wifi-steering`** — es war die Neuprovisionierung, die es stillschweigend verloren hatte; (c) ~~**cudy1 = verbleibender aktiver VLAN1-Leak**~~ → **ERLEDIGT (User-bestätigt + verifiziert 2026-08-14): cudy1 ist offline.** `core ether11` trägt **kein Link** (kein `R`-Flag), Nautobot-Status *Offline* (TP22 Schritt 5). Damit existiert kein aktiver VLAN1-Leak mehr; der Dual-DHCP-Race aus `project_vlan1_dhcp_incident` ist Historie. Der formale Teardown (Gerät ausbauen, Doku/Inventar) bleibt TP19.
|
||||||
> ⚠️ **HW-Constraint hAP ax²:** 1 GB RAM, aber nur **128 MB Flash + KEIN USB** → **Container auf dem Router NICHT praktikabel**.
|
> ⚠️ **HW-Constraint hAP ax²:** 1 GB RAM, aber nur **128 MB Flash + KEIN USB** → **Container auf dem Router NICHT praktikabel**.
|
||||||
@@ -513,43 +67,7 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na
|
|||||||
> ~~**TP17.2** ↔ TP17~~ *(TP17.2 ist am 2026-08-24 in **TP30** aufgegangen und kein eigenes TP mehr)*
|
> ~~**TP17.2** ↔ TP17~~ *(TP17.2 ist am 2026-08-24 in **TP30** aufgegangen und kein eigenes TP mehr)*
|
||||||
> und **TP20.1** ↔ TP20).
|
> und **TP20.1** ↔ TP20).
|
||||||
>
|
>
|
||||||
> ### 📊 Quick-Index (nur dieser Block ist für den Statusbaum nˆtig — 1 Read)
|
> 📊 **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.
|
||||||
>
|
|
||||||
> | # | Status | Kurztitel | Abschluss |
|
|
||||||
> |---|---|---|---|
|
|
||||||
> | TP5.1 | ⏸️ | DNS-Visibility Airfryer | — |
|
|
||||||
> | TP8 | ⏸️ | Identity / 802.1X | — |
|
|
||||||
> | TP11 | ⏸️ | Monitoring | — |
|
|
||||||
> | TP12 | ⏸️ | IPv6 (3 Standorte) | — |
|
|
||||||
> | TP12.1 | 🔜 | IPv6 ULA + v6-Firewall | — |
|
|
||||||
> | TP13 | 🔜 | Botschafts-WLANs | — |
|
|
||||||
> | TP14.4 | ✅ | CF-Access-Token — **in TP41 T4 aufgegangen** (`claude-mcp`→`zt-svc`, `.env` geschrieben) | 2026-09-01 |
|
|
||||||
> | TP16 | ✅ | CF-Tunnel HA wieder voll | 2026-08-24 |
|
|
||||||
> | TP16.2 | 🔜 | Härtung bypass-Dienste | — |
|
|
||||||
> | TP16.3 | ✅ | cloudflared-Reduktion — Flip + Abbau durch; nur noch `bf-public`+`fm`, 7 Container weg | 2026-09-01 |
|
|
||||||
> | TP17.1 | ❌ | Shelly Gen4 → Zigbee | 2026-08-23 |
|
|
||||||
> | TP17.3 | ✅ | Shelly-Regression behoben | 2026-08-23 |
|
|
||||||
> | TP18 | 🔜 | Garage-Objektspeicher | — |
|
|
||||||
> | TP20.1 | 🔜 | Vaultwarden → Platform-PG | — |
|
|
||||||
> | TP23 | ✅ | DNS-Policy (Stufe 1+2 ✅) | 2026-08-26 |
|
|
||||||
> | TP24 | 🔜 | DC-USV | — |
|
|
||||||
> | TP25 | ⏳ | WLAN-Stabilisierung (Kern ✅) — **Fund E ✅ geschlossen 09-02: 7.24.1 war die Ursache, nicht `ft-over-ds` (4/8,6 h, 0 Ausfälle)**; **`ft=no` auf `sec-main` als Dauerzustand ✅ 09-04**; **Konsole auf N Clients + Pin-UI ✅ 09-04**; offen nur ap2 unterperformt (2 m/−80, In-situ) | 2026-09-04 |
|
|
||||||
> | TP28 | ✅ | WLAN/CAPsMAN — dritter AP `ap3` (cAP ax) **live** | 2026-08-30 |
|
|
||||||
> | TP29 | 💡 | Anwesenheitserkennung (Idee) | — |
|
|
||||||
> | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — |
|
|
||||||
> | TP31 | ✅ | Reverse Proxy Caddy — 1–4 ✅ (08-25/26), **5+6 via TP16.3 (09-01)**; Abnahme-Tool `cf-sync.py` grün | 2026-09-01 |
|
|
||||||
> | TP32 | 🟢 | Nummern-Systematik (Konzept) | — |
|
|
||||||
> | TP33 | 💡 | Sonos fern (Idee, zurückgestellt) | — |
|
|
||||||
> | TP34 | 🔜 | Caddyfile via Nautobot/GC (Stub, Inventur-Handoff 2026-08-27) | — |
|
|
||||||
> | TP35 | ⏳ | MCP-Schichtung + 3-Schicht-CLAUDE.md + Backup-Guard (Schicht 1+3+6 ✅) | — |
|
|
||||||
> | TP36 | 🔜 | SSoT via Nautobot für teilprojekte.md-Live-Daten + Datei-Aufteilung | — |
|
|
||||||
> | TP37 | 🔜 | Nautobot als Netzwerkdoku: Hosts/Container/Dienste + CF-Connectoren (Okay 08-30) | — |
|
|
||||||
> | TP38 | 🔜 | OOB-Mgmt: untagged-VLAN99-Port je Switch | — |
|
|
||||||
> | TP40 | 🔜 | HA-Recorder-Retention: 41 GB `states` beschneiden (Okay 09-01) | — |
|
|
||||||
> | TP41 | ⏸️ | CF-Access-Reparatur **zurückgestellt 2026-09-01** (User): **T2/T3/T4/T5 ✅** (GitHub+Google-Login live); **Facebook abgesagt**; **Geli CF-seitig fertig** (zt-humans+geli-Policy) → offen nur noch WARP-Client auf Gelis Gerät, wartet auf User-Info | — |
|
|
||||||
> | TP42 | ✅ | Namens-Vereinheitlichung: ein interner Suffix **`bf.emsenhuber.net`** — DHCP-domain via GC geflippt (140/140), `.16` abgebaut (`.17`=FM bleibt); Nautobot-Doku-Teil an TP37 | 2026-09-02 |
|
|
||||||
> | TP43 | 🔜 | dw-Standort-DNS-Uplink: eigener Resolver mit site-Overrides (`420.teilzeitladen.xyz`→BF-Caddy/WG), WG-Disconnect bedacht; Geli sofort per WARP | — |
|
|
||||||
> | — | — | **35 archivierte TPs** in `teilprojekte-archiv.md` (inkl. TP39 ✅ 2026-09-01) | — |
|
|
||||||
|
|
||||||
### TP5.1 — DNS-Visibility & Flow-Observability für Low-Trust-Geräte (Airfryer-Fall) ▸ vorgemerkt
|
### TP5.1 — DNS-Visibility & Flow-Observability für Low-Trust-Geräte (Airfryer-Fall) ▸ vorgemerkt
|
||||||
- **Kontext (2026-07-08/09):** Airfryer „airschreier" (Philips/Versuni, ESP, `10.40.30.180`) bekam per DHCP-Option 6 `dns 1.1.1.1` als Override, weil das OTA-Update über den ap1-DoH-Gateway-Pfad nicht durchging (DoH-Latenz beim primitiven ESP-Stack — **kein** Content-Block, via Gateway-Logs bewiesen, s. `memory/ref_esp_ota_doh_not_block`). Nebenwirkung: public **1.1.1.1 = anonymer Consumer-Resolver ≠ accountgebundener Gateway-DoH** (`px35ynlol3.cloudflare-gateway.com`) → **DNS-Sicht auf das Gerät weg**, Gerät umgeht zudem die Filterung.
|
- **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.
|
||||||
@@ -1467,8 +985,11 @@ des Dimmers (= Lichtausfall in Diele UG/Wohnzimmer) wurde daraufhin **nicht** au
|
|||||||
> auf HAOS deployt; `shell_command` + 5 Helper + Timer + 2 Automations (`ha-mcp`). Button → Domain für
|
> auf HAOS deployt; `shell_command` + 5 Helper + Timer + 2 Automations (`ha-mcp`). Button → Domain für
|
||||||
> 15 min/1 h/4 h entsperren, nativer `timer` revertiert; RouterOS-`jit-dns-bypass`-Backstop erst mit Stufe 6.
|
> 15 min/1 h/4 h entsperren, nativer `timer` revertiert; RouterOS-`jit-dns-bypass`-Backstop erst mit Stufe 6.
|
||||||
> Doku: `adguard/README.md` + `adguard/ha-self-service.md`. End-to-End getestet (Start + Ablauf).
|
> Doku: `adguard/README.md` + `adguard/ha-self-service.md`. End-to-End getestet (Start + Ablauf).
|
||||||
> - **Offen:** nur **Stufe 6** (DNS-Bypass-Sperre) — bewusst NICHT geschaltet; braucht die scharfe
|
> - **Offen:** nur **Stufe 6** (DNS-Bypass-Sperre) — bewusst NICHT geschaltet. Runbook für eine
|
||||||
> Firewall-Regel (via Nautobot GC) + die abgenommene Self-Service-Oberfläche (jetzt vorhanden).
|
> **separate Session**: [`docs/tp23-stufe6-bypass-sperre-runbook.md`](tp23-stufe6-bypass-sperre-runbook.md).
|
||||||
|
> 🔴🔴 **HARTES VOR-ORT-GATE:** macht AGH `.36/.37` zum harten DNS-SPOF, aus der Ferne nicht
|
||||||
|
> reparierbar → **NUR vor Ort, NICHT während Christoph abwesend ist** (2026-09-06: ~1 Woche weg →
|
||||||
|
> in dem Fenster gar nicht anfangen). Methodik: log-only-Vorstufe zuerst, Drop mit Auto-Revert zuletzt.
|
||||||
> - **WARP-Managed-Network (Stufe 2 im alten Plan)** bleibt offen (TP41-nah). Der `.16`-basierte
|
> - **WARP-Managed-Network (Stufe 2 im alten Plan)** bleibt offen (TP41-nah). Der `.16`-basierte
|
||||||
> Abschnitt darunter ist die historische Fassung (2026-08-18), bewusst erhalten.
|
> Abschnitt darunter ist die historische Fassung (2026-08-18), bewusst erhalten.
|
||||||
|
|
||||||
@@ -2833,16 +2354,6 @@ kann in Ruhe nachziehen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TP34 — Caddyfile via Nautobot/GC (generische Reverse-Proxy-Schnittstelle) ▸ 🔜 STUB
|
|
||||||
> **Konzept + Architektur-Skizze:** [`docs/handoff-20260827-inventur-und-architektur.md`](handoff-20260827-inventur-und-architektur.md) — bitte dort weiterlesen (Architektur-Abschnitt).
|
|
||||||
- **Scope:** Reverse-Proxy-Hosts werden im **Nautobot-Datenmodell** gepflegt (Site, Host, Backend-Spec). **Golden Config** rendert den Caddyfile aus den Daten, Diff-Check gegen die Live-Version auf `caddy-bf1`, Compliance-Checks + Transaktionen identisch zum bestehenden Workflow für Router-Configs.
|
|
||||||
- **Auslöser (Inventur 2026-08-27):** der Caddyfile (`bf-infra/caddy/Caddyfile`) ist heute **manuell gepflegt** — kein Drift-Check, keine Transaktionen, andere Projekte können keine Sites anlegen ohne Christoph. Lösung: **Nautobot/GC**, kein Caddy-API-Sidecar (kein zweites SSoT).
|
|
||||||
- **Erster Anwendungsfall:** `terpene.teilzeitladen.xyz → http://10.40.20.167:8890` (Mac-mini-Backend, dort PostgREST + sync + web via lokalem Caddy `:8890`). Vor TP34: manueller Quick-Win-Caddy-Block.
|
|
||||||
- **Abh.:** TP22 (GC-Pipeline) ✅ vorhanden · TP23 (DNS-Policy + Caddy-Voraussetzungen) ✅ Stufe 1. Hängt an **nichts**, eigene Implementierung im Runbook nach dem Quick-Win.
|
|
||||||
- **Status:** 🔜 Stub; Konzept + Architektur-Skizze vollständig in Handoff-Datei. Implementierung in eigenem Runbook nach terpene-Caddy-Quick-Win.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### TP35 — MCP-Schichtung global/projekt/user + 3-Schicht-CLAUDE.md + Announcement-Mechanismus + Backup-Guard ▸ ⏳ **ANGELEGT 2026-08-28, Schicht 1 (FQDN→IP) + Schicht 3 (globale Regeln) + Schicht 6 (Skill) ✅**
|
### TP35 — MCP-Schichtung global/projekt/user + 3-Schicht-CLAUDE.md + Announcement-Mechanismus + Backup-Guard ▸ ⏳ **ANGELEGT 2026-08-28, Schicht 1 (FQDN→IP) + Schicht 3 (globale Regeln) + Schicht 6 (Skill) ✅**
|
||||||
- **Auslöser:** Bestandsaufnahme Session 2026-08-28 (Handoff `docs/handoff-20260828-pre-opencode-migration.md`). Erkenntnis: MCP-Configs sind heute über `~/.config/opencode/opencode.jsonc` UND `~/.claude.json` verstreut, ohne klares Schema.
|
- **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).
|
||||||
@@ -2860,17 +2371,17 @@ kann in Ruhe nachziehen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### TP36 — SSoT via Nautobot für `teilprojekte.md`-Live-Daten + Datei-Aufteilung ▸ 🔜 ANGELEGT 2026-08-28, Scope-Ausarbeitung eigene Session
|
### 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
|
||||||
- **Auslöser:** Skill-Run 2026-08-28 (`docs/handoff-20260828-session-a-projektstatus.md`) lieferte „Nächster Schritt (2026-08-26): TP31 Schritt 5" als Output, obwohl seit dem 26. drei Folge-Sessions parallel offen waren. **Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, sobald sie lang wird.**
|
- **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).
|
||||||
- **Befund:** Drei Stellen pro Edit berücksichtigen müssen — Quick-Index (Tabelle), TP-Abschnitt, Ist-Stand-Block. Keine schlägt die andere. Wer eine ändert, **muss** die anderen mitlesen. Die Datei hat aktuell 1940 Zeilen, der Ist-Stand-Block steht bei Z. 28, das Archiv-Cover bei Z. 1771. Strukturelles Risiko, kein Einzelfall.
|
- 🔄 **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.
|
||||||
- **Scope (Skizze, eigene Session):**
|
- **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**.
|
||||||
1. **Nautobot Custom Fields für Live-Daten** — RouterOS-Version, Compliance-Zahl (104/104, 26 Rules × 4 Geräte), DHCP-Server-Anzahl, MGMT-IPs, DNS-Upstream-Modus. Script `nautobot/gc-snapshot.py` rendert `docs/teilprojekte.md` (oder einen schmaleren Ersatz) als **read-only Snapshot**.
|
- ✅ **Erledigt (A1–A3):** Labels/Modell · 68 TPs → Issues · Board = Index (Label-Filter statt Handtabelle).
|
||||||
2. **Datei-Aufteilung** — `docs/teilprojekte-index.md` (1 Seite: nur aktive TPs, Titel + Status + Datum), `docs/teilprojekte-aktuell.md` (Live-Snapshot aus Nautobot), `docs/teilprojekte-archiv.md` (bleibt Volltext). `/projektstatus`-Skill liest Index + Archiv, nicht die Snapshot-Datei.
|
- ⏳ **Cutover 2026-09-07 (diese Session):**
|
||||||
3. **Archiv-Anzeige im Quick-Index** — die heute 34 Namen stehen heute in *einer* Zeile am Ende. Sichtbarkeit = „Geschichte zählt". Quick-Index-Block bekommt eine zweite Tabelle mit „abgeschlossen seit …" und Link aufs Archiv.
|
- **A4** — Quick-Index-Tabelle + „Ist-Stand (Kurz)"-Block aus dieser Datei entfernt; Header zeigt aufs Gitea-Board; die vier **Genehmigten Ausnahmen** unter eigenem Governance-Header behalten. Alle `### TPnn`-Prosa bleibt.
|
||||||
4. **Migrationspfad** — kompatibel mit bestehendem Workflow (`/projektstatus`-Skill konsumieren, niemand außer mir editiert Live-Markdown). Testphase = beide Dateien parallel führen.
|
- **A5** — `/projektstatus` liest Baum+Status aus der **Gitea-API** (Issues + `status:*`/`thema:*`), Transaktionen weiter aus `nautobot/gc-transaktion.py status`, Prosa-Detail bei Bedarf aus dem Repo. Beide Skill-Kopien (`~/.claude/skills/…` **und** `~/.config/opencode/skills/…`, `cp`, kein Symlink).
|
||||||
- **Abgrenzung:** TP36 ist Querschnitt, nicht Kern-BF. Eigene Session, eigener Handoff. Nicht heute.
|
- **A6** — Projekt-`CLAUDE.md`: Pflichtlektüre = Gitea-Board (Status) + Prosa-Datei (Detail); Branch-Konvention (`tpNN-kurz` jederzeit, Merge nach `main` wenn Code/Config steht, Steuerungs-Updates nach Gitea); SSoT-Notiz.
|
||||||
- **Verzahnt mit:** TP22 (Nautobot-GC als Vorbild), TP35 (geht Hand in Hand — beide sind „SSoT + Konsolidierung"), `bf-infra/CLAUDE.md`-Lessons.
|
- 🔜 **Optionaler Rest (entkoppelt, „TP36-Live"):** Live-Daten-CustomFields auf den **4 `dcim.Device`** (`ros_version`, `compliance_pct`, `dhcp_server_count`, `mgmt_ip`, `dns_upstream_mode`) + Snapshot-Skript `nautobot/gc-snapshot.py` (existiert noch nicht). Unabhängig von diesem Cutover; nur relevant, wenn die Doku Live-Zahlen tragen soll. ⚠️ **Namensfalle bereinigt:** `nautobot/tp36-cf-estate-seed.py` gehörte **nie** zu TP36 → seit 2026-09-06 `tp44-cf-estate-seed.py`.
|
||||||
- **Referenzen:** Lessons-Block-Eintrag „Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität" · Handoff Session A 2026-08-28.
|
- **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 — Out-of-Band-Management: ein untagged-VLAN99-Port je Switch ▸ 🔜 **ANGELEGT 2026-08-31**
|
||||||
@@ -2990,157 +2501,6 @@ 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.
|
||||||
|
|
||||||
### TP41 — CF-Access-Reparatur + Zero-Trust-Konsolidierung ▸ ⏸️ **ZURÜCKGESTELLT 2026-09-01 (User-Entscheid)** — Auth-Kern durch; offen nur Gelis WARP-Client (nicht aus der Ferne machbar), User gibt ggf. später Info
|
|
||||||
|
|
||||||
**Auslöser:** CF-Access zwang Christoph **und Geli** täglich zum OTP-Login (soll: 30 Tage); GitHub/Google
|
|
||||||
„funktionieren nicht". Root-Causes: Team-Domain-Rename `bf4`→`emsenhuber` (Provider-Callback veraltet),
|
|
||||||
App-Session 24h, diverse Ordnungs-/Sicherheitslecks. Vollständiger Verlauf + offene Schritte:
|
|
||||||
**[`docs/handoff-20260901-tp41.md`](handoff-20260901-tp41.md)**. Branch `tp41-cf-access-repair`.
|
|
||||||
|
|
||||||
**✅ Live erledigt:** Session 720h (Wildcard/vw/Warp-Login) · login_method-Loch zu (reusable Policy
|
|
||||||
`bf-users` = nur `email:christoph`+`email:geli`) · `8e44f5d1` von 4 Apps auf **nur Terpenfinder**
|
|
||||||
entkoppelt · Access-**Gruppen** `zt-humans`/`zt-home`/`zt-automation`, Policies zeigen darauf · **vw voll
|
|
||||||
Bypass** (Lebensader offen) + **`vw/admin` per WAF-Regel öffentlich geblockt** (CF-Edge + Mobilfunk
|
|
||||||
verifiziert) · API-Token auf das vollständige `…4148` konsolidiert (User hat Service-Tokens- + Zero-Trust-
|
|
||||||
Write ergänzt), altes `…2174` revoken.
|
|
||||||
|
|
||||||
**✅ T4 DURCH (2026-09-01, Variante B):** ein Access-Service-Token **`zt-svc`** (`65f1821a`, rotiert) über
|
|
||||||
Gruppe `zt-automation`; Terpene-Worker per `wrangler secret put TERPENE_SVC_CLIENT_ID` auf zt-svc
|
|
||||||
umgestellt (Worker-Pin `index.js:44`); `terpene-verify`+`-v2` **gelöscht**; `test_whoami` grün.
|
|
||||||
**Token-Altlast geklärt:** `b00722` = LIVE Caddy-DNS-Token (LXC130 caddy.env, SSoT), `d47435` = stale
|
|
||||||
Kopie (kein Consumer) → **User revoket `d47435`**; `credentials.env`-Kopie auf b00722 realignt.
|
|
||||||
|
|
||||||
**✅ T3 APPLIED (2026-09-01):** Default-Profil (`0197d1b3`; das custom „Cloudflare Mesh" matcht nur den
|
|
||||||
`warp_connector`) `fallback_domains += emsenhuber.net → [10.40.20.36, 10.40.20.37]`; Tunnel-Route
|
|
||||||
`10.40.0.0/16` (`d1da7c8a`) da, kein `10/8`-Exclude → routbar. ⏳ Client-Verify: WARP neu verbinden.
|
|
||||||
**✅ T2 DURCH (2026-09-01):** WAF-Regel `f26744eb` gelöscht; Caddy `vw`-Block `respond @cf_admin 404` bei
|
|
||||||
Header `CF-Connecting-IP` (LXC130). Verify: Edge `/admin`=404, lokal/WARP `/admin`=200. ✅
|
|
||||||
|
|
||||||
**✅ T5 DURCH (2026-09-01, User-bestätigt):** GitHub-Login läuft; Google-Login läuft nach dem
|
|
||||||
Consent-Screen-Fix (§A des Follow-up-Handoffs: User Type *Internal*→*External*/veröffentlicht).
|
|
||||||
Callback für beide `https://emsenhuber.cloudflareaccess.com/cdn-cgi/access/callback`.
|
|
||||||
**❌ Facebook/Meta für TP41 abgesagt (2026-09-01, User-Entscheid):** optional, kein Bedarf — Geli ist
|
|
||||||
über OTP `angelikahy@outlook.de` + Google `pillepalle670@gmail.com` voll gedeckt. **Keine Facebook-App,
|
|
||||||
kein CF-Facebook-IdP angelegt → nichts aufzuräumen.** Instagram hat ohnehin keinen CF-IdP.
|
|
||||||
**✅ App-Launcher (D1):** je eine Bookmark-Kachel vw/hass/terpenfinder, Wildcard-Kachel raus, Launcher
|
|
||||||
gestylt (Follow-up-Handoff §D, Policies unverändert).
|
|
||||||
|
|
||||||
**✅ Geli in WARP — CF-seitig nichts zu tun (2026-09-01 live geprüft):** Device-Enrollment läuft über
|
|
||||||
die App **„Warp Login App"** (`6c32fe23…`, type `warp`) → Policy `bf-users` → Gruppe **`zt-humans`**, die
|
|
||||||
Gelis beide Mails **schon enthält** → sie **darf bereits enrollen** (deshalb war „Device enrollment
|
|
||||||
permissions" nicht als fehlendes Setting zu finden). Terpenfinder-`geli`-Policy lässt sie per E-Mail durch.
|
|
||||||
⇒ verbleibt **nur ein Handgriff auf Gelis Gerät** (WARP installieren, Team `emsenhuber`, Login OTP
|
|
||||||
`angelikahy@outlook.de` / Google `pillepalle670@gmail.com`) — nicht aus der Ferne machbar.
|
|
||||||
🔑 **Permission-Frage beantwortet:** CF-seitig **keine** nötig. Für generelle WARP/Enrollment/User-Verwaltung
|
|
||||||
per Account-Token: *Access: Apps and Policies → Edit* + *Access: Organizations, IdPs, and Groups → Edit*
|
|
||||||
(User erteilt 2026-09-01). **Seat-Verwaltung (`/access/seats`) bleibt per MCP gesperrt** — `10405 Method
|
|
||||||
not allowed for this authentication scheme` (Endpoint will User-Token, nicht Account-Token; kein Scope-Fix,
|
|
||||||
für Enroll irrelevant).
|
|
||||||
|
|
||||||
**🔜 Offen — nur noch außerhalb CF:** **Gelis WARP-Client** auf ihrem Gerät (s.o.) · **T6** optional
|
|
||||||
Rate-Limit · 🔴 `TERPENE_ANDROID_KEYSTORE_PW`-Rotation erwägen (TP30 Posten #7, Transkript-Leak).
|
|
||||||
**d47435-Revoke ✅ · T3-WARP-Client-Verify ✅** (User, 2026-09-01).
|
|
||||||
|
|
||||||
**🔻 Optionaler Security-Cleanup (eigener Punkt, nicht in dieser Runde):** die `geli`-Terpenfinder-Policy
|
|
||||||
hat **OR-Semantik** und enthält den breiten CGNAT-Bereich `94.154.138.0/24` als IP-Include → jeder aus
|
|
||||||
dieser geteilten Range käme **ohne Login** rein. Über WARP ist der Egress ohnehin Cloudflare (nicht diese
|
|
||||||
IP) → der IP-Include wäre dann überflüssig und entfernbar.
|
|
||||||
|
|
||||||
**Abgrenzung:** hass/gitea bleiben (diese Runde) öffentlich; Terpenfinder-Human-Policies nicht angefasst;
|
|
||||||
**Gitea-Least-Privilege** = eigener künftiger TP (jedes Projekt repo-scoped Login, Admin nur Claude/Christoph).
|
|
||||||
- Bezug **TP11** (Monitoring): eine Plattenfüllstand-Warnung hätte das Wochen früher gezeigt.
|
|
||||||
|
|
||||||
### TP42 — Namens-Vereinheitlichung bf.lan → emsenhuber.net ▸ 🔜 ANGELEGT 2026-09-01 (User-Okay erteilt)
|
|
||||||
|
|
||||||
**Auslöser (2026-09-01, TP41-Randbefund):** live gemessen liefern die realen Client-Resolver
|
|
||||||
`10.40.20.36`/`.37` `*.emsenhuber.net → 10.40.20.40` (Caddy) — korrekt. Daneben existiert aber ein
|
|
||||||
zweiter, gedrifteter interner Namensraum: der **Core** trägt in `/ip dns static` 11 Einträge auf
|
|
||||||
`*.bf.lan`, der **Caddyfile** doppelt jeden internen Namen als `*.bf.emsenhuber.net`, und die alten
|
|
||||||
AdGuard `.16`/`.17` antworten noch mit `nginx*.bf.emsenhuber.net` bzw. totem VLAN1 `10.40.41.123`.
|
|
||||||
Christoph: „aus emsenhuber.net wurde schon wieder bf.lan — das müssen wir ein für alle mal aus allen
|
|
||||||
Geräten rausprügeln und in Nautobot festhalten."
|
|
||||||
|
|
||||||
**Ziel:** genau **ein** interner Namensraum `emsenhuber.net` (Split-Horizon `.36/.37 → .40`). `.bf.lan`
|
|
||||||
und `.bf.emsenhuber.net` werden entfernt; Sollzustand in Nautobot dokumentiert.
|
|
||||||
|
|
||||||
**Scope (Reihenfolge = eigene Session):**
|
|
||||||
1. **Bestandsaufnahme** aller Fundstellen: Core `/ip dns static` (`*.bf.lan`), DHCP-Option
|
|
||||||
`domain`/Search-Domain je Netz, Caddyfile-Aliase (`*.bf.emsenhuber.net`), AdGuard-Rewrites `.36/.37`,
|
|
||||||
stale `.16/.17`, Geräte-`identity`.
|
|
||||||
2. **Entscheidung Zielschema** (nur `emsenhuber.net`, ggf. `bf.emsenhuber.net` als *einzige* interne
|
|
||||||
Zone — Christoph-Entscheid) und Migrationsreihenfolge ohne DNS-Loch.
|
|
||||||
3. **Umsetzen:** Core-DNS + DHCP + Caddyfile angleichen; stale `.16/.17` klären/abbauen.
|
|
||||||
4. **Nautobot:** interne Namen/Records als Sollzustand erfassen (Bezug TP37 Datenmodell).
|
|
||||||
|
|
||||||
**Abgrenzung:** kein CF-Public-DNS-Umbau (Zone `emsenhuber.net` öffentlich bleibt); kein Resolver-Swap
|
|
||||||
(`.36/.37` bleiben); `.1`-als-DNS-Reste auf Clients sind separat/tolerierbar (User: „bis jetzt kein
|
|
||||||
Problem"). **Abh.:** TP37 (Nautobot-Datenmodell) für Punkt 4; sonst nichts Offenes.
|
|
||||||
|
|
||||||
#### ✅ Bestandsaufnahme LIVE (2026-09-02) — Drift hat 80 % schon erledigt
|
|
||||||
Gegen die Geräte gemessen (MikroMCP + dig), nicht aus dem Handoff:
|
|
||||||
|
|
||||||
| Fundstelle | Handoff 09-01 | Live 09-02 |
|
|
||||||
|---|---|---|
|
|
||||||
| Core `/ip dns static` | 11× `*.bf.lan` | ✅ **0× `.bf.lan`** — 10× `*.bf.emsenhuber.net` (deckt sich mit GC-Template Z.284–293) |
|
|
||||||
| DHCP `domain` (7 Netze) | „prüfen" | `emsenhuber.net` (GC-Template Z.133 = gleich ⇒ **keine Drift**), DNS `.36/.37` |
|
|
||||||
| `.36/.37` (Soll) | aktiv | `vw.emsenhuber.net→.40` ✅ · `macmini.bf.emsenhuber.net→.20.167` ✅ · **`macmini.emsenhuber.net→leer`** ❌ |
|
|
||||||
| Caddyfile | `*.bf.emsenhuber.net`-Aliase | unverändert, **konsistent** mit Core-Static |
|
|
||||||
| `.16`/`.17` | tot | laufen (:53), liefern totes VLAN1; **kein Core-Objekt** (Firewall/NAT/DNS-Upstream/Addr-List) hängt an `.16` |
|
|
||||||
|
|
||||||
**Kernkonflikt:** DHCP verteilt `emsenhuber.net` (bare), Records leben unter `bf.emsenhuber.net` ⇒ bare `macmini` löst nicht.
|
|
||||||
|
|
||||||
#### ✅ Entscheidungen (User, 2026-09-02)
|
|
||||||
1. **Zielschema = `bf.emsenhuber.net`** (der wenig-Arbeit-Weg): DHCP-domain → `bf.emsenhuber.net`, dann
|
|
||||||
löst `macmini` bare korrekt; Core-Static + Caddyfile bleiben (tragen schon `.bf.`). Öffentliche Zone
|
|
||||||
`emsenhuber.net` bleibt getrennt.
|
|
||||||
2. **`.16` (adguard_bf1) abbauen**, **`.17` (adguard_fm2 = Standort FM) NICHT anfassen.**
|
|
||||||
|
|
||||||
#### ✅ Umsetzung DURCH (2026-09-02)
|
|
||||||
- **(A) DHCP-domain `emsenhuber.net → bf.emsenhuber.net` über GC ✅** — kein Direkt-Write:
|
|
||||||
`mikrotik_routeros.j2` Z.133 gehoben → `push-gc-template.py` (Commit `7a4068c4`, **Nebenbefund:** das
|
|
||||||
Remote-Template hing dem lokalen hinterher — es trug noch `*.bf.lan`+`router.lan`, obwohl Gerät+lokal
|
|
||||||
längst `.bf.emsenhuber.net` fuhren; der Push hat das GC-Repo an den Live-Stand angeglichen) →
|
|
||||||
GitRepo-Sync (SUCCESS) → manueller ConfigPlan **`tp42-dhcp-domain`** (`set [find] domain=…`, weil
|
|
||||||
`add` das Netz doppelt anlegen würde) → Deploy an **crs326-core** → Live **7/7 = bf.emsenhuber.net**,
|
|
||||||
Compliance **140/140, keine Drifts**. Der Job steht dauerhaft in `nautobot/gc-configplan.py`.
|
|
||||||
- **(B) `.16`-Teardown ✅** — `adguard_bf1` lag im geteilten Portainer-Stack **195 `adguard`** (Docker-LXC
|
|
||||||
`.126`, **ep2** — *nicht* ep14) **zusammen mit `adguard_fm2` (`.17`, Standort FM!)**. Deshalb **nicht**
|
|
||||||
Stack löschen, sondern compose auf nur `.17` getrimmt + Stack redeployt (Prune): `.16`-Container weg,
|
|
||||||
`.16:53` tot; **`.17` läuft** (⚠️ ~40 s mit-neugestartet durch den Redeploy — kurzer FM-DNS-Blip,
|
|
||||||
danach sauber). Rollback-compose gesichert. 🧹 Daten-Dirs `/opt/docker/adguard_bf1/*` bleiben als
|
|
||||||
harmlose Leichen auf dem LXC.
|
|
||||||
- **(C) Nautobot-Soll** (Scope-Punkt 4) bleibt an **TP37** gekoppelt (Datenmodell), separat — kein Blocker.
|
|
||||||
|
|
||||||
**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.
|
|
||||||
Handoff: [`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md).
|
|
||||||
|
|
||||||
### TP43 — dw-Standort-DNS-Uplink (site-spezifische Overrides) ▸ 🔜 ANGELEGT 2026-09-01 (User-Okay erteilt)
|
|
||||||
|
|
||||||
**Auslöser (2026-09-01, TP41-Follow-up):** Geli soll `420.teilzeitladen.xyz` am dw-Standort erreichen.
|
|
||||||
Split-Horizon am dw scheitert an der **FritzBox** (`10.95.10.1`): kein Per-Host-A-Override im Standard
|
|
||||||
(nur DNS-Rebind-Ausnahmen ohne IP). Christoph: *„keine Chance im FritzBox-Standard? … lieber ein TP als
|
|
||||||
uns verfranzen. Geli kommt rein, das zählt."*
|
|
||||||
|
|
||||||
**Ziel:** dem dw-Standort einen **eigenen Resolver/DNS-Uplink** geben, der **site-spezifische Overrides**
|
|
||||||
hält (erster Fall `420.teilzeitladen.xyz` → BF-Caddy über WG), als DNS der FritzBox/Clients gesetzt.
|
|
||||||
|
|
||||||
**Kernfrage (explizit):** Verhalten bei **WG-Disconnect.** Ein Override auf eine BF-interne IP
|
|
||||||
(`10.40.20.40`) fällt aus, wenn der Tunnel unten ist. Für einen Login-gegateten Dienst gibt es kein
|
|
||||||
„kein Login **und** funktioniert bei WG-down" — Optionen: (a) Override nur bei WG-up (Failover-Resolver),
|
|
||||||
(b) öffentlicher Pfad (CF, mit Login) als Fallback. Zu entwerfen.
|
|
||||||
|
|
||||||
**Sofortlösung (nicht Teil von TP43):** Geli per **WARP** (Session statt täglichem Login, CGNAT egal,
|
|
||||||
degradiert sauber: WG-up = nahtlos, WG-down = öffentliche URL + Login).
|
|
||||||
|
|
||||||
**BF-seitige Prereqs:** Caddy-vhost `420.teilzeitladen.xyz` → macmini-Worker (`10.40.20.167:8890`) mit
|
|
||||||
Cert (DNS-01, `…4148` hat teilzeitladen.xyz-DNS) · WG-Route dw→`10.40.20.40` · Split-Horizon für
|
|
||||||
`420.teilzeitladen.xyz` (heute liefern auch `.36/.37` public). **Vorher verifizieren:** FritzOS-Version +
|
|
||||||
Dauerläufer am dw. Cross-Projekt (teilzeitladen.xyz). Handoff:
|
|
||||||
[`docs/handoff-20260901-tp41-followup.md`](handoff-20260901-tp41-followup.md).
|
|
||||||
|
|
||||||
**Status:** 🔜 angelegt 2026-09-01, eigene Session.
|
|
||||||
|
|
||||||
### TP37 — Nautobot als durchgehende Netzwerkdoku: Hosts/Container/Dienste, erster Anwendungsfall CF-Connectoren ▸ 🔜 ANGELEGT 2026-08-30 (User-Okay erteilt)
|
### TP37 — Nautobot als durchgehende Netzwerkdoku: Hosts/Container/Dienste, erster Anwendungsfall CF-Connectoren ▸ 🔜 ANGELEGT 2026-08-30 (User-Okay erteilt)
|
||||||
> **Christophs Formulierung (2026-08-30):** *„können wir das setup mit den cf-connectoren dann auch in
|
> **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
|
||||||
@@ -3165,10 +2525,10 @@ Dauerläufer am dw. Cross-Projekt (teilzeitladen.xyz). Handoff:
|
|||||||
„wo läuft was" steht.
|
„wo läuft was" steht.
|
||||||
2. **Dienste abbilden** — `ipam.Service` je exponiertem Port (Caddy `:80/:443`, AdGuard `:53`,
|
2. **Dienste abbilden** — `ipam.Service` je exponiertem Port (Caddy `:80/:443`, AdGuard `:53`,
|
||||||
Nautobot `:8080`, n8n `:5678`, …). Das ist Nautobot-Kernmodell, **keine eigene App nötig**.
|
Nautobot `:8080`, n8n `:5678`, …). Das ist Nautobot-Kernmodell, **keine eigene App nötig**.
|
||||||
3. **CF-Connectoren als erster Anwendungsfall** — Tunnel (`bf-public`, historisch `bf-mm`/`bf-nas`/`bf-pve`)
|
3. ✅ **CF-Connectoren — ERLEDIGT via [TP44](teilprojekte-archiv.md#tp44) (Etappe 1, 2026-09-02).** Die Modellwahl ist damit
|
||||||
als Objekt, Connector als Relation Tunnel↔Host, Ingress-Regel als Hostname→Backend-Paar.
|
entschieden: **native `nautobot.cloud`-Modelle** (Tunnel/WARP = `CloudNetwork`, VPC/Hyperdrive/Worker/
|
||||||
Modellwahl offen: Custom Fields + Relationships auf Bestandsmodellen **oder** eine kleine eigene App.
|
Access-App = `CloudService`, IDs/Ports/Origins in `extra_config`), **keine eigene App**. Offen nur noch
|
||||||
🔴 **Erst Punkt 1–2 entscheiden, dann diese Frage** — umgekehrt entsteht ein Modell für einen Sonderfall.
|
TP44 Etappe 2 (CF-API→Nautobot-Sync). ⇒ **Punkt 1+2 unten sind der echte Rest von TP37.**
|
||||||
4. **Drift-Check** — Skript liest CF-API (`cfd_tunnel` + `configurations`) und vergleicht gegen Nautobot,
|
4. **Drift-Check** — Skript liest CF-API (`cfd_tunnel` + `configurations`) und vergleicht gegen Nautobot,
|
||||||
analog `nautobot/gc-configplan.py --refresh-compliance`. **Read-only zuerst**; ob Nautobot je nach
|
analog `nautobot/gc-configplan.py --refresh-compliance`. **Read-only zuerst**; ob Nautobot je nach
|
||||||
Cloudflare *schreibt*, ist eine spätere, eigene Entscheidung.
|
Cloudflare *schreibt*, ist eine spätere, eigene Entscheidung.
|
||||||
@@ -3186,7 +2546,11 @@ Dauerläufer am dw. Cross-Projekt (teilzeitladen.xyz). Handoff:
|
|||||||
die Pipeline ist stabil und **104/104 compliant**. TP37 vergrößert die Angriffsfläche dieser Pipeline.
|
die Pipeline ist stabil und **104/104 compliant**. TP37 vergrößert die Angriffsfläche dieser Pipeline.
|
||||||
Deshalb Punkt 1–2 als eigener, abnehmbarer Schritt **mit Compliance-Gegenprobe danach**, nicht als
|
Deshalb Punkt 1–2 als eigener, abnehmbarer Schritt **mit Compliance-Gegenprobe danach**, nicht als
|
||||||
Nebeneffekt von Punkt 3.
|
Nebeneffekt von Punkt 3.
|
||||||
- **Status:** 🔜 angelegt 2026-08-30, Scope-Ausarbeitung in eigener Session. Kein Runbook.
|
- **Status:** ⏳ **Punkt 3 (CF) ✅ via TP44; Punkt 1+2 (Substrat + Dienste) offen** — das ist die
|
||||||
|
fehlende Voraussetzung für TP34. Datenmodell **entschieden 2026-09-06 (native Modelle:
|
||||||
|
`virtualization.Cluster/VirtualMachine` + `ipam.Service`, keine App)**. Runbook liegt jetzt vor:
|
||||||
|
[`handoff-20260906-tp37-34-36-plan.md`](handoff-20260906-tp37-34-36-plan.md). Live-Ausgangslage:
|
||||||
|
VM 0 · Cluster 0 · `ipam.Service` 0 · CustomField 0 (2026-09-06 gegen die API gelesen).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -3206,7 +2570,7 @@ Kurzorientierung, wo was zu finden ist:
|
|||||||
| **Firewall / QoS** | [TP5](teilprojekte-archiv.md#tp5) deny-by-default · [TP26](teilprojekte-archiv.md#tp26) `ip-firewall-filter` unter Drift-Kontrolle · [TP6](teilprojekte-archiv.md#tp6) CAKE *(geschlossen statt gebaut)* · [TP6.1](teilprojekte-archiv.md#tp6-1) Upload-Priorisierung |
|
| **Firewall / QoS** | [TP5](teilprojekte-archiv.md#tp5) deny-by-default · [TP26](teilprojekte-archiv.md#tp26) `ip-firewall-filter` unter Drift-Kontrolle · [TP6](teilprojekte-archiv.md#tp6) CAKE *(geschlossen statt gebaut)* · [TP6.1](teilprojekte-archiv.md#tp6-1) Upload-Priorisierung |
|
||||||
| **Discovery / Media** | [TP7](teilprojekte-archiv.md#tp7) mDNS · [TP7.1](teilprojekte-archiv.md#tp7-1) SSDP-Relay · [TP7.2](teilprojekte-archiv.md#tp7-2) HA↔Sonos |
|
| **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 |
|
| **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) Multi-Site-VPN *(offen nur der DDNS-Updater, s. Follow-ups)* · [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) |
|
||||||
|
|
||||||
@@ -3361,3 +2725,78 @@ TP22 · TP-R · **TP3.2** (08-17) · **TP5** (08-18) · **TP7.1** (08-18) · **T
|
|||||||
7. **Foundation (2026-07-14):** **TP15** (MikroTik-Switching / Omada-WLAN, 2× CRS326 bestellt) ∥ **TP20 (Platform-Postgres, single-site replication-ready)** → **TP21 (Nautobot Golden Config)** ∥ **TP20.1 (Vaultwarden→PG + 2-Site-HA)**. TP20 hängt an **TP18 ①** (Garage-Bucket für pgBackRest).
|
7. **Foundation (2026-07-14):** **TP15** (MikroTik-Switching / Omada-WLAN, 2× CRS326 bestellt) ∥ **TP20 (Platform-Postgres, single-site replication-ready)** → **TP21 (Nautobot Golden Config)** ∥ **TP20.1 (Vaultwarden→PG + 2-Site-HA)**. TP20 hängt an **TP18 ①** (Garage-Bucket für pgBackRest).
|
||||||
|
|
||||||
</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)
|
||||||
|
|
||||||
|
**Live-Auslöser (2026-09-11):** Christoph am dw-Standort, WG-Tunnel zu BF up, kann `portainer.bf.emsenhuber.net`
|
||||||
|
nicht erreichen. Direkte IP `https://10.40.20.126:9443` geht ⇒ **reines DNS-, kein Routing-Problem.** Der
|
||||||
|
dw-Client nutzt die FritzBox (`10.95.10.1`) als Resolver; die kennt die interne Zone `bf.emsenhuber.net`
|
||||||
|
(Split-Horizon `.36/.37 → 10.40.20.40` Caddy) nicht.
|
||||||
|
|
||||||
|
**Korrektur gegenüber dem verworfenen Scope:** Der ursprüngliche Fall (`420.teilzeitladen.xyz`) ist ein
|
||||||
|
**öffentlicher** CF-Worker und war nie das Problem — der ist via TP41/WARP + CF Access gelöst. Das echte,
|
||||||
|
offene Problem sind **interne** BF-Namen (`*.bf.emsenhuber.net`) über den WG-Tunnel.
|
||||||
|
|
||||||
|
**Live verifiziert (2026-09-11, `dig @10.40.20.36` über den Tunnel):** `portainer.bf.emsenhuber.net
|
||||||
|
→ 10.40.20.40` (Caddy), `macmini.bf.emsenhuber.net → 10.40.20.167` — alle internen bf-Namen landen
|
||||||
|
in `10.40.0.0/16`, über WG erreichbar. `ds-fm`/`gateway-fm.emsenhuber.net → 188.114.96.10/.97.10`
|
||||||
|
= **Cloudflare-Anycast** (public), **nie `10.101.0.x`**. ⇒ **Der fm-Einwand (10.101.x intern ohne
|
||||||
|
Pfad von dw) trifft für `.36/.37` nicht zu** — fm läuft über den public `fm.emsenhuber.net`-Tunnel,
|
||||||
|
nicht über interne Records. (WG-Topologie TP10: dw-Peer AllowedIPs BF-seitig = `10.40.0.0/16`, dw
|
||||||
|
routet **nicht** nach `10.101.0.0/24`.)
|
||||||
|
|
||||||
|
**Constraint am dw:** die 24/7-Geräte `10.95.10.246/.247` sind **Cudy-APs (kein DNS-Host)**; die
|
||||||
|
FritzBox kann **kein Per-Zone-Forwarding** (nur einen globalen Upstream) **und per DHCP nur EINE
|
||||||
|
DNS-IP ausliefern** (⇒ „mehrere NS an die Clients" scheidet aus). Ein echter conditional forwarder
|
||||||
|
(dnsmasq `server=/bf.emsenhuber.net/.36`) bräuchte einen Zusatz-Dauerläufer — abgelehnt.
|
||||||
|
|
||||||
|
**Entscheidung (Christoph, 2026-09-11): FB-Upstream + Rebind-Ausnahme.** Clients bekommen per DHCP
|
||||||
|
`10.95.10.1` (die FB selbst, Default); die **FB forwardet an die BF-Resolver**: *DNS-Server → „Andere
|
||||||
|
DNSv4-Server"* Bevorzugt **`10.40.20.36`**, Alternativ **`10.40.20.37`** (beide BF, beide
|
||||||
|
Voll-Resolver → Redundanz bleibt). Sauberer als der zwischenzeitliche DHCP-Multi-Gedanke: die FB ist
|
||||||
|
der **einzige** Client-Resolver und `.36` löst **intern und public** auf → alles geht zuerst an `.36`,
|
||||||
|
public wird nur als Unreachable-Fallback konsultiert, **nie „zuerst"** → kein NXDOMAIN-Leak im
|
||||||
|
Normalbetrieb.
|
||||||
|
|
||||||
|
🔴 **DNS-Rebind-Schutz-Ausnahme `bf.emsenhuber.net`** (*Heimnetz → Netzwerk → Netzwerkeinstellungen →
|
||||||
|
DNS-Rebind-Schutz*) ist **zwingend**: sonst filtert die FB die Privat-IP-Antworten (`10.40.x`) als
|
||||||
|
Rebind-Angriff weg → bf-Namen liefern nichts, obwohl `.36` korrekt antwortet. Die Ausnahme ist ein
|
||||||
|
reines Namens-Whitelist (keine IP).
|
||||||
|
|
||||||
|
**BF-down-Fallback (Verzweigung am FRITZ!OS-Stand):** Christoph erinnert ein Setting „öffentliche NS
|
||||||
|
verwenden, wenn die primären nicht erreichbar sind". Vorhanden → aktivieren, Upstream bleibt
|
||||||
|
`.36`+`.37`, BF-down geht auf public. Fehlt → Alternativ-Slot statt `.37` auf **public `9.9.9.9`**
|
||||||
|
(kostet `.37`-Redundanz); dann zusätzlich prüfen, dass die FB **strikt primär** fragt (nicht
|
||||||
|
round-robbt), sonst NXDOMAIN-Leak für `bf.*` bei BF-up.
|
||||||
|
|
||||||
|
- **BF-up:** FB → `.36` → interne `bf.*` (→ `.40`, dank Rebind-Ausnahme durchgereicht) **und** public.
|
||||||
|
- **BF-down:** `.36`/`.37` unreachable → public-Fallback → public lebt, `bf.*` = NXDOMAIN (erwartet).
|
||||||
|
|
||||||
|
**Bewusst akzeptierter Tradeoff:** bei BF-up läuft Geli's **gesamter** Haushalt-DNS über Linz
|
||||||
|
(`.36`/AdGuard-Filter) — vermeidbar nur mit conditional forwarder (Zusatz-HW, abgelehnt).
|
||||||
|
|
||||||
|
**On-site-Verifikationen (Christoph, am dw):** (1) Rebind-Ausnahme greift — `nslookup
|
||||||
|
portainer.bf.emsenhuber.net` über die FB → stabil `10.40.20.40`; (2) BF-down-Fallback vorhanden
|
||||||
|
(Setting) oder via public-Alternativ-Slot gesetzt; (3) nur falls BF+public gemischt: FB fragt strikt
|
||||||
|
primär. Rückfall pro Gerät: `/etc/hosts` `10.40.20.40 <name>` oder WARP `fallback_domains
|
||||||
|
bf.emsenhuber.net → [.36,.37]` (TP41-Präzedenz).
|
||||||
|
|
||||||
|
**Sofort-Workaround (läuft unabhängig):** `https://10.40.20.126:9443` direkt bzw.
|
||||||
|
`/etc/hosts`-Zeile `10.40.20.40 portainer.bf.emsenhuber.net`.
|
||||||
|
|
||||||
|
**Abhängigkeiten:** WG-Route dw→`10.40.0.0/16` (steht). Grenzt an TP42 (interner Namensraum
|
||||||
|
`bf.emsenhuber.net`) und TP10 (Multi-Site-VPN). **Umsetzung = eigene On-site-Session** (FritzBox-DHCP
|
||||||
|
ist Christophs manueller Schritt, kein Tool-Zugriff).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Historie — verworfener 420-Scope (angelegt 2026-09-01, verworfen 2026-09-11):**
|
||||||
|
|
||||||
|
**Auslöser (2026-09-01, TP41-Follow-up):** Geli soll `420.teilzeitladen.xyz` am dw-Standort erreichen.
|
||||||
|
Split-Horizon am dw scheitert an der **FritzBox** (`10.95.10.1`): kein Per-Host-A-Override im Standard
|
||||||
|
(nur DNS-Rebind-Ausnahmen ohne IP). Christoph: *„keine Chance im FritzBox-Standard? … lieber ein TP als
|
||||||
|
uns verfranzen. Geli kommt rein, das zählt."* — Diese Prämisse fiel weg: `420.teilzeitladen.xyz` ist ein
|
||||||
|
public CF-Worker hinter CF Access (löst auf CF-Anycast), kein BF-Origin ⇒ dafür braucht es kein
|
||||||
|
Split-Horizon; Zugang via TP41-**WARP** gelöst (Geli 2026-09-10 via Android). Der Override-Bedarf für
|
||||||
|
**interne** Namen blieb dabei übersehen — deshalb reopened, s.o.
|
||||||
|
|||||||
82
docs/tp23-stufe6-bypass-sperre-runbook.md
Normal file
82
docs/tp23-stufe6-bypass-sperre-runbook.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# TP23 Stufe 6 — DNS-Bypass-Sperre (Runbook für eine SEPARATE, VOR-ORT-Session)
|
||||||
|
|
||||||
|
> 🔴🔴 **HARTES GATE: NUR VOR ORT ausführen. NICHT aus der Ferne, nicht während Christoph
|
||||||
|
> abwesend ist.** (Notiert 2026-09-06: Christoph ist ~1 Woche nicht vor Ort → in diesem Fenster
|
||||||
|
> **gar nicht anfangen**, auch nicht die log-only-Vorstufe scharf lassen.)
|
||||||
|
>
|
||||||
|
> Grund: Die Sperre macht das AGH-Paar `.36`/`.37` zum **harten DNS-SPOF**. Bis heute kann jeder
|
||||||
|
> Client bei einem AGH-Ausfall auf einen externen Resolver ausweichen; **nach** der Sperre nicht
|
||||||
|
> mehr — ein AGH-Ausfall = **hausweiter DNS-Ausfall** (betrifft die Familie), und ohne physischen
|
||||||
|
> Zugriff gibt es keinen schnellen Rollback. Eine falsch platzierte Drop-Regel kann außerdem den
|
||||||
|
> **Fernzugang selbst** (WG/cf-mesh) aussperren, mit dem man den Fehler zurücknehmen wollte.
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
Clients dürfen plain **DNS (udp/tcp 53)** und **DoT (853)** nur noch zu den erlaubten Resolvern
|
||||||
|
(`10.40.20.36`, `10.40.20.37`, Core `10.40.X.1`) schicken. Externe DNS-Server (Smart-TV mit
|
||||||
|
`8.8.8.8` hartkodiert, App-eigene Resolver) werden geblockt → **kein Umgehen des Filters**.
|
||||||
|
Das ist der letzte Baustein von TP23 und lebt in der **TP5-Firewall** (`forward`-Kette).
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
**Erfüllt (Stand 2026-09-06):**
|
||||||
|
- Self-Service-Oberfläche steht und ist getestet (HA-Dashboard `dns-ausnahmen`, Stufe 5) → ein
|
||||||
|
geblockter Dienst kann **ohne Admin** temporär freigegeben werden. Das war die harte
|
||||||
|
Reihenfolge-Regel („erst der Ersatz, dann die Sperre").
|
||||||
|
- `dns-bypass-exempt`-Gruppe existiert auf dem Resolver-Paar (AGH-Client-Seite).
|
||||||
|
|
||||||
|
**Entwurf liegt bereit (freigegeben 2026-09-11, NOCH NICHT deployt — Christoph war remote):**
|
||||||
|
- Phase A (Substrat + Exempt/JIT-Accepts + log-only): `routeros/tp23-stufe6-phaseA-exempt-log.rsc`
|
||||||
|
- Phase B (reject + Auto-Revert): `routeros/tp23-stufe6-phaseB-reject.rsc`
|
||||||
|
- 🔴 **Vor dem Deploy live verifizieren** (im Dateikopf markiert): (1) der Egress-Accept-Anker in
|
||||||
|
der forward-Kette, vor dem die Regeln stehen müssen; (2) die Exempt-IPs des dev-MacBooks.
|
||||||
|
|
||||||
|
**Noch zu bauen (Teil dieser Session, VOR dem Drop):**
|
||||||
|
- **Firewall-Exempt** ist noch NICHT da: eine Address-List `dns-bypass-exempt` (permanent) mit dem
|
||||||
|
dev-MacBook (`10.40.10.183` **und** `10.40.10.82`, dual-homed) + Agent-Toolchain, plus eine
|
||||||
|
`accept`-Regel **vor** dem Drop. Ohne die verliert der Entwicklungsrechner seinen Ausweg.
|
||||||
|
- Ad-hoc-JIT: Address-List `jit-dns-bypass` mit `timeout` nach dem Muster `jit-admin`
|
||||||
|
(`routeros/tp5-etappe4-input-mgmt.rsc`) — selbstverfallende Freigabe.
|
||||||
|
|
||||||
|
## Bekannte Grenze (offen benennen, nicht „lösen")
|
||||||
|
Die Sperre greift auf **Port** (53/853). **DoH läuft über 443** und ist damit portgleich zu HTTPS —
|
||||||
|
ein Gerät, das DoH gegen `8.8.8.8:443` fährt, umgeht die Sperre. Das per SNI/IP zu schließen ist
|
||||||
|
ein eigenes, größeres Thema (nicht Teil von TP23). Für den Alltag (TVs/IoT mit Plain-DNS) reicht die
|
||||||
|
Port-Sperre.
|
||||||
|
|
||||||
|
## Sichere Methodik — log-only zuerst, Drop zuletzt
|
||||||
|
1. **Exempt + JIT bauen** (via Nautobot GC, `ip-firewall-filter` steht unter Drift-Kontrolle → **kein
|
||||||
|
Direkt-Write**): `accept` für `dns-bypass-exempt`/`jit-dns-bypass` → 53/853, platziert **vor** der
|
||||||
|
künftigen Drop-Regel. Erst hier abnehmen, dass die Exempts wirklich durchkommen.
|
||||||
|
2. **Log-only-Vorstufe**: eine Regel `TP5-WOULD-DROP-DNS`, die LAN→extern auf `dst-port 53,853`
|
||||||
|
(udp+tcp), **nicht** zu `.36/.37/Core`, **nur loggt** (in den Kollektor `10.40.20.18:514`, wie die
|
||||||
|
bestehende `TP5-WOULD-DROP`). **Mehrere Tage laufen lassen** und den Drop-Log lesen: *welche*
|
||||||
|
Geräte reden mit *welchen* externen Resolvern? Jedes davon ist ein Kandidat, der nach dem Drop
|
||||||
|
bricht — vorab entscheiden (Reservierung auf AGH, Exempt, oder bewusst brechen lassen).
|
||||||
|
🔑 Merkregel aus dem E5-Drop-Log: Geräte fallen oft mit **udp/161/andere** nebenbei auf — nur die
|
||||||
|
53/853-Zeilen zählen hier.
|
||||||
|
3. **Erst nach ruhigem/verstandenem Log**: die Log-only-Regel auf `action=drop` (oder `reject` mit
|
||||||
|
`icmp-port-unreachable`, damit Clients schneller auf den nächsten Resolver fallen) heben —
|
||||||
|
**mit Auto-Revert-Absicherung** (Scheduler/Netwatch, der die Regel nach X Minuten deaktiviert,
|
||||||
|
falls nicht bestätigt), analog zum TP5-E3/E4-Vorgehen. **Vor Ort**, mit offener Konsole.
|
||||||
|
|
||||||
|
## Was NICHT gesperrt werden darf (sonst Selbst-Aussperrung)
|
||||||
|
- `.36`/`.37` → CF-DoH-Upstream (443) und Core-Resolver-Egress: die Resolver müssen raus.
|
||||||
|
- Core `10.40.X.1` DoH-Egress.
|
||||||
|
- **cf-mesh / WG-Pfad** (Fernzugang) — die Drop-Regel matcht nur `dst-port 53,853`; trotzdem vor dem
|
||||||
|
Scharfschalten gegenprüfen, dass Management-Input (`cf-mesh`, HA `8728`, Nautobot `22`) unberührt
|
||||||
|
bleibt.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
- GC-Deploy der vorherigen Version (Regel zurück auf `disabled`/entfernt) — bzw. der Auto-Revert.
|
||||||
|
- Fällt das AGH-Paar bei aktiver Sperre aus: Drop-Regel deaktivieren (Vor-Ort-Konsole) → Clients
|
||||||
|
können sofort wieder externe Resolver nutzen.
|
||||||
|
|
||||||
|
## Abnahme
|
||||||
|
- Exempt-Host (dev-MacBook) löst extern weiter auf; ein Test-Gerät mit hartkodiertem `8.8.8.8` löst
|
||||||
|
**nicht** mehr (aber über AGH schon).
|
||||||
|
- Self-Service: eine geblockte Domain per HA-Button freigeben → wirkt; nach Ablauf wieder gefiltert.
|
||||||
|
- Compliance nach dem GC-Deploy grün (`python3 nautobot/gc-transaktion.py status`, GC 140/140).
|
||||||
|
|
||||||
|
## Verweise
|
||||||
|
- Stufe 4/5 (fertig): `adguard/README.md`, `adguard/ha-self-service.md`, `adguard/agh-exception.py`.
|
||||||
|
- JIT-/Firewall-Muster: `routeros/tp5-etappe4-input-mgmt.rsc`; GC-Pipeline: `nautobot/gc-configplan.py`.
|
||||||
|
- Kontext/Reihenfolge-Regel: TP23-Abschnitt in `docs/teilprojekte.md`.
|
||||||
14
jobs/__init__.py
Normal file
14
jobs/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""bf-infra Nautobot-Jobs (Git-Datenquelle).
|
||||||
|
|
||||||
|
Nautobot importiert nur dieses Paket; damit die enthaltenen Jobs sich
|
||||||
|
registrieren, muss jedes Job-Modul hier importiert werden. Neue periodische
|
||||||
|
externe->Nautobot-Syncs kommen als weiteres Modul dazu (kein per-Task-Container).
|
||||||
|
"""
|
||||||
|
from nautobot.apps.jobs import register_jobs
|
||||||
|
|
||||||
|
from .cf_estate_sync import CFEstateSync
|
||||||
|
|
||||||
|
# Nautobot importiert nur dieses Paket (bf_infra.jobs) und erwartet register_jobs()
|
||||||
|
# HIER im __init__ (nicht im Submodul) — sonst: "No jobs were registered on loading
|
||||||
|
# the bf_infra.jobs submodule". Neue Jobs: Klasse importieren + hier mitregistrieren.
|
||||||
|
register_jobs(CFEstateSync)
|
||||||
267
jobs/cf_estate_sync.py
Normal file
267
jobs/cf_estate_sync.py
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
"""
|
||||||
|
TP44 Teil B — CF-Estate → Nautobot, als geplanter Nautobot-Job (ORM).
|
||||||
|
|
||||||
|
Kanonische Runtime des CF→Nautobot-Sync. Laeuft im nautobot-worker (kein extra
|
||||||
|
Container), schreibt per ORM (keine Nautobot-Exposure), zieht die CF-API outbound.
|
||||||
|
Portierung der Reconcile-Logik aus nautobot/tp44-cf-sync.py (REST) auf das ORM.
|
||||||
|
|
||||||
|
Governance (User 2026-09-09): periodische externe->Nautobot-Syncs leben als
|
||||||
|
Nautobot-Jobs in bf-infra/jobs/ und werden in Nautobot geplant — KEIN per-Task-
|
||||||
|
Container/-Cron. Kuenftige Syncs = weitere Jobs hier.
|
||||||
|
|
||||||
|
Deckung: CloudNetwork (cfd_tunnel + warp_connector), CloudService (workers,
|
||||||
|
access-apps, hyperdrive, VPC), Prefix-Assignments (teamnet/routes), Verwaisung
|
||||||
|
(stale-Markierung statt Loeschen). ID-Matching + extra_config-Merge wie im Skript.
|
||||||
|
|
||||||
|
Token: Nautobot-Secret "cf-api-token" (Provider environment-variable -> CF_API_TOKEN
|
||||||
|
am Worker). CF-Account-ID: Secret "cf-account-id" ODER Konstante unten.
|
||||||
|
|
||||||
|
CF bleibt operativer Master; Nautobot ist Dokumentations-SSoT (read-only Pull).
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from nautobot.apps.jobs import BooleanVar, Job
|
||||||
|
from nautobot.cloud.models import (
|
||||||
|
CloudAccount,
|
||||||
|
CloudNetwork,
|
||||||
|
CloudNetworkPrefixAssignment,
|
||||||
|
CloudResourceType,
|
||||||
|
CloudService,
|
||||||
|
)
|
||||||
|
from nautobot.dcim.models import Manufacturer
|
||||||
|
from nautobot.extras.models import Secret
|
||||||
|
from nautobot.ipam.models import Prefix
|
||||||
|
|
||||||
|
CF_API = "https://api.cloudflare.com/client/v4"
|
||||||
|
CF_ACCOUNT_ID = "393c051f3accf51881d6c98fc5b66c36"
|
||||||
|
|
||||||
|
RESOURCE_TYPES = [
|
||||||
|
("Cloudflare cloudflared Tunnel", "cloud", "cloudnetwork"),
|
||||||
|
("Cloudflare WARP Connector", "cloud", "cloudnetwork"),
|
||||||
|
("Cloudflare VPC Service", "cloud", "cloudservice"),
|
||||||
|
("Cloudflare Hyperdrive", "cloud", "cloudservice"),
|
||||||
|
("Cloudflare Worker", "cloud", "cloudservice"),
|
||||||
|
("Cloudflare Access Application", "cloud", "cloudservice"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CFEstateSync(Job):
|
||||||
|
dryrun = BooleanVar(
|
||||||
|
default=True,
|
||||||
|
description="Nur berichten, nichts schreiben (empfohlen fuer den ersten Lauf).",
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
name = "CF Estate Sync"
|
||||||
|
description = "Cloudflare-Estate (Tunnel/Worker/Access/Hyperdrive/VPC) nach Nautobot syncen (Doku-SSoT)."
|
||||||
|
grouping = "TP44 / Cloudflare"
|
||||||
|
# Nur ein Boolean (dryrun) als Eingabe; das CF-Token kommt aus dem Nautobot-Secret,
|
||||||
|
# nicht aus einer Job-Variable. Ohne diese Zeile (Default True) verweigert Nautobot
|
||||||
|
# das Scheduling: "Job may have sensitive input variables".
|
||||||
|
has_sensitive_variables = False
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- CF-Pull
|
||||||
|
def _cf_get(self, path):
|
||||||
|
"""(ok, result) — 403/404 werden abgefangen, damit fehlende Scopes den Sync nicht kippen."""
|
||||||
|
req = urllib.request.Request(CF_API + path, headers={"Authorization": "Bearer " + self._tok})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return True, json.load(r).get("result")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code in (403, 404):
|
||||||
|
return False, None
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _ingress(self, tunnel_id):
|
||||||
|
ok, res = self._cf_get(f"/accounts/{CF_ACCOUNT_ID}/cfd_tunnel/{tunnel_id}/configurations")
|
||||||
|
rules = ((res or {}).get("config") or {}).get("ingress") or [] if ok else []
|
||||||
|
return [r["hostname"] for r in rules if r.get("hostname")]
|
||||||
|
|
||||||
|
def _load_cf(self):
|
||||||
|
"""networks/services: (name, rt_name, desc, extra, match_key, match_val).
|
||||||
|
routes: {tunnel_id: cidr}. skipped: Liste nicht lesbarer Endpoints."""
|
||||||
|
A = CF_ACCOUNT_ID
|
||||||
|
networks, services, routes, skipped = [], [], {}, []
|
||||||
|
|
||||||
|
_, tuns = self._cf_get(f"/accounts/{A}/cfd_tunnel?is_deleted=false&per_page=50")
|
||||||
|
for t in (tuns or []):
|
||||||
|
networks.append((t["name"], "Cloudflare cloudflared Tunnel",
|
||||||
|
f"cloudflared-Tunnel ({len(t.get('connections') or [])} Connectoren)",
|
||||||
|
{"tunnel_id": t["id"], "kind": "cloudflared",
|
||||||
|
"connector_count": len(t.get("connections") or []),
|
||||||
|
"ingress_hostnames": self._ingress(t["id"])},
|
||||||
|
"tunnel_id", t["id"]))
|
||||||
|
_, warps = self._cf_get(f"/accounts/{A}/warp_connector?per_page=50")
|
||||||
|
for w in (warps or []):
|
||||||
|
networks.append((w["name"], "Cloudflare WARP Connector",
|
||||||
|
f"WARP-Connector ({len(w.get('connections') or [])} Connectoren)",
|
||||||
|
{"tunnel_id": w["id"], "kind": "warp_connector",
|
||||||
|
"connector_count": len(w.get("connections") or [])},
|
||||||
|
"tunnel_id", w["id"]))
|
||||||
|
|
||||||
|
_, scripts = self._cf_get(f"/accounts/{A}/workers/scripts")
|
||||||
|
for s in (scripts or []):
|
||||||
|
services.append((s["id"], "Cloudflare Worker", "Cloudflare Worker",
|
||||||
|
{"script": s["id"]}, "script", s["id"]))
|
||||||
|
|
||||||
|
ok, apps = self._cf_get(f"/accounts/{A}/access/apps?per_page=100")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("access/apps")
|
||||||
|
for a in (apps or []):
|
||||||
|
services.append((a.get("name") or a.get("id"), "Cloudflare Access Application",
|
||||||
|
f"Access-App ({a.get('type')})",
|
||||||
|
{"id": a.get("id"), "type": a.get("type"), "domain": a.get("domain")},
|
||||||
|
"id", a.get("id")))
|
||||||
|
|
||||||
|
ok, hyper = self._cf_get(f"/accounts/{A}/hyperdrive/configs")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("hyperdrive/configs")
|
||||||
|
for h in (hyper or []):
|
||||||
|
origin = h.get("origin") or {}
|
||||||
|
services.append((h.get("name") or h.get("id"), "Cloudflare Hyperdrive", "Hyperdrive-Config",
|
||||||
|
{"id": h.get("id"), "database": origin.get("database"),
|
||||||
|
"user": origin.get("user")}, "id", h.get("id")))
|
||||||
|
|
||||||
|
ok, vpc = self._cf_get(f"/accounts/{A}/connectivity/directory/services")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("connectivity/directory/services")
|
||||||
|
for v in (vpc or []):
|
||||||
|
# VPC-Directory-Services tragen ihren Identifier in "service_id" (NICHT "id").
|
||||||
|
sid = v.get("service_id") or v.get("id")
|
||||||
|
services.append((v.get("name") or sid, "Cloudflare VPC Service", "VPC-Service",
|
||||||
|
{"service_id": sid, "type": v.get("type")}, "service_id", sid))
|
||||||
|
|
||||||
|
ok, rts = self._cf_get(f"/accounts/{A}/teamnet/routes?per_page=50")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("teamnet/routes")
|
||||||
|
for r in (rts or []):
|
||||||
|
if r.get("network") and r.get("tunnel_id"):
|
||||||
|
routes[r["tunnel_id"]] = r["network"]
|
||||||
|
|
||||||
|
return networks, services, routes, skipped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- ORM-Upsert
|
||||||
|
def _upsert(self, model, cache, name, rt, acct, desc, extra, mk, mv, label, seen, dryrun):
|
||||||
|
# mv=None nie matchen: sonst greift .get(mk)==None auf das erste Objekt OHNE mk
|
||||||
|
# und mischt fremde extra_config zusammen (z.B. VPC ohne id -> App-Launcher).
|
||||||
|
if mv is None:
|
||||||
|
self.logger.warning("~ %s '%s' ohne Match-Wert (%s) — uebersprungen", label, name, mk)
|
||||||
|
return None
|
||||||
|
ex = next((o for o in cache if (o.extra_config or {}).get(mk) == mv), None)
|
||||||
|
if ex:
|
||||||
|
seen.add(ex.id)
|
||||||
|
merged = dict(ex.extra_config or {})
|
||||||
|
merged.update(extra)
|
||||||
|
if merged == (ex.extra_config or {}):
|
||||||
|
return ex
|
||||||
|
if dryrun:
|
||||||
|
self.logger.info("~ %s '%s': extra_config-Update [DRY-RUN]", label, ex.name)
|
||||||
|
return ex
|
||||||
|
ex.extra_config = merged
|
||||||
|
ex.validated_save()
|
||||||
|
self.logger.info("~ %s '%s' aktualisiert", label, ex.name)
|
||||||
|
return ex
|
||||||
|
if dryrun:
|
||||||
|
self.logger.info("+ %s '%s' [DRY-RUN]", label, name)
|
||||||
|
return None
|
||||||
|
obj = model(name=name, cloud_resource_type=rt, cloud_account=acct,
|
||||||
|
description=desc, extra_config=extra)
|
||||||
|
obj.validated_save()
|
||||||
|
cache.append(obj)
|
||||||
|
seen.add(obj.id)
|
||||||
|
self.logger.info("+ %s '%s' angelegt", label, name)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def _mark_stale(self, cache, seen, label, dryrun):
|
||||||
|
for o in cache:
|
||||||
|
extra = o.extra_config or {}
|
||||||
|
if o.id in seen:
|
||||||
|
if extra.get("stale") and not dryrun:
|
||||||
|
o.extra_config = {k: v for k, v in extra.items() if k != "stale"}
|
||||||
|
o.validated_save()
|
||||||
|
self.logger.info("↩ %s '%s': stale entfernt (wieder da)", label, o.name)
|
||||||
|
continue
|
||||||
|
if extra.get("stale"):
|
||||||
|
continue
|
||||||
|
if dryrun:
|
||||||
|
self.logger.warning("⚠ %s '%s' fehlt im Live-Pull → wuerde stale markiert [DRY-RUN]", label, o.name)
|
||||||
|
continue
|
||||||
|
o.extra_config = dict(extra, stale=True)
|
||||||
|
o.validated_save()
|
||||||
|
self.logger.warning("⚠ %s '%s' als stale markiert (nicht mehr in CF)", label, o.name)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- run
|
||||||
|
def run(self, dryrun=True):
|
||||||
|
try:
|
||||||
|
self._tok = Secret.objects.get(name="cf-api-token").get_value()
|
||||||
|
except Secret.DoesNotExist:
|
||||||
|
raise RuntimeError("Nautobot-Secret 'cf-api-token' fehlt — bitte anlegen (Provider env, CF_API_TOKEN).")
|
||||||
|
|
||||||
|
networks, services, routes, skipped = self._load_cf()
|
||||||
|
self.logger.info("Live-Pull: %d CloudNetworks · %d CloudServices%s",
|
||||||
|
len(networks), len(services),
|
||||||
|
f" · SKIP: {', '.join(skipped)}" if skipped else " · alle Endpoints lesbar")
|
||||||
|
|
||||||
|
mfr, _ = Manufacturer.objects.get_or_create(name="Cloudflare")
|
||||||
|
acct, created = CloudAccount.objects.get_or_create(
|
||||||
|
name="Cloudflare", defaults={"account_number": CF_ACCOUNT_ID, "provider": mfr})
|
||||||
|
if created and dryrun:
|
||||||
|
pass
|
||||||
|
|
||||||
|
rt_by_name = {}
|
||||||
|
for name, app, model_name in RESOURCE_TYPES:
|
||||||
|
ct = ContentType.objects.get(app_label=app, model=model_name)
|
||||||
|
rt, _ = CloudResourceType.objects.get_or_create(name=name, defaults={"provider": mfr})
|
||||||
|
rt.content_types.add(ct)
|
||||||
|
rt_by_name[name] = rt
|
||||||
|
|
||||||
|
net_cache = list(CloudNetwork.objects.filter(cloud_account=acct))
|
||||||
|
svc_cache = list(CloudService.objects.filter(cloud_account=acct))
|
||||||
|
seen_net, seen_svc = set(), set()
|
||||||
|
|
||||||
|
self.logger.info("[CloudNetworks]")
|
||||||
|
for name, rt_name, desc, extra, mk, mv in networks:
|
||||||
|
self._upsert(CloudNetwork, net_cache, name, rt_by_name.get(rt_name), acct,
|
||||||
|
desc, extra, mk, mv, "CloudNetwork", seen_net, dryrun)
|
||||||
|
|
||||||
|
self.logger.info("[CloudServices]")
|
||||||
|
for name, rt_name, desc, extra, mk, mv in services:
|
||||||
|
self._upsert(CloudService, svc_cache, name, rt_by_name.get(rt_name), acct,
|
||||||
|
desc, extra, mk, mv, "CloudService", seen_svc, dryrun)
|
||||||
|
|
||||||
|
self.logger.info("[Prefix-Assignments]")
|
||||||
|
for tunnel_id, cidr in routes.items():
|
||||||
|
net = next((n for n in net_cache if (n.extra_config or {}).get("tunnel_id") == tunnel_id), None)
|
||||||
|
if not net or getattr(net, "id", None) is None:
|
||||||
|
continue
|
||||||
|
pfx = Prefix.objects.filter(prefix=cidr).first()
|
||||||
|
if not pfx:
|
||||||
|
self.logger.info(" ~ Route %s: Prefix nicht in IPAM → uebersprungen", cidr)
|
||||||
|
continue
|
||||||
|
if CloudNetworkPrefixAssignment.objects.filter(cloud_network=net, prefix=pfx).exists():
|
||||||
|
continue
|
||||||
|
if dryrun:
|
||||||
|
self.logger.info(" + %s ↔ %s [DRY-RUN]", net.name, cidr)
|
||||||
|
continue
|
||||||
|
CloudNetworkPrefixAssignment(cloud_network=net, prefix=pfx).validated_save()
|
||||||
|
self.logger.info(" + %s ↔ %s angelegt", net.name, cidr)
|
||||||
|
|
||||||
|
self.logger.info("[Verwaisung]")
|
||||||
|
# CloudServices nur markieren, wenn ALLE Service-Endpoints lesbar waren — sonst
|
||||||
|
# wuerde ein fehlender Scope echte Objekte faelschlich als verwaist markieren.
|
||||||
|
svc_safe = not ({"access/apps", "hyperdrive/configs", "connectivity/directory/services"} & set(skipped))
|
||||||
|
if svc_safe:
|
||||||
|
self._mark_stale(svc_cache, seen_svc, "CloudService", dryrun)
|
||||||
|
else:
|
||||||
|
self.logger.info(" · CloudService-Verwaisung uebersprungen (Token-Scope unvollstaendig)")
|
||||||
|
self._mark_stale(net_cache, seen_net, "CloudNetwork", dryrun)
|
||||||
|
|
||||||
|
self.logger.info("Fertig (%s).", "DRY-RUN — nichts geschrieben" if dryrun else "COMMIT")
|
||||||
|
|
||||||
|
|
||||||
|
# Registrierung zentral in jobs/__init__.py (Nautobot importiert nur das Paket).
|
||||||
178
nautobot/tp37-dienste-seed.py
Normal file
178
nautobot/tp37-dienste-seed.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TP37 Punkt 2 (Dienste) — exponierte Ports als native ipam.Service-Objekte.
|
||||||
|
|
||||||
|
Idempotenter Seeder (get-or-create) fuer ipam.Service:
|
||||||
|
je VM-Name + Port + Protokoll ein Service-Eintrag.
|
||||||
|
|
||||||
|
Vorbedingung: TP37 P1 (tp37-substrat-seed.py --commit) muss vorher gelaufen sein,
|
||||||
|
da die VirtualMachine-Objekte existieren muessen.
|
||||||
|
|
||||||
|
Datenmodell-Entscheid (User 2026-09-06): native Modelle, KEINE eigene App.
|
||||||
|
Muster: nautobot/tp37-substrat-seed.py.
|
||||||
|
|
||||||
|
Umfang (User-Default 2026-09-07):
|
||||||
|
Caddy (:80/:443), Caddy-Backends (haos:8123, vaultwarden:80, gitea:3000, n8n:5678),
|
||||||
|
Nautobot:8080, AdGuard:53 (bf1+bf2, tcp+udp), platform-pg:5432, mariadb:3306,
|
||||||
|
influxdb2:8086, mqtt5:1883, portainer:9443,
|
||||||
|
MCP-Dienste (pve-mcp:3000, shelly-mcp:3030, n8n-mcp:3001, gitea-mcp:3000).
|
||||||
|
+ Smart-Home (vervollstaendigt 2026-09-08, Ports per Socket-Probe verifiziert):
|
||||||
|
RaspberryMatic/CCU (80/443/2001/2010), iob-master:8081, z2m25:8080.
|
||||||
|
iob-exposed/iob_slave BEWUSST ohne Service (kein exponierter Port gefunden).
|
||||||
|
|
||||||
|
Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored),
|
||||||
|
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
python3 nautobot/tp37-dienste-seed.py # Dry-Run (default)
|
||||||
|
python3 nautobot/tp37-dienste-seed.py --commit # legt an
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
|
COMMIT = "--commit" in sys.argv
|
||||||
|
|
||||||
|
|
||||||
|
def load_token() -> str:
|
||||||
|
for line in YML.read_text().splitlines():
|
||||||
|
m = re.search(r"NAUTOBOT_SUPERUSER_API_TOKEN[=:]\s*([^\s\"']+)", line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
sys.exit(f"NAUTOBOT_SUPERUSER_API_TOKEN nicht in {YML} gefunden")
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN = load_token()
|
||||||
|
HDRS = {"Authorization": f"Token {TOKEN}", "Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
|
def req(method: str, path: str, body=None):
|
||||||
|
url = f"{NB}{path}"
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
r = urllib.request.Request(url, data=data, headers=HDRS, method=method)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(r, timeout=20) as resp:
|
||||||
|
return resp.status, json.loads(resp.read().decode() or "{}")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, json.loads(e.read().decode() or "{}")
|
||||||
|
|
||||||
|
|
||||||
|
def find(endpoint: str, params: dict, match_key: str, match_val: str):
|
||||||
|
q = urllib.parse.urlencode(dict(params, depth=0))
|
||||||
|
st, d = req("GET", f"/api/{endpoint}/?{q}")
|
||||||
|
for row in d.get("results", []):
|
||||||
|
if row.get(match_key) == match_val:
|
||||||
|
return row
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create(endpoint: str, find_params: dict, match_key: str,
|
||||||
|
match_val: str, payload: dict, label: str):
|
||||||
|
existing = find(endpoint, find_params, match_key, match_val)
|
||||||
|
if existing:
|
||||||
|
print(f" = {label}: '{match_val}' existiert ({existing['id']})")
|
||||||
|
return existing
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" + {label}: '{match_val}' [DRY-RUN]")
|
||||||
|
return {"id": f"<dry:{match_val}>"}
|
||||||
|
st, d = req("POST", f"/api/{endpoint}/", payload)
|
||||||
|
if st in (200, 201):
|
||||||
|
print(f" + {label}: '{match_val}' angelegt ({d.get('id')})")
|
||||||
|
return d
|
||||||
|
print(f" ! {label}: '{match_val}' FEHLER {st}: {json.dumps(d)[:300]}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Inventar
|
||||||
|
# (vm_name, svc_name, ports, protocol, description)
|
||||||
|
SERVICES = [
|
||||||
|
# Caddy Reverse-Proxy (TP34 critical)
|
||||||
|
("caddy-bf1", "caddy-http", [80], "tcp", "Caddy HTTP"),
|
||||||
|
("caddy-bf1", "caddy-https", [443], "tcp", "Caddy HTTPS Reverse Proxy"),
|
||||||
|
# Caddy-Backends via bf-public Tunnel
|
||||||
|
("haos", "home-assistant", [8123], "tcp", "Home Assistant Web"),
|
||||||
|
("vaultwarden", "vaultwarden-http", [80], "tcp", "Vaultwarden (HTTP, TLS via Caddy)"),
|
||||||
|
("gitea", "gitea-http", [3000], "tcp", "Gitea Web"),
|
||||||
|
("n8n", "n8n-web", [5678], "tcp", "n8n Workflow Engine"),
|
||||||
|
# Nautobot
|
||||||
|
("nautobot", "nautobot-web", [8080], "tcp", "Nautobot API/Web"),
|
||||||
|
# DNS (TCP + UDP je Resolver)
|
||||||
|
("agh-bf1", "adguard-dns-tcp", [53], "tcp", "AdGuard Home DNS/TCP"),
|
||||||
|
("agh-bf1", "adguard-dns-udp", [53], "udp", "AdGuard Home DNS/UDP"),
|
||||||
|
("agh-bf2", "adguard-dns-tcp", [53], "tcp", "AdGuard Home DNS/TCP"),
|
||||||
|
("agh-bf2", "adguard-dns-udp", [53], "udp", "AdGuard Home DNS/UDP"),
|
||||||
|
# Infrastruktur
|
||||||
|
("platform-pg", "postgresql", [5432], "tcp", "PostgreSQL 16 (Nautobot-DB)"),
|
||||||
|
("mariadb", "mariadb", [3306], "tcp", "MariaDB (vw/hass/gitea)"),
|
||||||
|
("influxdb2", "influxdb", [8086], "tcp", "InfluxDB 2"),
|
||||||
|
("mqtt5", "mqtt", [1883], "tcp", "MQTT Broker (Mosquitto)"),
|
||||||
|
("docker", "portainer", [9443], "tcp", "Portainer CE"),
|
||||||
|
# MCP-Dienste
|
||||||
|
("pve-mcp", "pve-mcp", [3000], "tcp", "Proxmox MCP (StreamableHTTP)"),
|
||||||
|
("shelly-mcp", "shelly-mcp", [3030], "tcp", "Shelly MCP"),
|
||||||
|
("n8n-mcp", "n8n-mcp", [3001], "tcp", "n8n MCP"),
|
||||||
|
("gitea-mcp", "gitea-mcp", [3000], "tcp", "Gitea MCP"),
|
||||||
|
# Smart-Home / IoT — Ports live per Socket-Probe verifiziert 2026-09-08 (NICHT raten!)
|
||||||
|
("RaspberryMatic","ccu-http", [80], "tcp", "Homematic CCU Web (HTTP)"),
|
||||||
|
("RaspberryMatic","ccu-https", [443], "tcp", "Homematic CCU Web (HTTPS)"),
|
||||||
|
("RaspberryMatic","ccu-rpc-bidcos", [2001], "tcp", "Homematic BidCos-RF XML-RPC"),
|
||||||
|
("RaspberryMatic","ccu-rpc-hmip", [2010], "tcp", "Homematic-IP XML-RPC"),
|
||||||
|
("iob-master", "iobroker-admin", [8081], "tcp", "ioBroker Admin"),
|
||||||
|
("z2m25", "zigbee2mqtt", [8080], "tcp", "Zigbee2MQTT Frontend"),
|
||||||
|
# iob-exposed (.19) + iob_slave (.33): kein exponierter Port gefunden (13 Ports, alle Timeout)
|
||||||
|
# -> BEWUSST kein Service, bis ein Port live belegt ist. Nicht "hilfreich" einen erfinden.
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"== TP37 Dienste-Seed ({'COMMIT' if COMMIT else 'DRY-RUN'}) @ {NB} ==")
|
||||||
|
|
||||||
|
# VM-IDs vorab holen (unique Namen, Batch)
|
||||||
|
vm_names = sorted({vm for vm, *_ in SERVICES})
|
||||||
|
print(f"\n[0] VM-IDs holen ({len(vm_names)} unique VMs)")
|
||||||
|
vm_ids = {}
|
||||||
|
for name in vm_names:
|
||||||
|
row = find("virtualization/virtual-machines", {"name": name}, "name", name)
|
||||||
|
if row:
|
||||||
|
vm_ids[name] = row["id"]
|
||||||
|
print(f" = {name}: {row['id']}")
|
||||||
|
else:
|
||||||
|
vm_ids[name] = None
|
||||||
|
print(f" ! {name}: NICHT gefunden — Service-Eintraege werden uebersprungen")
|
||||||
|
|
||||||
|
print(f"\n[1] ipam.Services ({len(SERVICES)} Eintraege)")
|
||||||
|
n_ok = n_skip = 0
|
||||||
|
for vm_name, svc_name, ports, protocol, desc in SERVICES:
|
||||||
|
vm_id = vm_ids.get(vm_name)
|
||||||
|
if not vm_id and COMMIT:
|
||||||
|
print(f" ~ {vm_name}/{svc_name}: VM fehlt, uebersprungen")
|
||||||
|
n_skip += 1
|
||||||
|
continue
|
||||||
|
svc = get_or_create(
|
||||||
|
"ipam/services",
|
||||||
|
{"name": svc_name, "virtual_machine": vm_name},
|
||||||
|
"name", svc_name,
|
||||||
|
{"virtual_machine": vm_id, "name": svc_name,
|
||||||
|
"ports": ports, "protocol": protocol, "description": desc},
|
||||||
|
f"{vm_name}/{protocol}/{ports[0]}",
|
||||||
|
)
|
||||||
|
if svc:
|
||||||
|
n_ok += 1
|
||||||
|
else:
|
||||||
|
n_skip += 1
|
||||||
|
|
||||||
|
print(f"\n== fertig ({'COMMIT' if COMMIT else 'DRY-RUN — nichts geschrieben'}) "
|
||||||
|
f"| Services={n_ok} uebersprungen={n_skip} ==")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
256
nautobot/tp37-substrat-seed.py
Normal file
256
nautobot/tp37-substrat-seed.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TP37 Punkt 1 (Substrat) — Hosts/Container als native Nautobot-Objekte.
|
||||||
|
|
||||||
|
Idempotenter Seeder (get-or-create) fuer die native virtualization-Modellwelt:
|
||||||
|
ClusterType (3x: Proxmox VE / Synology Docker / Docker Desktop)
|
||||||
|
-> Cluster (pve / nas-bf / macmini)
|
||||||
|
-> VirtualMachine je Workload (Status Active)
|
||||||
|
+ VMInterface "eth0" + ipam.IPAddress (Namespace Global) + primary_ip4
|
||||||
|
|
||||||
|
Datenmodell-Entscheid (User 2026-09-06): native Modelle, KEINE eigene App
|
||||||
|
(konsistent zu TP44). Muster: nautobot/tp44-cf-estate-seed.py.
|
||||||
|
|
||||||
|
Umfang Etappe 1 (User-Default 2026-09-07):
|
||||||
|
- komplettes Substrat: alle 15 pve-Gaeste (inkl. Smart-Home) + alle running
|
||||||
|
Container auf nas-bf (Portainer ep14) und macmini (ep20).
|
||||||
|
- macmini-Container teilen sich die Host-IP 10.40.20.167 (Bridge/Port-Mapping)
|
||||||
|
-> KEIN primary_ip4; Port kommt in TP37 P2 (ipam.Service).
|
||||||
|
- portainer_agent (Bridge 172.17.0.2, kein Workload) weggelassen.
|
||||||
|
- gestoppte (exited) NAS-Container weggelassen (nur running).
|
||||||
|
|
||||||
|
Datenstand: Live-Inventur 2026-09-07 (pve-mcp pct config, Portainer ep14,
|
||||||
|
ssh macmini docker ps, ARP fuer die 2 QEMU-IPs).
|
||||||
|
|
||||||
|
Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored),
|
||||||
|
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
python3 nautobot/tp37-substrat-seed.py # Dry-Run (default)
|
||||||
|
python3 nautobot/tp37-substrat-seed.py --commit # legt an
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
|
COMMIT = "--commit" in sys.argv
|
||||||
|
|
||||||
|
|
||||||
|
def load_token() -> str:
|
||||||
|
for line in YML.read_text().splitlines():
|
||||||
|
m = re.search(r"NAUTOBOT_SUPERUSER_API_TOKEN[=:]\s*([^\s\"']+)", line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
sys.exit(f"NAUTOBOT_SUPERUSER_API_TOKEN nicht in {YML} gefunden")
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN = load_token()
|
||||||
|
HDRS = {"Authorization": f"Token {TOKEN}", "Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
|
def req(method: str, path: str, body=None):
|
||||||
|
url = f"{NB}{path}"
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
r = urllib.request.Request(url, data=data, headers=HDRS, method=method)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(r, timeout=20) as resp:
|
||||||
|
return resp.status, json.loads(resp.read().decode() or "{}")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, json.loads(e.read().decode() or "{}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_dry(v) -> bool:
|
||||||
|
return isinstance(v, str) and v.startswith("<dry:")
|
||||||
|
|
||||||
|
|
||||||
|
def find(endpoint: str, params: dict, match_key: str, match_val: str):
|
||||||
|
q = urllib.parse.urlencode(dict(params, depth=0))
|
||||||
|
st, d = req("GET", f"/api/{endpoint}/?{q}")
|
||||||
|
for row in d.get("results", []):
|
||||||
|
if row.get(match_key) == match_val:
|
||||||
|
return row
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create(endpoint: str, find_params: dict, match_key: str,
|
||||||
|
match_val: str, payload: dict, label: str):
|
||||||
|
existing = find(endpoint, find_params, match_key, match_val)
|
||||||
|
if existing:
|
||||||
|
print(f" = {label}: '{match_val}' existiert ({existing['id']})")
|
||||||
|
return existing
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" + {label}: '{match_val}' [DRY-RUN]")
|
||||||
|
return {"id": f"<dry:{match_val}>"}
|
||||||
|
st, d = req("POST", f"/api/{endpoint}/", payload)
|
||||||
|
if st in (200, 201):
|
||||||
|
print(f" + {label}: '{match_val}' angelegt ({d.get('id')})")
|
||||||
|
return d
|
||||||
|
print(f" ! {label}: '{match_val}' FEHLER {st}: {json.dumps(d)[:300]}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def status_id(name: str):
|
||||||
|
return find("extras/statuses", {"name": name}, "name", name)["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def namespace_id(name: str):
|
||||||
|
return find("ipam/namespaces", {"name": name}, "name", name)["id"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Inventar
|
||||||
|
CLUSTER_TYPES = ["Proxmox VE", "Synology Docker", "Docker Desktop"]
|
||||||
|
|
||||||
|
# cluster -> clustertype
|
||||||
|
CLUSTERS = [("pve", "Proxmox VE"),
|
||||||
|
("nas-bf", "Synology Docker"),
|
||||||
|
("macmini", "Docker Desktop")]
|
||||||
|
|
||||||
|
# cluster -> [ (vm_name, primary_ip_with_mask_or_None, comment) ]
|
||||||
|
VMS = {
|
||||||
|
"pve": [
|
||||||
|
("haos", "10.40.20.105/24", "Home Assistant OS (QEMU 100)"),
|
||||||
|
("iob-master", "10.40.20.107/24", "ioBroker Master (LXC 101)"),
|
||||||
|
("mqtt5", "10.40.20.108/24", "MQTT-Broker Mosquitto (LXC 102)"),
|
||||||
|
("platform-pg", "10.40.20.20/24", "PostgreSQL 16 - Nautobot-DB (LXC 105, TP20)"),
|
||||||
|
("influxdb2", "10.40.20.8/24", "InfluxDB 2 (LXC 106)"),
|
||||||
|
("redis", "10.40.20.111/24", "Redis (LXC 107)"),
|
||||||
|
("mcast-relay", "10.40.10.21/24", "SSDP-Relay VLAN10<->40 (LXC 108); 2. Bein 10.40.40.21"),
|
||||||
|
("agh-bf1", "10.40.20.36/24", "AdGuard Home Resolver (LXC 110, TP23)"),
|
||||||
|
("agh-bf2", "10.40.20.37/24", "AdGuard Home Resolver (LXC 111, TP23)"),
|
||||||
|
("z2m25", "10.40.20.106/24", "Zigbee2MQTT (LXC 112)"),
|
||||||
|
("caddy-bf1", "10.40.20.40/24", "Caddy Reverse-Proxy (LXC 130, TP31)"),
|
||||||
|
("iob-exposed", "10.40.20.19/24", "ioBroker exposed (LXC 150)"),
|
||||||
|
("mariadb", "10.40.20.23/24", "MariaDB - vw/hass/gitea (LXC 302)"),
|
||||||
|
("RaspberryMatic","10.40.20.232/24", "Homematic CCU (QEMU 330)"),
|
||||||
|
("docker", "10.40.20.126/24", "Docker-Host nested (LXC 400)"),
|
||||||
|
],
|
||||||
|
"nas-bf": [
|
||||||
|
("nautobot", "10.40.20.30/24", "Nautobot Web (DS923 ep14)"),
|
||||||
|
("nautobot-worker", "10.40.20.31/24", "Nautobot Golden-Config Worker"),
|
||||||
|
("nautobot-scheduler", "10.40.20.32/24", "Nautobot Scheduler"),
|
||||||
|
("nautobot-redis", "10.40.20.34/24", "Nautobot Redis"),
|
||||||
|
("gitea", "10.40.20.157/24", "Gitea (git-SSoT)"),
|
||||||
|
("vaultwarden", "10.40.20.127/24", "Vaultwarden (DB=mariadb LXC302)"),
|
||||||
|
("shelly-mcp", "10.40.20.180/24", "Shelly-MCP"),
|
||||||
|
("pve-mcp", "10.40.20.173/24", "Proxmox-MCP (mcp-pve)"),
|
||||||
|
("iob_slave", "10.40.20.33/24", "ioBroker Slave"),
|
||||||
|
],
|
||||||
|
"macmini": [
|
||||||
|
# Bridge/Port-Mapping auf Host 10.40.20.167 -> kein eigenes primary_ip4
|
||||||
|
("n8n", None, "n8n (Docker Desktop, Host 10.40.20.167:5678)"),
|
||||||
|
("n8n-redis", None, "n8n Redis (intern, kein Host-Port)"),
|
||||||
|
("n8n-mcp", None, "n8n-MCP (Host 10.40.20.167:3001)"),
|
||||||
|
("gitea-mcp", None, "Gitea-MCP (Host 10.40.20.167:3000)"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"== TP37 Substrat-Seed ({'COMMIT' if COMMIT else 'DRY-RUN'}) @ {NB} ==")
|
||||||
|
|
||||||
|
st_active = status_id("Active")
|
||||||
|
ns_global = namespace_id("Global")
|
||||||
|
print(f" (Status Active={st_active} Namespace Global={ns_global})")
|
||||||
|
|
||||||
|
print("\n[1] ClusterTypes")
|
||||||
|
for ct in CLUSTER_TYPES:
|
||||||
|
get_or_create("virtualization/cluster-types", {"name": ct}, "name", ct,
|
||||||
|
{"name": ct}, "ClusterType")
|
||||||
|
|
||||||
|
print("\n[2] Clusters")
|
||||||
|
ct_lookup = {ct: find("virtualization/cluster-types", {"name": ct}, "name", ct)
|
||||||
|
for ct in CLUSTER_TYPES}
|
||||||
|
cluster_ids = {}
|
||||||
|
for cname, ctname in CLUSTERS:
|
||||||
|
ctrow = ct_lookup.get(ctname)
|
||||||
|
ct_id = ctrow["id"] if ctrow else None
|
||||||
|
cl = get_or_create("virtualization/clusters", {"name": cname}, "name", cname,
|
||||||
|
{"name": cname, "cluster_type": ct_id}, "Cluster")
|
||||||
|
cluster_ids[cname] = cl["id"] if cl else None
|
||||||
|
|
||||||
|
print("\n[3] VirtualMachines (+ eth0 + IPAddress + primary_ip4)")
|
||||||
|
n_vm = n_ip = 0
|
||||||
|
for cname, _ in CLUSTERS:
|
||||||
|
cl_id = cluster_ids.get(cname)
|
||||||
|
print(f" -- Cluster {cname} --")
|
||||||
|
for name, ip, comment in VMS[cname]:
|
||||||
|
vm = get_or_create(
|
||||||
|
"virtualization/virtual-machines", {"name": name}, "name", name,
|
||||||
|
{"name": name, "cluster": cl_id, "status": st_active,
|
||||||
|
"comments": comment}, "VM")
|
||||||
|
if not vm:
|
||||||
|
continue
|
||||||
|
n_vm += 1
|
||||||
|
if not ip:
|
||||||
|
print(f" (ohne primary_ip4 - Host-geteilt)")
|
||||||
|
continue
|
||||||
|
# VMInterface eth0
|
||||||
|
vmif = None
|
||||||
|
if is_dry(vm["id"]):
|
||||||
|
print(f" + eth0 @ {name} [DRY-RUN]")
|
||||||
|
vmif = {"id": f"<dry:{name}/eth0>"}
|
||||||
|
else:
|
||||||
|
vmif = find("virtualization/interfaces",
|
||||||
|
{"virtual_machine": name, "name": "eth0"}, "name", "eth0")
|
||||||
|
if vmif:
|
||||||
|
print(f" = eth0 @ {name} ({vmif['id']})")
|
||||||
|
else:
|
||||||
|
s, d = req("POST", "/api/virtualization/interfaces/",
|
||||||
|
{"virtual_machine": vm["id"], "name": "eth0",
|
||||||
|
"status": st_active})
|
||||||
|
if s in (200, 201):
|
||||||
|
vmif = d
|
||||||
|
print(f" + eth0 @ {name} ({d.get('id')})")
|
||||||
|
else:
|
||||||
|
print(f" ! eth0 @ {name} FEHLER {s}: {json.dumps(d)[:200]}")
|
||||||
|
continue
|
||||||
|
# IPAddress
|
||||||
|
ipobj = get_or_create("ipam/ip-addresses", {"address": ip}, "address", ip,
|
||||||
|
{"address": ip, "status": st_active,
|
||||||
|
"namespace": ns_global, "dns_name": ""},
|
||||||
|
"IP")
|
||||||
|
if not ipobj:
|
||||||
|
continue
|
||||||
|
n_ip += 1
|
||||||
|
if is_dry(vm["id"]) or is_dry(ipobj["id"]) or is_dry(vmif["id"]):
|
||||||
|
print(f" + assign {ip} -> {name}/eth0 + primary_ip4 [DRY-RUN]")
|
||||||
|
continue
|
||||||
|
# assign IP <-> interface (idempotent)
|
||||||
|
q = urllib.parse.urlencode({"vm_interface": vmif["id"], "depth": 0})
|
||||||
|
s, d = req("GET", f"/api/ipam/ip-address-to-interface/?{q}")
|
||||||
|
have = any((r.get("ip_address") or {}).get("id") == ipobj["id"]
|
||||||
|
for r in d.get("results", []))
|
||||||
|
if have:
|
||||||
|
print(f" = assign {ip} -> {name}/eth0")
|
||||||
|
else:
|
||||||
|
s, d = req("POST", "/api/ipam/ip-address-to-interface/",
|
||||||
|
{"ip_address": ipobj["id"], "vm_interface": vmif["id"]})
|
||||||
|
print(f" {'+' if s in (200,201) else '!'} assign {ip} -> {name}/eth0"
|
||||||
|
+ ("" if s in (200, 201) else f" FEHLER {s}: {json.dumps(d)[:150]}"))
|
||||||
|
# primary_ip4 (idempotent)
|
||||||
|
s, vmnow = req("GET", f"/api/virtualization/virtual-machines/{vm['id']}/?depth=0")
|
||||||
|
cur = vmnow.get("primary_ip4")
|
||||||
|
cur = cur.get("id") if isinstance(cur, dict) else cur
|
||||||
|
if cur == ipobj["id"]:
|
||||||
|
print(f" = primary_ip4 {ip} @ {name}")
|
||||||
|
else:
|
||||||
|
s, d = req("PATCH", f"/api/virtualization/virtual-machines/{vm['id']}/",
|
||||||
|
{"primary_ip4": ipobj["id"]})
|
||||||
|
print(f" {'+' if s == 200 else '!'} primary_ip4 {ip} @ {name}"
|
||||||
|
+ ("" if s == 200 else f" FEHLER {s}: {json.dumps(d)[:150]}"))
|
||||||
|
|
||||||
|
print(f"\n== fertig ({'COMMIT' if COMMIT else 'DRY-RUN - nichts geschrieben'}) "
|
||||||
|
f"| VMs={n_vm} IPs={n_ip} ==")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
TP36 — Cloudflare-Estate als SSoT in Nautobot (Etappe 1: manuelle Erstbefuellung).
|
TP44 — Cloudflare-Estate als SSoT in Nautobot (Etappe 1: manuelle Erstbefuellung).
|
||||||
|
|
||||||
Idempotenter Seeder (get-or-create) fuer die native nautobot.cloud-Modellwelt:
|
Idempotenter Seeder (get-or-create) fuer die native nautobot.cloud-Modellwelt:
|
||||||
Manufacturer "Cloudflare"
|
Manufacturer "Cloudflare"
|
||||||
@@ -18,8 +18,8 @@ Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored),
|
|||||||
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
||||||
|
|
||||||
Aufruf:
|
Aufruf:
|
||||||
python3 nautobot/tp36-cf-estate-seed.py # Dry-Run (default)
|
python3 nautobot/tp44-cf-estate-seed.py # Dry-Run (default)
|
||||||
python3 nautobot/tp36-cf-estate-seed.py --commit # legt an
|
python3 nautobot/tp44-cf-estate-seed.py --commit # legt an
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
315
nautobot/tp44-cf-sync.py
Normal file
315
nautobot/tp44-cf-sync.py
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TP44 Etappe 2 Teil B — CF-Estate LIVE aus der Cloudflare-API nach Nautobot syncen.
|
||||||
|
|
||||||
|
Ersetzt den Inline-Snapshot von tp44-cf-estate-seed.py durch einen periodischen
|
||||||
|
Live-Pull (get-or-create + Merge). Idempotent, Dry-Run default. Kein Push nach CF —
|
||||||
|
read-only Pull, Nautobot bleibt Dokumentations-SSoT, CF operativer Master.
|
||||||
|
|
||||||
|
Deckung (Live-Pull, je Endpoint 403-tolerant):
|
||||||
|
CloudNetwork cfd_tunnel + warp_connector (4)
|
||||||
|
CloudService workers/scripts + access/apps (11) [Repo-Token OK]
|
||||||
|
CloudService hyperdrive/configs + VPC directory (3) [braucht Token-Scope]
|
||||||
|
|
||||||
|
Verwaisung: ein CF-Objekt, das in Nautobot steht, aber im Live-Pull fehlt, wird
|
||||||
|
NICHT geloescht, sondern extra_config.stale=true gesetzt (Doku-SSoT bewahrt Historie).
|
||||||
|
Ein Objekt, das wieder auftaucht, verliert die Markierung.
|
||||||
|
|
||||||
|
Tokens:
|
||||||
|
CF : CF_API_TOKEN + CF_ACCOUNT_ID aus ~/.secrets/credentials.env (nie im Output)
|
||||||
|
Nautobot: NAUTOBOT_SUPERUSER_API_TOKEN aus docs/nas-vlan20/stacks/210-nautobot.yml (gitignored)
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
python3 nautobot/tp44-cf-sync.py # Dry-Run (default) — zeigt Diff
|
||||||
|
python3 nautobot/tp44-cf-sync.py --commit # schreibt
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
|
COMMIT = "--commit" in sys.argv
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Cloudflare-Seite
|
||||||
|
def cf_secrets():
|
||||||
|
env = {}
|
||||||
|
with open(os.path.expanduser("~/.secrets/credentials.env")) as fh:
|
||||||
|
for line in fh:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
env[k.strip()] = v.strip().strip("'\"")
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
_CF = cf_secrets()
|
||||||
|
CF_TOK, CF_ACCT = _CF.get("CF_API_TOKEN"), _CF.get("CF_ACCOUNT_ID")
|
||||||
|
if not CF_TOK or not CF_ACCT:
|
||||||
|
sys.exit("CF_API_TOKEN/CF_ACCOUNT_ID fehlen in ~/.secrets/credentials.env")
|
||||||
|
|
||||||
|
|
||||||
|
def cf_get(path):
|
||||||
|
"""(ok, result) — faengt 403/404 ab, damit fehlende Token-Scopes den Sync nicht kippen."""
|
||||||
|
req = urllib.request.Request("https://api.cloudflare.com/client/v4" + path,
|
||||||
|
headers={"Authorization": "Bearer " + CF_TOK})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return True, json.load(r).get("result")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code in (403, 404):
|
||||||
|
return False, None
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def cf_ingress(tunnel_id):
|
||||||
|
ok, res = cf_get(f"/accounts/{CF_ACCT}/cfd_tunnel/{tunnel_id}/configurations")
|
||||||
|
rules = ((res or {}).get("config") or {}).get("ingress") or [] if ok else []
|
||||||
|
return [r["hostname"] for r in rules if r.get("hostname")]
|
||||||
|
|
||||||
|
|
||||||
|
def load_cf_estate():
|
||||||
|
"""Baut (networks, services) aus dem Live-Pull. Jedes Objekt:
|
||||||
|
(name, resource_type, description, extra_config, match_key, match_val).
|
||||||
|
match_key/-val = stabile CF-ID → idempotentes Matching trotz kuratierter Namen.
|
||||||
|
Live-Zaehl-/Statusfelder liegen unter eigenen Keys (connector_count, ingress_hostnames),
|
||||||
|
damit der Merge kuratierte Strings (z.B. connectors='3x @ ap1/ap2/ap3') nicht ueberschreibt."""
|
||||||
|
networks, services = [], []
|
||||||
|
skipped = []
|
||||||
|
|
||||||
|
# --- CloudNetworks: cloudflared-Tunnel (+ Connector-Zahl, Ingress-Hostnames)
|
||||||
|
ok, tuns = cf_get(f"/accounts/{CF_ACCT}/cfd_tunnel?is_deleted=false&per_page=50")
|
||||||
|
for t in (tuns or []):
|
||||||
|
networks.append((t["name"], "Cloudflare cloudflared Tunnel",
|
||||||
|
f"cloudflared-Tunnel ({len(t.get('connections') or [])} Connectoren)",
|
||||||
|
{"tunnel_id": t["id"], "kind": "cloudflared",
|
||||||
|
"connector_count": len(t.get("connections") or []),
|
||||||
|
"ingress_hostnames": cf_ingress(t["id"])},
|
||||||
|
"tunnel_id", t["id"]))
|
||||||
|
# --- CloudNetworks: WARP-Connector
|
||||||
|
ok, warps = cf_get(f"/accounts/{CF_ACCT}/warp_connector?per_page=50")
|
||||||
|
for w in (warps or []):
|
||||||
|
networks.append((w["name"], "Cloudflare WARP Connector",
|
||||||
|
f"WARP-Connector ({len(w.get('connections') or [])} Connectoren)",
|
||||||
|
{"tunnel_id": w["id"], "kind": "warp_connector",
|
||||||
|
"connector_count": len(w.get("connections") or [])},
|
||||||
|
"tunnel_id", w["id"]))
|
||||||
|
|
||||||
|
# --- CloudServices: Workers
|
||||||
|
ok, scripts = cf_get(f"/accounts/{CF_ACCT}/workers/scripts")
|
||||||
|
for s in (scripts or []):
|
||||||
|
services.append((s["id"], "Cloudflare Worker", "Cloudflare Worker",
|
||||||
|
{"script": s["id"]}, "script", s["id"]))
|
||||||
|
|
||||||
|
# --- CloudServices: Access-Apps
|
||||||
|
ok, apps = cf_get(f"/accounts/{CF_ACCT}/access/apps?per_page=100")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("access/apps")
|
||||||
|
for a in (apps or []):
|
||||||
|
services.append((a.get("name") or a.get("id"), "Cloudflare Access Application",
|
||||||
|
f"Access-App ({a.get('type')})",
|
||||||
|
{"id": a.get("id"), "type": a.get("type"), "domain": a.get("domain")},
|
||||||
|
"id", a.get("id")))
|
||||||
|
|
||||||
|
# --- CloudServices: Hyperdrive (braucht Scope)
|
||||||
|
ok, hyper = cf_get(f"/accounts/{CF_ACCT}/hyperdrive/configs")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("hyperdrive/configs")
|
||||||
|
for h in (hyper or []):
|
||||||
|
origin = h.get("origin") or {}
|
||||||
|
services.append((h.get("name") or h.get("id"), "Cloudflare Hyperdrive",
|
||||||
|
"Hyperdrive-Config",
|
||||||
|
{"id": h.get("id"), "database": origin.get("database"),
|
||||||
|
"user": origin.get("user")}, "id", h.get("id")))
|
||||||
|
|
||||||
|
# --- CloudServices: VPC-Service (braucht Scope)
|
||||||
|
ok, vpc = cf_get(f"/accounts/{CF_ACCT}/connectivity/directory/services")
|
||||||
|
if not ok:
|
||||||
|
skipped.append("connectivity/directory/services")
|
||||||
|
for v in (vpc or []):
|
||||||
|
services.append((v.get("name") or v.get("id"), "Cloudflare VPC Service",
|
||||||
|
"VPC-Service",
|
||||||
|
{"service_id": v.get("id"), "type": v.get("type")},
|
||||||
|
"service_id", v.get("id")))
|
||||||
|
|
||||||
|
return networks, services, skipped
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ Nautobot-Seite
|
||||||
|
def nb_token():
|
||||||
|
for line in YML.read_text().splitlines():
|
||||||
|
m = re.search(r"NAUTOBOT_SUPERUSER_API_TOKEN[=:]\s*([^\s\"']+)", line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
sys.exit(f"NAUTOBOT_SUPERUSER_API_TOKEN nicht in {YML} gefunden")
|
||||||
|
|
||||||
|
|
||||||
|
NB_HDRS = {"Authorization": f"Token {nb_token()}", "Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
|
def nb(method, path, body=None):
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
r = urllib.request.Request(f"{NB}{path}", data=data, headers=NB_HDRS, method=method)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(r, timeout=20) as resp:
|
||||||
|
return resp.status, json.loads(resp.read().decode() or "{}")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, json.loads(e.read().decode() or "{}")
|
||||||
|
|
||||||
|
|
||||||
|
def nb_all(endpoint, params):
|
||||||
|
"""Alle Rows eines Endpoints (paginiert)."""
|
||||||
|
out, url = [], f"/api/{endpoint}/?{urllib.parse.urlencode(dict(params, depth=0, limit=200))}"
|
||||||
|
while url:
|
||||||
|
st, d = nb("GET", url)
|
||||||
|
out += d.get("results", [])
|
||||||
|
nxt = d.get("next")
|
||||||
|
url = nxt.replace(NB, "") if nxt else None
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_INDEX = {} # endpoint -> [rows] (einmal je Account geladen)
|
||||||
|
|
||||||
|
|
||||||
|
def index(endpoint, acct_id):
|
||||||
|
if endpoint not in _INDEX:
|
||||||
|
_INDEX[endpoint] = nb_all(endpoint, {"cloud_account": acct_id})
|
||||||
|
return _INDEX[endpoint]
|
||||||
|
|
||||||
|
|
||||||
|
def upsert(endpoint, name, payload, label, seen, acct_id=None,
|
||||||
|
match_key=None, match_val=None):
|
||||||
|
"""get-or-create + Merge von extra_config.
|
||||||
|
Matching: bei (match_key,match_val) nach der stabilen CF-ID in extra_config
|
||||||
|
(Rename-fest), sonst nach Name. Trackt die getroffene id in seen[endpoint]."""
|
||||||
|
ex = None
|
||||||
|
if match_val is not None:
|
||||||
|
for row in index(endpoint, acct_id):
|
||||||
|
if (row.get("extra_config") or {}).get(match_key) == match_val:
|
||||||
|
ex = row
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
for row in (index(endpoint, acct_id) if acct_id else
|
||||||
|
nb_all(endpoint, {"name": name})):
|
||||||
|
if row.get("name") == name:
|
||||||
|
ex = row
|
||||||
|
break
|
||||||
|
if ex:
|
||||||
|
seen.setdefault(endpoint, set()).add(ex["id"])
|
||||||
|
merged = dict(ex.get("extra_config") or {})
|
||||||
|
merged.update(payload.get("extra_config") or {})
|
||||||
|
if merged == (ex.get("extra_config") or {}):
|
||||||
|
print(f" = {label}: '{name}' unveraendert")
|
||||||
|
return ex
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" ~ {label}: '{name}' extra_config-Update [DRY-RUN]")
|
||||||
|
return ex
|
||||||
|
st, d = nb("PATCH", f"/api/{endpoint}/{ex['id']}/", {"extra_config": merged})
|
||||||
|
print(f" ~ {label}: '{name}' aktualisiert" if st == 200
|
||||||
|
else f" ! {label}: '{name}' PATCH {st}: {json.dumps(d)[:200]}")
|
||||||
|
return d if st == 200 else ex
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" + {label}: '{name}' [DRY-RUN]")
|
||||||
|
return {"id": f"<dry:{name}>"}
|
||||||
|
st, d = nb("POST", f"/api/{endpoint}/", payload)
|
||||||
|
if st in (200, 201):
|
||||||
|
seen.setdefault(endpoint, set()).add(d["id"])
|
||||||
|
print(f" + {label}: '{name}' angelegt")
|
||||||
|
return d
|
||||||
|
print(f" ! {label}: '{name}' FEHLER {st}: {json.dumps(d)[:200]}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def mark_stale(endpoint, acct_id, seen_ids, label):
|
||||||
|
"""CF-Objekte in Nautobot (per id), die im Live-Pull fehlen → extra_config.stale=true.
|
||||||
|
Ein wieder aufgetauchtes Objekt verliert die Markierung."""
|
||||||
|
for row in index(endpoint, acct_id):
|
||||||
|
extra = row.get("extra_config") or {}
|
||||||
|
nm = row.get("name")
|
||||||
|
if row["id"] in seen_ids:
|
||||||
|
if extra.get("stale") and COMMIT:
|
||||||
|
nb("PATCH", f"/api/{endpoint}/{row['id']}/",
|
||||||
|
{"extra_config": {k: v for k, v in extra.items() if k != "stale"}})
|
||||||
|
print(f" ↩ {label}: '{nm}' stale-Markierung entfernt (wieder da)")
|
||||||
|
continue
|
||||||
|
if extra.get("stale"):
|
||||||
|
continue
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" ⚠ {label}: '{nm}' fehlt im Live-Pull → wuerde stale markiert [DRY-RUN]")
|
||||||
|
continue
|
||||||
|
nb("PATCH", f"/api/{endpoint}/{row['id']}/", {"extra_config": dict(extra, stale=True)})
|
||||||
|
print(f" ⚠ {label}: '{nm}' als stale markiert (nicht mehr in CF)")
|
||||||
|
|
||||||
|
|
||||||
|
RESOURCE_TYPES = [
|
||||||
|
("Cloudflare cloudflared Tunnel", "cloud.cloudnetwork"),
|
||||||
|
("Cloudflare WARP Connector", "cloud.cloudnetwork"),
|
||||||
|
("Cloudflare VPC Service", "cloud.cloudservice"),
|
||||||
|
("Cloudflare Hyperdrive", "cloud.cloudservice"),
|
||||||
|
("Cloudflare Worker", "cloud.cloudservice"),
|
||||||
|
("Cloudflare Access Application", "cloud.cloudservice"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"== TP44 CF-Sync ({'COMMIT' if COMMIT else 'DRY-RUN'}) CF→{NB} ==")
|
||||||
|
networks, services, skipped = load_cf_estate()
|
||||||
|
print(f"Live-Pull: {len(networks)} CloudNetworks · {len(services)} CloudServices"
|
||||||
|
+ (f" · SKIP (Token-Scope): {', '.join(skipped)}" if skipped else " · alle Endpoints lesbar"))
|
||||||
|
|
||||||
|
print("\n[1] Manufacturer + CloudAccount")
|
||||||
|
mfr = upsert("dcim/manufacturers", "Cloudflare", {"name": "Cloudflare"}, "Manufacturer", {})
|
||||||
|
mfr_id = mfr["id"]
|
||||||
|
acct = upsert("cloud/cloud-accounts", "Cloudflare",
|
||||||
|
{"name": "Cloudflare", "account_number": CF_ACCT, "provider": mfr_id},
|
||||||
|
"CloudAccount", {})
|
||||||
|
acct_id = acct["id"]
|
||||||
|
|
||||||
|
print("\n[2] CloudResourceTypes")
|
||||||
|
rt_ids = {}
|
||||||
|
for name, ct in RESOURCE_TYPES:
|
||||||
|
rt = upsert("cloud/cloud-resource-types", name,
|
||||||
|
{"name": name, "provider": mfr_id, "content_types": [ct]}, "ResourceType", {})
|
||||||
|
rt_ids[name] = rt["id"]
|
||||||
|
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
print("\n[3] CloudNetworks")
|
||||||
|
for name, rt_name, desc, extra, mk, mv in networks:
|
||||||
|
upsert("cloud/cloud-networks", name,
|
||||||
|
{"name": name, "cloud_resource_type": rt_ids.get(rt_name), "cloud_account": acct_id,
|
||||||
|
"description": desc, "extra_config": extra}, "CloudNetwork", seen,
|
||||||
|
acct_id=acct_id, match_key=mk, match_val=mv)
|
||||||
|
|
||||||
|
print("\n[4] CloudServices")
|
||||||
|
for name, rt_name, desc, extra, mk, mv in services:
|
||||||
|
upsert("cloud/cloud-services", name,
|
||||||
|
{"name": name, "cloud_resource_type": rt_ids.get(rt_name), "cloud_account": acct_id,
|
||||||
|
"description": desc, "extra_config": extra}, "CloudService", seen,
|
||||||
|
acct_id=acct_id, match_key=mk, match_val=mv)
|
||||||
|
|
||||||
|
print("\n[5] Verwaisung (stale-Markierung; nur Endpoints, die gelesen wurden)")
|
||||||
|
# Nur markieren, wenn der zugehoerige Endpoint erreichbar war — sonst wuerde ein
|
||||||
|
# fehlender Token-Scope echte Objekte faelschlich als verwaist markieren.
|
||||||
|
if "access/apps" not in skipped and "hyperdrive/configs" not in skipped \
|
||||||
|
and "connectivity/directory/services" not in skipped:
|
||||||
|
mark_stale("cloud/cloud-services", acct_id, seen.get("cloud/cloud-services", set()), "CloudService")
|
||||||
|
else:
|
||||||
|
print(" · CloudService-Verwaisung uebersprungen (Token-Scope unvollstaendig — kein sicherer Vergleich)")
|
||||||
|
mark_stale("cloud/cloud-networks", acct_id, seen.get("cloud/cloud-networks", set()), "CloudNetwork")
|
||||||
|
|
||||||
|
print(f"\n== fertig ({'COMMIT' if COMMIT else 'DRY-RUN — nichts geschrieben'}) ==")
|
||||||
|
if skipped:
|
||||||
|
print(f"Hinweis: {', '.join(skipped)} nicht lesbar → Token-Scope erweitern, dann syncen sie mit.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
119
nautobot/tp44-ipam-prefixes.py
Normal file
119
nautobot/tp44-ipam-prefixes.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
TP44 Etappe 2 Teil A — die zwei fehlenden IPAM-Prefixe der CF-CloudNetworks anlegen.
|
||||||
|
|
||||||
|
Zwei CloudNetworks (tp44-cf-estate-seed.py) tragen eine Route, die der Seeder als
|
||||||
|
cloud-network-prefix-assignment verknuepfen wuerde -- aber nur, wenn der Prefix in
|
||||||
|
IPAM existiert (dort Schritt [7], sonst "~ Prefix ... nicht in IPAM"):
|
||||||
|
|
||||||
|
10.40.0.0/16 -> bf-core-mesh (WARP-Connector auf crs326-core, TP16.1) [container]
|
||||||
|
10.101.0.0/24 -> fm.emsenhuber.net (Standort FM, cloudflared-Tunnel) [network]
|
||||||
|
|
||||||
|
Idempotenter get-or-create (Dry-Run default). Nach dem --commit den Seeder erneut
|
||||||
|
laufen lassen -- er haengt die zwei Assignments dann automatisch ein.
|
||||||
|
|
||||||
|
10.40.0.0/16 als 'container', weil die VLAN-/24 (10.40.10/20/30/...) aus
|
||||||
|
ingest-baseline.py bereits als Prefixe existieren; Nautobot nistet sie
|
||||||
|
automatisch darunter (rein hierarchisch, nicht-destruktiv).
|
||||||
|
|
||||||
|
Token-Quelle: docs/nas-vlan20/stacks/210-nautobot.yml (gitignored),
|
||||||
|
Schluessel NAUTOBOT_SUPERUSER_API_TOKEN.
|
||||||
|
|
||||||
|
Aufruf:
|
||||||
|
python3 nautobot/tp44-ipam-prefixes.py # Dry-Run (default)
|
||||||
|
python3 nautobot/tp44-ipam-prefixes.py --commit # legt an
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.error
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080")
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
YML = REPO / "docs" / "nas-vlan20" / "stacks" / "210-nautobot.yml"
|
||||||
|
COMMIT = "--commit" in sys.argv
|
||||||
|
|
||||||
|
|
||||||
|
def load_token() -> str:
|
||||||
|
for line in YML.read_text().splitlines():
|
||||||
|
m = re.search(r"NAUTOBOT_SUPERUSER_API_TOKEN[=:]\s*([^\s\"']+)", line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
sys.exit(f"NAUTOBOT_SUPERUSER_API_TOKEN nicht in {YML} gefunden")
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN = load_token()
|
||||||
|
HDRS = {"Authorization": f"Token {TOKEN}", "Accept": "application/json",
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
|
def req(method: str, path: str, body=None):
|
||||||
|
url = f"{NB}{path}"
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
r = urllib.request.Request(url, data=data, headers=HDRS, method=method)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(r, timeout=20) as resp:
|
||||||
|
return resp.status, json.loads(resp.read().decode() or "{}")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, json.loads(e.read().decode() or "{}")
|
||||||
|
|
||||||
|
|
||||||
|
def find(endpoint: str, params: dict, match_key: str, match_val: str):
|
||||||
|
q = urllib.parse.urlencode(dict(params, depth=0))
|
||||||
|
st, d = req("GET", f"/api/{endpoint}/?{q}")
|
||||||
|
for row in d.get("results", []):
|
||||||
|
if row.get(match_key) == match_val:
|
||||||
|
return row
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def status_id(name: str):
|
||||||
|
return find("extras/statuses", {"name": name}, "name", name)["id"]
|
||||||
|
|
||||||
|
|
||||||
|
def namespace_id(name: str):
|
||||||
|
return find("ipam/namespaces", {"name": name}, "name", name)["id"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- Inventar
|
||||||
|
# (prefix, type, description)
|
||||||
|
PREFIXES = [
|
||||||
|
("10.40.0.0/16", "container",
|
||||||
|
"BF-Supernet; Route des CF WARP-Connectors bf-core-mesh (crs326-core, TP16.1)"),
|
||||||
|
("10.101.0.0/24", "network",
|
||||||
|
"Standort FM; Route des CF cloudflared-Tunnels fm.emsenhuber.net"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"== TP44 IPAM-Prefixe ({'COMMIT' if COMMIT else 'DRY-RUN'}) @ {NB} ==")
|
||||||
|
st_active = status_id("Active")
|
||||||
|
ns_global = namespace_id("Global")
|
||||||
|
|
||||||
|
for prefix, ptype, desc in PREFIXES:
|
||||||
|
existing = find("ipam/prefixes", {"prefix": prefix}, "prefix", prefix)
|
||||||
|
if existing:
|
||||||
|
print(f" = Prefix: '{prefix}' existiert ({existing['id']})")
|
||||||
|
continue
|
||||||
|
if not COMMIT:
|
||||||
|
print(f" + Prefix: '{prefix}' [{ptype}] [DRY-RUN]")
|
||||||
|
continue
|
||||||
|
payload = {"prefix": prefix, "status": st_active, "namespace": ns_global,
|
||||||
|
"type": ptype, "description": desc}
|
||||||
|
st, d = req("POST", "/api/ipam/prefixes/", payload)
|
||||||
|
if st in (200, 201):
|
||||||
|
print(f" + Prefix: '{prefix}' [{ptype}] angelegt ({d.get('id')})")
|
||||||
|
else:
|
||||||
|
print(f" ! Prefix: '{prefix}' FEHLER {st}: {json.dumps(d)[:300]}")
|
||||||
|
|
||||||
|
print(f"\n== fertig ({'COMMIT' if COMMIT else 'DRY-RUN — nichts geschrieben'}) ==")
|
||||||
|
if not COMMIT:
|
||||||
|
print("Danach: python3 nautobot/tp44-cf-estate-seed.py --commit (haengt die 2 Assignments ein)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
59
routeros/tp23-stufe6-phaseA-exempt-log.rsc
Normal file
59
routeros/tp23-stufe6-phaseA-exempt-log.rsc
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# TP23 Stufe 6 — PHASE A: Substrat + Exempt/JIT-Accepts + log-only-Vorstufe.
|
||||||
|
#
|
||||||
|
# 🔴🔴 NUR VOR ORT. Dieser Deploy allein bricht noch nichts (kein drop/reject),
|
||||||
|
# aber Phase B (der reject) macht .36/.37 zum DNS-SPOF ohne Fern-Rollback.
|
||||||
|
# Runbook: docs/tp23-stufe6-bypass-sperre-runbook.md.
|
||||||
|
#
|
||||||
|
# STATUS: ENTWURF, freigegeben 2026-09-11, NOCH NICHT DEPLOYT (Christoph war remote).
|
||||||
|
#
|
||||||
|
# ┌─ LIVE-VERIFY VOR DEM DEPLOY (nicht raten — MikroMCP war beim Schreiben tot) ─┐
|
||||||
|
# │ (1) ANKER <<EGRESS-ACCEPT>>: die forward-Regel, die HEUTE LAN->WAN-DNS zu │
|
||||||
|
# │ 8.8.8.8:53 durchlaesst (der generische Internet-Egress-Accept). Meine │
|
||||||
|
# │ Regeln muessen DAVOR stehen, sonst greift der Egress-Accept zuerst und │
|
||||||
|
# │ die log-only-Regel zaehlt 0. Anker aus der Live-Kette holen: │
|
||||||
|
# │ /ip firewall filter print where chain=forward │
|
||||||
|
# │ und unten das place-before-Kommentar ersetzen. Faellt externer DNS heute │
|
||||||
|
# │ schon in deny-by-default (TVs mit 8.8.8.8 loesen JETZT nicht auf), ist │
|
||||||
|
# │ Stufe 6 gegenstandslos — dann Runbook neu bewerten. │
|
||||||
|
# │ (2) EXEMPT-IPs: dev-MacBook ist dual-homed. Beide Beine gegen die Realitaet │
|
||||||
|
# │ pruefen (en5/en0 koennen per DHCP gewechselt haben): │
|
||||||
|
# │ /ip dhcp-server lease print where comment~"macbook" │
|
||||||
|
# │ und mit der AGH-Seite (clients.persistent 'dns-bypass-exempt') abgleichen.│
|
||||||
|
# └───────────────────────────────────────────────────────────────────────────┘
|
||||||
|
#
|
||||||
|
# Weg (KEIN mikromcp-Direkt-Write — ip-firewall-filter steht unter Drift-Kontrolle):
|
||||||
|
# Backup davor: create_backup routerId=crs326-core (core-pre-tp23-s6-<datum>.backup)
|
||||||
|
# Deploy: python3 nautobot/gc-manual-plan.py --device crs326-core \
|
||||||
|
# --commands-file routeros/tp23-stufe6-phaseA-exempt-log.rsc \
|
||||||
|
# --change-id TP23-S6-PhaseA --deploy
|
||||||
|
# log-only als befristeter Versuch fuehren (NICHT als Versehen lesbar machen):
|
||||||
|
# python3 nautobot/gc-transaktion.py (Transaktion 'TP23-S6-would-drop-dns')
|
||||||
|
# Soll-Sync (Substrat + Accepts sind Dauerzustand -> ins intended):
|
||||||
|
# python3 nautobot/ingest-core-firewall.py --apply
|
||||||
|
# python3 nautobot/gc-configplan.py --refresh-compliance
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# 0) Substrat: erlaubte Resolver (dst), permanente Exempts (src), leere JIT-Liste.
|
||||||
|
:do { :if ([:len [/ip firewall address-list find list="dns-resolvers" address="10.40.20.36"]] = 0) do={ /ip firewall address-list add list=dns-resolvers address=10.40.20.36 comment="TP23-S6: AGH agh-bf1" } } on-error={ :log info "tp23s6A: 00a warf" }
|
||||||
|
:do { :if ([:len [/ip firewall address-list find list="dns-resolvers" address="10.40.20.37"]] = 0) do={ /ip firewall address-list add list=dns-resolvers address=10.40.20.37 comment="TP23-S6: AGH agh-bf2" } } on-error={ :log info "tp23s6A: 00b warf" }
|
||||||
|
:do { :if ([:len [/ip firewall address-list find list="dns-bypass-exempt" address="10.40.10.183"]] = 0) do={ /ip firewall address-list add list=dns-bypass-exempt address=10.40.10.183 comment="TP23-S6: dev-MacBook en5 (VERIFY)" } } on-error={ :log info "tp23s6A: 00c warf" }
|
||||||
|
:do { :if ([:len [/ip firewall address-list find list="dns-bypass-exempt" address="10.40.10.82"]] = 0) do={ /ip firewall address-list add list=dns-bypass-exempt address=10.40.10.82 comment="TP23-S6: dev-MacBook en0 (VERIFY)" } } on-error={ :log info "tp23s6A: 00d warf" }
|
||||||
|
|
||||||
|
# 1) Exempt-Accepts. dev-MacBook + Agent-Toolchain behalten ihren DNS-Ausweg.
|
||||||
|
# MUSS vor der log-only-Regel stehen (action=accept terminiert -> Exempt-Traffic
|
||||||
|
# erreicht die Log-Regel nie und taucht im WOULD-DROP-Log korrekt NICHT auf).
|
||||||
|
:do { :if ([:len [/ip firewall filter find comment="TP23-S6: dns-exempt udp"]] = 0) do={ /ip firewall filter add chain=forward action=accept src-address-list=dns-bypass-exempt protocol=udp dst-port=53,853 comment="TP23-S6: dns-exempt udp" place-before=[/ip firewall filter find comment="<<EGRESS-ACCEPT — LIVE VERIFY>>"] } } on-error={ :log info "tp23s6A: 01a warf" }
|
||||||
|
:do { :if ([:len [/ip firewall filter find comment="TP23-S6: dns-exempt tcp"]] = 0) do={ /ip firewall filter add chain=forward action=accept src-address-list=dns-bypass-exempt protocol=tcp dst-port=53,853 comment="TP23-S6: dns-exempt tcp" place-before=[/ip firewall filter find comment="<<EGRESS-ACCEPT — LIVE VERIFY>>"] } } on-error={ :log info "tp23s6A: 01b warf" }
|
||||||
|
|
||||||
|
# 2) JIT-Accepts. Leere, selbstverfallende Liste (Muster jit-admin, tp5-etappe4):
|
||||||
|
# /ip firewall address-list add list=jit-dns-bypass address=<ip> timeout=1h
|
||||||
|
:do { :if ([:len [/ip firewall filter find comment="TP23-S6: dns-jit udp"]] = 0) do={ /ip firewall filter add chain=forward action=accept src-address-list=jit-dns-bypass protocol=udp dst-port=53,853 comment="TP23-S6: dns-jit udp" place-before=[/ip firewall filter find comment="<<EGRESS-ACCEPT — LIVE VERIFY>>"] } } on-error={ :log info "tp23s6A: 02a warf" }
|
||||||
|
:do { :if ([:len [/ip firewall filter find comment="TP23-S6: dns-jit tcp"]] = 0) do={ /ip firewall filter add chain=forward action=accept src-address-list=jit-dns-bypass protocol=tcp dst-port=53,853 comment="TP23-S6: dns-jit tcp" place-before=[/ip firewall filter find comment="<<EGRESS-ACCEPT — LIVE VERIFY>>"] } } on-error={ :log info "tp23s6A: 02b warf" }
|
||||||
|
|
||||||
|
# 3) log-only-Vorstufe. Loggt LAN->extern DNS, das NICHT zu einem erlaubten
|
||||||
|
# Resolver geht. dst-address-list=!dns-resolvers negiert die Liste. Mehrere
|
||||||
|
# Tage lesen (Kollektor 10.40.20.18:514), dann Phase B. NUR 53/853-Zeilen zaehlen.
|
||||||
|
:do { :if ([:len [/ip firewall filter find comment="TP23-S6: WOULD-DROP-DNS udp"]] = 0) do={ /ip firewall filter add chain=forward action=log log-prefix="TP5-WOULD-DROP-DNS" protocol=udp dst-port=53,853 dst-address-list=!dns-resolvers comment="TP23-S6: WOULD-DROP-DNS udp" place-before=[/ip firewall filter find comment="<<EGRESS-ACCEPT — LIVE VERIFY>>"] } } on-error={ :log info "tp23s6A: 03a warf" }
|
||||||
|
:do { :if ([:len [/ip firewall filter find comment="TP23-S6: WOULD-DROP-DNS tcp"]] = 0) do={ /ip firewall filter add chain=forward action=log log-prefix="TP5-WOULD-DROP-DNS" protocol=tcp dst-port=53,853 dst-address-list=!dns-resolvers comment="TP23-S6: WOULD-DROP-DNS tcp" place-before=[/ip firewall filter find comment="<<EGRESS-ACCEPT — LIVE VERIFY>>"] } } on-error={ :log info "tp23s6A: 03b warf" }
|
||||||
|
|
||||||
|
:log info ("tp23s6A: ENDE exempt-udp=" . [:tostr [:len [/ip firewall filter find comment="TP23-S6: dns-exempt udp"]]] . " log-udp=" . [:tostr [:len [/ip firewall filter find comment="TP23-S6: WOULD-DROP-DNS udp"]]] . " resolvers=" . [:tostr [:len [/ip firewall address-list find list="dns-resolvers"]]] . " exempt=" . [:tostr [:len [/ip firewall address-list find list="dns-bypass-exempt"]]])
|
||||||
40
routeros/tp23-stufe6-phaseB-reject.rsc
Normal file
40
routeros/tp23-stufe6-phaseB-reject.rsc
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# TP23 Stufe 6 — PHASE B: der scharfe Schnitt (reject) + Auto-Revert.
|
||||||
|
#
|
||||||
|
# 🔴🔴 NUR VOR ORT, mit offener Konsole. Erst NACHDEM das WOULD-DROP-DNS-Log
|
||||||
|
# (Phase A) mehrere Tage ruhig/verstanden ist und jeder externe Resolver
|
||||||
|
# entschieden wurde (Reservierung auf AGH / Exempt / bewusst brechen lassen).
|
||||||
|
# AB HIER ist .36/.37 ein DNS-SPOF ohne Fern-Rollback (Runbook, Gate).
|
||||||
|
#
|
||||||
|
# STATUS: ENTWURF, freigegeben 2026-09-11, NOCH NICHT DEPLOYT.
|
||||||
|
#
|
||||||
|
# WARUM reject STATT drop: reject icmp-port-unreachable laesst Clients schneller
|
||||||
|
# auf den naechsten (erlaubten) Resolver fallen, statt in einen Timeout zu laufen.
|
||||||
|
#
|
||||||
|
# WARUM die Log-Regel STEHEN BLEIBT: action=log terminiert NICHT — das Paket wird
|
||||||
|
# geloggt und faellt dann in die reject-Regel darunter. Gleiche Doktrin wie
|
||||||
|
# tp5-etappe3-drop (Log VOR Drop), der Kollektor zeigt weiter, was reject frisst.
|
||||||
|
#
|
||||||
|
# Weg (wie Phase A, ueber Nautobot GC):
|
||||||
|
# Deploy: python3 nautobot/gc-manual-plan.py --device crs326-core \
|
||||||
|
# --commands-file routeros/tp23-stufe6-phaseB-reject.rsc \
|
||||||
|
# --change-id TP23-S6-PhaseB --deploy
|
||||||
|
# Danach (reject ist jetzt Dauerzustand -> Soll, log-only-Transaktion schliessen):
|
||||||
|
# python3 nautobot/ingest-core-firewall.py --apply
|
||||||
|
# python3 nautobot/gc-configplan.py --refresh-compliance # Ziel gruen
|
||||||
|
#
|
||||||
|
# Rollback sofort (Vor-Ort-Konsole): /ip firewall filter set [find comment~"TP23-S6: reject"] disabled=yes
|
||||||
|
# -> Clients koennen sofort wieder externe Resolver nutzen. Bzw. Auto-Revert unten.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# 0) Auto-Revert ZUERST — deaktiviert die reject-Regeln nach 10m, falls nicht
|
||||||
|
# bestaetigt (analog tp5-etappe3/4). Nach der Abnahme abbestellen:
|
||||||
|
# /system scheduler remove [find name="tp23s6autorevert"]
|
||||||
|
:do { :if ([:len [/system scheduler find name="tp23s6autorevert"]] = 0) do={ /system scheduler add name="tp23s6autorevert" interval=10m on-event="/ip firewall filter set [find comment~\"TP23-S6: reject\"] disabled=yes; /system scheduler remove [find name=\"tp23s6autorevert\"]; :log warning \"tp23s6: AUTO-REVERT hat die DNS-reject-Regeln abgeschaltet\"" } } on-error={ :log info "tp23s6B: 00 scheduler warf" }
|
||||||
|
|
||||||
|
# 1) reject. Gleiche Match-Tupel wie die WOULD-DROP-DNS-Log-Regeln, direkt
|
||||||
|
# dahinter (place-before denselben Egress-Anker -> landet nach den Log-Regeln).
|
||||||
|
# <<EGRESS-ACCEPT>>: derselbe live-verifizierte Anker wie in Phase A.
|
||||||
|
:do { :if ([:len [/ip firewall filter find comment="TP23-S6: reject dns udp"]] = 0) do={ /ip firewall filter add chain=forward action=reject reject-with=icmp-port-unreachable protocol=udp dst-port=53,853 dst-address-list=!dns-resolvers comment="TP23-S6: reject dns udp" place-before=[/ip firewall filter find comment="<<EGRESS-ACCEPT — LIVE VERIFY>>"] } } on-error={ :log info "tp23s6B: 01a warf" }
|
||||||
|
:do { :if ([:len [/ip firewall filter find comment="TP23-S6: reject dns tcp"]] = 0) do={ /ip firewall filter add chain=forward action=reject reject-with=icmp-port-unreachable protocol=tcp dst-port=53,853 dst-address-list=!dns-resolvers comment="TP23-S6: reject dns tcp" place-before=[/ip firewall filter find comment="<<EGRESS-ACCEPT — LIVE VERIFY>>"] } } on-error={ :log info "tp23s6B: 01b warf" }
|
||||||
|
|
||||||
|
:log info ("tp23s6B: ENDE reject-udp=" . [:tostr [:len [/ip firewall filter find comment="TP23-S6: reject dns udp"]]] . " reject-tcp=" . [:tostr [:len [/ip firewall filter find comment="TP23-S6: reject dns tcp"]]] . " autorevert=" . [:tostr [:len [/system scheduler find name="tp23s6autorevert"]]])
|
||||||
Reference in New Issue
Block a user