From af6abf54ae62292fdc25d3e690d3ef91dc40b9c0 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:08:58 +0200 Subject: [PATCH] Add SSH-only emergency access fallback --- README.md | 1 + docs/EMERGENCY_UNI_ACCESS.md | 83 ++++++++++++++++++++++++++ install.sh | 2 + platform/host/mike-ai-emergency-access | 80 +++++++++++++++++++++++++ 4 files changed, 166 insertions(+) create mode 100644 docs/EMERGENCY_UNI_ACCESS.md create mode 100644 platform/host/mike-ai-emergency-access diff --git a/README.md b/README.md index 580a221..5c8efc5 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ Option „Bewegung reduzieren“ erhalten automatisch eine unbewegte Variante. - [Installation und Abnahme](docs/INSTALLATION.md) - [WireGuard-Heimseite](docs/WIREGUARD_HOME_PEER.md) - [Checkliste für den unbeaufsichtigten Standort](docs/REMOTE_SITE_CHECKLIST.md) +- [Temporärer Notfallzugriff über SSH](docs/EMERGENCY_UNI_ACCESS.md) - [Betrieb und Profilwechsel](docs/OPERATIONS.md) - [Sicherheitsmodell](docs/SECURITY.md) - [Disaster Recovery](docs/DISASTER_RECOVERY.md) diff --git a/docs/EMERGENCY_UNI_ACCESS.md b/docs/EMERGENCY_UNI_ACCESS.md new file mode 100644 index 0000000..fad50b1 --- /dev/null +++ b/docs/EMERGENCY_UNI_ACCESS.md @@ -0,0 +1,83 @@ +# Notfallzugriff aus dem Universitätsnetz + +## Zweck + +Der normale Zugang zu Open WebUI und den MCP-Endpunkten erfolgt ausschließlich +über WireGuard. Falls vorübergehend ein administrativer Zugriff über die +Standortverbindung erforderlich ist, werden die KI-Ports **nicht** im +Universitätsnetz veröffentlicht. Stattdessen stellt ein kurzlebiger Proxy sie +nur auf Athenas Loopback-Adresse bereit; ein authentifizierter SSH-Tunnel bringt +sie verschlüsselt zum eigenen Rechner. + +Dieser Weg eignet sich beispielsweise zur Diagnose eines gestörten VPN-Tunnels. +Er verändert das Fail-Closed-Egress nicht: Ist die Fritzbox nicht erreichbar, +funktionieren lokaler Chat und Administration, aber MCP-Zugriffe auf Heimnetz +und Internet bleiben absichtlich blockiert. + +## 1. Auf Athena aktivieren + +Per SSH auf Athena anmelden und als root ausführen: + +```bash +mike-ai-emergency-access start +mike-ai-emergency-access status +``` + +Die Ausgabe darf ausschließlich Bindings mit `127.0.0.1` zeigen. Verwendet +werden: + +| Lokaler Port auf Athena | Ziel | +|---:|---| +| 18080 | Open WebUI | +| 18090 | Web-MCP | +| 18091 | Home-Assistant-MCP | +| 18092 | ARR-MCP | +| 18093 | Unraid-MCP (read-only) | + +Fehlende optionale MCP-Container werden übersprungen. Die Proxy-Container +verwenden kein neues Image, keine Secrets und keine zusätzlichen Rechte. Sie +besitzen keine Restart-Policy und verschwinden spätestens beim Hostneustart. + +## 2. SSH-Tunnel auf dem eigenen Rechner öffnen + +`` durch die aktuelle Standortadresse von Athena ersetzen: + +```bash +ssh -N \ + -L 18080:127.0.0.1:18080 \ + -L 18090:127.0.0.1:18090 \ + -L 18091:127.0.0.1:18091 \ + -L 18092:127.0.0.1:18092 \ + -L 18093:127.0.0.1:18093 \ + -i ~/.ssh/athena_key root@ +``` + +Das Terminal bleibt während der Nutzung geöffnet. Danach ist Open WebUI unter +`http://127.0.0.1:18080` erreichbar. Die MCP-URLs lauten entsprechend +`http://127.0.0.1:18090/mcp` bis `http://127.0.0.1:18093/mcp`. + +## 3. Sofort wieder schließen + +Den SSH-Tunnel mit `Ctrl+C` beenden und auf Athena ausführen: + +```bash +mike-ai-emergency-access stop +mike-ai-emergency-access status +``` + +Zusätzlich prüfen: + +```bash +ss -lnt | grep -E ':(18080|18090|18091|18092|18093) ' +``` + +Nach `stop` darf dieser Befehl nichts mehr ausgeben. Port 8080 und 8081 bleiben +während des gesamten Vorgangs an der physischen Standortadresse geschlossen. + +## Was ausdrücklich nicht gemacht wird + +- kein Binding auf `0.0.0.0` +- keine direkte Freigabe von 8080/8081 im Universitätsnetz +- keine Änderung der Fail-Closed-Routingregeln +- kein Fallback der MCPs auf das Universitäts-Internet +- keine dauerhafte Notfallfreigabe und kein automatischer Neustart der Proxys diff --git a/install.sh b/install.sh index ea3c02e..19109a9 100755 --- a/install.sh +++ b/install.sh @@ -342,6 +342,8 @@ install_routing_guard() { /usr/local/sbin/mike-ai-container-vpn-guard install -m 0644 "$ROOT_DIR/platform/host/mike-ai-container-vpn-guard.service" \ /etc/systemd/system/mike-ai-container-vpn-guard.service + install -m 0755 "$ROOT_DIR/platform/host/mike-ai-emergency-access" \ + /usr/local/sbin/mike-ai-emergency-access systemctl daemon-reload systemctl enable mike-ai-container-vpn-guard.service return 0 diff --git a/platform/host/mike-ai-emergency-access b/platform/host/mike-ai-emergency-access new file mode 100644 index 0000000..01dc57e --- /dev/null +++ b/platform/host/mike-ai-emergency-access @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +IMAGE=mike-ai/wireguard-gateway:local + +proxy_names=( + mike-ai-emergency-openwebui + mike-ai-emergency-mcp-web + mike-ai-emergency-mcp-homeassistant + mike-ai-emergency-mcp-arr + mike-ai-emergency-mcp-unraid +) + +remove_proxies() { + docker rm -f "${proxy_names[@]}" >/dev/null 2>&1 || true +} + +start_proxy() { + local name=$1 network=$2 host_port=$3 target=$4 + docker container inspect "$target" >/dev/null 2>&1 || { + printf 'Übersprungen (Ziel fehlt): %s\n' "$target" >&2 + return 0 + } + docker start "$target" >/dev/null 2>&1 || true + [ "$(docker inspect -f '{{.State.Running}}' "$target" 2>/dev/null || true)" = true ] || { + printf 'Übersprungen (Ziel startet nicht): %s\n' "$target" >&2 + return 0 + } + docker run --detach --rm --pull never \ + --name "$name" \ + --network mike-ai_frontend \ + --publish "127.0.0.1:${host_port}:${host_port}" \ + --read-only --cap-drop ALL --pids-limit 64 --memory 64m \ + --security-opt no-new-privileges:true \ + --entrypoint socat "$IMAGE" \ + "TCP-LISTEN:${host_port},bind=0.0.0.0,reuseaddr,fork" \ + "TCP:${target}:8000" >/dev/null + # Docker deliberately discards published ports on an internal-only network. + # The proxy therefore publishes through frontend and receives the tools + # network as a second, non-egress attachment for target discovery. + docker network connect "$network" "$name" +} + +case "${1:-}" in + start) + remove_proxies + docker image inspect "$IMAGE" >/dev/null + + docker start mike-ai-router mike-ai-open-webui >/dev/null 2>&1 || true + docker run --detach --rm --pull never \ + --name mike-ai-emergency-openwebui \ + --network mike-ai_frontend \ + --publish 127.0.0.1:18080:18080 \ + --read-only --cap-drop ALL --pids-limit 64 --memory 64m \ + --security-opt no-new-privileges:true \ + --entrypoint socat "$IMAGE" \ + TCP-LISTEN:18080,bind=0.0.0.0,reuseaddr,fork \ + TCP:mike-ai-open-webui:8080 >/dev/null + + start_proxy mike-ai-emergency-mcp-web mike-ai-tools 18090 mike-ai-mcp-web + start_proxy mike-ai-emergency-mcp-homeassistant mike-ai-tools 18091 mike-ai-mcp-homeassistant + start_proxy mike-ai-emergency-mcp-arr mike-ai-tools 18092 mike-ai-mcp-arr + start_proxy mike-ai-emergency-mcp-unraid mike-ai-tools 18093 mike-ai-mcp-unraid-official + + echo 'Notfall-Proxys laufen ausschließlich auf 127.0.0.1.' + echo 'Jetzt vom Client einen SSH-Tunnel auf die Ports 18080 und 18090-18093 öffnen.' + ;; + stop) + remove_proxies + echo 'Notfall-Proxys entfernt.' + ;; + status) + docker ps --filter 'name=mike-ai-emergency-' \ + --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}' + ;; + *) + echo "Aufruf: $0 start|stop|status" >&2 + exit 2 + ;; +esac