diff --git a/06-VPS-STRATO.md b/06-VPS-STRATO.md index c1f3591..186dceb 100644 --- a/06-VPS-STRATO.md +++ b/06-VPS-STRATO.md @@ -223,7 +223,7 @@ Danach: beim ersten Login jedes Nutzers erscheint automatisch ein QR-Code zur Er ## 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). +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 @@ -296,6 +296,137 @@ services: Nach Änderung: `docker compose up -d` (Container-Neustart nötig, Limits gelten nur für neu erstellte Container, nicht rückwirkend). +## 12. SSH: nur über Tailscale erreichbar + +SSH (Port 22) muss nach Abschluss der Ersteinrichtung nicht mehr öffentlich erreichbar sein — alle künftigen Änderungen laufen ausschließlich von Tailscale-verbundenen Geräten aus. Trotz Key-only-Auth + fail2ban erzeugt ein offener Port dauerhaftes Scan-/Bot-Rauschen ohne Nutzen (Fund eines externen Security-Reviews). + +```bash +cat > /etc/ssh/sshd_config.d/tailscale-only.conf <<'EOF' +ListenAddress +ListenAddress 127.0.0.1 +ListenAddress ::1 +EOF + +mkdir -p /etc/systemd/system/ssh.service.d +cat > /etc/systemd/system/ssh.service.d/after-tailscale.conf <<'EOF' +[Unit] +After=tailscaled.service +Wants=tailscaled.service +EOF + +sshd -t && systemctl daemon-reload && systemctl restart ssh.service +``` + +`ListenAddress` bindet `sshd` nur noch an die Tailscale-IP + Loopback statt an `0.0.0.0`/`[::]:22` — Port 22 verschwindet komplett aus dem öffentlichen Internet, bleibt aber über Tailscale normal erreichbar. + +⚠️ **NICHT TUN: `ListenAddress` auf die Tailscale-IP setzen, ohne `ip_nonlocal_bind` zu aktivieren.** Beim Booten startet `sshd`, **bevor** `tailscaled` die Adresse vergeben hat. Der Bind scheitert: + +``` +sshd[848]: Server listening on 127.0.0.1 port 22. +sshd[848]: error: Bind to port 22 on failed: Cannot assign requested address. +``` + +**Tückisch daran: der Dienst bleibt `active (running)`.** Weil der Bind an Loopback gelingt, meldet systemd keinen Fehlschlag — von außen sieht es aus wie „SSH tot", intern ist alles grün. Ohne den Blick ins Journal sucht man am falschen Ende. + +Ein Drop-In mit `After=tailscaled.service` reicht **nicht**: das garantiert nur, dass der Dienst *gestartet* wurde, nicht dass die IP schon zugewiesen ist. Korrekt ist: + +```bash +echo 'net.ipv4.ip_nonlocal_bind=1' > /etc/sysctl.d/99-nonlocal-bind.conf +sysctl -p /etc/sysctl.d/99-nonlocal-bind.conf +``` + +Damit darf sich ein Prozess an eine Adresse binden, die es noch nicht gibt. Gegenprobe ohne Reboot — an eine nicht vergebene Adresse binden, das muss gelingen: + +```bash +python3 -c "import socket; s=socket.socket(); s.bind(('', 22222)); print('ok')" +``` + +⚠️ **Die STRATO-VNC-Konsole liegt auf en-US.** Ein deutsches Passwort mit `z`, `y` oder Sonderzeichen kommt dort anders an, als es getippt wird — das korrekte Passwort wirkt dann falsch. Beim Tippen die US-Belegung mitdenken. **Genau daran ist auch der Fix zunächst gescheitert:** in der Datei landete ein falscher, ähnlich klingender sysctl-Schlüssel statt `net.ipv4.ip_nonlocal_bind=1`. `sysctl` quittiert einen falschen Schlüssel mit `cannot stat ...` — wer die Meldung übersieht, hat eine Datei, die nach Lösung aussieht und nichts tut. **Nach jedem sysctl-Eingriff den Laufzeitwert prüfen**, nicht die Datei: + +```bash +sysctl -n net.ipv4.ip_nonlocal_bind # muss 1 liefern +``` + +⚠️ **NICHT TUN:** Stattdessen eine eigene Firewall-Regel (iptables/nftables) einziehen, um Port 22 auf öffentlichen Interfaces zu blocken. Das VPS-Regelwerk wird bereits dicht von drei Stellen dynamisch verwaltet — Tailscale selbst (`ts-input`/`ts-forward`-Chains, `nft list ruleset` markiert sie explizit als „managed by iptables-nft, do not touch!"), Docker (eigene `DOCKER-*`-Chains) und fail2ban (`f2b-table`). Eine eigene Regel (z.B. über den bisher deaktivierten `nftables.service`, dessen Default-Config mit `flush ruleset` startet) riskiert, Tailscales/Dockers Regeln beim Boot zu überschreiben, abhängig von der Start-Reihenfolge der systemd-Units. Die `sshd`-Bindung ist der isolierte, leicht zu verifizierende/revertierende Weg. + +⚠️ **NICHT TUN:** Den systemd-Override fürs Boot-Ordering vergessen. `sshd` versucht beim Start sofort an die Tailscale-IP zu binden — existiert das `tailscale0`-Interface zu dem Zeitpunkt noch nicht (z.B. direkt nach einem Reboot, bevor `tailscaled` hochgefahren ist), schlägt der Bind fehl und `sshd` startet gar nicht, auch nicht über Tailscale. `After=`/`Wants=tailscaled.service` in `ssh.service.d/` stellt die richtige Reihenfolge sicher. + +**Verifikation:** `ss -tlnp | grep :22` zeigt nur noch die Tailscale-IP/`127.0.0.1:22`/`[::1]:22`, kein `0.0.0.0:22` mehr; SSH über die Tailscale-IP weiterhin normal getestet. Externe Nicht-Erreichbarkeit lässt sich nicht von innerhalb des Tailnets verifizieren — dafür ein Gerät ohne Tailscale-Verbindung (z.B. Handy mit Mobilfunk, Tailscale aus) oder einen Online-Port-Checker nutzen. + +**Rollback:** beide neuen Dateien löschen, `systemctl daemon-reload && systemctl restart ssh.service` — SSH ist sofort wieder auf allen Interfaces erreichbar. + +### `root`-`authorized_keys`: persönlicher Key jetzt über den Passwortmanager + +Ein eingebautes SSH-Key-Feature des Passwortmanagers (Key-Erzeugung + SSH-Agent, privater +Schlüssel verlässt den Vault nie) ersetzt den bisherigen, lokal auf einem Windows-PC liegenden Key +(`@PC1`) dagegen. Alter Key entfernt (lokal *und* aus `authorized_keys`), neuer Key +zunächst als geteilter Eintrag (`@vaultwarden`) ergänzt — auf **VPS und PVE-Host identisch**. + +⚠️ **Bewusst nur als Zwischenschritt dokumentiert, nicht als Zielzustand:** ein einzelner Key für +mehrere Systeme mit unterschiedlichem Risikoprofil (VPS öffentlich exponiert, PVE-Host nie) hebt die +Kompromiss-Eindämmung teilweise wieder auf, die getrennte Keys bringen würden. Vollständige +Individualisierung (ein dedizierter Key je System) ist der eigentliche Zielzustand — bewusst als +eigene, offene Übung dokumentiert statt vom KI-Agenten automatisiert. + +**Zweiter, nicht eindeutig zugeordneter Altkey auf dem PVE-Host** — Herkunft nicht mehr +rekonstruierbar, bewusst nicht blind entfernt, sondern als offener Punkt dokumentiert. + +Service-Keys bleiben davon unberührt (z. B. ein Backup-Pull-Key mit erzwungenem `restrict,command="rrsync +-ro /root/backups/"` — kein interaktiver Zugriff, eigener Zweck, kein Teil dieser Aufräumaktion). + +## 13. Tailscale Exit-Node + +Bewusst auf dem VPS statt dem PVE-Host eingerichtet — Speedtest zeigt einen deutlich höheren Upload auf dem VPS gegen die vergleichsweise langsame Heimleitung (DS-Lite). Der PVE-Host bleibt reiner Subnet-Router, beide Rollen laufen gleichzeitig. + +```bash +cat > /etc/sysctl.d/99-tailscale.conf <<'EOF' +net.ipv4.ip_forward=1 +net.ipv6.conf.all.forwarding=1 +EOF +sysctl -p /etc/sysctl.d/99-tailscale.conf + +tailscale set --advertise-exit-node +``` + +⚠️ **NICHT TUN:** `tailscale up` mit neuen Flags absetzen, ohne alle bisherigen Flags (insbesondere `--accept-routes`) erneut mitzugeben — `tailscale up` ersetzt die komplette Flag-Menge, nicht nur die neu angegebenen. `tailscale set` ergänzt dagegen inkrementell, ohne Bestehendes zu verwerfen. Nach der Umstellung mit `tailscale debug prefs` prüfen, dass `AdvertiseRoutes` jetzt `0.0.0.0/0`/`::/0` enthält UND das Heimnetz weiterhin erreichbar ist (z.B. `ping `). + +Danach in der Tailscale-Admin-Konsole (Machines → vps → „Edit route settings") den Exit-Node genehmigen — analog zur Subnet-Route-Genehmigung für den PVE-Host. + +**Firewall-Check (Docker + Exit-Node auf derselben Maschine):** `FORWARD`-Chain hat `policy DROP`, `DOCKER-USER`/`DOCKER-FORWARD` laufen vor Tailscales eigener `ts-forward`-Chain. Kein Problem, solange `DOCKER-USER` leer ist und `DOCKER-FORWARD` nur die Docker-Bridge-Interfaces explizit behandelt (alles andere fällt durch zu `ts-forward`, das Tailscale selbst verwaltet) — mit `iptables -L DOCKER-USER -n -v` und `iptables -L DOCKER-FORWARD -n -v` verifizieren, bevor man vorschnell eine eigene FORWARD-Regel ergänzt (siehe auch die „NICHT TUN"-Warnung zu eigenen Firewall-Regeln in Abschnitt 12). + +**Verifikation:** `tailscale debug prefs | grep AdvertiseRoutes` zeigt `0.0.0.0/0`/`::/0`. Auf einem Testgerät in der Tailscale-App den Exit-Node auswählen, dann öffentliche IP prüfen (z.B. `curl icanhazip.com`) — muss die `` zeigen. Parallel dazu Heimnetz-Zugriff (NAS/PVE-Web) testen, um zu bestätigen, dass Subnet-Router und Exit-Node gleichzeitig funktionieren. + +⚠️ **Geräteseitige Auswahl des Exit-Node ist kein Server-Schritt** — jedes Gerät wählt seinen Exit-Node selbst in der jeweiligen Tailscale-App (Einstellungen → Exit-Node), das lässt sich nicht zentral vom VPS oder PVE-Host aus vorkonfigurieren. + +## 14. Projekt-Übersichtsseite + +Statische Einzelseite am Apex, kein Backend, kein Build-Schritt — Inhalt liegt versioniert im Doku-Repo unter `website/index.html`. + +```bash +mkdir -p /var/www/homepage +scp website/index.html root@:/var/www/homepage/index.html +ssh root@ 'chown -R caddy:caddy /var/www/homepage' +``` + +```caddyfile + { + import security_headers + root * /var/www/homepage + encode gzip + file_server +} + +www. { + redir https://{uri} permanent +} +``` + +DNS: A-Record auf die Apex-Domain → VPS-IP, optional CNAME `www` → Apex-Domain (folgt damit automatisch der Apex-IP, kein zweiter A-Record nötig). + +⚠️ **NICHT TUN:** beim Setzen des Apex-A-Records aus Versehen die MX-Records mit anfassen — beide stehen im selben DNS-Panel-Bereich, sind aber komplett unabhängig. Falls ein Mail-Postfach an der Domain hängt, vorher gegenchecken (`dig MX`), dass sich daran nichts ändert. + +**Update-Workflow:** Datei im Repo editieren → `scp` aufs VPS → fertig. Kein Caddy-Reload nötig für reine Inhaltsänderungen (nur bei neuen Vhost-Blöcken). + ## Zugriffswege (Zusammenfassung) | Weg | Erreichbarkeit | Absicherung |