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
This commit is contained in:
2026-09-19 15:52:33 +02:00
parent a42cacd5b5
commit 5a5f7aca40
2 changed files with 408 additions and 0 deletions
+263
View File
@@ -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 "<passwort>"`) 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 |
+145
View File
@@ -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 = <peer-ip>/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.