diff --git a/06-VPS-STRATO.md b/06-VPS-STRATO.md new file mode 100644 index 0000000..c6d68db --- /dev/null +++ b/06-VPS-STRATO.md @@ -0,0 +1,312 @@ +# VPS (STRATO) — Tailscale-Bridge + Guacamole-Gateway — Rebuild-Runbook + +Rolle: Tailscale-Mitglied + Subnet-Router-Bridge (für den Fall, dass kein Tailscale-Client verfügbar ist), Guacamole als öffentlich erreichbares HTML5-Gateway für den Internetcafé-Fall. + +## 1. OS-Installation (manueller Schritt im STRATO-Panel, nicht CLI) + +- Debian 13 (Konsistenz mit dem Rest der Infrastruktur) +- Public SSH-Key beim Anlegen hinterlegen (Key-only, kein Passwort-Login) — Key vom PVE-Host: `cat /root/.ssh/id_rsa.pub` +- Root-Passwort trotzdem setzen (nur als VNC-Konsolen-Fallback, falls SSH mal nicht geht) + +⚠️ **NICHT TUN:** Plesk oder n8n beim Setup mitinstallieren. Die VPS-Rolle bleibt bewusst minimal (nur Tailscale-Bridge + Guacamole) — Plesk ist für Webhosting mit mehreren Domains gedacht (hier nicht gebraucht, frisst RAM auf einem 2-GB-VPS), n8n ist ein eigenes Projekt-Thema. + +## 2. Grundsetup + +```bash +hostnamectl set-hostname vps +echo '127.0.1.1 vps' >> /etc/hosts + +export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a +apt-get update -qq +apt-get upgrade -y -qq -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" +``` + +## 3. Docker + +```bash +curl -fsSL https://get.docker.com | sh +``` + +## 4. Tailscale (mit Subnet-Route-Akzeptanz — dieser Host braucht sie tatsächlich) + +```bash +curl -fsSL https://tailscale.com/install.sh | sh +tailscale up --authkey= --accept-routes +``` + +Anders als bei CT 102 (siehe `04-ct102-management-desktop.md`) ist `--accept-routes` hier **richtig und nötig** — der VPS sitzt nicht selbst im ``-LAN, braucht die vom PVE-Host advertiste Subnet-Route also tatsächlich, um das Heimnetz zu erreichen (NAS, AdGuard etc.). + +Verifikation: +```bash +ping -c3 # AdGuard +ping -c3 # NAS +getent hosts nas.pve adguard.pve # DNS-Override auf AdGuard sollte greifen +``` + +## 5. Guacamole: Grundgerüst (guacd + Postgres + Webapp) + +```bash +mkdir -p /opt/guacamole/{extensions,guac-home,jdbc-schema,pg-data} + +# Postgres-JDBC-Schema besorgen (nur die Schema-Datei, NICHT die postgresql-Extension-Jar — siehe unten) +curl -fsSL -o /tmp/jdbc.tar.gz https://downloads.apache.org/guacamole/1.6.0/binary/guacamole-auth-jdbc-1.6.0.tar.gz +tar xzf /tmp/jdbc.tar.gz -C /tmp +cp /tmp/guacamole-auth-jdbc-1.6.0/postgresql/schema/001-create-schema.sql /opt/guacamole/jdbc-schema/ +rm -rf /tmp/jdbc.tar.gz /tmp/guacamole-auth-jdbc-1.6.0 + +# TOTP-Extension (wird tatsächlich gebraucht, nicht im Image enthalten) +curl -fsSL -o /tmp/totp.tar.gz https://downloads.apache.org/guacamole/1.6.0/binary/guacamole-auth-totp-1.6.0.tar.gz +tar xzf /tmp/totp.tar.gz -C /tmp +cp /tmp/guacamole-auth-totp-1.6.0/guacamole-auth-totp-1.6.0.jar /opt/guacamole/extensions/ +rm -rf /tmp/totp.tar.gz /tmp/guacamole-auth-totp-1.6.0 +``` + +⚠️ **NICHT TUN:** die `guacamole-auth-jdbc-postgresql-*.jar` manuell in `extensions/` legen. Das offizielle `guacamole/guacamole`-Docker-Image bringt diese Extension bereits eingebaut mit und aktiviert sie automatisch über die `POSTGRESQL_*`-Umgebungsvariablen (siehe Compose-Datei unten). Eine manuell hinzugefügte zweite Kopie führt zu einer Namenskollision (zwei `[postgresql]`-Provider gleichzeitig geladen) und macht den Login kaputt ("Invalid login" trotz korrektem Passwort). + +⚠️ **NICHT TUN:** `002-create-admin-user.sql` mit einspielen. Das legt den Standard-Account `guacadmin`/`guacadmin` an. Stattdessen den eigenen Nutzer direkt per SQL anlegen (Schritt 7) — kein Zeitfenster mit bekanntem Standard-Passwort. + +## 6. Docker Compose + +```bash +DBPASS=$(openssl rand -base64 24 | tr -d '=+/') + +cat > /opt/guacamole/docker-compose.yml <:8080:8080" + networks: + - guac-net + depends_on: + - guacd + - postgres + +networks: + guac-net: +EOF + +cd /opt/guacamole +docker compose up -d postgres # erst Postgres, damit das Schema importiert wird +sleep 10 +docker compose up -d # dann den Rest +``` + +`127.0.0.1:8080` + Tailscale-IP `:8080` gebunden — **nie an `0.0.0.0`**, sonst wäre Guacamole ungeschützt (kein TLS) direkt öffentlich erreichbar. Öffentlicher Zugriff läuft ausschließlich über Caddy (Schritt 9). + +## 7. Nutzer + Verbindungen per SQL anlegen + +Passwort-Hash-Formel (Guacamole-JDBC-spezifisch, **nicht** bcrypt wie bei AdGuard Home): +``` +password_hash = SHA256(UTF8(password + HEX_UPPERCASE(salt))) +``` +Salt wird als **Hex-String an das Passwort angehängt**, nicht als rohe Bytes verknüpft. + +```bash +python3 -c " +import hashlib, secrets +pw = '' +salt = secrets.token_bytes(32) +salt_hex = salt.hex().upper() +h = hashlib.sha256((pw + salt_hex).encode('utf-8')).hexdigest() +print('HASH:' + h) +print('SALT:' + salt_hex) +" +``` + +⚠️ **NICHT TUN:** Passwort und **rohe** Salt-Bytes verketten (`password.encode() + salt_bytes`). Das ist falsch und führt zu "Invalid login" trotz korrektem Passwort — die tatsächliche Formel verkettet den **Hex-String** des Salts (als Text), nicht die Binärdaten. (Nachgeprüft im Guacamole-Quellcode: `SHA256PasswordEncryptionService`.) + +Nutzer + Admin-Recht: +```bash +HASH="" +SALT="" +docker exec -i guac-postgres psql -U guacamole_user -d guacamole_db <', 'USER'); +INSERT INTO guacamole_user (entity_id, password_hash, password_salt, password_date) +SELECT entity_id, decode('${HASH}','hex'), decode('${SALT}','hex'), now() +FROM guacamole_entity WHERE name='' AND type='USER'; +INSERT INTO guacamole_system_permission (entity_id, permission) +SELECT entity_id, 'ADMINISTER' FROM guacamole_entity WHERE name='' AND type='USER'; +COMMIT; +EOSQL +``` + +Verbindungen (Beispiel NAS-SFTP, analog für RDP/SSH): +```sql +INSERT INTO guacamole_connection (connection_name, protocol) VALUES ('NAS Dateien (SSH/SFTP)', 'ssh'); +INSERT INTO guacamole_connection_parameter (connection_id, parameter_name, parameter_value) +SELECT connection_id, 'hostname', '' FROM guacamole_connection WHERE connection_name='NAS Dateien (SSH/SFTP)'; +INSERT INTO guacamole_connection_parameter (connection_id, parameter_name, parameter_value) +SELECT connection_id, 'port', '22' FROM guacamole_connection WHERE connection_name='NAS Dateien (SSH/SFTP)'; +INSERT INTO guacamole_connection_parameter (connection_id, parameter_name, parameter_value) +SELECT connection_id, 'username', '' FROM guacamole_connection WHERE connection_name='NAS Dateien (SSH/SFTP)'; +INSERT INTO guacamole_connection_parameter (connection_id, parameter_name, parameter_value) +SELECT connection_id, 'enable-sftp', 'true' FROM guacamole_connection WHERE connection_name='NAS Dateien (SSH/SFTP)'; +INSERT INTO guacamole_connection_parameter (connection_id, parameter_name, parameter_value) +SELECT connection_id, 'sftp-root-directory', '/srv/freigabe' FROM guacamole_connection WHERE connection_name='NAS Dateien (SSH/SFTP)'; +INSERT INTO guacamole_connection_permission (entity_id, connection_id, permission) +SELECT e.entity_id, c.connection_id, 'READ' +FROM guacamole_entity e, guacamole_connection c +WHERE e.name='' AND e.type='USER' AND c.connection_name='NAS Dateien (SSH/SFTP)'; +``` + +**Passwort bewusst nicht als Parameter gesetzt** — Guacamole fragt es beim Verbinden ab, kein Klartext in der DB. + +⚠️ **NICHT TUN:** Hostnamen wie `nas.pve` oder MagicDNS-Namen (`pc1`, ``) als `hostname`-Parameter in Guacamole-Verbindungen verwenden. Der `guacd`-Container läuft in einem eigenen Docker-Netzwerk und löst diese Namen nicht zuverlässig auf (Docker ersetzt den DNS-Resolver). Stattdessen **immer IP-Adressen** verwenden: `` (NAS), `` (PC1), `` (PVE-Host für den WoL-Trigger). + +Weitere Verbindungen nach demselben Muster: +- **"PC1 RDP"**: `protocol=rdp`, `hostname=`, `domain=pc1` (lokales Windows-Konto, nicht Domain-Konto!), `port=3389`, `username=`, `ignore-cert=true` +- **"Wake PC1"**: `protocol=ssh`, `hostname=`, `port=22`, `username=wolonly`, `private-key=`, `command=/usr/local/bin/wake-pc1.sh` + +⚠️ **NICHT TUN (RDP):** Falls „Authentication failure (invalid credentials?)" trotz korrektem Passwort kommt — meist fehlt der `domain`-Parameter bei einem lokalen Windows-Konto. Mit `whoami` auf dem Windows-Rechner den echten Kontonamen prüfen (`RECHNERNAME\Username`), `domain` entsprechend setzen. Zum Testen, ob ein Windows-Passwort grundsätzlich stimmt (unabhängig von RDP): `runas /user:RECHNERNAME\Username cmd` direkt am Windows-Gerät. + +⚠️ **NICHT TUN (private-key für "Wake PC1"):** Verschlüsselte SSH-Keys im neuen OpenSSH-Format (ed25519, mit Passphrase) funktionieren oft nicht mit Guacamoles `libssh2` ("Unable to extract public key ... Unsupported private key file format"). Falls ein Key mit Passphrase gebraucht wird: RSA im alten PEM-Format erzeugen (`ssh-keygen -m PEM -t rsa -b 4096 ...`), das ist kompatibel. Für "Wake PC1" reicht aber ein **unverschlüsselter** Key (kein Passphrase-Prompt gewünscht, da automatisiert ausgelöst). + +Zusätzliche Nutzer (z.B. Ehefrau) mit eingeschränktem Zugriff, gleiches Muster wie oben, aber nur die gewünschte(n) Verbindung(en) per `guacamole_connection_permission` freigeben, kein `guacamole_system_permission`. + +## 8. TOTP-2FA aktivieren + +TOTP funktioniert **nur** mit Datenbank-Auth (nicht mit dateibasierter `user-mapping.xml`-Auth) — Voraussetzung ist also bereits mit Schritt 5–7 erfüllt. + +```bash +chown -R 1001:1001 /opt/guacamole/guac-home +docker restart guacamole +``` + +⚠️ **NICHT TUN:** `/opt/guacamole/guac-home` root-owned lassen. Der Guacamole-Container läuft intern als UID 1001 (`guacamole`-User) — ohne Schreibrecht auf dieses Verzeichnis schlägt die TOTP-Erstregistrierung **still** fehl (kein Fehler im Log, einfach kein QR-Code, Login funktioniert normal ohne 2FA weiter). + +Danach: beim ersten Login jedes Nutzers erscheint automatisch ein QR-Code zur Ersteinrichtung (kein weiterer Konfigurationsschritt nötig). + +**Backup/Recovery falls Gerät mit Authenticator-App verloren geht:** +- Bevorzugt: TOTP-Secret zusätzlich auf ein zweites, unabhängiges Gerät legen (z.B. Partner/in), bevor es gebraucht wird +- Absoluter Notfall (kein Gerät mit dem Secret mehr verfügbar): direkter DB-Reset erzwingt bei nächstem Login einen neuen QR-Code + ```sql + DELETE FROM guacamole_user_attribute WHERE user_id = AND attribute_name LIKE 'guac-totp%'; + ``` + TOTP-Secret selbst liegt in `guacamole_user_attribute` (Attribute `guac-totp-key-secret`, `guac-totp-key-confirmed`), keine eigene Tabelle. + +⚠️ **NICHT TUN:** Email-basiertes 2FA suchen/erwarten. Guacamole unterstützt offiziell nur **Duo** und **TOTP**, kein E-Mail-OTP. + +## 9. Caddy (öffentlicher Reverse Proxy mit Auto-HTTPS) + +Voraussetzung: Domain bei STRATO konnektiert (dauert ggf., bis DENIC-Registrierung durch ist — separater Schritt im STRATO-Panel), A-Record `guac.` → `` im STRATO-DNS-Panel gesetzt (kein CLI-Schritt). + +```bash +apt-get install -y caddy + +cat > /etc/caddy/Caddyfile <<'EOF' +guac. { + @root path / + redir @root /guacamole/ + reverse_proxy 127.0.0.1:8080 +} +EOF + +systemctl reload caddy +``` + +Caddy holt sich automatisch ein Let's-Encrypt-Zertifikat, sobald der A-Record propagiert ist (mehrere Versuche in Intervallen, kein manuelles Eingreifen nötig — ggf. `systemctl restart caddy` um einen sofortigen neuen Versuch zu erzwingen). + +⚠️ **NICHT TUN:** Guacamole zusätzlich direkt öffentlich auf Port 8080 exponieren (`0.0.0.0:8080` in der Compose-Datei). Nur Caddy (80/443) und SSH (22, Key-only) sind öffentlich — Guacamole selbst bleibt hinter `127.0.0.1` + Tailscale-IP. + +## 10. Log-Hygiene: fail2ban + journald-Deckel + +Ohne das wächst SSH-Scan-Rauschen unbegrenzt und kann den VPS-Speicher füllen (in der Praxis beobachtet: ~4 GB/Woche bei einem vergleichbaren Setup ohne diese Maßnahmen). + +```bash +apt-get install -y fail2ban + +cat > /etc/fail2ban/jail.local <<'EOF' +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 4 +backend = systemd + +[sshd] +enabled = true +port = 22 +EOF +systemctl enable --now fail2ban +``` + +```bash +sed -i 's/^#SystemMaxUse=/SystemMaxUse=200M/' /etc/systemd/journald.conf +systemctl restart systemd-journald +``` + +⚠️ **NICHT TUN:** fail2ban auch auf Containern installieren, die nur über Tailscale erreichbar sind (z.B. CT103/Vaultwarden). Ohne öffentliche Erreichbarkeit gibt es keinen Angriffsverkehr zum Bannen — reine Verschwendung. + +## 11. Docker-Log-Limits + +Docker-Container loggen standardmäßig **unbegrenzt** (`json-file`-Treiber ohne Limit) — betrifft normale Betriebslogs, nicht nur Angriffsverkehr, daher unabhängig von fail2ban relevant. In `docker-compose.yml` per YAML-Anchor für alle Services: + +```yaml +x-logging: &default-logging + driver: json-file + options: + max-size: "10m" + max-file: "3" + +services: + guacd: + # ... + logging: *default-logging + postgres: + # ... + logging: *default-logging + guacamole: + # ... + logging: *default-logging +``` + +Nach Änderung: `docker compose up -d` (Container-Neustart nötig, Limits gelten nur für neu erstellte Container, nicht rückwirkend). + +## Zugriffswege (Zusammenfassung) + +| Weg | Erreichbarkeit | Absicherung | +|---|---|---| +| `https://guac.` | öffentlich, überall | Caddy/Let's-Encrypt-TLS + Guacamole-Login (Passwort + TOTP) + Brute-Force-Ban (5 Versuche → 5 Min Sperre) | +| `http://:8080/guacamole/` | nur Tailnet | wie oben, ohne öffentliche Exposition | + +## Verifikation + +```bash +curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/guacamole/ +curl -s -o /dev/null -w "%{http_code}\n" https://guac./guacamole/ +docker ps --format 'table {{.Names}}\t{{.Status}}' +```