2 01 PVE Host
arnol edited this page 2026-08-07 19:05:43 +02:00

PVE-Host (<PVE_HOSTNAME>) — Rebuild-Runbook

Basis: Proxmox VE, frisch installiert (Fujitsu Esprimo, i5-6500, 4 Kerne kein HT, 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

Automatische Sicherheitsupdates

Baseline auf allen Nodes (PVE-Host, LXCs, VPS). Konfiguration versioniert im Repo — am Node nicht bearbeiten, sonst driftet es auseinander.

# je Node
apt-get install -y unattended-upgrades
# Configs aus dem Repo nach /etc/apt/apt.conf.d/ kopieren (pct push bzw. scp)

Bewusst nur Sicherheitsupdates. Origins-Pattern deckt ausschließlich label=Debian-Security ab. Funktions-Updates bleiben manuell.

Auf der Blacklist — alles, dessen Aktualisierung einen Neustart nach sich zöge:

Muster Grund
linux-image, linux-headers Kernel erfordert Reboot, und der hängt am Strommess-Ritual
proxmox-, pve- das ist der Hypervisor selbst
docker-ce, containerd ein Daemon-Neustart reißt alle Container mit

⚠️ Unattended-Upgrade::Automatic-Reboot "false" — ein unbeaufsichtigter Neustart würde das Strommess-Ritual überspringen (uptime notieren, kWh ablesen) und könnte mitten ins Backup-Fenster fallen.

⚠️ NICHT TUN: Remove-Unused-Dependencies einschalten. Ein überraschend entferntes Paket ist schwerer zu diagnostizieren als ein paar belegte MB.

Meldung: eigenes Report-Skript (PVE-Host, täglich)

Fragt alle Nodes ab und schickt eine Push-Nachricht statt einer je Node — der Zustand ist damit an einer Stelle nachlesbar. Meldet, welche Pakete in 24 h automatisch aktualisiert wurden und welche Nodes einen Neustart brauchen. Gestoppte Container werden als solche erkannt und übersprungen.

Pusht nur, wenn es etwas zu sagen gibt. Ein tägliches „nichts passiert" wäre Rauschen und würde genau die Meldung entwerten, auf die es ankommt.

Erster Lauf fand sofort etwas: ein Node lief mehrere Tage auf einem älteren Kernel-Patch-Level, obwohl das passende Sicherheitsupdate bereits installiert war — ohne Neustart wirkungslos, unbemerkt, weil niemand danach schaut. Genau die Lücke, die dieser Report schließt.

⚠️ Ein installierter Kernel ist kein aktiver Kernel. uname -r gegen dpkg -l 'linux-image-*' prüfen; die Differenz ist der ungenutzte Patch.