Table of contents
- CT 102 — Management-Desktop (Browser-VNC) — Rebuild-Runbook
- 1. Container erstellen
- 2. TUN-Device für Tailscale (siehe 01-pve-host.md, Abschnitt 8)
- 3. Desktop-Umgebung + Browser + VNC
- 4. Firefox-Profil + Startseiten
- 5. Tailscale-Mitgliedschaft
- 6. Guacamole-Verbindung (auf dem VPS, siehe 06-vps-strato.md)
- 7. Downloads aus der VNC-Sitzung aufs lokale Gerät übertragen
- 8. Auto-Stop bei Nichtnutzung + Wake-Mechanismus (Stromsparen)
- Wake-Mechanismus (spiegelt "Wake PC1" aus 01-pve-host.md Abschnitt 6)
- Auto-Stop bei Inaktivität
- Screen-Lock nach 3 Min. Inaktivität (Enterprise-Richtlinie als Orientierung)
- Verifikation
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
pct create 102 local:vztmpl/<DEBIAN_TEMPLATE> \
--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)
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
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:
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:
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
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:
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
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.
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):
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.
8. Auto-Stop bei Nichtnutzung + Wake-Mechanismus (Stromsparen)
CT102 läuft dauerhaft (~335 MB RAM + etwas CPU im Leerlauf), auch wenn niemand per VNC verbunden ist. Ziel: automatisch stoppen, wenn ungenutzt, mit einfachem Weg zum Wiederstarten.
⚠️ NICHT TUN: pct suspend/resume (CRIU-Checkpointing) für einen echten Ruhezustand verwenden. Proxmox unterstützt das für Container nur experimentell — für einen vollen Desktop-Prozessbaum (X11/VNC/Firefox) real riskant (bekannte CRIU-Einschränkungen bei komplexen Prozessbäumen/offenen Sockets). Stattdessen: komplett stoppen und bei Bedarf neu starten — bei einem LXC dauert das nur Sekunden, kein Kernel-Boot wie bei einer echten VM.
Wake-Mechanismus (spiegelt "Wake PC1" aus 01-pve-host.md Abschnitt 6)
useradd -m -s /bin/bash browserwake
ssh-keygen -t ed25519 -N "" -f /home/browserwake/.ssh/browserwake_key
# WICHTIG: Optionen und Schlüssel MÜSSEN auf einer einzigen Zeile stehen!
PUBKEY=$(cat /home/browserwake/.ssh/browserwake_key.pub)
echo "no-port-forwarding,no-x11-forwarding,no-agent-forwarding,command=\"/usr/local/bin/start-browser-desktop.sh\" ${PUBKEY}" \
> /home/browserwake/.ssh/authorized_keys
apt-get install -y sudo
cat > /etc/sudoers.d/browserwake <<'EOF'
browserwake ALL=(root) NOPASSWD: /usr/sbin/pct start 102
EOF
chmod 440 /etc/sudoers.d/browserwake
/usr/local/bin/start-browser-desktop.sh:
#!/usr/bin/env bash
sudo /usr/sbin/pct start 102
echo "Management-Desktop (CT102) gestartet (oder lief bereits)."
Neue Guacamole-SSH-Verbindung "Management-Desktop starten" (hostname=<LAN_IP_PVE>, port=22, username=browserwake, private-key, command=/usr/local/bin/start-browser-desktop.sh) — gleicher Aufbau wie "Wake PC1". Nutzung: erst diese Verbindung anklicken (paar Sekunden warten), danach die normale "Management-Desktop (Browser)"-VNC-Verbindung öffnen.
⚠️ NICHT TUN: Optionen (no-port-forwarding,...,command="...") und den Public-Key-String auf zwei separate Zeilen in authorized_keys schreiben (z.B. weil man die Optionen erst per echo schreibt und den Key danach per >> anhängt). SSHs Format verlangt beides auf einer Zeile — zwei Zeilen ergeben eine kaputte erste Zeile (wird ignoriert) und einen komplett unrestriktiven zweiten Schlüssel ohne jede Einschränkung. Beim ersten Testlauf genau so passiert und erst durch einen expliziten Test (ssh -i key user@host "beliebiger befehl" — führte den beliebigen Befehl aus statt des erzwungenen Skripts) aufgefallen. Nach jeder authorized_keys-Änderung mit forced command immer gegentesten: ein beliebiger Befehl darf niemals durchkommen.
pct erfordert Root-Rechte — ein unprivilegierter Linux-User kann es nicht direkt aufrufen (pct: command not found bzw. Permission-Fehler). Deshalb die eng begrenzte sudo-Regel nur für exakt pct start 102, nicht für pct allgemein.
Auto-Stop bei Inaktivität
/usr/local/bin/browser-desktop-idle-check.sh, Cron alle 5 Minuten. Finale Version prüft die tatsächliche X11-Eingabe-Idle-Zeit (xprintidle, direkt im Container gemessen) statt eines Verbindungsstatus:
IDLE_THRESHOLD_MS=900000 # 15 Minuten
STATUS=$(/usr/sbin/pct status 102 2>/dev/null | awk '{print $2}')
[ "$STATUS" != "running" ] && exit 0
IDLE_MS=$(/usr/sbin/pct exec 102 -- env DISPLAY=:1 XAUTHORITY=/root/.Xauthority xprintidle 2>/dev/null)
case "$IDLE_MS" in ''|*[!0-9]*) exit 0 ;; esac
if [ "$IDLE_MS" -ge "$IDLE_THRESHOLD_MS" ]; then
/usr/sbin/pct shutdown 102 --timeout 30
# + ntfy-Push
fi
Vorgeschichte — drei verworfene Verbindungsstatus-Ansätze, bevor die eigentliche Ursache klar wurde:
⚠️ NICHT TUN (erste Version): Aktive Verbindung per pct exec 102 -- ss ... innerhalb des Containers prüfen — die eingehende Verbindung ist dort nicht sichtbar, wohl aber vom PVE-Host aus (unter der Tailscale-IP des Containers).
⚠️ NICHT TUN: Auch die host-seitige ss-Prüfung auf Port 5901 ist nicht zuverlässig. Reale Beobachtung: eine Guacamole-Sitzung endete laut eigener guacamole_connection_history-Tabelle sauber nach 15 Sekunden — ss zeigte aber über Stunden weiterhin ESTABLISHED. Vermutlich hält guacd die Backend-Verbindung länger offen als die Nutzer-Sitzung dauert.
⚠️ NICHT TUN: Guacamoles eigene History-Tabelle (end_date IS NULL = aktive Sitzung) abfragen — es gab verwaiste Einträge ohne end_date von nie sauber beendeten alten Sitzungen, nicht von einer echten laufenden Sitzung unterscheidbar.
Direkter Check im guacd-Container selbst (docker exec guacd netstat -tn) war technisch die zuverlässigste der drei TCP-Varianten — hatte aber einen grundsätzlichen, nicht behebbaren blinden Fleck: eine offene Verbindung sagt nichts darüber aus, ob der Nutzer gerade AFK ist. Bleibt der Guacamole-Tab offen und der Nutzer geht weg, bleibt die TCP-Verbindung bestehen — der Auto-Stop hätte nie ausgelöst. Erst ein Live-Test (Nutzer bewusst AFK mit offenem Tab) deckte das auf.
⚠️ NICHT TUN (zusätzlicher Bug, unabhängig vom obigen): pct ohne absoluten Pfad aufrufen. Cron läuft mit PATH=/usr/bin:/bin (ohne /usr/sbin, wo pct liegt) — Aufrufe ohne absoluten Pfad schlagen unter Cron mit "not found" fehl, STATUS bleibt leer, das Skript nimmt den "läuft nicht"-Zweig und erreicht die eigentliche Prüfung nie. Gefunden durch gezieltes Nachstellen der Cron-Umgebung (env -i ... PATH=/usr/bin:/bin). Lektion: bei "funktioniert manuell, aber nicht unter Cron" immer zuerst die PATH-Differenz prüfen.
Finale Lösung: xprintidle misst die Zeit seit der letzten echten Maus-/Tastatur-Eingabe auf X11-Ebene, unabhängig vom Verbindungsstatus — löst das AFK-Problem strukturell und macht das bisherige Zähler-Statefile überflüssig (Idle-Zeit ist bereits selbst der Zustand).
Verifikation: Wake-Key-Test (erzwungener Befehl + Port-Forwarding) bestanden. Auto-Stop unter simulierter Cron-Umgebung (minimaler PATH) mit künstlich niedrigem Threshold end-to-end getestet: Container wurde korrekt gestoppt.
Screen-Lock nach 3 Min. Inaktivität (Enterprise-Richtlinie als Orientierung)
Zusätzlich zum Auto-Stop: die Desktop-Sitzung selbst soll sich sperren, wenn niemand aktiv ist — unabhängig vom Auto-Stop-Timer. xautolock ist nicht paketiert (Debian <DEBIAN_RELEASE>), daher ein eigener minimaler Watcher statt eines fertigen Tools.
⚠️ NICHT TUN (erste Version): xtrlock als Locker einsetzen. Funktional korrekt, aber xtrlock ist ein reines Xlib-Programm ohne jede Unterstützung für die X-Zwischenablage — entsperren geht nur durch blindes Eintippen des Passworts, ein Paste aus einem Passwortmanager (xdotool key ctrl+v gegen den laufenden Lock getestet) hat nachweislich keine Wirkung. Für ein starkes, zufälliges Passwort aus dem Passwortmanager unpraktikabel. i3lock/slock sind dieselbe Xlib-Keygrab-Familie (Debian-Tag uitoolkit::xlib) und hätten dieselbe Einschränkung.
⚠️ NICHT TUN (zweiter Versuch): xfce4-screensaver (GTK-basiert, uitoolkit::gtk, unterstützt Paste in der Theorie) als Ersatz einsetzen. In dieser konkreten Umgebung (TigerVNC-Session ohne Display-Manager-Login, ohne xfce4-power-manager, Xtigervnc ohne DPMS-Extension) funktioniert weder die manuelle Sperr-Anweisung (xfce4-screensaver-command --lock hängt den Daemon auf, D-Bus-Anfragen laufen danach in einen Timeout) noch die automatische Idle-Auslösung (kein xfce4-power-manager, das das nötige Idle-Signal liefert). Der Daemon selbst crasht nicht, wird aber dauerhaft unresponsive — ein reales Risiko, den Desktop komplett gesperrt und unentsperrbar zu hinterlassen. Deshalb wieder deinstalliert.
Finale Lösung: ein minimaler, selbst geschriebener Python/Tkinter-Lock-Dialog (python3-tk) — echtes GUI-Textfeld, Paste funktioniert standardmäßig (tkinter.Entry unterstützt Strg+V ohne Zusatzaufwand), kein Display-Manager/D-Bus-Session-Dienst nötig.
/usr/local/bin/gui-lock.py:
import tkinter as tk
SECRET_FILE = "/root/.config/lock-secret"
def check_and_maybe_unlock(event=None):
with open(SECRET_FILE) as f:
secret = f.read().strip()
if entry.get() == secret:
root.destroy()
else:
entry.delete(0, tk.END)
root.bell()
root = tk.Tk()
root.attributes("-fullscreen", True)
root.attributes("-topmost", True)
root.configure(bg="black")
root.config(cursor="none")
label = tk.Label(root, text="Gesperrt — Passwort eingeben", fg="white", bg="black", font=("sans", 18))
label.pack(pady=(300, 20))
entry = tk.Entry(root, show="*", font=("sans", 18), width=30, justify="center")
entry.pack()
entry.bind("<Return>", check_and_maybe_unlock)
entry.focus_force()
root.wait_visibility() # sonst: "grab failed: window not viewable"
root.grab_set_global()
root.mainloop()
Vergleichs-Passwort steht in /root/.config/lock-secret (chmod 600) — kein PAM/Linux-Account-Passwort, eigenständiges Secret nur für diesen Lock. Dadurch komplett unabhängig vom Root-Account, kein SSH-Bezug, keine PAM-Komplexität.
/usr/local/bin/lock-on-idle.sh:
#!/bin/sh
THRESHOLD_MS=180000 # 3 Minuten
while true; do
IDLE=$(xprintidle 2>/dev/null || echo 0)
[ "$IDLE" -ge "$THRESHOLD_MS" ] && python3 /usr/local/bin/gui-lock.py
sleep 10
done
/root/.config/tigervnc/xstartup:
#!/bin/sh
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
/usr/local/bin/lock-on-idle.sh &
exec startxfce4
⚠️ NICHT TUN: root.grab_set_global() direkt nach dem Erstellen des Fensters aufrufen — Tk wirft TclError: grab failed: window not viewable, weil das Fenster vom Window-Manager noch nicht gemappt wurde. Vorher root.wait_visibility() aufrufen.
Verifikation: Watcher-Loop mit künstlich niedrigem Threshold end-to-end getestet — löst automatisch aus, ohne manuellen Aufruf. Entsperren per xdotool-simuliertem Strg+V-Paste (aus der X-Zwischenablage) erfolgreich getestet. Gegentest mit falschem Passwort bestätigt: Lock bleibt bestehen. Getipptes (nicht eingefügtes) Passwort funktioniert weiterhin normal.
Verifikation
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 "/")
Rebuild-Runbooks
Aufbau
- Übersicht
- PVE-Host
- AdGuard · CT100
- NAS · CT101
- Management-Desktop · CT102
- Vaultwarden · CT103
- VPS · STRATO
- Backup & Restore
- Monitoring · CT104
- Gast-Zugang
Referenz