Compare commits
10 Commits
0680ccabc8
...
e474e5e38a
| Author | SHA1 | Date | |
|---|---|---|---|
| e474e5e38a | |||
| 17e8e9f72e | |||
| c8b2a15d5f | |||
| 6f6fa71ffa | |||
| 60360fc3cc | |||
| d5b3ce37ff | |||
| 452ea53576 | |||
| badf715f5a | |||
| 810126c01b | |||
| b697ffc6fa |
29
CLAUDE.md
29
CLAUDE.md
@@ -21,7 +21,7 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
|
|||||||
|
|
||||||
## 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. 🔴 **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**.
|
||||||
- **`admin/bf-network-r6`** (Gitea) ist Archiv, letzte aktive Commits 2026-08-25 → read-only-Header.
|
- **`admin/bf-network-r6`** (Gitea) ist Archiv, letzte aktive Commits 2026-08-25 → read-only-Header.
|
||||||
- **`admin/bf-golden-config`** (Gitea) ist Bot-Master (Nautobot 2×/Tag), nicht manuell.
|
- **`admin/bf-golden-config`** (Gitea) ist Bot-Master (Nautobot 2×/Tag), nicht manuell.
|
||||||
- **`admin/blaueblueten-n8n`** (Gitea) ist Master für terpene-Repo.
|
- **`admin/blaueblueten-n8n`** (Gitea) ist Master für terpene-Repo.
|
||||||
@@ -289,6 +289,33 @@ Verhaltensregeln für nachfolgende AI-Sessions — kleiner, klarer, weniger Drif
|
|||||||
- **Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, wenn sie lang wird.** Befund 2026-08-28: der „Nächster Schritt (2026-08-26): TP31 Schritt 5"-Satz stand zwei Tage nach seiner Formulierung noch im Ist-Stand-Block, obwohl ich in derselben Zeit drei Folge-Sessions eröffnet hatte. **Merkregel pro Edit:** drei Stellen berücksichtigen — Quick-Index (Tabelle), TP-Abschnitt, Ist-Stand-Block. Keine schlägt die andere; wer eine ändert, **muss** die anderen mitlesen. Strukturelle Antwort = TP36 (SSoT via Nautobot, Datei wird read-only Snapshot) — eigene Session, nicht nebenbei. **Lessons-Block erweitert 2026-08-28 nach Skill-Verifikation in opencode.**
|
- **Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, wenn sie lang wird.** Befund 2026-08-28: der „Nächster Schritt (2026-08-26): TP31 Schritt 5"-Satz stand zwei Tage nach seiner Formulierung noch im Ist-Stand-Block, obwohl ich in derselben Zeit drei Folge-Sessions eröffnet hatte. **Merkregel pro Edit:** drei Stellen berücksichtigen — Quick-Index (Tabelle), TP-Abschnitt, Ist-Stand-Block. Keine schlägt die andere; wer eine ändert, **muss** die anderen mitlesen. Strukturelle Antwort = TP36 (SSoT via Nautobot, Datei wird read-only Snapshot) — eigene Session, nicht nebenbei. **Lessons-Block erweitert 2026-08-28 nach Skill-Verifikation in opencode.**
|
||||||
- **`sed 's/=.*/=<set>/'` maskiert NICHT, wenn das `.*` greedy ist und der Wert Slashes/Backslashes enthält** (Befund 2026-08-28, shelly-mcp-Deploy). Ich habe `sed 's/SHELLY_DEVICE_PW=.*/<set>/'` auf die Stack-YML angewendet — der **Wert selbst** wurde in den Output geschrieben, weil mein Replacement-String nicht greedy genug war und ich die Pipe nicht mit `cat -v` oder `head -15 | sed 's|=.*|=<set>|'` abgesichert hatte. Folge: **`SHELLY_DEVICE_PW` ist jetzt als verbrannt zu betrachten** — wandert in **TP30 Posten #3** mit Hochstufung. **Merkregel:** bei jeder Ausgabe von Secret-haltigen Dateien entweder `head -c <n>` (Abbruch nach Bytes), oder die Datei vor dem Read mit `tr -d '\n' | sed 's|=.*|=<set>|'` über einen Stream-Filter schicken — **niemals** einfach `cat` und auf einen Replacement-Pattern vertrauen. Vergleichbar zum PSK-Leak 2026-08-24.
|
- **`sed 's/=.*/=<set>/'` maskiert NICHT, wenn das `.*` greedy ist und der Wert Slashes/Backslashes enthält** (Befund 2026-08-28, shelly-mcp-Deploy). Ich habe `sed 's/SHELLY_DEVICE_PW=.*/<set>/'` auf die Stack-YML angewendet — der **Wert selbst** wurde in den Output geschrieben, weil mein Replacement-String nicht greedy genug war und ich die Pipe nicht mit `cat -v` oder `head -15 | sed 's|=.*|=<set>|'` abgesichert hatte. Folge: **`SHELLY_DEVICE_PW` ist jetzt als verbrannt zu betrachten** — wandert in **TP30 Posten #3** mit Hochstufung. **Merkregel:** bei jeder Ausgabe von Secret-haltigen Dateien entweder `head -c <n>` (Abbruch nach Bytes), oder die Datei vor dem Read mit `tr -d '\n' | sed 's|=.*|=<set>|'` über einen Stream-Filter schicken — **niemals** einfach `cat` und auf einen Replacement-Pattern vertrauen. Vergleichbar zum PSK-Leak 2026-08-24.
|
||||||
|
|
||||||
|
- **Vor jeder Drift-Bewertung `python3 nautobot/gc-transaktion.py status` lesen** (Befund 2026-08-31,
|
||||||
|
zweimal am selben Tag gestolpert). **Drift aus einem laufenden Versuch sieht identisch aus wie ein
|
||||||
|
Versehen** — die Compliance-Zahl unterscheidet nicht. Konkret: `crs326-core`/`wifi-security`
|
||||||
|
(`ft-over-ds=no`) wurde dem 7.24.1-Upgrade zugeschrieben und als „braucht eine Entscheidung"
|
||||||
|
weitergereicht; tatsächlich ist es **TP25 Fund E Hebel 1**, um 08:09 bewusst gesetzt, während der
|
||||||
|
Core erst um 14:36 gehoben wurde, geführt als offene Transaktion `TP25-fund-e-ft-over-ds`.
|
||||||
|
🔴 **Eine solche Drift grün zu machen macht den Versuch unsichtbar.** Umgekehrt gilt: was **dauerhaft**
|
||||||
|
gelten soll (z.B. das `veth`/`masquerade` aus TP16.3 §3.4), gehört **nicht** in eine Transaktion,
|
||||||
|
sondern ins **Soll** — Transaktion = befristeter Versuch, Template = Dauerzustand.
|
||||||
|
⚠️ Das Skript löst den Token-Pfad gegen die Repo-Wurzel auf und findet ihn **aus einem Worktree
|
||||||
|
heraus nicht** (`210-nautobot.yml` liegt nur im Haupt-Checkout) — die Transaktionsliste kommt
|
||||||
|
trotzdem, nur die Compliance-Spalte fehlt dann.
|
||||||
|
- **Maskierungs-Muster auf den SCHLÜSSELNAMEN ankern, nie auf ein Trennzeichen** (Befund 2026-08-31,
|
||||||
|
zweiter Fall dieser Klasse nach `SHELLY_DEVICE_PW`). `grep -n "TOKEN" datei | sed 's/:.*TOKEN/…/'`
|
||||||
|
maskiert **nicht**: `grep -n` stellt `<zeile>:` voran, und das `:.*TOKEN` matcht diesen Präfix statt
|
||||||
|
den Wert — ein nachgeschaltetes `cut -c1-60` schneidet dann genau nichts weg. So sind zwei
|
||||||
|
Nautobot-Tokens ins Transkript geraten. **Sicher ist nur**
|
||||||
|
`sed -E 's/(TOKEN|PASSWORD|SECRET|KEY|PASS)=.*/\1=<redacted>/'`. Und: **Werte nie suchen, um sie zu
|
||||||
|
sehen** — den Schlüssel per `sed -n 's/^…KEY=//p'` direkt in eine Variable holen, nie über `grep`
|
||||||
|
mit Ausgabe. Vergleichbar zum PSK-Leak 2026-08-24 und zum `sed`-Fall 2026-08-28.
|
||||||
|
- **Vor jedem Flash-Budget auf MikroTik `docker export | wc -c` messen, nicht `docker image inspect
|
||||||
|
.Size`** (Befund 2026-08-31, TP16.3). `.Size` meldete für `cloudflared` 27,5 MB, entpackt waren es
|
||||||
|
**69,5 MiB** — das offizielle Image passt damit auf keinen hAP/cAP. RouterOS braucht beim Import
|
||||||
|
**Tar UND entpacktes Abbild gleichzeitig**. 🔴 **Und `docker save` schreibt seit Docker 25
|
||||||
|
(containerd-Store) OCI statt docker-v1** → RouterOS meldet `no config found in manifest`, was nach
|
||||||
|
kaputtem Image klingt und ein Formatproblem ist. Beides in `docs/tp16.3-bf-public-ap-connector-runbook.md` §2.2.
|
||||||
|
|
||||||
**Checkliste vor jedem Edit-Vorschlag:**
|
**Checkliste vor jedem Edit-Vorschlag:**
|
||||||
|
|
||||||
1. Habe ich die SSoT für die Datei/das Repo benannt?
|
1. Habe ich die SSoT für die Datei/das Repo benannt?
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ unter OpenCode und anderem LLM weitergepflegt.
|
|||||||
|
|
||||||
| Repo | Rolle | Stand |
|
| Repo | Rolle | Stand |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `~/dev/bf-infra/` (lokal) | Aktiver Master, Working-Copy | kein Gitea-Remote (Sicherheitslücke, offen) |
|
| `~/dev/bf-infra/` + `admin/bf-infra` (Gitea) | Aktiver Master | **Remote seit 2026-08-30**, `main` synchron — Backup-Lücke geschlossen |
|
||||||
| `admin/bf-network-r6` (Gitea) | Archiv, historischer Master | read-only-Header, letzte aktive Commits 2026-08-25 |
|
| `admin/bf-network-r6` (Gitea) | Archiv, historischer Master | read-only-Header, letzte aktive Commits 2026-08-25 |
|
||||||
| `admin/bf-golden-config` (Gitea) | Bot-Master (Nautobot 2×/Tag) | intended/ + backups/, nicht manuell |
|
| `admin/bf-golden-config` (Gitea) | Bot-Master (Nautobot 2×/Tag) | intended/ + backups/, nicht manuell |
|
||||||
| `admin/blaueblueten-n8n` (Gitea) | Master für terpene-Repo | aktiv, lokal synchron |
|
| `admin/blaueblueten-n8n` (Gitea) | Master für terpene-Repo | aktiv, lokal synchron |
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"errors": [],
|
||||||
|
"messages": [],
|
||||||
|
"result": {
|
||||||
|
"tunnel_id": "7a322261-380a-447b-97d2-8ebce99f5ed1",
|
||||||
|
"version": 4,
|
||||||
|
"config": {
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.167:5678",
|
||||||
|
"hostname": "n8n.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.167:3001",
|
||||||
|
"hostname": "n8n-mcp.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.167:3000",
|
||||||
|
"hostname": "gitea-mcp.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "ssh://10.40.20.167:22",
|
||||||
|
"hostname": "82unkpb5.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "https://10.40.20.135:8007",
|
||||||
|
"hostname": "pbs.emsenhuber.net",
|
||||||
|
"originRequest": {
|
||||||
|
"noTLSVerify": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warp-routing": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": "cloudflare",
|
||||||
|
"created_at": "2026-07-27T13:53:23.512528Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,20 @@
|
|||||||
{"success":true,"errors":[],"messages":[],"result":{"tunnel_id":"7a322261-380a-447b-97d2-8ebce99f5ed1","version":4,"config":{"ingress":[{"service":"http://10.40.20.167:5678","hostname":"n8n.emsenhuber.net"},{"service":"http://10.40.20.167:3001","hostname":"n8n-mcp.emsenhuber.net"},{"service":"http://10.40.20.167:3000","hostname":"gitea-mcp.emsenhuber.net"},{"service":"ssh://10.40.20.167:22","hostname":"82unkpb5.emsenhuber.net"},{"service":"https://10.40.20.135:8007","hostname":"pbs.emsenhuber.net","originRequest":{"noTLSVerify":true}},{"service":"http_status:404"}],"warp-routing":{"enabled":false}},"source":"cloudflare","created_at":"2026-07-27T13:53:23.512528Z"}}
|
{
|
||||||
|
"tunnel_id": "7a322261-380a-447b-97d2-8ebce99f5ed1",
|
||||||
|
"version": 6,
|
||||||
|
"config": {
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.167:5678",
|
||||||
|
"hostname": "n8n.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warp-routing": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": "cloudflare",
|
||||||
|
"created_at": "2026-08-30T10:19:30.297725Z"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"errors": [],
|
||||||
|
"messages": [],
|
||||||
|
"result": {
|
||||||
|
"tunnel_id": "220c9958-401b-4111-9451-f3db09bba2d5",
|
||||||
|
"version": 6,
|
||||||
|
"config": {
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.157:3000",
|
||||||
|
"hostname": "gitea.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.173:3000",
|
||||||
|
"hostname": "pve-mcp.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.127",
|
||||||
|
"hostname": "vw.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.10:8080",
|
||||||
|
"hostname": "ds-bf.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "ssh://10.40.20.10:22",
|
||||||
|
"hostname": "3k23uow7.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warp-routing": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": "cloudflare",
|
||||||
|
"created_at": "2026-08-29T17:30:22.531419Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,24 @@
|
|||||||
{"success":true,"errors":[],"messages":[],"result":{"tunnel_id":"220c9958-401b-4111-9451-f3db09bba2d5","version":6,"config":{"ingress":[{"service":"http://10.40.20.157:3000","hostname":"gitea.emsenhuber.net"},{"service":"http://10.40.20.173:3000","hostname":"pve-mcp.emsenhuber.net"},{"service":"http://10.40.20.127","hostname":"vw.emsenhuber.net"},{"service":"http://10.40.20.10:8080","hostname":"ds-bf.emsenhuber.net"},{"service":"ssh://10.40.20.10:22","hostname":"3k23uow7.emsenhuber.net"},{"service":"http_status:404"}],"warp-routing":{"enabled":true}},"source":"cloudflare","created_at":"2026-08-29T17:30:22.531419Z"}}
|
{
|
||||||
|
"tunnel_id": "220c9958-401b-4111-9451-f3db09bba2d5",
|
||||||
|
"version": 7,
|
||||||
|
"config": {
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.157:3000",
|
||||||
|
"hostname": "gitea.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.127",
|
||||||
|
"hostname": "vw.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warp-routing": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": "cloudflare",
|
||||||
|
"created_at": "2026-08-30T08:50:21.241084Z"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,44 @@
|
|||||||
{"success":true,"errors":[],"messages":[],"result":{"tunnel_id":"48d3b0b6-7233-4781-a76a-1de4a5e957ce","version":2,"config":{"ingress":[{"service":"http://10.40.20.40:80","hostname":"vw.emsenhuber.net","originRequest":{"noTLSVerify":true}},{"service":"http://10.40.20.40:80","hostname":"hass.emsenhuber.net","originRequest":{"noTLSVerify":true}},{"service":"http://10.40.20.40:80","hostname":"n8n.emsenhuber.net","originRequest":{"noTLSVerify":true}},{"service":"http://10.40.20.40:80","hostname":"gitea.emsenhuber.net","originRequest":{"noTLSVerify":true}},{"service":"http_status:404"}],"warp-routing":{"enabled":false}},"source":"cloudflare","created_at":"2026-08-27T05:45:54.534390Z"}}
|
{
|
||||||
|
"tunnel_id": "48d3b0b6-7233-4781-a76a-1de4a5e957ce",
|
||||||
|
"version": 2,
|
||||||
|
"config": {
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.40:80",
|
||||||
|
"hostname": "vw.emsenhuber.net",
|
||||||
|
"originRequest": {
|
||||||
|
"noTLSVerify": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.40:80",
|
||||||
|
"hostname": "hass.emsenhuber.net",
|
||||||
|
"originRequest": {
|
||||||
|
"noTLSVerify": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.40:80",
|
||||||
|
"hostname": "n8n.emsenhuber.net",
|
||||||
|
"originRequest": {
|
||||||
|
"noTLSVerify": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.40:80",
|
||||||
|
"hostname": "gitea.emsenhuber.net",
|
||||||
|
"originRequest": {
|
||||||
|
"noTLSVerify": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warp-routing": {
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": "cloudflare",
|
||||||
|
"created_at": "2026-08-27T05:45:54.534390Z"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"errors": [],
|
||||||
|
"messages": [],
|
||||||
|
"result": {
|
||||||
|
"tunnel_id": "f7fdf3a6-b67d-4567-93b1-b0f273794d09",
|
||||||
|
"version": 3,
|
||||||
|
"config": {
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"service": "https://10.40.20.100:8006",
|
||||||
|
"hostname": "pve.emsenhuber.net",
|
||||||
|
"originRequest": {
|
||||||
|
"noTLSVerify": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "ssh://10.40.20.100:22",
|
||||||
|
"hostname": "j44d6tua.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.232",
|
||||||
|
"hostname": "ccu.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.107:8081",
|
||||||
|
"hostname": "iobroker.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.106:8080",
|
||||||
|
"hostname": "z2m25.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "https://10.40.20.126:9443",
|
||||||
|
"hostname": "portainer.emsenhuber.net",
|
||||||
|
"originRequest": {
|
||||||
|
"noTLSVerify": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.105:8123",
|
||||||
|
"hostname": "hass.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.105:9583",
|
||||||
|
"hostname": "ha-mcp.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warp-routing": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": "cloudflare",
|
||||||
|
"created_at": "2026-07-16T11:36:24.193655Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,20 @@
|
|||||||
{"success":true,"errors":[],"messages":[],"result":{"tunnel_id":"f7fdf3a6-b67d-4567-93b1-b0f273794d09","version":3,"config":{"ingress":[{"service":"https://10.40.20.100:8006","hostname":"pve.emsenhuber.net","originRequest":{"noTLSVerify":true}},{"service":"ssh://10.40.20.100:22","hostname":"j44d6tua.emsenhuber.net"},{"service":"http://10.40.20.232","hostname":"ccu.emsenhuber.net"},{"service":"http://10.40.20.107:8081","hostname":"iobroker.emsenhuber.net"},{"service":"http://10.40.20.106:8080","hostname":"z2m25.emsenhuber.net"},{"service":"https://10.40.20.126:9443","hostname":"portainer.emsenhuber.net","originRequest":{"noTLSVerify":true}},{"service":"http://10.40.20.105:8123","hostname":"hass.emsenhuber.net"},{"service":"http://10.40.20.105:9583","hostname":"ha-mcp.emsenhuber.net"},{"service":"http_status:404"}],"warp-routing":{"enabled":true}},"source":"cloudflare","created_at":"2026-07-16T11:36:24.193655Z"}}
|
{
|
||||||
|
"tunnel_id": "f7fdf3a6-b67d-4567-93b1-b0f273794d09",
|
||||||
|
"version": 4,
|
||||||
|
"config": {
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"service": "http://10.40.20.105:8123",
|
||||||
|
"hostname": "hass.emsenhuber.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"warp-routing": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"source": "cloudflare",
|
||||||
|
"created_at": "2026-08-30T08:50:25.151983Z"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "ds-fm.emsenhuber.net",
|
||||||
|
"type": "CNAME",
|
||||||
|
"content": "b697d124-53bf-4a33-8fcf-384cced4db1a.cfargotunnel.com",
|
||||||
|
"proxied": true,
|
||||||
|
"id": "0da61c6143bfd0d5f9c47ca4b4147b06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gateway-fm.emsenhuber.net",
|
||||||
|
"type": "CNAME",
|
||||||
|
"content": "b697d124-53bf-4a33-8fcf-384cced4db1a.cfargotunnel.com",
|
||||||
|
"proxied": true,
|
||||||
|
"id": "54ac8b816765b45191469678891389bd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gitea.emsenhuber.net",
|
||||||
|
"type": "CNAME",
|
||||||
|
"content": "220c9958-401b-4111-9451-f3db09bba2d5.cfargotunnel.com",
|
||||||
|
"proxied": true,
|
||||||
|
"id": "3d6053529f99fc5832d9956ee6e8834c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hass.emsenhuber.net",
|
||||||
|
"type": "CNAME",
|
||||||
|
"content": "f7fdf3a6-b67d-4567-93b1-b0f273794d09.cfargotunnel.com",
|
||||||
|
"proxied": true,
|
||||||
|
"id": "0841148f401b5db40fa02f69d0f0e848"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "n8n.emsenhuber.net",
|
||||||
|
"type": "CNAME",
|
||||||
|
"content": "7a322261-380a-447b-97d2-8ebce99f5ed1.cfargotunnel.com",
|
||||||
|
"proxied": true,
|
||||||
|
"id": "40b311a40e41fbeae85ca097a4036e6c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vw.emsenhuber.net",
|
||||||
|
"type": "CNAME",
|
||||||
|
"content": "220c9958-401b-4111-9451-f3db09bba2d5.cfargotunnel.com",
|
||||||
|
"proxied": true,
|
||||||
|
"id": "2971d3ba9410e841b35ec58ffda59be4"
|
||||||
|
}
|
||||||
|
]
|
||||||
146
docs/handoff-20260831-tp16.3-paragraph2-3-durch.md
Normal file
146
docs/handoff-20260831-tp16.3-paragraph2-3-durch.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Handoff — TP16.3 §2/§3 durch: drei Connectoren live, eine Entscheidung offen
|
||||||
|
|
||||||
|
> **2026-08-31, Vor-Ort-Session.** Nachfolger von
|
||||||
|
> [`handoff-20260831-tp16.3-vorort-session.md`](handoff-20260831-tp16.3-vorort-session.md).
|
||||||
|
> **Nicht** Teil dieser Session: §5 (CNAME-Flip) und §6 (Abbau).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Was jetzt steht
|
||||||
|
|
||||||
|
`bf-public` ist **healthy mit 12 Connectoren** (3 APs × 4 CF-Edges) — der Tunnel hatte vorher
|
||||||
|
**null**. Je ein `cloudflared 2026.6.1` läuft auf ap1, ap2 und ap3.
|
||||||
|
|
||||||
|
| | ap1 | ap2 | ap3 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `container`-Paket | 7.24.1 ✅ | 7.24.1 ✅ | 7.24.1 ✅ |
|
||||||
|
| `device-mode container` | `yes` ✅ | `yes` ✅ | `yes` ✅ |
|
||||||
|
| Container | `R` running | `R` running | `R` running |
|
||||||
|
| `.tar` gelöscht | ✅ | ✅ | ✅ |
|
||||||
|
| Flash frei | 54,0 MiB | 54,6 MiB | 55,0 MiB |
|
||||||
|
|
||||||
|
WLAN auf allen drei zurück, CAPs `state=Ok` am Manager. Backups je AP liegen auf dem Gerät
|
||||||
|
(`vor-tp16.3-apN.backup`) **und** lokal unter `~/tp16.3/backups/` — inklusive vollständigem
|
||||||
|
`/export` (nicht der MCP-Export, s. Punkt 4).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 🔴 Die eine offene Entscheidung: Compliance steht auf 128/135
|
||||||
|
|
||||||
|
Nach vollem Refresh (Backup → Intended → Compliance, **nicht** die stale Zahl von vorher):
|
||||||
|
|
||||||
|
| Gerät | Regel | Inhalt |
|
||||||
|
|---|---|---|
|
||||||
|
| ap1 / ap2 / ap3 | `ip-address` | `/ip address add address=172.17.0.1/24 interface=veth-cf` |
|
||||||
|
| ap1 / ap2 / ap3 | `ip-firewall-nat` | `masquerade`, Kommentar `TP31: cloudflared egress` |
|
||||||
|
| crs326-core | `wifi-security` | `ft-over-ds` — **offene Transaktion `TP25-fund-e-ft-over-ds`**, kein Versehen |
|
||||||
|
|
||||||
|
**Ursache ist ein Widerspruch zwischen zwei Dokumenten**, kein Ausrutscher: Runbook §3.4 schreibt
|
||||||
|
Direkt-Writes für `veth`/`ip address`/`nat` vor, die Governance stellt `/ip address` und
|
||||||
|
`/ip firewall` unter GC-Pflicht. Ich bin dem Runbook gefolgt.
|
||||||
|
|
||||||
|
⚠️ **Korrektur 2026-08-31 (nachgetragen):** die siebte Drift wurde hier zuerst dem 7.24.1-Upgrade
|
||||||
|
zugeschrieben — falsch. `gc-transaktion.py status` führt **`TP25-fund-e-ft-over-ds` als OFFEN**
|
||||||
|
(gesetzt 08:09, Core-Upgrade erst 14:36). Sie ist Absicht einer laufenden Messung und braucht **keine**
|
||||||
|
Entscheidung. **Merkregel: vor jeder Drift-Bewertung `gc-transaktion.py status` lesen** — Drift aus
|
||||||
|
einem laufenden Versuch sieht identisch aus wie ein Versehen.
|
||||||
|
|
||||||
|
**Die sechs Drifts sind inhaltlich richtig** — die Config *soll* dort stehen, ohne sie läuft kein
|
||||||
|
Container. Der saubere Weg ist, sie ins **Soll** zu heben (AP-Template bzw. ConfigContext
|
||||||
|
`ap-common`), damit die Compliance sie als konform sieht.
|
||||||
|
🔴 **Kein Re-Baseline** (macht die Drift-Erkennung wertlos) und **kein Rückbau** (killt die Tunnel).
|
||||||
|
⚠️ `/interface veth` erzeugt **keine** Drift — dafür existiert keine ComplianceRule. Wenn das Soll
|
||||||
|
angepasst wird, gehört die Frage dazu, ob `veth` überhaupt erfasst sein soll.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 🔴 Drei Runbook-Fehler, die diese Session aufgedeckt hat
|
||||||
|
|
||||||
|
**(a) Die Flash-Rechnung war um Faktor 2,5 zu optimistisch.** §3.5 rechnete mit „28 MB `.tar`
|
||||||
|
+ ~28 MB entpackt ≈ 39 MB Rest". Die 28 MB sind die **komprimierte** Archivgrösse; entpackt misst
|
||||||
|
`cloudflare/cloudflared:2026.6.1` **72.926.208 B = 69,5 MiB**. Der Import braucht Tar **und**
|
||||||
|
entpacktes Abbild gleichzeitig ⇒ 95,7 MiB gegen ~90 MiB frei. **Das offizielle Image passt auf
|
||||||
|
keinen der drei APs.**
|
||||||
|
⚠️ **`docker image inspect .Size` führt in die Irre** (meldete 27.463.950). Für Flash-Budgets
|
||||||
|
`docker export | wc -c` messen.
|
||||||
|
|
||||||
|
**Lösung, ausgerollt:** eigenes `FROM scratch`-Image aus dem — nachweislich **statisch gelinkten** —
|
||||||
|
Binary (36,8 MB) plus CA-Bundle: **35,3 MiB**, Spitzenbedarf 70,6 MiB. Build-Skript liegt jetzt im
|
||||||
|
Repo (`tools/build-cfd-image.py`), ein Neubau ist **byte-identisch** (`f818bf40…`).
|
||||||
|
🔑 Der Pin ist die **Binary-SHA256 `e93824f6…`**, und cloudflared bestätigt sie beim Start selbst
|
||||||
|
(`Version 2026.6.1 (Checksum e93824f6…)`) — im Log aller drei APs nachlesbar.
|
||||||
|
|
||||||
|
**(b) `docker save` taugt seit Docker 25 nicht mehr als Quelle.** Mit containerd-Image-Store schreibt
|
||||||
|
es OCI-Layout (`blobs/sha256/`, gzip-Layer); RouterOS will docker-v1 und meldet
|
||||||
|
**`no config found in manifest`** — klingt nach kaputtem Image, ist das Format.
|
||||||
|
(Konverter für den Fall, dass man doch ein Docker-Image braucht: `tools/oci2docker.py`, prüft die
|
||||||
|
Layer-Digests gegen `rootfs.diff_ids`.)
|
||||||
|
|
||||||
|
**(c) `/container/envs` heisst auf ROS 7.24.1 `list=`, nicht `name=`.** Mit `name=` kommt
|
||||||
|
`bad parameter name` bzw. `unknown parameter name`. **Auch MikroMCPs `manage_container_env` benutzt
|
||||||
|
`name=` und ist damit für dieses Feld unbrauchbar.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Vier weitere Funde
|
||||||
|
|
||||||
|
- 🔴 **Der `device-mode`-Bestätigungs-Boot installiert KEINE Pakete.** Auf ap1 lag die `.npk` schon
|
||||||
|
bereit, als `device-mode` gesetzt wurde — der Stromzyklus hat `container: yes` bestätigt und die
|
||||||
|
Datei **verbraucht, ohne zu installieren**. **Reihenfolge ist Pflicht: erst Paket-Reboot, dann
|
||||||
|
`device-mode`.** (Nachholen ging per SSH, ohne zweiten Gang zum Gerät.)
|
||||||
|
- 🟡 **`A - AVAILABLE` in `/system package print` ist ein Download-Katalog, kein Paket auf dem Gerät.**
|
||||||
|
Gegenprobe: `crs326-core` hat 2,9 MB frei von 16 MB, listet aber `wifi-qcom` (10,1 MB) und
|
||||||
|
`rose-storage` (4,3 MB) als „available"; die Grössen sind arch-spezifisch. `enable` löste einen
|
||||||
|
Internet-Fetch aus — auf ap1/ap2 per `device-mode=home` gesperrt. **Der `.npk`-Weg bleibt richtig.**
|
||||||
|
- 🟡 **MikroMCPs `export_config` exportiert unvollständig:** 301 Bytes gegen 6,2 KiB bei direktem
|
||||||
|
`/export`. Ein Rollback-Artefakt, das existiert und trotzdem fast leer ist — dieselbe Klasse wie
|
||||||
|
„Lücke statt Fehlermeldung". Für Backups `/export file=…` per SSH.
|
||||||
|
- ⚠️ **WLAN-Abnahme nur teilweise belegbar:** `main` (3 Clients) und `iot` (27) haben frische Leases;
|
||||||
|
`media`/`untrusted`/`guest` hatten **schon vor** dem Umbau null Clients. Kein Rückschritt, aber
|
||||||
|
auch kein Beleg — nicht als „grün" buchen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 🔴 Sicherheitsvorfall in dieser Session — Rotation nötig
|
||||||
|
|
||||||
|
Beim Suchen des Nautobot-Tokens hat eine unzureichende `sed`-Maskierung
|
||||||
|
(`grep -n … | sed 's/:.*TOKEN/…/' | cut -c1-60`) **zwei Werte im Klartext ins Transkript geschrieben**:
|
||||||
|
|
||||||
|
- `NAUTOBOT_SUPERUSER_API_TOKEN`
|
||||||
|
- `NAUTOBOT_GITEA_TOKEN`
|
||||||
|
|
||||||
|
beide aus `docs/nas-vlan20/stacks/210-nautobot.yml`. **Als verbrannt behandeln → TP30.**
|
||||||
|
Das ist exakt die Falle, die seit 2026-08-28 in der CLAUDE.md steht (der `SHELLY_DEVICE_PW`-Fall).
|
||||||
|
**Merkregel, die offenbar noch nicht scharf genug formuliert war:** ein Maskierungs-Muster, das auf
|
||||||
|
`:` ankert, matcht bei `grep -n` den **Zeilennummer-Präfix** statt den Wert. Sicher ist nur
|
||||||
|
`sed -E 's/(TOKEN|PASSWORD|SECRET|KEY|PASS)=.*/\1=<redacted>/'` — auf den **Schlüsselnamen** ankern,
|
||||||
|
nie auf ein Trennzeichen, das auch im Präfix vorkommt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Repo-Zustand
|
||||||
|
|
||||||
|
- Branch `claude/nervous-williams-acf000` ist hier **hereingemergt**. Konflikte aufgelöst:
|
||||||
|
- **Nummernkonflikt TP37**: `main` hatte TP37 = *Nautobot als Netzwerkdoku* (User-Okay 08-30),
|
||||||
|
der Branch TP37 = *OOB-Mgmt*. Letzterer ist auf **TP38** umnummeriert.
|
||||||
|
✅ **Von Christoph bestätigt (2026-08-31).**
|
||||||
|
- **Drei CF-Rollback-Snapshots hatten denselben Dateinamen bei verschiedenem Inhalt**
|
||||||
|
(`bf-mm` v4/6 Regeln vs. v6/2 Regeln, analog `bf-nas`, `bf-pve`) — zwei Zeitpunkte, beide
|
||||||
|
„vor-tp16.3-schnitt" genannt. **Keiner verworfen:** der ältere liegt jetzt als
|
||||||
|
`*-vor-hostname-schnitt.json` daneben.
|
||||||
|
- Neu: `tools/build-cfd-image.py`, `tools/oci2docker.py`.
|
||||||
|
- `mcp/mikromcp/config/routers.yaml` meldet weiter `rosVersion: "7.24"` für alle fünf (live 7.24.1) —
|
||||||
|
kosmetisch, unangetastet gelassen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Nächste Schritte
|
||||||
|
|
||||||
|
1. **Entscheidung zu Punkt 2** (sechs Drifts ins Soll heben) — vor dem Flip, sonst geht man mit
|
||||||
|
erklärter, aber ungeklärter Drift in den Produktivpfad.
|
||||||
|
2. **§5 CNAME-Flip**, einzeln mit Abnahme dazwischen, Testname zuerst (§5.1).
|
||||||
|
▸ Fertiger Übergabeprompt: [`tp16.3-paragraph5-session-handoff-prompt.md`](tp16.3-paragraph5-session-handoff-prompt.md).
|
||||||
|
🔴 `healthy` beweist nur, dass der Connector mit Cloudflare redet — **nicht**, dass das Origin
|
||||||
|
antwortet. Der Streckenbeweis bis Caddy ist §5.1.
|
||||||
|
3. **§6 Abbau** frühestens 7 Tage nach dem Flip.
|
||||||
|
4. **TP30**: die zwei Tokens aus Punkt 5 rotieren.
|
||||||
139
docs/handoff-20260831-tp16.3-vorort-session.md
Normal file
139
docs/handoff-20260831-tp16.3-vorort-session.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Handover — TP16.3 §2/§3: Container-Bootstrap auf ap1 / ap2 / ap3
|
||||||
|
|
||||||
|
> ✅ **ERLEDIGT am 2026-08-31.** Nachfolge-Dokument mit Ergebnis, drei korrigierten Runbook-Fehlern
|
||||||
|
> und der einen offenen Entscheidung (Compliance 128/135):
|
||||||
|
> [`handoff-20260831-tp16.3-paragraph2-3-durch.md`](handoff-20260831-tp16.3-paragraph2-3-durch.md).
|
||||||
|
> Dieses Dokument bleibt als Ausgangslage stehen — **Abschnitt 3 und 7 sind inhaltlich überholt**
|
||||||
|
> (Flash-Budget, `docker save`, `envs`-Parametername, Reihenfolge Paket/device-mode).
|
||||||
|
|
||||||
|
> **Erstellt 2026-08-31.** Zielsession: **§2 + §3** des Runbooks
|
||||||
|
> [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md) — also
|
||||||
|
> `container.npk`, `device-mode`, Container. **Vor Ort**, weil jeder AP eine physische Bestätigung braucht.
|
||||||
|
> **Nicht** Teil dieser Session: der CNAME-Flip (§5) und der Abbau (§6).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Was vor dir schon erledigt ist — nicht nochmal machen
|
||||||
|
|
||||||
|
| Punkt | Stand | Nachweis |
|
||||||
|
|---|---|---|
|
||||||
|
| **§1 Blocker A** — Redirect-Schleife | ✅ 08-31 | `bf-public`-Ingress → `https://10.40.20.40:443`, `originServerName` je Regel, zurückgelesen |
|
||||||
|
| **§1 Blocker B** — Firewall `vlan99→vlan20:443` | ✅ 08-31 | via **Nautobot GC** (Plan `Completed`), Position vor `TP5: log-only` geprüft, funktional gegen Caddy nachgewiesen |
|
||||||
|
| **§1 Blocker C** — `ROUTER_AP3_PASS` | ✅ 08-31 | in `~/.secrets/credentials.env`, REST-Auth ap3 = **200** |
|
||||||
|
| Rollback-Snapshots | ✅ 08-30 | `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json` |
|
||||||
|
| ROS-Upgrade auf **7.24.1** | ✅ 08-31 (User) | alle fünf Geräte, RouterBOOT mitgezogen |
|
||||||
|
|
||||||
|
**⇒ Die Session startet bei §2.** Wer §1 nochmal anfasst, arbeitet doppelt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Die eine Änderung gegenüber dem Runbook-Stand vom 08-30
|
||||||
|
|
||||||
|
🎉 **Aus zwei `container.npk` ist eine geworden.** Christoph hat am 08-31 alle fünf MikroTik auf
|
||||||
|
**7.24.1** gehoben. Damit gilt:
|
||||||
|
|
||||||
|
```
|
||||||
|
container-7.24.1-arm64.npk → ap1, ap2 UND ap3
|
||||||
|
```
|
||||||
|
|
||||||
|
Die frühere Zwei-Dateien-Regel war **rein versionsbedingt**, nicht modellbedingt — `cAP ax` und
|
||||||
|
`hAP ax²` teilen sich die `arm64`-Pakete. Das Runbook ist entsprechend umgeschrieben (§0, §2.1, §3.1,
|
||||||
|
Falle 1).
|
||||||
|
|
||||||
|
⚠️ **Die Regel dahinter bleibt:** `container.npk` muss **exakt** zur ROS-Version passen. Vor dem Upload
|
||||||
|
`/system resource print` lesen. Falsche Datei ⇒ Paket ist nach dem Reboot **inaktiv, ohne Fehlermeldung**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ablauf in einem Satz
|
||||||
|
|
||||||
|
Je AP, **einzeln und vollständig, Reihenfolge ap3 → ap2 → ap1**:
|
||||||
|
`.npk` hoch → Reboot (SSH, **nicht** MikroMCP) → `/system package print` prüfen →
|
||||||
|
`device-mode update container=yes` → **physisch bestätigen** → `veth` + `masquerade` →
|
||||||
|
`.tar` hoch → `/container add file=` → starten → `.tar` löschen → Log prüfen.
|
||||||
|
|
||||||
|
**Details, Befehle und Abnahmekriterien: Runbook §2 und §3.** Hier nur die Punkte, die man
|
||||||
|
erfahrungsgemäß falsch macht:
|
||||||
|
|
||||||
|
- 🔴 **Reboot geht NICHT über MikroMCP** (ROS kennt den `delay`-Parameter nicht, `run_command` ist für
|
||||||
|
`/system reboot*` gesperrt) → `SSHPASS="$PW" sshpass -e ssh admin@<ip> "/system reboot"`.
|
||||||
|
**Niemals `sshpass -p`** — das schreibt das Passwort in `ps`.
|
||||||
|
- 🔴 **`~/.secrets/credentials.env` in zsh sourcen schlägt still fehl** (rohe WireGuard-Zeilen ab ~Z. 69).
|
||||||
|
Variablen bleiben **leer**, und der Fehler sieht aus wie ein ungültiger Token. **Immer `bash -c`.**
|
||||||
|
- 🔴 **Kein PoE an den CRS326** (beide haben gar kein PoE-Menü) ⇒ kein Remote-Power-Cycle. Die
|
||||||
|
Bestätigung ist wirklich physisch: **Stecker ziehen/stecken** (Weg A, eindeutig) oder Reset-Knopf am
|
||||||
|
*laufenden* Gerät kurz drücken (Weg B). Weg B **nie während des Bootens** — das wäre ein Werksreset.
|
||||||
|
- 🔴 **Ein Container pro AP.** Nach dem Import bleiben ~39 MB. Kein Caddy, kein AGH dazu.
|
||||||
|
- ⚠️ **Jeder AP rebootet zweimal** → WLAN dieses APs ist je ~1–2 min weg. Die CAPs holen ihre Config
|
||||||
|
per L2-Discovery vom Core. **Nicht ungeduldig nachprovisionieren.**
|
||||||
|
- ⚠️ **WLAN-Abnahme ist eine frische DHCP-Lease je SSID, nicht die Registration-Table.**
|
||||||
|
|
||||||
|
**Erwartetes Ergebnis am Ende:** `bf-public` meldet `healthy` mit Connectoren (Richtung 12 = 3 APs × 4
|
||||||
|
CF-Edge). 🔴 **`healthy` heißt nur „Connector redet mit Cloudflare", nicht „Origin antwortet"** — der
|
||||||
|
Beweis für die Strecke bis Caddy ist §5.1, und der gehört in die **Folge**session.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Was du unterwegs sehen wirst und **nicht** als Fehler behandeln darfst
|
||||||
|
|
||||||
|
**`/tool fetch` von einem AP auf `https://10.40.20.40` schlägt mit einem TLS-Alert fehl.**
|
||||||
|
Das ist **korrekt und erwartet**: Caddy weist Handshakes ohne passenden SNI ab (gemessen: ohne SNI
|
||||||
|
`000`, mit korrektem SNI `200`). Der Alert **beweist sogar**, dass das Paket ankommt — ein TLS-Alert
|
||||||
|
kann nur vom Server kommen, die Firewall-Freigabe trägt also. cloudflared schickt den richtigen SNI
|
||||||
|
über `originServerName`. **Nicht „reparieren".**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Offene Punkte, die NICHT in diese Session gehören
|
||||||
|
|
||||||
|
1. ✅ **Die 134/135 sind ABSICHT — nichts tun.** `crs326-core` / `wifi-security`: `sec-main` hat
|
||||||
|
`ft-over-ds=no` statt `yes`. Das ist **TP25 Fund E, Hebel 1**, am 2026-08-31 um 08:09 bewusst gesetzt
|
||||||
|
und als **offene Transaktion `TP25-fund-e-ft-over-ds`** geführt (Scope `wifi-security`, Ablauf
|
||||||
|
2026-09-03, gehört einer anderen Session). **Nicht anfassen, nicht grün machen, nicht re-baselinen** —
|
||||||
|
das würde einen laufenden Versuch unsichtbar machen.
|
||||||
|
*(Eine frühere Fassung dieses Handovers schrieb die Drift dem 7.24.1-Upgrade zu. Falsch: der Hebel
|
||||||
|
kam um 08:09, der Core wurde erst 14:36 gehoben.)*
|
||||||
|
🔑 **Merkregel für die Session:** wenn Compliance nicht 135/135 zeigt, **zuerst**
|
||||||
|
`nautobot/gc-transaktion.py status` lesen. Drift aus einem laufenden Versuch sieht genauso aus wie
|
||||||
|
ein Versehen.
|
||||||
|
2. 🆕 **[TP37](teilprojekte.md#tp37) — OOB-Management, angelegt 2026-08-31.** Je ein untagged-VLAN99-Port
|
||||||
|
auf beiden Switches (Kandidat `ether23`, auf core **und** branch frei). Auslöser: beim Core-Neustart
|
||||||
|
war `crs326-branch` nicht erreichbar, weil sein Mgmt **ausschließlich** über den Core läuft.
|
||||||
|
🔴 Nebenbefund mit eigenem Gewicht: **der Branch hat gar keine `input`-Firewallregeln** — sein Schutz
|
||||||
|
ist heute allein die Erreichbarkeit über den gehärteten Core.
|
||||||
|
💡 **Berührt diese Session, ist aber keine Voraussetzung:** hier rebooten dreimal Geräte, deren Mgmt
|
||||||
|
am Core hängt. Wer vor Ort ist, kann `ether23` schon mal beschriften — gebaut wird TP37 separat.
|
||||||
|
3. 🟡 **`mcp/mikromcp/config/routers.yaml` hält `rosVersion: "7.24"`** für alle fünf — jetzt `7.24.1`.
|
||||||
|
⚠️ **Die Datei liegt auf `main`, nicht auf diesem Branch** (der Worktree hat eine ältere Kopie
|
||||||
|
**ohne** ap3). Der Fix gehört auf `main`, nicht hierher — sonst gibt es einen Konflikt.
|
||||||
|
4. **CNAME-Flip (§5) und Abbau (§6)** — eigene Session, §6 frühestens 7 Tage nach dem Flip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Zugänge und Werkzeuge
|
||||||
|
|
||||||
|
| Was | Wie |
|
||||||
|
|---|---|
|
||||||
|
| APs | MikroMCP `ap1`/`ap2`/`ap3` (Reads), SSH `admin@10.40.99.11/.12/.13`, Creds `ROUTER_AP{1,2,3}_PASS` in `~/.secrets` |
|
||||||
|
| Reboot | `bash -c 'set -a; . ~/.secrets/credentials.env; set +a; SSHPASS="$ROUTER_AP3_PASS" sshpass -e ssh admin@10.40.99.13 "/system reboot"'` |
|
||||||
|
| Config-Änderungen an MikroTik | **ausschließlich Nautobot GC** (`nautobot/gc-manual-plan.py`), Token aus `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored, **nur im Haupt-Checkout**) |
|
||||||
|
| Cloudflare | `CF_API_TOKEN` / `CF_ACCOUNT_ID` in `~/.secrets`, Zone `1b5bc02fdf61b6b989afde543f3f4134` |
|
||||||
|
| Caddy | `caddy-bf1`, LXC 130 auf pve, `10.40.20.40`; Logs `ssh root@10.40.20.100 'pct exec 130 -- journalctl -u caddy -n 50'` |
|
||||||
|
|
||||||
|
⚠️ **`device-mode`- und Container-Schritte sind KEINE Golden-Config-Sache** — sie sind Bootstrapping am
|
||||||
|
Gerät (wie die genehmigte Ausnahme #2 beim CRS326-Bring-up). Die GC-Pflicht gilt für
|
||||||
|
`/ip firewall`, `/interface` & Co. **Wenn die Session eine Firewall- oder Interface-Zeile braucht:
|
||||||
|
über `gc-manual-plan.py`, nicht per Direkt-Write.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Abnahme dieser Session
|
||||||
|
|
||||||
|
- [ ] `/system package print` zeigt auf **allen drei** APs `container` in **7.24.1**, nicht disabled
|
||||||
|
- [ ] `/system device-mode print` zeigt `container: yes` auf allen dreien
|
||||||
|
- [ ] `/container print` zeigt je AP **einen** Container `running`
|
||||||
|
- [ ] `.tar` je AP gelöscht (Flash zurück)
|
||||||
|
- [ ] `bf-public` meldet `healthy` mit Connectoren > 0
|
||||||
|
- [ ] WLAN je AP zurück — **frische DHCP-Lease pro SSID**, nicht Registration-Table
|
||||||
|
- [ ] Compliance nicht schlechter als **134/135** — die `wifi-security`-Drift ist die offene
|
||||||
|
TP25-Transaktion aus Punkt 5.1 und **muss** bestehen bleiben
|
||||||
@@ -27,12 +27,21 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` ·
|
|||||||
|
|
||||||
## Ist-Stand (Kurz)
|
## Ist-Stand (Kurz)
|
||||||
|
|
||||||
**Stand 2026-08-28 — live gegen die Geräte geprüft (MikroMCP/REST), nicht aus der Doku abgeschrieben.**
|
**Stand 2026-08-31 — live gegen die Geräte geprüft (MikroMCP/REST/SSH), nicht aus der Doku abgeschrieben.**
|
||||||
**Die Compliance-Zeile unten ist am 2026-08-23 gegen die Nautobot-API nachgezogen.**
|
**Compliance am 2026-08-31 nach vollem Refresh (Backup→Intended→Compliance) gelesen: 128/135.**
|
||||||
*(Die Fassung vom 2026-08-16 stand bis zum 18. hier und war an vier Stellen überholt. Wer diesen Block
|
*(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.)*
|
liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)*
|
||||||
- 🆕 **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-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).
|
- 🆕 **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
|
- 🆕 **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
|
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`,
|
**nur die 8 Netzgeräte** (4 aktiv, 4 decommissioned) — `ipam.Service`, `CustomField`, `Relationship`,
|
||||||
@@ -51,11 +60,43 @@ liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur d
|
|||||||
laut Entscheid auf **ap1/ap2**, dort fehlen aber `container`-Paket **und** `device-mode container=yes`
|
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**.
|
(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`.
|
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`.
|
- 🆕 **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
|
- **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;
|
selbst durchgezogen, TP27 damit erledigt** — ap2 08:56, core 14:36, ap1 14:50, branch 14:52;
|
||||||
ap3 kam bereits so). Vorher: vier auf 7.24 (08-17), nur ap3 auf 7.24.1.
|
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,
|
- **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`)
|
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
|
laufen hier. Mgmt-SVI **`10.40.99.1`** (vlan99), Gateway `.1` je VLAN. **WAN = `vlan100`** — DHCP-Client
|
||||||
@@ -95,7 +136,15 @@ liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur d
|
|||||||
**16 Sites** (4 public, 12 intern mit `.bf.`-Suffix), Zertifikate ausschliesslich
|
**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
|
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.
|
account-weite). Abnahme über den Host-Header von einem zweiten Host: 11× 200, `nautobot` 302.
|
||||||
Offen: Schritt 5–6 (Tunnel `bf-public`, Abbau).
|
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`**,
|
- **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
|
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
|
`[/bf.emsenhuber.net/] → 10.40.20.1`, 47 Rewrites (16 Service → Caddy `.20.40` + 10 Host-Namen
|
||||||
@@ -162,6 +211,11 @@ Public-Strang auf `bf-public` zu heben und die drei Alt-Tunnel samt sechs Contai
|
|||||||
> - 🔜 **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 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).
|
> - 🔜 **Session C** — `caddy/cf-sync.py`-Drift-Check + Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5).
|
||||||
>
|
>
|
||||||
|
> 🆕 **Dazu seit 2026-08-30: TP16.3/TP31 Schritt 5 = Vor-Ort-Session** an ap1/ap2/**ap3**
|
||||||
|
> ([Runbook](tp16.3-bf-public-ap-connector-runbook.md)). **Reihenfolge beachten:** die drei Blocker in
|
||||||
|
> §1 sind **remote** zu erledigen, *bevor* jemand in den Keller geht — sonst steht man vor einem
|
||||||
|
> Connector, der `healthy` meldet, während die Seite eine Redirect-Schleife dreht.
|
||||||
|
>
|
||||||
> 🆕 **Strukturelle Folge = TP36:** SSoT für `teilprojekte.md`-Live-Daten via Nautobot (eigene Session, nicht heute). Befund 2026-08-28: manuell gepflegte Datei verliert gegen die Realität, sobald sie lang wird. Quickref-Index, TP-Abschnitt und Ist-Stand-Block müssen bei jedem Edit mitgezogen werden — keine schlägt die andere. **Merkregel im Lessons-Block.**
|
> 🆕 **Strukturelle Folge = TP36:** SSoT für `teilprojekte.md`-Live-Daten via Nautobot (eigene Session, nicht heute). Befund 2026-08-28: manuell gepflegte Datei verliert gegen die Realität, sobald sie lang wird. Quickref-Index, TP-Abschnitt und Ist-Stand-Block müssen bei jedem Edit mitgezogen werden — keine schlägt die andere. **Merkregel im Lessons-Block.**
|
||||||
|
|
||||||
> 🆕 **Neu am 2026-08-24/25:** [TP31](#tp31) löst **TP16** und **TP16.2** ab und dampft cloudflared
|
> 🆕 **Neu am 2026-08-24/25:** [TP31](#tp31) löst **TP16** und **TP16.2** ab und dampft cloudflared
|
||||||
@@ -265,7 +319,7 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na
|
|||||||
> | TP14.4 | 🔜 | CF-Access-Token | — |
|
> | TP14.4 | 🔜 | CF-Access-Token | — |
|
||||||
> | TP16 | ✅ | CF-Tunnel HA wieder voll | 2026-08-24 |
|
> | TP16 | ✅ | CF-Tunnel HA wieder voll | 2026-08-24 |
|
||||||
> | TP16.2 | 🔜 | Härtung bypass-Dienste | — |
|
> | TP16.2 | 🔜 | Härtung bypass-Dienste | — |
|
||||||
> | TP16.3 | ⏳ | cloudflared-Reduktion (14 Hostnames geschnitten 08-30, F1 durch; Abbau braucht Vor-Ort-Session) | — |
|
> | TP16.3 | ⏳ | cloudflared-Reduktion (14 Hostnames geschnitten 08-30, F1 durch; §1–§3 durch 08-31, `bf-public` healthy/12 — offen: Flip §5, Abbau §6) | — |
|
||||||
> | TP17.1 | ❌ | Shelly Gen4 → Zigbee | 2026-08-23 |
|
> | TP17.1 | ❌ | Shelly Gen4 → Zigbee | 2026-08-23 |
|
||||||
> | TP17.3 | ✅ | Shelly-Regression behoben | 2026-08-23 |
|
> | TP17.3 | ✅ | Shelly-Regression behoben | 2026-08-23 |
|
||||||
> | TP18 | 🔜 | Garage-Objektspeicher | — |
|
> | TP18 | 🔜 | Garage-Objektspeicher | — |
|
||||||
@@ -276,13 +330,14 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na
|
|||||||
> | TP28 | ✅ | WLAN/CAPsMAN — dritter AP `ap3` (cAP ax) **live** | 2026-08-30 |
|
> | TP28 | ✅ | WLAN/CAPsMAN — dritter AP `ap3` (cAP ax) **live** | 2026-08-30 |
|
||||||
> | TP29 | 💡 | Anwesenheitserkennung (Idee) | — |
|
> | TP29 | 💡 | Anwesenheitserkennung (Idee) | — |
|
||||||
> | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — |
|
> | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — |
|
||||||
> | TP31 | ✅ | Reverse Proxy Caddy (Schritt 1–4 ✅) | — |
|
> | TP31 | ⏳ | Reverse Proxy Caddy (1–4 ✅, 5–6 = Vor-Ort-Session) | — |
|
||||||
> | TP32 | 🟢 | Nummern-Systematik (Konzept) | — |
|
> | TP32 | 🟢 | Nummern-Systematik (Konzept) | — |
|
||||||
> | TP33 | 💡 | Sonos fern (Idee, zurückgestellt) | — |
|
> | TP33 | 💡 | Sonos fern (Idee, zurückgestellt) | — |
|
||||||
> | TP34 | 🔜 | Caddyfile via Nautobot/GC (Stub, Inventur-Handoff 2026-08-27) | — |
|
> | TP34 | 🔜 | Caddyfile via Nautobot/GC (Stub, Inventur-Handoff 2026-08-27) | — |
|
||||||
> | TP35 | ⏳ | MCP-Schichtung + 3-Schicht-CLAUDE.md + Backup-Guard (Schicht 1+3+6 ✅) | — |
|
> | 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 | — |
|
> | TP36 | 🔜 | SSoT via Nautobot für teilprojekte.md-Live-Daten + Datei-Aufteilung | — |
|
||||||
> | TP37 | 🔜 | Nautobot als Netzwerkdoku: Hosts/Container/Dienste + CF-Connectoren (Okay 08-30) | — |
|
> | TP37 | 🔜 | Nautobot als Netzwerkdoku: Hosts/Container/Dienste + CF-Connectoren (Okay 08-30) | — |
|
||||||
|
> | TP38 | 🔜 | OOB-Mgmt: untagged-VLAN99-Port je Switch | — |
|
||||||
> | — | — | **34 archivierte TPs** in `teilprojekte-archiv.md` | — |
|
> | — | — | **34 archivierte TPs** in `teilprojekte-archiv.md` | — |
|
||||||
|
|
||||||
### 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
|
||||||
@@ -475,7 +530,49 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na
|
|||||||
- **Reihenfolge:** (1) Client-IP-Weitergabe fixen (inkl. HA `trusted_proxies`) → (2) `/admin`-Pfade path-scoped schützen → (3) App-Limits + 2FA → (4) Edge-Rules → (5) CrowdSec/fail2ban mit CF-Bouncer.
|
- **Reihenfolge:** (1) Client-IP-Weitergabe fixen (inkl. HA `trusted_proxies`) → (2) `/admin`-Pfade path-scoped schützen → (3) App-Limits + 2FA → (4) Edge-Rules → (5) CrowdSec/fail2ban mit CF-Bouncer.
|
||||||
- **Abh.:** TP16 ✅ (Tunnel-HA). Verwandt: **TP20.1** (vw wandert auf Platform-Postgres — DB-HA ≠ Access, getrennt halten), **TP23** (dessen WARP-Managed-Network-Idee ist durch diesen Constraint **entlastet**: `vw` verlangt keine Device-Identity, also kollidiert „WARP zuhause aus" nicht mehr mit dem Access-Strang).
|
- **Abh.:** TP16 ✅ (Tunnel-HA). Verwandt: **TP20.1** (vw wandert auf Platform-Postgres — DB-HA ≠ Access, getrennt halten), **TP23** (dessen WARP-Managed-Network-Idee ist durch diesen Constraint **entlastet**: `vw` verlangt keine Device-Identity, also kollidiert „WARP zuhause aus" nicht mehr mit dem Access-Strang).
|
||||||
|
|
||||||
### TP16.3 — cloudflared-Reduktion: was der Mesh als Datenpfad kann (und was nicht) ▸ ⏳ **GROSSER SCHNITT DURCH (2026-08-30), Public-Strang blockiert** — `docs/tp16.3-nautobot-mesh-only-runbook.md §8`
|
### TP16.3 — cloudflared-Reduktion: was der Mesh als Datenpfad kann (und was nicht) ▸ ⏳ **§1–§3 DURCH (2026-08-31): drei Connectoren live, `bf-public` healthy/12** — offen: Flip §5, Abbau §6 — `docs/tp16.3-nautobot-mesh-only-runbook.md §8`
|
||||||
|
|
||||||
|
#### 🔜 Follow-up F2 — `bf-public`-Connector auf die APs (Runbook 2026-08-30)
|
||||||
|
**Runbook: [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)**
|
||||||
|
— Vor-Ort-Session an **ap1 + ap3 (Technik UG)** und **ap2 (Technik OG)**, weil der
|
||||||
|
`device-mode`-Wechsel eine **physische** Bestätigung am Gerät verlangt.
|
||||||
|
|
||||||
|
- 🆕 **ap3 ist neu und nicht baugleich** (live 08-30): **cAP ax** (`cAPGi-5HaxD2HaxD`, `10.40.99.13`),
|
||||||
|
arm64, 96 MB Flash frei, 689 MB RAM frei. ~~Zwei verschiedene `container.npk`~~ — **überholt seit dem
|
||||||
|
ROS-Upgrade 08-31: alle drei auf `7.24.1`/arm64 ⇒ EINE Datei** (`container-7.24.1-arm64.npk`,
|
||||||
|
1.187.985 B — exakt die Grösse, die die APs selbst für das Paket melden, damit belegt statt
|
||||||
|
angenommen). ap3 steht auf `device-mode=basic` (nicht `home`), dort ist `fetch` bereits erlaubt —
|
||||||
|
wir nutzen trotzdem auf allen dreien den `.tar`-Import, damit alle dasselbe Image fahren.
|
||||||
|
- ✅ **Blocker A — Redirect-Schleife, behoben 2026-08-31.** `bf-public` zeigt auf `http://10.40.20.40:80`; Caddy antwortet
|
||||||
|
dort mit **308 → `https://<name>/`**, also zurück durch Cloudflare in denselben Tunnel ⇒
|
||||||
|
`ERR_TOO_MANY_REDIRECTS` für alle vier Namen. **Fällt heute nicht auf**, weil die drei Alttunnel
|
||||||
|
**an Caddy vorbei** direkt aufs Backend gehen — die Schleife entsteht erst mit dem Umstieg.
|
||||||
|
Fix = `https://10.40.20.40:443` + `originServerName` je Regel (über 443 mit korrekter SNI: 4× 200).
|
||||||
|
- ✅ **Blocker B — die Freigabe fehlte, angelegt 2026-08-31 über Nautobot GC.** Die `forward`-Kette des Core wurde vollständig
|
||||||
|
gelesen (36 Regeln, Nr. 35 = `TP5: deny-by-default`): **keine** Regel `vlan99 → vlan20` für 80/443.
|
||||||
|
Die einzige MGMT→INFRA-Regel (Nr. 26) ist ein `src-port=8728`-**Rückweg** der API.
|
||||||
|
⚠️ **Der Vorgänger-Runbook behauptet das Gegenteil** („Regel 27 deckt `vlan99→vlan20:80` ab ✅") —
|
||||||
|
Regel 27 ist `IOT->INFRA http-rueckweg`, `in-interface=vlan30`. Ohne neue Regel läuft der Connector
|
||||||
|
in `TP5-WOULD-DROP` + Drop. Egress zu CF (Regel 13 `LAN->WAN`) und DNS (Regeln 30–33) sind dagegen ✅.
|
||||||
|
⇒ Das `masquerade` des `veth`-Netzes ist deshalb **Pflicht**, nicht Kosmetik: sonst kommen die Pakete
|
||||||
|
mit `172.17.0.2` am Core an, wo weder Route noch Regel sie kennt.
|
||||||
|
- ✅ **Blocker C — `ROUTER_AP3_PASS` fehlte in `~/.secrets/credentials.env`, nachgetragen 2026-08-31** (steht nur in
|
||||||
|
`~/.claude.json`), gegen die Credential-Store-Konvention. Der `sshpass -e`-Reboot-Weg braucht es dort.
|
||||||
|
⚠️ Nebenbefund: **`credentials.env` lässt sich in zsh nicht sourcen** — ab ~Zeile 69 stehen rohe
|
||||||
|
WireGuard-Zeilen, zsh bricht ab und lässt die Variablen **leer**; das sieht aus wie ein ungültiger
|
||||||
|
Token. Alle Snippets im Runbook laufen deshalb in `bash -c`.
|
||||||
|
- 🔩 **Kein PoE als Abkürzung:** beide CRS326 haben nachweislich gar kein PoE-Menü
|
||||||
|
(`bad command name poe`) ⇒ es gibt **keinen** Remote-Power-Cycle, die Bestätigung ist wirklich physisch.
|
||||||
|
- 🧹 **Abbau (frühestens 7 Tage nach dem Flip):** Tunnel `bf-mm`/`bf-nas`/`bf-pve` löschen und die
|
||||||
|
**sechs** Container entfernen — LXC 400 (`cloudflared-bfmm`, `-bfnas`), macmini (`-bfpve`, `-bfnas`),
|
||||||
|
NAS ep14 (`-bfpve`, `-bfmm`); alle drei Hosts 08-30 verifiziert. **Erst Container, dann Tunnel** —
|
||||||
|
umgekehrt erzeugt man 530er. 🚫 **`terpene-cloudflared` (LXC 400) und `fm.emsenhuber.net` bleiben.**
|
||||||
|
- 🔴 **Ehrliche Einschränkung (aus TP31, gilt unverändert):** solange Caddy nur auf `caddy-bf1` liegt,
|
||||||
|
ist das Origin aller drei Connectoren **derselbe Rechner** — drei Connectoren kaufen
|
||||||
|
**Hardware-Diversität am Eingang, aber keine Verfügbarkeit**. Der echte Gewinn kommt mit `caddy-bf2`
|
||||||
|
(LXC 131); für `vw` gilt dabei die TP20.1-Bremse (Schreibzugriffe auf einen read-only Standby).
|
||||||
|
- **Rollback:** `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json` (vier Ingress-Configs + CNAMEs
|
||||||
|
inkl. Record-IDs), am 2026-08-30 **neu angelegt**.
|
||||||
- ▶️ **Schnitt ausgeführt 2026-08-24:** `nautobot.emsenhuber.net` ist aus dem Ingress von `bf-nas`
|
- ▶️ **Schnitt ausgeführt 2026-08-24:** `nautobot.emsenhuber.net` ist aus dem Ingress von `bf-nas`
|
||||||
entfernt (**8 → 7 Regeln**, Catch-all weiterhin letzte). Rollback-Sicherung liegt unter
|
entfernt (**8 → 7 Regeln**, Catch-all weiterhin letzte). Rollback-Sicherung liegt unter
|
||||||
`docs/cf-backup/bf-nas-ingress-20260824-vor-tp16.3.json` — Rückweg ist ein `PUT`, eine Minute.
|
`docs/cf-backup/bf-nas-ingress-20260824-vor-tp16.3.json` — Rückweg ist ein `PUT`, eine Minute.
|
||||||
@@ -500,6 +597,58 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na
|
|||||||
(mit den CF-Leichen am 08-29 gelöscht, Commit `39ba5da`) — der Dienst ist vollständig aus dem
|
(mit den CF-Leichen am 08-29 gelöscht, Commit `39ba5da`) — der Dienst ist vollständig aus dem
|
||||||
öffentlichen Strang heraus und ausschließlich über `10.40.20.30:8080` bzw. intern über Caddy erreichbar.
|
öffentlichen Strang heraus und ausschließlich über `10.40.20.30:8080` bzw. intern über Caddy erreichbar.
|
||||||
|
|
||||||
|
##### ✅ §2/§3 durchgezogen — Vor-Ort-Session 2026-08-31
|
||||||
|
|
||||||
|
**Ergebnis:** je ein `cloudflared` auf ap1/ap2/ap3, `bf-public` **healthy mit 12 Connectoren**
|
||||||
|
(3 APs × 4 CF-Edges, alle `2026.6.1`, Egress über `90.146.33.177`). WLAN auf allen drei zurück
|
||||||
|
(CAPs `state=Ok`), Flash je AP **54–55 MiB frei**.
|
||||||
|
|
||||||
|
- 🔴 **Die Platzrechnung des Runbooks war falsch — Faktor 2,5.** §3.5 rechnete mit „28 MB `.tar`
|
||||||
|
+ ~28 MB entpackte Layer ≈ 39 MB Rest". Die 28 MB sind die **komprimierte** Archivgrösse; das
|
||||||
|
entpackte Image misst **72.926.208 B = 69,5 MiB** (`docker export | wc -c`). Spitzenbedarf beim
|
||||||
|
Import ist **Tar + entpacktes Abbild gleichzeitig** ⇒ 26,2 + 69,5 = **95,7 MiB gegen 90,5 MiB frei**.
|
||||||
|
Das offizielle `cloudflare/cloudflared`-Image passt auf diese Geräte **nicht** — auf keinem der drei.
|
||||||
|
⚠️ **`docker image inspect .Size` führt hier in die Irre** (meldete 27.463.950); `docker history`
|
||||||
|
summiert auf ~70 MB. **Für Flash-Budgets `docker export` messen, nicht `inspect`.**
|
||||||
|
- ✅ **Lösung: eigenes Minimal-Image, `FROM scratch`.** Das Binary im offiziellen Image ist
|
||||||
|
**statisch gelinkt** (verifiziert) und 36,8 MB gross; die restlichen ~33 MB sind distroless-Debian,
|
||||||
|
das cloudflared nicht braucht. Das selbst gebaute Archiv (Binary + CA-Bundle) misst **35,3 MiB**,
|
||||||
|
Spitzenbedarf 70,6 MiB. Gepinnt über die **Binary-SHA256 `e93824f6…`** — cloudflared bestätigt sie
|
||||||
|
beim Start selbst (`Version 2026.6.1 (Checksum e93824f6…)`), das ist der Reproduzierbarkeits-Nachweis.
|
||||||
|
Build-Skript erzeugt das docker-v1-Archiv direkt, ohne Docker-Umweg.
|
||||||
|
- 🔴 **`docker save` taugt seit Docker 25 nicht mehr als Quelle.** Mit containerd-Image-Store schreibt
|
||||||
|
es ein **OCI-Layout** (`blobs/sha256/`, gzip-Layer); RouterOS will docker-v1 und quittiert das mit
|
||||||
|
**`no config found in manifest`** — eine Meldung, die nach kaputtem Image klingt, nicht nach Format.
|
||||||
|
- 🔴 **`/container/envs` heisst auf ROS 7.24.1 `list=`, nicht `name=`.** Das Runbook (§3.5) und das
|
||||||
|
MikroMCP-Tool `manage_container_env` benutzen beide `name=` und scheitern mit
|
||||||
|
`unknown parameter name`. Token-Übergabe deshalb per `.rsc` + `/import` (Datei danach löschen) —
|
||||||
|
so steht der Wert weder in `ps` noch im Transkript.
|
||||||
|
- 🔴 **Der `device-mode`-Bestätigungs-Boot installiert KEINE Pakete.** Auf ap1 wurde die `.npk` vor
|
||||||
|
dem Setzen von `device-mode` hochgeladen, um Christoph einen Reboot zu sparen: der Stromzyklus hat
|
||||||
|
`container: yes` bestätigt und die Datei **verbraucht, ohne sie zu installieren** — `/system package
|
||||||
|
print` zeigte danach nur zwei Pakete. **Reihenfolge ist also Pflicht: erst Paket-Reboot, dann
|
||||||
|
`device-mode`.** Nachholen ging ohne physischen Gang (SSH-Reboot).
|
||||||
|
- 🟡 **`A - AVAILABLE` bei `/system package print` ist ein Download-Katalog, kein Paket auf dem Gerät.**
|
||||||
|
Gegenprobe: `crs326-core` hat 2,9 MB frei von 16 MB, listet aber `wifi-qcom` (10,1 MB) und
|
||||||
|
`rose-storage` (4,3 MB) als „available"; die Grössen sind zudem arch-spezifisch. Ein
|
||||||
|
`/system package enable` löst also einen Internet-Fetch aus — auf ap1/ap2 per `device-mode=home`
|
||||||
|
gesperrt. **Der `.npk`-Weg bleibt richtig.**
|
||||||
|
- 🟡 **MikroMCPs `export_config` liefert einen unvollständigen Export** (301 B gegen 6,2 KiB bei
|
||||||
|
direktem `/export`) — ein Rollback-Artefakt, das existiert und trotzdem leer ist. Für die Backups
|
||||||
|
dieser Session wurden die MCP-Exporte verworfen und `/export file=…` verwendet.
|
||||||
|
- 🔴 **Offen und entscheidungsbedürftig: Compliance steht auf 128/135, nicht 134/135.** §3.4 schreibt
|
||||||
|
Direkt-Writes für `veth` / `ip address` / `nat` vor, während `/ip address` und `/ip firewall` unter
|
||||||
|
GC-Drift-Kontrolle stehen — **Runbook und Governance widersprechen sich.** Folge: je AP zwei neue
|
||||||
|
`extra`-Drifts (`ip-address` mit `172.17.0.1/24 interface=veth-cf`, `ip-firewall-nat` mit dem
|
||||||
|
`TP31: cloudflared egress`-Masquerade), also **sechs**. Sie sind **inhaltlich richtig** — die Config
|
||||||
|
soll dort stehen. Der saubere Weg ist, sie ins Soll zu heben (AP-Template/ConfigContext), **nicht**
|
||||||
|
ein Re-Baseline und **nicht** ein Rückbau. ⚠️ Bis dahin ist die Zahl erklärt, aber nicht in Ordnung.
|
||||||
|
(Die siebte Drift, `crs326-core` / `wifi-security` / `ft-over-ds`, ist die bekannte und unverändert.)
|
||||||
|
- ⚠️ **WLAN-Abnahme mit Einschränkung:** `main` (3) und `iot` (27) haben Clients und frische Leases;
|
||||||
|
`media`/`untrusted`/`guest` hatten **schon vor** dem Umbau null Clients — für die drei gibt es
|
||||||
|
keinen Lease-Nachweis, weil niemand eine zieht. Kein Rückschritt, aber auch kein Beleg.
|
||||||
|
|
||||||
|
|
||||||
#### ▶️ Der grosse Schnitt — ausgefuehrt 2026-08-30 (User-Entscheid „kompletter Schnitt in einem Zug")
|
#### ▶️ Der grosse Schnitt — ausgefuehrt 2026-08-30 (User-Entscheid „kompletter Schnitt in einem Zug")
|
||||||
- **12 Hostnames entfernt**, aus Ingress **und** DNS, in einem Durchgang:
|
- **12 Hostnames entfernt**, aus Ingress **und** DNS, in einem Durchgang:
|
||||||
`pve` · `portainer` · `pve-mcp` · `ha-mcp` · `ds-bf` · `ccu` · `iobroker` · `z2m25` · `pbs`
|
`pve` · `portainer` · `pve-mcp` · `ha-mcp` · `ds-bf` · `ccu` · `iobroker` · `z2m25` · `pbs`
|
||||||
@@ -2359,6 +2508,81 @@ kann in Ruhe nachziehen.
|
|||||||
- **Verzahnt mit:** TP22 (Nautobot-GC als Vorbild), TP35 (geht Hand in Hand — beide sind „SSoT + Konsolidierung"), `bf-infra/CLAUDE.md`-Lessons.
|
- **Verzahnt mit:** TP22 (Nautobot-GC als Vorbild), TP35 (geht Hand in Hand — beide sind „SSoT + Konsolidierung"), `bf-infra/CLAUDE.md`-Lessons.
|
||||||
- **Referenzen:** Lessons-Block-Eintrag „Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität" · Handoff Session A 2026-08-28.
|
- **Referenzen:** Lessons-Block-Eintrag „Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität" · Handoff Session A 2026-08-28.
|
||||||
|
|
||||||
|
<a id="tp37"></a>
|
||||||
|
### TP38 — Out-of-Band-Management: ein untagged-VLAN99-Port je Switch ▸ 🔜 **ANGELEGT 2026-08-31**
|
||||||
|
|
||||||
|
*(Nummer 2026-08-31 von 37 auf 38 korrigiert und von Christoph bestätigt: TP37 war zeitgleich in
|
||||||
|
einem zweiten Branch für „Nautobot als Netzwerkdoku" vergeben worden.)*
|
||||||
|
|
||||||
|
> **Auslöser (User 2026-08-31, aus eigener Anschauung):** *„ich durfte beim Neustart vom core gerade
|
||||||
|
> erleben, dass ich nicht mehr auf den branch zum verbinden kam. weil das eben alles über den core
|
||||||
|
> läuft."*
|
||||||
|
|
||||||
|
#### Der Befund, live nachgemessen (2026-08-31)
|
||||||
|
Das Management des Standorts hat **einen einzigen Weg**, und der führt über den Core:
|
||||||
|
|
||||||
|
| | `crs326-core` | `crs326-branch` |
|
||||||
|
|---|---|---|
|
||||||
|
| Mgmt-IP | `10.40.99.1` (VLAN99-SVI) | `10.40.99.2` (`vlan99` auf der Bridge) |
|
||||||
|
| VLAN99 auf der Bridge | SVI + Trunks | **`tagged=ether1,ether24,bridge`**, `untagged=""` |
|
||||||
|
| Weg dorthin | direkt | **nur über `ether1` → core `ether8`** |
|
||||||
|
|
||||||
|
⇒ **`crs326-branch` ist ausschließlich durch den Core hindurch erreichbar.** Rebootet der Core, ist der
|
||||||
|
Branch für die Dauer weg — nicht weil er ausfällt, sondern weil der einzige Pfad zu ihm fehlt. Dasselbe
|
||||||
|
gilt für jeden Fehler, der den Core-Trunk oder dessen VLAN99-Konfiguration betrifft. **Wer den Core
|
||||||
|
zerkonfiguriert, verliert den Branch gleich mit** — und damit die Möglichkeit, es vom Branch aus zu
|
||||||
|
reparieren.
|
||||||
|
|
||||||
|
🔴 **Der zweite Befund ist der unangenehmere: `crs326-branch` hat KEINE `input`-Firewallregeln** (Kette
|
||||||
|
leer, live gelesen). Sein gesamter Schutz besteht heute darin, dass VLAN99 nur über den Core erreichbar
|
||||||
|
ist und **dessen** `forward`-Kette seit TP5 deny-by-default fährt. **Die Erreichbarkeitsbeschränkung
|
||||||
|
IST die Zugangskontrolle.** Das ist für die Lösung unten kein Detail, sondern die Hauptauflage.
|
||||||
|
|
||||||
|
#### Zielbild
|
||||||
|
**Je Switch ein Port, untagged in VLAN99** — ein Laptop direkt daran erreicht das Gerät ohne jeden
|
||||||
|
Umweg über den Core, ohne Trunk, ohne funktionierendes Routing.
|
||||||
|
|
||||||
|
```
|
||||||
|
Laptop ──(statische IP 10.40.99.x/24)── ether23 (untagged VLAN99) ── crs326-branch 10.40.99.2
|
||||||
|
^ funktioniert auch, wenn der Core steht
|
||||||
|
```
|
||||||
|
|
||||||
|
**Kandidat `ether23` auf beiden Geräten** — auf core **und** branch heute `running=false`, also frei,
|
||||||
|
und symmetrisch (ein Port, eine Nummer, an beiden Standorten dieselbe). *(Core hat reichlich freie
|
||||||
|
Ports; auf dem Branch sind `ether23` und die Reihe darunter frei, `ether24` ist ap2.)*
|
||||||
|
|
||||||
|
#### Umsetzung (Skizze — eigene Session)
|
||||||
|
1. **Port aus dem Trunk-Verhalten nehmen und in VLAN99 untaggen:**
|
||||||
|
`bridge port` mit `pvid=99` + `frame-types=admit-only-untagged-and-priority-tagged`,
|
||||||
|
dazu `ether23` als `untagged`-Member in die `bridge vlan`-Zeile für VLAN99.
|
||||||
|
2. **Kein DHCP dort** — der VLAN99-DHCP läuft auf dem Core, und genau der soll ja nicht Voraussetzung
|
||||||
|
sein. ⇒ **statische IP am Laptop** (`10.40.99.50/24`), im Runbook mitschreiben.
|
||||||
|
3. **Beschriftung.** Ein OOB-Port, den niemand erkennt, wird im Zweifel für „ist ja frei" gehalten.
|
||||||
|
4. **Abnahme, die den Zweck wirklich prüft:** Laptop an `ether23` des Branch, **Core-Trunk `ether1`
|
||||||
|
gezogen** (oder Core im Reboot) → `10.40.99.2` muss antworten. Alles andere misst nur den Normalfall.
|
||||||
|
|
||||||
|
#### 🔴 Auflage, ohne die das TP nicht gebaut werden darf
|
||||||
|
Ein untagged-MGMT-Port ist **ein physischer Generalschlüssel**: wer ihn steckt, steht ungefiltert im
|
||||||
|
Management-VLAN eines Geräts, das (Branch) keinerlei `input`-Filter hat. Deshalb gehört **eine** der
|
||||||
|
folgenden Absicherungen dazu, und die Entscheidung darüber in die Umsetzungs-Session:
|
||||||
|
- **Port `disabled=yes` im Normalbetrieb**, im Bedarfsfall am *anderen* Gerät freigeschaltet — schützt,
|
||||||
|
kostet aber genau dann Zugriff, wenn das andere Gerät weg ist (halbierter Nutzen, ehrlich benennen);
|
||||||
|
- **oder** `input`-Regeln auf beiden Switches nachziehen (der Branch braucht sie ohnehin — das ist die
|
||||||
|
eigentliche Lücke, die dieses TP nebenbei aufgedeckt hat);
|
||||||
|
- **oder** rein physisch: abschließbarer Schrank, Port beschriftet und dokumentiert.
|
||||||
|
|
||||||
|
⚖️ **Meine Einschätzung:** die `input`-Regeln sind der richtige Weg, weil sie eine Lücke schließen, die
|
||||||
|
**unabhängig von diesem TP** besteht. Der OOB-Port macht sie nur sichtbar.
|
||||||
|
|
||||||
|
#### Abgrenzung
|
||||||
|
- **Nicht** Teil von TP16.3 — dort wird nur festgestellt, dass dreimal Geräte rebooten, deren Mgmt am
|
||||||
|
Core hängt. TP37 entschärft das für künftige Sessions, ist aber **keine Voraussetzung** dafür.
|
||||||
|
- **Nicht** VLAN-Schema-Änderung (TP32 bleibt unberührt), **nicht** Konsolen-/Serial-Zugang (das wäre
|
||||||
|
echtes OOB im engeren Sinn; hier geht es um ein zweites *Netz*-Bein).
|
||||||
|
- **Abh.:** TP5 (Firewall-Systematik, liefert das Muster für die `input`-Regeln), TP15 (Portbelegung
|
||||||
|
beider CRS326), TP22 (die Umsetzung läuft über Golden Config, `ip-firewall-filter` und
|
||||||
|
`bridge`-Sektionen stehen unter Drift-Kontrolle).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 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)
|
||||||
|
|||||||
671
docs/tp16.3-bf-public-ap-connector-runbook.md
Normal file
671
docs/tp16.3-bf-public-ap-connector-runbook.md
Normal file
@@ -0,0 +1,671 @@
|
|||||||
|
# TP16.3 / TP31 Schritt 5 — Runbook: `bf-public`-Connector auf ap1 / ap2 / **ap3**, CNAME-Flip, Abbau
|
||||||
|
|
||||||
|
> **§1–§3 AUSGEFÜHRT 2026-08-31.** Drei Connectoren laufen, `bf-public` healthy/12.
|
||||||
|
> §2.2, §3.4 und §3.5 sind unten **korrigiert** — die ursprüngliche Fassung war an drei
|
||||||
|
> Stellen falsch (Flash-Budget, `docker save`-Format, `envs`-Parametername).
|
||||||
|
> Offen bleiben **§5 (Flip)** und **§6 (Abbau)**.
|
||||||
|
> **Löst [`tp31-schritt5-runbook.md`](tp31-schritt5-runbook.md) ab** (Stand 08-27). Jenes Dokument
|
||||||
|
> kennt ap3 nicht, unterstellt einen Registry-Pull (geht bei `fetch: no` nicht) und enthält **eine
|
||||||
|
> nachweislich falsche Freigabe-Behauptung** (§8 Falle 2). Es bleibt als Historie liegen, ist aber
|
||||||
|
> **nicht mehr auszuführen**.
|
||||||
|
|
||||||
|
**Charakter:** Vor-Ort-Session. Der `device-mode`-Wechsel verlangt an **jedem** der drei APs eine
|
||||||
|
**physische** Bestätigung — ap1 + ap3 in **Technik UG**, ap2 in **Technik OG**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §0 Ist-Stand (live gelesen 2026-08-30, ~20:45)
|
||||||
|
|
||||||
|
### Die drei APs — **sie sind NICHT gleich**
|
||||||
|
|
||||||
|
| AP | IP | Modell | Arch | ROS **live** | Flash frei | RAM frei | `device-mode` | `container` | `fetch` |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| **ap1** | 10.40.99.11 | hAP ax² (`C52iG`) | arm64 | **7.24.1** | 95 MB / 128 | 712 MB | `home` | ❌ `no` | ❌ `no` |
|
||||||
|
| **ap2** | 10.40.99.12 | hAP ax² (`C52iG`) | arm64 | **7.24.1** | 95 MB / 128 | 699 MB | `home` | ❌ `no` | ❌ `no` |
|
||||||
|
| **ap3** | 10.40.99.13 | **cAP ax** (`cAPGi`) | arm64 | **7.24.1** | 96 MB / 128 | 689 MB | **`basic`** | ❌ `no` | ✅ `yes` |
|
||||||
|
|
||||||
|
*(Versionsspalte am **2026-08-31** nachgelesen: Christoph hat alle fünf MikroTik auf **7.24.1** gehoben,
|
||||||
|
RouterBOOT mitgezogen. Bis 08-30 liefen ap1/ap2 auf 7.24 — daher stand hier vorher eine Zwei-Dateien-Regel.)*
|
||||||
|
|
||||||
|
✅ **Seit dem Upgrade auf 7.24.1 (2026-08-31) reicht EINE `.npk` für alle drei APs** — alle sind `arm64`
|
||||||
|
und alle auf derselben Version. Die frühere Zwei-Dateien-Regel war **rein versionsbedingt**, nicht
|
||||||
|
modellbedingt: `cAP ax` und `hAP ax²` teilen sich die `arm64`-Pakete.
|
||||||
|
🔴 **Die Regel dahinter gilt weiter:** ein `container.npk` **muss exakt zur RouterOS-Version passen** —
|
||||||
|
sonst bleibt es nach dem Reboot deaktiviert. Wer künftig einen AP einzeln hebt, hat wieder zwei Dateien.
|
||||||
|
|
||||||
|
🟡 **Ein Unterschied bleibt:**
|
||||||
|
- **ap3 steht auf `device-mode=basic`**, nicht `home` — dort sind `fetch`/`scheduler` bereits erlaubt.
|
||||||
|
Das ändert am `container`-Flag nichts (auch dort `no`, auch dort physische Bestätigung), macht ap3
|
||||||
|
aber zum einzigen AP, der theoretisch aus der Registry ziehen könnte. **Wir nutzen das bewusst
|
||||||
|
nicht** — gleicher `.tar`-Weg auf allen dreien, damit alle drei dasselbe Image fahren.
|
||||||
|
|
||||||
|
*(Auf ap3 lief zum Lesezeitpunkt 4 h Uptime — frisch in Betrieb. Uplink laut Registry `core ether21`.)*
|
||||||
|
|
||||||
|
### Cloudflare
|
||||||
|
|
||||||
|
| Tunnel | ID | Status | Connectoren |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **bf-public** | `48d3b0b6-7233-4781-a76a-1de4a5e957ce` | **inactive** | **0** ← Ziel dieser Session |
|
||||||
|
| bf-mm | `7a322261-380a-447b-97d2-8ebce99f5ed1` | healthy | 8 |
|
||||||
|
| bf-nas | `220c9958-401b-4111-9451-f3db09bba2d5` | healthy | 8 |
|
||||||
|
| bf-pve | `f7fdf3a6-b67d-4567-93b1-b0f273794d09` | healthy | 8 |
|
||||||
|
| `fm.emsenhuber.net` | `b697d124-…` | healthy | 4 | 🚫 **Standort FM — nicht anfassen** |
|
||||||
|
| `terpene-db` | `b83b68f0-…` | healthy | 4 | 🚫 **anderes Projekt — nicht anfassen** |
|
||||||
|
|
||||||
|
**Heutige Wege der vier öffentlichen Namen** (alle noch **an Caddy vorbei**, direkt aufs Backend):
|
||||||
|
|
||||||
|
| Name | CNAME → Tunnel | Ingress-Ziel |
|
||||||
|
|---|---|---|
|
||||||
|
| `n8n` | bf-mm | `http://10.40.20.167:5678` |
|
||||||
|
| `gitea` | bf-nas | `http://10.40.20.157:3000` |
|
||||||
|
| `vw` | bf-nas | `http://10.40.20.127` |
|
||||||
|
| `hass` | bf-pve | `http://10.40.20.105:8123` |
|
||||||
|
|
||||||
|
**`bf-public` hat bereits alle vier Ingress-Regeln** → `http://10.40.20.40:80` (Caddy) + Catch-all 404.
|
||||||
|
⚠️ Genau dieses `:80` ist Blocker A, siehe §1.1.
|
||||||
|
|
||||||
|
### Caddy (`caddy-bf1`, LXC 130, `10.40.20.40`) — gemessen
|
||||||
|
|
||||||
|
| Name | `:80` | `:443` (SNI korrekt) |
|
||||||
|
|---|---|---|
|
||||||
|
| `vw` / `hass` / `n8n` / `gitea` | **308 → `https://…`** | **200** |
|
||||||
|
|
||||||
|
### Die sechs Container, die am Ende weg müssen — alle drei Hosts verifiziert
|
||||||
|
|
||||||
|
| Host | Zugang | Container |
|
||||||
|
|---|---|---|
|
||||||
|
| LXC 400 (pve) | `ssh root@10.40.20.100` → `pct exec 400 -- docker` | `cloudflared-bfmm`, `cloudflared-bfnas` |
|
||||||
|
| macmini | `ssh macmini` | `cloudflared-bfpve`, `cloudflared-bfnas` |
|
||||||
|
| NAS DS923 | Portainer `endpointId=14` | `cloudflared-bfpve`, `cloudflared-bfmm` |
|
||||||
|
|
||||||
|
🚫 **`terpene-cloudflared` auf LXC 400 bleibt** (Tunnel `terpene-db`, anderes Projekt). Steht im
|
||||||
|
`docker ps` **direkt neben** den beiden zu löschenden — beim Löschen den Namen lesen, nicht die Zeile zählen.
|
||||||
|
|
||||||
|
### Rollback-Material (2026-08-30 **neu angelegt**, es gab es vorher nicht)
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/cf-backup/bf-public-ingress-20260830-vor-tp16.3-schnitt.json
|
||||||
|
docs/cf-backup/bf-mm-ingress-20260830-vor-tp16.3-schnitt.json
|
||||||
|
docs/cf-backup/bf-nas-ingress-20260830-vor-tp16.3-schnitt.json
|
||||||
|
docs/cf-backup/bf-pve-ingress-20260830-vor-tp16.3-schnitt.json
|
||||||
|
docs/cf-backup/cnames-tunnel-20260830-vor-tp16.3-schnitt.json # inkl. Record-IDs für den Flip-Rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1 Drei Blocker — ✅ **ALLE ERLEDIGT 2026-08-31 (remote)**
|
||||||
|
|
||||||
|
> ✅ **Nichts davon ist vor Ort noch zu tun.** Abschnitt bleibt als Nachweis stehen; die Fixes sind
|
||||||
|
> gemessen, nicht nur abgesetzt. **Vor Ort startet die Arbeit bei §2/§3.**
|
||||||
|
>
|
||||||
|
> | Blocker | Stand | Nachweis |
|
||||||
|
> |---|---|---|
|
||||||
|
> | **A** Redirect-Schleife (`:80`) | ✅ behoben | Ingress zeigt auf `https://10.40.20.40:443`, `originServerName` je Regel gesetzt |
|
||||||
|
> | **B** Freigabe `vlan99 → vlan20:443` | ✅ behoben | Regel via Nautobot GC deployt, Plan `Completed`, am Gerät verifiziert, **135/135 compliant, 0 Drifts** |
|
||||||
|
> | **C** `ROUTER_AP3_PASS` | ✅ behoben | in `~/.secrets/credentials.env` (`chmod 600`), REST-Auth gegen ap3 = **HTTP 200** |
|
||||||
|
|
||||||
|
### 1.1 ✅ Blocker A — `bf-public` zeigte auf `:80`, und Caddy antwortet dort mit 308
|
||||||
|
|
||||||
|
> **Erledigt 2026-08-31.** Ingress zurückgelesen: alle vier Namen → `https://10.40.20.40:443`,
|
||||||
|
> `SNI` = der jeweilige Hostname. Der `PUT` unten ist der ausgeführte Stand, nicht mehr eine Aufgabe.
|
||||||
|
|
||||||
|
Gemessen: `curl -H "Host: vw.emsenhuber.net" http://10.40.20.40/` → **`308` → `Location: https://vw.emsenhuber.net/`**.
|
||||||
|
|
||||||
|
Der Weg wäre also: Browser → CF → Tunnel → Caddy `:80` → **308 zurück auf denselben öffentlichen
|
||||||
|
Namen** → CF → Tunnel → Caddy `:80` → … ⇒ **`ERR_TOO_MANY_REDIRECTS`**, für alle vier Namen.
|
||||||
|
|
||||||
|
Das fällt heute niemandem auf, weil die drei Alttunnel **an Caddy vorbei** direkt aufs Backend gehen
|
||||||
|
(§0) — die Schleife entsteht **erst** mit dem Umstieg auf Caddy als Origin.
|
||||||
|
|
||||||
|
**Fix:** Origin auf `https://10.40.20.40:443` mit **`originServerName` je Regel** (Caddy hält für alle
|
||||||
|
vier Namen gültige LE-Zertifikate ⇒ **kein** `noTLSVerify` nötig, die Kette bleibt geprüft):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
TID=48d3b0b6-7233-4781-a76a-1de4a5e957ce
|
||||||
|
curl -s -X PUT -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$TID/configurations" \
|
||||||
|
-d "{\"config\":{\"ingress\":[
|
||||||
|
{\"hostname\":\"vw.emsenhuber.net\",\"service\":\"https://10.40.20.40:443\",\"originRequest\":{\"originServerName\":\"vw.emsenhuber.net\"}},
|
||||||
|
{\"hostname\":\"hass.emsenhuber.net\",\"service\":\"https://10.40.20.40:443\",\"originRequest\":{\"originServerName\":\"hass.emsenhuber.net\"}},
|
||||||
|
{\"hostname\":\"n8n.emsenhuber.net\",\"service\":\"https://10.40.20.40:443\",\"originRequest\":{\"originServerName\":\"n8n.emsenhuber.net\"}},
|
||||||
|
{\"hostname\":\"gitea.emsenhuber.net\",\"service\":\"https://10.40.20.40:443\",\"originRequest\":{\"originServerName\":\"gitea.emsenhuber.net\"}},
|
||||||
|
{\"service\":\"http_status:404\"}]}}" | jq -r ".success"'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Abnahme:** `.success == true`, danach Ingress zurücklesen und die vier `https://…:443` sehen.
|
||||||
|
|
||||||
|
### 1.2 ✅ Blocker B — die Firewall liess MGMT → Caddy **nicht** durch
|
||||||
|
|
||||||
|
> **Erledigt 2026-08-31 über Nautobot Golden Config** (`gc-manual-plan.py --device crs326-core --deploy`,
|
||||||
|
> Change-ID `TP16.3-BlockerB`, Plan `23e5b667-…` **Completed**), **nicht** per Direkt-Write — `ip-firewall-filter`
|
||||||
|
> steht unter Drift-Kontrolle (TP26). Backup vorher: `core-pre-tp16.3-blockerB-20260831.backup` (TP22-Regel 3).
|
||||||
|
> Danach `ingest-core-firewall.py --apply` (Re-Baseline) + `--refresh-compliance` ⇒ **135/135, 0 Drifts.**
|
||||||
|
> **Position verifiziert:** die Regel sitzt direkt **vor** `TP5: log-only` und `TP5: deny-by-default`.
|
||||||
|
> **Funktional verifiziert:** `/tool fetch` von ap3 auf `https://10.40.20.40` erreicht Caddy und bekommt
|
||||||
|
> einen **TLS-Alert von Caddy selbst** — ein Alert kann nur vom Server kommen, das Paket ist also da.
|
||||||
|
> Der Alert ist die **SNI-Abweisung** (Gegenprobe: ohne SNI `000`, mit korrektem SNI `200`) und genau
|
||||||
|
> der Grund, warum `originServerName` in §1.1 gesetzt ist. **Kein Fehler, sondern der Beweis.**
|
||||||
|
|
||||||
|
Die `forward`-Kette des Core wurde vollständig gelesen (36 Regeln, Nr. 35 = `TP5: deny-by-default`).
|
||||||
|
**Es gibt keine Regel `vlan99 → vlan20` für 80/443.** Die einzige Regel in dieser Richtung ist
|
||||||
|
|
||||||
|
```
|
||||||
|
26 TP5-E2: MGMT->INFRA api-rueckweg
|
||||||
|
chain=forward accept protocol=tcp in-interface=vlan99 out-interface=vlan20 src-port=8728
|
||||||
|
```
|
||||||
|
|
||||||
|
— ein **Rückweg** der RouterOS-API, `src-port`, nicht `dst-port`. Sie trägt den Connector nicht.
|
||||||
|
|
||||||
|
🔴 **Der Vorgänger-Runbook behauptet in §0 wörtlich, „Regel 27 `MGMT→INFRA TCP` deckt `vlan99→vlan20:80`
|
||||||
|
bereits ab ✅". Das ist falsch** — Regel 27 ist `IOT->INFRA http-rueckweg` (`in-interface=vlan30`).
|
||||||
|
Ohne die Regel unten läuft der Connector in Regel 34 (`TP5-WOULD-DROP`, Log) und Regel 35 (Drop).
|
||||||
|
|
||||||
|
**Fix** (vor die Log-Regel setzen, nicht ans Ende):
|
||||||
|
|
||||||
|
```
|
||||||
|
/ip firewall filter add chain=forward action=accept protocol=tcp \
|
||||||
|
in-interface=vlan99 out-interface=vlan20 dst-address=10.40.20.40 dst-port=443 \
|
||||||
|
comment="TP31: AP-cloudflared -> caddy-bf1" \
|
||||||
|
place-before=[find comment~"TP5: log-only"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Über MikroMCP: `plan_changes` mit `dryRun:true`, dann `apply_plan`.
|
||||||
|
|
||||||
|
**Was NICHT gebraucht wird:**
|
||||||
|
- **Egress zu Cloudflare** (QUIC udp/7844): Regel 13 `TP5-E2: LAN->WAN` (`out-interface-list=WAN`,
|
||||||
|
ohne `in-interface`-Einschränkung) deckt das ab. ✅
|
||||||
|
- **DNS für den Container**: Regeln 30–33 erlauben `LAN → 10.40.20.36/.37:53` aus **jedem** VLAN. ✅
|
||||||
|
⇒ im Container `dns=10.40.20.36` setzen.
|
||||||
|
|
||||||
|
⚠️ **Damit die Regel greift, muss der Container-Verkehr auch wirklich als `vlan99` ankommen** — das
|
||||||
|
`veth`-Netz `172.17.0.0/24` ist dem Core unbekannt und hätte keinen Rückweg. Deshalb ist das
|
||||||
|
`masquerade` in §3.4 **nicht optional**, sondern die Voraussetzung dieser Regel.
|
||||||
|
|
||||||
|
### 1.3 ✅ Blocker C — `ROUTER_AP3_PASS` fehlte in `~/.secrets/credentials.env`
|
||||||
|
|
||||||
|
> **Erledigt 2026-08-31.** `ROUTER_AP3_USER`/`_PASS` nachgetragen, Datei `chmod 600`,
|
||||||
|
> Sicherung `~/.secrets/credentials.env.bak-20260831`. Funktionsbeweis: REST-Auth gegen ap3 = **HTTP 200**.
|
||||||
|
|
||||||
|
Vorhanden sind `ROUTER_AP1_PASS`, `ROUTER_AP2_PASS`, `CRS326_CORE_PASS`, `CRS326_BRANCH_PASS`.
|
||||||
|
Das **ap3-Passwort steht nur in `~/.claude.json`** (MCP-Env) — gegen die globale Konvention
|
||||||
|
(`~/.secrets` ist der Credential-Store) **und** ein Problem für §3.2: der Reboot-Weg liest
|
||||||
|
`SSHPASS` aus `~/.secrets`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Wert aus ~/.claude.json übernehmen und ergänzen — Wert dabei NICHT auf stdout bringen:
|
||||||
|
printf 'ROUTER_AP3_USER=admin\nROUTER_AP3_PASS=%s\n' "$(python3 -c "
|
||||||
|
import json;print(json.load(open('$HOME/.claude.json'))['mcpServers']['mikromcp']['env']['ROUTER_AP3_PASS'])")" \
|
||||||
|
>> ~/.secrets/credentials.env
|
||||||
|
chmod 600 ~/.secrets/credentials.env
|
||||||
|
grep -c '^ROUTER_AP3_PASS=' ~/.secrets/credentials.env # muss 1 sein
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **`~/.secrets/credentials.env` ist kein sauberes `KEY=VALUE`-File** — ab Zeile ~69 stehen rohe
|
||||||
|
WireGuard-Zeilen (`Endpoint:`, `AllowedIPs:`, `PersistentKeepalive:`). In **zsh** bricht
|
||||||
|
`source` daran ab und lässt die Variablen **leer**, ohne Fehler. Alle Snippets hier laufen deshalb
|
||||||
|
in `bash -c '…'`. **Wer das übersieht, jagt einen „ungültigen Token", der in Wahrheit ein leerer ist.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2 Vorbereitung am Schreibtisch (vor der Fahrt, mit Internet)
|
||||||
|
|
||||||
|
### 2.1 Die `container.npk` holen — **eine Datei für alle drei APs**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/tp16.3 && cd ~/tp16.3
|
||||||
|
curl -fLO https://download.mikrotik.com/routeros/7.24.1/all_packages-arm64-7.24.1.zip
|
||||||
|
unzip -o -j all_packages-arm64-7.24.1.zip 'container-7.24.1-arm64.npk'
|
||||||
|
ls -la container-7.24.1-arm64.npk
|
||||||
|
```
|
||||||
|
*(URL am 2026-08-30 mit `HTTP 200` verifiziert.)*
|
||||||
|
|
||||||
|
⚠️ **Vor dem Upload gegenprüfen, dass das Gerät wirklich auf 7.24.1 steht** (`/system resource print`).
|
||||||
|
Stimmt die Version nicht, ist die Datei die falsche — und das merkt man erst nach dem Reboot.
|
||||||
|
|
||||||
|
### 2.2 cloudflared als `.tar` bauen — 🔴 **KORRIGIERT 2026-08-31, die alte Fassung funktioniert nicht**
|
||||||
|
|
||||||
|
Zwei Gründe, warum `docker save` des offiziellen Images hier scheitert:
|
||||||
|
|
||||||
|
1. **Es passt nicht ins Flash.** Entpackt misst `cloudflare/cloudflared:2026.6.1`
|
||||||
|
**72.926.208 B = 69,5 MiB** (`docker export | wc -c`), nicht die früher hier genannten 28 MB —
|
||||||
|
das ist die *komprimierte* Archivgrösse. RouterOS braucht beim Import **Tar und entpacktes Abbild
|
||||||
|
gleichzeitig**: 26,2 + 69,5 = **95,7 MiB gegen ~90 MiB frei**. ⚠️ **`docker image inspect .Size`
|
||||||
|
lügt hier** (meldet 27.463.950); `docker history` summiert korrekt auf ~70 MB.
|
||||||
|
2. **Das Format stimmt nicht.** Seit Docker 25 (containerd-Image-Store) schreibt `docker save` ein
|
||||||
|
**OCI-Layout** (`blobs/sha256/`, gzip-Layer). RouterOS will docker-v1 und meldet
|
||||||
|
**`no config found in manifest`** — klingt nach kaputtem Image, ist aber das Format.
|
||||||
|
|
||||||
|
**Stattdessen: Minimal-Image aus dem Binary bauen.** Das Binary im offiziellen Image ist
|
||||||
|
**statisch gelinkt** und 36,8 MB gross; die restlichen ~33 MB sind distroless-Debian, das
|
||||||
|
cloudflared nicht braucht.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Binary + CA-Bundle aus dem gepinnten Upstream-Image ziehen
|
||||||
|
ssh macmini 'docker pull --platform linux/arm64 \
|
||||||
|
cloudflare/cloudflared@sha256:6d91c121b803126f7a5344005d17a9324788fc09d305b6e2560ec6040a7ae283 \
|
||||||
|
&& docker tag cloudflare/cloudflared@sha256:6d91c121b803126f7a5344005d17a9324788fc09d305b6e2560ec6040a7ae283 cfd:2026.6.1'
|
||||||
|
ssh macmini 'C=$(docker create cfd:2026.6.1); \
|
||||||
|
docker cp "$C":/usr/local/bin/cloudflared /tmp/cfd-bin; \
|
||||||
|
docker cp "$C":/etc/ssl/certs/ca-certificates.crt /tmp/cfd-ca.crt; docker rm -f "$C"'
|
||||||
|
scp macmini:/tmp/cfd-bin ~/tp16.3/cloudflared-linux-arm64
|
||||||
|
scp macmini:/tmp/cfd-ca.crt ~/tp16.3/ca-certificates.crt
|
||||||
|
|
||||||
|
# docker-v1-Archiv direkt bauen (kein Docker-Umweg -> Format haengt nicht am Image-Store)
|
||||||
|
python3 tools/build-cfd-image.py ~/tp16.3/cloudflared-linux-arm64 \
|
||||||
|
~/tp16.3/ca-certificates.crt ~/tp16.3/cfd-min-arm64.tar
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ergebnis: 35,3 MiB Archiv, 35,3 MiB entpackt, Spitzenbedarf 70,6 MiB** — passt mit ~20 MiB Reserve,
|
||||||
|
nach dem Löschen des Tars bleiben **54–55 MiB** frei.
|
||||||
|
|
||||||
|
🔑 **Der Pin ist die Binary-SHA256 `e93824f677eb2f072b0a42b7ad8b57e1ed76c6aed51b13ed805a6064a82d424e`.**
|
||||||
|
cloudflared gibt sie beim Start selbst aus (`Version 2026.6.1 (Checksum e93824f6…)`) — damit ist im
|
||||||
|
Log nachweisbar, dass alle drei APs dasselbe Binary fahren.
|
||||||
|
|
||||||
|
⚠️ **Gegenprobe vor dem Ausrollen:** `docker load -i` + `docker run --rm <tag> version` auf dem macmini.
|
||||||
|
Ein Formatfehler fällt dort in Sekunden auf, auf dem AP erst nach einem 35-MiB-Upload.
|
||||||
|
|
||||||
|
### 2.3 Tunnel-Token bereitlegen
|
||||||
|
|
||||||
|
Der Token liegt bereits als `BF_PUBLIC_TUNNEL_TOKEN` in `~/.secrets/credentials.env`. Frisch ziehen
|
||||||
|
(z. B. nach Token-Rotation) — **Ausgabe nie in eine Kommandozeile oder ins Transkript**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
TID=48d3b0b6-7233-4781-a76a-1de4a5e957ce
|
||||||
|
T=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$TID/token" | jq -r .result)
|
||||||
|
echo "Token-Länge: ${#T}" # nur die Länge, nie der Wert
|
||||||
|
printf "%s" "$T" > ~/tp16.3/bf-public.token; chmod 600 ~/tp16.3/bf-public.token'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Backups je AP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# je AP, über MikroMCP oder SSH:
|
||||||
|
/system backup save name=vor-tp16.3-ap1 # bzw. -ap2 / -ap3
|
||||||
|
/export file=vor-tp16.3-ap1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3 Vor Ort — **je AP einzeln, komplett durchziehen, dann der nächste**
|
||||||
|
|
||||||
|
> **Reihenfolge: ap3 → ap2 → ap1.**
|
||||||
|
> ap3 zuerst, weil dort am wenigsten hängt (frisch in Betrieb, 4 h Uptime) — wenn der Ablauf
|
||||||
|
> irgendwo klemmt, klemmt er am unkritischsten Gerät. **ap1 zuletzt**, weil an ihm die
|
||||||
|
> `crs326-core`-Seite des WLAN hängt.
|
||||||
|
>
|
||||||
|
> ⚠️ **Jeder Schritt hier trennt kurzzeitig das WLAN dieses APs** (zwei Reboots). Die CAPs holen
|
||||||
|
> sich ihre Config nach dem Boot vom Manager (`crs326-core`) per **L2-Discovery auf `vlan99`** —
|
||||||
|
> das kommt von selbst zurück, braucht aber ~1–2 min. **Nicht ungeduldig nachprovisionieren.**
|
||||||
|
|
||||||
|
### 3.1 `.npk` hochladen
|
||||||
|
|
||||||
|
| AP | Datei |
|
||||||
|
|---|---|
|
||||||
|
| ap1 (`.99.11`) | `container-7.24.1-arm64.npk` |
|
||||||
|
| ap2 (`.99.12`) | `container-7.24.1-arm64.npk` |
|
||||||
|
| ap3 (`.99.13`) | `container-7.24.1-arm64.npk` |
|
||||||
|
|
||||||
|
✅ **Seit 2026-08-31 dieselbe Datei überall** (alle drei `arm64` + 7.24.1).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/tp16.3
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
SSHPASS="$ROUTER_AP3_PASS" sshpass -e scp -O container-7.24.1-arm64.npk admin@10.40.99.13:'
|
||||||
|
```
|
||||||
|
|
||||||
|
🔴 **Niemals `sshpass -p`** — das schreibt das Passwort in die Kommandozeile und damit in `ps` und
|
||||||
|
jeden argv-Trace. **Genau so ist am 2026-08-22 das Core-Passwort in ein Transkript geraten.**
|
||||||
|
`-e` hält es in der Umgebung des Kindprozesses.
|
||||||
|
|
||||||
|
Gegenprobe vor dem Reboot — **die Datei muss wirklich liegen**, sonst rebootet man umsonst:
|
||||||
|
```
|
||||||
|
/file print where name~"container"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Reboot (installiert das Paket)
|
||||||
|
|
||||||
|
🔴 **Der Reboot geht NICHT über MikroMCP.** Das `reboot`-Tool sendet einen `delay`-Parameter, den die
|
||||||
|
REST-API auf ROS 7.24 nicht kennt (`unknown parameter delay`); `run_command` ist für `/system reboot*`
|
||||||
|
per Deny-Policy gesperrt. Der Weg ist SSH:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
SSHPASS="$ROUTER_AP3_PASS" sshpass -e ssh -o StrictHostKeyChecking=accept-new \
|
||||||
|
admin@10.40.99.13 "/system reboot"'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Abnahme nach ~90 s:**
|
||||||
|
```
|
||||||
|
/system package print
|
||||||
|
# muss jetzt DREI Pakete zeigen: routeros, wifi-qcom, container — alle gleiche Version
|
||||||
|
```
|
||||||
|
⚠️ **Steht `container` da mit abweichender Version oder `disabled=yes`, war es die falsche `.npk`.**
|
||||||
|
Dann: Datei löschen, richtige hochladen, erneut rebooten. Nicht weitermachen.
|
||||||
|
|
||||||
|
### 3.3 `device-mode` — **der physische Schritt**
|
||||||
|
|
||||||
|
🔴 **Reihenfolge ist Pflicht: erst der Paket-Reboot (§3.2), dann `device-mode`.**
|
||||||
|
Der Bestätigungs-Boot **installiert keine Pakete** — er verbraucht eine wartende `.npk`, ohne sie zu
|
||||||
|
installieren. Am 2026-08-31 wurde auf ap1 versucht, beides in einem Stromzyklus zu erledigen (um einen
|
||||||
|
Gang zu sparen): danach stand `container: yes`, die Datei war weg und `/system package print` zeigte
|
||||||
|
**zwei** Pakete. Nachholen ging per SSH-Reboot, aber die Abkürzung spart nichts.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
/system device-mode update container=yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Session **bricht danach ab** und RouterOS wartet auf eine Bestätigung **am Gerät** mit einer
|
||||||
|
Frist (typisch **5 Minuten**, die Konsole nennt die genaue Deadline). Bestätigt wird durch:
|
||||||
|
|
||||||
|
**Weg A — Kaltstart (empfohlen, eindeutig):**
|
||||||
|
> **Stecker ziehen, ~5 Sekunden warten, wieder einstecken.**
|
||||||
|
|
||||||
|
🔴 **Wichtig: die APs hängen NICHT an PoE aus den Switches** — beide CRS326 haben nachweislich gar
|
||||||
|
kein PoE-Menü (`bad command name poe`). Es gibt also **keinen** Weg, das aus der Ferne zu tun; der
|
||||||
|
Strom kommt vom jeweiligen Netzteil bzw. Injektor am AP.
|
||||||
|
|
||||||
|
**Weg B — Taster:**
|
||||||
|
> **Reset-Knopf am laufenden Gerät kurz drücken und loslassen.** Am hAP ax² sitzt er an der
|
||||||
|
> Unterseite, am cAP ax unter der Abdeckung an der Rückseite.
|
||||||
|
|
||||||
|
⚠️ **Weg B nur am LAUFENDEN Gerät und nur kurz.** Den Reset-Knopf **während des Bootens** gedrückt zu
|
||||||
|
halten ist etwas völlig anderes — das setzt die Konfiguration zurück bzw. geht nach Netinstall.
|
||||||
|
Wer unsicher ist, nimmt **Weg A**; ein Kaltstart kann nicht versehentlich zum Werksreset werden.
|
||||||
|
|
||||||
|
Verstreicht die Frist ohne Bestätigung, passiert **nichts** — die Änderung verfällt, das Gerät bleibt
|
||||||
|
wie es war. Dann einfach neu absetzen.
|
||||||
|
|
||||||
|
**Abnahme:**
|
||||||
|
```
|
||||||
|
/system device-mode print
|
||||||
|
# container: yes
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Container-Netz anlegen
|
||||||
|
|
||||||
|
```
|
||||||
|
/interface veth add name=veth-cf address=172.17.0.2/24 gateway=172.17.0.1
|
||||||
|
/ip address add address=172.17.0.1/24 interface=veth-cf
|
||||||
|
/ip firewall nat add chain=srcnat action=masquerade src-address=172.17.0.0/24 \
|
||||||
|
comment="TP31: cloudflared egress"
|
||||||
|
```
|
||||||
|
|
||||||
|
Das `masquerade` ist **Pflicht**, nicht Kosmetik — siehe §1.2: ohne es kommen die Pakete mit
|
||||||
|
`172.17.0.2` am Core an, wo weder Route noch Firewall-Regel sie kennt.
|
||||||
|
|
||||||
|
### 3.5 Image importieren und Container bauen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .tar hochladen (~28 MB)
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
SSHPASS="$ROUTER_AP3_PASS" sshpass -e scp -O ~/tp16.3/cloudflared-arm64.tar admin@10.40.99.13:'
|
||||||
|
```
|
||||||
|
|
||||||
|
Auf dem AP:
|
||||||
|
```
|
||||||
|
/container config set tmpdir=tmp
|
||||||
|
/container add file=cloudflared-arm64.tar interface=veth-cf root-dir=cf \
|
||||||
|
dns=10.40.20.36 logging=yes start-on-boot=yes \
|
||||||
|
cmd="tunnel --no-autoupdate run --token=<TOKEN>"
|
||||||
|
```
|
||||||
|
|
||||||
|
🔑 **Token-Handling.** 🔴 **Die Eigenschaft heisst auf ROS 7.24.1 `list=`, NICHT `name=`** — mit
|
||||||
|
`name=` antwortet das Gerät `bad parameter name` bzw. die REST-API `unknown parameter name`.
|
||||||
|
(Das MikroMCP-Tool `manage_container_env` benutzt `name=` und ist damit hier unbrauchbar.)
|
||||||
|
|
||||||
|
Der Token darf weder in eine Kommandozeile (→ `ps`) noch ins Sitzungstranskript. Deshalb per Datei:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
T=$(cat ~/tp16.3/bf-public.token)
|
||||||
|
TMP=$(mktemp /tmp/cfdenv.XXXXXX.rsc); chmod 600 "$TMP"
|
||||||
|
{ printf '/container/envs/add list=cfd key=TUNNEL_TOKEN value="%s"\n' "$T"
|
||||||
|
printf '/container/envs/add list=cfd key=TUNNEL_TRANSPORT_PROTOCOL value="quic"\n'; } > "$TMP"
|
||||||
|
SSHPASS="$PW" sshpass -e scp -O "$TMP" admin@<ip>:cfdenv.rsc && rm -f "$TMP"
|
||||||
|
```
|
||||||
|
Auf dem AP: `/import file=cfdenv.rsc`, danach **`/file/remove [find name="cfdenv.rsc"]`** — die Datei
|
||||||
|
enthält den Token im Klartext. Gegenprobe ohne Wert-Ausgabe:
|
||||||
|
```
|
||||||
|
:foreach e in=[/container/envs/find] do={:put ([/container/envs/get $e list] . " / " . \
|
||||||
|
[/container/envs/get $e key] . " / len=" . [:len [/container/envs/get $e value]])}
|
||||||
|
```
|
||||||
|
⚠️ **Niemals `/container/envs/print`** — das gibt den Token aus.
|
||||||
|
|
||||||
|
Starten und aufräumen:
|
||||||
|
```
|
||||||
|
/container start [find root-dir=cf]
|
||||||
|
/container print # status: running
|
||||||
|
/file remove [find name~"cloudflared-arm64.tar"] # ~28 MB Flash zurückholen
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **Platzrechnung (korrigiert 2026-08-31, gemessen):** ~90 MiB frei − 35,3 MiB `.tar`
|
||||||
|
− 35,3 MiB entpacktes Abbild ≈ **20 MiB** in der Spitze, nach dem Löschen des Tars **54–55 MiB** frei.
|
||||||
|
🔴 **Mit dem offiziellen Image wären es 95,7 MiB Bedarf gegen 90 MiB frei — es passt schlicht nicht.**
|
||||||
|
Auch mit dem Minimal-Image gilt weiter: **ein Container pro AP**, kein zweiter.
|
||||||
|
|
||||||
|
### 3.6 Abnahme **auf diesem AP**
|
||||||
|
|
||||||
|
```
|
||||||
|
/log print where message~"cloudflared" or message~"tunnel"
|
||||||
|
# erwartet: "Registered tunnel connection" (mehrfach, je CF-Edge)
|
||||||
|
```
|
||||||
|
|
||||||
|
Und **die Strecke zu Caddy separat prüfen** — sie ist der Teil, den Blocker B betraf:
|
||||||
|
```
|
||||||
|
/tool fetch url="https://10.40.20.40/" check-certificate=no output=none
|
||||||
|
```
|
||||||
|
*(Auf ap1/ap2 ist `fetch` per `device-mode=home` gesperrt — dort stattdessen aus dem Container heraus
|
||||||
|
oder über den Drop-Log gegenprüfen, siehe §8.)*
|
||||||
|
|
||||||
|
**Der Drop-Log ist das erste Diagnosewerkzeug, nicht das letzte:**
|
||||||
|
`10.40.20.18:514`, Dateien auf dem Docker-LXC unter `/opt/tp5-syslog/log/`, Präfix `TP5-WOULD-DROP`.
|
||||||
|
Taucht dort `10.40.99.13 → 10.40.20.40:443` auf, fehlt die Regel aus §1.2.
|
||||||
|
|
||||||
|
### 3.7 Erst wenn dieser AP sauber ist: nächster AP
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4 Abnahme des Tunnels (nach allen drei APs)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/48d3b0b6-7233-4781-a76a-1de4a5e957ce" \
|
||||||
|
| jq "{status:.result.status, conns:(.result.connections|length)}"'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Kriterium: `status: "healthy"`, `conns` > 0** (erfahrungsgemäß 4 CF-Edge-Verbindungen je Connector,
|
||||||
|
bei drei APs also Richtung 12).
|
||||||
|
|
||||||
|
🔴 **`healthy` heißt „der Connector redet mit Cloudflare" — nicht „das Origin antwortet".** Der Beweis
|
||||||
|
für die Strecke bis Caddy ist §5.1, nicht diese Zahl. Genau diese Verwechslung hat beim
|
||||||
|
`grafana`-Fall monatelang einen toten Dienst gesund aussehen lassen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §5 CNAME-Flip — einzeln, mit Abnahme dazwischen
|
||||||
|
|
||||||
|
**Reihenfolge: `n8n` → `gitea` → `hass` → `vw`** (steigende Kritikalität; `vw` zuletzt).
|
||||||
|
Zone-ID `1b5bc02fdf61b6b989afde543f3f4134`.
|
||||||
|
|
||||||
|
### 5.1 Vorab: Testname statt Produktivname
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
Z=1b5bc02fdf61b6b989afde543f3f4134
|
||||||
|
curl -s -X POST -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/$Z/dns_records" \
|
||||||
|
-d "{\"type\":\"CNAME\",\"name\":\"test-tp163\",\"content\":\"48d3b0b6-7233-4781-a76a-1de4a5e957ce.cfargotunnel.com\",\"proxied\":true}" | jq -r .success'
|
||||||
|
```
|
||||||
|
Dazu **eine Ingress-Regel** für `test-tp163.emsenhuber.net` → `https://10.40.20.40:443` mit
|
||||||
|
`originServerName: vw.emsenhuber.net` in `bf-public` ergänzen. Dann **von außerhalb des LAN, ohne
|
||||||
|
WARP** aufrufen. Erwartet: Access-Login, **keine Redirect-Schleife**.
|
||||||
|
⇒ Erst wenn das steht, wird ein Produktivname angefasst. Danach Testname + Testregel wieder entfernen.
|
||||||
|
|
||||||
|
### 5.2 Flip je Name
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
Z=1b5bc02fdf61b6b989afde543f3f4134
|
||||||
|
N=n8n # danach: gitea, hass, vw
|
||||||
|
ID=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/$Z/dns_records?name=$N.emsenhuber.net" | jq -r ".result[0].id")
|
||||||
|
curl -s -X PATCH -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/$Z/dns_records/$ID" \
|
||||||
|
-d "{\"content\":\"48d3b0b6-7233-4781-a76a-1de4a5e957ce.cfargotunnel.com\",\"proxied\":true}" | jq -r .success'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 Abnahme je Name — **von außerhalb des LAN, WARP AUS**
|
||||||
|
|
||||||
|
- [ ] Aufruf über Mobilfunk (oder anderer Standort) → Access-Login → **200**
|
||||||
|
- [ ] **Kein** `ERR_TOO_MANY_REDIRECTS` (das wäre Blocker A)
|
||||||
|
- [ ] Caddy sieht den Request: `ssh root@10.40.20.100 'pct exec 130 -- journalctl -u caddy -n 20'`
|
||||||
|
- [ ] 15 min beobachten, kein 530/1033
|
||||||
|
|
||||||
|
🔴 **WARP muss aus sein.** Mit aktivem WARP läuft der Test über den Mesh und beweist den öffentlichen
|
||||||
|
Weg **nicht** — dieselbe Klasse Fehldiagnose wie beim ICMP-Fall 2026-08-13.
|
||||||
|
|
||||||
|
**Namensspezifisch:**
|
||||||
|
- **`gitea`** — Access gilt **nur der Web-UI**, Git-/API-Pfade laufen daran vorbei. **Vor** dem Flip
|
||||||
|
einen Push von außen testen, **nach** dem Flip denselben Push wiederholen.
|
||||||
|
- **`hass`** — die Companion-App kann kein Access-Login. Nach dem Flip App auf dem Handy **über
|
||||||
|
Mobilfunk** prüfen, nicht nur den Browser.
|
||||||
|
- **`vw`** — Bitwarden läuft auf **Access-Bypass**. **Vor** dem Flip prüfen, dass die Bypass-Policy
|
||||||
|
für `vw.emsenhuber.net` steht; sonst sperrt der Flip alle Bitwarden-Clients aus.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §6 Abbau — **frühestens nach 7 Tagen störungsfreiem Betrieb**
|
||||||
|
|
||||||
|
> Die drei Alttunnel sind die Rückfallebene. Solange sie stehen, ist jeder Flip eine
|
||||||
|
> Ein-Feld-Änderung zurück. **Nicht am selben Abend abbauen.**
|
||||||
|
|
||||||
|
### 6.1 Erst die Container stoppen, dann die Tunnel löschen
|
||||||
|
|
||||||
|
Umgekehrt genommen läuft man in 530er von Connectoren, die auf einen gelöschten Tunnel zeigen.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# LXC 400 auf pve — terpene-cloudflared NICHT anfassen
|
||||||
|
ssh root@10.40.20.100 'pct exec 400 -- docker rm -f cloudflared-bfmm cloudflared-bfnas'
|
||||||
|
ssh root@10.40.20.100 'pct exec 400 -- docker ps --format "{{.Names}}"' # terpene-cloudflared muss bleiben
|
||||||
|
|
||||||
|
# macmini
|
||||||
|
ssh macmini 'docker rm -f cloudflared-bfpve cloudflared-bfnas && docker ps --format "{{.Names}}"'
|
||||||
|
```
|
||||||
|
|
||||||
|
NAS (DS923, Portainer `endpointId=14`, Key aus `mcp/portainer/.env`):
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/dev/bf-infra/mcp/portainer/.env; set +a
|
||||||
|
K="${PORTAINER_API_KEY:-$PORTAINER_TOKEN}"; U="${PORTAINER_URL:-https://10.40.20.126:9443}"
|
||||||
|
for c in cloudflared-bfpve cloudflared-bfmm; do
|
||||||
|
ID=$(curl -sk -H "X-API-Key: $K" "$U/api/endpoints/14/docker/containers/json?all=true" \
|
||||||
|
| jq -r ".[]|select(.Names[0]==\"/$c\")|.Id")
|
||||||
|
curl -sk -X DELETE -H "X-API-Key: $K" "$U/api/endpoints/14/docker/containers/$ID?force=true"
|
||||||
|
echo "removed $c"
|
||||||
|
done'
|
||||||
|
```
|
||||||
|
⚠️ Sind es **Stacks** statt einzelner Container, kommen sie sonst beim nächsten Stack-Deploy zurück —
|
||||||
|
dann den Stack-Eintrag entfernen, nicht nur den Container.
|
||||||
|
|
||||||
|
### 6.2 Dann die drei Tunnel löschen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
for T in 7a322261-380a-447b-97d2-8ebce99f5ed1 220c9958-401b-4111-9451-f3db09bba2d5 f7fdf3a6-b67d-4567-93b1-b0f273794d09; do
|
||||||
|
curl -s -X DELETE -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$T" | jq -r "{id:\"$T\",ok:.success}"
|
||||||
|
done'
|
||||||
|
```
|
||||||
|
|
||||||
|
🚫 **Nicht in dieser Schleife und nirgends sonst:** `b697d124-…` (`fm.emsenhuber.net`, Standort FM)
|
||||||
|
und `b83b68f0-…` (`terpene-db`). **IDs vor dem Absetzen einzeln gegen §0 prüfen.**
|
||||||
|
|
||||||
|
### 6.3 Schlusskontrolle
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash -c 'set -a; . ~/.secrets/credentials.env; set +a
|
||||||
|
curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel?is_deleted=false" \
|
||||||
|
| jq -r ".result[]|\"\(.name)\t\(.status)\tconns=\(.connections|length)\""'
|
||||||
|
```
|
||||||
|
**Soll:** `bf-public healthy`, `fm.emsenhuber.net healthy`, `terpene-db healthy` — sonst nichts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §7 Rollback
|
||||||
|
|
||||||
|
| Lage | Rückweg |
|
||||||
|
|---|---|
|
||||||
|
| Ein Name kaputt nach Flip | CNAME zurück auf die alte Tunnel-ID — Werte in `cnames-tunnel-20260830-…json` (inkl. Record-ID) |
|
||||||
|
| `bf-public`-Ingress kaputt | `PUT …/configurations` mit `bf-public-ingress-20260830-…json` |
|
||||||
|
| Container auf einem AP stört | `/container stop` + `/container remove`, WLAN läuft davon unabhängig |
|
||||||
|
| AP-Config verkorkst | `/system backup load name=vor-tp16.3-apX` |
|
||||||
|
| Firewall-Regel unerwünscht | `/ip firewall filter remove [find comment~"TP31: AP-cloudflared"]` |
|
||||||
|
|
||||||
|
**`device-mode=container:yes` ist nicht Teil des Rollbacks.** Zurückdrehen ginge nur mit erneutem
|
||||||
|
physischem Gang. Es schadet nicht — ohne laufenden Container ist es ein gesetztes Flag, sonst nichts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §8 Fallen
|
||||||
|
|
||||||
|
1. **`container.npk` ist versionsgebunden.** Seit 08-31 sind alle drei APs auf 7.24.1 ⇒ **eine** Datei.
|
||||||
|
Weicht ein Gerät später ab, braucht es wieder seine eigene. Falsche Datei ⇒ Paket bleibt nach dem
|
||||||
|
Reboot **inaktiv, ohne Fehler**. **Nach jedem Reboot `/system package print` lesen**, nicht annehmen.
|
||||||
|
2. **Die Freigabe `vlan99 → vlan20:443` existiert nicht** — entgegen der Behauptung im
|
||||||
|
Vorgänger-Runbook. Ohne §1.2 landet der Connector im `TP5-WOULD-DROP`-Log. **Erst dort nachsehen.**
|
||||||
|
3. **Caddy antwortet auf `:80` mit 308.** Ein Ingress auf `:80` erzeugt eine Redirect-Schleife —
|
||||||
|
und zwar **erst** beim Umstieg auf Caddy, weil die Altwege am Proxy vorbeigehen.
|
||||||
|
4. **`healthy` ≠ „Dienst erreichbar".** Der Tunnelstatus sagt nichts über das Origin; die
|
||||||
|
Wildcard-Access-App `*.emsenhuber.net` liefert für **jeden** Namen ein sauberes 302. Ein toter
|
||||||
|
Dienst sieht so monatelang gesund aus (der `grafana`-Fall).
|
||||||
|
5. **Kein `sshpass -p`.** Nur `-e` mit `SSHPASS` aus der Umgebung.
|
||||||
|
6. **`~/.secrets/credentials.env` in zsh sourcen schlägt still fehl** (rohe WireGuard-Zeilen ab ~Z. 69)
|
||||||
|
— Variablen bleiben **leer**, die API meldet einen „ungültigen Token". Immer `bash -c`.
|
||||||
|
7. **`terpene-cloudflared` steht direkt neben den zu löschenden Containern.** Namen lesen.
|
||||||
|
8. **Ein Container pro AP, keiner mehr.** Nach dem Import bleiben ~39 MB. Caddy oder AGH gehören
|
||||||
|
nicht dazu (die eine Config ändert sich ständig, die andere schreibt dauernd auf NAND).
|
||||||
|
9. **Reboot nie über MikroMCP** (`unknown parameter delay`), sondern per SSH.
|
||||||
|
10. **WLAN-Abnahme ist eine frische DHCP-Lease je SSID, nicht die Registration-Table.**
|
||||||
|
⚠️ Ergänzung 08-31: `media`/`untrusted`/`guest` haben im Normalbetrieb **null** Clients — dort ist
|
||||||
|
gar kein Lease-Nachweis zu holen. Das ist kein Fehler, aber auch kein Beleg; nicht als „grün" buchen.
|
||||||
|
11. 🔴 **Das offizielle `cloudflared`-Image passt nicht ins Flash** (69,5 MiB entpackt gegen ~90 MiB
|
||||||
|
frei, Tar kommt obendrauf). Minimal-Image bauen, s. §2.2. **Flash-Budgets mit `docker export`
|
||||||
|
messen, nie mit `docker image inspect .Size`.**
|
||||||
|
12. 🔴 **`docker save` liefert seit Docker 25 OCI statt docker-v1** → `no config found in manifest`.
|
||||||
|
13. 🔴 **`/container/envs` nimmt `list=`, nicht `name=`.**
|
||||||
|
14. 🔴 **Der `device-mode`-Bestätigungs-Boot installiert keine Pakete** — Reihenfolge s. §3.3.
|
||||||
|
15. 🔴 **§3.4 kollidiert mit der GC-Governance:** `ip address` und `ip firewall nat` stehen unter
|
||||||
|
Drift-Kontrolle. Direkt gesetzt ⇒ **zwei `extra`-Drifts je AP**. Entweder vorher ins Soll heben
|
||||||
|
(AP-Template/ConfigContext) oder die Drift bewusst als offene Transaktion führen.
|
||||||
|
16. 🟡 **`A - AVAILABLE` in `/system package print` ist ein Download-Katalog**, kein Paket auf dem
|
||||||
|
Gerät — `enable` würde einen Internet-Fetch auslösen (auf ap1/ap2 gesperrt). `.npk` bleibt der Weg.
|
||||||
|
17. 🟡 **MikroMCPs `export_config` exportiert unvollständig** (301 B statt 6,2 KiB). Für Backups
|
||||||
|
`/export file=…` per SSH verwenden. Nach zwei
|
||||||
|
Reboots je AP lohnt der Blick — die Tabelle zeigte beim CAPsMAN-Ausfall 14 h lang zufriedene
|
||||||
|
Clients ohne Adresse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §9 Ehrliche Einschränkung — was diese Session **nicht** kauft
|
||||||
|
|
||||||
|
Solange Caddy **nur** auf `caddy-bf1` (LXC 130, pve) läuft, ist das Origin aller drei Connectoren
|
||||||
|
**derselbe Rechner**. Drei Connectoren auf drei APs kaufen damit **Hardware-Diversität am Eingang,
|
||||||
|
aber keine Verfügbarkeit**: fällt pve aus, sind alle vier öffentlichen Dienste weg — mit drei
|
||||||
|
Connectoren genauso vollständig wie mit einem.
|
||||||
|
|
||||||
|
**Der Gewinn ist real, aber begrenzt:** kein Docker-Host mehr im öffentlichen Pfad (die APs sind
|
||||||
|
eigene Maschinen ⇒ die macvlan/ipvlan-Blindheit kann dort prinzipiell nicht auftreten), sechs
|
||||||
|
Container werden drei, und die Host→Origin-Zuordnung lebt nur noch im `caddy/Caddyfile`.
|
||||||
|
|
||||||
|
**Der echte Verfügbarkeitsgewinn kommt erst mit `caddy-bf2` (LXC 131)** und einer zweiten
|
||||||
|
Ingress-Regel. ⚠️ Für `vw` gilt dabei die TP20.1-Bremse: zwei Caddys an verschiedenen Standorten
|
||||||
|
würden die Hälfte der **Schreib**zugriffe auf einen read-only Standby schicken — das #1033-Muster,
|
||||||
|
diesmal mit Datenverlust. **Automatisches Ingress-Failover erst, wenn der DB-Teil es trägt.**
|
||||||
|
|
||||||
|
Bis dahin ist die Reserve die manuelle: derselbe Connector kann über eine zusätzliche Ingress-Regel
|
||||||
|
**direkt** auf ein kritisches Backend zeigen, an Caddy vorbei.
|
||||||
124
docs/tp16.3-paragraph5-session-handoff-prompt.md
Normal file
124
docs/tp16.3-paragraph5-session-handoff-prompt.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# Übergabeprompt — TP16.3 §5: CNAME-Flip auf `bf-public`
|
||||||
|
|
||||||
|
> **Zum Einfügen in eine frische Session.** Erstellt 2026-08-31, direkt nach §2/§3.
|
||||||
|
> Alles unterhalb der Linie ist der Prompt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
TP16.3 / TP31 Schritt 5 — **§5 CNAME-Flip** (Repo `~/dev/bf-infra`).
|
||||||
|
|
||||||
|
Aufgabe: **§5 des Runbooks** `docs/tp16.3-bf-public-ap-connector-runbook.md` ausführen — die vier
|
||||||
|
öffentlichen Namen von den Alttunneln auf `bf-public` umhängen. **§6 (Abbau) gehört NICHT dazu**,
|
||||||
|
frühestens 7 Tage nach dem Flip.
|
||||||
|
|
||||||
|
## PFLICHTLEKTÜRE ZUERST, in dieser Reihenfolge
|
||||||
|
|
||||||
|
1. `docs/handoff-20260831-tp16.3-paragraph2-3-durch.md` — Ergebnis und Funde der Vorsession
|
||||||
|
2. `docs/tp16.3-bf-public-ap-connector-runbook.md` **§5** (und §7 Rollback)
|
||||||
|
3. `docs/teilprojekte.md` (Quick-Index + Ist-Stand + TP16.3-Abschnitt)
|
||||||
|
|
||||||
|
## AUSGANGSLAGE — nicht nochmal machen
|
||||||
|
|
||||||
|
- `bf-public` ist **healthy mit 12 Connectoren** (ap1/ap2/ap3, je ein `cloudflared 2026.6.1`,
|
||||||
|
Container `running`, `start-on-boot=yes`). Tunnel-ID `48d3b0b6-7233-4781-a76a-1de4a5e957ce`.
|
||||||
|
- Ingress steht auf `https://10.40.20.40:443` mit `originServerName` je Regel (Blocker A behoben).
|
||||||
|
- Firewall-Freigabe `vlan99 → vlan20:443` auf `crs326-core` liegt und ist verifiziert (Blocker B).
|
||||||
|
- Caddy antwortet **mit korrektem SNI 200, ohne SNI 000** — gemessen, das ist gesundes Verhalten.
|
||||||
|
- Rollback-Material: `docs/cf-backup/` — Ingress-Configs, CNAMEs **inklusive Record-IDs**, und für
|
||||||
|
drei Tunnel zusätzlich der ältere Stand als `*-vor-hostname-schnitt.json`.
|
||||||
|
|
||||||
|
## 🔴 ZUERST ENTSCHEIDEN, BEVOR EIN NAME GEFLIPPT WIRD
|
||||||
|
|
||||||
|
**Compliance steht auf 128/135.** Sechs `extra`-Drifts, zwei je AP: `ip-address`
|
||||||
|
(`172.17.0.1/24 interface=veth-cf`) und `ip-firewall-nat` (`masquerade`, Kommentar
|
||||||
|
`TP31: cloudflared egress`). Sie sind **inhaltlich richtig** — ohne sie läuft kein Container —, aber
|
||||||
|
sie stehen nicht im Soll, weil Runbook §3.4 Direkt-Writes vorschreibt für Features, die unter
|
||||||
|
Golden-Config-Kontrolle stehen.
|
||||||
|
|
||||||
|
Der saubere Weg ist, sie ins **Soll** zu heben (AP-Template bzw. ConfigContext `ap-common`).
|
||||||
|
🔑 **Bewusst KEINE Transaktion:** `gc-transaktion.py` ist für **befristete Versuche** da (so wie
|
||||||
|
`TP25-fund-e-ft-over-ds`). Das `veth`/`masquerade` soll **dauerhaft** gelten — Dauerzustand gehört
|
||||||
|
ins Template, nicht in eine Transaktion mit Ablaufdatum.
|
||||||
|
🔴 **Kein Re-Baseline** (macht die Drift-Erkennung wertlos), **kein Rückbau** (killt die Tunnel).
|
||||||
|
⚠️ Dabei mitentscheiden, ob `/interface veth` überhaupt erfasst sein soll — dafür existiert heute
|
||||||
|
keine ComplianceRule, deshalb erzeugt `veth` auch keine Drift.
|
||||||
|
|
||||||
|
Mit ungeklärter Drift in den Produktivpfad zu gehen ist die eine Sache, die diese Session
|
||||||
|
**nicht** tun sollte. Christoph entscheidet, nicht die Session.
|
||||||
|
|
||||||
|
🔴 **Die siebte Drift ist KEINE Drift im Sinne eines Versehens:** `crs326-core` / `wifi-security`
|
||||||
|
(`ft-over-ds=no`) ist **TP25 Fund E, Hebel 1** — am 2026-08-31 um 08:09 bewusst gesetzt und als
|
||||||
|
**offene Transaktion `TP25-fund-e-ft-over-ds`** geführt (Scope `wifi-security`, Ablauf 2026-09-03).
|
||||||
|
Sie gehört einer anderen, laufenden Session; **grün machen würde deren Versuch unsichtbar machen.**
|
||||||
|
⚠️ **Merkregel:** vor jeder Drift-Bewertung `python3 nautobot/gc-transaktion.py status` lesen.
|
||||||
|
Drift aus einem laufenden Versuch sieht identisch aus wie ein Versehen — genau dafür gibt es das
|
||||||
|
Werkzeug. (Diese Zeile behauptete zuerst, die Drift käme vom 7.24.1-Upgrade. Falsch: der Core wurde
|
||||||
|
erst um 14:36 gehoben, sechseinhalb Stunden nach dem Hebel.)*
|
||||||
|
|
||||||
|
## REIHENFOLGE
|
||||||
|
|
||||||
|
**Erst `test-tp163` (§5.1), dann `n8n` → `gitea` → `hass` → `vw`** — steigende Kritikalität, `vw`
|
||||||
|
zuletzt. Nach **jedem** Namen die Abnahme aus §5.3, erst dann der nächste.
|
||||||
|
|
||||||
|
## WICHTIGSTE FALLEN
|
||||||
|
|
||||||
|
- 🔴 **`healthy` beweist nichts über das Origin.** Der Tunnelstatus sagt nur, dass der Connector mit
|
||||||
|
Cloudflare redet. Die Wildcard-Access-App `*.emsenhuber.net` liefert für **jeden** Namen ein
|
||||||
|
sauberes 302 — ein toter Dienst sieht so monatelang gesund aus (der `grafana`-Fall). Der einzige
|
||||||
|
Streckenbeweis ist §5.1 gegen einen Testnamen.
|
||||||
|
- 🔴 **WARP muss AUS sein.** Mit aktivem WARP läuft der Test über den Mesh und beweist den
|
||||||
|
öffentlichen Weg nicht — dieselbe Fehldiagnose-Klasse wie der ICMP-Fall 2026-08-13. Testen von
|
||||||
|
außerhalb des LAN, über Mobilfunk.
|
||||||
|
- 🔴 **`vw` = Bitwarden läuft auf Access-Bypass.** **Vor** dem Flip prüfen, dass die Bypass-Policy
|
||||||
|
für `vw.emsenhuber.net` steht — sonst sperrt der Flip alle Bitwarden-Clients aus.
|
||||||
|
- 🔴 **`gitea`: Access gilt nur der Web-UI**, Git-/API-Pfade laufen daran vorbei. Push von außen
|
||||||
|
**vor** und **nach** dem Flip testen, nicht nur die UI.
|
||||||
|
- 🔴 **`hass`: die Companion-App kann kein Access-Login.** Nach dem Flip die App auf dem Handy über
|
||||||
|
Mobilfunk prüfen, nicht nur den Browser.
|
||||||
|
- ⚠️ **`ERR_TOO_MANY_REDIRECTS` nach einem Flip = Blocker A ist zurück** (Ingress auf `:80` statt
|
||||||
|
`:443`, oder `originServerName` fehlt). Sofort zurückflippen, nicht debuggen während der Name tot ist.
|
||||||
|
- ⚠️ **`~/.secrets/credentials.env` lässt sich in zsh nicht sourcen** (rohe WireGuard-Zeilen ab
|
||||||
|
~Zeile 69) — Variablen bleiben still leer, das sieht aus wie ein ungültiger Token. **Immer `bash -c`.**
|
||||||
|
- 🔴 **Maskierung: Muster auf den Schlüsselnamen ankern, nie auf ein Trennzeichen.**
|
||||||
|
`grep -n "TOKEN" datei | sed 's/:.*TOKEN/…/'` maskiert **nicht** — `grep -n` stellt `<zeile>:`
|
||||||
|
voran, und das matcht das Muster. So sind in der Vorsession zwei Nautobot-Tokens ins Transkript
|
||||||
|
geraten. Sicher ist nur `sed -E 's/(TOKEN|PASSWORD|SECRET|KEY|PASS)=.*/\1=<redacted>/'`.
|
||||||
|
|
||||||
|
## GOVERNANCE
|
||||||
|
|
||||||
|
- Config-Änderungen an MikroTik-Geräten **ausschließlich** über Nautobot Golden Config
|
||||||
|
(`nautobot/gc-manual-plan.py`), nicht per Direkt-Write. Nautobot: **`http://10.40.20.30:8080`**
|
||||||
|
(Port 8080, **nicht** 80 — der Runbook-Stand nannte lange nur den Host), Token in
|
||||||
|
`docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored, nur im Haupt-Checkout).
|
||||||
|
- Cloudflare-Änderungen sind **outward-facing**: DNS-Flip und Ingress-Edits vor der Ausführung mit
|
||||||
|
Christoph bestätigen, einer nach dem anderen.
|
||||||
|
- **MCP-Lifecycle aktiv melden**, falls sich daran etwas ändert.
|
||||||
|
|
||||||
|
## OFFENE PUNKTE, DIE NICHT IN DIESE SESSION GEHÖREN
|
||||||
|
|
||||||
|
1. **Rotation zweier Nautobot-Tokens** (`NAUTOBOT_SUPERUSER_API_TOKEN`, `NAUTOBOT_GITEA_TOKEN`) —
|
||||||
|
in der Vorsession ins Transkript geraten, als verbrannt zu behandeln. Gehört zu **TP30**.
|
||||||
|
2. **`crs326-core` / `wifi-security` / `ft-over-ds`** — offene Transaktion `TP25-fund-e-ft-over-ds`
|
||||||
|
einer laufenden TP25-Session. **Nicht anfassen, nicht grün machen**, Ablauf 2026-09-03.
|
||||||
|
3. **`mcp/mikromcp/config/routers.yaml`** meldet `rosVersion: "7.24"` für alle fünf, live ist
|
||||||
|
`7.24.1` — kosmetisch, gehört auf `main`.
|
||||||
|
4. **§6 Abbau** (sechs Container, drei Alttunnel) — frühestens 7 Tage nach dem Flip.
|
||||||
|
🚫 `terpene-cloudflared` (LXC 400) und `fm.emsenhuber.net` bleiben, sie stehen im `docker ps`
|
||||||
|
direkt neben den zu löschenden. **Namen lesen, nicht Zeilen zählen.**
|
||||||
|
|
||||||
|
## ABNAHME DIESER SESSION
|
||||||
|
|
||||||
|
- [ ] Entscheidung zu den sechs Drifts getroffen und umgesetzt (Compliance erklärt oder bereinigt)
|
||||||
|
- [ ] `test-tp163` von außerhalb des LAN ohne WARP: Access-Login, **keine** Redirect-Schleife
|
||||||
|
- [ ] Testname **und** Testregel danach wieder entfernt
|
||||||
|
- [ ] Alle vier Namen geflippt, je einzeln mit 15 min Beobachtung, kein 530/1033
|
||||||
|
- [ ] `gitea`-Push von außen funktioniert nach dem Flip
|
||||||
|
- [ ] `hass`-Companion-App über Mobilfunk funktioniert nach dem Flip
|
||||||
|
- [ ] `vw` Bitwarden-Clients funktionieren nach dem Flip
|
||||||
|
- [ ] Caddy-Log zeigt die Requests (`ssh root@10.40.20.100 'pct exec 130 -- journalctl -u caddy -n 20'`)
|
||||||
|
|
||||||
|
## ARBEITSWEISE
|
||||||
|
|
||||||
|
Bestandsaufnahme vor Plan. Eine große Fragenrunde statt vieler kleiner. Bei Abweichungen zwischen
|
||||||
|
Doku und Live-Stand: **Drift melden, nicht stillschweigend korrigieren.** Vor jedem Flip den
|
||||||
|
Rollback-Weg (§7, Record-IDs in `docs/cf-backup/cnames-tunnel-*.json`) parat haben.
|
||||||
@@ -5,6 +5,22 @@
|
|||||||
# Mode-Button-Druck an jedem AP, danach Reboot. Dieser Runbook-Teil muss
|
# Mode-Button-Druck an jedem AP, danach Reboot. Dieser Runbook-Teil muss
|
||||||
# vor Ort ausgeführt werden.
|
# vor Ort ausgeführt werden.
|
||||||
|
|
||||||
|
> 🔴 **ABGELÖST 2026-08-30 — NICHT MEHR AUSFÜHREN.**
|
||||||
|
> Nachfolger: [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md).
|
||||||
|
> Dieses Dokument bleibt als Historie stehen. Drei Gründe, warum es nicht mehr trägt:
|
||||||
|
> 1. **Es kennt `ap3` nicht** (cAP ax, `10.40.99.13`, seit 2026-08-30 in Betrieb) — und ap3 läuft
|
||||||
|
> **ROS 7.24.1** gegen 7.24 auf ap1/ap2, braucht also ein **anderes `container.npk`**.
|
||||||
|
> 2. **§2b unterstellt einen Registry-Pull.** Auf ap1/ap2 ist `fetch: no` (`device-mode=home`) —
|
||||||
|
> das Image muss als `.tar` hoch und per `/container add file=` importiert werden.
|
||||||
|
> 3. 🔴 **§0 behauptet, „Regel 27 `MGMT→INFRA TCP` deckt `vlan99→vlan20:80` bereits ab ✅".**
|
||||||
|
> **Das ist falsch.** Die `forward`-Kette des Core wurde am 2026-08-30 vollständig gelesen:
|
||||||
|
> Regel 27 ist `IOT->INFRA http-rueckweg` (`in-interface=vlan30`), und die einzige MGMT→INFRA-Regel
|
||||||
|
> (Nr. 26) ist ein `src-port=8728`-Rückweg der API. **Eine Freigabe für den Connector existiert
|
||||||
|
> nicht** — ohne sie landet er in `TP5-WOULD-DROP` + Drop.
|
||||||
|
>
|
||||||
|
> Ebenfalls im Nachfolger, hier noch nicht enthalten: der **Redirect-Loop** über `:80`
|
||||||
|
> (Caddy antwortet dort mit 308) und der Abbau der sechs Container.
|
||||||
|
|
||||||
## 0. IST-STAND (live gelesen 2026-08-27)
|
## 0. IST-STAND (live gelesen 2026-08-27)
|
||||||
- Tunnel **bf-public** `48d3b0b6` ✅: Ingress mit 4 öffentlichen Hostnames → `10.40.20.40:80`
|
- Tunnel **bf-public** `48d3b0b6` ✅: Ingress mit 4 öffentlichen Hostnames → `10.40.20.40:80`
|
||||||
- Token: `~/.secrets/credentials.env` → `BF_PUBLIC_TUNNEL_TOKEN`
|
- Token: `~/.secrets/credentials.env` → `BF_PUBLIC_TUNNEL_TOKEN`
|
||||||
|
|||||||
59
tools/build-cfd-image.py
Executable file
59
tools/build-cfd-image.py
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Baut ein minimales docker-v1-Image-Archiv (nur cloudflared + CA-Bundle),
|
||||||
|
wie RouterOS /container es einliest. Kein Docker noetig, damit das Format
|
||||||
|
nicht vom containerd-Store der Build-Maschine abhaengt."""
|
||||||
|
import hashlib, io, json, os, sys, tarfile, time
|
||||||
|
|
||||||
|
binp, cap, out = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
|
MTIME = 0 # feste mtime -> reproduzierbares Archiv
|
||||||
|
|
||||||
|
def add(tf, name, data, mode, typ=tarfile.REGTYPE):
|
||||||
|
ti = tarfile.TarInfo(name)
|
||||||
|
ti.mode, ti.mtime, ti.uid, ti.gid, ti.type = mode, MTIME, 0, 0, typ
|
||||||
|
ti.size = len(data) if typ == tarfile.REGTYPE else 0
|
||||||
|
tf.addfile(ti, io.BytesIO(data) if typ == tarfile.REGTYPE else None)
|
||||||
|
|
||||||
|
# --- Layer bauen ---
|
||||||
|
layer = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=layer, mode="w", format=tarfile.GNU_FORMAT) as tf:
|
||||||
|
for d in ("usr/", "usr/local/", "usr/local/bin/", "etc/", "etc/ssl/",
|
||||||
|
"etc/ssl/certs/", "tmp/"):
|
||||||
|
add(tf, d, b"", 0o755 if d != "tmp/" else 0o1777, tarfile.DIRTYPE)
|
||||||
|
add(tf, "usr/local/bin/cloudflared", open(binp, "rb").read(), 0o755)
|
||||||
|
add(tf, "etc/ssl/certs/ca-certificates.crt", open(cap, "rb").read(), 0o644)
|
||||||
|
layer_bytes = layer.getvalue()
|
||||||
|
diff_id = "sha256:" + hashlib.sha256(layer_bytes).hexdigest()
|
||||||
|
|
||||||
|
# --- Image-Config ---
|
||||||
|
cfg = {
|
||||||
|
"architecture": "arm64",
|
||||||
|
"os": "linux",
|
||||||
|
"config": {
|
||||||
|
"Env": ["PATH=/usr/local/bin:/usr/bin:/bin",
|
||||||
|
"SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"],
|
||||||
|
"Entrypoint": ["/usr/local/bin/cloudflared"],
|
||||||
|
"Cmd": ["version"],
|
||||||
|
"WorkingDir": "/",
|
||||||
|
},
|
||||||
|
"rootfs": {"type": "layers", "diff_ids": [diff_id]},
|
||||||
|
"history": [{"created": "1970-01-01T00:00:00Z",
|
||||||
|
"created_by": "bf-infra TP16.3: cloudflared + CA, FROM scratch"}],
|
||||||
|
}
|
||||||
|
cfg_raw = json.dumps(cfg, sort_keys=True, separators=(",", ":")).encode()
|
||||||
|
cfg_name = hashlib.sha256(cfg_raw).hexdigest() + ".json"
|
||||||
|
|
||||||
|
manifest = json.dumps([{"Config": cfg_name,
|
||||||
|
"RepoTags": ["cfd-min:2026.6.1"],
|
||||||
|
"Layers": ["layer/layer.tar"]}]).encode()
|
||||||
|
|
||||||
|
with tarfile.open(out, "w", format=tarfile.GNU_FORMAT) as tf:
|
||||||
|
add(tf, cfg_name, cfg_raw, 0o644)
|
||||||
|
add(tf, "layer/", b"", 0o755, tarfile.DIRTYPE)
|
||||||
|
add(tf, "layer/VERSION", b"1.0", 0o644)
|
||||||
|
add(tf, "layer/json", json.dumps({"id": "layer"}).encode(), 0o644)
|
||||||
|
add(tf, "layer/layer.tar", layer_bytes, 0o644)
|
||||||
|
add(tf, "manifest.json", manifest, 0o644)
|
||||||
|
|
||||||
|
print(f"Layer roh : {len(layer_bytes)} B ({len(layer_bytes)/1048576:.1f} MiB)")
|
||||||
|
print(f"Archiv : {os.path.getsize(out)} B ({os.path.getsize(out)/1048576:.1f} MiB)")
|
||||||
|
print(f"diff_id : {diff_id}")
|
||||||
69
tools/oci2docker.py
Executable file
69
tools/oci2docker.py
Executable file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OCI-Archiv (docker save ab Docker 25 mit containerd-Store) -> klassisches
|
||||||
|
docker-archive v1, wie RouterOS /container es erwartet."""
|
||||||
|
import gzip, hashlib, json, os, shutil, sys, tarfile, tempfile
|
||||||
|
|
||||||
|
src, dst = sys.argv[1], sys.argv[2]
|
||||||
|
work = tempfile.mkdtemp(prefix="oci2docker.")
|
||||||
|
ext = os.path.join(work, "in")
|
||||||
|
out = os.path.join(work, "out")
|
||||||
|
os.makedirs(ext); os.makedirs(out)
|
||||||
|
|
||||||
|
with tarfile.open(src) as t:
|
||||||
|
t.extractall(ext)
|
||||||
|
|
||||||
|
man = json.load(open(os.path.join(ext, "manifest.json")))
|
||||||
|
if len(man) != 1:
|
||||||
|
sys.exit(f"erwarte genau 1 Image im Archiv, gefunden: {len(man)}")
|
||||||
|
entry = man[0]
|
||||||
|
|
||||||
|
# --- Config ---
|
||||||
|
cfg_src = os.path.join(ext, entry["Config"])
|
||||||
|
cfg_raw = open(cfg_src, "rb").read()
|
||||||
|
cfg_sha = hashlib.sha256(cfg_raw).hexdigest()
|
||||||
|
cfg_name = f"{cfg_sha}.json"
|
||||||
|
open(os.path.join(out, cfg_name), "wb").write(cfg_raw)
|
||||||
|
cfg = json.loads(cfg_raw)
|
||||||
|
|
||||||
|
# --- Layer: entpacken, falls gzip ---
|
||||||
|
layers_out = []
|
||||||
|
for i, lp in enumerate(entry["Layers"]):
|
||||||
|
blob = os.path.join(ext, lp)
|
||||||
|
with open(blob, "rb") as f:
|
||||||
|
magic = f.read(2)
|
||||||
|
d = os.path.join(out, f"layer{i:02d}")
|
||||||
|
os.makedirs(d)
|
||||||
|
target = os.path.join(d, "layer.tar")
|
||||||
|
if magic == b"\x1f\x8b":
|
||||||
|
with gzip.open(blob, "rb") as fi, open(target, "wb") as fo:
|
||||||
|
shutil.copyfileobj(fi, fo, 1024 * 1024)
|
||||||
|
else:
|
||||||
|
shutil.copyfile(blob, target)
|
||||||
|
open(os.path.join(d, "VERSION"), "w").write("1.0")
|
||||||
|
open(os.path.join(d, "json"), "w").write(json.dumps({"id": f"layer{i:02d}"}))
|
||||||
|
layers_out.append(f"layer{i:02d}/layer.tar")
|
||||||
|
|
||||||
|
# --- diff_ids gegenpruefen: sie muessen die UNKOMPRIMIERTEN Digests sein ---
|
||||||
|
diff_ids = cfg.get("rootfs", {}).get("diff_ids", [])
|
||||||
|
if len(diff_ids) != len(layers_out):
|
||||||
|
sys.exit(f"diff_ids ({len(diff_ids)}) != Layer ({len(layers_out)})")
|
||||||
|
for want, rel in zip(diff_ids, layers_out):
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with open(os.path.join(out, rel), "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||||
|
h.update(chunk)
|
||||||
|
got = "sha256:" + h.hexdigest()
|
||||||
|
if got != want:
|
||||||
|
sys.exit(f"Layer-Digest weicht ab:\n erwartet {want}\n erhalten {got}")
|
||||||
|
|
||||||
|
json.dump([{"Config": cfg_name,
|
||||||
|
"RepoTags": entry.get("RepoTags") or [],
|
||||||
|
"Layers": layers_out}],
|
||||||
|
open(os.path.join(out, "manifest.json"), "w"))
|
||||||
|
|
||||||
|
with tarfile.open(dst, "w") as t:
|
||||||
|
for n in sorted(os.listdir(out)):
|
||||||
|
t.add(os.path.join(out, n), arcname=n)
|
||||||
|
|
||||||
|
shutil.rmtree(work)
|
||||||
|
print(f"ok: {dst} ({os.path.getsize(dst)} Bytes, {len(layers_out)} Layer, diff_ids gepruegt)")
|
||||||
Reference in New Issue
Block a user