homelab-showcase/01-pve-host.md

8.4 KiB

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

apt-get install -y git tree

2. Tailscale (Subnet-Router)

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).

# 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)

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)

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):

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)

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:

--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):

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.

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.

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

pct list
for id in 100 101 102 103; do echo "CT $id:"; pct config $id | grep -E "memory|cores|net0"; done