Add fail-closed WireGuard container gateway
This commit is contained in:
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user