From bf07c642f71510681184bae95bad943b8dbe90f3 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Fri, 14 Aug 2026 17:30:15 +0200 Subject: [PATCH 1/6] TP5 Etappe 1: Log-Lauf live auf crs326-core Log-only-Regel am Ende der forward-Kette (blockiert nichts) plus Syslog- Kollektor, ausgerollt ueber Nautobot GC als manual Config Plans. - routeros/tp5-etappe1-log-lauf.rsc: der ausgerollte Endzustand + die am Geraet belegten Lehren aus dem Rollout - nautobot/gc-manual-plan.py: Push-Werkzeug (assertet plan.device, liest Plan-Status statt Job-Status, strippt Kommentarzeilen) Zwei Korrekturen an bisherigen Annahmen: - Ein gescheiterter Netmiko-Deploy hinterlaesst einen TEIL-Zustand. Die TP22-Notiz "sauberer Fehlschlag" galt dem NAPALM-Weg. - Bindestriche in name= werden von RouterOS abgelehnt, Quoten hilft nicht. Die zuerst notierte target=/remote=-Erklaerung war falsch und ist raus. Die Log-Regel steht auf out-interface-list="!WAN": bare fing sie auch allen Internet-Verkehr. LAN->WAN ist eine garantierte Whitelist-Zeile, also Rauschen - und ein Merkposten fuer Etappe 3, wo der Drop aus demselben Grund nicht bare stehen darf. D2 entschieden: iPhone .30.201 wird ins MAIN-SSID umgehaengt. Co-Authored-By: Claude Opus 5 --- docs/teilprojekte.md | 6 +- docs/tp5-deny-by-default-runbook.md | 62 ++++++++- nautobot/gc-manual-plan.py | 197 ++++++++++++++++++++++++++++ routeros/tp5-etappe1-log-lauf.rsc | 74 +++++++++++ 4 files changed, 332 insertions(+), 7 deletions(-) create mode 100644 nautobot/gc-manual-plan.py create mode 100644 routeros/tp5-etappe1-log-lauf.rsc diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 31ec163..633a3ae 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -205,7 +205,11 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - Abh.: TP1 ✅. TP3-Abhängigkeit entfällt (WLAN läuft weiterhin über FB bis TP3). - **Ermöglicht jetzt:** PVID-Fix in Omada-Profilen (MAIN→PVID10, INFRA→PVID20), untagNetworkIds-Bereinigung, DHCP-Reservierungen für NAS/macmini/pve auf neue VLAN-Subnetze migrieren. -### TP5 — Firewall & Inter-VLAN-Policy ▸ ⏳ TEIL-LIVE auf `crs326-core` (Holes + WAN-Seal re-verifiziert 2026-08-14; Deny-by-default steht aus) — **jetzt das nächste Arbeitspaket, Session vorbereitet** — 📋 `docs/tp5-deny-by-default-runbook.md` +### TP5 — Firewall & Inter-VLAN-Policy ▸ ⏳ **ETAPPE 1 LÄUFT** (Log-Lauf seit 2026-08-14 live auf `crs326-core`) — 📋 `docs/tp5-deny-by-default-runbook.md` +- ✅ **Etappe 1 ausgerollt 2026-08-14** (über Nautobot GC, manual Config Plans; Backups `core-pre-/post-tp5-etappe1-20260814.backup`). Auf dem Gerät: `forward`-Regel `action=log log-prefix="TP5-WOULD-DROP"` am Kettenende, Syslog-Action `tp5syslog` → **Kollektor `10.40.20.18:514`** (syslog-ng-Container `tp5-syslog` auf dem Docker-LXC, Logs `/opt/tp5-syslog/log/`, 10-Minuten-Cron kappt bei 500 MB — der LXC hat nur ~2,5 GB frei). `firewall` ist aus dem Memory-Ring genommen (`topics=info,!firewall`), sonst wäre der On-Box-Log tagelang unbrauchbar. **Blockiert nichts.** Artefakte: `routeros/tp5-etappe1-log-lauf.rsc`, Push-Werkzeug `nautobot/gc-manual-plan.py`. Compliance danach **16/17** (der eine Rotpunkt `ip-address` = bekannte VLAN1-Bring-up-IP `10.40.41.211` aus dem TP22-Backlog, **keine neue Drift**). +- 🔴 **Fund aus dem Log-Lauf, der Etappe 3 bestimmt: die Regel fing anfangs auch allen Internet-Verkehr** (`out:vlan100`, ~¼ der Zeilen in einer ruhigen Minute, unter Last mehr). Die Log-Regel steht deshalb jetzt auf `out-interface-list="!WAN"`. **Konsequenz für den späteren Drop: er darf NICHT bare ans Kettenende** — davor gehört `accept out-interface-list=WAN`, sonst ist das ganze Haus offline. Diese Whitelist-Zeile ist damit bekannt und muss nicht aus dem Log wiederentdeckt werden. +- 🔍 **Erste Befunde nach 60 s** (Rate nach Einengung ~166 Zeilen/min): **76 % des Inter-VLAN-Verkehrs ist `vlan20 → bridge`, also INFRA nach VLAN1** — ein Monitoring-Host `10.40.20.151` scrapet ~10 tote/alte `10.40.41.x`-Ziele, HA `.20.105` zieht auf `10.40.41.100:8006` (pve unter seiner **alten** VLAN1-Adresse, real `.20.100`), `.20.107` auf `10.40.41.10:8080`. **Das ist TP19-Material, das TP5 gerade sichtbar macht.** Weiter: `vlan20 → vlan99:8728` (MikroTik-API auf branch/ap1/ap2 = **der Management-Pfad selbst**, muss in die Whitelist, betrifft D4), `vlan20 → vlan30` (HA → Drucker `.30.5:631`, Shelly, Nuki `.30.244:8080`), `vlan10 ↔ vlan20` (Synology Drive `6690`, Apple-Continuity `udp/3722`). +- ✅ **D2 entschieden (User 2026-08-14): iPhone wird ins MAIN-SSID umgehängt.** Faktenlage war: `10.40.30.201`, dynamische Lease, Hostname `iPhone`, MAC `12:99:A6:AC:B0:CB` = iOS Private Wi-Fi Address, aktiv auf `dirtyvader-iot` mit 748 MB rx. Die statische Reservierung `10.40.10.196` „iPhone" (andere private MAC — iOS vergibt **pro SSID** eine eigene) belegt, dass es dasselbe Telefon ist. **Umhängen ist die Hands-on-Aufgabe von Christoph** (SSID am Gerät wechseln + `dirtyvader-iot` vergessen lassen); technisches Argument: eine Whitelist auf eine rotierende Private MAC wäre ohnehin instabil. - 📋 **Session-Runbook liegt (2026-08-14): `docs/tp5-deny-by-default-runbook.md`** — Ist-Stand live erhoben, **beobachtete Inter-VLAN-Flow-Matrix** (conntrack + Counter), 6 offene Entscheidungen (D1–D6), 4 Etappen mit Absicherung, Verifikation + Rollback. **Kernansatz: Etappe 1 ist ein reiner Log-Lauf ohne Blockierung** (`action=log` ans Ende von `forward`) — die Whitelist wird aus dem Log abgeleitet, nicht am Schreibtisch geraten. Drei Funde, die vorab eine Entscheidung brauchen: ein **iPhone sitzt in VLAN30**, ein **ICMP-Dauersweep von `.10.82` auf alle Shellys**, und **vlan99 ist Mitglied der `LAN`-Liste** (→ `seal-lan` erlaubt Mgmt-Input aus jedem VLAN). **VLAN50/90 tragen null Inter-VLAN-Verkehr** → Internet-only-Pinning ist dort risikofrei. - **Deny-by-default + least-privilege** (Ziel). MAIN→INFRA nur Whitelist-Dienste; MAIN↔MEDIA allow (+mDNS); UNTRUSTED/GUEST nur Internet; MGMT isoliert; Router-input gehärtet. - ⚠️ **Gerätewechsel beachten (2026-08-14 nachgetragen):** der ✅-Block unten war ursprünglich **auf ap1** verifiziert (2026-07-02) — ap1 ist seit 2026-08-08 Werksreset und reiner AP. Die Regeln sind beim Core-Cutover (TP21.1) mitgewandert, waren am neuen Gerät aber **nie gegengeprüft**. Am **2026-08-14 auf `crs326-core` live nachverifiziert** (`list_firewall_rules`, chains `forward` 13 Regeln / `input` 12 Regeln): **alle unten gelisteten Regeln existieren, sind enabled, tragen ihre `TP5:`/`TP10-seal-`-Kommentare und zählen Traffic.** Merkregel: nach jedem Rollenwechsel eines Geräts gilt eine Verifikation vom Vorgänger nicht weiter. diff --git a/docs/tp5-deny-by-default-runbook.md b/docs/tp5-deny-by-default-runbook.md index d104471..9f6c692 100644 --- a/docs/tp5-deny-by-default-runbook.md +++ b/docs/tp5-deny-by-default-runbook.md @@ -89,7 +89,7 @@ Beispiel: `INFRA→MEDIA` hat 178.142 Pakete auf dem Zähler, stand aber im Snap | # | Frage | Vorschlag | |---|---|---| | **D1** | MAIN→INFRA: welche Dienste? | Beobachtet: NAS `6690`, pve-mcp `3000`, macmini `49152`, `3722`. Vorschlag: **erst mit dem Log-Lauf (Etappe 1) vervollständigen**, dann Whitelist — nicht jetzt raten. | -| **D2** | iPhone `.30.201` in IOT | Ins MAIN-SSID holen (sauber) **oder** bewusst als Ausnahme dulden. Empfehlung: umhängen. | +| **D2** | ~~iPhone `.30.201` in IOT~~ | ✅ **ENTSCHIEDEN (2026-08-14): ins MAIN-SSID umhängen.** Hands-on bei Christoph (SSID wechseln + `dirtyvader-iot` vergessen lassen). Beleg, dass es dasselbe Telefon ist: die statische Reservierung `10.40.10.196` „iPhone" trägt eine *andere* private MAC — iOS vergibt **pro SSID** eine eigene Private Address. Genau darum wäre eine Whitelist auf `.30.201` instabil. | | **D3** | ~~ICMP-Sweep `.10.82`→IOT~~ | ✅ **ENTSCHIEDEN (2026-08-14): `lanscan.app`, Ad-hoc vom Arbeitsplatz.** Keine Dauerregel — läuft über die **JIT-Admin-Gruppe** (selbstverfallend). Deckt sich mit D4. | | **D4** | Wer darf auf VLAN99-Mgmt? | Vorschlag: nur MAIN (`vlan10`), Rest drop. `seal-lan` entsprechend verengen. | | **D5** | IOT→INFRA-Whitelist | Doku nennt MQTT 1883/8883, HA-API, Influx 8086. Beobachtet real: **`8123` (HA), `5540/udp` (Matter)**. Vorschlag: die beobachteten + MQTT. | @@ -104,19 +104,69 @@ Beispiel: `INFRA→MEDIA` hat 178.142 Pakete auf dem Zähler, stand aber im Snap > Vor **jedem** Push `/system backup` auf dem Core (Regel 3). Und: **nur die Gegenprüfung am Gerät zählt**, > Job- und Plan-Status sind kein Wirksamkeitsnachweis (zweimal belegt, TP21.1/TP22). -### Etappe 1 — Log-Lauf (blockiert nichts, Dauer: einige Tage) +#### Push-Werkzeug + Fallen (aus dem Etappe-1-Rollout, 2026-08-14 am Gerät belegt) -Eine einzige Regel **ans Ende von `forward`**, `action=log` statt `drop`: +Werkzeug: `nautobot/gc-manual-plan.py --device --commands-file --change-id [--dry-run|--deploy]`. +Es legt den manual Plan an, prüft `plan.device` gegen den Namen, gibt frei, deployt und liest +**den Plan-Status** zurück (nicht den Job-Status). Kommentar-/Leerzeilen werden abgestreift. + +1. 🔴 **Bindestriche in `name=` werden abgelehnt.** `add name="tp5-remote"` und `add name="tp5-abc"` + werfen, `add name="tp5syslog"` geht — **Quoten hilft nicht.** Das hat vier Deploys gekostet, weil die + naheliegende Erklärung („`remote=` darf nicht ins selbe `add` wie `target=remote`") *falsch* war: + `set remote=… remote-port=…` in einem Statement läuft anstandslos. Bei neuen benannten Objekten + also **Namen ohne Bindestrich** wählen, oder das Gegenteil erst beweisen. +2. 🔴 **Ein gescheiterter Netmiko-Deploy hinterlässt einen TEIL-Zustand.** Die TP22-Notiz + „gescheiterter Deploy = sauberer Fehlschlag" galt dem **NAPALM**-Weg, der scheiterte, *bevor* er das + Gerät anfasste. Netmiko schickt Zeile für Zeile und lässt Abgesetztes stehen: der erste Push hier + hinterließ Zeile 3+4 ohne 1+2. **Nach jedem Fehlschlag den Ist-Stand am Gerät prüfen.** +3. **Keine `print`-Zeilen in einen Config Plan.** Ein `/system identity print` als Kanarienzeile hat die + unmittelbar folgende Zeile verschluckt und eine Fehldiagnose erzeugt. +4. **Selbstprotokollierende Pläne sind die Diagnosemethode.** Der Deploy-Job liefert nur + `E1028: found bad/failed/failure in output` und zeigt den Geräte-Output **nicht**. Jedes Kommando in + `:do {…} on-error={ :log info "…" }` wickeln und danach eine Zählzeile + `:log info ("… count=" . [:tostr [:len [… find …]]])` setzen — anschließend per `get_log` zeilengenau + auslesen. Damit ist Root-Cause-Analyse **ohne Direkt-Write** möglich. + +### Etappe 1 — Log-Lauf (blockiert nichts, Dauer: einige Tage) ▸ ✅ **AUSGEROLLT 2026-08-14, läuft** + +Ausgerollt über Nautobot GC (manual Config Plans, `nautobot/gc-manual-plan.py`), Quelle +`routeros/tp5-etappe1-log-lauf.rsc`. Auf dem Gerät: ``` -/ip firewall filter add chain=forward action=log log-prefix="TP5-WOULD-DROP" comment="TP5: log-only, Vorstufe zum deny-by-default" +/ip firewall filter add chain=forward action=log log-prefix="TP5-WOULD-DROP" \ + comment="TP5: log-only, Vorstufe zum deny-by-default" out-interface-list="!WAN" ``` Das protokolliert exakt die Pakete, die der spätere Drop treffen würde — inklusive der Dinge, an die niemand denkt (Drucker-Discovery, Backup-Jobs, ein Gerät, das nur einmal pro Woche funkt). **Diese Liste -ist die Whitelist**, nicht eine am Schreibtisch ausgedachte. Danach D1/D3/D5 final beantworten. +ist die Whitelist**, nicht eine am Schreibtisch ausgedachte. Danach D1/D5 final beantworten. -⚠️ Log-Ziel vorher setzen (`/system logging`), sonst läuft nur der Speicher-Ringpuffer mit. +**Log-Ziel steht:** syslog-ng-Container `tp5-syslog` auf dem Docker-LXC, **`10.40.20.18:514`**, +Dateien unter `/opt/tp5-syslog/log/` (`tp5-would-drop.log` gefiltert + Host-Dateien). +Auf dem Core: Action `tp5syslog` + `logging add topics=firewall action=tp5syslog`. +Disk-Logging auf dem Core scheidet aus (**nur ~3 MB freier Flash**), der Memory-Ring (1000 Zeilen) +wickelt sich in Minuten um. `firewall` ist aus dem Memory-Ring genommen (`topics=info,!firewall`), +damit der On-Box-Log während des mehrtägigen Laufs für andere Fehlersuche brauchbar bleibt — +**am Ende von Etappe 1 auf `topics=info` zurückbauen.** +⚠️ Der LXC hat nur ~2,5 GB frei; `/usr/local/bin/tp5-log-guard.sh` (Cron, alle 10 min) kappt bei 500 MB +je Datei per Truncate-in-place. Ein vollgelaufenes Dateisystem risse dort auch adguard/cloudflared mit. + +#### 🔴 `out-interface-list="!WAN"` — warum die Regel nicht bare steht + +Bare gefasst fing sie **auch den gesamten Internet-Verkehr** (`out:vlan100`): in einer ruhigen Minute +242 Zeilen/min, davon ¼ WAN-bound, unter Last deutlich mehr. LAN→WAN ist aber eine **garantierte** +Whitelist-Zeile — ohne sie hat kein VLAN Internet, auch die „Internet-only"-VLANs 50/90 nicht. +Es wäre also Rauschen genau in der Matrix, die Etappe 2 ableiten soll. + +**Merkposten für Etappe 3:** der Drop darf aus demselben Grund **nicht** bare ans Kettenende — +`accept out-interface-list=WAN` gehört davor, sonst ist das ganze Haus offline. Diese Zeile ist +bekannt und braucht keinen Beleg aus dem Log. + +#### Auswertung + +```bash +ssh root@10.40.20.100 'pct exec 400 -- grep -o "in:vlan[0-9]* out:[a-z0-9]*" /opt/tp5-syslog/log/tp5-would-drop.log | sort | uniq -c | sort -rn' +``` ### Etappe 2 — Whitelists bauen (noch immer ohne Drop) diff --git a/nautobot/gc-manual-plan.py b/nautobot/gc-manual-plan.py new file mode 100644 index 0000000..665694e --- /dev/null +++ b/nautobot/gc-manual-plan.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +r"""Manuellen Golden-Config-Plan anlegen, freigeben und deployen. + +Der von der Governance (TP22) vorgeschriebene Weg fuer Geraeteaenderungen: +feature-/manual-scoped, **nie** "full intended" — ein Voll-Push schriebe +`<>`-Sektionen woertlich aufs Geraet (WLAN-PSKs, WG-Privkey). + + python3 nautobot/gc-manual-plan.py --device crs326-core \ + --commands-file /pfad/zu/commands.rsc --change-id "TP5-Etappe1" --dry-run + python3 nautobot/gc-manual-plan.py --device crs326-core \ + --commands-file /pfad/zu/commands.rsc --change-id "TP5-Etappe1" --deploy + +Ablauf: Job "Generate Config Plans" (plan_type=manual) -> Plan auf Approved +-> Job "Deploy Config Plans". Dazwischen wird jeweils auf den JobResult gewartet. + +ZWEI TP22-LEHREN SIND HIER EINGEBAUT +------------------------------------ +1. **Job-Status ist kein Wirksamkeitsnachweis.** Der Deploy-Job meldete schon + SUCCESS, waehrend der Plan auf Failed stand. Das Skript liest darum nach dem + Deploy den *Plan*-Status zurueck und richtet seinen Exit-Code danach. + Die eigentliche Abnahme bleibt trotzdem die Gegenpruefung am Geraet. +2. **Geraete werden einzeln per Name aufgeloest**, nie positional aus einer + Sammelabfrage. Nautobot sortiert Sammelabfragen alphabetisch; eine positionale + Zuordnung hat einen Deploy schon mal ans falsche Geraet geschickt. +""" +import argparse +import json +import os +import pathlib +import sys +import time +import urllib.error +import urllib.request + +NB = os.environ.get("NAUTOBOT_URL", "http://10.40.20.30:8080") +GEN_JOB = "f93bfa53-2332-4fa2-a6ab-5921c802fcee" # Generate Config Plans +DEPLOY_JOB = "31c3f093-eeec-43fa-ba4a-0aed2f3c67aa" # Deploy Config Plans + + +def token(): + t = os.environ.get("NAUTOBOT_TOKEN") + if not t: + raise SystemExit("NAUTOBOT_TOKEN nicht gesetzt " + "(steht in docs/nas-vlan20/stacks/210-nautobot.yml, gitignored)") + return t + + +def api(path, method="GET", payload=None): + req = urllib.request.Request( + f"{NB}{path}", + data=json.dumps(payload).encode() if payload is not None else None, + method=method, + headers={"Authorization": f"Token {token()}", + "Content-Type": "application/json", + "Accept": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=60) as resp: + raw = resp.read() + return json.loads(raw) if raw else {} + except urllib.error.HTTPError as e: + sys.stderr.write(f"HTTP {e.code} auf {method} {path}\n{e.read().decode()[:2000]}\n") + raise + + +def resolve_device(name): + """Einzeln per Name — siehe Kopfkommentar, Lehre 2.""" + res = api(f"/api/dcim/devices/?name={name}")["results"] + if len(res) != 1: + raise SystemExit(f"Device {name!r}: {len(res)} Treffer statt genau 1") + return res[0]["id"] + + +def status_id(name): + res = api(f"/api/extras/statuses/?content_types=nautobot_golden_config.configplan&name={name}")["results"] + if len(res) != 1: + raise SystemExit(f"ConfigPlan-Status {name!r}: {len(res)} Treffer statt genau 1") + return res[0]["id"] + + +def wait_job(job_result_id, label, timeout=900): + """Pollt bis der JobResult terminal ist. Gibt den finalen Status zurueck.""" + deadline = time.time() + timeout + last = None + while time.time() < deadline: + jr = api(f"/api/extras/job-results/{job_result_id}/") + st = (jr.get("status") or {}).get("value") or jr.get("status") + if st != last: + print(f" {label}: {st}") + last = st + if st in ("SUCCESS", "FAILURE", "ERRORED", "REVOKED"): + return st + time.sleep(5) + raise SystemExit(f"{label}: Timeout nach {timeout}s") + + +def job_log(job_result_id, levels=("error", "warning", "failure")): + try: + logs = api(f"/api/extras/job-log-entries/?job_result={job_result_id}&limit=200")["results"] + except Exception: + return + for e in logs: + if (e.get("log_level") or "").lower() in levels: + print(f" [{e.get('log_level')}] {e.get('message')}") + + +def main(): + p = argparse.ArgumentParser() + p.add_argument("--device", required=True) + p.add_argument("--commands-file", required=True) + p.add_argument("--change-id", default="") + p.add_argument("--change-url", default="") + p.add_argument("--deploy", action="store_true", + help="nach dem Anlegen freigeben und ausrollen") + p.add_argument("--dry-run", action="store_true", + help="nur zeigen, was gepusht wuerde — legt keinen Plan an") + args = p.parse_args() + + # Kommentar- und Leerzeilen fliegen raus: der Plan-config_set geht 1:1 per + # netmiko an die RouterOS-Konsole, dort haben Doku-Zeilen nichts verloren. + # Die kommentierte .rsc bleibt das lesbare Artefakt im Repo. + raw = pathlib.Path(args.commands_file).read_text() + commands = "\n".join(l for l in (x.rstrip() for x in raw.splitlines()) + if l.strip() and not l.lstrip().startswith("#")) + if not commands: + raise SystemExit("commands-file enthaelt keine Kommandozeilen") + + print(f" Geraet: {args.device}") + print(f" Change: {args.change_id or '(keine)'}") + print(f" Kommandos ({len(commands.splitlines())} Zeilen):") + for line in commands.splitlines(): + print(f" | {line}") + + if args.dry_run: + print("\n DRY-RUN — kein Plan angelegt, nichts gepusht.") + return + + device_id = resolve_device(args.device) + print(f"\n Device-UUID: {device_id}") + + known = {r["id"] for r in api(f"/api/plugins/golden-config/config-plan/?device_id={device_id}&limit=200")["results"]} + + print(" -> Generate Config Plans (plan_type=manual)") + run = api(f"/api/extras/jobs/{GEN_JOB}/run/", "POST", {"data": { + "device": [device_id], + "plan_type": "manual", + "commands": commands, + "change_control_id": args.change_id, + "change_control_url": args.change_url, + }}) + jr = run.get("job_result", run).get("id") + st = wait_job(jr, "generate") + job_log(jr) + if st != "SUCCESS": + raise SystemExit(f"Plan-Erzeugung fehlgeschlagen: {st}") + + plans = api(f"/api/plugins/golden-config/config-plan/?device_id={device_id}&limit=200")["results"] + new = [r for r in plans if r["id"] not in known] + if len(new) != 1: + raise SystemExit(f"Erwartet genau 1 neuen Plan, gefunden: {len(new)}") + plan = new[0] + print(f" Plan angelegt: {plan['id']}") + + # Gegenprobe, dass der Plan wirklich an diesem Geraet haengt (Lehre 2). + plan_dev = api(f"/api/plugins/golden-config/config-plan/{plan['id']}/?depth=1")["device"] + assert plan_dev["name"] == args.device, f"Plan haengt an {plan_dev['name']}, nicht an {args.device}" + print(f" Plan.device verifiziert: {plan_dev['name']}") + + if not args.deploy: + print("\n Plan liegt bereit (nicht freigegeben). Mit --deploy ausrollen.") + return + + print(" -> Plan auf Approved") + api(f"/api/plugins/golden-config/config-plan/{plan['id']}/", "PATCH", + {"status": status_id("Approved")}) + + print(" -> Deploy Config Plans") + run = api(f"/api/extras/jobs/{DEPLOY_JOB}/run/", "POST", {"data": { + "config_plan": [plan["id"]], + "fail_job_on_task_failure": True, + }}) + jr = run.get("job_result", run).get("id") + st = wait_job(jr, "deploy") + job_log(jr) + + # Lehre 1: nicht dem Job glauben, den Plan-Status lesen. + final = api(f"/api/plugins/golden-config/config-plan/{plan['id']}/?depth=1") + plan_status = (final.get("status") or {}).get("name") + print(f"\n Job-Status: {st}") + print(f" Plan-Status: {plan_status} <- der zaehlt") + if plan_status != "Completed": + raise SystemExit(f"Deploy nicht sauber durch (Plan: {plan_status})") + print("\n Plan Completed. Abnahme trotzdem erst nach Gegenpruefung am Geraet.") + + +if __name__ == "__main__": + main() diff --git a/routeros/tp5-etappe1-log-lauf.rsc b/routeros/tp5-etappe1-log-lauf.rsc new file mode 100644 index 0000000..4919783 --- /dev/null +++ b/routeros/tp5-etappe1-log-lauf.rsc @@ -0,0 +1,74 @@ +# TP5 Etappe 1 — Log-Lauf auf crs326-core. Blockiert NICHTS. +# Ausgerollt 2026-08-14 ueber Nautobot GC als manual Config Plans (Governance TP22), +# nicht per Direkt-Write. Backup davor: core-pre-tp5-etappe1-20260814.backup +# +# Diese Datei bildet den AUSGEROLLTEN Endzustand ab. Der Weg dorthin brauchte +# mehrere Anlaeufe; die Lehren stehen unten, damit Etappe 2-4 sie nicht wiederholen. +# Push: python3 nautobot/gc-manual-plan.py --device crs326-core \ +# --commands-file routeros/tp5-etappe1-log-lauf.rsc --change-id TP5-Etappe1 --deploy + +# --- 1) Syslog-Ziel --------------------------------------------------------- +# Der Core hat nur ~3 MB freien Flash -> Disk-Logging faellt aus (Platz + Flash-Verschleiss), +# und der Memory-Ring fasst 1000 Zeilen, den wickelt der Log-Lauf in Minuten um. +# Kollektor: syslog-ng auf dem Docker-LXC, 10.40.20.18:514 (Container `tp5-syslog`, +# Config /opt/tp5-syslog/config/syslog-ng.conf, Logs /opt/tp5-syslog/log/). +# ACHTUNG Name ohne Bindestrich — s. Lehre 1. +:do { /system logging action add name="tp5syslog" target=remote } on-error={ :log info "tp5: action add hat geworfen" } +:do { /system logging action set [find name="tp5syslog"] remote=10.40.20.18 remote-port=514 } on-error={ :log info "tp5: action set hat geworfen" } + +# --- 2) firewall-Topic an den Kollektor ------------------------------------- +:do { /system logging add action="tp5syslog" topics=firewall } on-error={ :log info "tp5: logging rule hat geworfen" } + +# --- 3) firewall aus dem Memory-Ring nehmen --------------------------------- +# Sonst besteht der 1000-Zeilen-Puffer tagelang zu 100 % aus TP5-Zeilen und der +# On-Box-Log ist fuer jede andere Fehlersuche wertlos — genau dann, wenn man ihn braucht. +# Matcher live geprueft: topics=info trifft NICHT (Listen-Property), topics~"info" trifft genau 1. +# Rueckbau am Ende von Etappe 1: topics=info +/system logging set [find topics~"info" and action=memory] topics=info,!firewall + +# --- 4) Die Log-Regel ans ENDE der forward-Kette ---------------------------- +# action=log ist nicht-terminierend: das Paket laeuft weiter und faellt danach aus +# der Kette in die Default-Policy (accept). Es wird nichts blockiert. +/ip firewall filter add action=log chain=forward comment="TP5: log-only, Vorstufe zum deny-by-default" log-prefix="TP5-WOULD-DROP" + +# --- 5) Einengung auf Nicht-WAN -------------------------------------------- +# Der erste Lauf zeigte, dass die Regel auch saemtlichen Internet-Verkehr faengt +# (out:vlan100) — bei 242 Zeilen/min war ein Viertel davon WAN-bound, unter Last mehr. +# LAN->WAN ist eine GARANTIERTE Whitelist-Zeile (ohne sie hat kein VLAN Internet, +# auch die "Internet-only"-VLANs 50/90 nicht), also reines Rauschen fuer die Matrix, +# die Etappe 2 ableiten soll. +# 🔴 MERKPOSTEN FUER ETAPPE 3: der spaetere drop DARF NICHT bare ans Kettenende — +# davor gehoert `accept out-interface-list=WAN`, sonst ist das ganze Haus offline. +# Diese Zeile ist bekannt und muss nicht aus dem Log wiederentdeckt werden. +/ip firewall filter set [find comment="TP5: log-only, Vorstufe zum deny-by-default"] out-interface-list="!WAN" + + +# =========================================================================== +# LEHREN AUS DEM ROLLOUT (jede am Geraet belegt, nicht vermutet) +# =========================================================================== +# +# 1) 🔴 BINDESTRICHE IN `name=` WERDEN ABGELEHNT — der teuerste Fund. +# /system logging action add name="tp5-remote" target=remote -> wirft +# /system logging action add name="tp5-abc" target=remote -> wirft +# /system logging action add name="tp5syslog" target=remote -> geht +# Quoten hilft NICHT. Isoliert per :do/on-error-Plan mit Zaehler-Checkpoints. +# Zwischenzeitlich hatte ich das faelschlich der Reihenfolge `target=`/`remote=` +# im selben add-Statement zugeschrieben — diese Erklaerung ist WIDERLEGT +# (`set remote=... remote-port=...` in einem Statement laeuft anstandslos). +# +# 2) 🔴 EIN GESCHEITERTER NETMIKO-DEPLOY HINTERLAESST EINEN TEIL-ZUSTAND. +# Die TP22-Notiz "gescheiterter Deploy = sauberer Fehlschlag" galt dem NAPALM-Weg, +# der scheiterte, BEVOR er das Geraet anfasste. Netmiko schickt Zeile fuer Zeile +# und laesst Abgesetztes stehen: der erste Push hier hinterliess Zeile 3+4 auf dem +# Geraet, ohne 1+2. **Nach jedem Fehlschlag den Geraete-Ist-Stand pruefen.** +# +# 3) KEINE `print`-ZEILEN IN EINEN CONFIG PLAN. Ein `/system identity print` als +# Kanarienzeile hat die unmittelbar folgende Zeile verschluckt und eine +# Fehldiagnose erzeugt. Config Plans enthalten nur schreibende Kommandos. +# +# 4) SELBSTPROTOKOLLIERENDE PLAENE SIND DIE DIAGNOSE-METHODE DER WAHL. +# Der Deploy-Job liefert nur "E1028: found bad/failed/failure in output" und +# zeigt den Geraete-Output NICHT. Wickelt man jedes Kommando in +# `:do {...} on-error={ :log info "..." }` und setzt danach eine Zaehlzeile +# `:log info ("... count=" . [:tostr [:len [... find ...]]])`, liest man das +# Ergebnis anschliessend per get_log zeilengenau aus — ohne Direkt-Write. From 10d93e62141bd5252ed5301095e1b55058816660 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Fri, 14 Aug 2026 18:02:51 +0200 Subject: [PATCH 2/6] TP5 Etappe 1 pausiert + VLAN1-Restliste aus dem Log-Lauf Etappe 1 ist aufgebaut und verifiziert, wird aber vor der Sammelphase pausiert (User-Entscheid): 76 % des beobachteten Inter-VLAN-Verkehrs waren .41.x-Altlasten. Eine Whitelist daraus haette nach dem Aufraeumen nicht mehr gegolten. - Log-Regel disabled=yes, Memory-Logging zurueck auf topics=info - Kollektor + Logging-Action bleiben stehen -> Wiederaufnahme = zwei Zeilen (routeros/tp5-etappe1-pause.rsc dokumentiert beide Richtungen) - D2 vollzogen: iPhone im MAIN-SSID, .30.201 im ARP auf failed TP19 bekommt die konkrete Restliste, die der Log-Lauf sichtbar gemacht hat: prometheus (.20.151) scrapet 10 tote 10.40.41.x-Ziele, .20.105 zieht auf pve unter der alten .41.100. Umzugsregel 41->20 bei gleichem letzten Oktett. Der gemeldete Timeout auf .20.100:8006 ist KEIN Firewall-/Portproblem: pveproxy lauscht auf *:8006, pve-firewall disabled, HTTP 200 in 0,06 s aus VLAN10 und VLAN20. Notiert ist auch die Fehlerquelle meines ersten Tests - `timeout` existiert auf macOS nicht und meldet falsch-negativ. Co-Authored-By: Claude Opus 5 --- docs/teilprojekte.md | 9 +++++++- docs/tp5-deny-by-default-runbook.md | 9 +++++++- routeros/tp5-etappe1-pause.rsc | 32 +++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 routeros/tp5-etappe1-pause.rsc diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 633a3ae..6a9d926 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -205,7 +205,8 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - Abh.: TP1 ✅. TP3-Abhängigkeit entfällt (WLAN läuft weiterhin über FB bis TP3). - **Ermöglicht jetzt:** PVID-Fix in Omada-Profilen (MAIN→PVID10, INFRA→PVID20), untagNetworkIds-Bereinigung, DHCP-Reservierungen für NAS/macmini/pve auf neue VLAN-Subnetze migrieren. -### TP5 — Firewall & Inter-VLAN-Policy ▸ ⏳ **ETAPPE 1 LÄUFT** (Log-Lauf seit 2026-08-14 live auf `crs326-core`) — 📋 `docs/tp5-deny-by-default-runbook.md` +### TP5 — Firewall & Inter-VLAN-Policy ▸ ⏸️ **ETAPPE 1 AUFGEBAUT, PAUSIERT** (User-Entscheid 2026-08-14) — 📋 `docs/tp5-deny-by-default-runbook.md` +- ⏸️ **Pausiert am 2026-08-14, bevor Daten gesammelt wurden.** Grund: erst die `.41.x`→`.20.x`-Altlasten aufräumen (s. TP19) — **76 % des beobachteten Inter-VLAN-Verkehrs war genau dieses Rauschen**, eine Sammelphase davor hätte eine Whitelist erzeugt, die nach dem Aufräumen nicht mehr gilt. Log-Regel steht auf `disabled=yes`, Memory-Logging zurück auf `topics=info`. **Kollektor + Logging-Action bleiben stehen** (ohne die Filter-Regel wirkungslos) → **Wiederaufnahme = zwei Zeilen**, s. `routeros/tp5-etappe1-pause.rsc`. ✅ **D2 vollzogen:** iPhone ist im MAIN-SSID, `10.40.30.201` steht im ARP auf `failed`. - ✅ **Etappe 1 ausgerollt 2026-08-14** (über Nautobot GC, manual Config Plans; Backups `core-pre-/post-tp5-etappe1-20260814.backup`). Auf dem Gerät: `forward`-Regel `action=log log-prefix="TP5-WOULD-DROP"` am Kettenende, Syslog-Action `tp5syslog` → **Kollektor `10.40.20.18:514`** (syslog-ng-Container `tp5-syslog` auf dem Docker-LXC, Logs `/opt/tp5-syslog/log/`, 10-Minuten-Cron kappt bei 500 MB — der LXC hat nur ~2,5 GB frei). `firewall` ist aus dem Memory-Ring genommen (`topics=info,!firewall`), sonst wäre der On-Box-Log tagelang unbrauchbar. **Blockiert nichts.** Artefakte: `routeros/tp5-etappe1-log-lauf.rsc`, Push-Werkzeug `nautobot/gc-manual-plan.py`. Compliance danach **16/17** (der eine Rotpunkt `ip-address` = bekannte VLAN1-Bring-up-IP `10.40.41.211` aus dem TP22-Backlog, **keine neue Drift**). - 🔴 **Fund aus dem Log-Lauf, der Etappe 3 bestimmt: die Regel fing anfangs auch allen Internet-Verkehr** (`out:vlan100`, ~¼ der Zeilen in einer ruhigen Minute, unter Last mehr). Die Log-Regel steht deshalb jetzt auf `out-interface-list="!WAN"`. **Konsequenz für den späteren Drop: er darf NICHT bare ans Kettenende** — davor gehört `accept out-interface-list=WAN`, sonst ist das ganze Haus offline. Diese Whitelist-Zeile ist damit bekannt und muss nicht aus dem Log wiederentdeckt werden. - 🔍 **Erste Befunde nach 60 s** (Rate nach Einengung ~166 Zeilen/min): **76 % des Inter-VLAN-Verkehrs ist `vlan20 → bridge`, also INFRA nach VLAN1** — ein Monitoring-Host `10.40.20.151` scrapet ~10 tote/alte `10.40.41.x`-Ziele, HA `.20.105` zieht auf `10.40.41.100:8006` (pve unter seiner **alten** VLAN1-Adresse, real `.20.100`), `.20.107` auf `10.40.41.10:8080`. **Das ist TP19-Material, das TP5 gerade sichtbar macht.** Weiter: `vlan20 → vlan99:8728` (MikroTik-API auf branch/ap1/ap2 = **der Management-Pfad selbst**, muss in die Whitelist, betrifft D4), `vlan20 → vlan30` (HA → Drucker `.30.5:631`, Shelly, Nuki `.30.244:8080`), `vlan10 ↔ vlan20` (Synology Drive `6690`, Apple-Continuity `udp/3722`). @@ -508,6 +509,12 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - Abh.: **TP10** (WG-Multi-Site = Transport, Upload-BW-Constraint), **TP14** (Container-Hosts), **TP15** (Node-HW/USV), Muster von **TP16** (reziprokes Per-Host-HA). Kein Blocker für laufende TPs. SSoT dieser Session. ### TP19 — cudy-Teardown + VLAN1-Client-Retirement ▸ ⏳ WEIT FORTGESCHRITTEN (Live-Sweep 2026-07-16: VLAN1 faktisch schon fast leer) — `docs/tp19-vlan1-cudy-teardown-runbook.md` +- 🔍 **Konkrete Restliste aus dem TP5-Log-Lauf (2026-08-14) — VLAN1 ist nicht leer, es wird noch *aktiv adressiert*.** Der Log-Lauf zeigte, dass **76 % allen Inter-VLAN-Verkehrs** aus VLAN20 nach VLAN1 (`out:bridge`) geht, obwohl dort **nichts mehr antwortet** (alle Ziele im ARP `failed`/`incomplete`). Verursacher sind Konfigurationen, die nie nachgezogen wurden: + - **`prometheus` (`10.40.20.151`, NAS)** scrapet 10 tote Ziele: `10.40.41.109:3000` · `.126:9443` · `.127:80` · `.147:5678` · `.149:3000` · `.152:3001` · `.153:9093` · `.154:9100` · `.159:6333` · `.23:80`. + - **`10.40.20.105`** zieht auf `10.40.41.100:8006` = **pve unter seiner alten VLAN1-Adresse** (real `10.40.20.100`). + - **`10.40.20.107`** auf `10.40.41.10:8080` (NAS alt). + - 📌 **Umzugsregel: das letzte Oktett bleibt gleich, nur `41`→`20`** — belegt für grafana (`.109`), vaultwarden (`.127`), Portainer (`.126`). Für `.23/.147/.149/.152/.153/.154/.159` existiert **kein** laufendes VLAN20-Gegenstück (anderswo oder tot; `alertmanager` ist seit TP14.1 als kaputt notiert) → diese Ziele gehören geprüft und dann aus Prometheus entfernt, nicht umgeschrieben. +- ⚠️ **Kein Firewall-/Portproblem** (2026-08-14 geprüft, nachdem beim Umstellen ein Timeout auftrat): `pveproxy` lauscht auf `*:8006`, `pve-firewall` ist *disabled*, keine iptables-Regel, kein `/etc/default/pveproxy`. `https://10.40.20.100:8006` antwortet aus VLAN10 **und** VLAN20 mit HTTP 200 in ~0,06 s. Wer hier einen Timeout sieht, sucht an der falschen Stelle — **und `timeout` gibt es auf macOS nicht**, ein `timeout bash -c '… /dev/tcp/…'`-Test meldet deshalb falsch-negativ. - ⚠️ **Eskaliert 2026-07-15:** der unter TP3.1 dokumentierte VLAN1-Leak tritt jetzt auch auf ap1/`dirtyvader-main` auf — die ap-seitige Interim-Härtung (TP3.1) mildert das Symptom, **die endgültige Lösung ist hier, TP19**: VLAN1 physisch weg = kein Leck mehr möglich. Das erhöht die Dringlichkeit dieses TPs, verschiebt aber keinen Scope hierher. - 🟢 **cudy-Stand (User 2026-07-16): cudy1 (`.41.246`) auf `10.40.10.246` umgehängt** — bewusst **kein Abbau-Ziel mehr**, dient als Standalone-WLAN-Sender während der Umstellungsphase (nicht Teil des Teardowns, extern vom User selbst umkonfiguriert). **cudy2 (`.41.247`) wird nicht mehr reaktiviert** — faktisch tot, ARP-Sweep bestätigt keine Präsenz mehr auf VLAN1. B2.2 („cudy1/cudy2 stromlos/Reset/ausbauen") ist damit **erledigt/gegenstandslos** (cudy2 aus, cudy1 schon migriert statt abgebaut). - 🟢 **Live-Sweep 2026-07-16 (ap1, vor geplantem Teardown):** VLAN1 ist bereits viel weiter zurückgebaut als dokumentiert: diff --git a/docs/tp5-deny-by-default-runbook.md b/docs/tp5-deny-by-default-runbook.md index 9f6c692..456f555 100644 --- a/docs/tp5-deny-by-default-runbook.md +++ b/docs/tp5-deny-by-default-runbook.md @@ -127,7 +127,14 @@ Es legt den manual Plan an, prüft `plan.device` gegen den Namen, gibt frei, dep `:log info ("… count=" . [:tostr [:len [… find …]]])` setzen — anschließend per `get_log` zeilengenau auslesen. Damit ist Root-Cause-Analyse **ohne Direkt-Write** möglich. -### Etappe 1 — Log-Lauf (blockiert nichts, Dauer: einige Tage) ▸ ✅ **AUSGEROLLT 2026-08-14, läuft** +### Etappe 1 — Log-Lauf (blockiert nichts, Dauer: einige Tage) ▸ ⏸️ **AUFGEBAUT, PAUSIERT (2026-08-14)** + +> ⏸️ **Pausiert, bevor gesammelt wurde** (User-Entscheid): erst die `.41.x`→`.20.x`-Altlasten aufräumen, +> sonst besteht die abgeleitete Whitelist zu drei Vierteln aus Verkehr, den es danach nicht mehr gibt. +> Log-Regel `disabled=yes`, Memory-Logging zurück auf `topics=info`; Kollektor und Logging-Action +> bleiben stehen. **Wiederaufnahme = zwei Zeilen + Logdatei leeren**, s. `routeros/tp5-etappe1-pause.rsc`. +> **Vorbedingung für die Wiederaufnahme:** Prometheus-Scrape-Ziele und die pve-Referenz `.41.100` +> sind umgezogen bzw. entfernt (Liste in `docs/teilprojekte.md` → TP19). Ausgerollt über Nautobot GC (manual Config Plans, `nautobot/gc-manual-plan.py`), Quelle `routeros/tp5-etappe1-log-lauf.rsc`. Auf dem Gerät: diff --git a/routeros/tp5-etappe1-pause.rsc b/routeros/tp5-etappe1-pause.rsc new file mode 100644 index 0000000..055ba40 --- /dev/null +++ b/routeros/tp5-etappe1-pause.rsc @@ -0,0 +1,32 @@ +# TP5 Etappe 1 — PAUSE (User-Entscheid 2026-08-14). +# Grund: erst die .41.x -> .20.x-Migration fertigstellen (Prometheus-Scrape-Ziele, +# HA-pve-Integration). Daten, die davor gesammelt werden, gelten danach nicht mehr — +# 76 % des Inter-VLAN-Verkehrs waren genau dieses Altlast-Rauschen. +# +# Push: python3 nautobot/gc-manual-plan.py --device crs326-core \ +# --commands-file routeros/tp5-etappe1-pause.rsc --change-id TP5-Etappe1-Pause --deploy + +# 1) Log-Regel stilllegen. Bleibt als Objekt stehen (Kommentar = Idempotenz-Schluessel), +# das Wiederanschalten ist damit eine Zeile. +:do { /ip firewall filter set [find comment="TP5: log-only, Vorstufe zum deny-by-default"] disabled=yes } on-error={ :log info "tp5p: disable hat geworfen" } + +# 2) Memory-Logging zurueckbauen. Der Ausschluss war nur wegen der Flut noetig; +# waehrend der Pause faellt keine an, und der On-Box-Log wird fuer die +# Migrationsarbeit wieder gebraucht. +:do { /system logging set [find topics~"info" and action=memory] topics=info } on-error={ :log info "tp5p: topics-restore hat geworfen" } + +:log info ("tp5p: ENDE regel-disabled=" . [:tostr [/ip firewall filter get [find comment="TP5: log-only, Vorstufe zum deny-by-default"] disabled]] . " memory-topics=" . [:tostr [/system logging get [find topics~"info" and action=memory] topics]]) + +# =========================================================================== +# WIEDERAUFNAHME (Etappe 1 fortsetzen) +# =========================================================================== +# Der Kollektor laeuft weiter (Container `tp5-syslog` auf dem Docker-LXC, +# restart=unless-stopped, 10.40.20.18:514) und die Logging-Action `tp5syslog` +# samt Regel `topics=firewall` bleibt auf dem Core stehen — beides ist ohne die +# Filter-Regel wirkungslos. Zum Fortsetzen genuegen zwei Zeilen: +# +# /ip firewall filter set [find comment="TP5: log-only, Vorstufe zum deny-by-default"] disabled=no +# /system logging set [find topics~"info" and action=memory] topics=info,!firewall +# +# Vorher aufraeumen, sonst mischt sich Altes unter die neue Sammelphase: +# ssh root@10.40.20.100 'pct exec 400 -- sh -c ": > /opt/tp5-syslog/log/tp5-would-drop.log"' From 39e92d336d8d26514e4a8ee99c50130b49e0dc73 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Sat, 15 Aug 2026 18:40:15 +0200 Subject: [PATCH 3/6] TP19: VLAN1-Adressen abgeraeumt (pve .41.100, core .41.211), Prometheus bereinigt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pve: /etc/network/interfaces vmbr0 von inet static auf inet manual, Adresse raus, angewendet per `ip addr del` ohne ifreload -a. Alle 9 Gaeste weiter running, Storage aktiv, nur noch vmbr0.20 traegt eine IP. core: 10.40.41.211 ueber Nautobot GC entfernt. Mgmt .99.1 + 10.40.10.1 intakt, Compliance 17/17 gruen — der letzte offene Drift ist damit weg. Prometheus: der pauschale 41->20-Ersatz war eine Falle. Gemessen statt angenommen — nur grafana und n8n liefern echte Metriken. gitea hat /metrics nicht aktiviert, vaultwarden gibt 404, portainer /api/status liefert JSON statt Prometheus-Format, node/alertmanager/qdrant sind exited, pihole ist abgewaehlt. 8 Jobs + alerting-Block auskommentiert, 3 Ziele aktiv und up. Zwei Korrekturen an eigenen Aussagen: - Die tote Portainer-Umgebung odroid-m1 war NICHT die Ursache des HA-Hangs. Sie ist entfernt, das Symptom blieb. Echte Ursache laut HA-Debug-Log: `docker system df` auf ep14 kommt nicht zurueck. - Auf der NAS ist prometheus.yml fuer den Login-User nicht schreibbar (65534:65534, Verzeichnis gehoert adm.christoph). Zwei `cat >`-Pushes sind still gescheitert. Schreiben nur per sudo tee + md5sum-Gegenprobe. Co-Authored-By: Claude Opus 5 --- docs/teilprojekte.md | 7 + .../vlan1-rueckbau-howto-manuelle-schritte.md | 169 ++++++++++++++++++ routeros/tp19-core-vlan1-address-remove.rsc | 21 +++ 3 files changed, 197 insertions(+) create mode 100644 docs/vlan1-rueckbau-howto-manuelle-schritte.md create mode 100644 routeros/tp19-core-vlan1-address-remove.rsc diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 6a9d926..cccab95 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -509,6 +509,13 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - Abh.: **TP10** (WG-Multi-Site = Transport, Upload-BW-Constraint), **TP14** (Container-Hosts), **TP15** (Node-HW/USV), Muster von **TP16** (reziprokes Per-Host-HA). Kein Blocker für laufende TPs. SSoT dieser Session. ### TP19 — cudy-Teardown + VLAN1-Client-Retirement ▸ ⏳ WEIT FORTGESCHRITTEN (Live-Sweep 2026-07-16: VLAN1 faktisch schon fast leer) — `docs/tp19-vlan1-cudy-teardown-runbook.md` +- ✅ **VLAN1-Adressen abgeräumt (2026-08-15)** — `docs/vlan1-rueckbau-howto-manuelle-schritte.md`: + - **pve `10.40.41.100` weg** (`/etc/network/interfaces`: `vmbr0` von `inet static` auf `inet manual`, Adresszeile raus; angewendet per `ip addr del` **ohne** `ifreload -a`, damit die Bridge nicht durchgetreten wird). Nachher nur noch `vmbr0.20`, keine `41.0/24`-Route, alle 9 Gäste weiter `running`, Storage aktiv. Backup `/root/interfaces.bak-tp19-20260815-183045`. + - **Core `10.40.41.211` weg** über Nautobot GC (`routeros/tp19-core-vlan1-address-remove.rsc`). Mgmt `.99.1` + `10.40.10.1` intakt. **Compliance 17/17 grün — der letzte offene Drift auf dem Core ist damit erledigt** (der TP22-Backlog-Punkt „`crs326-core 10.40.41.211`" entfällt). + - **HA zeigt auf `10.40.20.100`** (User), **ioBroker** korrigiert (User). + - **Prometheus bereinigt.** Der pauschale `41`→`20`-Ersatz war eine Falle: nur `grafana` (`.20.109`) und `n8n` (`.20.167:5678`) liefern echte Metriken. `gitea` hat `/metrics` gar nicht aktiviert, `vaultwarden` gibt 404, `portainer /api/status` liefert JSON statt Prometheus-Format, `node`/`alertmanager`/`qdrant` sind exited, `pihole` ist abgewählt → **8 Jobs + `alerting:`-Block auskommentiert**, 3 aktive Ziele, alle `up`. + - 🔴 **NAS-Falle:** `prometheus.yml` gehört `65534:65534`, das Verzeichnis `adm.christoph` → `cat > datei` scheitert still („Permission denied"), während ein `cp` daneben klappt. **Schreiben per `sudo tee` + `md5sum`-Gegenprobe**, nie der Shell-Erfolgsmeldung trauen. +- ⏳ **OFFEN: HA-Portainer-Integration hängt — Ursache `docker system df` auf nas-bf.** Aus dem HA-Debug-Log: `PortainerTimeoutError … GET /api/endpoints/14/docker/system/df` → `ConfigEntryNotReady` → `setup_retry`. Gemessen: ep2 0,05 s · **ep14 kam nach 180 s nicht zurück** · ep20 0,33 s · ep21 0,98 s. Lokal auf der NAS gibt `docker system df` **die Tabelle aus und beendet sich dann nicht** (rc=124 nach 120 s) — bei harmlosen Mengen (25 Images/13 GB, 10 Volumes/79 MB), also **kein Größen-, sondern ein dockerd-Problem**. Container-Listing auf demselben Endpoint: 0,16 s. **Wahrscheinlicher Fix = Container Manager auf DSM neu starten**, stoppt aber alle NAS-Container → user-gated. ⚠️ Die zuerst genannte Ursache „tote Portainer-Umgebung `odroid-m1`" ist **widerlegt** (entfernt, Symptom blieb; alle übrigen Endpoints antworten in ≤0,16 s). - 🔍 **Konkrete Restliste aus dem TP5-Log-Lauf (2026-08-14) — VLAN1 ist nicht leer, es wird noch *aktiv adressiert*.** Der Log-Lauf zeigte, dass **76 % allen Inter-VLAN-Verkehrs** aus VLAN20 nach VLAN1 (`out:bridge`) geht, obwohl dort **nichts mehr antwortet** (alle Ziele im ARP `failed`/`incomplete`). Verursacher sind Konfigurationen, die nie nachgezogen wurden: - **`prometheus` (`10.40.20.151`, NAS)** scrapet 10 tote Ziele: `10.40.41.109:3000` · `.126:9443` · `.127:80` · `.147:5678` · `.149:3000` · `.152:3001` · `.153:9093` · `.154:9100` · `.159:6333` · `.23:80`. - **`10.40.20.105`** zieht auf `10.40.41.100:8006` = **pve unter seiner alten VLAN1-Adresse** (real `10.40.20.100`). diff --git a/docs/vlan1-rueckbau-howto-manuelle-schritte.md b/docs/vlan1-rueckbau-howto-manuelle-schritte.md new file mode 100644 index 0000000..2bb8cb8 --- /dev/null +++ b/docs/vlan1-rueckbau-howto-manuelle-schritte.md @@ -0,0 +1,169 @@ +# VLAN1-Rückbau — Howto für die manuellen Schritte + +> Stand 2026-08-15. Gegenstück zum Plan „VLAN1-Rückbau: `.41.211` (Core), `.41.100` (pve), +> Prometheus-Ziele". Hier steht **nur, was du selbst machen musst** — was ich übernehmen kann, +> steht unten unter „Was ich mache". + +--- + +## Die eine Regel, die die Reihenfolge bestimmt + +`10.40.41.100` ist derzeit der **einzige lebende VLAN1-Host**, und Home Assistant erreicht pve +heute genau darüber. Dieser Pfad existiert nur, weil der Core noch die Adresse `10.40.41.211` +trägt und damit die Route zu `10.40.41.0/24` stellt. + +➡️ **Schritt 1 und 2 müssen fertig sein, bevor irgendjemand `.41.100` oder `.41.211` entfernt.** +Sonst reißt die laufende pve-Anbindung ab. + +--- + +## Schritt 1 — Home Assistant: pve-Integration auf `10.40.20.100` + +**Wo:** HA-Weboberfläche → Einstellungen → Geräte & Dienste → **Proxmox VE** (Kachel heißt heute +`10.40.41.100`) → ⋮ → **Neu konfigurieren**. Host auf `10.40.20.100` ändern, Port `8006` bleibt. + +**Warum du und nicht ich:** die Integration hat *keinen* Options-Flow (`supports_options: false`), +nur einen Reconfigure-Flow. Der fragt die Proxmox-Zugangsdaten erneut ab — und Passwörter oder +API-Token gebe ich grundsätzlich nicht ein. Das ist der Teil, der bei dir bleiben muss. + +**Das Netz ist nachweislich nicht das Problem** (falls es wieder hakt, such nicht dort): + +- `pveproxy` lauscht auf `*:8006`, `pve-firewall` ist *disabled*, keine iptables-Regel, + kein `/etc/default/pveproxy`. +- `https://10.40.20.100:8006` antwortet aus VLAN10 **und** VLAN20 mit HTTP 200 in ~0,06 s. +- HA selbst redet längst problemlos mit VLAN20 und VLAN99: `mqtt` → `10.40.20.108`, + `influxdb` → `10.40.20.8`, alle vier `mikrotik`-Integrationen auf `.99.x` sind `loaded`. + +--- + +## Schritt 2 — Portainer hängt: `docker system df` auf der NAS ⏳ OFFEN, deine Entscheidung + +> ⚠️ **Korrektur (2026-08-15):** meine erste Erklärung — die tote Umgebung `odroid-m1` — war +> **falsch**. Du hast sie entfernt, die Integration hängt weiter. Alle verbliebenen Endpoints +> antworten in 0,01–0,16 s. Die Vermutung ist damit widerlegt, nicht nur unbestätigt. + +**Die tatsächliche Ursache, aus dem HA-Debug-Log:** + +``` +pyportainer.exceptions.PortainerTimeoutError: Timeout error while accessing +GET https://10.40.20.126:9443/api/endpoints/14/docker/system/df + -> UpdateFailed -> ConfigEntryNotReady -> setup_retry +``` + +Die HA-Integration ruft beim Setup `docker system df` für **jede** Umgebung auf. Gemessen: + +| Endpoint | `docker system df` | +|---|---| +| ep2 `pve` | 0,05 s | +| **ep14 `nas-bf`** | 🔴 **kam nach 180 s nicht zurück** | +| ep20 `macmini` | 0,33 s | +| ep21 `nas-fm` | 0,98 s | + +Direkt auf der NAS gemessen (`sudo /usr/local/bin/docker system df`): der Befehl **gibt die Tabelle +aus und beendet sich dann nicht** — Exit-Code 124 nach 120 s, obwohl die Daten längst da waren. +Die Mengen sind harmlos (25 Images / 13 GB, 21 Container, 10 Volumes / 79 MB), es ist also **kein +Größenproblem**, sondern ein hängender Aufruf im dockerd der Synology. Das Container-Listing auf +demselben Endpoint dauert 0,16 s — nur `system df` klemmt. + +**Naheliegender Fix, aber deine Entscheidung:** Container Manager auf der DSM neu starten. +Das setzt den dockerd zurück, **stoppt dabei aber alle NAS-Container** (Nautobot, gitea, +vaultwarden, prometheus, grafana, pve-mcp, die cloudflared-Connectoren). Das ist nichts, was +nebenbei passieren sollte — sag, wann es dir passt, dann begleite ich es. + +**Bis dahin:** die HA-Portainer-Integration bleibt auf `setup_retry`. Sie stört nichts anderes. +Alternative ohne NAS-Neustart wäre, ep14 aus Portainer zu nehmen — das kostet aber die +NAS-Verwaltung und ist der schlechtere Tausch. + +--- + +## Schritt 3 — ioBroker: NAS-Ziel korrigieren + +**Wo:** ioBroker auf `10.40.20.107` (LXC 101 `iob-master`), in der betroffenen Adapter-Instanz. +`10.40.41.10:8080` → **`10.40.20.10:8080`** (die NAS unter ihrer aktuellen Adresse). + +Das steckt in der Adapterkonfiguration, nicht in einer Datei, die ich von außen sicher ändern kann. + +--- + +## Schritt 4 — Entscheidung: die 11 gestoppten VLAN1-LXCs + +Auf pve stehen 11 **gestoppte** Container mit VLAN1-Adressen und `gw=10.40.41.254` — das ist ap1s +abgeschaltete Sekundär-IP, dieses Gateway existiert nicht mehr: + +| LXC | Name | alte IP | +|---|---|---| +| 103 | minarca | `.41.114` | +| 104 | alpine-grafana | `.41.109` | +| 109 | node-red | `.41.119` | +| 115 | hms-mqtt-publisher | `.41.124` | +| 120 | adguard-bf1 | `.41.116` | +| 121 | adguard-bf2 | `.41.117` | +| 128 | nginx-proxy-manager | `.41.123` | +| 129 | caddy | `.41.118` | +| 130 | pihole1 | `.41.16` | +| 131 | pihole2 | `.41.131` | +| 201 | openproject | `.41.120` | + +Solange sie stehen, stören sie nichts. **Wer einen davon startet, landet in einem VLAN ohne +Gateway.** Je Container zu entscheiden: löschen, oder auf VLAN20 umziehen (`tag=20` + Adresse + +`nameserver`). Nichts davon ist dringend — es soll nur nicht aus Versehen passieren. + +Zusätzlich tragen mehrere *laufende* VLAN20-Gäste noch `nameserver: 10.40.41.254` in der +LXC-Config. Das greift erst beim nächsten Start neu, ist also eine stille Zeitbombe für später. + +--- + +## Was ich gemacht habe ✅ (2026-08-15) + +1. **pve `10.40.41.100` entfernt.** `/etc/network/interfaces`: Adresszeile raus, + `iface vmbr0 inet static` → `inet manual`; Bridge-Parameter unangetastet. Angewendet per + `ip addr del` **ohne** `ifreload -a`. Backup `/root/interfaces.bak-tp19-20260815-183045`. + Nachher: nur noch `vmbr0.20` trägt eine IP, keine `41.0/24`-Route, **alle 9 Gäste weiter + `running`**, `pvesm status` alle aktiv, SSH-Sitzung überlebt. +2. **Core `10.40.41.211` entfernt** über Nautobot GC (`routeros/tp19-core-vlan1-address-remove.rsc`), + Backup `core-pre-tp19-vlan1-addr-20260815.backup`. Nachher: keine Adresse auf `bridge`, keine + `.41`-Route, Mgmt über `.99.1` **und** `10.40.10.1` intakt. + **Compliance 17/17 grün** — der letzte offene Drift auf dem Core ist damit weg. +3. **Prometheus bereinigt.** Der pauschale `41`→`20`-Ersatz hatte 6 Jobs auf Adressen gesetzt, + die es nicht gibt. Ist-Zustand gemessen statt angenommen — **nur 2 Jobs liefern echte Metriken**: + + | Job | Befund | Ergebnis | + |---|---|---| + | `grafana` `10.40.20.109:3000` | 200, echte Metriken | ✅ aktiv | + | `n8n` `10.40.20.167:5678` | 200, echte Metriken | ✅ aktiv | + | `gitea` | `.20.157:3000/` gibt 200, aber `/metrics` ist in Gitea **nicht aktiviert** | stillgelegt | + | `vaultwarden` | `/metrics` → **404**, exportiert nativ keine Prometheus-Metriken | stillgelegt | + | `portainer` | `/api/status` → 200, liefert aber **JSON statt Prometheus-Format** | stillgelegt | + | `node`, `alertmanager`, `qdrant` | Container auf der NAS **exited** | stillgelegt | + | `pihole` | abgewählt (TP23 = AdGuard); `.20.23` ist ein fremder Dienst | stillgelegt | + + Alle acht **auskommentiert mit Begründung im File**, dazu der `alerting:`-Block (sonst bliebe ein + leerer alertmanagers-Eintrag stehen, der Schärfe vortäuscht). Backup + `prometheus.yml.bak-20260815`. Nach Reload: **3 aktive Ziele, alle `up`**. + + 🔴 **Falle für das nächste Mal — die Datei ist nicht mit dem Login-User schreibbar.** + `prometheus.yml` gehört `65534:65534`; das *Verzeichnis* gehört `adm.christoph`. Ein + `cat > datei` scheitert an „Permission denied", während ein `cp` daneben klappt — was leicht den + Eindruck erweckt, der Schreibvorgang sei durchgegangen. **Schreiben nur per `sudo tee`, und + danach per `md5sum` gegen die lokale Fassung prüfen**, nicht der Erfolgsmeldung der Shell trauen. + +--- + +## Abnahme + +- pve: `ip -4 addr show vmbr0` ohne `.41`, `ip route` ohne `41.0/24`, `pct list` weiter 9× running. +- Core: keine `.41`-Adresse, Mgmt über `.99.1` **und** `10.40.10.1` erreichbar. +- Prometheus: `http://10.40.20.151:9090/api/v1/targets` — alle aktiven Ziele `up`, kein `10.40.41.x`. +- HA: `proxmoxve` und `portainer` beide auf `loaded`. +- **Der eigentliche Test:** TP5-Log-Regel kurz einschalten, 5 Minuten laufen lassen, `out:bridge` + muss auf **0 Zeilen** stehen. Vorher waren das 76 % des gesamten Inter-VLAN-Verkehrs. + Danach ist TP5 Etappe 1 sauber neu startbar. + +--- + +## Nebenbefund + +Im HA-Log stehen 79 abgelehnte Anmeldeversuche auf `/api/websocket` von `90.146.33.177` — +das ist **eure eigene öffentliche IP**, der Verkehr kommt also über den CF-Tunnel herein. +Vermutlich der ha-mcp-Server mit einem alten Token vor dem Neustart. Wenn die Zahl jetzt nicht +weiter steigt, war es genau das; wenn doch, lohnt ein zweiter Blick. diff --git a/routeros/tp19-core-vlan1-address-remove.rsc b/routeros/tp19-core-vlan1-address-remove.rsc new file mode 100644 index 0000000..3e349e8 --- /dev/null +++ b/routeros/tp19-core-vlan1-address-remove.rsc @@ -0,0 +1,21 @@ +# TP19 — VLAN1-Bring-up-Adresse vom Core entfernen. +# Ausgerollt 2026-08-15 ueber Nautobot GC (manual Config Plan), Backup davor: +# core-pre-tp19-vlan1-addr-20260815.backup +# +# `10.40.41.211/24` auf `bridge` (Kommentar TP15-bringup-vlan1) war die letzte +# .41-Spur auf dem Core und der letzte offene Compliance-Drift (ip-address). +# Vorher geprueft: keine DNS-Statics, kein DHCP-Server auf `bridge`, kein Netwatch, +# keine Address-Lists mit .41; `bridge` ist NICHT Mitglied der LAN-Interface-Liste. +# Mgmt laeuft ueber .99.1 (+ Fallback 10.40.10.1), beides unberuehrt. +# +# ERWARTETE NEBENWIRKUNG: die dynamische Connected-Route 10.40.41.0/24 verschwindet +# mit der Adresse. Das ist gewollt — Restverkehr nach VLAN1 scheitert danach sofort +# statt in einen Timeout zu laufen. +# +# Reihenfolge war bindend und ist erfuellt: HA zeigt auf 10.40.20.100 (Integration +# `loaded`), pve hat 10.40.41.100 nicht mehr, Prometheus/ioBroker sind umgezogen. +# +# Selbstprotokollierend — der Deploy-Job zeigt den Geraete-Output nicht (E1028). + +:do { /ip address remove [find address="10.40.41.211/24"] } on-error={ :log info "tp19: remove hat geworfen" } +:log info ("tp19: ENDE rest-41-adressen=" . [:tostr [:len [/ip address find address~"10.40.41."]]] . " rest-41-routen=" . [:tostr [:len [/ip route find dst-address~"10.40.41."]]]) From 49f2bfd98f560c2cd576f81367509011251f384f Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Sat, 15 Aug 2026 18:57:04 +0200 Subject: [PATCH 4/6] =?UTF-8?q?NAS:=20Container-Manager-Neustart=20?= =?UTF-8?q?=E2=80=94=20Haenger=20weg,=20Portainer-Integration=20bleibt=20o?= =?UTF-8?q?ffen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit synopkg stop/start ContainerManager auf nas-bf. Alle 14 Container aus der Baseline sind zurueck, zusaetzlich kam hms-mqtt hoch (Restart-Policy, lief vorher nicht). Der Neustart hat den Haenger behoben: `docker system df` beendet sich wieder (vorher rc=124 nach Ausgabe der Tabelle). Er reicht aber nicht — der Aufruf bleibt mit 33s bis ueber 120s weit ueber dem pyportainer-Timeout, die HA-Integration bleibt auf setup_retry. Gemessen, wo die Zeit hingeht: images -q 84ms, ps -aq 95ms, volume ls 65ms, system df 33-92+s. Also vollstaendig in der Groessenberechnung (Durchlauf der overlay2-/Volume-Verzeichnisse auf NAS-Platten) — Plattformnachteil, kein Defekt. Aussichtsreichster Weg dokumentiert: eigener Portainer-Benutzer fuer HA ohne Zugriff auf ep14. Zwei widerlegte Zwischenthesen sind als solche festgehalten (odroid-m1 war es nicht; es ist kein Groessenproblem). Co-Authored-By: Claude Opus 5 --- docs/teilprojekte.md | 7 ++- .../vlan1-rueckbau-howto-manuelle-schritte.md | 48 ++++++++++++++++--- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index cccab95..f27c1ee 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -515,7 +515,12 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - **HA zeigt auf `10.40.20.100`** (User), **ioBroker** korrigiert (User). - **Prometheus bereinigt.** Der pauschale `41`→`20`-Ersatz war eine Falle: nur `grafana` (`.20.109`) und `n8n` (`.20.167:5678`) liefern echte Metriken. `gitea` hat `/metrics` gar nicht aktiviert, `vaultwarden` gibt 404, `portainer /api/status` liefert JSON statt Prometheus-Format, `node`/`alertmanager`/`qdrant` sind exited, `pihole` ist abgewählt → **8 Jobs + `alerting:`-Block auskommentiert**, 3 aktive Ziele, alle `up`. - 🔴 **NAS-Falle:** `prometheus.yml` gehört `65534:65534`, das Verzeichnis `adm.christoph` → `cat > datei` scheitert still („Permission denied"), während ein `cp` daneben klappt. **Schreiben per `sudo tee` + `md5sum`-Gegenprobe**, nie der Shell-Erfolgsmeldung trauen. -- ⏳ **OFFEN: HA-Portainer-Integration hängt — Ursache `docker system df` auf nas-bf.** Aus dem HA-Debug-Log: `PortainerTimeoutError … GET /api/endpoints/14/docker/system/df` → `ConfigEntryNotReady` → `setup_retry`. Gemessen: ep2 0,05 s · **ep14 kam nach 180 s nicht zurück** · ep20 0,33 s · ep21 0,98 s. Lokal auf der NAS gibt `docker system df` **die Tabelle aus und beendet sich dann nicht** (rc=124 nach 120 s) — bei harmlosen Mengen (25 Images/13 GB, 10 Volumes/79 MB), also **kein Größen-, sondern ein dockerd-Problem**. Container-Listing auf demselben Endpoint: 0,16 s. **Wahrscheinlicher Fix = Container Manager auf DSM neu starten**, stoppt aber alle NAS-Container → user-gated. ⚠️ Die zuerst genannte Ursache „tote Portainer-Umgebung `odroid-m1`" ist **widerlegt** (entfernt, Symptom blieb; alle übrigen Endpoints antworten in ≤0,16 s). +- ⏳ **OFFEN: HA-Portainer-Integration bleibt auf `setup_retry` — `docker system df` auf nas-bf ist zu langsam.** Aus dem HA-Debug-Log: `PortainerTimeoutError … GET /api/endpoints/14/docker/system/df` → `ConfigEntryNotReady`. Die Integration ruft `system df` für **jede** Umgebung bei **jedem** Update auf. + - **Container-Manager-Neustart am 2026-08-15 durchgeführt** (`/usr/syno/bin/synopkg stop|start ContainerManager` — Binary **nicht** im sudo-PATH; DSM meldet den Status danach weiterhin falsch als `stop`). **Alle 14 Container aus der Baseline zurück**, zusätzlich kam `hms-mqtt` hoch (Restart-Policy, lief vorher nicht → prüfen). + - ✅ Der **Hänger** ist damit weg: `system df` beendet sich wieder (vorher rc=124 nach Ausgabe der Tabelle). ❌ **Aber es bleibt viel zu langsam und schwankt: 33 s → 92 s → 3× >120 s.** + - 🔑 **Wo die Zeit hingeht (gemessen):** `images -q` 84 ms · `ps -aq` 95 ms · `volume ls -q` 65 ms · **`system df` 33–92+ s**. Also **vollständig in der Größenberechnung** (du-artiger Durchlauf der overlay2-/Volume-Verzeichnisse auf NAS-Platten) — ein struktureller Plattformnachteil, kein Defekt. Die Mengen sind harmlos (25 Images/13 GB, 10 Volumes/79 MB). + - **Aussichtsreichster Weg: eigener Portainer-Benutzer für HA ohne Zugriff auf ep14** → die Integration sieht die langsame Umgebung gar nicht. User-gated (Benutzer + Token anlegen). Alternativen: nichts tun (stört nichts) · Images aufräumen (6,98 GB reclaimable, Erfolg **offen**) · ep14 aus Portainer nehmen (schlechter Tausch). + - ⚠️ **Zwei widerlegte Zwischenthesen, nicht wieder aufgreifen:** die tote Umgebung `odroid-m1` war es **nicht** (entfernt, Symptom blieb, alle übrigen Endpoints ≤0,16 s); und es ist **kein** Größenproblem der Images. - 🔍 **Konkrete Restliste aus dem TP5-Log-Lauf (2026-08-14) — VLAN1 ist nicht leer, es wird noch *aktiv adressiert*.** Der Log-Lauf zeigte, dass **76 % allen Inter-VLAN-Verkehrs** aus VLAN20 nach VLAN1 (`out:bridge`) geht, obwohl dort **nichts mehr antwortet** (alle Ziele im ARP `failed`/`incomplete`). Verursacher sind Konfigurationen, die nie nachgezogen wurden: - **`prometheus` (`10.40.20.151`, NAS)** scrapet 10 tote Ziele: `10.40.41.109:3000` · `.126:9443` · `.127:80` · `.147:5678` · `.149:3000` · `.152:3001` · `.153:9093` · `.154:9100` · `.159:6333` · `.23:80`. - **`10.40.20.105`** zieht auf `10.40.41.100:8006` = **pve unter seiner alten VLAN1-Adresse** (real `10.40.20.100`). diff --git a/docs/vlan1-rueckbau-howto-manuelle-schritte.md b/docs/vlan1-rueckbau-howto-manuelle-schritte.md index 2bb8cb8..fccf99f 100644 --- a/docs/vlan1-rueckbau-howto-manuelle-schritte.md +++ b/docs/vlan1-rueckbau-howto-manuelle-schritte.md @@ -65,14 +65,48 @@ Die Mengen sind harmlos (25 Images / 13 GB, 21 Container, 10 Volumes / 79 MB), e Größenproblem**, sondern ein hängender Aufruf im dockerd der Synology. Das Container-Listing auf demselben Endpoint dauert 0,16 s — nur `system df` klemmt. -**Naheliegender Fix, aber deine Entscheidung:** Container Manager auf der DSM neu starten. -Das setzt den dockerd zurück, **stoppt dabei aber alle NAS-Container** (Nautobot, gitea, -vaultwarden, prometheus, grafana, pve-mcp, die cloudflared-Connectoren). Das ist nichts, was -nebenbei passieren sollte — sag, wann es dir passt, dann begleite ich es. +### Container-Manager-Neustart durchgeführt (2026-08-15) — hat den Hänger behoben, reicht aber nicht -**Bis dahin:** die HA-Portainer-Integration bleibt auf `setup_retry`. Sie stört nichts anderes. -Alternative ohne NAS-Neustart wäre, ep14 aus Portainer zu nehmen — das kostet aber die -NAS-Verwaltung und ist der schlechtere Tausch. +`synopkg stop/start ContainerManager` (Binary liegt unter `/usr/syno/bin/synopkg`, **nicht** im +sudo-PATH). Ergebnis: + +- ✅ **Alle 14 Container aus der Baseline sind zurück**, keiner fehlt. Zusätzlich kam `hms-mqtt` + hoch, der vorher nicht lief (Restart-Policy) — **prüfen, ob der bleiben soll**. +- ✅ `docker system df` **beendet sich wieder** (vorher: gab die Tabelle aus und hing dann; rc=124). +- ❌ **Aber es ist weiterhin viel zu langsam und schwankt stark:** 33 s → 92 s → 3× über 120 s. + `pyportainer` gibt lange vorher auf, die Integration bleibt auf `setup_retry`. + +**Wo die Zeit hingeht — gemessen, nicht vermutet:** + +| Aufruf | Dauer | +|---|---| +| `docker images -q` | 84 ms | +| `docker ps -aq` | 95 ms | +| `docker volume ls -q` | 65 ms | +| **`docker system df`** | **33–92+ s** | + +Das Auflisten ist trivial schnell; die Zeit geht **vollständig in die Größenberechnung**, also das +`du`-artige Durchlaufen der overlay2-/Volume-Verzeichnisse auf den NAS-Platten. Das ist ein +struktureller Nachteil dieser Plattform, kein Defekt — und die HA-Integration ruft `system df` +für **jede** Umgebung bei **jedem** Update auf. + +### Der aussichtsreichste Weg: eigener Portainer-Benutzer für HA + +Portainer kann Umgebungszugriff pro Benutzer beschränken. Ein HA-eigener Benutzer mit Zugriff auf +**ep2, ep20, ep21 — aber nicht ep14** löst das Problem an der Wurzel: die Integration sieht die +langsame NAS-Umgebung dann gar nicht. + +**Wo:** Portainer → Users → neuen Benutzer anlegen → Environments → Zugriff auf ep14 entziehen → +Access-Token erzeugen → in HA die Portainer-Integration mit diesem Token neu anlegen. + +**Warum du und nicht ich:** Benutzer anlegen und Token erzeugen sind Konto- und Credential-Schritte, +die ich grundsätzlich nicht übernehme. + +**Alternativen, ehrlich bewertet:** +- *Nichts tun* — die Integration bleibt auf `setup_retry`, stört sonst nichts. Völlig vertretbar. +- *Images aufräumen* (6,98 GB / 53 % sind reclaimable) — verkürzt den Durchlauf, aber + ob es unter das `pyportainer`-Limit kommt, ist **offen**. Ich würde nicht darauf setzen. +- *ep14 ganz aus Portainer nehmen* — kostet die NAS-Verwaltung, schlechter Tausch. --- From 415be1ed241320497f0de8c5b9e5e2d83aa553d1 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Sun, 16 Aug 2026 17:34:06 +0200 Subject: [PATCH 5/6] TP5 Etappe 1: Sammelphase laeuft, VLAN1-Rueckbau abgenommen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abnahme bestanden: out:bridge = 0 ueber ein 10-Minuten-Fenster (Logdatei vorher geleert). Vorher waren vlan20 -> bridge 76 % des gesamten Inter-VLAN-Verkehrs; die Rate fiel von ~166 auf ~34 Zeilen/min. Beobachtete Matrix (339 Zeilen / 10 min) als Arbeitsmaterial fuer Etappe 2 festgehalten. Zwei Befunde daraus: - D4 ist belegt statt geschaetzt: der Management-Pfad ist HA (10.40.20.105), das ueber seine mikrotik-Integrationen 8728 auf branch/ap1 pollt (je 67x). Ohne Whitelist sperrt der spaetere Drop die Netzverwaltung aus. - Neuer Merkposten: same-VLAN-Verkehr erreicht die forward-Kette — vlan10 -> vlan10 unicast mDNS, erzeugt vom mDNS-Repeater auf dem Core. Harmlos, aber ein blanker Drop wuerde Discovery innerhalb von MAIN beschaedigen. Damit sind drei Whitelist-Zeilen bekannt, bevor Etappe 2 beginnt: LAN->WAN, der Mgmt-Pfad, und same-VLAN-mDNS. Co-Authored-By: Claude Opus 5 --- docs/teilprojekte.md | 17 +++++++++++++++-- docs/tp5-deny-by-default-runbook.md | 21 ++++++++++++++------- routeros/tp5-etappe1-resume.rsc | 26 ++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 routeros/tp5-etappe1-resume.rsc diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index f27c1ee..23a47ab 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -205,8 +205,21 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - Abh.: TP1 ✅. TP3-Abhängigkeit entfällt (WLAN läuft weiterhin über FB bis TP3). - **Ermöglicht jetzt:** PVID-Fix in Omada-Profilen (MAIN→PVID10, INFRA→PVID20), untagNetworkIds-Bereinigung, DHCP-Reservierungen für NAS/macmini/pve auf neue VLAN-Subnetze migrieren. -### TP5 — Firewall & Inter-VLAN-Policy ▸ ⏸️ **ETAPPE 1 AUFGEBAUT, PAUSIERT** (User-Entscheid 2026-08-14) — 📋 `docs/tp5-deny-by-default-runbook.md` -- ⏸️ **Pausiert am 2026-08-14, bevor Daten gesammelt wurden.** Grund: erst die `.41.x`→`.20.x`-Altlasten aufräumen (s. TP19) — **76 % des beobachteten Inter-VLAN-Verkehrs war genau dieses Rauschen**, eine Sammelphase davor hätte eine Whitelist erzeugt, die nach dem Aufräumen nicht mehr gilt. Log-Regel steht auf `disabled=yes`, Memory-Logging zurück auf `topics=info`. **Kollektor + Logging-Action bleiben stehen** (ohne die Filter-Regel wirkungslos) → **Wiederaufnahme = zwei Zeilen**, s. `routeros/tp5-etappe1-pause.rsc`. ✅ **D2 vollzogen:** iPhone ist im MAIN-SSID, `10.40.30.201` steht im ARP auf `failed`. +### TP5 — Firewall & Inter-VLAN-Policy ▸ 🟢 **ETAPPE 1: SAMMELPHASE LÄUFT** (seit 2026-08-16) — 📋 `docs/tp5-deny-by-default-runbook.md` +- ✅ **ABNAHME DES VLAN1-RÜCKBAUS BESTANDEN (2026-08-16): `out:bridge` = 0.** 10-Minuten-Messfenster nach dem Wiederanschalten, Logdatei vorher geleert. Vorher waren `vlan20 → bridge` **76 %** des gesamten Inter-VLAN-Verkehrs. Die Rate fiel von ~166 auf ~34 Zeilen/min — der Rückbau hat das Log auf echten Nutzverkehr eingedampft. Wiederaufnahme über `routeros/tp5-etappe1-resume.rsc` (GC, Backup `core-pre-tp5-resume-20260815.backup`). +- 📊 **Beobachtete Matrix, 339 Zeilen / 10 min** — das Arbeitsmaterial für Etappe 2: + + | Paar | Zeilen | was genau | + |---|---|---| + | `vlan20 → vlan99` | 126 | **HA `10.40.20.105` → `10.40.99.2:8728` + `10.40.99.11:8728`** (je 67) = der Management-Pfad | + | `vlan20 → vlan30` | 85 | HA → IoT: `.30.211:80` (44), `.30.8:10081` (22), `.30.244:8080` Nuki (22), `.30.5:631` Drucker (4) | + | `vlan10 → vlan20` | 65 | Arbeitsplatz → INFRA | + | `vlan20 → vlan10` | 61 | INFRA → MAIN | + | `vlan10 → vlan10` | 2 | **mDNS `10.40.10.183:5353 → 10.40.10.82:5353`** — s.u. | + +- 🔑 **D4 ist damit belegt, nicht mehr geschätzt:** der Management-Pfad ist **HA** (`10.40.20.105`), das über seine vier `mikrotik`-Integrationen `8728` auf branch/ap1/ap2 pollt (core `.99.1` erscheint nicht, weil das `input` ist, nicht `forward`). Dazu MikroMCP vom Arbeitsplatz. **Ohne Whitelist sperrt der spätere Drop die Verwaltung des eigenen Netzes aus.** +- ⚠️ **Neuer Merkposten für Etappe 3 — same-VLAN-Verkehr erreicht die `forward`-Kette:** `vlan10 → vlan10`, unicast **mDNS** zwischen zwei MAIN-Clients, erzeugt vom mDNS-Repeater auf dem Core. Zwei Pakete in 10 Minuten, harmlos — **aber ein blanker Drop am Kettenende würde sie treffen** und damit Discovery *innerhalb* von MAIN beschädigen. Zusammen mit `accept out-interface-list=WAN` die zweite Regel, die bekannt ist und nicht aus dem Log wiederentdeckt werden muss. +- ✅ **D2 vollzogen:** iPhone ist im MAIN-SSID, `10.40.30.201` steht im ARP auf `failed`. - ✅ **Etappe 1 ausgerollt 2026-08-14** (über Nautobot GC, manual Config Plans; Backups `core-pre-/post-tp5-etappe1-20260814.backup`). Auf dem Gerät: `forward`-Regel `action=log log-prefix="TP5-WOULD-DROP"` am Kettenende, Syslog-Action `tp5syslog` → **Kollektor `10.40.20.18:514`** (syslog-ng-Container `tp5-syslog` auf dem Docker-LXC, Logs `/opt/tp5-syslog/log/`, 10-Minuten-Cron kappt bei 500 MB — der LXC hat nur ~2,5 GB frei). `firewall` ist aus dem Memory-Ring genommen (`topics=info,!firewall`), sonst wäre der On-Box-Log tagelang unbrauchbar. **Blockiert nichts.** Artefakte: `routeros/tp5-etappe1-log-lauf.rsc`, Push-Werkzeug `nautobot/gc-manual-plan.py`. Compliance danach **16/17** (der eine Rotpunkt `ip-address` = bekannte VLAN1-Bring-up-IP `10.40.41.211` aus dem TP22-Backlog, **keine neue Drift**). - 🔴 **Fund aus dem Log-Lauf, der Etappe 3 bestimmt: die Regel fing anfangs auch allen Internet-Verkehr** (`out:vlan100`, ~¼ der Zeilen in einer ruhigen Minute, unter Last mehr). Die Log-Regel steht deshalb jetzt auf `out-interface-list="!WAN"`. **Konsequenz für den späteren Drop: er darf NICHT bare ans Kettenende** — davor gehört `accept out-interface-list=WAN`, sonst ist das ganze Haus offline. Diese Whitelist-Zeile ist damit bekannt und muss nicht aus dem Log wiederentdeckt werden. - 🔍 **Erste Befunde nach 60 s** (Rate nach Einengung ~166 Zeilen/min): **76 % des Inter-VLAN-Verkehrs ist `vlan20 → bridge`, also INFRA nach VLAN1** — ein Monitoring-Host `10.40.20.151` scrapet ~10 tote/alte `10.40.41.x`-Ziele, HA `.20.105` zieht auf `10.40.41.100:8006` (pve unter seiner **alten** VLAN1-Adresse, real `.20.100`), `.20.107` auf `10.40.41.10:8080`. **Das ist TP19-Material, das TP5 gerade sichtbar macht.** Weiter: `vlan20 → vlan99:8728` (MikroTik-API auf branch/ap1/ap2 = **der Management-Pfad selbst**, muss in die Whitelist, betrifft D4), `vlan20 → vlan30` (HA → Drucker `.30.5:631`, Shelly, Nuki `.30.244:8080`), `vlan10 ↔ vlan20` (Synology Drive `6690`, Apple-Continuity `udp/3722`). diff --git a/docs/tp5-deny-by-default-runbook.md b/docs/tp5-deny-by-default-runbook.md index 456f555..8064bd5 100644 --- a/docs/tp5-deny-by-default-runbook.md +++ b/docs/tp5-deny-by-default-runbook.md @@ -127,14 +127,21 @@ Es legt den manual Plan an, prüft `plan.device` gegen den Namen, gibt frei, dep `:log info ("… count=" . [:tostr [:len [… find …]]])` setzen — anschließend per `get_log` zeilengenau auslesen. Damit ist Root-Cause-Analyse **ohne Direkt-Write** möglich. -### Etappe 1 — Log-Lauf (blockiert nichts, Dauer: einige Tage) ▸ ⏸️ **AUFGEBAUT, PAUSIERT (2026-08-14)** +### Etappe 1 — Log-Lauf (blockiert nichts, Dauer: einige Tage) ▸ 🟢 **SAMMELPHASE LÄUFT (seit 2026-08-16)** -> ⏸️ **Pausiert, bevor gesammelt wurde** (User-Entscheid): erst die `.41.x`→`.20.x`-Altlasten aufräumen, -> sonst besteht die abgeleitete Whitelist zu drei Vierteln aus Verkehr, den es danach nicht mehr gibt. -> Log-Regel `disabled=yes`, Memory-Logging zurück auf `topics=info`; Kollektor und Logging-Action -> bleiben stehen. **Wiederaufnahme = zwei Zeilen + Logdatei leeren**, s. `routeros/tp5-etappe1-pause.rsc`. -> **Vorbedingung für die Wiederaufnahme:** Prometheus-Scrape-Ziele und die pve-Referenz `.41.100` -> sind umgezogen bzw. entfernt (Liste in `docs/teilprojekte.md` → TP19). +> Historie: am 2026-08-14 **vor** der Sammelphase pausiert, weil 76 % des Verkehrs `.41.x`-Altlast war. +> Nach dem VLAN1-Rückbau (TP19) am 2026-08-16 wieder scharf — `routeros/tp5-etappe1-resume.rsc`. +> +> ✅ **Abnahme bestanden: `out:bridge` = 0** über ein 10-Minuten-Fenster (Logdatei vorher geleert). +> Rate von ~166 auf ~34 Zeilen/min gefallen. Die beobachtete Matrix steht in +> `docs/teilprojekte.md` → TP5 und ist ab jetzt das Arbeitsmaterial für Etappe 2. +> +> **Zwei Regeln stehen damit schon fest und brauchen keinen weiteren Beleg aus dem Log:** +> 1. `accept out-interface-list=WAN` — ohne sie nimmt der Drop das ganze Haus vom Netz. +> 2. Der **Management-Pfad** `10.40.20.105 → 10.40.99.{2,11,12}:8728` (HA-`mikrotik`-Integrationen) +> plus MikroMCP vom Arbeitsplatz — sonst sperrt der Drop die Netzverwaltung aus. +> 3. **Same-VLAN-mDNS** (`vlan10 → vlan10`, `5353`, vom Repeater auf dem Core erzeugt) erreicht die +> `forward`-Kette und würde von einem blanken Drop getroffen. Ausgerollt über Nautobot GC (manual Config Plans, `nautobot/gc-manual-plan.py`), Quelle `routeros/tp5-etappe1-log-lauf.rsc`. Auf dem Gerät: diff --git a/routeros/tp5-etappe1-resume.rsc b/routeros/tp5-etappe1-resume.rsc new file mode 100644 index 0000000..11cc233 --- /dev/null +++ b/routeros/tp5-etappe1-resume.rsc @@ -0,0 +1,26 @@ +# TP5 Etappe 1 — WIEDERAUFNAHME nach der Pause vom 2026-08-14. +# Gegenstueck zu routeros/tp5-etappe1-pause.rsc. +# +# Der Grund fuer die Pause ist weg: die .41.x-Altlasten sind abgeraeumt +# (HA -> 10.40.20.100, pve .41.100 entfernt, Core .41.211 entfernt, +# Prometheus bereinigt, ioBroker korrigiert). Vorher waren 76 % des +# Inter-VLAN-Verkehrs `vlan20 -> bridge`, also genau dieses Rauschen. +# +# Push: python3 nautobot/gc-manual-plan.py --device crs326-core \ +# --commands-file routeros/tp5-etappe1-resume.rsc \ +# --change-id TP5-Etappe1-Resume --deploy +# Backup davor: core-pre-tp5-resume-20260815.backup +# +# ABNAHME danach: `out:bridge` im Kollektor-Log muss auf 0 stehen. +# Die Logdatei ist vor dem Scharfschalten geleert worden. + +# 1) Log-Regel wieder scharf. Blockiert nichts: action=log ist nicht-terminierend, +# und out-interface-list="!WAN" haelt den Internet-Verkehr draussen. +:do { /ip firewall filter set [find comment="TP5: log-only, Vorstufe zum deny-by-default"] disabled=no } on-error={ :log info "tp5r: enable hat geworfen" } + +# 2) firewall wieder aus dem Memory-Ring nehmen. Sonst besteht der 1000-Zeilen-Puffer +# waehrend der Sammelphase zu 100 % aus TP5-Zeilen und der On-Box-Log ist fuer +# jede andere Fehlersuche wertlos — genau dann, wenn man ihn braucht. +:do { /system logging set [find topics~"info" and action=memory] topics=info,!firewall } on-error={ :log info "tp5r: topics hat geworfen" } + +:log info ("tp5r: ENDE disabled=" . [:tostr [/ip firewall filter get [find comment="TP5: log-only, Vorstufe zum deny-by-default"] disabled]] . " memory-topics=" . [:tostr [/system logging get [find topics~"info" and action=memory] topics]]) From 03963215604572de5310be0ba3871d98f5c08a82 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Sun, 16 Aug 2026 17:39:25 +0200 Subject: [PATCH 6/6] NAS: hms-mqtt stillgelegt (restart=always -> no) Der Container war bereits beendet; entscheidend war die Restart-Policy. Mit `always` waere er beim naechsten Daemon-Neustart wiedergekommen, ein blosses `docker stop` haette also nicht gehalten. Nebenbefund, nicht von mir ausgeloest: grafana, drawio-server und hms-mqtt wurden am 2026-08-16 um 17:14:54-17:15:06 CEST innerhalb von zwoelf Sekunden per Signal beendet (grafana exit 0 mit "System signal: terminated", drawio-server 143, hms-mqtt 137). Kein OOM, Speicher frei, restarts=0. Folge: das Prometheus-Ziel grafana steht auf down, n8n und prometheus weiter up. Co-Authored-By: Claude Opus 5 --- docs/teilprojekte.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 23a47ab..8ddae3d 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -529,7 +529,8 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". - **Prometheus bereinigt.** Der pauschale `41`→`20`-Ersatz war eine Falle: nur `grafana` (`.20.109`) und `n8n` (`.20.167:5678`) liefern echte Metriken. `gitea` hat `/metrics` gar nicht aktiviert, `vaultwarden` gibt 404, `portainer /api/status` liefert JSON statt Prometheus-Format, `node`/`alertmanager`/`qdrant` sind exited, `pihole` ist abgewählt → **8 Jobs + `alerting:`-Block auskommentiert**, 3 aktive Ziele, alle `up`. - 🔴 **NAS-Falle:** `prometheus.yml` gehört `65534:65534`, das Verzeichnis `adm.christoph` → `cat > datei` scheitert still („Permission denied"), während ein `cp` daneben klappt. **Schreiben per `sudo tee` + `md5sum`-Gegenprobe**, nie der Shell-Erfolgsmeldung trauen. - ⏳ **OFFEN: HA-Portainer-Integration bleibt auf `setup_retry` — `docker system df` auf nas-bf ist zu langsam.** Aus dem HA-Debug-Log: `PortainerTimeoutError … GET /api/endpoints/14/docker/system/df` → `ConfigEntryNotReady`. Die Integration ruft `system df` für **jede** Umgebung bei **jedem** Update auf. - - **Container-Manager-Neustart am 2026-08-15 durchgeführt** (`/usr/syno/bin/synopkg stop|start ContainerManager` — Binary **nicht** im sudo-PATH; DSM meldet den Status danach weiterhin falsch als `stop`). **Alle 14 Container aus der Baseline zurück**, zusätzlich kam `hms-mqtt` hoch (Restart-Policy, lief vorher nicht → prüfen). + - **Container-Manager-Neustart am 2026-08-15 durchgeführt** (`/usr/syno/bin/synopkg stop|start ContainerManager` — Binary **nicht** im sudo-PATH; DSM meldet den Status danach weiterhin falsch als `stop`). **Alle 14 Container aus der Baseline zurück**, zusätzlich kam `hms-mqtt` hoch (Restart-Policy `always`, lief vorher nicht). + - ✅ **`hms-mqtt` stillgelegt (2026-08-16, User: „wird bald platt gemacht").** Es war bereits beendet; entscheidend war die **Restart-Policy `always` → `no`** (`docker update --restart=no`), sonst wäre es beim nächsten Daemon-Neustart wiedergekommen. Ein bloßes `docker stop` hätte hier nicht gehalten. - ✅ Der **Hänger** ist damit weg: `system df` beendet sich wieder (vorher rc=124 nach Ausgabe der Tabelle). ❌ **Aber es bleibt viel zu langsam und schwankt: 33 s → 92 s → 3× >120 s.** - 🔑 **Wo die Zeit hingeht (gemessen):** `images -q` 84 ms · `ps -aq` 95 ms · `volume ls -q` 65 ms · **`system df` 33–92+ s**. Also **vollständig in der Größenberechnung** (du-artiger Durchlauf der overlay2-/Volume-Verzeichnisse auf NAS-Platten) — ein struktureller Plattformnachteil, kein Defekt. Die Mengen sind harmlos (25 Images/13 GB, 10 Volumes/79 MB). - **Aussichtsreichster Weg: eigener Portainer-Benutzer für HA ohne Zugriff auf ep14** → die Integration sieht die langsame Umgebung gar nicht. User-gated (Benutzer + Token anlegen). Alternativen: nichts tun (stört nichts) · Images aufräumen (6,98 GB reclaimable, Erfolg **offen**) · ep14 aus Portainer nehmen (schlechter Tausch).