Initial import: genericized homelab rebuild runbooks
This commit is contained in:
commit
eb9bbe6455
13 changed files with 1784 additions and 0 deletions
36
00-uebersicht.md
Normal file
36
00-uebersicht.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Rebuild-Dokumentation – Übersicht
|
||||
|
||||
Diese Dateien enthalten alle tatsächlich verwendeten CLI-Befehle, in Reihenfolge, um jede Instanz von Grund auf neu aufzusetzen — ohne Rückfrage. Fehlgeschlagene Zwischenschritte sind **nicht** enthalten, nur das jeweils korrekte Endergebnis plus eine explizite Warnung, was nicht funktioniert (siehe „⚠️ NICHT TUN"-Blöcke).
|
||||
|
||||
Für den Kontext/die Begründung hinter Entscheidungen: `ENTSCHEIDUNGEN.md` (hier rein technisch als Runbook, dort die Trade-offs/Alternativen). Zum Hintergrund dieser Freigabe: `ACHTUNG-DISCLAIMER.md`.
|
||||
|
||||
## Instanzen
|
||||
|
||||
| Datei | Host | IP (LAN) | IP (Tailscale) | Rolle |
|
||||
|---|---|---|---|---|
|
||||
| [01-pve-host.md](01-pve-host.md) | `<PVE_HOSTNAME>` | `<LAN_IP_PVE>` | `<TS_IP_PVE>` | Proxmox-Host, Subnet-Router, ZFS, WoL-Trigger |
|
||||
| [02-ct100-adguard.md](02-ct100-adguard.md) | `adguard` (CT 100) | `<LAN_IP_ADGUARD>` | — | DNS/Ad-Blocking |
|
||||
| [03-ct101-nas.md](03-ct101-nas.md) | `nas` (CT 101) | `<LAN_IP_NAS>` | — | Samba/SFTP/Filebrowser |
|
||||
| [04-ct102-management-desktop.md](04-ct102-management-desktop.md) | `browser` (CT 102) | `<LAN_IP_DESKTOP>` | `<TS_IP_DESKTOP>` | Gemeinsamer Browser-Desktop |
|
||||
| [05-ct103-vaultwarden.md](05-ct103-vaultwarden.md) | `vaultwarden` (CT 103) | `<LAN_IP_VAULTWARDEN>` | `<TS_IP_VAULTWARDEN>` | Passwortmanager |
|
||||
| [06-vps-strato.md](06-vps-strato.md) | `vps` (STRATO) | — | `<TS_IP_VPS>` | Guacamole-Gateway, öffentlicher Fallback-Zugang |
|
||||
| [08-monitoring.md](08-monitoring.md) | `monitoring` (CT 104) | `<LAN_IP_MONITORING>` | `<TS_IP_MONITORING>` | Grafana/InfluxDB/Uptime Kuma/ntfy |
|
||||
|
||||
Cross-cutting, kein eigener Host:
|
||||
- [07-backup-restore.md](07-backup-restore.md) — nächtliches Backup des Laufzeit-Zustands aller Instanzen (LXCs, Guacamole-DB) plus Restore-Anleitung.
|
||||
- [09-gast-zugang.md](09-gast-zugang.md) — read-only Showcase-Zugang (genau die Oberfläche, über die diese Dateien hier gerade ausgeliefert werden) für Freunde/Bekannte und Bewerbungsgespräche.
|
||||
|
||||
## Reihenfolge beim Neuaufbau
|
||||
|
||||
1. PVE-Host (Proxmox-Grundinstallation wird vorausgesetzt, danach Tailscale + ZFS + WoL)
|
||||
2. CT 100 (AdGuard) — muss vor DHCP-Umstellung stehen, da alle anderen Container/das LAN danach über AdGuard auflösen
|
||||
3. CT 101 (NAS) — ZFS-Pool muss vorher existieren
|
||||
4. VPS (STRATO) — unabhängig von den LXCs, kann parallel/vorher passieren
|
||||
5. CT 102 (Management-Desktop) und CT 103 (Vaultwarden) — brauchen Tailscale (Punkt 1) und idealerweise Guacamole (VPS) für vollen Funktionsumfang, technisch aber auch eigenständig baubar
|
||||
|
||||
## Globale Konventionen, die für alle Instanzen gelten
|
||||
|
||||
* **Nameserver bei jedem `pct create` explizit setzen:** `--nameserver <LAN_IP_ADGUARD>`. Grund: siehe `01-pve-host.md`, Abschnitt „DNS-Falle bei neuen Containern".
|
||||
* **IP-Schema:** `.1` Fritzbox, `.9` AdGuard, `.10` PVE-Host, `.11` NAS, `.12` Management-Desktop, `.13` Vaultwarden, `.14` Monitoring, `.254` PC1 (Windows). `.1`–`.20` sind im Fritzbox-DHCP-Pool ausgeschlossen, für feste Zuweisungen reserviert.
|
||||
* **Alle LXCs unprivileged**, außer es gibt einen zwingenden Grund (Kernel-Module/Storage-Passthrough) für privileged/VM.
|
||||
* **Passwörter/Tokens/private Keys stehen NICHT in diesen Dateien** — die liegen im Chat-Verlauf bzw. sind vom Nutzer selbst zu setzen (Vaultwarden-Master-Passwort) oder in Vaultwarden zu hinterlegen, sobald der Umzug dorthin gemacht ist.
|
||||
176
01-pve-host.md
Normal file
176
01-pve-host.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# PVE-Host (`<PVE_HOSTNAME>`) — Rebuild-Runbook
|
||||
|
||||
Basis: Proxmox VE, frisch installiert (Fujitsu Esprimo, i3, 32GB RAM). Alles ab hier ist tatsächlich ausgeführt.
|
||||
|
||||
## 1. Grundpakete
|
||||
|
||||
```bash
|
||||
apt-get install -y git tree
|
||||
```
|
||||
|
||||
## 2. Tailscale (Subnet-Router)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://tailscale.com/install.sh | sh
|
||||
|
||||
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 up --authkey=<AUTH_KEY> --advertise-routes=<LAN_SUBNET> --accept-risk=lose-ssh
|
||||
```
|
||||
|
||||
**Danach zwingend in der Tailscale-Admin-Konsole (login.tailscale.com/admin/machines):**
|
||||
- Bei `<PVE_HOSTNAME>` → „Edit route settings" → Route `<LAN_SUBNET>` genehmigen (Subnet-Routes werden nie automatisch aktiviert)
|
||||
- Unter Settings → DNS → Nameserver `<LAN_IP_ADGUARD>` (AdGuard) hinzufügen + „Override local DNS" aktivieren → macht Ad-Blocking im ganzen Tailnet aktiv, auch mobil
|
||||
- Unter Settings → DNS → „HTTPS Certificates" aktivieren (wird für `tailscale cert`/`tailscale serve` auf allen Tailnet-Mitgliedern gebraucht, z.B. Vaultwarden)
|
||||
- Auth-Key erzeugen unter Settings → Keys → „Generate auth key" (reusable, ~90 Tage), wird für alle weiteren Instanzen gebraucht
|
||||
|
||||
⚠️ **NICHT TUN:** `--advertise-exit-node` setzen. Bewusste Entscheidung: kein Exit-Node, sonst würde jeglicher mobiler Internet-Traffic über die Heimleitung (DS-Lite) zurückgeroutet — nur die Subnet-Route wird gebraucht.
|
||||
|
||||
## 3. NAS: ZFS-Mirror
|
||||
|
||||
Vorher: Laufwerke identifizieren, SMART-Check, Datenprüfung auf vorhandenen Inhalt (**vor** dem Löschen — hier nur der Soll-Zustand, nicht das chronologische Nachschauen-was-noch-drauf-war im Detail, das nicht Teil dieser Freigabe ist).
|
||||
|
||||
```bash
|
||||
# Stabile Device-Namen ermitteln
|
||||
ls -la /dev/disk/by-id/ | grep -E "ata-SAMSUNG|ata-ST1000"
|
||||
|
||||
# Beide Platten komplett löschen (IRREVERSIBEL — vorher SMART + Dateninhalt prüfen!)
|
||||
wipefs -a /dev/sda /dev/sdb
|
||||
sgdisk --zap-all /dev/sda
|
||||
sgdisk --zap-all /dev/sdb
|
||||
|
||||
# Mirror anlegen
|
||||
zpool create -o ashift=12 nas mirror \
|
||||
/dev/disk/by-id/ata-SAMSUNG_HD103SJ_S246J9AZ807325 \
|
||||
/dev/disk/by-id/ata-ST1000DM003-1CH162_Z1D7GZ5Q
|
||||
|
||||
zfs create -o compression=lz4 -o atime=off nas/freigabe
|
||||
```
|
||||
|
||||
## 4. HDD-Spindown (Stromsparen)
|
||||
|
||||
```bash
|
||||
hdparm -S 241 /dev/disk/by-id/ata-SAMSUNG_HD103SJ_S246J9AZ807325 /dev/disk/by-id/ata-ST1000DM003-1CH162_Z1D7GZ5Q
|
||||
|
||||
cat >> /etc/hdparm.conf <<'EOF'
|
||||
|
||||
# NAS-HDDs: Standby nach 30 Minuten Inaktivität (Stromsparen, ZFS-Mirror "nas")
|
||||
/dev/disk/by-id/ata-SAMSUNG_HD103SJ_S246J9AZ807325 {
|
||||
spindown_time = 241
|
||||
}
|
||||
/dev/disk/by-id/ata-ST1000DM003-1CH162_Z1D7GZ5Q {
|
||||
spindown_time = 241
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
`smartd` (Debian-Default-Config, `-n standby`) weckt schlafende Platten nicht — nichts weiter zu tun. Der ZFS-Pool ist bewusst **nicht** als PVE-Storage registriert, damit `pvestatd` ihn nicht permanent pollt.
|
||||
|
||||
## 5. Git-Repo (dieses Repo)
|
||||
|
||||
```bash
|
||||
mkdir -p /root/projects/homelab-config
|
||||
# Eigenen Projekt-Kontext + chronologisches Log anlegen (Format/Inspiration:
|
||||
# ENTSCHEIDUNGEN.md und der Aufbau dieses Portfolios selbst)
|
||||
cd /root/projects/homelab-config
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial project setup"
|
||||
```
|
||||
|
||||
Später auf die NAS-Freigabe verlagert (nachdem CT 101 steht):
|
||||
|
||||
```bash
|
||||
mv /root/projects/homelab-config /nas/freigabe/homelab-config
|
||||
chown -R 101000:101000 /nas/freigabe/homelab-config
|
||||
git config --global --add safe.directory /nas/freigabe/homelab-config
|
||||
```
|
||||
|
||||
⚠️ **Warum `chown 101000:101000` und `safe.directory`:** Der Ordner liegt physisch im ZFS-Dataset, das per Bind-Mount in CT 101 (unprivileged, UID-Offset 100000) eingebunden ist. Ohne `safe.directory`-Eintrag verweigert Git auf dem PVE-Host die Arbeit ("dubious ownership"), weil der Owner (101000) nicht root ist.
|
||||
|
||||
## 6. Wake-on-LAN-Trigger (für PC1, ausgelöst von Guacamole/VPS)
|
||||
|
||||
```bash
|
||||
apt-get install -y wakeonlan
|
||||
useradd -m -s /bin/bash wolonly
|
||||
mkdir -p /home/wolonly/.ssh
|
||||
ssh-keygen -t ed25519 -f /root/.ssh/wol_vps_key -N "" -C "vps-wol-trigger"
|
||||
|
||||
cat > /usr/local/bin/wake-pc1.sh <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
wakeonlan -i <LAN_BROADCAST> <PC1_MAC_ADDRESS>
|
||||
echo "Magic packet an <PC1_MAC_ADDRESS> gesendet."
|
||||
EOF
|
||||
chmod +x /usr/local/bin/wake-pc1.sh
|
||||
|
||||
KEY=$(cat /root/.ssh/wol_vps_key.pub)
|
||||
echo "no-port-forwarding,no-x11-forwarding,no-agent-forwarding,command=\"/usr/local/bin/wake-pc1.sh\" $KEY" > /home/wolonly/.ssh/authorized_keys
|
||||
chown -R wolonly:wolonly /home/wolonly/.ssh
|
||||
chmod 700 /home/wolonly/.ssh
|
||||
chmod 600 /home/wolonly/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
Der private Key (`/root/.ssh/wol_vps_key`) wird in die Guacamole-Verbindung "Wake PC1" eingebettet (siehe `06-vps-strato.md`).
|
||||
|
||||
⚠️ **NICHT TUN:** `restrict,command="..."` als Kurzform in `authorized_keys` verwenden. `restrict` schließt `no-pty` mit ein — Guacamoles SSH-Client fordert aber immer ein Pseudo-Terminal an, auch für einen reinen Einzelbefehl. Mit `no-pty` schlägt die Verbindung mit „Unable to allocate PTY" fehl. Stattdessen die Einzel-Restriktionen explizit auflisten (siehe oben), `no-pty` weglassen.
|
||||
|
||||
⚠️ **NICHT TUN:** MAC-Adresse mit Bindestrichen (`D8-BB-C1-90-66-3F`, Windows-`ipconfig`-Format) an `wakeonlan` übergeben. Erwartet Doppelpunkte (`<PC1_MAC_ADDRESS>`), sonst Fehler "not a hardware address".
|
||||
|
||||
## 7. DNS-Falle bei neuen Containern (wichtig für alle folgenden `pct create`-Schritte)
|
||||
|
||||
Seit der PVE-Host selbst Tailscale-Mitglied ist, zeigt sein `/etc/resolv.conf` auf `100.100.100.100` (Tailscales interner Stub-Resolver). Neue Container erben das automatisch (kein `--nameserver` bei `pct create` angegeben) — sind selbst aber noch keine Tailscale-Mitglieder und können diese Adresse nicht erreichen. Resultat: `apt-get update` hängt endlos, ohne Fehlermeldung.
|
||||
|
||||
**Immer bei `pct create` mitgeben:**
|
||||
```bash
|
||||
--nameserver <LAN_IP_ADGUARD>
|
||||
```
|
||||
|
||||
**Für bereits existierende Container dauerhaft fixen** (übersteht Neustarts, im Gegensatz zum manuellen Editieren von `/etc/resolv.conf`, das bei jedem Containerstart überschrieben wird):
|
||||
```bash
|
||||
pct set <ID> --nameserver <LAN_IP_ADGUARD>
|
||||
```
|
||||
Angewendet auf CT 100, 101, 102, 103.
|
||||
|
||||
## 8. TUN-Device für Tailscale-in-LXC (für CT 102, CT 103)
|
||||
|
||||
Unprivileged Container haben standardmäßig kein `/dev/net/tun` — ohne das startet `tailscaled` nicht.
|
||||
|
||||
```bash
|
||||
pct stop <ID>
|
||||
cat >> /etc/pve/lxc/<ID>.conf <<'EOF'
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0
|
||||
EOF
|
||||
pct start <ID>
|
||||
```
|
||||
|
||||
Danach sollte `ls -la /dev/net/tun` im Container ein Character-Device zeigen (nicht "No such file or directory").
|
||||
|
||||
## 9. Tailscale-Route-Konflikt bei LAN-eigenen Containern
|
||||
|
||||
⚠️ **NICHT TUN:** `--accept-routes` bei einem Container setzen, der selbst schon direkt im `<LAN_SUBNET>`-LAN hängt (z.B. CT 102). Der PVE-Host advertised dieses Subnetz als Tailscale-Route — akzeptiert der Container das, landet in Tailscales eigener Policy-Routing-Tabelle (`ip route show table 52`) ein Eintrag `<LAN_SUBNET> dev tailscale0`, der Rückverkehr zur eigenen LAN-IP fälschlich über Tailscale statt direkt über `eth0` schickt. Symptom: Dienste auf dem Container sind über die LAN-IP nicht mehr erreichbar (Timeout, nicht Refused), obwohl sie laufen.
|
||||
|
||||
Für Container, die bereits nativ im LAN sitzen: `tailscale up ... --accept-routes=false`. Peer-zu-Peer-Erreichbarkeit zu anderen Tailnet-Mitgliedern funktioniert davon unabhängig.
|
||||
|
||||
## 10. PVE-Weboberfläche mit echtem Zertifikat (via `tailscale serve`)
|
||||
|
||||
Proxmox' Standard-Webinterface (`:8006`) nutzt ein selbstsigniertes Zertifikat → Browser-Warnung bei jedem Zugriff. Gleicher Trick wie bei Vaultwarden: **nicht** Proxmox' eigene Zertifikatsdateien anfassen (die sind für die interne Cluster-Kommunikation kritisch), sondern per `tailscale serve` reverse-proxyen.
|
||||
|
||||
```bash
|
||||
tailscale serve --bg https+insecure://localhost:8006
|
||||
```
|
||||
|
||||
`https+insecure://` ist hier bewusst nötig — das Backend (`pveproxy` auf `:8006`) hat selbst ein selbstsigniertes Zertifikat, das `tailscale serve` beim lokalen Proxy-Hop akzeptieren muss, ohne es zu verifizieren (nur der äußere, dem Nutzer sichtbare Hop bekommt das echte Tailscale-Zertifikat).
|
||||
|
||||
Erreichbar unter `https://<PVE_HOSTNAME>.<TAILNET>` (kein `:8006` mehr nötig, Port 443 Standard), automatisch gültiges Let's-Encrypt-Zertifikat, automatische Erneuerung. Voraussetzung: „HTTPS Certificates" in der Tailscale-Admin-Konsole aktiviert (siehe Abschnitt 2).
|
||||
|
||||
## Referenz: aktuelle LXC-Übersicht
|
||||
|
||||
```bash
|
||||
pct list
|
||||
for id in 100 101 102 103; do echo "CT $id:"; pct config $id | grep -E "memory|cores|net0"; done
|
||||
```
|
||||
154
02-ct100-adguard.md
Normal file
154
02-ct100-adguard.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# CT 100 — AdGuard Home — Rebuild-Runbook
|
||||
|
||||
## 1. Container erstellen (via Proxmox Community Script)
|
||||
|
||||
Interaktiv am PVE-Host ausführen (whiptail-Dialog):
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/adguard.sh)"
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** `adguardhome.sh` als Dateiname verwenden — 404. Der korrekte Dateiname im Repo ist `ct/adguard.sh`.
|
||||
|
||||
**Einstellungen im Advanced-Install-Dialog:**
|
||||
- Unprivileged, Debian 13
|
||||
- 1 CPU-Core, **1024 MiB RAM** (Standard 512 MiB reicht nicht für die großen Filterlisten, siehe unten), 2 GB Disk
|
||||
- Bridge `vmbr0`, statische IPv4 `<LAN_IP_ADGUARD>/24`
|
||||
- IPv6: none (Container bekommt trotzdem automatisch SLAAC-Adressen, unkritisch)
|
||||
- Timezone Europe/Berlin, kein HTTP-Proxy
|
||||
|
||||
Falls mit 512 MiB angelegt, nachträglich erhöhen:
|
||||
```bash
|
||||
pct set 100 --memory 1024
|
||||
```
|
||||
|
||||
## 2. Web-Wizard (einmalig, interaktiv unter `http://<LAN_IP_ADGUARD>:3000` bzw. später Port 80)
|
||||
|
||||
- Admin-User: `root` (oder eigener Name)
|
||||
- Passwort: eigenes wählen
|
||||
- Upstream-DNS: `https://dns10.quad9.net/dns-query` (Quad9 DoH)
|
||||
|
||||
Alle folgenden Schritte patchen `/opt/AdGuardHome/AdGuardHome.yaml` direkt (kein Web-UI-Login nötig, da Root-Zugriff auf den Container besteht). **Immer:**
|
||||
```bash
|
||||
pct exec 100 -- systemctl stop AdGuardHome
|
||||
# Datei anpassen (siehe unten)
|
||||
pct exec 100 -- systemctl start AdGuardHome
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** die Config bei laufendem Dienst editieren — Änderungen werden beim nächsten internen Save von AdGuard überschrieben.
|
||||
|
||||
## 3. Conditional Forwarding für `fritz.box`
|
||||
|
||||
In `dns:` → `upstream_dns:` ergänzen:
|
||||
```yaml
|
||||
upstream_dns:
|
||||
- https://dns10.quad9.net/dns-query
|
||||
- '[/fritz.box/]<LAN_IP_ROUTER>'
|
||||
```
|
||||
|
||||
Private Reverse-DNS aktivieren (für Gerätenamen im Query-Log):
|
||||
```yaml
|
||||
local_ptr_upstreams:
|
||||
- <LAN_IP_ROUTER>
|
||||
```
|
||||
(Im Web-UI: Einstellungen → DNS-Einstellungen → Private Reverse-DNS-Server aktivieren, `<LAN_IP_ROUTER>`, privates Subnetz `<LAN_SUBNET>`)
|
||||
|
||||
## 4. Filterlisten
|
||||
|
||||
`filters:`-Block (Filter-IDs müssen eindeutig sein, 1–2 sind Standard-AdGuard-Defaults):
|
||||
|
||||
```yaml
|
||||
filters:
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
|
||||
name: AdGuard DNS filter
|
||||
id: 1
|
||||
- enabled: false
|
||||
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt
|
||||
name: AdAway Default Blocklist
|
||||
id: 2
|
||||
- enabled: true
|
||||
url: https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/pro.txt
|
||||
name: HaGeZi's Pro Blocklist
|
||||
id: 3
|
||||
- enabled: true
|
||||
url: https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/tif.txt
|
||||
name: HaGeZi's Threat Intelligence Feeds
|
||||
id: 4
|
||||
- enabled: true
|
||||
url: https://filters.adtidy.org/extension/chromium/filters/11.txt
|
||||
name: AdGuard Mobile Ads filter
|
||||
id: 5
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_53.txt
|
||||
name: AWAvenue Ads Rule
|
||||
id: 7
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_59.txt
|
||||
name: AdGuard DNS Popup Hosts filter
|
||||
id: 8
|
||||
whitelist_filters:
|
||||
- enabled: true
|
||||
url: https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/whitelist-referral.txt
|
||||
name: HaGeZi's Allowlist Referral
|
||||
id: 6
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN (bewusst weggelassen, nicht aus Versehen):** oisd NSFW, HaGeZi Anti-Piracy, ShadowWhisperer Dating List, HaGeZi Safesearch Not Supported (reine Content-Filter, kein Ad-Blocking-Zweck), HaGeZi DynDNS-Blocklist (Konflikt mit eigenem STRATO-DynDNS), HaGeZi Encrypted DNS/VPN/TOR/Proxy Bypass (Risiko für den Firmen-VPN-Tunnel der Ehefrau), Dandelion Sprout's Anti-Malware List + ShadowWhisperer's Malware List (redundant zu HaGeZi TIF, nur RAM-Verschwendung).
|
||||
|
||||
## 5. DNS-Rewrites (interne Kurznamen)
|
||||
|
||||
```yaml
|
||||
rewrites:
|
||||
- domain: nas.pve
|
||||
answer: <LAN_IP_NAS>
|
||||
enabled: true
|
||||
- domain: adguard.pve
|
||||
answer: <LAN_IP_ADGUARD>
|
||||
enabled: true
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** das Feld `enabled: true` weglassen. AdGuard Home ergänzt es beim nächsten Neustart automatisch mit dem Default `enabled: false` — der Rewrite bleibt dann wirkungslos (Query geht bis zu den Root-DNS-Servern durch, NXDOMAIN), ohne erkennbaren Fehler in der Config selbst.
|
||||
|
||||
## 6. Passwort-Reset (falls nötig)
|
||||
|
||||
AdGuard speichert nur den bcrypt-Hash, keine Wiederherstellung möglich — neues Passwort setzen statt altes wiederherstellen:
|
||||
|
||||
```bash
|
||||
pct exec 100 -- apt-get install -y apache2-utils # für htpasswd -B
|
||||
NEWPASS="<neues Passwort>"
|
||||
HASH=$(pct exec 100 -- htpasswd -bnBC 10 "" "$NEWPASS" | tr -d ':\n')
|
||||
# In AdGuardHome.yaml, Zeile "password:" unter users: ersetzen durch $HASH
|
||||
```
|
||||
|
||||
Der von `htpasswd -B` erzeugte Hash hat `$2y$`-Präfix statt AdGuards eigenem `$2a$` — wird von Go/AdGuard-Home-bcrypt trotzdem korrekt akzeptiert.
|
||||
|
||||
## 7. Fritzbox-DHCP umstellen (manueller Schritt am Router, kein CLI)
|
||||
|
||||
Fritzbox-Oberfläche → **Heimnetz → Netzwerk → Netzwerkeinstellungen** → IPv4-Adressen bearbeiten → Abschnitt **DNS-Server** → „Andere DNSv4-Server verwenden" → Bevorzugter DNSv4-Server: `<LAN_IP_ADGUARD>`.
|
||||
|
||||
⚠️ **NICHT TUN (verworfene Variante):** Client fragt die Fritzbox, die AdGuard als Upstream nutzt. Stattdessen: Client fragt AdGuard direkt (wie oben), Fritzbox nur noch für `fritz.box`-Auflösung via Conditional Forwarding.
|
||||
|
||||
## 8. IPv6-DNS aktivieren
|
||||
|
||||
Standardmäßig lauscht AdGuard Home nur auf IPv4 (`bind_hosts: [0.0.0.0]`), obwohl der Container per SLAAC auch IPv6-Adressen bekommt — DNS-Anfragen über IPv6 werden dann gar nicht erst beantwortet, unabhängig davon, was die Fritzbox verteilt.
|
||||
|
||||
```yaml
|
||||
dns:
|
||||
bind_hosts:
|
||||
- 0.0.0.0
|
||||
- "::"
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** direkt vom PVE-Host aus testen, falls der selbst kein SLAAC-IPv6 auf der Bridge hat (`ip -6 addr show vmbr0` prüfen — meist nur Link-Local vorhanden). Test stattdessen von einem Container aus, der eine globale IPv6-Adresse hat (z. B. `pct exec 101 -- dig @<ULA-Adresse> google.com`).
|
||||
|
||||
**Manueller Schritt an der Fritzbox:** Heimnetz → Netzwerk → Netzwerkeinstellungen → IPv6-Adressen bearbeiten → DNS-Server → eigenen DNSv6-Server eintragen. **Die ULA-Adresse verwenden** (`fd23:...`, per `ip -6 addr show eth0` im Container ermitteln — steht als "scope global dynamic mngtmpaddr"), **nicht** die GUA (`2a02:...`) — die ULA bleibt stabil, die GUA kann sich bei einem ISP-Präfixwechsel ändern und die Konfiguration stillschweigend brechen.
|
||||
|
||||
## Verifikation
|
||||
|
||||
```bash
|
||||
dig @<LAN_IP_ADGUARD> doubleclick.net # sollte 0.0.0.0 liefern (geblockt)
|
||||
dig @<LAN_IP_ADGUARD> google.com # normale Antwort
|
||||
dig @<LAN_IP_ADGUARD> fritz.box # <LAN_IP_ROUTER> (Conditional Forwarding)
|
||||
dig @<LAN_IP_ADGUARD> nas.pve # <LAN_IP_NAS> (Rewrite)
|
||||
```
|
||||
145
03-ct101-nas.md
Normal file
145
03-ct101-nas.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# CT 101 — NAS (Samba/SFTP/Filebrowser) — Rebuild-Runbook
|
||||
|
||||
Voraussetzung: ZFS-Pool `nas` mit Dataset `nas/freigabe` existiert auf dem PVE-Host (siehe `01-pve-host.md`).
|
||||
|
||||
## 1. Container erstellen
|
||||
|
||||
```bash
|
||||
pct create 101 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
|
||||
--hostname nas \
|
||||
--unprivileged 1 \
|
||||
--cores 1 \
|
||||
--memory 512 \
|
||||
--swap 512 \
|
||||
--rootfs local-lvm:4 \
|
||||
--net0 name=eth0,bridge=vmbr0,ip=<LAN_IP_NAS>/24,gw=<LAN_IP_ROUTER> \
|
||||
--features nesting=1 \
|
||||
--timezone Europe/Berlin \
|
||||
--onboot 1 \
|
||||
--nameserver <LAN_IP_ADGUARD>
|
||||
|
||||
pct set 101 -mp0 /nas/freigabe,mp=/srv/freigabe
|
||||
chown -R 100000:100000 /nas/freigabe
|
||||
pct start 101
|
||||
```
|
||||
|
||||
Ownership `100000:100000` = Standard-UID-Offset für unprivileged Container (Container-UID 0 = Host-UID 100000).
|
||||
|
||||
## 2. Samba
|
||||
|
||||
```bash
|
||||
pct exec 101 -- apt-get update
|
||||
pct exec 101 -- apt-get install -y samba openssh-server
|
||||
```
|
||||
|
||||
Debians `samba`-Metapaket zieht standardmäßig `samba-ad-dc` + `winbind` mit (nicht gebraucht, kein AD-DC-Zweck):
|
||||
```bash
|
||||
pct exec 101 -- systemctl disable --now winbind
|
||||
pct exec 101 -- systemctl mask samba-ad-dc
|
||||
```
|
||||
|
||||
`/etc/samba/smb.conf` ergänzen:
|
||||
```ini
|
||||
[freigabe]
|
||||
path = /srv/freigabe
|
||||
browseable = yes
|
||||
read only = no
|
||||
guest ok = no
|
||||
valid users = <PRIMARY_USER>
|
||||
create mask = 0664
|
||||
directory mask = 0775
|
||||
```
|
||||
|
||||
Workgroup und Mindest-Protokoll im `[global]`-Block:
|
||||
```ini
|
||||
workgroup = WORKGROUP
|
||||
server min protocol = SMB2
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** das Standard-`[homes]`-Share per `sed` nur an der Kopfzeile auskommentieren. Der Block hat mehrere Folgezeilen (`browseable`, `create mask`, `valid users = %S` etc.) — werden die nicht mit auskommentiert, hängen sie als „verwaiste" Zeilen im vorherigen Abschnitt (`[global]`) und überschreiben dort Werte, u.a. `valid users = %S`. Folge: IPC$-Zugriff für **alle** Nutzer blockiert (`NT_STATUS_ACCESS_DENIED`), auch für die eigentlich erlaubte Freigabe. Immer den **kompletten** `[homes]`-Block (Kopfzeile + alle Optionszeilen bis zur nächsten Sektion) auskommentieren. Mit `testparm -s` nach jeder Änderung prüfen, dass `[global]` sauber bleibt.
|
||||
|
||||
Benutzer anlegen:
|
||||
```bash
|
||||
pct exec 101 -- useradd -M -s /bin/bash -d /srv/freigabe <PRIMARY_USER>
|
||||
pct exec 101 -- smbpasswd -a <PRIMARY_USER>
|
||||
pct exec 101 -- smbpasswd -e <PRIMARY_USER>
|
||||
pct exec 101 -- chpasswd # gleiches Passwort auch als Unix-Login setzen (für SSH/SFTP)
|
||||
pct exec 101 -- chown -R <PRIMARY_USER>:<PRIMARY_USER> /srv/freigabe
|
||||
pct exec 101 -- systemctl restart smbd nmbd
|
||||
```
|
||||
|
||||
Unix- und Samba-Passwort bewusst identisch gehalten — ein Passwort für SMB, SFTP und Filebrowser.
|
||||
|
||||
## 3. SSH/SFTP-Zugriff
|
||||
|
||||
Mit obigem `useradd -s /bin/bash` ist SSH bereits nutzbar (Shell + Home = `/srv/freigabe`, Passwort per `chpasswd` gesetzt). Kein separater Schritt nötig, wenn Schritt 2 komplett ausgeführt wurde.
|
||||
|
||||
⚠️ **NICHT TUN:** einen passwortlosen SSH-Key für Fernzugriff (z.B. aus Guacamole) auf dieses Share hinterlegen. Zugriff auf sensible Daten (Dokumente, Fotos) sollte immer noch ein Passwort verlangen — bei Kompromittierung der aufrufenden Instanz (z.B. Guacamole/VPS) sonst direkter, ungeschützter Zugriff. Auch verschlüsselte Keys mit Passphrase lösen kein echtes UX-Problem (siehe `06-vps-strato.md`, Abschnitt Guacamole-Verbindungen) — einfach normales Passwort verwenden.
|
||||
|
||||
## 3b. Zweite Freigabe: Backups (read-only)
|
||||
|
||||
Bind-Mount und Samba-Share für `/nas/backups` (siehe `07-backup-restore.md`) — bewusst **read-only**, da die Backups volle Container-Images inkl. aller Secrets enthalten:
|
||||
|
||||
```bash
|
||||
pct set 101 -mp1 /nas/backups,mp=/srv/backups,backup=0
|
||||
```
|
||||
|
||||
`/etc/samba/smb.conf` ergänzen:
|
||||
```ini
|
||||
[backups]
|
||||
path = /srv/backups
|
||||
browseable = yes
|
||||
read only = yes
|
||||
guest ok = no
|
||||
valid users = <PRIMARY_USER>
|
||||
```
|
||||
|
||||
Kein separater Nutzer nötig, `<PRIMARY_USER>` deckt beide Shares ab. Erreichbar unter `\\nas.pve\backups`.
|
||||
|
||||
## 4. Filebrowser (grafische Web-Oberfläche)
|
||||
|
||||
```bash
|
||||
VER=$(curl -s https://api.github.com/repos/filebrowser/filebrowser/releases/latest | grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4)
|
||||
curl -fsSL -o /tmp/filebrowser.tar.gz "https://github.com/filebrowser/filebrowser/releases/download/$VER/linux-amd64-filebrowser.tar.gz"
|
||||
tar xzf /tmp/filebrowser.tar.gz -C /tmp filebrowser
|
||||
pct push 101 /tmp/filebrowser /usr/local/bin/filebrowser
|
||||
pct exec 101 -- chmod +x /usr/local/bin/filebrowser
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** das offizielle `curl | bash`-Installskript von Filebrowser verwenden — stattdessen Release-Binary direkt von GitHub laden (transparenter, keine Shell-Pipe von einem Skript, das man vorher nicht liest).
|
||||
|
||||
Config + Service:
|
||||
```bash
|
||||
pct exec 101 -- mkdir -p /var/lib/filebrowser
|
||||
pct exec 101 -- chown <PRIMARY_USER>:<PRIMARY_USER> /var/lib/filebrowser
|
||||
pct exec 101 -- su -s /bin/bash <PRIMARY_USER> -c '/usr/local/bin/filebrowser config init -d /var/lib/filebrowser/filebrowser.db'
|
||||
pct exec 101 -- su -s /bin/bash <PRIMARY_USER> -c '/usr/local/bin/filebrowser config set -a 0.0.0.0 -p 8080 -r /srv/freigabe -d /var/lib/filebrowser/filebrowser.db'
|
||||
pct exec 101 -- su -s /bin/bash <PRIMARY_USER> -c "/usr/local/bin/filebrowser users add <PRIMARY_USER> '<PASSWORT>' --perm.admin -d /var/lib/filebrowser/filebrowser.db"
|
||||
|
||||
cat > /etc/systemd/system/filebrowser.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Filebrowser Web UI
|
||||
After=network.target srv-freigabe.mount
|
||||
|
||||
[Service]
|
||||
User=<PRIMARY_USER>
|
||||
Group=<PRIMARY_USER>
|
||||
ExecStart=/usr/local/bin/filebrowser -d /var/lib/filebrowser/filebrowser.db
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now filebrowser
|
||||
```
|
||||
|
||||
Erreichbar unter `http://nas.pve:8080` (LAN + Tailnet). **Bewusst nicht öffentlich exponiert** — für den Internetcafé-Fall gibt's den SFTP-Dateibrowser innerhalb der TOTP-gesicherten Guacamole-Verbindung (siehe `06-vps-strato.md`).
|
||||
|
||||
## Verifikation
|
||||
|
||||
```bash
|
||||
smbclient -L //<LAN_IP_NAS> -U <PRIMARY_USER>%'<PASSWORT>'
|
||||
sftp <PRIMARY_USER>@<LAN_IP_NAS>
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://<LAN_IP_NAS>:8080/
|
||||
```
|
||||
164
04-ct102-management-desktop.md
Normal file
164
04-ct102-management-desktop.md
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# CT 102 — Management-Desktop (Browser-VNC) — Rebuild-Runbook
|
||||
|
||||
Zweck: gemeinsamer grafischer Zugang (für Nutzer + Ehefrau) zu allen Web-Oberflächen, ohne die RDP-Session von PC1 zu belegen (Windows Pro erlaubt nur eine aktive Sitzung).
|
||||
|
||||
## 1. Container erstellen
|
||||
|
||||
```bash
|
||||
pct create 102 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
|
||||
--hostname browser \
|
||||
--unprivileged 1 \
|
||||
--cores 2 \
|
||||
--memory 1536 \
|
||||
--swap 512 \
|
||||
--rootfs local-lvm:6 \
|
||||
--net0 name=eth0,bridge=vmbr0,ip=<LAN_IP_DESKTOP>/24,gw=<LAN_IP_ROUTER> \
|
||||
--features nesting=1 \
|
||||
--timezone Europe/Berlin \
|
||||
--onboot 1 \
|
||||
--nameserver <LAN_IP_ADGUARD>
|
||||
pct start 102
|
||||
```
|
||||
|
||||
## 2. TUN-Device für Tailscale (siehe `01-pve-host.md`, Abschnitt 8)
|
||||
|
||||
```bash
|
||||
pct stop 102
|
||||
cat >> /etc/pve/lxc/102.conf <<'EOF'
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0
|
||||
EOF
|
||||
pct start 102
|
||||
```
|
||||
|
||||
## 3. Desktop-Umgebung + Browser + VNC
|
||||
|
||||
```bash
|
||||
pct exec 102 -- apt-get update
|
||||
pct exec 102 -- apt-get install -y curl ca-certificates
|
||||
pct exec 102 -- apt-get install -y xfce4 xfce4-terminal firefox-esr tigervnc-standalone-server tigervnc-common dbus-x11 fonts-liberation
|
||||
```
|
||||
|
||||
VNC-Konfiguration:
|
||||
```bash
|
||||
pct exec 102 -- mkdir -p /root/.config/tigervnc
|
||||
VNCPASS="<VNC-Passwort>"
|
||||
pct exec 102 -- bash -c "printf '%s\n%s\nn\n' '$VNCPASS' '$VNCPASS' | vncpasswd -f > /root/.config/tigervnc/passwd && chmod 600 /root/.config/tigervnc/passwd"
|
||||
|
||||
pct exec 102 -- bash -c "cat > /root/.config/tigervnc/xstartup <<'EOF'
|
||||
#!/bin/sh
|
||||
unset SESSION_MANAGER
|
||||
unset DBUS_SESSION_BUS_ADDRESS
|
||||
exec startxfce4
|
||||
EOF
|
||||
chmod +x /root/.config/tigervnc/xstartup"
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** Config unter `~/.vnc/` anlegen. Neuere TigerVNC-Versionen erwarten `~/.config/tigervnc/` (XDG-Standard) und versuchen beim Start eine automatische Migration von `~/.vnc/`, die zuverlässig fehlschlägt ("Could not migrate ... to ... tigervnc", Dienst crasht in einer Neustart-Schleife). Config direkt im neuen Pfad anlegen, `~/.vnc/` gar nicht erst befüllen.
|
||||
|
||||
Systemd-Service:
|
||||
```bash
|
||||
cat > /etc/systemd/system/vncserver.service <<'EOF'
|
||||
[Unit]
|
||||
Description=TigerVNC Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/root
|
||||
ExecStartPre=-/usr/bin/vncserver -kill :1
|
||||
ExecStart=/usr/bin/vncserver -fg -localhost no -geometry 1600x900 -depth 24 :1
|
||||
ExecStop=/usr/bin/vncserver -kill :1
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now vncserver
|
||||
```
|
||||
|
||||
`-localhost no` ist Pflicht — sonst nimmt TigerVNC nur lokale Verbindungen an, Guacamole (auf einer anderen Maschine) käme nicht durch.
|
||||
|
||||
## 4. Firefox-Profil + Startseiten
|
||||
|
||||
```bash
|
||||
pct exec 102 -- bash -c "DISPLAY=:1 firefox-esr -CreateProfile 'default /root/.mozilla/firefox/default'"
|
||||
|
||||
pct exec 102 -- bash -c "cat > /root/.mozilla/firefox/default/user.js <<'EOF'
|
||||
user_pref(\"browser.startup.homepage\", \"http://adguard.pve|https://<PVE_HOSTNAME>.<TAILNET>|http://nas.pve:8080|https://guac.<DOMAIN>|https://vaultwarden.<TAILNET>\");
|
||||
user_pref(\"browser.startup.page\", 1);
|
||||
user_pref(\"browser.tabs.warnOnClose\", false);
|
||||
EOF
|
||||
"
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** sich darauf verlassen, dass dieses manuell erstellte `default`-Profil auch tatsächlich benutzt wird. `firefox-esr` legt beim **allerersten echten Start** (nicht beim `-CreateProfile`-Aufruf, sondern beim ersten normalen Öffnen über die GUI) automatisch ein zusätzliches Profil an (Name-Muster `<zufallsstring>.default-esr`) und trägt es in `profiles.ini` unter einem `[InstallXXXX]`-Block mit `Locked=1` als tatsächlichen Standard ein — das überschreibt die klassische `Default=1`-Markierung des manuell erstellten Profils. Ergebnis: die `user.js` im manuellen Profil wird nie gelesen, Startseiten erscheinen einfach nicht, ohne Fehlermeldung.
|
||||
|
||||
**Nach dem ersten echten GUI-Start prüfen und ggf. korrigieren:**
|
||||
```bash
|
||||
cat /root/.mozilla/firefox/profiles.ini # zeigt unter [InstallXXXX] den tatsächlich genutzten Profil-Pfad
|
||||
cp /root/.mozilla/firefox/default/user.js /root/.mozilla/firefox/<echter-profil-ordner>/user.js
|
||||
```
|
||||
|
||||
Öffnet beim Start automatisch: AdGuard, PVE-Weboberfläche, NAS-Filebrowser, Guacamole, Vaultwarden.
|
||||
|
||||
## 5. Tailscale-Mitgliedschaft
|
||||
|
||||
```bash
|
||||
pct exec 102 -- bash -c "curl -fsSL https://tailscale.com/install.sh | sh"
|
||||
pct exec 102 -- tailscale up --authkey=<AUTH_KEY> --hostname=browser --accept-routes=false
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** `--accept-routes` (ohne `=false`) setzen. Siehe `01-pve-host.md`, Abschnitt 9 — der Container sitzt selbst schon im `<LAN_SUBNET>`-LAN, das Akzeptieren der vom PVE-Host advertisten Subnet-Route erzeugt einen Policy-Routing-Konflikt (Tabelle 52), der die eigene LAN-Erreichbarkeit über Timeouts killt. Peer-zu-Peer zu anderen Tailnet-Mitgliedern (z.B. Vaultwarden) funktioniert auch ohne `--accept-routes`.
|
||||
|
||||
## 6. Guacamole-Verbindung (auf dem VPS, siehe `06-vps-strato.md`)
|
||||
|
||||
VNC-Verbindung "Management-Desktop (Browser)", Hostname zeigt auf die **Tailscale-IP** des Containers (`<TS_IP_DESKTOP>`), nicht die LAN-IP — damit läuft die Backend-Strecke Guacamole↔VNC-Server über den WireGuard-verschlüsselten Tailscale-Tunnel statt übers offene LAN.
|
||||
|
||||
⚠️ **Bekannte Einschränkung:** Guacamoles VNC-Protokoll-Implementierung unterstützt kein TLS/Zertifikat (anders als RDP mit `security`/`ignore-cert`-Parametern) — das ist eine echte Protokoll-Grenze, keine Fehlkonfiguration. Der Tailscale-Tunnel ist der bestmögliche Ersatz dafür.
|
||||
|
||||
## 7. Downloads aus der VNC-Sitzung aufs lokale Gerät übertragen
|
||||
|
||||
Ein Download in Firefox innerhalb der Guacamole-VNC-Sitzung landet zunächst nur auf CT 102 selbst (`/root/Downloads`), nicht automatisch auf dem tatsächlichen Endgerät des Nutzers. Fix: dedizierter, stark eingeschränkter SFTP-Only-User als Guacamole-Begleitverbindung.
|
||||
|
||||
```bash
|
||||
useradd -M -s /usr/sbin/nologin transfer
|
||||
echo 'transfer:<Passwort>' | chpasswd
|
||||
|
||||
cat >> /etc/ssh/sshd_config <<'EOF'
|
||||
|
||||
Match User transfer
|
||||
ChrootDirectory /root/Downloads
|
||||
ForceCommand internal-sftp
|
||||
AllowTcpForwarding no
|
||||
X11Forwarding no
|
||||
EOF
|
||||
|
||||
mkdir -p /run/sshd
|
||||
systemctl restart ssh
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** nach einer `sshd_config`-Änderung `systemctl reload ssh` statt `restart` verwenden. Reload löst ein SIGHUP-getriggertes Re-Exec des laufenden sshd-Prozesses aus, **ohne** die vollständige systemd-Start-Sequenz zu durchlaufen — dabei fehlt `/run/sshd` (Privilege-Separation-Verzeichnis), Ergebnis: `fatal: Cannot bind any address`, Dienst stirbt komplett. Immer `restart` nutzen (oder `mkdir -p /run/sshd` vor einem Reload sicherstellen).
|
||||
|
||||
`/root/Downloads` erfüllt die OpenSSH-Chroot-Anforderungen bereits von selbst (root:root, kein Gruppen-/Other-Schreibrecht, Standard-Firefox-Download-Ziel für root).
|
||||
|
||||
In Guacamole die bestehende VNC-Verbindung um SFTP-Begleitparameter ergänzen (**gleiche Tailscale-IP wie der Hostname-Parameter**, aus Konsistenzgründen — sonst liefe der Dateiinhalt beim Download plaintext übers LAN, während die VNC-Steuerung schon verschlüsselt tunnelt):
|
||||
```sql
|
||||
enable-sftp = true
|
||||
sftp-hostname = <TS_IP_DESKTOP>
|
||||
sftp-port = 22
|
||||
sftp-username = transfer
|
||||
sftp-root-directory = /
|
||||
```
|
||||
Passwort nicht als Parameter setzen — wird beim ersten Datei-Transfer-Zugriff im Guacamole-Seitenpanel abgefragt.
|
||||
|
||||
## Verifikation
|
||||
|
||||
```bash
|
||||
nc -zv -w3 <TS_IP_DESKTOP> 5901 # sollte "open" zeigen (von einem Tailnet-Mitglied aus)
|
||||
pct exec 102 -- systemctl is-active vncserver
|
||||
pct exec 102 -- systemctl is-active ssh
|
||||
sftp transfer@<LAN_IP_DESKTOP> # sollte nach /root/Downloads chrooten (pwd zeigt "/")
|
||||
```
|
||||
116
05-ct103-vaultwarden.md
Normal file
116
05-ct103-vaultwarden.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# CT 103 — Vaultwarden (Passwortmanager) — Rebuild-Runbook
|
||||
|
||||
Sensibelster Dienst im gesamten Setup — bewusst **nur über Tailscale** erreichbar, nie öffentlich.
|
||||
|
||||
## 1. Container erstellen
|
||||
|
||||
```bash
|
||||
pct create 103 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
|
||||
--hostname vaultwarden \
|
||||
--unprivileged 1 \
|
||||
--cores 1 \
|
||||
--memory 512 \
|
||||
--swap 512 \
|
||||
--rootfs local-lvm:4 \
|
||||
--net0 name=eth0,bridge=vmbr0,ip=<LAN_IP_VAULTWARDEN>/24,gw=<LAN_IP_ROUTER> \
|
||||
--features nesting=1 \
|
||||
--timezone Europe/Berlin \
|
||||
--onboot 1 \
|
||||
--nameserver <LAN_IP_ADGUARD>
|
||||
pct start 103
|
||||
```
|
||||
|
||||
## 2. TUN-Device für Tailscale (siehe `01-pve-host.md`, Abschnitt 8)
|
||||
|
||||
```bash
|
||||
pct stop 103
|
||||
cat >> /etc/pve/lxc/103.conf <<'EOF'
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0
|
||||
EOF
|
||||
pct start 103
|
||||
```
|
||||
|
||||
## 3. Docker
|
||||
|
||||
```bash
|
||||
pct exec 103 -- apt-get update
|
||||
pct exec 103 -- apt-get install -y curl ca-certificates
|
||||
pct exec 103 -- bash -c "curl -fsSL https://get.docker.com | sh"
|
||||
```
|
||||
|
||||
## 4. Tailscale-Mitgliedschaft + HTTPS-Zertifikat
|
||||
|
||||
```bash
|
||||
pct exec 103 -- bash -c "curl -fsSL https://tailscale.com/install.sh | sh"
|
||||
pct exec 103 -- tailscale up --authkey=<AUTH_KEY> --hostname=vaultwarden
|
||||
```
|
||||
|
||||
Kein `--accept-routes` nötig — anders als CT 102 sitzt dieser Container nicht zusätzlich im `<LAN_SUBNET>`-LAN in einer Weise, die einen Routing-Konflikt erzeugen würde (Netz-Interface ist zwar auf dem LAN, aber der Dienst wird ausschließlich über die Tailscale-IP angesprochen).
|
||||
|
||||
Voraussetzung in der Tailscale-Admin-Konsole: **Settings → DNS → „HTTPS Certificates"** muss aktiviert sein (einmalig für den ganzen Tailnet, siehe `01-pve-host.md`).
|
||||
|
||||
## 5. Vaultwarden via Docker Compose
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/vaultwarden/data
|
||||
ADMIN_TOKEN=$(openssl rand -base64 48 | tr -d '\n')
|
||||
|
||||
cat > /opt/vaultwarden/docker-compose.yml <<EOF
|
||||
services:
|
||||
vaultwarden:
|
||||
image: vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
environment:
|
||||
DOMAIN: "https://vaultwarden.<TAILNET>"
|
||||
ADMIN_TOKEN: "$ADMIN_TOKEN"
|
||||
SIGNUPS_ALLOWED: "true"
|
||||
WEBSOCKET_ENABLED: "true"
|
||||
volumes:
|
||||
- ./data:/data
|
||||
ports:
|
||||
- "127.0.0.1:8080:80"
|
||||
EOF
|
||||
|
||||
cd /opt/vaultwarden && docker compose up -d
|
||||
```
|
||||
|
||||
`SIGNUPS_ALLOWED` bewusst zuerst `true`, um das/die Konto(s) anzulegen. **Nach der Konto-Anlage zwingend zurückstellen:**
|
||||
```bash
|
||||
sed -i 's/SIGNUPS_ALLOWED: "true"/SIGNUPS_ALLOWED: "false"/' /opt/vaultwarden/docker-compose.yml
|
||||
cd /opt/vaultwarden && docker compose up -d
|
||||
```
|
||||
|
||||
## 6. HTTPS-Exposition via `tailscale serve` (kein Caddy, kein manuelles Zertifikat)
|
||||
|
||||
```bash
|
||||
tailscale serve --bg http://127.0.0.1:8080
|
||||
```
|
||||
|
||||
Erreichbar unter `https://vaultwarden.<TAILNET>`, automatisch gültiges Let's-Encrypt-Zertifikat, automatische Erneuerung, **nicht öffentlich** (nur Tailnet-Mitglieder).
|
||||
|
||||
⚠️ **NICHT TUN:** `tailscale serve --bg https / http://...` (alte Syntax). Neuere Tailscale-CLI-Versionen erwarten nur noch `tailscale serve --bg http://127.0.0.1:8080` (HTTPS wird automatisch angenommen).
|
||||
|
||||
⚠️ **NICHT TUN:** Vaultwarden zusätzlich über Caddy auf dem VPS öffentlich exponieren (wie bei Guacamole). Bewusste Entscheidung: der Passwort-Tresor ist der sensibelste Dienst im gesamten Setup, öffentliche Erreichbarkeit erhöht die Angriffsfläche unnötig — der Internetcafé-Fall wird stattdessen über Guacamole selbst abgedeckt (SFTP/RDP-Zugriff auf andere Dienste, nicht direkt auf den Tresor).
|
||||
|
||||
## 7. Konto-Anlage (manueller Web-Schritt, nicht per CLI)
|
||||
|
||||
Über `https://vaultwarden.<TAILNET>` (nur von einem Tailnet-Mitglied aus erreichbar) im Browser ein Konto mit **selbst gewähltem Master-Passwort** anlegen. Das Master-Passwort wird bewusst **nie** programmatisch gesetzt oder irgendwo notiert — es ist der einzige Schlüssel zum gesamten Tresor.
|
||||
|
||||
## Bekannte Stolpersteine (bereits behoben, hier nur als Hinweis)
|
||||
|
||||
* Beim Passwort-Import versehentlich Duplikate erzeugt (zweimal importiert) → behoben durch: alle Einträge auswählen → löschen → Papierkorb leeren → sauber neu importieren.
|
||||
|
||||
## Verifikation
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/ # lokal, im Container
|
||||
curl -s -o /dev/null -w "%{http_code}\n" https://vaultwarden.<TAILNET>/ # von einem Tailnet-Mitglied
|
||||
tailscale serve status
|
||||
```
|
||||
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}}'
|
||||
```
|
||||
99
07-backup-restore.md
Normal file
99
07-backup-restore.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Backup & Restore — Laufzeit-Zustand (PVE-LXCs + VPS-Guacamole-Stack)
|
||||
|
||||
Sichert den Laufzeit-Zustand, nicht nur Dateien: LXC-Configs, Datenbanken, Guacamole-Verbindungsdefinitionen/TOTP-Secrets, Vaultwardens verschlüsselten Tresor. Der ZFS-Mirror (`nas/freigabe`) deckt reinen Dateiverlust ab — dieses Kapitel deckt den Fall "Server/Container ist weg und muss aus dem Nichts wiederhergestellt werden".
|
||||
|
||||
## 1. PVE-Host: Backup-Dataset + Ausschluss der NAS-Freigabe
|
||||
|
||||
```bash
|
||||
zfs create nas/backups
|
||||
mkdir -p /nas/backups/pve /nas/backups/vps
|
||||
pct set 101 -mp0 /nas/freigabe,mp=/srv/freigabe,backup=0
|
||||
```
|
||||
|
||||
`nas/backups` bewusst **nicht** als PVE-Storage registrieren (gleiche Begründung wie beim `nas`-Pool selbst, siehe `01-pve-host.md` Abschnitt 3) — `vzdump --dumpdir` schreibt direkt dorthin, kein `pvestatd`-Polling nötig. CT101s `mp0` (Bind-Mount von `/nas/freigabe`, 37GB) wird von Container-Backups ausgeschlossen, sonst läge die komplette NAS-Freigabe redundant auch im Backup-Ziel auf demselben Pool.
|
||||
|
||||
## 2. PVE-Host: nächtlicher vzdump für alle 4 LXCs
|
||||
|
||||
```bash
|
||||
(crontab -l 2>/dev/null; echo "0 3 * * * vzdump 100 101 102 103 --mode snapshot --compress zstd --dumpdir /nas/backups/pve --prune-backups keep-last=3 >> /var/log/vzdump-cron.log 2>&1") | crontab -
|
||||
```
|
||||
|
||||
LVM-Thin-Snapshot-Modus (`--mode snapshot`) → alle 4 Container laufen auf `local-lvm`, minimale Downtime. Vaultwarden (CT103, SQLite im WAL-Modus) ist damit ohne separates DB-Skript crash-konsistent abgedeckt — SQLite ist genau für diesen Fall ausgelegt.
|
||||
|
||||
## 3. VPS: rsync/cron nachinstallieren
|
||||
|
||||
```bash
|
||||
apt update && apt install -y rsync cron
|
||||
systemctl enable --now cron
|
||||
```
|
||||
|
||||
⚠️ **NICHT vergessen:** Die minimale Debian-13-Installation auf dem VPS hat weder `rsync` (liefert `rrsync` mit, ab Version 3.4.x fertig unter `/usr/bin/rrsync`) noch `cron` vorinstalliert. Ohne `cron`-Paket schlägt `crontab -l`/`crontab -` mit "command not found" fehl.
|
||||
|
||||
## 4. VPS: Backup-Skript für die Guacamole-Stack
|
||||
|
||||
`/usr/local/bin/backup-guacamole.sh`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
DEST=/root/backups
|
||||
DATE=$(date +%F)
|
||||
mkdir -p "$DEST"
|
||||
docker exec guac-postgres pg_dump -U guacamole_user guacamole_db | gzip > "$DEST/guacamole_db_$DATE.sql.gz"
|
||||
tar czf "$DEST/guacamole-opt_$DATE.tar.gz" --exclude=pg-data -C /opt guacamole
|
||||
find "$DEST" -mtime +7 -delete
|
||||
```
|
||||
```bash
|
||||
chmod +x /usr/local/bin/backup-guacamole.sh
|
||||
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/backup-guacamole.sh >> /var/log/backup-guacamole.log 2>&1") | crontab -
|
||||
```
|
||||
|
||||
`pg_dump` statt rohem Kopieren von `pg-data` — transaktionssicher bei laufendem Postgres, roher Kopiervorgang eines laufenden Datenverzeichnisses wäre das nicht. Rotation: 7 Tage lokal auf dem VPS (das eigentliche Archiv liegt nach Schritt 6 auf dem NAS).
|
||||
|
||||
## 5. Restriktiver Pull-Key (VPS → NAS)
|
||||
|
||||
Dediziertes Keypair auf dem PVE-Host, **nicht** `id_rsa` oder `wol_vps_key` wiederverwenden:
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f /root/.ssh/backup_pull_key -N "" -C "backup-pull@pve"
|
||||
```
|
||||
|
||||
Public Key auf dem VPS in `authorized_keys` ergänzen:
|
||||
```bash
|
||||
PUBKEY=$(cat /root/.ssh/backup_pull_key.pub)
|
||||
ssh root@<TS_IP_VPS> "echo 'restrict,command=\"rrsync -ro /root/backups/\" $PUBKEY' >> /root/.ssh/authorized_keys"
|
||||
```
|
||||
|
||||
→ Der Key kann ausschließlich lesend aus `/root/backups/` syncen, keine Shell, kein Port-/Agent-Forwarding. Im Gegensatz zum `wolonly`-Key (`01-pve-host.md` Abschnitt 6) ist `restrict` hier unproblematisch — der Pull läuft per Cron ohne PTY-Anforderung, das PTY-Problem betraf nur Guacamoles SSH-Client.
|
||||
|
||||
## 6. PVE-Host: nächtlicher Pull-Cron
|
||||
|
||||
```bash
|
||||
(crontab -l 2>/dev/null; echo '0 4 * * * rsync -av -e "ssh -i /root/.ssh/backup_pull_key" root@<TS_IP_VPS>: /nas/backups/vps/ >> /var/log/vps-backup-pull.log 2>&1') | crontab -
|
||||
```
|
||||
|
||||
Zeitlich nach beiden vorherigen Jobs (02:00 VPS-Backup, 03:00 vzdump).
|
||||
|
||||
⚠️ **NICHT TUN:** Auf dem Client den vollen Pfad wiederholen (`root@vps:/root/backups/ ziel/`). `rrsync -ro /root/backups/` fixiert das Restricted-Verzeichnis bereits serverseitig; ein vom Client mitgegebener Pfad wird (nach Entfernen des führenden `/`) daran **angehängt** — Ergebnis ist der doppelte, nicht existierende Pfad `/root/backups/root/backups/` (Fehler "No such file or directory"). Korrekt ist ein leerer Pfad nach dem Doppelpunkt: `root@<TS_IP_VPS>:` — das adressiert die Wurzel des Restricted-Verzeichnisses.
|
||||
|
||||
## 7. Restore
|
||||
|
||||
**LXC (jede der 4 Instanzen):**
|
||||
```bash
|
||||
pct restore <vmid> /nas/backups/pve/vzdump-lxc-<vmid>-<timestamp>.tar.zst --storage local-lvm
|
||||
```
|
||||
Vaultwarden (CT103) braucht keinen separaten Restore-Schritt — Tresor + `rsa_key.pem` sind im LXC-Restore enthalten.
|
||||
|
||||
**Guacamole-Stack (VPS):** Compose-Stack aus dem Tar-Archiv wiederherstellen, dann DB einspielen:
|
||||
```bash
|
||||
gunzip -c guacamole-opt_<date>.tar.gz | tar x -C /opt # falls /opt/guacamole fehlt
|
||||
cd /opt/guacamole && docker compose up -d postgres guacd
|
||||
gunzip -c guacamole_db_<date>.sql.gz | docker exec -i guac-postgres psql -U guacamole_user guacamole_db
|
||||
docker compose up -d guacamole
|
||||
```
|
||||
|
||||
## Referenz: Backup-Übersicht
|
||||
|
||||
| Was | Wo (Quelle) | Mechanismus | Ziel | Rotation |
|
||||
|---|---|---|---|---|
|
||||
| 4 LXCs (100–103) | PVE-Host | `vzdump` Snapshot, 03:00 | `/nas/backups/pve` | keep-last=3 |
|
||||
| Guacamole-DB + Configs | VPS | `pg_dump` + `tar`, 02:00 | `/root/backups` (VPS, Zwischenstand) | 7 Tage |
|
||||
| — Pull der VPS-Backups | PVE-Host | `rsync` über `backup_pull_key`, 04:00 | `/nas/backups/vps` | folgt VPS-Rotation |
|
||||
235
08-monitoring.md
Normal file
235
08-monitoring.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# Monitoring — Rebuild-Runbook
|
||||
|
||||
Neue LXC `CT104 monitoring` (`<LAN_IP_MONITORING>`, Tailscale-Mitglied `monitoring.<TAILNET>`) hostet als Docker-Compose-Stack: InfluxDB (Metrik-Datenbank), Grafana (Dashboards), Uptime Kuma (Erreichbarkeit + Alert-Routing), ntfy (Push-Benachrichtigungen). Ergänzt um PVE-Metric-Server-Export, Telegraf auf dem VPS, ein HDD-SMART-Tracking-Script und Login-/Zugriffs-Tracking je Dienst.
|
||||
|
||||
## 1. CT104 anlegen
|
||||
|
||||
```bash
|
||||
pct create 104 local:vztmpl/debian-13-standard_13.6-1_amd64.tar.zst \
|
||||
--hostname monitoring --memory 1536 --swap 512 --cores 2 \
|
||||
--rootfs local-lvm:12 \
|
||||
--net0 name=eth0,bridge=vmbr0,ip=<LAN_IP_MONITORING>/24,gw=<LAN_IP_ROUTER> \
|
||||
--nameserver <LAN_IP_ADGUARD> --unprivileged 1 --features nesting=1 \
|
||||
--onboot 1 --timezone Europe/Berlin --ostype debian
|
||||
pct start 104
|
||||
|
||||
pct stop 104
|
||||
cat >> /etc/pve/lxc/104.conf <<'EOF'
|
||||
lxc.cgroup2.devices.allow: c 10:200 rwm
|
||||
lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file 0 0
|
||||
EOF
|
||||
pct start 104
|
||||
```
|
||||
|
||||
Danach Docker (`curl -fsSL https://get.docker.com | sh`) und Tailscale (`curl -fsSL https://tailscale.com/install.sh | sh && tailscale up --hostname=monitoring --accept-routes=false`) installieren.
|
||||
|
||||
⚠️ **NICHT vergessen:** CT104 ins nächtliche `vzdump`-Backup aufnehmen (`crontab -e` auf dem PVE-Host, `104` zur ID-Liste ergänzen) — sonst ist der Monitoring-Stack selbst nicht gesichert.
|
||||
|
||||
## 2. Docker-Compose-Stack
|
||||
|
||||
`/opt/monitoring/docker-compose.yml` (Auszug, vollständige Datei im laufenden System): vier Services `influxdb`, `grafana`, `uptime-kuma`, `ntfy`, gemeinsamer `x-logging`-Anchor (10m×3 Dateien, bestehende Konvention). Ports: InfluxDB 8086, Grafana 3000, Uptime Kuma 3001, ntfy 8090→80.
|
||||
|
||||
⚠️ **NICHT TUN:** Grafana-Datenverzeichnis (`./grafana`) beim ersten Start nicht vor-chownen — der Container läuft als UID 472, ein per `pct exec`/root angelegtes Bind-Mount-Verzeichnis gehört aber root:root. Fix: `chown -R 472:472 /opt/monitoring/grafana` **vor** dem ersten Start von Grafana, sonst Crash-Loop ("not writable").
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/monitoring/{influxdb,grafana,uptime-kuma,ntfy}
|
||||
chown -R 472:472 /opt/monitoring/grafana
|
||||
cd /opt/monitoring && docker compose up -d
|
||||
```
|
||||
|
||||
## 3. InfluxDB: Setup, Buckets, Retention, Downsampling
|
||||
|
||||
```bash
|
||||
docker exec influxdb influx setup --username admin --password '<PW>' \
|
||||
--org homelab --bucket raw --retention 14d --force
|
||||
docker exec influxdb influx bucket create --name aggregated --org homelab --retention 730d
|
||||
```
|
||||
|
||||
Zwei Buckets statt vieler kleiner: `raw` (14 Tage, volle Auflösung — deckt die längste Rohdaten-Anforderung aller Datentypen ab) und `aggregated` (2 Jahre, stündliche Mittelwerte). Downsampling-Task (`influx task create -f downsample.flux`):
|
||||
|
||||
```flux
|
||||
option task = {name: "downsample-hourly", every: 1h}
|
||||
|
||||
from(bucket: "raw")
|
||||
|> range(start: -task.every)
|
||||
|> filter(fn: (r) => r._field != "name" and r._field != "status" and r._field != "type" and r._field != "tags" and r._field != "uptime_format" and r._field != "content")
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> to(bucket: "aggregated", org: "homelab")
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** Den `raw`-Bucket ohne Feld-Filter blind mit `aggregateWindow(fn: mean)` downsamplen. Proxmox' `system`-Measurement (LXC- und Storage-Objekte) enthält Text-Felder (`name`, `status`, `type`, `tags`, `uptime_format`, `content`) — `mean` auf einer String-Spalte lässt den Task mit `unsupported input type for mean aggregate: string` fehlschlagen. Vor dem Anlegen des Tasks die tatsächlichen Feldnamen je Measurement prüfen (`schema.fieldKeys`), nicht raten.
|
||||
|
||||
Retention-Prinzip (Details/Begründung siehe Plan-Historie): Rohdaten kurz (Tage), Aggregate lang (Jahre) — hält die Gesamtgröße über Jahre konstant klein statt linear mit der Laufzeit zu wachsen.
|
||||
|
||||
## 4. PVE Metric Server → InfluxDB
|
||||
|
||||
```bash
|
||||
pvesh create /cluster/metrics/server/monitoring \
|
||||
--type influxdb --server <LAN_IP_MONITORING> --port 8086 \
|
||||
--influxdbproto http --organization homelab --bucket raw --token '<TOKEN>'
|
||||
```
|
||||
|
||||
Pusht Host- und LXC-Metriken automatisch alle ~10s, kein Agent in den LXCs nötig. Guest-Metriken landen unter Measurement `system` (Felder `cpu`, `mem`, `maxmem`, …, Tag `object=lxc`), Host-Metriken unter `cpustat`/`memory`/`blockstat`/`nics` (Tag `object=nodes`) — **unterschiedliches Schema**, beim Dashboard-Bau beide Fälle abdecken.
|
||||
|
||||
## 5. Telegraf auf dem VPS
|
||||
|
||||
VPS ist kein PVE-Cluster-Mitglied, braucht eigenen Agent:
|
||||
|
||||
```bash
|
||||
apt install -y gnupg
|
||||
curl -sL -o /tmp/telegraf.deb https://dl.influxdata.com/telegraf/releases/telegraf_1.39.1-1_amd64.deb
|
||||
dpkg -i /tmp/telegraf.deb
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** Das offizielle InfluxData-apt-Repo (`repos.influxdata.com/debian`) auf Debian 13 (trixie) einbinden — die GPG-Signatur wird von `apt`/`sqv` mit "Missing key" abgelehnt (Schlüssel-Rotation nicht mit dem dokumentierten `_compat.key` synchron). Stattdessen das `.deb` direkt von `dl.influxdata.com/telegraf/releases/` laden und mit `dpkg -i` installieren.
|
||||
|
||||
⚠️ **NICHT vergessen:** Minimale Debian-13-Installationen haben oft kein `cron`-Paket (`crontab: command not found`) — `apt install -y cron && systemctl enable --now cron` vor dem ersten `crontab -e`.
|
||||
|
||||
`/etc/telegraf/telegraf.conf`: `outputs.influxdb_v2` auf `http://<CT104-Tailscale-IP>:8086`, Bucket `raw`, Org `homelab`; Inputs `cpu`, `mem`, `disk`, `net`, `system`.
|
||||
|
||||
## 6. HDD-SMART-Tracking (PVE-Host)
|
||||
|
||||
`/usr/local/bin/hdd-smart-to-influx.sh` liest `smartctl -A` für beide Platten (Attribute `Load_Cycle_Count`, `Start_Stop_Count`, `Temperature_Celsius` — Raw-Wert ist das erste Feld vor evtl. Klammertext), `zfs list -H -o name,used,avail,refer -p` für `nas`/`nas/freigabe`/`nas/backups` (Measurement `zfs_usage`, `/` im Dataset-Namen zu `_` normalisiert für den Tag-Wert) **und** NVMe-Belegung (Measurement `nvme_usage`) — die VG `pve` deckt praktisch die gesamte Intenso-NVMe ab (nur EFI-/Boot-Partitionen liegen außerhalb, vernachlässigbar). Schreibt alles per InfluxDB-Line-Protocol via `curl` in den `raw`-Bucket. systemd-Timer `hdd-smart-to-influx.timer`, alle 5 Minuten:
|
||||
|
||||
⚠️ **NICHT TUN:** Bei LVM-Thin-Provisioning (hier: LV `pve/data`, 137GB Pool für alle LXC-Disks) `vgs -o vg_size,vg_free` als "wie voll ist die Platte" interpretieren. Das misst nur, wie viel VG-Fläche noch **unallokiert** ist (also Platz für neue LVs) — nicht, wie viel der bereits allokierten LVs tatsächlich mit Daten beschrieben ist. Bei diesem Setup zeigte `vgs` 93% "belegt", obwohl real nur ~8% Daten auf der Platte lagen (root-LV zu 68GB allokiert, aber nur 6GB genutzt; Thin-Pool zu 137GB allokiert, aber nur 8,88% beschrieben). Für eine ehrliche Nutzungsanzeige: `df --output=used -B1 /` (root-FS) + `lvs -o lv_size,data_percent pve/data` (Thin-Pool-Ist-Nutzung) addieren → Feld `actual_used_gb`, getrennt von `total_gb`/`free_gb` (VG-Allokation, weiterhin nützlich um zu wissen, wie viel Raum für neue LVs übrig ist).
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/hdd-smart-to-influx.timer
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
```
|
||||
|
||||
Grafana-Panel "HDD Load-Cycle-Rate" (`derivative(unit: 1h, nonNegative: true)` auf `load_cycle_count`) ist die eigentliche Verifikation, ob ein Spindown-Timeout in der Praxis zu häufiges Spin-up/down verursacht.
|
||||
|
||||
⚠️ **NICHT TUN:** Bei Prozent-Berechnungen aus zwei InfluxDB-Feldern (z.B. `used_bytes / (used_bytes + avail_bytes)`) auf automatische Int→Float-Konvertierung verlassen. Line-Protocol-Felder mit `i`-Suffix sind in Flux `int`, eine Division mit einem Float-Literal (`* 100.0`) scheitert dann mit `type conflict: float != int`. Fix: `float(v: r.used_bytes) / float(v: r.used_bytes + r.avail_bytes) * 100.0`.
|
||||
|
||||
## 7. Grafana: Datasource + Dashboard per Provisioning
|
||||
|
||||
`/opt/monitoring/grafana-provisioning/datasources/influxdb.yaml` (InfluxDB2/Flux, fester `uid: influxdb-homelab`) und `/opt/monitoring/grafana-provisioning/dashboards/{provider.yaml,homelab.json}`, gemountet nach `/etc/grafana/provisioning`. Dashboard-JSON referenziert die Datasource über die feste `uid`, nicht per Name.
|
||||
|
||||
Dashboard-Struktur (per `"type": "row"`-Panels gruppiert, jede Zeile bewusst `"collapsed": false`): **Übersicht** (Esprimo/PVE-Host CPU+RAM als Graph + Intenso-NVMe frei/gesamt/Belegung-%, direkt darunter dasselbe Muster für den VPS: CPU+RAM als Graph + Root-Filesystem frei/gesamt/Belegung-%), **Ressourcen** (CPU/RAM/Speicherbelegung nur LXCs — PVE-Host und VPS bewusst nicht doppelt, stehen schon in der Übersicht), **Festplatten** (ZFS-Belegung in Bytes, HDD-Load-Cycle-Count/-Rate, Temperatur), **Logins & Zugriffe** (erfolgreiche/fehlgeschlagene Logins je Dienst, gestapelte Balken). `refresh: "1m"` auf Dashboard-Ebene gesetzt.
|
||||
|
||||
VPS-Speicherbelegung braucht kein neues Tracking — Telegrafs `disk`-Input-Plugin (bereits für die Ressourcen-Metriken konfiguriert, siehe Abschnitt 5) liefert `free`/`total`/`used_percent` für `/` schon fertig aufbereitet, kein manueller Prozent-Umweg wie bei NVMe/ZFS nötig.
|
||||
|
||||
**LXC-Speicherbelegung** (Panel "Speicherbelegung: LXCs", Ressourcen-Abschnitt): braucht kein neues Tracking-Script — Felder `disk`/`maxdisk` kommen schon über den PVE-Metric-Export im `system`-Measurement mit (Rootfs-Füllstand je Container, nicht zu verwechseln mit der NVMe- oder ZFS-Belegung).
|
||||
|
||||
⚠️ **NICHT TUN:** Bei Stat-Panels mit `pivot()`+`map()`-Berechnung (z.B. Prozent aus zwei Feldern) das Ergebnis ohne abschließendes `|> keep(columns: ["_time", "_value"])` zurückgeben. Grafana zeigt bei `reduceOptions.fields: ""` (Auto-Erkennung) sonst **jedes** numerische Feld der Tabelle als eigene Kachel-Zahl an — inklusive der Zwischenwerte (`used_bytes`, `avail_bytes`) neben dem eigentlichen `_value`. Gleiches Prinzip für Legenden bei Mehrserien-Timeseries-Panels: ohne `keep(columns: ["_time", "host", "_value"])` zeigt die Legende alle Tags (`nodename`, `object`, `vmid`, …) statt nur des relevanten (`host`).
|
||||
|
||||
⚠️ **NICHT TUN:** Bei Stat-Panels mit reinen Referenzzahlen (z.B. "frei (GB)", "gesamt (GB)") `colorMode: "value"` ohne explizite Thresholds stehen lassen. Grafanas Default-Threshold färbt ab Wert 80 automatisch rot — bei einer GB-Zahl wie 213,9 (viel freier Platz, eigentlich gut) sieht das wie ein Alarm aus, obwohl "hoch" hier positiv ist. Für reine Referenzzahlen `colorMode: "none"` setzen; Ampelfarben nur bei tatsächlich alarmwürdigen Metriken (z.B. Belegungs-Prozent, wo hoch = schlecht stimmt) verwenden.
|
||||
|
||||
⚠️ **NICHT vergessen:** `GF_SECURITY_ADMIN_PASSWORD` wirkt nur bei der **ersten** DB-Initialisierung. Ist Grafana schon einmal mit Default-Credentials (`admin`/`admin`) gestartet, hilft nur `docker exec grafana grafana cli admin reset-admin-password '<PW>'` (neuere Grafana-Versionen: `grafana cli`, nicht mehr das alte `grafana-cli`-Binary).
|
||||
|
||||
## 8. Uptime Kuma: Monitore + ntfy-Notification
|
||||
|
||||
Kein offizielles REST-API für Monitor-Verwaltung — Setup über die Python-Bibliothek `uptime-kuma-api` (nutzt das interne Socket.IO-Protokoll):
|
||||
|
||||
```bash
|
||||
python3 -m venv /opt/monitoring/kuma-venv
|
||||
/opt/monitoring/kuma-venv/bin/pip install uptime-kuma-api
|
||||
```
|
||||
|
||||
```python
|
||||
from uptime_kuma_api import UptimeKumaApi, MonitorType, NotificationType
|
||||
api = UptimeKumaApi('http://localhost:3001')
|
||||
api.setup('admin', '<PW>') # nur beim allerersten Mal
|
||||
api.login('admin', '<PW>')
|
||||
api.add_notification(name='ntfy-homelab', type=NotificationType.NTFY, isDefault=True,
|
||||
ntfyserverurl='http://ntfy:80', ntfytopic='homelab-alerts',
|
||||
ntfyPriority=4, ntfyAuthenticationMethod='none')
|
||||
api.add_monitor(type=MonitorType.HTTP, name='...', url='...', interval=60, maxretries=2)
|
||||
api.add_monitor(type=MonitorType.PING, name='...', hostname='...', interval=60, maxretries=2)
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** Sich auf `isDefault=True` bei `add_notification` verlassen, um die Notification automatisch an alle (auch später erstellte) Monitore zu hängen — greift über die API nicht zuverlässig. Stattdessen nach dem Anlegen explizit für jeden Monitor `api.edit_monitor(id, notificationIDList={'<notif_id>': True})` setzen.
|
||||
|
||||
⚠️ **NICHT TUN:** `retries=` als Kwarg für `add_monitor` verwenden — die Bibliothek erwartet `maxretries`, sonst `TypeError`.
|
||||
|
||||
Angelegte Monitore: 5× HTTP (Guacamole öffentlich, Vaultwarden/PVE-Web über Tailnet-Namen, AdGuard/Filebrowser über LAN-IP — bewusst IP statt `*.pve`-Name bei AdGuard, um keine Zirkularität zu erzeugen, falls AdGuard selbst der Ausfall ist), 6× Ping (VPS über Tailscale-IP, PVE-Host + 4 LXCs über LAN-IP).
|
||||
|
||||
## 9. ntfy: Erreichbarkeit + tailscale serve
|
||||
|
||||
```bash
|
||||
tailscale serve --bg http://localhost:8090
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** `https+insecure://localhost:8090` verwenden (das Muster aus `01-pve-host.md`/PVE-Webinterface) — dort war es richtig, weil das Backend (`pveproxy`) selbst HTTPS mit selbstsigniertem Zertifikat spricht. ntfy spricht intern nur **plain HTTP**; `https+insecure://` gegen einen HTTP-Backend führt zu `502 Bad Gateway`. Korrekt: `http://localhost:8090`.
|
||||
|
||||
Nach Einrichtung `NTFY_BASE_URL` in der Compose-Datei auf `https://monitoring.<TAILNET>` setzen und `docker compose up -d ntfy` (Recreate).
|
||||
|
||||
## 10. Backup-Cronjobs: Fehlschlag-Alerts
|
||||
|
||||
Bestehende Cronjobs (`vzdump`, `backup-guacamole.sh`, rsync-Pull) auf Wrapper-Skripte umgestellt, die bei Fehlschlag (`if ! <command>; then curl ntfy; fi` bzw. `trap ... ERR`) eine Push-Nachricht senden. Bewusst nur bei Fehlschlag, kein täglicher Erfolgs-Spam. Ersetzt die bis dahin wirkungslose `vzdump`-Mail-Benachrichtigung (Postfix auf dem PVE-Host ist `inet_interfaces = loopback-only`, kein Relay, kein root-Postfach — die Mail ging faktisch ins Leere).
|
||||
|
||||
## 11. Login-/Zugriffs-Tracking
|
||||
|
||||
Kleine Skripte (alle 5 Min. per Cron) statt eines Loki/Promtail-Log-Stacks — schreiben Erfolgs-/Fehlschlag-Zähler nach InfluxDB, pushen bei Schwellenwert (>3 Fehlschläge/5 Min.) einen ntfy-Alert. Zusätzlich pusht jedes Skript bei `SUCCESS -gt 0` eine niedrigprioritäre ntfy-Meldung für erfolgreiche Logins (eigener Block je Skript, `Priority: default` statt `high`, Tag `white_check_mark` statt `warning`).
|
||||
|
||||
⚠️ **Bekannte Rauschquelle:** Vaultwardens Clients nutzen denselben `/identity/connect/token`-Endpunkt auch für OAuth-Token-Refreshes, nicht nur für echte Logins — der Erfolgs-Alert kann dadurch öfter feuern als ein tatsächlicher neuer Login stattfindet. Bewusst so belassen (Nutzeranforderung), aber beim nächsten Mal ggf. auf `grant_type=password` in den Logs filtern, um echte Logins von Refreshes zu trennen.
|
||||
|
||||
| Dienst | Quelle |
|
||||
|---|---|
|
||||
| Guacamole (VPS) | Docker-Logs (`InMemoryAuthenticationFailureTracker`) + `guacamole_user_history`-Tabelle für Erfolge |
|
||||
| Vaultwarden (CT103) | Docker-Logs, `(login) POST /identity/connect/token => <Status>` |
|
||||
| PVE-Weboberfläche (Host) | `/var/log/pveproxy/access.log`, byte-offset-basiertes Tailing (kein natives `--since`) |
|
||||
| SSH (PVE-Host) | `journalctl -u ssh --since "5 min ago"` |
|
||||
| SSH/fail2ban (VPS) | `fail2ban-client status sshd` (Currently/Total failed/banned) — kein Log-Parsing nötig, fail2ban liefert die Zahlen direkt; Alert nur wenn „Currently banned" gegenüber letzter Messung steigt |
|
||||
| AdGuard-Web-UI (CT100) | **nicht umgesetzt** — AdGuard loggt fehlgeschlagene Web-UI-Logins nachweislich nicht (getestet: weder Journal noch mit `verbose`-Flag), trotz eingebautem Lockout (`auth_attempts: 5`, `block_auth_min: 15`). Bewusste, dokumentierte Lücke statt einer fragilen Behelfslösung |
|
||||
|
||||
**Update 2026-07-19 — Wer/IP statt nur Zahlen:** Alle fünf Skripte (außer fail2ban, das schon Zahlen liefert) hängen den echten Login-Kontext an den ntfy-Text an:
|
||||
- **SSH (Host):** `user@ip` per `sed -nE` aus `Accepted`/`Failed password`/`Invalid user`-Zeilen extrahiert
|
||||
- **PVE-Web:** Quell-IP (erstes Feld in `access.log`, `::ffff:`-Präfix entfernt)
|
||||
- **fail2ban:** von reinem Status-Polling auf `fail2ban.log`-Tailing (byte-offset) umgestellt — liefert die tatsächlich gesperrte(n) IP(s) aus den `NOTICE [sshd] Ban <ip>`-Zeilen statt nur "eine neue IP"
|
||||
- **Vaultwarden:** brauchte `IP_HEADER: "X-Forwarded-For"` in der Compose-Datei (Vaultwarden loggt IPs sonst gar nicht) — `tailscale serve` setzt den Header bereits automatisch, nur Vaultwarden vertraute ihm nicht. Danach erscheint bei Fehlschlägen `Username or password is incorrect... IP: X. Username: Y.` im Log, extrahierbar. Bei **Erfolg** bleibt die IP unverfügbar (dieser Log-Pfad existiert nur im Fehlerfall)
|
||||
- **Guacamole:** brauchte `REMOTE_IP_VALVE_ENABLED: "true"` in der Compose-Datei (Tomcats RemoteIpValve, liest `X-Forwarded-For` von Caddy). Vorher zeigte der Fehlschlag-Tracker nur die interne Docker-Gateway-IP (`172.18.0.1`), nach dem Fix die echte Client-IP. Bei **Erfolg** bleibt `guacamole_user_history.remote_host` trotzdem auf der internen Adresse — der DB-Verlauf nutzt offenbar einen anderen internen Pfad als der Live-Fehlschlag-Tracker, der die Valve-korrigierte Adresse honoriert. Nicht weiter debuggt (Aufwand/Nutzen), Username bei Erfolg ist trotzdem neu und nützlich
|
||||
|
||||
⚠️ **NICHT TUN:** `paste -sd', ' -` verwenden, um mehrere Werte mit `, ` zu verbinden. `paste -s -d LISTE` zykelt durch die **einzelnen Zeichen** der Delimiter-Liste statt sie als einen mehrzeichigen Trenner zu nutzen — bei 3+ Werten entsteht `a,b c,d e,f` (abwechselnd Komma und Leerzeichen) statt `a, b, c, d, e, f`. Fällt bei nur 1-2 Werten nicht auf (Zufallstreffer), erst bei mehreren wird der Bug sichtbar. Fix: `paste -sd',' - | sed 's/,/, /g'`.
|
||||
|
||||
## 12. HDD-Tracking hat sich selbst sabotiert (kritischer Fund)
|
||||
|
||||
Das 5-Minuten-Tracking-Script (`hdd-smart-to-influx.sh`) weckte mit jedem Lauf die schlafenden Platten — `smartctl -A` liest per Default auch bei einer Platte im Standby, was sie aufweckt. Da Check-Takt (5 Min.) und Spindown-Timeout (5 Min., siehe `01-pve-host.md`) identisch sind, hat das Monitoring den gerade erst eingerichteten Spindown fast vollständig ausgehebelt — bewiesen durch manuellen Test (Platte per `hdparm -y` schlafen gelegt, `smartctl -A` drüber, Status sofort wieder `active/idle`).
|
||||
|
||||
**Fix:** `smartctl -n standby -A <dev>` — überspringt (ohne Weckvorgang) die Abfrage, wenn die Platte schon schläft; Skript erkennt das am Exit-Code/der Meldung `Device is in STANDBY mode` und schreibt für diesen Durchlauf einfach keinen Datenpunkt für die schlafende Platte (InfluxDB/Grafana kommen mit Lücken problemlos klar).
|
||||
|
||||
⚠️ **NICHT TUN:** `smartctl -A` (ohne `-n standby`) in einem Monitoring-Script verwenden, das eine Platte im Standby beobachten soll — das ist ein Widerspruch in sich, das Skript verhindert genau das, was es messen will.
|
||||
|
||||
## 13. Esprimo-Hardware-Sensoren (CPU-Temp + Lüfter)
|
||||
|
||||
Fujitsu-eigener Sensorchip, nicht automatisch aktiv:
|
||||
```bash
|
||||
apt install -y lm-sensors
|
||||
yes | sensors-detect --auto
|
||||
echo -e "ftsteutates\ncoretemp" > /etc/modules-load.d/ftsteutates.conf
|
||||
modprobe ftsteutates coretemp
|
||||
```
|
||||
Liefert `coretemp-isa-0000` (`Package id 0` = CPU-Temperatur) und `ftsteutates-i2c-0-73` (`fan2` = einziger belegter Lüfterkanal von 8, Rest `FAULT`/nicht verbaut). Werte per `sensors <chip>` parsen, ins `hdd-smart-to-influx.sh`-Script integriert (Measurement `esprimo_hw`, Felder `cpu_temp_c`/`fan_rpm`) — läuft im selben 5-Min-Timer mit statt einen eigenen zu brauchen.
|
||||
|
||||
Grafana-Panel kombiniert beide Metriken in einer Kachel mit zwei Y-Achsen (Temperatur links, RPM rechts, per `byFrameRefID`-Field-Override) — zeigt auf einen Blick, ob und wann der Lüfter bei steigender Last einsetzt (Ziel: möglichst viel passive Kühlung).
|
||||
|
||||
## 14. Load-Cycle-Verifikation: tägliche Momentaufnahme statt Dauerkurve
|
||||
|
||||
Zusätzlich zur laufenden 5-Min-Erfassung (jetzt korrekt, siehe Punkt 12) schreibt `vzdump-backup.sh` einmal täglich (03:00, während die Platten wegen des Snapshots ohnehin wach sind — kein zusätzlicher Weckvorgang) den rohen `Load_Cycle_Count` nach InfluxDB. Grafana-Panel "Differenz zum Vortag":
|
||||
```flux
|
||||
from(bucket: "aggregated")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r._measurement == "smart" and r._field == "load_cycle_count")
|
||||
|> aggregateWindow(every: 1d, fn: last, createEmpty: false)
|
||||
|> difference(nonNegative: true)
|
||||
```
|
||||
Beantwortet direkt "wie viele Spin-ups gab es seit gestern" — eindeutiger als die stündliche Rate-Kurve, die bei Datenlücken (Platte war lange am Stück wach oder lange am Stück im Standby) leicht misszuinterpretieren ist.
|
||||
|
||||
## Guacamole-SFTP: bekanntes, ungelöstes Problem
|
||||
|
||||
Die SFTP-Begleitverbindung (Datei-Transfer-Feature, `enable-sftp`) der VNC-Verbindung "Management-Desktop (Browser)" ist aktuell **deaktiviert** (`enable-sftp: false` in der DB). Ursache war zunächst ein reines Passwort-Drift zwischen dem in Guacamole hinterlegten Passwort und dem tatsächlichen System-Passwort des `transfer`-Users auf CT102 (behoben, `chpasswd`) — aber selbst mit korrektem Passwort scheiterte `guacd`s eingebauter SSH-Client (`libssh2`) weiterhin an der Authentifizierung, während derselbe Login mit dem normalen OpenSSH-Client (von PVE-Host **und** vom VPS aus, exakt derselbe Netzwerkpfad wie `guacd`) anstandslos funktionierte. Deutet auf eine Algorithmus-/Protokoll-Inkompatibilität zwischen `libssh2` und CT102s OpenSSH-Server hin (z.B. moderne KEX-Defaults, die `libssh2` nicht unterstützt), nicht auf ein Credential-Problem.
|
||||
|
||||
VNC selbst funktioniert einwandfrei (davon unabhängig). Für eine vollständige Lösung: `sshd_config` auf CT102 um ältere, `libssh2`-kompatible `KexAlgorithms`/`HostKeyAlgorithms` erweitern (Kompatibilitäts-Fallback, nicht global schwächen) und erneut mit aktiviertem `enable-sftp` testen — noch nicht gemacht (Priorität war, den Nutzer schnell wieder reinzulassen).
|
||||
|
||||
## Referenz: Zugänge
|
||||
|
||||
| Dienst | URL | Hinweis |
|
||||
|---|---|---|
|
||||
| Grafana | `http://<LAN_IP_MONITORING>:3000` | Admin-Passwort in Vaultwarden |
|
||||
| Uptime Kuma | `http://<LAN_IP_MONITORING>:3001` | Admin-Passwort in Vaultwarden |
|
||||
| InfluxDB | `http://<LAN_IP_MONITORING>:8086` | Admin-Passwort + Token in Vaultwarden |
|
||||
| ntfy (Push) | `https://monitoring.<TAILNET>`, Topic `homelab-alerts` | Android/iOS-App auf den self-hosted Server zeigen lassen |
|
||||
| NAS-Backups (SMB) | `\\nas.pve\backups` | Read-only, gleicher Nutzer `<PRIMARY_USER>` wie `freigabe` |
|
||||
240
09-gast-zugang.md
Normal file
240
09-gast-zugang.md
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# Gast-Zugang: Read-only Showcase für Freunde/Bekannte und Bewerbungsgespräche — Rebuild-Runbook
|
||||
|
||||
Cross-cutting, kein eigener Host: nutzt CT101 (NAS), CT104 (Monitoring) und VPS (Caddy). Die Sicherheitsdiskussion, die zu diesem Design geführt hat: siehe `ENTSCHEIDUNGEN.md`, Abschnitt „Warum der Gast-Zugang eine eigene, komplett neue Oberfläche bekam".
|
||||
|
||||
## Prinzip
|
||||
|
||||
Zwei komplett read-only Showcase-Konten (`gast-freunde`, `gast-bewerbung`), standardmäßig **deaktiviert**, manuell vor einem Termin aktiviert (plus optionales Ablaufzeit-Sicherheitsnetz). Kein Terminal, kein Schreibzugriff, keine Sicht auf interne Login-/Zugriffs-Daten. Bewusst **kein** Zugriff auf den bestehenden Management-Desktop (CT102) — der hat ein Terminal und sitzt im flachen LAN, für Gäste zu riskant.
|
||||
|
||||
Drei Bausteine:
|
||||
1. **Grafana-Gast-Dashboard** (CT104) — kuratierte Kopie ohne Login-Daten, plus fail2ban-Statistik als Sicherheits-Showcase
|
||||
2. **Zweite Filebrowser-Instanz** (CT101) — eigener Port/eigene DB, read-only, zeigt nur `/srv/gast-demo` (= die genericierten Portfolio-Runbooks)
|
||||
3. **Uptime-Kuma-Status-Page** (CT104) — öffentliche Ampel-Ansicht ohne technische Details
|
||||
|
||||
Alle drei über neue öffentliche Caddy-Vhosts auf dem VPS erreichbar (gleiches Muster wie `guac.<DOMAIN>`).
|
||||
|
||||
## 1. Grafana: Ordner-getrenntes Gast-Dashboard
|
||||
|
||||
Grafanas eingebaute Rolle „Viewer" reicht **nicht** als Isolation — ein Viewer sieht standardmäßig alle Dashboards der Org. Für echte Trennung: zwei Ordner mit gebrochener Berechtigungs-Vererbung.
|
||||
|
||||
```bash
|
||||
# Dashboards in getrennte Unterordner der Provisionierung legen
|
||||
mkdir -p /opt/monitoring/grafana-provisioning/dashboards/intern /opt/monitoring/grafana-provisioning/dashboards/gast
|
||||
mv .../dashboards/homelab.json .../dashboards/intern/
|
||||
mv .../dashboards/homelab-gast.json .../dashboards/gast/
|
||||
```
|
||||
|
||||
`provider.yaml` mit zwei Providern (je eigenem `folder:` und `path:`), danach `docker compose restart grafana` (reine Datei-Provisionierung reicht sonst nicht für neue Ordner-Zuordnung).
|
||||
|
||||
Anschließend Ordner-Berechtigungen umbauen (klassische Permissions-API, **nicht** die neue `/api/access-control/...`-RBAC-API — letztere zeigt zwar den Ist-Zustand an, ist aber für Änderungen umständlicher):
|
||||
|
||||
```bash
|
||||
# Intern: Viewer-Rolle komplett entfernen -> kein Viewer-Konto sieht das je automatisch
|
||||
curl -u admin:$PW -X POST http://localhost:3000/api/folders/$INTERN_UID/permissions \
|
||||
-d '{"items":[{"role":"Editor","permission":2}]}'
|
||||
|
||||
# Gast: Viewer-Rolle raus, stattdessen nur die zwei konkreten User explizit erlauben
|
||||
curl -u admin:$PW -X POST http://localhost:3000/api/folders/$GAST_UID/permissions \
|
||||
-d '{"items":[{"role":"Editor","permission":2},{"userId":2,"permission":1},{"userId":3,"permission":1}]}'
|
||||
```
|
||||
|
||||
Nutzer anlegen (Rolle Viewer ist bei Admin-erstellten Usern bereits Default):
|
||||
|
||||
```bash
|
||||
curl -u admin:$PW -X POST http://localhost:3000/api/admin/users \
|
||||
-d '{"name":"Gast Freunde","login":"gast-freunde","password":"...","OrgId":1}'
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** sich auf `isDefault`/Standard-Ordner-Vererbung verlassen und nur ein Dashboard ohne Ordner-Trennung anlegen — ein Viewer-Account sieht dann trotzdem alle anderen Dashboards der Instanz mit (bestätigt: `curl -u gast-freunde ... /api/dashboards/uid/homelab-overview` gab vor der Ordner-Trennung 200 zurück, keine echte Isolation).
|
||||
|
||||
Deaktivieren/Aktivieren eines Users:
|
||||
```bash
|
||||
curl -u admin:$PW -X POST http://localhost:3000/api/admin/users/$ID/enable # bzw. /disable
|
||||
```
|
||||
|
||||
### Echte Hostnamen aus den Legenden entfernt (2026-07-20, Nutzerwunsch)
|
||||
|
||||
Die Ressourcen-Panels (CPU/RAM/Disk je LXC) wurden zunächst 1:1 aus dem internen Dashboard übernommen — die Grafana-Legende zeigt dabei den `host`-Tag aus InfluxDB, also die echten Container-Hostnamen (`adguard`, `nas`, `browser`, `vaultwarden`, `monitoring`) bzw. den PVE-Hostnamen (`<PVE_HOSTNAME>`). Auf Nutzerwunsch durch **Rollen-Bezeichnungen statt Servernamen** ersetzt — man soll erkennen können, *was* läuft (DNS-Blocking, NAS, Passwort-Manager, …), ohne die echten internen Namen zu erfahren. Umsetzung per Flux `map()`, das den `host`-Wert vor dem finalen `keep()` umschreibt:
|
||||
|
||||
```flux
|
||||
|> map(fn: (r) => ({ r with host:
|
||||
if r.host == "adguard" then "DNS / Ad-Blocking"
|
||||
else if r.host == "nas" then "NAS / Dateiablage"
|
||||
else if r.host == "browser" then "Remote-Desktop"
|
||||
else if r.host == "vaultwarden" then "Passwort-Manager"
|
||||
else if r.host == "monitoring" then "Monitoring-Stack"
|
||||
else r.host }))
|
||||
|> keep(columns: ["_time", "host", "_value"])
|
||||
```
|
||||
|
||||
Der PVE-Host selbst hat nur eine Zeile (kein Tag-Vergleich nötig): `map(fn: (r) => ({ r with host: "Hauptserver" }))`. Zusätzlich Panel-**Titel** generisiert, die Hardware-Marken/Dataset-Namen im Klartext hatten: „Esprimo (PVE-Host)" → „Hauptserver", „Intenso NVMe" → „NVMe-Speicher", „nas/freigabe"/"nas/backups" → „Dateiablage"/"Backup-Speicher", „VPS" → „VPN-Gateway". Das interne Dashboard (`Intern`-Ordner) bleibt bewusst unverändert mit den echten Namen — nur die Gast-Kopie wurde angepasst. Per Live-Query gegen InfluxDB verifiziert: alle fünf LXC-Rollen + Hauptserver lösen korrekt auf, keine rohen Hostnamen mehr in den Ergebnissen.
|
||||
|
||||
⚠️ **NICHT TUN:** Rollen-Namen so generisch wählen, dass der Showcase-Zweck verloren geht (z.B. "System A"/"System B"). Ziel war ausdrücklich, dass ein Betrachter weiterhin erkennt, *welche Art* Dienst läuft — nur der konkrete interne Name/Hostname soll verborgen bleiben.
|
||||
|
||||
## 2. Zweite Filebrowser-Instanz auf CT101
|
||||
|
||||
Eigener Systemd-Unit, eigene DB, eigener Port, eigener dedizierter Linux-User (kein Zugriff auf sonstige Homelab-Pfade — Defense in Depth für den einzigen wirklich öffentlich erreichbaren Datei-Endpunkt):
|
||||
|
||||
```bash
|
||||
useradd --system --no-create-home --shell /usr/sbin/nologin gastdemo
|
||||
mkdir -p /srv/gast-demo /var/lib/filebrowser-gast
|
||||
chown -R gastdemo:gastdemo /srv/gast-demo /var/lib/filebrowser-gast
|
||||
# Inhalt: die genericierten Portfolio-Runbooks (siehe Abschnitt 4)
|
||||
|
||||
runuser -u gastdemo -- filebrowser config init -d /var/lib/filebrowser-gast/filebrowser.db --root /srv/gast-demo
|
||||
runuser -u gastdemo -- filebrowser users add gast-freunde '...' -d /var/lib/filebrowser-gast/filebrowser.db \
|
||||
--perm.admin=false --perm.execute=false --perm.create=false --perm.rename=false \
|
||||
--perm.modify=false --perm.delete=false --perm.share=false --perm.download=true --scope /
|
||||
```
|
||||
|
||||
Systemd-Unit bindet **nur an die Tailscale-IP** (`<TS_IP_NAS>:8081`), nicht ans LAN — der einzige Zugriffsweg ist über den VPS-Caddy-Vhost + Tailscale, LAN-Nutzer erreichen die Instanz gar nicht:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
User=gastdemo
|
||||
Group=gastdemo
|
||||
ExecStart=/usr/local/bin/filebrowser -d /var/lib/filebrowser-gast/filebrowser.db -a <TS_IP_NAS> -p 8081
|
||||
Restart=on-failure
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** `--perm.execute` vergessen. Der `users add`-Default für Execute ist `true` (Command-Runner-Feature) — beim ersten Anlegen übersehen, per `users update ... --perm.execute=false` nachgezogen. Immer nach dem Anlegen mit `filebrowser users ls -d ...` verifizieren, dass wirklich nur `Download=true` gesetzt ist.
|
||||
|
||||
⚠️ **NICHT TUN:** die bestehende NAS-Freigabe read-only exponieren wollen. Ein Berechtigungs-Bug in der Konfiguration würde dann echte Daten zeigen. Stattdessen komplett eigenes Verzeichnis + eigene Instanz, die von Anfang an nichts anderes kennt.
|
||||
|
||||
**Bekannte Einschränkung:** Filebrowser hat in dieser Version **kein** Feld/Flag zum Deaktivieren einzelner Nutzer (anders als Grafana). Der „Aus"-Zustand wird deshalb über den ganzen Systemd-Service abgebildet (`systemctl stop filebrowser-gast`) — das deaktiviert zwangsläufig beide Gast-Logins gleichzeitig auf dieser Ebene. Da beide Konten ohnehin unabhängige Passwörter mit identischen Rechten haben, ist das funktional unkritisch, aber beachten: „nur `freunde` aktiv lassen" gilt nur für Grafana, nicht für die Dateifreigabe.
|
||||
|
||||
## 3. Caddy-Vhosts (VPS)
|
||||
|
||||
Gleiches Muster wie `guac.<DOMAIN>`, `/etc/caddy/Caddyfile`:
|
||||
|
||||
```
|
||||
grafana-gast.<DOMAIN> {
|
||||
reverse_proxy <TS_IP_MONITORING>:3000
|
||||
}
|
||||
dateien-gast.<DOMAIN> {
|
||||
reverse_proxy <TS_IP_NAS>:8081
|
||||
}
|
||||
status-gast.<DOMAIN> {
|
||||
reverse_proxy <TS_IP_MONITORING>:3001
|
||||
}
|
||||
```
|
||||
|
||||
`caddy validate --config /etc/caddy/Caddyfile` vor `systemctl reload caddy`. TLS-Zertifikate zieht Caddy automatisch, sobald die DNS-A-Records existieren (siehe Abschnitt 6).
|
||||
|
||||
## 4. Portfolio-Runbooks (Inhalt von `/srv/gast-demo`)
|
||||
|
||||
Genericierte Kopien aller `rebuild/*.md` in `../rebuild-portfolio/` — echte LAN-/Tailscale-IPs, Domain, Tailnet-Name, MAC-Adresse, Benutzername ersetzt durch Platzhalter (`<LAN_SUBNET>`, `<DOMAIN>`, `<TAILNET>`, `<PC1_MAC_ADDRESS>`, `<PVE_HOSTNAME>`, `<PRIMARY_USER>`, …). Erklärung der Platzhalter in `../rebuild-portfolio/VARIABLEN.md`. Debugging-Geschichten/„NICHT TUN"-Blöcke bleiben bewusst unverändert drin — nach der Anonymisierung nicht mehr auf die echte Instanz rückführbar, und genau das zeigt in einem Bewerbungsgespräch, dass reale Probleme selbst gefunden und gelöst wurden.
|
||||
|
||||
```bash
|
||||
sed -i -e 's/192\.168\.178\.10\b/<LAN_IP_PVE>/g' ... *.md # vollständige sed-Kette: siehe Session-Log
|
||||
```
|
||||
|
||||
Nach jeder Änderung an den echten `rebuild/*.md`-Dateien: Portfolio-Kopie manuell nachziehen (kein automatischer Sync) und erneut gegen die bekannten echten Werte grep-verifizieren.
|
||||
|
||||
## 5. Uptime Kuma: Gast-Monitore + Status-Page
|
||||
|
||||
Kein REST-API für Monitor-/Status-Page-Verwaltung — Python-Bibliothek `uptime-kuma-api` (Socket.IO-Client) nötig:
|
||||
|
||||
```python
|
||||
from uptime_kuma_api import UptimeKumaApi, MonitorType
|
||||
api = UptimeKumaApi('http://<TS_IP_MONITORING>:3001')
|
||||
api.login('admin', PASSWORD)
|
||||
api.add_monitor(type=MonitorType.HTTP, name='Grafana (Gast)', url='https://grafana-gast.<DOMAIN>',
|
||||
interval=60, retryInterval=60, maxretries=2, notificationIDList=[])
|
||||
api.add_status_page('gast', 'Homelab Showcase')
|
||||
api.save_status_page(slug='gast', title='Homelab Showcase', published=True, showPoweredBy=False,
|
||||
publicGroupList=[{'name': 'Showcase', 'weight': 1,
|
||||
'monitorList': [{'id': 12}, {'id': 13}, {'id': 14}]}])
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** den drei Gast-Monitoren die ntfy-Notification zuweisen (weder explizit noch über `isDefault`). Die Konten werden absichtlich manuell rauf-/runtergefahren — mit Notification würde jedes Deaktivieren einen „Down"-Alarm auslösen. `notificationIDList=[]` beim Anlegen explizit setzen.
|
||||
|
||||
### Zweite Gruppe: echte Infrastruktur-Telemetrie hinter Rollen-Namen (2026-07-20)
|
||||
|
||||
Die Status-Page wirkte mit nur drei Kacheln leer. Statt reiner Ampeln (Up/Down) zeigt eine zweite Gruppe „Infrastruktur" jetzt **echte Ping-Telemetrie** (Antwortzeit-Verlauf, Uptime-%) für die Kern-Systeme — mit denselben generischen Rollen-Namen wie im Gast-Grafana-Dashboard (siehe Abschnitt 1), damit beide Oberflächen konsistent wirken:
|
||||
|
||||
```python
|
||||
specs = [
|
||||
('Hauptserver', '<LAN_IP_PVE>'), ('DNS / Ad-Blocking', '<LAN_IP_ADGUARD>'),
|
||||
('NAS / Dateiablage', '<LAN_IP_NAS>'), ('Remote-Desktop', '<LAN_IP_DESKTOP>'),
|
||||
('Passwort-Manager', '<LAN_IP_VAULTWARDEN>'), ('Monitoring-Stack', '<LAN_IP_MONITORING>'),
|
||||
('VPN-Gateway', '<TS_IP_VPS>'),
|
||||
]
|
||||
for name, ip in specs:
|
||||
api.add_monitor(type=MonitorType.PING, name=name, hostname=ip,
|
||||
interval=60, retryInterval=60, maxretries=2, notificationIDList=[])
|
||||
```
|
||||
|
||||
Uptime Kuma zeigt auf der Status-Page nur den konfigurierten Namen + Antwortzeit-Graph, nicht die Ziel-IP — die echten LAN-IPs sind also nur intern für den Check relevant, nie öffentlich sichtbar. Zweite Gruppe per `publicGroupList` mit eigenem `weight` ergänzt (bestehende „Showcase"-Gruppe bleibt unverändert stehen, Gruppen werden per Name identifiziert und beim Speichern komplett ersetzt — beide Gruppen müssen bei jedem `save_status_page`-Aufruf mit angegeben werden, sonst verschwindet die andere).
|
||||
|
||||
Verifiziert über die öffentliche URL (`curl https://status-gast.<DOMAIN>/api/status-page/heartbeat/gast`): alle sieben neuen Monitore liefern echte Ping-Antwortzeiten (0,05–25 ms je nach Ziel), Gruppennamen und Monitor-Namen kommen unverändert als reine Rollen-Bezeichnungen an — keine IP/Hostname in der API-Antwort sichtbar.
|
||||
|
||||
**Uptime-Kuma-Admin-Passwort verloren gegangen:** Die ursprünglichen Zugangsdaten wurden nur im Chat einer früheren Session ausgegeben und nirgends dauerhaft abgelegt (Konvention: Passwörter gehören in Vaultwarden, das ist hier nicht passiert). Reset direkt in der SQLite-DB des laufenden Containers (Uptime Kuma bringt `node`+`bcryptjs`+`sqlite3` im Image mit):
|
||||
|
||||
```bash
|
||||
HASH=$(docker exec uptime-kuma node -e "const bcrypt=require('bcryptjs'); console.log(bcrypt.hashSync(process.argv[1], 10));" "$NEWPW")
|
||||
docker exec uptime-kuma sqlite3 /app/data/kuma.db "UPDATE user SET password = '$HASH' WHERE id = 1;"
|
||||
```
|
||||
|
||||
Funktioniert bei laufendem Container (SQLite/WAL erlaubt den kurzen Fremdzugriff für ein einzelnes UPDATE). **Lehre:** Admin-Zugangsdaten für alle Dienste dieser Session tatsächlich in Vaultwarden ablegen, nicht nur im Chat stehen lassen.
|
||||
|
||||
## 6. DNS (manueller Schritt, keine API verfügbar)
|
||||
|
||||
Drei A-Records im STRATO-Panel für `<DOMAIN>`, alle auf die VPS-IP (gleiche IP wie der bestehende `guac`-Eintrag):
|
||||
- `grafana-gast` → VPS-IP
|
||||
- `dateien-gast` → VPS-IP
|
||||
- `status-gast` → VPS-IP
|
||||
|
||||
⚠️ **NICHT TUN:** Caddy neu laden/starten, *bevor* alle A-Records korrekt gesetzt sind, und dann erwarten, dass es sich von selbst korrigiert. Certmagic (Caddys ACME-Bibliothek) merkt sich fehlgeschlagene Versuche pro Hostname und schaltet nach mehreren Fehlschlägen zur Schonung des Let's-Encrypt-Produktiv-Rate-Limits automatisch auf die **Staging-CA** um (liefert kein von Browsern akzeptiertes Zertifikat) plus einen Backoff von bis zu 20 Minuten zwischen Versuchen. Gleiches Muster wie beim ursprünglichen `guac`-Setup. Fix: erst alle DNS-Einträge verifizieren (`dig @1.1.1.1 <name>.<DOMAIN> A`, gegen mehrere öffentliche Resolver prüfen, eigener AdGuard-Resolver cached u.U. noch den alten Stand), danach `systemctl restart caddy` (nicht nur `reload` — ein Neustart setzt den internen Fehlversuch-/Backoff-Zustand zurück, ein reines `reload` nicht zuverlässig). Direkt danach in `journalctl -u caddy` auf `"certificate obtained successfully"` mit `ca":"https://acme-v02.api.letsencrypt.org/directory"` (nicht `-staging-`) prüfen.
|
||||
|
||||
**Stolperstein bei diesem Durchlauf:** ein Subdomain-Eintrag im STRATO-Panel landete zunächst auf `217.160.0.161` (STRATOs eigenem Parking-/Webhosting-Server) statt der eingetragenen Ziel-IP — vermutlich ein falsch vorausgewählter Eintragstyp im Formular (z.B. "STRATO-Webhosting" statt "eigene IP-Adresse"). Per `dig` direkt gegen die autoritativen STRATO-Nameserver (`docks14.rzone.de`/`shades02.rzone.de`) und mehrere öffentliche Resolver (1.1.1.1/8.8.8.8/9.9.9.9) geprüft, um Panel-Fehler von reiner Propagationsverzögerung zu unterscheiden — eine SOA-Antwort statt einer A-Antwort bedeutet "Eintrag existiert nicht", nicht "noch nicht propagiert". Ein Tippfehler in der Subdomain selbst (`datein-gast` statt `dateien-gast`) trat ebenfalls auf — beim Anlegen den tatsächlich in Caddy konfigurierten Namen exakt gegenprüfen.
|
||||
|
||||
## 7. Login-Tracking (Gast-Nutzung sichtbar machen)
|
||||
|
||||
Anders als bei Guacamole/Vaultwarden/PVE-Web/SSH ist bei den Gast-Oberflächen die Logausbeute schlechter:
|
||||
|
||||
- **Grafana:** loggt bei Formular-Login (`/login`, POST) **weder bei Erfolg noch bei Fehlschlag den Benutzernamen**. Ein Fehlschlag erzeugt `level=info msg="Failed to authenticate request" client=auth.client.form` + eine „Request Completed"-Zeile mit `path=/login status=401` — aber ohne Username, könnte auch ein vertippter Admin-Login sein. Ein Erfolg erzeugt **gar keine** Log-Zeile beim Login selbst; einzig nachfolgende authentifizierte Requests tragen `uname=gast-freunde`/`uname=gast-bewerbung` als Tag. `track-grafana-gast.sh` (Cron alle 5 Min. auf CT104) zählt deshalb Fehlschläge generisch und deutet „Aktivität" über das Vorkommen dieser `uname=`-Tags.
|
||||
- **Filebrowser:** loggt nur Fehlschläge, mit IP (`/api/login: 403 <ip> <err>`), keine Erfolge — gleiche Einschränkung wie beim bereits dokumentierten AdGuard-Gap. `track-filebrowser-gast.sh` (Cron alle 5 Min. auf CT101, no-op wenn der Service gerade gestoppt ist) trackt entsprechend nur `fail`.
|
||||
|
||||
Beide schreiben ins bestehende `logins`-Measurement (`service=grafana-gast`/`service=filebrowser-gast`), gleiches Muster wie die übrigen Tracker.
|
||||
|
||||
**ntfy-Push (2026-07-20 ergänzt):** `track-grafana-gast.sh` pusht bei >3 Fehlversuchen (hohe Priorität) und bei jeder erkannten Aktivität (`uname=`-Tag, Standard-Priorität — das deckt "erfolgreicher Login" für Grafana ab, da ein echtes Login-Event selbst nicht separat loggbar ist). `track-filebrowser-gast.sh` pusht nur bei >3 Fehlversuchen — ein Push bei erfolgreichem Filebrowser-Login ist **technisch nicht möglich**: kein Verbose-/Debug-Log-Flag vorhanden (`filebrowser -h` zeigt nur `-l/--log` für das Ausgabeziel, keine Log-Level-Option), das Tool loggt nachweislich nur Fehlschläge — exakt dieselbe Einschränkung wie bei AdGuard.
|
||||
|
||||
## 8. Aktivieren/Deaktivieren + Ablaufzeit-Sicherheitsnetz
|
||||
|
||||
Zwei Skripte auf dem PVE-Host (steuern CT104 + CT101 gemeinsam über `pct exec`):
|
||||
|
||||
```bash
|
||||
gast-aktivieren.sh freunde|bewerbung [stunden]
|
||||
gast-deaktivieren.sh freunde|bewerbung|alle
|
||||
```
|
||||
|
||||
`gast-aktivieren.sh` aktiviert den Grafana-User, startet `filebrowser-gast.service`, und plant bei Angabe von `[stunden]` per `at` einen einmaligen Folgejob (`gast-deaktivieren.sh <wer>`) — reines Sicherheitsnetz für den Fall, dass das manuelle Abschalten vergessen wird, kein Ersatz dafür. `gast-deaktivieren.sh` stoppt `filebrowser-gast.service` nur dann mit, wenn danach **kein** Gast-Grafana-Konto mehr aktiv ist (sonst würde das Deaktivieren von `freunde` auch `bewerbung`s Dateizugriff kappen, obwohl der ggf. noch laufen soll).
|
||||
|
||||
```bash
|
||||
apt-get install -y at && systemctl enable --now atd
|
||||
```
|
||||
|
||||
⚠️ **NICHT TUN:** `at`-Pakete für die Auto-Deaktivierung annehmen, ohne `atd` separat zu aktivieren — auf frisch installiertem Debian läuft der Dienst nach `apt-get install at` nicht automatisch.
|
||||
|
||||
### Automatisches Passwort-Cycling
|
||||
|
||||
Weder Grafana OSS noch Filebrowser kennen ein "Passwort muss nach Login neu gesetzt werden"-Flag — als praktisches Äquivalent erzeugt `gast-aktivieren.sh` bei **jeder** Aktivierung ein frisches Zufallspasswort (12 Zeichen, nur Buchstaben/Ziffern ohne verwechselbare Zeichen wie `0`/`O`/`1`/`l`/`I` — bewusst niedrige Komplexität, da es dem Gast mündlich/per Kurznachricht mitgeteilt werden soll) und setzt es für **beide** Dienste gleich, damit sich der Gast nur ein Passwort merken muss:
|
||||
|
||||
```bash
|
||||
NEWPASS=$(tr -dc 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789' < /dev/urandom | head -c 12 || true)
|
||||
curl -u admin:$GF_PASS -X PUT http://localhost:3000/api/admin/users/$ID/password -d "{\"password\":\"$NEWPASS\"}"
|
||||
filebrowser users update gast-freunde -d /var/lib/filebrowser-gast/filebrowser.db -p "$NEWPASS"
|
||||
```
|
||||
|
||||
Altes Passwort wird dadurch automatisch ungültig — keine manuelle Rotation nötig, das alte Passwort muss auch nirgends aufgehoben werden.
|
||||
|
||||
⚠️ **NICHT TUN:** `tr ... | head -c N` unter `set -o pipefail` verwenden, ohne den SIGPIPE-Fall abzufangen. `head` beendet die Pipe, sobald genug Bytes gelesen sind — `tr` bekommt dadurch SIGPIPE (Exit 141), was mit `pipefail` das ganze Skript abbricht. Fix: `... | head -c N || true`.
|
||||
|
||||
⚠️ **NICHT TUN:** das Filebrowser-Passwort per CLI setzen, während `filebrowser-gast.service` bereits läuft. Filebrowsers bbolt-Datenbank erlaubt nur einen Prozess gleichzeitig — CLI und laufender Dienst kollidieren mit `Error: timeout`. Reihenfolge im Skript: Service stoppen → Passwort per CLI setzen → Service starten.
|
||||
|
||||
Verifiziert: `gast-aktivieren.sh freunde` → Grafana-Login + Filebrowser-Login funktionieren; `gast-deaktivieren.sh freunde` → beides sofort wieder zu; `at`-Job mit 1 Minute Vorlauf → `gast-deaktivieren.sh` feuert zuverlässig automatisch. Nach dem DNS-/Zertifikats-Fix zusätzlich vollständig über die echten öffentlichen URLs (nicht nur intern/Tailnet) durchgetestet: Login bei aktiviertem Konto funktioniert über `https://grafana-gast.<DOMAIN>` und `https://dateien-gast.<DOMAIN>`, zeigt nachweislich nur das Gast-Dashboard, Status-Page lädt; nach Deaktivierung liefern beide öffentlichen URLs wieder 401/502. Passwort-Rotation getestet: nach zweiter Aktivierung meldet das alte Passwort 401, nur das neu ausgegebene funktioniert — für beide Konten (`freunde`/`bewerbung`) einzeln bestätigt.
|
||||
|
||||
### ntfy-Push bei Aktivieren/Deaktivieren (2026-07-20)
|
||||
|
||||
Beide Skripte schließen jetzt mit einem `curl` an `http://localhost:8090/homelab-alerts` (via `pct exec 104`, da ntfy auf CT104 läuft) ab — Standard-Priorität, kein Alarm-Ton, reine Statusmeldung ("Gast-Zugang 'freunde' wurde soeben aktiviert (Auto-Deaktivierung in 3h)." bzw. "... deaktiviert."). Über `curl "http://localhost:8090/homelab-alerts/json?poll=1&since=2m"` gegen echte Zustellung verifiziert (nicht nur, dass der `curl`-Aufruf ohne Fehler durchläuft).
|
||||
16
ACHTUNG-DISCLAIMER.md
Normal file
16
ACHTUNG-DISCLAIMER.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Achtung: KI-unterstützt entstandene Dokumentation
|
||||
|
||||
Die Runbooks in diesem Ordner (`00`–`09`, `VARIABLEN.md`, `ENTSCHEIDUNGEN.md`) sind **nicht nachträglich für diese Freigabe geschrieben** — sie sind ein Mitschnitt des tatsächlichen Aufbau-Prozesses eines echten Homelabs, entstanden in Zusammenarbeit mit [Claude Code](https://claude.com/claude-code), Anthropics KI-gestütztem Kommandozeilen-Werkzeug für Software-/Infrastrukturarbeit.
|
||||
|
||||
## Was das konkret bedeutet
|
||||
|
||||
- Jeder Befehl in diesen Dateien wurde **tatsächlich so ausgeführt**, live, während des Aufbaus — keine nachträglich glattgebügelte Reinschrift.
|
||||
- Die „⚠️ NICHT TUN"-Hinweise sind echte Stolpersteine, auf die der Aufbau tatsächlich gelaufen ist (falsche Protokoll-Wahl, Feldtyp-Kollisionen, Boot-Reihenfolge-Fallstricke, SIGPIPE-Bugs in eigenen Skripten, …) — nicht erfundene Lehrbuch-Beispiele.
|
||||
- Architektur-Entscheidungen wurden im Dialog getroffen: der Nutzer bringt Ziel, Kontext und Rückfragen ein, die KI schlägt Optionen samt Trade-offs vor, der Nutzer entscheidet. `ENTSCHEIDUNGEN.md` fasst diese Begründungen zusammen.
|
||||
- Echte Namen, IP-Adressen, die eigene Domain und andere identifizierende Details wurden nachträglich durch Platzhalter ersetzt (siehe `VARIABLEN.md`) — das ist die einzige nachträgliche Änderung an den Inhalten. Struktur, Reihenfolge und alle Fehlersuche-Geschichten sind unverändert.
|
||||
|
||||
## Warum das hier offen steht
|
||||
|
||||
Weil es zum eigentlichen Punkt gehört: der Umgang mit einem KI-Werkzeug wie diesem — Aufgaben klar formulieren, Ergebnisse kritisch prüfen, nachfragen, korrigieren, am Ende die Verantwortung für das Ergebnis behalten — ist selbst eine Fähigkeit. Diese Dokumentation zu verstecken oder so zu tun, als sei jede Zeile von Hand entstanden, würde diesen Teil der Arbeit unterschlagen.
|
||||
|
||||
Die technischen Entscheidungen, die Fehlersuche und das Ergebnis sind trotzdem real — es lief ein echtes System, mit echten Bugs, die echt gelöst werden mussten.
|
||||
64
ENTSCHEIDUNGEN.md
Normal file
64
ENTSCHEIDUNGEN.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Architektur-Entscheidungen und ihre Begründung
|
||||
|
||||
Die Runbooks (`00`–`09`) zeigen *wie* jede Instanz aufgebaut wird. Diese Datei erklärt *warum* — die Kompromisse und Alternativen, die vor jeder Entscheidung abgewogen wurden. Genericierte Zusammenfassung des internen Projekt-Kontexts (der im Original nicht Teil dieser Freigabe ist, siehe `ACHTUNG-DISCLAIMER.md`).
|
||||
|
||||
## Ausgangslage: kein öffentliches IPv4 zuhause
|
||||
|
||||
Der Internet-Anschluss läuft über Dual-Stack-Lite (DS-Lite) — nur natives IPv6 + eine geteilte, nicht erreichbare IPv4-Adresse (CGNAT) ausgehend. Das bestimmt die halbe Architektur: ein rein lokales Setup wäre von außen gar nicht erreichbar. Zwei Konsequenzen:
|
||||
|
||||
- **Tailscale** (Mesh-VPN) für den Normalfall — jedes eigene Gerät mit installiertem Client kommt direkt ins Heimnetz, unabhängig von der eigenen öffentlichen IP
|
||||
- **Ein VPS mit echter öffentlicher IPv4/IPv6** als Fallback für den Fall, dass kein Tailscale-Client verfügbar ist (fremder Rechner, Internetcafé) — bewusst nur als schmale Bridge, keine große Konfiguration dort
|
||||
|
||||
## Warum Tailscale statt WireGuard standalone
|
||||
|
||||
Tailscale nutzt WireGuard als Transportprotokoll, übernimmt aber Schlüsselverwaltung, NAT-Traversal und Gerätediscovery (MagicDNS) automatisch. Ein eigenständiges WireGuard-Setup hätte denselben Zweck erfüllt, aber jeden dieser Punkte manuell verlangt — für ein Ein-Personen/Familien-Setup ohne Compliance-Anforderungen ist der Zusatzaufwand nicht gerechtfertigt. Bewusst **kein Exit-Node** (der gesamte Internet-Traffic mobiler Geräte würde sonst über die eigene, langsamere Heimleitung laufen) — nur Subnet-Routing ins Heimnetz.
|
||||
|
||||
## Warum AdGuard Home als aller erster Schritt
|
||||
|
||||
DNS-basiertes Werbe-/Tracker-Blocking ersetzt Browser-Adblocker zuverlässiger, seit Browser-Erweiterungs-APIs (Manifest V3) deren Möglichkeiten eingeschränkt haben — und wirkt netzwerkweit für jedes Gerät, auch IoT/Smart-TV ohne Adblocker-Unterstützung. Es kommt bewusst vor allen anderen Diensten, weil jede spätere Instanz ohnehin über diesen DNS-Server auflösen soll (Rewrites für interne Kurznamen).
|
||||
|
||||
**Filterlisten-Philosophie:** kuratiert statt maximal. Ein Abgleich mit einem deutlich umfangreicheren Fremd-Setup (~60 aktive Listen) zeigte massive Redundanz (mehrere überlappende Malware-/Werbelisten) ohne messbaren Zusatznutzen — mehr Listen bedeuten mehr RAM-Bedarf und mehr False-Positive-Risiko, nicht automatisch mehr Sicherheit. Bewusst **nicht** übernommen: Content-Filter (NSFW/Anti-Piracy/Dating — reine Lifestyle-Entscheidung, nicht Ad-Blocking-Zweck) und VPN/Proxy-Bypass-/DynDNS-Blocklisten (Kollisionsrisiko mit einem im Haushalt genutzten Firmen-VPN-Tunnel eines Familienmitglieds).
|
||||
|
||||
## Warum ZFS-Mirror statt TrueNAS-VM für den NAS-Speicher
|
||||
|
||||
Mit nur zwei verfügbaren Laufwerksschächten scheidet ein Storage-Passthrough-Setup mit mehr Redundanzstufen praktisch aus. Optionen waren: (a) einfacher Samba-LXC ohne Redundanz, (b) TrueNAS Scale als VM mit Storage-Passthrough, (c) ZFS-Mirror auf Host-Ebene + schlanker Samba-LXC per Bind-Mount. Entschieden für (c) — liefert dieselbe Redundanz (RAID 1) wie eine dedizierte NAS-Lösung, ohne den Ressourcen-Overhead einer vollständigen TrueNAS-VM für nur zwei Platten zu rechtfertigen.
|
||||
|
||||
**NAS-Protokolle grundsätzlich nie öffentlich exponieren** — nur über den Tailscale-Subnet-Router erreichbar. SMB/NFS sind nicht für Angriffsverkehr aus dem offenen Internet gebaut; das öffentliche Zugriffsszenario läuft stattdessen über das Gateway (nächster Abschnitt).
|
||||
|
||||
## Warum ein zusätzliches Gateway statt NAS direkt öffentlich
|
||||
|
||||
Für den seltenen Fall eines fremden Geräts ohne Tailscale-Client übernimmt ein HTML5-Gateway auf dem VPS die Vermittlung (RDP/SFTP/VNC-Verbindungen im Browser, mit eigenem Login + Pflicht-2FA). Das hält den öffentlichen Angriffsfläche auf **einen** gehärteten Übergang beschränkt, statt mehrere Dienste (NAS, Management-Desktop, …) einzeln absichern zu müssen. Public-Key-only-SSH + `fail2ban` zusätzlich auf dem VPS selbst, da dort der einzige öffentlich erreichbare Admin-Zugang liegt.
|
||||
|
||||
## Warum ein separater "Management-Desktop"-Container
|
||||
|
||||
Ursprünglich nicht geplant — entstand aus einem konkreten Bedarf: ein zweites Haushaltsmitglied brauchte grafischen Zugriff (v.a. auf die Datei-Oberfläche), ohne die bestehende Fernzugriffs-Sitzung auf den Haupt-PC zu kapern (Windows erlaubt dort nur eine aktive Sitzung gleichzeitig). Lösung: ein minimaler LXC mit Desktop-Umgebung + Browser, rein für Web-Oberflächen gedacht ("Mini-Linux, das nur einen Browser können muss") — kein Vollzugriffs-Arbeitsplatz, kein Terminal für Gäste (siehe Gast-Zugang unten, warum das wichtig wurde).
|
||||
|
||||
## Warum der Passwort-Tresor NIE öffentlich
|
||||
|
||||
Höchste Sensitivität aller Dienste — bekommt bewusst als einziger Dienst **keinen** öffentlichen Zugriffsweg, nur Tailnet-intern über automatisches Zertifikats-Handling (statt eines eigenen Reverse-Proxys — weniger bewegliche Teile für den sensibelsten Dienst). Selbst bei einer hypothetischen Kompromittierung des öffentlichen Gateways bliebe der Tresor unerreichbar.
|
||||
|
||||
## Warum Monitoring: zweistufige Datenhaltung statt einer großen Datenbank
|
||||
|
||||
Die einzige verfügbare Storage für den Monitoring-Container ist eine nicht sehr große NVMe (die Festplatten sind fürs allgemeine Stromsparziel tabu, siehe unten) — unbegrenzt wachsende Rohdaten waren keine Option. Fix: kurze Rohdaten-Aufbewahrung (Tage) für Detail-Debugging, parallel dazu automatisch verdichtete Stundenmittelwerte über Jahre für Langzeit-Trends. Ergebnis: dauerhaft kleine Datenmenge, ohne auf Verlaufsvergleiche über Monate verzichten zu müssen.
|
||||
|
||||
**Kein Agent pro Container** — die Virtualisierungsplattform bringt einen eingebauten Metrik-Export für Host + alle Gastsysteme bereits mit, das spart einen zusätzlichen Prozess pro Instanz. Nur das externe VPS (kein Mitglied der Virtualisierungs-Cluster) bekommt einen schlanken externen Agenten.
|
||||
|
||||
**Selbstgehostete Push-Benachrichtigungen statt E-Mail** — der ursprüngliche Plan (Warnmails bei Backup-Fehlschlägen) scheiterte daran, dass der Mailversand vom Host aus nirgendwo ankam (kein Mail-Relay konfiguriert, nur lokal zustellbar). Ein selbstgehosteter Push-Dienst aufs eigene Handy war der pragmatischere Ersatz, ganz ohne Abhängigkeit von einem externen Mail-Anbieter.
|
||||
|
||||
## Warum Backups per Pull vom Zuhause statt Push vom externen Server
|
||||
|
||||
Der externe Server (VPS) erstellt seine eigenen Backups lokal, aber das Heimnetz **zieht** sie sich anschließend selbst ab (per stark eingeschränktem Schlüssel, nur Lesezugriff auf genau ein Verzeichnis) — nicht umgekehrt. Vorteil: die eigentliche Datenhoheit bleibt beim eigenen Netzwerk, und ein späterer Wechsel des externen Anbieters oder der Domain bleibt unkritisch (einfach vor der Kündigung den letzten Stand ziehen). Bewusst kein Push-Zugriff vom VPS auf das Heimnetz — ein kompromittierter öffentlicher Server hätte sonst Schreibzugriff auf die eigene Backup-Infrastruktur.
|
||||
|
||||
## Warum Festplatten-Spindown ein echtes Projekt wurde, nicht nur ein Einzeiler
|
||||
|
||||
Der Wunsch war simpel: Festplatten sollen möglichst selten aktiv sein statt dauerhaft zu laufen. Die Umsetzung eines aggressiven Spindown-Timeouts warf aber eine echte Frage auf: sind häufige Start/Stop-Zyklen schädlicher für die Plattenlebensdauer als dauerhafter Betrieb? Eine SMART-Auswertung zeigte, dass eine der beiden Platten bereits einen sehr hohen Load-Cycle-Zählerstand hatte (nahe der vom Hersteller spezifizierten Zyklengrenze), während die zweite, baugleiche Platte einen deutlich niedrigeren Wert zeigte — das machte Monitoring dieser Kennzahl zur Voraussetzung für die Entscheidung, nicht zum Nice-to-have. Details zur Fehlersuche (inkl. eines Bugs, bei dem das eigene Monitoring-Skript die Platten unbeabsichtigt bei jedem Check aufweckte) stehen in `08-monitoring.md`.
|
||||
|
||||
## Warum der Gast-Zugang eine eigene, komplett neue Oberfläche bekam
|
||||
|
||||
Naheliegend wäre gewesen, den bestehenden Management-Desktop für Besucher freizugeben. Bewusst verworfen: der hat ein Terminal und sitzt im selben flachen Netzwerk wie alle anderen Geräte — für Familie unkritisch, für Fremde (Bewerbungsgespräch-Kontext!) ein echtes Risiko. Stattdessen eine komplett separate, rein lesende Oberfläche (kuratiertes Dashboard, separate Dateifreigabe mit eigens dafür vorbereiteten Inhalten, öffentliche Status-Seite) — lieber ein zweites, bewusst eingeschränktes System bauen als ein bestehendes privilegiertes System nur "ein bisschen" zu öffnen. Details, inkl. der Sicherheitsdiskussion, die zu diesem Design geführt hat: `09-gast-zugang.md`.
|
||||
|
||||
## Bewusst verworfene Ansätze (nicht aus Unwissenheit, sondern nach Abwägung)
|
||||
|
||||
- **Eigenständige Firewall-Appliance inline** — keine passende Hardware für einen dedizierten Router-Ersatz vorhanden, kein Neukauf gerechtfertigt für den erreichten Sicherheitsgewinn
|
||||
- **Cloud-Dokumentenverwaltung mit öffentlichem Reverse-Proxy** — zu viel Betriebsaufwand für reinen Eigenbedarf, das bestehende Mesh-VPN deckt den Zugriffsbedarf bereits ab
|
||||
- **DNS-Anfragen über den Router statt direkt zum Ad-Blocking-Server** — ein zusätzlicher Hop ohne Mehrwert, der Router wird komplett aus dem DNS-Pfad herausgehalten
|
||||
27
VARIABLEN.md
Normal file
27
VARIABLEN.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Variablen in diesen Runbooks
|
||||
|
||||
Diese Dateien sind generische Kopien der tatsächlich verwendeten Rebuild-Runbooks — echte IP-Adressen, die eigene Domain, der Tailnet-Name, die MAC-Adresse und Benutzernamen wurden durch Platzhalter ersetzt. Struktur, Reihenfolge und alle "NICHT TUN"-Stolpersteine sind unverändert und tatsächlich so aufgetreten — nur die konkreten Werte gehören zur Originalinstanz und müssten durch eigene ersetzt werden, um das Setup nachzubauen.
|
||||
|
||||
| Platzhalter | Bedeutung | Beispiel |
|
||||
|---|---|---|
|
||||
| `<LAN_SUBNET>` | Das eigene LAN-Subnetz | `192.168.1.0/24` |
|
||||
| `<LAN_IP_ROUTER>` | IP des Heimrouters | `192.168.1.1` |
|
||||
| `<LAN_IP_ADGUARD>` | IP des DNS-/Ad-Blocking-Containers | `192.168.1.9` |
|
||||
| `<LAN_IP_PVE>` | IP des Proxmox-Hosts | `192.168.1.10` |
|
||||
| `<LAN_IP_NAS>` | IP des NAS-Containers | `192.168.1.11` |
|
||||
| `<LAN_IP_DESKTOP>` | IP des Management-Desktop-Containers | `192.168.1.12` |
|
||||
| `<LAN_IP_VAULTWARDEN>` | IP des Passwortmanager-Containers | `192.168.1.13` |
|
||||
| `<LAN_IP_MONITORING>` | IP des Monitoring-Containers | `192.168.1.14` |
|
||||
| `<LAN_IP_PC1>` | IP des Windows-Rechners (Wake-on-LAN-Ziel) | `192.168.1.254` |
|
||||
| `<LAN_BROADCAST>` | Broadcast-Adresse des LAN-Subnetzes | `192.168.1.255` |
|
||||
| `<DOMAIN>` | Die eigene registrierte Domain | `meinhomelab.example` |
|
||||
| `<TAILNET>` | Der eigene Tailscale-Tailnet-Name (MagicDNS-Suffix) | `tailXXXXXX.ts.net` |
|
||||
| `<TS_IP_*>` | Jeweilige Tailscale-IP der Instanz (vergibt Tailscale automatisch beim Beitritt) | `100.x.x.x` |
|
||||
| `<PC1_MAC_ADDRESS>` | MAC-Adresse des Wake-on-LAN-Ziels | `AA:BB:CC:DD:EE:FF` |
|
||||
| `<PVE_HOSTNAME>` | Frei gewählter Hostname des Proxmox-Hosts | `mein-pve` |
|
||||
| `<PRIMARY_USER>` | Der eigene Haupt-Benutzername (Samba/SSH/Guacamole) | `max` |
|
||||
| `<AUTH_KEY>` | Tailscale-Auth-Key, selbst erzeugt (Settings → Keys → Generate auth key) | — |
|
||||
|
||||
Alles, was nicht in dieser Tabelle steht (Paketnamen, Befehle, Container-IDs, Rollen-Bezeichnungen wie "adguard"/"nas"/"browser"), ist bereits generisch und muss nicht angepasst werden.
|
||||
|
||||
Details, Kontext und die dazugehörigen Fehlersuche-Geschichten (warum eine Einstellung so und nicht anders gewählt wurde) stehen jeweils in den einzelnen Kapiteln — die sind bewusst nicht gekürzt, nur die Werte anonymisiert.
|
||||
Loading…
Reference in a new issue