Showcase nachgezogen: restliche Portfolio-Dateien auf aktuellen Stand

arnol 2026-08-07 19:05:45 +02:00
commit 40bc8913e7

@ -154,6 +154,143 @@ 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)
```bash
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`:
```bash
#!/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:
```bash
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`:
```python
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`:
```bash
#!/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`:
```sh
#!/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
```bash