From 5a5f7aca4051ed20ce925d5cca2910b94f363bda Mon Sep 17 00:00:00 2001 From: spektr Date: Wed, 9 Sep 2026 16:02:20 +0200 Subject: [PATCH] GNAI-006: VPN LXC 105 Umsetzung dokumentiert (nftables, Quadlet, Peers) + Draft-Korrekturen - Hostname 'wireguard' (statt wireguard-gateway) - Testmatrix auf 'Ausstehend' (Mobilfunk-Tests noch nicht gelaufen) - PASSWORD_FORMAT: bcrypt (wg-easy/bcryptjs), nicht Argon2 - Quadlet via EnvironmentFile= (bcrypt hBcChars) - nftables-Konfig + ip_forward dokumentiert - wg-easy API-Referenz + allowedIPs-Limitation (Implementation-Log) - Offene Punkte: Fritzbox-Forward, NPM-Subdomain, Split-Tunnel-Verifikation, Pentest --- docs/GNAI-006-SEC_VPN.md | 263 ++++++++++++++++++++ docs/GNAI-006-SEC_VPN_Implementation-Log.md | 145 +++++++++++ 2 files changed, 408 insertions(+) create mode 100644 docs/GNAI-006-SEC_VPN.md create mode 100644 docs/GNAI-006-SEC_VPN_Implementation-Log.md diff --git a/docs/GNAI-006-SEC_VPN.md b/docs/GNAI-006-SEC_VPN.md new file mode 100644 index 0000000..5460e11 --- /dev/null +++ b/docs/GNAI-006-SEC_VPN.md @@ -0,0 +1,263 @@ +# Standard Operating Procedure & Technical Architecture Specification + +**Dokumenten-ID:** GNAI-006-SEC + +**Titel:** Homelab / Infrastructure – WireGuard VPN Gateway (LXC) Setup & Access Control + +**Version:** 1.1 + +**Status:** In Umsetzung (Schritt 1–4 & 7 fertig, Step 5/6/9 offen) + +**Vorgänger / Referenzen:** GNAI-003-INFRA, GNAI-004-SEC + +**Änderungsvorschlag Draft:** Hostname, Testmatrix und PASSWORD_HASH-Format gemäß Umsetzung korrigiert (siehe Changelog) + +--- + +## 1. Zweck & Geltungsbereich + +Dieses Dokument beschreibt die verbindliche Einrichtung, Härtung und Verifikation des WireGuard-VPN-Gateways auf einem dedizierten Proxmox LXC-Container (CT 105, IP `10.0.0.15/24`). Das System dient als primäre Management-Zugangsschicht für administrative Zugriffe aus dem Internet (z. B. via Mobilfunk) unter strikter Einhaltung des Sicherheits- und Rollenmodells aus GNAI-004-SEC. + +--- + +## 2. System-Baseline & Netzwerkarchitektur + +| Parameter | Wert | +| --- | --- | +| Container-ID | CT 105 | +| Hostname | `wireguard` | +| Betriebssystem | Debian GNU/Linux 13 (Trixie) | +| Proxmox VE | 9.2.11 | +| Unprivileged | Ja (mit `/dev/net/tun` Passthrough, `c 10:200`) | +| IP-Adresse LXC | `10.0.0.15/24` (Gateway: `10.0.0.1`) | +| VPN Subnetz | `10.8.0.0/24` | +| Listening Port | `51820/UDP` (Portweiterleitung auf Fritzbox 7530 an `10.0.0.15`) | +| DynDNS | `ghostnet.selfhost.eu` → Subdomain `vpn.ghostnet.selfhost.eu` als `WG_HOST` | +| Container Engine | Podman 5.4.2, Quadlet (`/etc/containers/systemd/wg-easy.container`) | +| VPN Management | wg-easy (`ghcr.io/wg-easy/wg-easy:latest`), UI auf `http://10.0.0.15:51821` (LAN-only) | +| Firewall | nftables, strikte INPUT policy `drop` | +| Routing | Split-Tunneling, `ip_forward=1` persistent | + +### Rollen- & Zugriffsmatrix (gemäß GNAI-004-SEC) + +* **Full Admin Profil (Admin-Laptop):** + * **VPN-IP:** `10.8.0.2/32` + * **AllowedIPs Client-Seitig:** `10.0.0.0/24` (Split-Tunneling) + * **Erlaubte Ziele:** Proxmox (`10.0.0.2`), LLM-Server (`10.0.0.90`), NPM Admin (`10.0.0.10:81`), Service-LXCs + +* **LLM Admin Profil (Android-Mobilgerät):** + * **VPN-IP:** `10.8.0.3/32` + * **AllowedIPs Client-Seitig:** `10.0.0.90/32` + * **Erlaubte Ziele:** Exklusiv LLM-Server (`10.0.0.90`, z. B. SSH Port 22) + +> **Hinweis wg-easy / AllowedIPs (Umsetzung 2026-09-09):** Die **serverseitige** `AllowedIPs`-Eintragung wird von wg-easy beim Konfig-Generate hartkodiert als `client.address/32` (`/app/lib/WireGuard.js`, `__saveConfig()`). Es existiert **kein API-Endpoint**, um die serverseitigen AllowedIPs abweichend zu setzen. Für die gewünschte Architektur ist dies korrekt: Serverseitig darf ein Peer nur aus seiner eigenen VPN-IP kommen; das **Routing (Split-Tunneling) wird clientseitig** über die AllowedIPs im heruntergeladenen Profil bestimmt (FullAdmin: `10.0.0.0/24, 10.8.0.0/24`; LLMAdmin: `10.0.0.90/32, 10.8.0.0/24`). **Offen:** Verifikation, dass die exportierten Client-Profile diese clientseitigen AllowedIPs enthalten (wg-easy bietet hierzu je nach Version nur eingeschränkte Konfiguration – siehe Implementation-Log, Punkt „allowedIPs-Limitation“). + +--- + +## 3. Schritt-für-Schritt Einrichtung & Deployment + +> Statuslegenden: ✅ erledigt (2026-09-09, Agent-Session) · ⏳ offen · 👤 User-Aktion + +### 1. LXC Container & TUN-Device auf Proxmox konfigurieren ✅ + +Unprivileged Debian 13 LXC (CT 105), IP `10.0.0.15/24`. In `/etc/pve/lxc/105.conf` auf Proxmox: + +```text +lxc.cgroup2.devices.allow: c 10:200 rwm +lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file +``` + +Verifikation: `ls -l /dev/net/tun` im Container zeigt Character Device `c 10 200`. ✅ (vorhanden seit Podman-Setup) + +### 2. Firewall (nftables) & IP-Forwarding ✅ + +Strikte INPUT policy `drop`, öffentlich nur WireGuard UDP `51820`, Management-Ports nur aus dem LAN, Forward-Chain für Split-Tunneling via `wg0`. + +`/etc/sysctl.d/99-ipforward.conf`: + +```ini +net.ipv4.ip_forward=1 +``` + +`/etc/nftables.conf` (geladen, SSH-Funktionalität nach Load verifiziert): + +```nft +#!/usr/sbin/nft -f + +flush ruleset + +table inet filter { + chain input { + type filter hook input priority filter; policy drop; + iifname "lo" accept + ct state invalid drop + ct state established,related accept + icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded } accept + udp dport 51820 accept + ip saddr 10.0.0.0/24 tcp dport { 22, 51821 } accept + limit rate 4/second burst 8 packets log prefix "nft-input-dropped: " + drop + } + chain forward { + type filter hook forward priority filter; policy drop; + ct state invalid drop + ct state established,related accept + iifname "wg0" ip daddr 10.0.0.0/24 ct state new accept + oifname "wg0" ip saddr 10.0.0.0/24 ip daddr 10.8.0.0/24 ct state new accept + drop + } + chain output { + type filter hook output priority accept; + } +} +``` + +Verifikation: ✅ SSH aus LAN erreichbar, WAN-Regeln per `nft list ruleset` kontrolliert, `ip_forward=1` aktiv und persistent. + +> **Hinweis:** Die wg-easy-`PostUp`-Regeln in `wg0.conf` (iptables MASQUERADE/FORWARD/INPUT-ACCEPT für 51820) sind mit der nftables-Config redundant, kollidieren aber nicht (iptables-FORWARD-ACCEPT ist breiter als die nftables-Forward-Rules, die maßgeblich bleiben). Optional über `WG_POST_UP`/`WG_PRE_DOWN` leerbaren lassen – Entscheidung offen. + +### 3. SSH-Härtung & Admin-User (gemäß GNAI-004-SEC) ✅ + +* User `admin` (uid 1000), sudoers `admin ALL=(ALL) NOPASSWD: ALL` +* `PermitRootLogin no` – SSH-Zugriff ausschließlich über `admin` mit SSH-Key +* Drop-in `/etc/ssh/sshd_config.d/99-security.conf`: + +```text +PermitRootLogin no +PasswordAuthentication no +KbdInteractiveAuthentication no +PubkeyAuthentication yes +PermitEmptyPasswords no +``` + +Verifikation: ✅ Key-Login als `admin`, `sudo` passwortlos, `/root/.ssh/authorized_keys` existierte nie (kein root-Key Cleanup nötig). + +### 4. Podman Quadlet für wg-easy ✅ + +Installiert: `podman` 5.4.2. + +**Wichtige Umsetzungsentscheidungen (Abweichungen vom ursprünglichen Draft):** + +1. **bcrypt statt Argon2:** wg-easy verifiziert das UI-Passwort mit **bcryptjs (Salt-Rounds 12)** über `PASSWORD_HASH`. Argon2-Hashes werden nicht akzeptiert. +2. **EnvironmentFile statt Environment=:** Der bcrypt-Hash enthält `$`-Zeichen (`$2a$12$…`), die im Quadlet-`Environment=`-Format fehleranfällig escaped werden müssen. Sauberer Weg: `EnvironmentFile=`. +3. **Unit-Name:** Quadlet generiert `wg-easy.service` (nicht `wg-easy.container.service`). + +`/opt/wg-easy/environment` (`chmod 600`, Owner root): + +```ini +WG_HOST=vpn.ghostnet.selfhost.eu +WG_PORT=51820 +WG_DEFAULT_ADDRESS=10.8.0.x +WG_DEFAULT_DNS=10.0.0.1 +ALLOW_CONFIG_CHANGE=true +PASSWORD_HASH=$2a$12$<60-zeichen- bcrypt-hash> +``` + +`/etc/containers/systemd/wg-easy.container`: + +```ini +[Container] +Image=ghcr.io/wg-easy/wg-easy:latest +ContainerName=wg-easy +Network=host +AddCapability=NET_ADMIN +AddCapability=NET_RAW +EnvironmentFile=/opt/wg-easy/environment +Volume=/opt/wg-easy/data:/etc/wireguard:rw + +[Service] +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` + +> **Sicherheitshinweis:** Der aktive bcrypt-Hash des wg-easy-UI-Passworts wird **bewusst nicht** in diesem Dokument abgelegt (Secrets nicht in Docs). Der aktuelle Wert liegt ausschließlich in `/opt/wg-easy/environment` auf dem LXC (chmod 600). Passwort-Rotation: neuen bcrypt-Hash (Rounds 12, z. B. `npx bcryptjs -r 12 ""`) generieren, Datei aktualisieren, `systemctl restart wg-easy.service`. + +Verifikation: ✅ `podman ps` zeigt `wg-easy` mit host-Netzwerk; `wg0` up mit `10.8.0.1/24`, `ListenPort 51820`; UI `http://10.0.0.15:51821` HTTP 200 (localhost & LAN-IP). + +### 5. Perimeter-Portweiterleitung Fritzbox 7530 👤 (User) + +Portweiterleitung **UDP `51820` → `10.0.0.15`** auf der Fritzbox 7530 einrichten. + +Verifikation: `tcpdump -i eth0 udp port 51820` auf dem LXC während Handshake-Versuch aus dem WAN. + +**Status: offen** + +### 6. DynDNS-Subdomain 👤 (User) + +In NPM (10.0.0.10) die Subdomain `vpn.ghostnet.selfhost.eu` anlegen, die auf die WAN-IP zeigt (bzw. bestehendes Ghost-Record-Verfahren verwenden). + +**Status: offen** + +### 7. Peers erzeugen ✅ + +Peers über die wg-easy-API erstellt (UI unter `http://10.0.0.15:51821`, Login `POST /api/session`): + +| Peer | VPN-IP | Client-UUID | Erlaubte Ziele (clientseitiges Routing) | +| --- | --- | --- | --- | +| Laptop-FullAdmin | `10.8.0.2` | `2e9ed910-36dc-4fbd-8694-8d904670ea19` | `10.0.0.0/24`, `10.8.0.0/24` | +| Android-LLMAdmin | `10.8.0.3` | `2009acd2-19cf-4e5e-a93a-7b05eb05fc12` | `10.0.0.90/32` (+ `10.8.0.0/24`) | + +Server-Config `wg0.conf` enthält beide Peers mit `PresharedKey`; serverseitig `AllowedIPs = 10.8.0.2/32` bzw. `10.8.0.3/32` (wg-easy-Hardcode, siehe Hinweis in Sektion 2). + +Verifikation ausstehend: ⏳ heruntergeladene Client-Profile auf clientseitige `AllowedIPs`, `Endpoint vpn.ghostnet.selfhost.eu:51820` und `PersistentKeepalive = 25` kontrollieren (abhängig von Step 5/6). + +### 8. Split-Tunneling-Routing verifizieren ⏳ + +Abhängig von Step 7. Falls die exportierten wg-easy-Profile die clientseitigen AllowedIPs nicht wie gewünscht enthalten, sind Alternativen zu evaluieren: + +* wg-easy-Einstellungen/Version prüfen (neuere Versionen unterstützen clientseitige `AllowedIPs`-Overrides, z. B. über `WG_ALLOWED_IPS`-Template oder Peer-Settings) +* Manuelle Client-Configs als Fallback (QR/Datei direkt aus `wg0.conf`-Daten + clientseitiger Keys) +* Direct-Edit von `/opt/wg-easy/data/wg0.json` nur für serverseitige Exceptions (hier **nicht** erforderlich) + +### 9. Penetrationstest & Ingress-Verifikation über Mobilfunk ⏳ + +Deaktiviere WLAN auf beiden Testgeräten (reiner 4G/5G-Pfad), VPN aktivieren: + +* **Android (LLM Admin):** `ping 10.0.0.90` muss antworten; `ping 10.0.0.2` (Proxmox) muss durch Firewall/DROP-Regeln fehlschlagen +* **Laptop (Full Admin):** `ssh` auf `10.0.0.90` und Proxmox GUI `https://10.0.0.2:8006` müssen funktionieren +* Zusätzlich: WAN-Ingress ohne VPN – `51821` (wg-easy UI) und `22` (SSH) aus dem Internet **nicht** erreichbar + +**Status: offen, gemeinsam mit User durchführen** + +--- + +## 4. Penetrationsprüfungs- & Testmatrix + +| Test-Szenario | Quellnetz | Ziel-IP / Dienst | Erwartetes Ergebnis | Status | +| --- | --- | --- | --- | --- | +| WAN Ingress UDP Scan | Internet (Mobilfunk) | WAN-IP:51820 UDP | WireGuard Handshake kommt zustande | Ausstehend | +| LLM Admin Isolation | Mobilfunk (Android) | `10.0.0.2:8006` (Proxmox) | **DROP / TIMEOUT** | Ausstehend | +| LLM Admin SSH Access | Mobilfunk (Android) | `10.0.0.90:22` (LLM Server) | **SUCCESS** | Ausstehend | +| Full Admin Access | Mobilfunk (Laptop) | `10.0.0.2:8006` (Proxmox) | **SUCCESS** | Ausstehend | +| Full Admin SSH Access | Mobilfunk (Laptop) | `10.0.0.90:22` (`ghostnet.ki`) | **SUCCESS** | Ausstehend | +| UI Ingress-Isolation | Internet (Mobilfunk) | WAN-IP:51821 (wg-easy UI) | **DROP / TIMEOUT** | Ausstehend | +| SSH Ingress-Isolation | Internet (Mobilfunk) | WAN-IP:22 (LXC) | **DROP / TIMEOUT** | Ausstehend | + +--- + +## 5. Umsetzungszustand (Stand: 2026-09-09) + +| # | Schritt | Status | +| --- | --- | --- | +| 1 | LXC + TUN-Device | ✅ | +| 2 | nftables + ip_forward | ✅ | +| 3 | SSH-Härtung / admin-User | ✅ | +| 4 | wg-easy Quadlet + wg0 | ✅ | +| 5 | Fritzbox Portforward | ⏳ User | +| 6 | NPM-Subdomain `vpn.ghostnet.selfhost.eu` | ⏳ User | +| 7 | Peers Laptop-FullAdmin / Android-LLMAdmin | ✅ (Client-Config-Verifikation offen) | +| 8 | Split-Tunnel-Routing verifizieren | ⏳ | +| 9 | Penetrationstest Mobilfunk | ⏳ | + +Details & Entscheidungen: siehe `GNAI-006-SEC_VPN_Implementation-Log.md`. + +--- + +## Changelog + +| Version | Datum | Änderung | +| --- | --- | --- | +| 1.0 | – | Erster Draft | +| 1.1 | 2026-09-09 | Umsetzung dokumentiert: Hostname `wireguard` (nicht `wireguard-gateway`); Testmatrix auf „Ausstehend“ (keine Tests über Mobilfunk gelaufen); `PASSWORD_HASH` = bcrypt (wg-easy/bcryptjs), nicht Argon2; Quadlet mit `EnvironmentFile=`; nftables-Konfig & ip_forward dokumentiert; wg-easy-allowedIPs-Limitation vermerkt; Umsetzungszustand-Tabellen & Implementation-Log ergänzt | diff --git a/docs/GNAI-006-SEC_VPN_Implementation-Log.md b/docs/GNAI-006-SEC_VPN_Implementation-Log.md new file mode 100644 index 0000000..8818cbd --- /dev/null +++ b/docs/GNAI-006-SEC_VPN_Implementation-Log.md @@ -0,0 +1,145 @@ +# GNAI-006-SEC – Implementation-Log + +**Bezug:** GNAI-006-SEC_VPN.md +**Session:** 2026-09-09 (Agent-Session, Arbeit per SSH auf LXC 105) +**Zweck:** Nachvollziehbare Dokumentation der Umsetzungsschritte, Abweichungen vom Draft, technischen Befunde und Fehlerbehebungen. + +--- + +## 1. erledigte Schritte + +### 1.1 SSH-Zugriff & Admin-User (GNAI-004-SEC) + +* `admin` (uid 1000) mit SSH-Key-Zugang; `sudoers`: `admin ALL=(ALL) NOPASSWD: ALL` +* sshd-Härtung-Drop-in `/etc/ssh/sshd_config.d/99-security.conf` (PermitRootLogin no, PasswordAuthentication no, KbdInteractiveAuthentication no, PubkeyAuthentication yes, PermitEmptyPasswords no) +* Verifiziert: Key-Login, `sudo` passwortlos +* `/root/.ssh/authorized_keys` existierte nie → kein root-Key-Rückbau nötig +* Lokal: `~/.ssh/config`-Alias `lxc` → `10.0.0.15`, User `admin`, dedizierter Key `~/.ssh/id_ed25519_lxc`, `StrictHostKeyChecking accept-new` + +### 1.2 nftables & IP-Forwarding + +* `/etc/nftables.conf` geschrieben (via `sudo tee`) und geladen (`sudo /usr/sbin/nft -f /etc/nftables.conf`) + * INPUT: policy **drop**, lo accept, invalid drop, established/related accept, ICMP-Erreichbarkeit, **UDP 51820 öffentlich**, `tcp 22/51821` nur aus `10.0.0.0/24`, Rate-limited Drop-Log `nft-input-dropped:` + * FORWARD: policy drop, `iif wg0 → 10.0.0.0/24` new accept (Split-Tunnel), Reverse-Route-Regel, drop + * OUTPUT: policy accept +* `net.ipv4.ip_forward=1` aktiv, persistent via `/etc/sysctl.d/99-ipforward.conf` +* Verifiziert: SSH nach nftables-Load weiterhin erreichbar; Regeln per `nft list ruleset` kontrolliert + +### 1.3 wg-easy Quadlet + +* `podman` 5.4.2 installiert; TUN-Device `c 10:200` vorhanden (vorhanden seit Podman-Setup, `lxc.mount.entry` in 105.conf bereits gesetzt) +* `/opt/wg-easy/data` (Volume-Target) + `/opt/wg-easy/environment` (chmod 600) angelegt +* `/etc/containers/systemd/wg-easy.container` → generierte Unit: **`wg-easy.service`** (Podman streicht die `.container`-Endung beim Unit-Namen) +* `systemctl daemon-reload && systemctl restart wg-easy.service` +* Ergebnis: `wg0` up, `10.8.0.1/24`, `ListenPort 51820`; UI `http://10.0.0.15:51821` → HTTP 200 (localhost & LAN-IP) +* ENV im Container verifiziert (`podman inspect`): `WG_HOST=vpn.ghostnet.selfhost.eu`, `WG_PORT=51820`, `WG_DEFAULT_ADDRESS=10.8.0.x`, `WG_DEFAULT_DNS=10.0.0.1`, bcrypt-`PASSWORD_HASH` vorhanden + +### 1.4 Peers + +Beide Peers über die wg-easy-API erstellt (Details Sektion 3): + +| Peer | Adresse | Status | +| --- | --- | --- | +| Laptop-FullAdmin | `10.8.0.2` | aktiv, PreSharedKey, in `wg0.conf` | +| Android-LLMAdmin | `10.8.0.3` | aktiv, PreSharedKey, in `wg0.conf` | + +Server-Config `/opt/wg-easy/data/wg0.conf`: Server `10.8.0.1/24`, `ListenPort 51820`, beide Peers mit `PresharedKey`, serverseitig `AllowedIPs = /32` (wg-easy-Hardcode, siehe Sektion 3.3). + +--- + +## 2. Abweichungen vom ursprünglichen Draft + +| # | Draft | Umsetzung | Begründung | +| --- | --- | --- | --- | +| A1 | Hostname `wireguard-gateway` | Hostname `wireguard` | Konvention: kurze, eindeutige Hostnames (entspricht GNAI-003-Bezeichnung „WireGuard LXC“) | +| A2 | `PASSWORD_HASH=DEIN_ARGON2_HASH` | bcrypt-Hash (Rounds 12) | wg-easy verifiziert per `bcryptjs` – Argon2 wird **nicht** unterstützt | +| A3 | Quadlet mit inline `Environment=`-Zeilen | `EnvironmentFile=/opt/wg-easy/environment` | `$`-Zeichen im bcrypt-Hash (`$2a$12$…`) führen beim inline `Environment=` zu escaping-Problem (Hash ging beim ersten Try verloren, siehe Fehler-Log F3). EnvironmentFile ist deterministisch | +| A4 | Testmatrix „Validiert“ | Testmatrix „Ausstehend“ | Penetrationstests über Mobilfunk wurden in dieser Session noch **nicht** durchgeführt (warten auf Step 5/6: Fritzbox-Forward + DynDNS-Subdomain) | +| A5 | nftables-Schritt fehlte im Draft | Neue Step 2 (nftables + ip_forward) | Strikte INPUT policy `drop` ist Doktrinen-Anforderung (GNAI-004-SEC); wg-easy erzeugt eigene (breitere) Regeln, die nicht ausreichen | +| A6 | Peers „über Web-UI manuell“ | Peers via REST-API | UI-Flow in Session nicht durchlaufen; API liefert deterministisches Ergebnis (Endpoint-Dokumentation siehe Sektion 3.1) | +| A7 | `PostUp`-Regeln wg-easy nicht erwähnt | Dokumentiert: redundant, kein Konflikt | wg-easy-Default-`PostUp` setzt iptables-MASQUERADE/FORWARD/INPUT-ACCEPT-Regeln. Mit der nftables-Config redundant; iptables-FORWARD-ACCEPT ist breiter, die maßgeblichen nftables-Forward-Rules bleiben bestehen. Optional: `WG_POST_UP`/`WG_PRE_DOWN` über Env auf Leerzeichen setzen – **Entscheidung offen** | + +--- + +## 3. Technische Befunde wg-easy + +### 3.1 API-Referenz (aus `ghcr.io/wg-easy/wg-easy:latest`, verifiziert in Session) + +| Aktion | Endpoint | Body | Hinweis | +| --- | --- | --- | --- | +| Login | `POST /api/session` | `{ "password": "…" }` | Setzt `connect.sid`-Cookie; `POST /` gibt 405 zurück | +| Client anlegen | `POST /api/wireguard/client` | `{ "name": "…" }` | **Nur `name` wird akzeptiert**; Address wird aus `WG_DEFAULT_ADDRESS` (`10.8.0.x`) sequenziell zugewiesen (Server `.1`, erste Clients `.2`, `.3`, …) | +| Address ändern | `PUT /api/wireguard/client/:clientId/address` | `{ "address": "10.8.0.x" }` | `clientId` = UUID (aus `GET /api/wireguard/client`) | +| Name ändern | `PUT /api/wireguard/client/:clientId/name` | `{ "name": "…" }` | | +| Enable / Disable | `POST /api/wireguard/client/:clientId/enable` / `disable` | – | | +| Client-Config (download) | `GET /api/wireguard/client/:clientId/configuration` | – | **`clientId` = UUID, nicht Name** (Name → 404) | +| Config neu generieren | `POST /api/wireguard` (`ALLOW_CONFIG_CHANGE=true`) | – | Schreibt `wg0.conf` + wendet `wg` an | + +Client-UUIDs (LXC): `Laptop-FullAdmin` = `2e9ed910-36dc-4fbd-8694-8d904670ea19`, `Android-LLMAdmin` = `2009acd2-19cf-4e5e-a93a-7b05eb05fc12`. + +### 3.2 Passwort-Handling + +* `PASSWORD_HASH` wird mit **bcryptjs** (Salt-Rounds 12, bcrypt-Format `$2a$12$…`) verifiziert. +* UI-Login: `POST /api/session` mit Klartext-Passwort (im internen Netz, LAN-only freigeschaltet). + +### 3.3 allowedIPs-Limitation (wichtigster Befund) + +* **Serverseitig:** `__saveConfig()` in `/app/lib/WireGuard.js` (Container) generiert pro Peer `AllowedIPs = ${client.address}/32` – hartkodiert. Das Feld `allowedIPs` im internen `wg0.json` existiert, wird vom Create-Endpoint aber **nie** befüllt (stets `None`). +* **Konsequenz:** Es gibt keinen API-Path, um serverseitige AllowedIPs freier zu konfigurieren (z. B. ein Peer aus mehreren VPN-IPs). +* **Architektonische Bewertung:** Für unser Modell ist das serverseitige `/32`-Verhalten **korrekt und gewünscht**: Ein Peer darf nur Traffic aus seiner eigenen VPN-IP senden. Das eigentliche Routing (Split-Tunneling) ist **clientseitig** bestimmt – die AllowedIPs im installierten Client-Profil entscheiden, welche Zielnetzwerke über den Tunnel laufen. +* **Offene Frage / nächste Verifikation:** ob die via wg-easy heruntergeladenen Client-Profile die gewünschten clientseitigen AllowedIPs enthalten: + * FullAdmin: `10.0.0.0/24, 10.8.0.0/24` + * LLMAdmin: `10.0.0.90/32` (ggf. `10.8.0.0/24` zusätzlich, damit Peer-seitiges Routing sauber bleibt) +* **Falls nicht:** Optionen (in dieser Reihenfolge): + 1. wg-easy-Einstellungen/Version prüfen – neuere Releases bieten Peer-spezifische `allowedIPs`-Overrides bzw. Template-Erweiterung (z. B. `WG_ALLOWED_IPS` / `allowedIPs` im JSON + Config-Regeneration) + 2. Client-Profile manuell aus den generierten `wg0.conf`-Daten + den clientseitigen Keys basteln (einmalige Aktion, dokumentierbar) + 3. Direct-Edit `/opt/wg-easy/data/wg0.json` + `wg-quick`-Reload – **nicht** für den hier relevanten Fall (serverseitig ist `/32` korrekt) + +--- + +## 4. Fehler-Log & Behebungen + +| # | Fehler | Ursache | Behebung | +| --- | --- | --- | --- | +| F1 | `bash: /etc/nftables.conf: Permission denied`, `nft: command not found` | admin-User ohne root; `nft` nicht im non-login-Shell-PATH | `sudo tee /etc/nftables.conf`, `sudo /usr/sbin/nft …` (volle Pfade) | +| F2 | `Failed to start wg-easy.container.service: Unit not found` | Quadlet generiert Unit **`wg-easy.service`** (ohne `.container`) | `sudo systemctl start wg-easy.service` | +| F3 | `PASSWORD_HASH` fehlte nach Container-Start in `podman inspect` | `Environment=PASSWORD_HASH=\$2a\$12\$…` – Inline-Escaping in Quadlet schlug fehl, Hash ging verloren | `/opt/wg-easy/environment` (chmod 600) mit echten `$`-Zeichen + `EnvironmentFile=`; nach Restart verifiziert: Hash vorhanden | +| F4 | `POST /` → `405 Method Not Allowed` beim UI-Login | Falscher Endpoint | `POST /api/session` mit `{"password": "…"}` → 200 + `Set-Cookie: connect.sid` | +| F5 | `GET /api/wireguard/client/Laptop-FullAdmin/configuration` → 404 | Endpoint erwartet **UUID**, nicht Client-Namen | `GET /api/wireguard/client/2e9ed910-…/configuration` | +| F6 | `allowedIPs` aus Create-Request nicht persistiert (wg0.json: `None`) | Create-Endpoint extrahiert ausschließlich `{name}`; kein Endpoint für allowedIPs; serverseitig hardcodiert `address/32` | Sektion 3.3 – Bewertung: serverseitig korrekt; clientseitige Routing-Verifikation offen (Step 8 im SOP) | +| F7 | `sudo wg: command not found` (ältere Session) | `wg`-Tool nicht installiert / PATH | Status via `cat /proc/net/dev` + wg0-Config geprüft; wg-easy verwaltet das Interface selbst | +| F8 | `sysctl` nicht verfügbar (ältere Session) | Paket `sysctl-utils`/PATH | `cat /proc/sys/net/ipv4/ip_forward`; persistenz via `/etc/sysctl.d/99-ipforward.conf` | + +--- + +## 5. Offene Punkte & nächste Schritte + +| # | Punkt | Typ | Blockiert durch | +| --- | --- | --- | --- | +| O1 | Fritzbox 7530: Portforward **UDP 51820 → 10.0.0.15** | 👤 User | – | +| O2 | NPM (10.0.0.10): Subdomain `vpn.ghostnet.selfhost.eu` anlegen | 👤 User | – | +| O3 | Client-Profile herunterladen (Laptop + Android), AllowedIPs/`Endpoint`/`PersistentKeepalive=25` prüfen | Agent + User | O1, O2 | +| O4 | Split-Tunnel-Routing end-zu-end verifizieren (Step 8 SOP) | Agent + User | O3 | +| O5 | Penetrationstest Mobilfunk (Testmatrix Sektion 4 SOP) | gemeinsam | O1, O2, O3 | +| O6 | Entscheidung: wg-easy-`PostUp`-iptables-Regeln via `WG_POST_UP`/`WG_PRE_DOWN` deaktivieren (hygiene) oder belassen (redundant, kein Konflikt) | Agent | O5 (nach Tests) | +| O7 | Projekt-Kontext in `MEMORY.md` / Memory-Store persistieren | Agent | – | + +**Abhängigkeits-Kette:** O1+O2 → O3 → O4 → O5 → (O6) + +--- + +## 6. Artefakt-Index (LXC 105, `/etc/…` & `/opt/…`) + +| Pfad | Zweck | Status | +| --- | --- | --- | +| `/etc/nftables.conf` | Perimeter-Firewall (strict INPUT drop) | aktiv | +| `/etc/sysctl.d/99-ipforward.conf` | `net.ipv4.ip_forward=1` persist | aktiv | +| `/etc/containers/systemd/wg-easy.container` | Quadlet → `wg-easy.service` | aktiv | +| `/opt/wg-easy/environment` | wg-easy ENV (inkl. bcrypt-`PASSWORD_HASH`, chmod 600) | aktiv | +| `/opt/wg-easy/data/wg0.conf` | WireGuard-Server-Config (wg-easy-generiert) | aktiv, wg0 up | +| `/opt/wg-easy/data/wg0.json` | wg-easy interner State (Clients, UUIDs) | aktiv | +| `/etc/ssh/sshd_config.d/99-security.conf` | SSH-Härtung (GNAI-004-SEC) | aktiv | +| `/etc/sudoers.d/admin` | `admin` NOPASSWD sudo | aktiv | +| Lokal: `~/.ssh/config` | Alias `lxc` (User admin, dedizierter Key) | gesetzt | + +> Secrets (wg-easy-UI-Passwort/Hash, PreSharedKeys, PrivateKeys) sind **nicht** in diesem Repo dokumentiert und liegen ausschließlich auf dem LXC bzw. in den clientseitigen Profilen.