From c7b0a494cd084f716149b1ffef0495916fe7b6d9 Mon Sep 17 00:00:00 2001 From: arnol Date: Wed, 22 Jul 2026 21:18:42 +0200 Subject: [PATCH] Move 06-vps-strato.md to wiki --- 06-vps-strato.md | 312 ----------------------------------------------- 1 file changed, 312 deletions(-) delete mode 100644 06-vps-strato.md diff --git a/06-vps-strato.md b/06-vps-strato.md deleted file mode 100644 index c6d68db..0000000 --- a/06-vps-strato.md +++ /dev/null @@ -1,312 +0,0 @@ -# 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}}' -```