Re-add 06 VPS STRATO
parent
361c5d7a22
commit
be857c83cf
1 changed files with 312 additions and 0 deletions
312
06-VPS-STRATO.md
Normal file
312
06-VPS-STRATO.md
Normal file
|
|
@ -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=<AUTH_KEY> --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_SUBNET>`-LAN, braucht die vom PVE-Host advertiste Subnet-Route also tatsächlich, um das Heimnetz zu erreichen (NAS, AdGuard etc.).
|
||||
|
||||
Verifikation:
|
||||
```bash
|
||||
ping -c3 <LAN_IP_ADGUARD> # AdGuard
|
||||
ping -c3 <LAN_IP_NAS> # 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 <<EOF
|
||||
services:
|
||||
guacd:
|
||||
image: guacamole/guacd:1.6.0
|
||||
container_name: guacd
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- guac-net
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: guac-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: guacamole_db
|
||||
POSTGRES_USER: guacamole_user
|
||||
POSTGRES_PASSWORD: "$DBPASS"
|
||||
volumes:
|
||||
- ./pg-data:/var/lib/postgresql/data
|
||||
- ./jdbc-schema:/docker-entrypoint-initdb.d:ro
|
||||
networks:
|
||||
- guac-net
|
||||
|
||||
guacamole:
|
||||
image: guacamole/guacamole:1.6.0
|
||||
container_name: guacamole
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GUACD_HOSTNAME: guacd
|
||||
POSTGRESQL_HOSTNAME: postgres
|
||||
POSTGRESQL_DATABASE: guacamole_db
|
||||
POSTGRESQL_USER: guacamole_user
|
||||
POSTGRESQL_PASSWORD: "$DBPASS"
|
||||
volumes:
|
||||
- ./extensions:/etc/guacamole/extensions:ro
|
||||
- ./guac-home:/etc/guacamole/data
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
- "<TS_IP_VPS>: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 = '<Passwort>'
|
||||
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="<oben berechnet>"
|
||||
SALT="<oben berechnet>"
|
||||
docker exec -i guac-postgres psql -U guacamole_user -d guacamole_db <<EOSQL
|
||||
BEGIN;
|
||||
INSERT INTO guacamole_entity (name, type) VALUES ('<PRIMARY_USER>', '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='<PRIMARY_USER>' AND type='USER';
|
||||
INSERT INTO guacamole_system_permission (entity_id, permission)
|
||||
SELECT entity_id, 'ADMINISTER' FROM guacamole_entity WHERE name='<PRIMARY_USER>' 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', '<LAN_IP_NAS>' 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', '<PRIMARY_USER>' 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='<PRIMARY_USER>' 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`, `<PVE_HOSTNAME>`) 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: `<LAN_IP_NAS>` (NAS), `<LAN_IP_PC1>` (PC1), `<LAN_IP_PVE>` (PVE-Host für den WoL-Trigger).
|
||||
|
||||
Weitere Verbindungen nach demselben Muster:
|
||||
- **"PC1 RDP"**: `protocol=rdp`, `hostname=<LAN_IP_PC1>`, `domain=pc1` (lokales Windows-Konto, nicht Domain-Konto!), `port=3389`, `username=<PRIMARY_USER>`, `ignore-cert=true`
|
||||
- **"Wake PC1"**: `protocol=ssh`, `hostname=<LAN_IP_PVE>`, `port=22`, `username=wolonly`, `private-key=<Inhalt von /root/.ssh/wol_vps_key vom PVE-Host>`, `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 = <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.<DOMAIN>` → `<VPS-öffentliche-IP>` im STRATO-DNS-Panel gesetzt (kein CLI-Schritt).
|
||||
|
||||
```bash
|
||||
apt-get install -y caddy
|
||||
|
||||
cat > /etc/caddy/Caddyfile <<'EOF'
|
||||
guac.<DOMAIN> {
|
||||
@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.<DOMAIN>` | öffentlich, überall | Caddy/Let's-Encrypt-TLS + Guacamole-Login (Passwort + TOTP) + Brute-Force-Ban (5 Versuche → 5 Min Sperre) |
|
||||
| `http://<TS_IP_VPS>: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.<DOMAIN>/guacamole/
|
||||
docker ps --format 'table {{.Names}}\t{{.Status}}'
|
||||
```
|
||||
Loading…
Reference in a new issue