Add fail-closed WireGuard container gateway

This commit is contained in:
Mikei386
2026-08-22 20:48:49 +02:00
parent 2f3bdde8b0
commit 0e5876f1a9
17 changed files with 365 additions and 109 deletions
+4 -3
View File
@@ -19,7 +19,7 @@ WireGuard-Isolation.
- SearXNG/Web-MCP ohne externen API-Schlüssel - SearXNG/Web-MCP ohne externen API-Schlüssel
- zentrale MCP-Werkzeugebene: getrennte Container für Web, HA, ARR, Unraid - zentrale MCP-Werkzeugebene: getrennte Container für Web, HA, ARR, Unraid
und Sandbox, gemeinsam nutzbar durch Open WebUI und andere Clients und Sandbox, gemeinsam nutzbar durch Open WebUI und andere Clients
- KI-Dienste ausschließlich über die WireGuard-Adresse erreichbar - KI-Dienste ausschließlich über den containerisierten WireGuard-Gateway erreichbar
- KI-Ausgangsverkehr über das Heimnetz, bei Tunnelausfall fail-closed - KI-Ausgangsverkehr über das Heimnetz, bei Tunnelausfall fail-closed
- keine Secrets, Chats, Logs oder Modelldateien im Repository - keine Secrets, Chats, Logs oder Modelldateien im Repository
@@ -37,7 +37,7 @@ editor config/install.env
sudo ./install.sh --config config/install.env sudo ./install.sh --config config/install.env
``` ```
Installiert werden Docker CE, NVIDIA Container Toolkit, WireGuard, der Installiert werden Docker CE, NVIDIA Container Toolkit, WireGuard-Werkzeuge, der
gepinnt gebaute llama.cpp-Server, die Modelle und der komplette Compose-Stack. gepinnt gebaute llama.cpp-Server, die Modelle und der komplette Compose-Stack.
Bei einer erstmaligen NVIDIA-Treiberinstallation fordert das Skript einen Bei einer erstmaligen NVIDIA-Treiberinstallation fordert das Skript einen
Neustart an; danach wird derselbe Befehl erneut ausgeführt. Neustart an; danach wird derselbe Befehl erneut ausgeführt.
@@ -98,7 +98,8 @@ router/ OpenAI-kompatibler Profile Router
- API-, Controller-, WebUI- und WireGuard-Schlüssel entstehen erst am Host. - API-, Controller-, WebUI- und WireGuard-Schlüssel entstehen erst am Host.
- Nur der kleine Profile Controller sieht den Docker-Socket. - Nur der kleine Profile Controller sieht den Docker-Socket.
- llama.cpp veröffentlicht weder Port noch WebUI. - llama.cpp veröffentlicht weder Port noch WebUI.
- Ein Blackhole-Fallback verhindert Traffic-Leaks bei WireGuard-Ausfall. - Quellrouting ohne alternative Route verhindert Traffic-Leaks bei
WireGuard-Ausfall (fail-closed).
- Das Uni-Netz und das Heimnetz dürfen diesen Host nicht als Transit benutzen. - Das Uni-Netz und das Heimnetz dürfen diesen Host nicht als Transit benutzen.
Die Profilwerte wurden auf RTX 5080 und RTX 3060 vermessen und bilden die Die Profilwerte wurden auf RTX 5080 und RTX 3060 vermessen und bilden die
+38 -4
View File
@@ -27,6 +27,37 @@ x-llama-common: &llama-common
start_period: 30s start_period: 30s
services: services:
wireguard-gateway:
build: ./platform/docker/wireguard-gateway
image: mike-ai/wireguard-gateway:local
container_name: mike-ai-wireguard-gateway
restart: unless-stopped
cap_add: [NET_ADMIN]
devices:
- /dev/net/tun:/dev/net/tun
sysctls:
net.ipv4.ip_forward: "1"
net.ipv4.conf.all.src_valid_mark: "1"
net.ipv6.conf.all.forwarding: "1"
read_only: true
tmpfs:
- /run:size=16m,mode=0755
- /tmp:size=16m,mode=1777
volumes:
- "${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf}:/run/secrets/fritz-athena.conf:ro"
networks:
frontend:
ipv4_address: 172.30.10.254
tools-egress:
ipv4_address: 172.30.50.254
security_opt: ["no-new-privileges:true"]
healthcheck:
test: [CMD, /usr/local/sbin/mike-ai-wireguard-healthcheck]
interval: 10s
timeout: 3s
retries: 12
start_period: 10s
llama-fast: llama-fast:
<<: *llama-common <<: *llama-common
container_name: mike-ai-llama-fast container_name: mike-ai-llama-fast
@@ -416,8 +447,6 @@ services:
TTS_VOICES: alloy TTS_VOICES: alloy
TTS_DEFAULT_VOICE: alloy TTS_DEFAULT_VOICE: alloy
ENABLE_STT: "false" ENABLE_STT: "false"
ports:
- "${AI_BIND_ADDRESS:-127.0.0.1}:8081:8081"
networks: [frontend, control, inference] networks: [frontend, control, inference]
security_opt: ["no-new-privileges:true"] security_opt: ["no-new-privileges:true"]
cap_drop: [ALL] cap_drop: [ALL]
@@ -433,6 +462,8 @@ services:
retries: 24 retries: 24
start_period: 5s start_period: 5s
depends_on: depends_on:
wireguard-gateway:
condition: service_healthy
profile-controller: profile-controller:
condition: service_healthy condition: service_healthy
piper: piper:
@@ -550,11 +581,11 @@ services:
[{"url":"http://mike-ai-mcp-web:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"web-local","name":"Web (öffentlich, read-only)","description":"Für aktuelle öffentliche Internetdaten, Quellenprüfung, GitHub/Hugging Face und Produktsuche. Nicht für Home Assistant, Medienverwaltung oder NAS-Diagnose."}},{"url":"http://mike-ai-mcp-homeassistant:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"homeassistant-local","name":"Home Assistant (lokal)","description":"Nur für Home-Assistant-Entitäten, Zustände, Historie, Automationen, Dashboards und HA-Diagnose. Nicht für Unraid, Sonarr/Radarr oder allgemeine Websuche."}},{"url":"http://mike-ai-mcp-arr:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"arr-local","name":"Sonarr und Radarr (lokal)","description":"Nur für verwaltete Serien/Filme, fehlende Episoden, Queue und Suche über konfigurierte Indexer. Keine allgemeine Websuche; Schreibaktionen benötigen Vorschau und Freigabe."}},{"url":"http://mike-ai-mcp-unraid-official:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"unraid-readonly-local","name":"Unraid (Systemdiagnose)","description":"Nur für Unraid-Host, Array, Datenträger, Docker-Container, Shares, Netzwerk, UPS und Systemlogs. Nicht für Home Assistant oder Medieninhalte; Standardzugriff read-only."}}] [{"url":"http://mike-ai-mcp-web:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"web-local","name":"Web (öffentlich, read-only)","description":"Für aktuelle öffentliche Internetdaten, Quellenprüfung, GitHub/Hugging Face und Produktsuche. Nicht für Home Assistant, Medienverwaltung oder NAS-Diagnose."}},{"url":"http://mike-ai-mcp-homeassistant:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"homeassistant-local","name":"Home Assistant (lokal)","description":"Nur für Home-Assistant-Entitäten, Zustände, Historie, Automationen, Dashboards und HA-Diagnose. Nicht für Unraid, Sonarr/Radarr oder allgemeine Websuche."}},{"url":"http://mike-ai-mcp-arr:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"arr-local","name":"Sonarr und Radarr (lokal)","description":"Nur für verwaltete Serien/Filme, fehlende Episoden, Queue und Suche über konfigurierte Indexer. Keine allgemeine Websuche; Schreibaktionen benötigen Vorschau und Freigabe."}},{"url":"http://mike-ai-mcp-unraid-official:8000/mcp","path":"","type":"mcp","auth_type":"none","headers":null,"key":"","config":{"enable":true,"access_grants":[]},"info":{"id":"unraid-readonly-local","name":"Unraid (Systemdiagnose)","description":"Nur für Unraid-Host, Array, Datenträger, Docker-Container, Shares, Netzwerk, UPS und Systemlogs. Nicht für Home Assistant oder Medieninhalte; Standardzugriff read-only."}}]
DO_NOT_TRACK: "true" DO_NOT_TRACK: "true"
SCARF_NO_ANALYTICS: "true" SCARF_NO_ANALYTICS: "true"
ports:
- "${AI_BIND_ADDRESS:-127.0.0.1}:8080:8080"
dns: ["${AI_DNS:-1.1.1.1}"] dns: ["${AI_DNS:-1.1.1.1}"]
networks: [frontend, tools] networks: [frontend, tools]
depends_on: depends_on:
wireguard-gateway:
condition: service_healthy
router: router:
condition: service_healthy condition: service_healthy
security_opt: ["no-new-privileges:true"] security_opt: ["no-new-privileges:true"]
@@ -575,6 +606,9 @@ networks:
tools: tools:
external: true external: true
name: mike-ai-tools name: mike-ai-tools
tools-egress:
external: true
name: mike-ai-tools-egress
volumes: volumes:
open-webui-data: open-webui-data:
+9 -3
View File
@@ -3,7 +3,6 @@
AI_HOSTNAME=ki-host AI_HOSTNAME=ki-host
ADMIN_USER=mike ADMIN_USER=mike
AI_BIND_ADDRESS=10.77.0.2
MODEL_DIR=/srv/mike-ai/models MODEL_DIR=/srv/mike-ai/models
# Installing a new NVIDIA driver can require one reboot. In that case this # Installing a new NVIDIA driver can require one reboot. In that case this
@@ -23,9 +22,16 @@ FLUX_MODEL_DIR=/data/models/FLUX.2-klein-4B
ENABLE_HARDWARE_WATCHDOG=true ENABLE_HARDWARE_WATCHDOG=true
PRIMARY_NETWORK_INTERFACE=enp7s0 PRIMARY_NETWORK_INTERFACE=enp7s0
WAKE_ON_LAN_INTERFACE=enp7s0 WAKE_ON_LAN_INTERFACE=enp7s0
SSH_KEY_ONLY=true
# WireGuard client. The home peer must route 10.77.0.2/32 back to this host. # WireGuard terminates in a dedicated Docker gateway. The Fritzbox export is a
WIREGUARD_ENABLE=true # root-only deployment secret and must never be committed.
WIREGUARD_MODE=container
WIREGUARD_CONFIG_FILE=/etc/mike-ai/wireguard/fritz-athena.conf
WIREGUARD_ENABLE=false
# The values below are only used by the legacy host-terminated mode. The
# default container mode takes address, peer and routes from the Fritzbox file.
AI_BIND_ADDRESS=10.77.0.2
WG_INTERFACE=wg0 WG_INTERFACE=wg0
WG_ADDRESS=10.77.0.2/32 WG_ADDRESS=10.77.0.2/32
WG_HOME_SUBNET=192.168.1.0/24 WG_HOME_SUBNET=192.168.1.0/24
+20 -15
View File
@@ -3,18 +3,18 @@
## Grundsatz ## Grundsatz
Der Host läuft auf Debian 13. Das Betriebssystem darf im Universitätsnetz Der Host läuft auf Debian 13. Das Betriebssystem darf im Universitätsnetz
administrierbar bleiben; die KI-Plattform wird ausschließlich an die administrierbar bleiben. Ein eigener Docker-Gateway beendet WireGuard und ist
WireGuard-Adresse gebunden. KI-Container erreichen Heimnetz und Internet über der einzige von außen erreichbare Einstieg in die KI-Plattform. KI-Container
den Heim-WireGuard-Peer. Bei Tunnelausfall verhindert eine Blackhole-Route den erreichen Heimnetz und Internet über die Fritzbox; Quellrouting zum Gateway
unbeabsichtigten Rückfall auf das Universitätsgateway. verhindert bei Tunnelausfall einen Rückfall auf das Universitätsgateway.
```text ```text
Heimnetz / VPN-Clients Heimnetz / VPN-Clients
| |
WireGuard WireGuard
| |
10.77.0.2:8080 Open WebUI <Fritz-VPN-IP>:8080 Open WebUI
10.77.0.2:8081 Profile Router API <Fritz-VPN-IP>:8081 Profile Router API
| |
Docker-intern Docker-intern
+-- Profile Controller -- Docker Socket (feste Allowlist) +-- Profile Controller -- Docker Socket (feste Allowlist)
@@ -35,8 +35,9 @@ Heimnetz / VPN-Clients
| Komponente | Außen erreichbar | Aufgabe | | Komponente | Außen erreichbar | Aufgabe |
|---|---|---| |---|---|---|
| Open WebUI | nur WireGuard, Port 8080 | Chat-Oberfläche | | WireGuard Gateway | VPN-Adresse, Port 8080/8081 | Tunnel und eng begrenzte TCP-Proxys |
| Profile Router | nur WireGuard, Port 8081 | OpenAI-API und Profilwahl | | Open WebUI | nur Docker-intern | Chat-Oberfläche |
| Profile Router | nur Docker-intern | OpenAI-API und Profilwahl |
| Profile Controller | nein | startet ausschließlich vier bekannte Profile | | Profile Controller | nein | startet ausschließlich vier bekannte Profile |
| llama.cpp Profile | nein | Inferenz, Tool Calling, integrierte Vision | | llama.cpp Profile | nein | Inferenz, Tool Calling, integrierte Vision |
| Piper | nein | lokale deutsche Text-to-Speech-Ausgabe | | Piper | nein | lokale deutsche Text-to-Speech-Ausgabe |
@@ -79,15 +80,19 @@ auf der 3060. Ultra bleibt für maximalen Kontext bewusst text-only.
## Netzwerk ## Netzwerk
- Docker-Netze liegen ausschließlich unter `172.30.0.0/16`. - Docker-Netze liegen ausschließlich unter `172.30.0.0/16`.
- Open WebUI und Router binden an `AI_BIND_ADDRESS`, die WireGuard-IP. - Open WebUI und Router besitzen keine Docker-Host-Portfreigabe.
- Quellrouting schickt KI-Container in Tabelle 51820 über WireGuard. - Der Gateway lauscht in seinem eigenen Namespace auf der Fritz-VPN-IP und
- Eine Blackhole-Default-Route bleibt als Fail-Closed-Fallback bestehen. leitet nur 8080/8081 zu den internen Diensten weiter.
- Firewallregeln gestatten aus dem VPN nur die beiden veröffentlichten Ports. - Quellrouting schickt `172.30.10.0/24` und `172.30.50.0/24` zum Gateway;
Regeln für `172.30.0.0/16` bewahren rein internen Docker-Verkehr.
- Die verschlüsselten äußeren Gateway-Pakete sind eng von diesen Quellregeln
ausgenommen und verlassen Athena über die normale Standortverbindung.
- Bleibt die Gateway-Adresse aus, existiert keine alternative Route für die
Anwendungscontainer (fail-closed).
- Der Host routet weder Universitätsverkehr ins Heimnetz noch Heimverkehr ins - Der Host routet weder Universitätsverkehr ins Heimnetz noch Heimverkehr ins
Universitätsnetz. Universitätsnetz.
- Das Heimnetz muss die Rückroute zur WireGuard-Adresse kennen. Soll auch der - Adressen, Heimrouten, Full-Tunnel und Keepalive stammen aus dem root-only
Internetzugang der KI über zuhause laufen, braucht der Heim-Peer zusätzlich Fritzbox-Clientexport; der Debian-Host übernimmt dessen Default-Route nicht.
IP-Forwarding und NAT ins Heim-WAN.
## Optionale Erweiterungen ## Optionale Erweiterungen
+3 -1
View File
@@ -81,7 +81,9 @@ laufen, sondern alle fachlichen Funktionen geprüft wurden.
## Phase F – Sicherheitsprüfung ## Phase F – Sicherheitsprüfung
- [ ] Port 8080 von normalen Clients nicht erreichbar - [ ] Port 8080/8081 an der physischen Hostadresse nicht erreichbar
- [ ] beide Ports über die Fritz-VPN-Adresse erreichbar
- [ ] gestopptes WireGuard-Gateway blockiert Container-Egress
- [ ] Hilfsports nur localhost - [ ] Hilfsports nur localhost
- [ ] Router nur aus erlaubtem Netz erreichbar - [ ] Router nur aus erlaubtem Netz erreichbar
- [ ] Dienste laufen mit minimalen Rechten - [ ] Dienste laufen mit minimalen Rechten
+17 -19
View File
@@ -11,8 +11,8 @@ startet den Stack.
1. Die Universität muss den ausgehenden WireGuard-Tunnel erlauben. 1. Die Universität muss den ausgehenden WireGuard-Tunnel erlauben.
2. Heimnetz, Universitätsnetz und Docker-Netz dürfen sich nicht überschneiden. 2. Heimnetz, Universitätsnetz und Docker-Netz dürfen sich nicht überschneiden.
3. Der WireGuard-Heim-Peer braucht eine feste öffentliche Adresse oder DNS. 3. In der Fritzbox eine Konfiguration für **einen einzelnen Client** exportieren.
4. Für KI-Internetzugang über zuhause: Forwarding und NAT am Heim-Peer. 4. Der Fritzbox-Zugang muss Heimnetz und gewünschten Internetverkehr erlauben.
5. Das private Repository muss auf dem neuen Host lesbar sein. 5. Das private Repository muss auf dem neuen Host lesbar sein.
## Debian installieren ## Debian installieren
@@ -32,10 +32,16 @@ chmod 600 config/install.env
editor config/install.env editor config/install.env
``` ```
Mindestens `ADMIN_USER`, `AI_BIND_ADDRESS`, `WG_ADDRESS`, Mindestens `ADMIN_USER`, Netzwerkschnittstellen, GPU-Zuordnung und Modellwerte
`WG_PEER_PUBLIC_KEY`, `WG_ENDPOINT` und `WG_HOME_SUBNET` anpassen. Private prüfen. Die Fritzbox-Datei vor dem Start root-only ablegen:
WireGuard-, Router- und WebUI-Schlüssel werden lokal erzeugt und nur unter
`/etc/mike-ai` gespeichert. ```bash
sudo install -d -m 700 /etc/mike-ai/wireguard
sudo install -m 600 fritz-athena.conf /etc/mike-ai/wireguard/fritz-athena.conf
```
Router- und WebUI-Schlüssel werden lokal erzeugt und nur unter `/etc/mike-ai`
gespeichert. Der Installer gibt keine privaten WireGuard-Werte aus.
## Installation starten ## Installation starten
@@ -50,16 +56,7 @@ Beim ersten Stackstart lädt der interne Piper-Container die konfigurierte
deutsche Stimme in sein persistentes Volume. Dadurch kann seine erste deutsche Stimme in sein persistentes Volume. Dadurch kann seine erste
Bereitschaft je nach Internetverbindung etwas länger dauern. Bereitschaft je nach Internetverbindung etwas länger dauern.
Der Installer zeigt nur den öffentlichen WireGuard-Schlüssel. Diesen am Der Compose-Start wartet auf einen aktuellen WireGuard-Handshake. API-Schlüssel
Heim-Peer eintragen:
```ini
[Peer]
PublicKey = <AUSGABE-DES-INSTALLERS>
AllowedIPs = 10.77.0.2/32
```
Erst wenn der Tunnel steht, kann der Bootstrap fortfahren. API-Schlüssel
werden nicht ausgegeben. Sie liegen root-only unter `/etc/mike-ai`. werden nicht ausgegeben. Sie liegen root-only unter `/etc/mike-ai`.
## Ergebnis und Abnahme ## Ergebnis und Abnahme
@@ -69,7 +66,8 @@ werden nicht ausgegeben. Sie liegen root-only unter `/etc/mike-ai`.
- llama.cpp-WebUI: absichtlich deaktiviert und nicht veröffentlicht - llama.cpp-WebUI: absichtlich deaktiviert und nicht veröffentlicht
```bash ```bash
sudo systemctl status wg-quick@wg0 mike-ai-network-guard sudo systemctl status mike-ai-container-vpn-guard
sudo docker inspect -f '{{.State.Health.Status}}' mike-ai-wireguard-gateway
sudo docker compose --env-file /etc/mike-ai/stack.env \ sudo docker compose --env-file /etc/mike-ai/stack.env \
-f /opt/mike-ai/stack/compose.yaml ps -f /opt/mike-ai/stack/compose.yaml ps
curl http://<WIREGUARD-IP>:8081/health curl http://<WIREGUARD-IP>:8081/health
@@ -91,8 +89,8 @@ curl -fsS http://127.0.0.1:8081/v1/audio/speech \
-o /tmp/athena-piper-test.mp3 -o /tmp/athena-piper-test.mp3
``` ```
Zusätzlich prüfen: Uni-LAN sieht keine KI-Ports; Heimnetz erreicht beide; Zusätzlich prüfen: Standort-LAN sieht keine KI-Ports; Heimnetz erreicht beide;
gestopptes WireGuard lässt KI-Container nicht ins Internet; jeder Profilwechsel gestopptes VPN-Gateway lässt KI-Container nicht ins Internet; jeder Profilwechsel
startet exakt einen llama-Container; Text, Tool Call, Bild und Sprachausgabe funktionieren. startet exakt einen llama-Container; Text, Tool Call, Bild und Sprachausgabe funktionieren.
Nach dem ersten Anlegen des OpenWebUI-Administrators werden Filter, Quick Nach dem ersten Anlegen des OpenWebUI-Administrators werden Filter, Quick
+2 -2
View File
@@ -103,7 +103,7 @@ erfolgreich. Medium ist das Start- und Standardprofil.
Clients verbinden sich mit: Clients verbinden sich mit:
```text ```text
http://HOST:8081/v1 http://<FRITZ-VPN-IP>:8081/v1
``` ```
Als API-Key verwenden sie den Inhalt von `/etc/mike-ai/router-api-key` über Als API-Key verwenden sie den Inhalt von `/etc/mike-ai/router-api-key` über
@@ -120,7 +120,7 @@ Vision, Bildgenerierung, STT und TTS umgehen.
- `GET /ready`: Router und Textmodell sind einsatzbereit - `GET /ready`: Router und Textmodell sind einsatzbereit
- `GET /status`: authentifizierter Detailstatus, Profil, Upstream, aktive Jobs - `GET /status`: authentifizierter Detailstatus, Profil, Upstream, aktive Jobs
- `GET /v1/models`: virtuelle Modelle - `GET /v1/models`: virtuelle Modelle
- llama.cpp-Metriken: Port 8080, nur im administrativen Netz freigeben - llama.cpp-Metriken: ausschließlich Docker-intern abfragen
- systemd-Journal: nur Metadaten und Fehler prüfen; keine Promptinhalte sammeln - systemd-Journal: nur Metadaten und Fehler prüfen; keine Promptinhalte sammeln
## Upgrade-Regel ## Upgrade-Regel
+6 -2
View File
@@ -114,13 +114,17 @@ Zielmatrix:
| Port | Zugriff | | Port | Zugriff |
|---:|---| |---:|---|
| 22 | nur Administration | | 22 | nur Administration |
| 8080 | localhost | | 8080 | ausschließlich WireGuard-Gateway (Open WebUI) |
| 8081 | vertrauenswürdiges LAN/VPN | | 8081 | ausschließlich WireGuard-Gateway (Router) |
| 8084 | localhost | | 8084 | localhost |
| 8085 | localhost | | 8085 | localhost |
| 8000 | localhost | | 8000 | localhost |
| 5240 | optional nur Administration | | 5240 | optional nur Administration |
Open WebUI und Router selbst besitzen keine Host-Portfreigaben. Zusätzlich zum
Repository muss die verschlüsselt gesicherte Fritzbox-Clientdatei als
`/etc/mike-ai/wireguard/fritz-athena.conf` (0600) wiederhergestellt werden.
## 6. Speicherlayout – offen ## 6. Speicherlayout – offen
Vor dem Neuaufbau festlegen: Vor dem Neuaufbau festlegen:
+9 -4
View File
@@ -29,11 +29,12 @@ müssen aktiviert sein.
Vor dem Transport prüfen: Vor dem Transport prüfen:
```bash ```bash
systemctl is-enabled ssh docker wg-quick@wg0 systemctl is-enabled ssh docker mike-ai-container-vpn-guard
systemctl is-active ssh docker wg-quick@wg0 systemctl is-active ssh docker mike-ai-container-vpn-guard
ethtool enp7s0 | grep Wake-on ethtool enp7s0 | grep Wake-on
systemctl show -p RuntimeWatchdogUSec systemctl show -p RuntimeWatchdogUSec
wg show docker inspect -f '{{.State.Health.Status}}' mike-ai-wireguard-gateway
docker exec mike-ai-wireguard-gateway wg show wg0 latest-handshakes
``` ```
Erwartet werden `enabled`, `active`, `Wake-on: g`, ein Watchdog-Wert von einer Erwartet werden `enabled`, `active`, `Wake-on: g`, ein Watchdog-Wert von einer
@@ -43,6 +44,9 @@ Minute sowie ein aktueller WireGuard-Handshake.
- Der physische Anschluss bezieht seine Adresse per DHCP; im Standortnetz muss - Der physische Anschluss bezieht seine Adresse per DHCP; im Standortnetz muss
dafür eine Freigabe bzw. Registrierung existieren. dafür eine Freigabe bzw. Registrierung existieren.
- SSH bleibt auf der Standort-Schnittstelle erreichbar, akzeptiert aber nur
Public-Key-Anmeldungen. Vor dem Transport muss der Schlüsselzugriff getestet
werden.
- Der WireGuard-Tunnel muss **vor dem Transport** erfolgreich aufgebaut und von - Der WireGuard-Tunnel muss **vor dem Transport** erfolgreich aufgebaut und von
zuhause erreichbar getestet sein. zuhause erreichbar getestet sein.
- Für WireGuard braucht Athena keine eingehende Portfreigabe am Standort: Der - Für WireGuard braucht Athena keine eingehende Portfreigabe am Standort: Der
@@ -59,7 +63,8 @@ Minute sowie ein aktueller WireGuard-Handshake.
2. Rechner sauber herunterfahren und per Wake-on-LAN einschalten. 2. Rechner sauber herunterfahren und per Wake-on-LAN einschalten.
3. Netzspannung bei laufendem Rechner trennen, 30 Sekunden warten, wieder 3. Netzspannung bei laufendem Rechner trennen, 30 Sekunden warten, wieder
einschalten: Athena bootet automatisch vollständig hoch. einschalten: Athena bootet automatisch vollständig hoch.
4. WireGuard aus- und wieder einschalten und Fail-closed-Routing prüfen. 4. `mike-ai-wireguard-gateway` stoppen: Container-Egress muss scheitern;
Gateway wieder starten und aktuellen Handshake prüfen.
5. Von zuhause aus ausschließlich über die spätere VPN-Adresse zugreifen. 5. Von zuhause aus ausschließlich über die spätere VPN-Adresse zugreifen.
Ohne erfolgreich getesteten WireGuard-Tunnel und die UEFI-Stromoptionen gilt der Ohne erfolgreich getesteten WireGuard-Tunnel und die UEFI-Stromoptionen gilt der
+9 -8
View File
@@ -2,17 +2,18 @@
## Netzgrenze ## Netzgrenze
- KI-Ports binden ausschließlich an die WireGuard-IP. - Open WebUI und Router veröffentlichen keinerlei Host-Ports.
- Docker-Netze `172.30.0.0/16` verwenden eine eigene Routingtabelle. - Ein dedizierter WireGuard-Container stellt auf seiner VPN-Adresse nur 8080
und 8081 bereit.
- Die egressfähigen Docker-Netze verwenden eigene Routingtabellen.
- Heimnetz- und optionaler Internetverkehr laufen über WireGuard. - Heimnetz- und optionaler Internetverkehr laufen über WireGuard.
- Eine Blackhole-Default-Route verhindert Fail-open bei Tunnelverlust. - Die Tabellen zeigen ausschließlich zum Gateway-Container und besitzen keine
- `DOCKER-USER` erlaubt nur etablierte Verbindungen, KI→WireGuard und Route über das Standortgateway. Das ist der Fail-Closed-Mechanismus.
WireGuard→Open-WebUI/Router.
- Der Host ist kein Router zwischen Universitäts- und Heimnetz. - Der Host ist kein Router zwischen Universitäts- und Heimnetz.
Docker-publizierte Ports können gewöhnliche Host-Firewallregeln umgehen. Da keine KI-Ports publiziert werden, kann Docker die Host-Firewall an dieser
Darum setzt der Installer seine Regeln ausdrücklich in `DOCKER-USER` und Stelle nicht umgehen. SSH bleibt davon getrennt und schlüsselbasiert auf der
verlässt sich nicht allein auf UFW. physischen Schnittstelle erreichbar.
## Containergrenzen ## Containergrenzen
+40 -42
View File
@@ -1,55 +1,53 @@
# WireGuard-Heimseite # WireGuard über die Fritzbox
Der Installer kann nur den KI-Host konfigurieren. Einmalig muss der vorhandene ## Gewählter Betriebsmodus
WireGuard-Router im Heimnetz den neuen Peer kennen. Ohne diesen externen Schritt
kann kein automatisches Skript auf dem Uni-Host den Tunnel fertigstellen.
## Peer ergänzen Athena verwendet die Fritzbox-Konfiguration für **einen einzelnen
WireGuard-Client**. Die unveränderte Exportdatei wird als root-only Secret nach
Beispiel mit KI-WireGuard-Adresse `10.77.0.2/32`: ```text
/etc/mike-ai/wireguard/fritz-athena.conf
```ini
[Peer]
PublicKey = <PUBLIC-KEY-DES-KI-HOSTS>
AllowedIPs = 10.77.0.2/32
``` ```
Das Heimgerät muss das LAN `192.168.1.0/24` zum Tunnel routen können. Geräte im kopiert (`chmod 600`). Sie gehört weder ins Git-Repository noch in Backups ohne
Heimnetz benötigen entweder eine Route für `10.77.0.2/32` über den Verschlüsselung. Eine LAN-zu-LAN-Konfiguration ist für diesen Host nicht nötig.
WireGuard-Router oder der Router maskiert den VPN-Verkehr passend.
## KI-Internetzugang über zuhause Der Tunnel endet im Container `mike-ai-wireguard-gateway`. Nur dieser Container
erhält `NET_ADMIN` und `/dev/net/tun`. Open WebUI und Router veröffentlichen
keine Host-Ports; der Gateway stellt ausschließlich Port 8080 und 8081 auf der
von der Fritzbox zugeteilten VPN-Adresse bereit.
Wenn `WG_ROUTE_AI_INTERNET=true` gesetzt ist, muss der Heim-Peer IPv4-Forwarding ## Split der Verantwortlichkeiten
und NAT ins WAN erlauben. Das wird auf dem Heimrouter eingerichtet, nicht auf
dem Universitätsnetz. Beispielprinzip für nftables:
```nft - Debian, Paketverwaltung und SSH benutzen die normale Standortverbindung.
table inet mike_ai { - Die Docker-Netze `frontend` und `tools-egress` werden per Quellrouting zum
chain forward { WireGuard-Gateway geschickt.
type filter hook forward priority 0; policy accept; - Interner Docker-Verkehr bleibt lokal und durchquert den Tunnel nicht.
iifname "wg0" ip saddr 10.77.0.2 accept - Der Fritzbox-Export darf `0.0.0.0/0` und `::/0` enthalten. Das ändert **nicht**
oifname "wg0" ip daddr 10.77.0.2 ct state established,related accept die Default-Route des Debian-Hosts, sondern nur die des Gateway-Namespace.
} - Fällt WireGuard aus, bleibt die Quellroute auf den dann unerreichbaren
} Gateway zeigen: Anwendungscontainer fallen geschlossen aus.
table ip mike_ai_nat { ## Kontrolle ohne Geheimnisse auszugeben
chain postrouting {
type nat hook postrouting priority 100; policy accept; ```bash
ip saddr 10.77.0.2 oifname "<HEIM-WAN-INTERFACE>" masquerade docker inspect -f '{{.State.Health.Status}}' mike-ai-wireguard-gateway
} docker exec mike-ai-wireguard-gateway wg show wg0 latest-handshakes
} systemctl status mike-ai-container-vpn-guard
``` ```
Die tatsächlichen Interface-Namen und die bestehende Firewall des Heimrouters Der Healthcheck verlangt einen Handshake, der jünger als drei Minuten ist.
gehen vor. Regeln nicht blind neben eine bereits verwaltete Firewall kopieren. Open WebUI liegt unter `http://<VPN-IP>:8080`, der Router unter
`http://<VPN-IP>:8081`. An der physischen Standortadresse dürfen beide Ports
nicht antworten.
## Sicherheitsprüfung ## Getestetes Verhalten am 22. August 2026
1. Vom Heimnetz `10.77.0.2` erreichen. - Fritzbox-Handshake und Datenverkehr in beide Richtungen: erfolgreich
2. Open WebUI auf `10.77.0.2:8080` erreichen. - Open WebUI und Router über die VPN-Adresse: HTTP 200
3. Aus dem Universitäts-LAN Port 8080/8081 nicht erreichen. - dieselben Ports über die physische Hostadresse: geschlossen
4. `wg0` am KI-Host stoppen: KI-Container dürfen nun weder Heimnetz noch - VPN-Gateway gestoppt: ausgehender Open-WebUI-Test blockiert (fail-closed)
Internet erreichen. - Gateway erneut gestartet: automatischer aktueller Handshake
5. Der Debian-Host selbst darf weiterhin nur die ausdrücklich gewünschte
Administration über das Uni-LAN anbieten. Die Exportdatei muss Bestandteil des verschlüsselten Disaster-Recovery-Satzes
sein. Ohne sie kann ein neuer Host den Heimtunnel nicht rekonstruieren.
+52 -6
View File
@@ -38,7 +38,7 @@ fi
# shellcheck disable=SC1090 # shellcheck disable=SC1090
source "$CONFIG" source "$CONFIG"
required=(AI_HOSTNAME ADMIN_USER AI_BIND_ADDRESS MODEL_DIR FAST_MODEL_FILE required=(AI_HOSTNAME ADMIN_USER MODEL_DIR FAST_MODEL_FILE
FAST_MODEL_URL FAST_MODEL_SHA256 MEDIUM_MODEL_FILE MEDIUM_MODEL_URL FAST_MODEL_URL FAST_MODEL_SHA256 MEDIUM_MODEL_FILE MEDIUM_MODEL_URL
MEDIUM_MODEL_SHA256 LARGE_MODEL_FILE LARGE_MODEL_URL LARGE_MODEL_SHA256 MEDIUM_MODEL_SHA256 LARGE_MODEL_FILE LARGE_MODEL_URL LARGE_MODEL_SHA256
ULTRA_MODEL_FILE ULTRA_MODEL_URL ULTRA_MODEL_SHA256 ULTRA_MODEL_FILE ULTRA_MODEL_URL ULTRA_MODEL_SHA256
@@ -47,6 +47,9 @@ required=(AI_HOSTNAME ADMIN_USER AI_BIND_ADDRESS MODEL_DIR FAST_MODEL_FILE
for name in "${required[@]}"; do for name in "${required[@]}"; do
[[ -n "${!name:-}" ]] || die "Pflichtwert $name fehlt." [[ -n "${!name:-}" ]] || die "Pflichtwert $name fehlt."
done done
if [[ ${WIREGUARD_MODE:-container} != container ]]; then
[[ -n ${AI_BIND_ADDRESS:-} ]] || die "Pflichtwert AI_BIND_ADDRESS fehlt."
fi
source /etc/os-release source /etc/os-release
[[ ${ID:-} == debian ]] || die "Unterstützt wird Debian, gefunden: ${ID:-unbekannt}." [[ ${ID:-} == debian ]] || die "Unterstützt wird Debian, gefunden: ${ID:-unbekannt}."
@@ -111,6 +114,19 @@ EOF
fi fi
} }
setup_ssh_hardening() {
[[ ${SSH_KEY_ONLY:-true} == true ]] || return 0
log "SSH auf Schlüsselanmeldung beschränken"
install -d -m 0755 /etc/ssh/sshd_config.d
cat >/etc/ssh/sshd_config.d/20-athena-key-only.conf <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
EOF
sshd -t
systemctl reload ssh
}
install_docker() { install_docker() {
log "Docker CE aus dem offiziellen Repository installieren" log "Docker CE aus dem offiziellen Repository installieren"
install -m 0755 -d /etc/apt/keyrings install -m 0755 -d /etc/apt/keyrings
@@ -188,6 +204,7 @@ install_nvidia() {
} }
setup_wireguard() { setup_wireguard() {
[[ ${WIREGUARD_MODE:-container} != container ]] || return 0
[[ ${WIREGUARD_ENABLE:-false} == true ]] || return 0 [[ ${WIREGUARD_ENABLE:-false} == true ]] || return 0
for name in WG_INTERFACE WG_ADDRESS WG_HOME_SUBNET WG_PEER_PUBLIC_KEY WG_PEER_ENDPOINT; do for name in WG_INTERFACE WG_ADDRESS WG_HOME_SUBNET WG_PEER_PUBLIC_KEY WG_PEER_ENDPOINT; do
[[ -n "${!name:-}" && ${!name} != REPLACE_* ]] || die "WireGuard-Wert $name fehlt." [[ -n "${!name:-}" && ${!name} != REPLACE_* ]] || die "WireGuard-Wert $name fehlt."
@@ -240,8 +257,9 @@ install_stack_files() {
chmod 0640 "$searx" chmod 0640 "$searx"
cat >$SECRETS_DIR/stack.env <<EOF cat >$SECRETS_DIR/stack.env <<EOF
AI_BIND_ADDRESS=$AI_BIND_ADDRESS AI_BIND_ADDRESS=${AI_BIND_ADDRESS:-127.0.0.1}
MODEL_DIR=$MODEL_DIR MODEL_DIR=$MODEL_DIR
WIREGUARD_CONFIG_FILE=${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf}
ROUTER_API_KEY=$(<$SECRETS_DIR/router-api-key) ROUTER_API_KEY=$(<$SECRETS_DIR/router-api-key)
CONTROLLER_TOKEN=$(<$SECRETS_DIR/controller-token) CONTROLLER_TOKEN=$(<$SECRETS_DIR/controller-token)
WEBUI_SECRET_KEY=$(<$SECRETS_DIR/webui-secret) WEBUI_SECRET_KEY=$(<$SECRETS_DIR/webui-secret)
@@ -316,6 +334,18 @@ EOF
} }
install_routing_guard() { install_routing_guard() {
if [[ ${WIREGUARD_MODE:-container} == container ]]; then
log "Container-WireGuard-Routing installieren"
[[ -s ${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf} ]] || \
die "Fritzbox-WireGuard-Datei fehlt: ${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf}"
install -m 0755 "$ROOT_DIR/platform/host/mike-ai-container-vpn-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
systemctl daemon-reload
systemctl enable mike-ai-container-vpn-guard.service
return 0
fi
[[ ${WIREGUARD_ENABLE:-false} == true ]] || return 0 [[ ${WIREGUARD_ENABLE:-false} == true ]] || return 0
log "KI-Container auf WireGuard routen und Uni-Netz als Transit sperren" log "KI-Container auf WireGuard routen und Uni-Netz als Transit sperren"
cat >/usr/local/sbin/mike-ai-network-guard <<EOF cat >/usr/local/sbin/mike-ai-network-guard <<EOF
@@ -392,6 +422,10 @@ build_and_start() {
docker compose --env-file "$SECRETS_DIR/stack.env" up -d --build \ docker compose --env-file "$SECRETS_DIR/stack.env" up -d --build \
profile-controller router open-webui profile-controller router open-webui
if [[ ${WIREGUARD_MODE:-container} == container ]]; then
systemctl restart mike-ai-container-vpn-guard.service
fi
log "Auf gesunden Router warten" log "Auf gesunden Router warten"
local deadline=$((SECONDS + 180)) local deadline=$((SECONDS + 180))
until [ "$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' mike-ai-router 2>/dev/null || true)" = "healthy" ]; do until [ "$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' mike-ai-router 2>/dev/null || true)" = "healthy" ]; do
@@ -438,6 +472,7 @@ PY
hostnamectl set-hostname "$AI_HOSTNAME" hostnamectl set-hostname "$AI_HOSTNAME"
install_base_packages install_base_packages
setup_remote_recovery setup_remote_recovery
setup_ssh_hardening
install_docker install_docker
install_nvidia install_nvidia
setup_wireguard setup_wireguard
@@ -447,11 +482,22 @@ install_routing_guard
build_and_start build_and_start
log "Installation abgeschlossen" log "Installation abgeschlossen"
if [[ ${WIREGUARD_MODE:-container} == container ]]; then
vpn_address=$(awk -F= '
/^[[:space:]]*Address[[:space:]]*=/ {
value=$2; gsub(/[[:space:]]/, "", value); split(value, addresses, ",")
for (i in addresses) if (addresses[i] !~ /:/) { sub(/\/.*/, "", addresses[i]); print addresses[i]; exit }
}
' "${WIREGUARD_CONFIG_FILE:-/etc/mike-ai/wireguard/fritz-athena.conf}")
else
vpn_address=$AI_BIND_ADDRESS
fi
cat <<EOF cat <<EOF
OpenWebUI: http://${AI_BIND_ADDRESS}:8080 OpenWebUI: http://${vpn_address}:8080
Router-API: http://${AI_BIND_ADDRESS}:8081 Router-API: http://${vpn_address}:8081
Die geheimen Schlüssel liegen ausschließlich unter $SECRETS_DIR (0600). Die geheimen Schlüssel liegen ausschließlich unter $SECRETS_DIR (0600).
Konfiguriere auf der Heimseite für den KI-Peer mindestens die Rückroute Im Container-Modus stammen Peer, Adresse und Heimrouten vollständig aus dem
${WG_ADDRESS:-10.77.0.2/32}; bei Internet-Routing zusätzlich NAT/Forwarding. root-only Fritzbox-Export. Die physischen Host-Adressen veröffentlichen keine
KI-Ports.
EOF EOF
@@ -0,0 +1,13 @@
FROM debian:13-slim
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates iproute2 iptables procps socat wireguard-tools \
&& rm -rf /var/lib/apt/lists/*
COPY entrypoint.sh /usr/local/sbin/mike-ai-wireguard-entrypoint
COPY healthcheck.sh /usr/local/sbin/mike-ai-wireguard-healthcheck
RUN chmod 0755 /usr/local/sbin/mike-ai-wireguard-entrypoint \
/usr/local/sbin/mike-ai-wireguard-healthcheck
ENTRYPOINT ["/usr/local/sbin/mike-ai-wireguard-entrypoint"]
@@ -0,0 +1,68 @@
#!/bin/sh
set -eu
SOURCE=${WG_CONFIG_SOURCE:-/run/secrets/fritz-athena.conf}
RUNTIME=/run/wireguard/wg0.conf
[ -s "$SOURCE" ] || { echo "WireGuard configuration is missing" >&2; exit 1; }
install -d -m 0700 /run/wireguard
# Fritzbox exports global DNS directives and wg-quick hooks. DNS is assigned
# per application container by Docker; executable hooks are deliberately not
# accepted from a secret file. All cryptographic values remain untouched.
awk '
/^[[:space:]]*(DNS|Table|PreUp|PostUp|PreDown|PostDown|SaveConfig)[[:space:]]*=/ { next }
/^\[Interface\][[:space:]]*$/ { print; print "Table = off"; next }
{ print }
' "$SOURCE" >"$RUNTIME"
chmod 0600 "$RUNTIME"
# Docker deliberately keeps /proc/sys read-only inside this narrowly
# privileged container. Table=off prevents wg-quick from trying to modify
# global policy-routing sysctls; the two required routes are installed below.
physical_default=$(ip -4 route show default | head -n 1)
physical_gateway=$(printf '%s\n' "$physical_default" | awk '{for (i=1; i<=NF; i++) if ($i == "via") print $(i+1)}')
physical_device=$(printf '%s\n' "$physical_default" | awk '{for (i=1; i<=NF; i++) if ($i == "dev") print $(i+1)}')
wg-quick up "$RUNTIME"
# Keep the encrypted peer itself reachable over Docker's physical network,
# then make the tunnel the namespace default. Connected Docker routes remain
# intact for the reverse proxies and internal service discovery.
endpoint=$(wg show wg0 endpoints | awk 'NR == 1 { print $2 }')
case "$endpoint" in
\[*\]:*) endpoint_ip=${endpoint#\[}; endpoint_ip=${endpoint_ip%%\]*} ;;
*:*) endpoint_ip=${endpoint%:*} ;;
*) endpoint_ip= ;;
esac
if [ -n "$endpoint_ip" ] && [ -n "$physical_gateway" ] && [ -n "$physical_device" ]; then
case "$endpoint_ip" in
*:*) : ;; # Docker gateway networks are intentionally IPv4-only.
*) ip -4 route replace "$endpoint_ip/32" via "$physical_gateway" dev "$physical_device" ;;
esac
fi
ip -4 route replace default dev wg0
ip -6 route replace default dev wg0 2>/dev/null || true
cleanup() {
kill "${proxy_ui_pid:-}" "${proxy_router_pid:-}" 2>/dev/null || true
wg-quick down "$RUNTIME" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
# Forward only explicitly selected Docker networks into the tunnel. The
# gateway itself is the only container that receives NET_ADMIN.
iptables -P FORWARD DROP
iptables -A FORWARD -o wg0 -j ACCEPT
iptables -A FORWARD -i wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
# Nothing is published on the physical host. These listeners exist only in
# the WireGuard container namespace and forward VPN clients to internal names.
socat TCP-LISTEN:8080,bind=0.0.0.0,reuseaddr,fork TCP:open-webui:8080 &
proxy_ui_pid=$!
socat TCP-LISTEN:8081,bind=0.0.0.0,reuseaddr,fork TCP:router:8081 &
proxy_router_pid=$!
wait "$proxy_ui_pid"
@@ -0,0 +1,9 @@
#!/bin/sh
set -eu
wg show wg0 >/dev/null
ip link show wg0 | grep -q 'state UNKNOWN\|state UP'
latest=$(wg show wg0 latest-handshakes | cut -f2 | head -n 1)
now=$(date +%s)
[ "${latest:-0}" -gt 0 ]
[ $((now - latest)) -lt 180 ]
kill -0 1
+52
View File
@@ -0,0 +1,52 @@
#!/bin/sh
set -eu
network_bridge() {
id=$(docker network inspect -f '{{.Id}}' "$1")
printf 'br-%.12s\n' "$id"
}
wait_network() {
count=0
until docker network inspect "$1" >/dev/null 2>&1; do
count=$((count + 1))
[ "$count" -lt 60 ] || return 1
sleep 2
done
}
install_rule() {
network=$1 subnet=$2 gateway=$3 table=$4 priority=$5
bridge=$(network_bridge "$network")
ip rule del from "$subnet" table "$table" priority "$priority" 2>/dev/null || true
ip rule add from "$subnet" table "$table" priority "$priority"
# A fresh host has no FIB object for the custom table yet; iproute2 returns
# an error in that perfectly normal case.
ip route flush table "$table" 2>/dev/null || true
ip route add "$subnet" dev "$bridge" scope link table "$table"
ip route add default via "$gateway" dev "$bridge" table "$table"
}
wait_network mike-ai_frontend
wait_network mike-ai-tools-egress
# Preserve all east/west Docker communication before source-policy routing.
ip rule del to 172.30.0.0/16 lookup main priority 11000 2>/dev/null || true
ip rule add to 172.30.0.0/16 lookup main priority 11000
# The gateway's encrypted outer packets must leave through the host's normal
# uplink. Without these narrow exceptions they would match the source rules
# below and be routed straight back into the gateway (a routing loop).
ip rule del from 172.30.10.254/32 lookup main priority 11010 2>/dev/null || true
ip rule add from 172.30.10.254/32 lookup main priority 11010
ip rule del from 172.30.50.254/32 lookup main priority 11011 2>/dev/null || true
ip rule add from 172.30.50.254/32 lookup main priority 11011
install_rule mike-ai_frontend 172.30.10.0/24 172.30.10.254 51821 12010
install_rule mike-ai-tools-egress 172.30.50.0/24 172.30.50.254 51825 12050
# Drop any route/conntrack decisions learned before the policy rules existed.
# Compose will wait for the gateway healthcheck before exposing dependants.
if [ "$(docker inspect -f '{{.State.Running}}' mike-ai-wireguard-gateway 2>/dev/null || true)" = true ]; then
docker restart mike-ai-wireguard-gateway >/dev/null
fi
@@ -0,0 +1,14 @@
[Unit]
Description=Route Mike AI Docker egress through the WireGuard gateway container
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/mike-ai-container-vpn-guard
RemainAfterExit=yes
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target