Compare commits
3 Commits
759491b512
...
433e600f83
| Author | SHA1 | Date | |
|---|---|---|---|
| 433e600f83 | |||
| 840d57bca3 | |||
| 27863c1f2e |
176
docs/tp31-schritt5-runbook.md
Normal file
176
docs/tp31-schritt5-runbook.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# TP31 Schritt 5 — Runbook: cloudflared auf ap1/ap2 + CNAME-Flip
|
||||||
|
# Stand 2026-08-27 — vorbereitet, nicht ausgeführt.
|
||||||
|
#
|
||||||
|
# ⚠️ device-mode-Wechsel ist NICHT remote möglich — erfordert physischen
|
||||||
|
# Mode-Button-Druck an jedem AP, danach Reboot. Dieser Runbook-Teil muss
|
||||||
|
# vor Ort ausgeführt werden.
|
||||||
|
|
||||||
|
## 0. IST-STAND (live gelesen 2026-08-27)
|
||||||
|
- Tunnel **bf-public** `48d3b0b6` ✅: Ingress mit 4 öffentlichen Hostnames → `10.40.20.40:80`
|
||||||
|
- Token: `~/.secrets/credentials.env` → `BF_PUBLIC_TUNNEL_TOKEN`
|
||||||
|
- Config-Dateien: `routeros/cloudflared/credentials.json`, `cloudflared-bf-public-config.yml`, `ap-bootstrap.rsc`
|
||||||
|
- Firewall: Regel 27 `MGMT→INFRA TCP` deckt `vlan99→vlan20:80` bereits ab ✅
|
||||||
|
- Alte Tunnel: bf-mm (n8n, n8n-mcp, gitea-mcp, pbs), bf-nas (gitea, grafana, pve-mcp, vw, ds-bf), bf-pve (pve, ccu, iobroker, z2m25, portainer, hass, ha-mcp)
|
||||||
|
|
||||||
|
## 1. DEVICE-MODE-WECHSEL (physisch, vor Ort)
|
||||||
|
|
||||||
|
### 1a. Container-Paket checken
|
||||||
|
```
|
||||||
|
/system package print where name="container"
|
||||||
|
```
|
||||||
|
Falls nicht installiert: Winbox → System → Packages → Check For Updates → container aktivieren.
|
||||||
|
|
||||||
|
### 1b. device-mode ändern
|
||||||
|
```
|
||||||
|
/system device-mode update container=yes
|
||||||
|
```
|
||||||
|
Das Gerät fordert auf, den Mode-Button zu drücken. Am hAP ax² ist das der
|
||||||
|
Knopf an der Front. Nach Drücken: „configuration changed, reboot required".
|
||||||
|
|
||||||
|
### 1c. Reboot
|
||||||
|
```
|
||||||
|
/system reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1d. Prüfen
|
||||||
|
```
|
||||||
|
/system device-mode print
|
||||||
|
# output muss zeigen: container: yes
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. CONTAINER BOOTSTRAP (auf JEDEM AP einzeln)
|
||||||
|
|
||||||
|
### 2a. Backup
|
||||||
|
```
|
||||||
|
/system backup save name=vor-tp31-apX
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2b. Dateien hochladen (via SFTP, Port 22)
|
||||||
|
```bash
|
||||||
|
# ap1 (10.40.99.11)
|
||||||
|
sftp admin@10.40.99.11 <<'EOF'
|
||||||
|
put routeros/cloudflared/credentials.json flash/
|
||||||
|
put routeros/cloudflared-bf-public-config.yml flash/
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ap2 (10.40.99.12)
|
||||||
|
sftp admin@10.40.99.12 <<'EOF'
|
||||||
|
put routeros/cloudflared/credentials.json flash/
|
||||||
|
put routeros/cloudflared-bf-public-config.yml flash/
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2c. Bootstrap-Script importieren + ausführen
|
||||||
|
```
|
||||||
|
/import routeros/cloudflared/ap-bootstrap.rsc
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls `/import` Probleme macht: Zeilen einzeln per SSH absetzen.
|
||||||
|
|
||||||
|
### 2d. Abnahme
|
||||||
|
```
|
||||||
|
/container print
|
||||||
|
# Status muss "started" sein
|
||||||
|
/log print where message~"cloudflared"
|
||||||
|
# Muss "Registered tunnel connection" zeigen
|
||||||
|
```
|
||||||
|
|
||||||
|
CF-Dashboard: Tunnel bf-public → Connections muss > 0 zeigen (1 Connector pro AP).
|
||||||
|
|
||||||
|
## 3. CNAME-FLIP (nach erfolgreichem Bootstrap, sequentiell)
|
||||||
|
|
||||||
|
Reihenfolge: **n8n → gitea → hass → vw**. Jeweils mit Abnahme zwischen den Flips.
|
||||||
|
|
||||||
|
### 3a. Vor jedem Flip: Test-CNAME temporär
|
||||||
|
|
||||||
|
Vor dem Umhängen eines Produktiv-Namens testen wir mit einem **Extra-CNAME**
|
||||||
|
`test-tp31.emsenhuber.net → bf-public.cfargotunnel.com`, der durch die
|
||||||
|
Wildcard-Access-App `*.emsenhuber.net` bereits geschützt ist.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# curl im BF-LAN (testet Tunnel→Caddy→Backend)
|
||||||
|
curl -s -o /dev/null -w '%{http_code}' --resolve test-tp31.emsenhuber.net:443:10.40.99.11 http://test-tp31.emsenhuber.net
|
||||||
|
# Erwartet: 302 (Access-Login-Redirect)
|
||||||
|
# Bei 5xx: Container-Log lesen, Caddy erreichbar? /log print auf dem AP
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3b. Flip n8n.emsenhuber.net
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Zone-ID ist 1b5bc02fdf61b6b989afde543f3f4134
|
||||||
|
# Alten CNAME lesen:
|
||||||
|
CNAME_ID=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/1b5bc02fdf61b6b989afde543f3f4134/dns_records?name=n8n.emsenhuber.net" \
|
||||||
|
| jq -r '.result[0].id')
|
||||||
|
|
||||||
|
# Auf bf-public.cfargotunnel.com umhängen:
|
||||||
|
curl -s -X PATCH -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/1b5bc02fdf61b6b989afde543f3f4134/dns_records/$CNAME_ID" \
|
||||||
|
-d '{"content":"48d3b0b6-7233-4781-a76a-1de4a5e957ce.cfargotunnel.com","proxied":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Abnahme:** Browser → n8n.emsenhuber.net → Access-Login → 200 von n8n.
|
||||||
|
|
||||||
|
### 3c. Flip gitea.emsenhuber.net
|
||||||
|
Analog, aber VORHER prüfen ob Git-Push über Token von außen noch geht.
|
||||||
|
|
||||||
|
### 3d. Flip hass.emsenhuber.net
|
||||||
|
**VORSICHT:** HA Companion App kann Access-Login nicht. Vor dem Test:
|
||||||
|
- Browser öffnen, Access-Login durchführen, Session-Cookie setzen
|
||||||
|
- App öffnen und prüfen ob sie die Session übernimmt
|
||||||
|
|
||||||
|
### 3e. Flip vw.emsenhuber.net
|
||||||
|
**VORSICHT:** Bitwarden auf bypass, NICHT hinter Access. Prüfen dass
|
||||||
|
die Access-Policy `vw.emsenhuber.net` weiterhin `bypass` ist.
|
||||||
|
|
||||||
|
## 4. AUFRÄUMEN (nach erfolgreichem Flip ALLER 4 Namen)
|
||||||
|
|
||||||
|
### 4a. Alte CNAMEs löschen
|
||||||
|
Löschen aller CNAMEs, die noch auf bf-mm/bf-nas/bf-pve zeigen.
|
||||||
|
**Ausnahme:** `3k23uow7`, `82unkpb5`, `j44d6tua` (SSH-Tunnel-Namen,
|
||||||
|
keine öffentlichen Hostnames — erst mit Schritt 6 anfassen).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for name in n8n-mcp gitea-mcp grafana pve-mcp ds-bf ccu iobroker z2m25 pve portainer pbs ha-mcp nautobot gateway-dw; do
|
||||||
|
ID=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/1b5bc02fdf61b6b989afde543f3f4134/dns_records?name=${name}.emsenhuber.net" \
|
||||||
|
| jq -r '.result[0].id // empty')
|
||||||
|
if [ -n "$ID" ]; then
|
||||||
|
curl -s -X DELETE -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/1b5bc02fdf61b6b989afde543f3f4134/dns_records/$ID"
|
||||||
|
echo "Deleted ${name}.emsenhuber.net"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4b. Alte Tunnel deaktiviert lassen (nicht löschen!)
|
||||||
|
bf-mm, bf-nas, bf-pve erst löschen nach 1 Woche Betrieb ohne Vorfälle.
|
||||||
|
|
||||||
|
## 5. ABNAHME-KRITERIEN (pro Flip)
|
||||||
|
- [ ] Tunnel bf-public zeigt 2+ Connections (ap1 + ap2)
|
||||||
|
- [ ] Browser: Name → Access-Login → 200/302 vom Caddy
|
||||||
|
- [ ] Caddy-Log zeigt eingehenden Request (`pct exec 130 -- journalctl -u caddy -n 5`)
|
||||||
|
- [ ] Kein 530/1033 über 24h (alte Tunnel noch aktiv → Round-Robin wäre sichtbar)
|
||||||
|
|
||||||
|
## 6. ROLLBACK (je Flip)
|
||||||
|
CNAME zurück auf den alten Tunnel setzen:
|
||||||
|
```bash
|
||||||
|
curl -s -X PATCH -H "Authorization: Bearer $CF_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/1b5bc02fdf61b6b989afde543f3f4134/dns_records/$CNAME_ID" \
|
||||||
|
-d '{"content":"<alter-tunnel-id>.cfargotunnel.com","proxied":true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. FALLEN
|
||||||
|
- **Kein Container ohne device-mode-Wechsel** — `container`-Paket ist mit
|
||||||
|
`device-mode=home` gesperrt. Physischer Zugang zwingend.
|
||||||
|
- **hAP Flash-Constraint:** ~95 MB frei. cloudflared-Image ~30 MB → passt.
|
||||||
|
Aber keine weiteren Container auf demselben AP.
|
||||||
|
- **NAND-Verschleiß:** cloudflared schreibt kaum (Config in Cloudflare, kein
|
||||||
|
Query-Log wie AGH). Trotzdem kein zweiter Container.
|
||||||
|
- **MGMT→INFRA forward nur TCP:** Cloudflared baut QUIC (UDP 7844) zu CF.
|
||||||
|
Das ist Egress (LAN→WAN), nicht INFRA — die Regel 27 greift nicht für
|
||||||
|
Cloudflareds eigenen Tunnel-Traffic. Der geht über die `defconf`-Regel 2
|
||||||
|
(est/rel) oder über `LAN→WAN` (Regel 14). **Kein zusätzlicher Firewall-Change
|
||||||
|
nötig.**
|
||||||
4
mcp/shelly/.gitignore
vendored
Normal file
4
mcp/shelly/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Secrets & runtime — niemals committen
|
||||||
|
.env
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
72
mcp/shelly/BUILD.md
Normal file
72
mcp/shelly/BUILD.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# shelly-mcp — Build-Anleitung (DS923, lokal)
|
||||||
|
|
||||||
|
## Source-Tree (im Repo, gepflegt)
|
||||||
|
- `mcp/shelly/Dockerfile` — Multi-stage-frei, python:3.12-slim, COPY requirements.txt + server.py + discover.py
|
||||||
|
- `mcp/shelly/requirements.txt` — `mcp[cli]>=1.0`, `starlette>=0.36`
|
||||||
|
- `mcp/shelly/server.py` — FastMCP, StreamableHTTP `/mcp` + `/health` + `/`
|
||||||
|
- `mcp/shelly/discover.py` — Read-only-Sweep (CLI-Argument, nicht Env)
|
||||||
|
|
||||||
|
## Secrets
|
||||||
|
- **SSoT:** `~/.secrets/credentials.env` + `~/.secrets/exports.zsh`
|
||||||
|
- **Niemals ins Image brennen.** Dockerfile macht **kein** `COPY .env`.
|
||||||
|
- **Im Stack:** `docs/nas-vlan20/stacks/shelly-mcp.yml` (gitignored) trägt die Werte als `environment:` — folgt der Konvention der anderen DS923-Stacks.
|
||||||
|
|
||||||
|
## Build (auf der DS923, einmalig)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Build-Verzeichnis auf der DS923 anlegen (außerhalb des Compose-Stacks)
|
||||||
|
ssh adm.christoph@10.40.20.10 "sudo -i mkdir -p /data/compose/shelly-mcp-build"
|
||||||
|
|
||||||
|
# 2. Source rsyncen (vom Mac)
|
||||||
|
rsync -avz --delete \
|
||||||
|
mcp/shelly/Dockerfile \
|
||||||
|
mcp/shelly/requirements.txt \
|
||||||
|
mcp/shelly/server.py \
|
||||||
|
mcp/shelly/discover.py \
|
||||||
|
adm.christoph@10.40.20.10:/data/compose/shelly-mcp-build/
|
||||||
|
|
||||||
|
# 3. Image bauen
|
||||||
|
ssh adm.christoph@10.40.20.10 "sudo -i docker build -t bf/shelly-mcp:0.1 /data/compose/shelly-mcp-build"
|
||||||
|
|
||||||
|
# 4. Stack deployen (Portainer → Stacks → Add stack → Repository upload
|
||||||
|
# ODER Portainer-API CreateStack mit Git-Repository-Pfad)
|
||||||
|
ssh adm.christoph@10.40.20.10 "sudo -i cd /data/compose && docker stack deploy -c shelly-mcp.yml shelly-mcp"
|
||||||
|
# (Hinweis: der pve-mcp-Stack wurde via Portainer-UI deployed, nicht docker-stack.
|
||||||
|
# Vorzugsweise Portainer-API CreateStack mit GitRepo-Ref auf docs/nas-vlan20/stacks/shelly-mcp.yml.)
|
||||||
|
|
||||||
|
# 5. IP am DHCP-Server (crs326-core, dhcp-v20) als statische Reservierung eintragen
|
||||||
|
# MAC wird nach dem ersten Boot sichtbar (docker inspect shelly-mcp | jq '.[0].NetworkSettings.Networks.vlan20.MacAddress')
|
||||||
|
# Reservierung in Nautobot/RouterOS: dhcp-server lease add ... server=dhcp-v20
|
||||||
|
```
|
||||||
|
|
||||||
|
## Abnahme
|
||||||
|
```bash
|
||||||
|
# Health-Endpoint
|
||||||
|
curl http://10.40.20.180:3030/health
|
||||||
|
|
||||||
|
# MCP-Discovery (StreamableHTTP erwartet initialize)
|
||||||
|
curl -X POST http://10.40.20.180:3030/mcp \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.0"}}}'
|
||||||
|
# → serverInfo.name = "shelly-mcp"
|
||||||
|
|
||||||
|
# Live-Sweep aus dem Container heraus
|
||||||
|
docker exec shelly-mcp python3 discover.py 10.40.30.180-250
|
||||||
|
# → erwartet 23 Shellys (s. Handoff 2026-08-28)
|
||||||
|
|
||||||
|
# MCP-Client-Konfiguration (in ~/.claude.json + ~/.config/opencode/opencode.jsonc):
|
||||||
|
# {
|
||||||
|
# "shelly-mcp": {
|
||||||
|
# "url": "http://10.40.20.180:3030/mcp",
|
||||||
|
# "enabled": true,
|
||||||
|
# "headers": {} # falls Bearer gewünscht: "Authorization": "Bearer ${SHELLY_MCP_BEARER}"
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
```bash
|
||||||
|
docker rm -f shelly-mcp
|
||||||
|
docker rmi bf/shelly-mcp:0.1
|
||||||
|
```
|
||||||
15
mcp/shelly/Dockerfile
Normal file
15
mcp/shelly/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY server.py discover.py ./
|
||||||
|
|
||||||
|
EXPOSE 3030
|
||||||
|
|
||||||
|
ENV SHELLY_MCP_HOST=0.0.0.0 \
|
||||||
|
SHELLY_MCP_PORT=3030
|
||||||
|
|
||||||
|
CMD ["python3", "server.py"]
|
||||||
2
mcp/shelly/requirements.txt
Normal file
2
mcp/shelly/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mcp[cli]>=1.0
|
||||||
|
starlette>=0.36
|
||||||
14
routeros/cloudflared-bf-public-config.yml
Normal file
14
routeros/cloudflared-bf-public-config.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# cloudflared config for TP31 — Tunnel bf-public on ap1/ap2
|
||||||
|
# Device: hAP ax² (arm64), RouterOS container
|
||||||
|
# Deploy: als /etc/cloudflared/config.yml im Container (mount via RouterOS)
|
||||||
|
# Token liegt in /etc/cloudflared/credentials.json (separater Mount)
|
||||||
|
# Der Ingress wird zentral im CF-Dashboard verwaltet (config_src=cloudflare)
|
||||||
|
|
||||||
|
tunnel: 48d3b0b6-7233-4781-a76a-1de4a5e957ce
|
||||||
|
credentials-file: /etc/cloudflared/credentials.json
|
||||||
|
|
||||||
|
# Kein Ingress hier — wird via Dashboard gemanagt (remote config).
|
||||||
|
# Kein originRequest nötig — der Ingress definiert das pro Hostname.
|
||||||
|
|
||||||
|
metrics: 0.0.0.0:49312
|
||||||
|
loglevel: info
|
||||||
2
routeros/cloudflared/.gitignore
vendored
Normal file
2
routeros/cloudflared/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Tunnel-Secret — niemals committen
|
||||||
|
credentials.json
|
||||||
49
routeros/cloudflared/ap-bootstrap.rsc
Normal file
49
routeros/cloudflared/ap-bootstrap.rsc
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# RouterOS Container-Bootstrap für cloudflared (TP31, Tunnel bf-public)
|
||||||
|
# Auf ap1 UND ap2 identisch ausfuehren — beide sind arm64, gleiche Config.
|
||||||
|
#
|
||||||
|
# Voraussetzung: device-mode-Wechsel + Reboot durchgefuehrt,
|
||||||
|
# container-Paket enabled, veth-Interface existiert.
|
||||||
|
# VOR dem Import: /system backup (jeder AP einzeln)
|
||||||
|
#
|
||||||
|
# Token: BF_PUBLIC_TUNNEL_TOKEN in ~/.secrets/credentials.env
|
||||||
|
# Ingress: Dashboard-managed (config_src=cloudflare) — kein local config.yml noetig.
|
||||||
|
|
||||||
|
# 1. Dateien auf dem Router ablegen (vor dem Import, via SFTP):
|
||||||
|
# routeros/cloudflared/credentials.json → flash/cloudflared-credentials.json
|
||||||
|
# routeros/cloudflared-bf-public-config.yml → flash/cloudflared-config.yml
|
||||||
|
#
|
||||||
|
# 2. Dann importieren:
|
||||||
|
|
||||||
|
# — DNS fuer den Container ——— (benutzt den Core-Resolver)
|
||||||
|
/ip dns set allow-remote-requests=yes servers=10.40.20.36
|
||||||
|
|
||||||
|
# — Veth-Interface (Bru cke zum Container, einmalig) ———
|
||||||
|
/interface veth add name=veth-cloudflared address=172.17.0.2/30 gateway=172.17.0.1 comment="cloudflared-container"
|
||||||
|
# 172.17.0.1 = Host · 172.17.0.2 = Container
|
||||||
|
|
||||||
|
# — Source-NAT fuer den Container-Outbound ——— (Core=10.40.99.1)
|
||||||
|
/ip firewall nat add chain=srcnat src-address=172.17.0.0/30 out-interface=vlan99 action=masquerade comment="TP31: cloudflared-Outbound"
|
||||||
|
|
||||||
|
# — Container-Config ———
|
||||||
|
/container config set registry-url=https://ghcr.io ram-high=0 tmpdir=flash/containers
|
||||||
|
|
||||||
|
# — Mount: credentials ———
|
||||||
|
/container mounts add name=cf-credentials src=flash/cloudflared-credentials.json dst=/etc/cloudflared/credentials.json
|
||||||
|
|
||||||
|
# — Mount: config ———
|
||||||
|
/container mounts add name=cf-config src=flash/cloudflared-config.yml dst=/etc/cloudflared/config.yml
|
||||||
|
|
||||||
|
# — Environment ———
|
||||||
|
/container envs add name=cf-env key=TUNNEL_METRICS value=0.0.0.0:49312
|
||||||
|
/container envs add name=cf-env key=TUNNEL_LOGLEVEL value=info
|
||||||
|
|
||||||
|
# — Container erstellen ———
|
||||||
|
/container add remote-image=cloudflare/cloudflared:latest interface=veth-cloudflared root-dir=flash/containers/cf-data mounts=cf-credentials,cf-config envlist=cf-env logging=yes comment="bf-public tunnel (TP31)"
|
||||||
|
|
||||||
|
# — Start ———
|
||||||
|
/container start 0
|
||||||
|
|
||||||
|
# — Abnahme ———
|
||||||
|
# /container print (status=started)
|
||||||
|
# /log print where message~"cloudflared" (sollte "Registered tunnel connection" zeigen)
|
||||||
|
# CF-Dashboard: bf-public Tunnel → connections > 0
|
||||||
Reference in New Issue
Block a user