--- id: PROJ-68 title: sudo-Provisionierung für Admin-Dienststeuerung fehlte komplett status: In Progress created: 2026-07-05 --- ## Kontext Beim QA-Test der neuen Versions-Spalte im Dienste-Tab (siehe Commit 8564d7c) wurde ein Neustart-Versuch für `manticore` über `POST /api/admin/services/manticore` getestet — Ergebnis: HTTP 500, Dienst nicht neu gestartet. **Root Cause (verifiziert auf 192.168.1.132):** `sudo` ist auf 132 gar nicht installiert (kein `/usr/bin/sudo`, kein `/etc/sudoers`/`/etc/sudoers.d/`). `internal/api/admin_services_handlers.go` ruft aber für JEDE Dienst-Aktion `exec.Command("sudo", "/usr/bin/systemctl", action, name+".service")` auf — das schlägt fehl mit „executable file not found“, `CombinedOutput()` ist leer, daher `writeError(500, "")` (leerer Body, schwer zu diagnostizieren). **Betrifft nicht nur Manticore** — jede Dienst-Aktion (Start/Stop/Restart/ Enable/Disable) für ALLE Dienste in der Whitelist war auf 132 schon vorher kaputt, das PROJ-67/Versions-Feature hat es nur erstmals sichtbar gemacht, weil vorher niemand `manticore` (das jetzt neu in der Liste ist) über die UI neugestartet hat — wahrscheinlich wurde die Dienststeuerung insgesamt nie über die UI getestet, seit sie gebaut wurde. `install.sh` legt den `archivmail`-Systembenutzer an (`useradd --system --shell /bin/false ...`), installiert aber nie `sudo` und legt nie eine `/etc/sudoers.d/archivmail`-Regel an. Der Go-Code setzt eine Server-Provisionierung voraus, die im Repo nirgends existiert — das war vermutlich ein manueller, nie dokumentierter Schritt bei der Ersteinrichtung von 131, der bei 132 (und jeder künftigen Neuinstallation) fehlt. Separater, verwandter Fund: `/usr/local/sbin/archivmail-nft` (für die "Extern sperren/freigeben"-Buttons beim `archivmail`-Dienst) fehlt auf 132 ebenfalls und wird von `install.sh` auch nicht deployt — das ist vermutlich Teil des `firewall-security`-Skills und bewusst außerhalb dieses Tickets (siehe Non-Goals). ## User Stories - Als Superadmin möchte ich Dienste (inkl. Manticore) über die Web-UI starten/stoppen/neustarten können, ohne mich per SSH einzuloggen. - Als Betreiber möchte ich, dass eine frische Installation (`install.sh`) die Dienststeuerung sofort funktionsfähig macht, ohne manuelle Nacharbeit auf dem Server. ## Acceptance Criteria - [x] `install.sh` installiert `sudo` (falls nicht vorhanden) und legt `/etc/sudoers.d/archivmail` mit `NOPASSWD`-Regeln für `systemctl {start,stop,restart,enable,disable}` auf genau die Service-Whitelist aus `internal/api/admin_services_handlers.go` (`archivmail`, `archivmail-web`, `manticore`, `postgresql@17-main`, `postfix`, `nginx`) an — keine allgemeine `ALL`-Regel. - [x] Sudoers-Datei wird vor dem Einspielen mit `visudo -c -f` validiert (Syntaxfehler dürfen niemals eine funktionierende sudoers-Config kaputt machen). - [x] `update.sh` zieht dieselbe Provisionierung nach (idempotent, prüft ob die Datei schon existiert/aktuell ist), damit auch 132 (und jede andere Bestandsinstallation) beim nächsten Deploy automatisch nachgerüstet wird — analog zum PROJ-67-Upgrade-Pfad-Muster. - [ ] Nach der Provisionierung: Start/Stop/Restart über `POST /api/admin/services/{name}` funktioniert für alle Dienste in der Whitelist, verifiziert auf 132. **NICHT erfüllt** — durch systemd-CapabilityBoundingSet blockiert, sudo fehlen CAP_SETUID/CAP_SETGID. Siehe QA Test Results (BLOCKER). - [ ] `archivmail-nft`-Fehlen ist dokumentiert (nicht Teil dieses Tickets, siehe Non-Goals), damit es nicht als überraschender Folgefehler auftaucht. ## Edge Cases - Sudoers-Syntaxfehler durch einen Bug im Provisionierungs-Code → darf niemals live installiert werden (`visudo -c` VOR dem Kopieren nach `/etc/sudoers.d/`, bei Fehler abbrechen statt eine korrupte Datei zu hinterlassen, die den gesamten sudo-Mechanismus auf dem Server lahmlegen könnte). - Datei existiert schon (z.B. von einem früheren manuellen Setup mit abweichendem Inhalt) → nicht blind überschreiben, sondern nur ergänzen falls Einträge fehlen, oder zumindest den bestehenden Inhalt vor dem Überschreiben sichern (`.bak`). - 131 (Produktiv) hat vermutlich schon eine funktionierende, manuell angelegte sudoers-Config (sonst wären Service-Aktionen dort auch nie gegangen) — Provisionierung muss idempotent/additiv sein, darf eine funktionierende Config nicht durch einen abweichenden Regelsatz ersetzen und brechen. ## Non-Goals - `archivmail-nft`-Helper-Script (externe Zugriffskontrolle für Port 8080) wird NICHT in diesem Ticket gebaut — vermutlich Verantwortungsbereich des `firewall-security`-Skills, separates Ticket falls gewünscht. - Kein Umbau auf D-Bus/PolicyKit als Alternative zu sudo (größerer Architektur-Schnitt, nicht durch den akuten Bug gerechtfertigt). ## Technical Requirements - Betroffene Dateien: `install.sh`, `update.sh`. - Whitelist im Sudoers-Generator muss mit `allowedServices` in `internal/api/admin_services_handlers.go` synchron gehalten werden (bei künftigen neuen Diensten beide Stellen anfassen — Risiko, dass das wieder auseinanderläuft, wie es beim `manticore`-Eintrag ja gerade passiert ist). --- ## Implementation Notes _wird ergänzt._ ## QA Test Results ### BLOCKER (offen) — systemd-Hardening blockiert sudo trotz korrekter sudoers-Regel **Status: NICHT gelöst.** Die sudoers-Provisionierung (AC 1-3, 5) ist zwar auf 132 korrekt eingespielt (`/etc/sudoers.d/archivmail`, 2544 Bytes, `visudo`-valide, NOPASSWD für start/stop/restart/enable/disable auf die Whitelist), ABER die Dienststeuerung über die UI funktioniert trotzdem NICHT, weil ein zweites, unabhängiges Problem greift: das systemd-Capability-Hardening der `archivmail.service`-Unit. **Root Cause (verifiziert auf 192.168.1.132, 2026-07-05):** Die Unit setzt `CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN` (update.sh:344, install.sh:758). Der Bounding-Set kappt das maximale Capability-Set des GESAMTEN Prozessbaums des Daemons — inklusive der von ihm geforkten `exec.Command("sudo", ...)`-Kindprozesse. `sudo` ist zwar setuid-root, benötigt aber zusätzlich **CAP_SETUID** und **CAP_SETGID**, um real/saved uid+gid auf 0 zu setzen. Da diese beiden Capabilities NICHT im Bounding-Set stehen, sind sie auch für den setuid-root-sudo-Prozess nicht im Permitted-Set → sudo bricht ab. **Reproduktion (systemd-run repliziert exakt den Daemon-Capability-Kontext):** ``` # FEHLSCHLAG mit dem aktuell deployten Bounding-Set: systemd-run --uid=archivmail --gid=archivmail \ -p CapabilityBoundingSet="CAP_NET_BIND_SERVICE CAP_NET_ADMIN" \ -p AmbientCapabilities="CAP_NET_BIND_SERVICE CAP_NET_ADMIN" \ -p NoNewPrivileges=no --wait --pipe --collect \ /usr/bin/sudo -n /usr/bin/systemctl restart manticore.service # → sudo: unable to change to root gid: Operation not permitted # → sudo: error initializing audit plugin sudoers_audit (exit 1) # ERFOLG mit erweitertem Bounding-Set: systemd-run ... -p CapabilityBoundingSet="CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETUID CAP_SETGID" ... /usr/bin/sudo -n /usr/bin/systemctl restart manticore.service # → exit 0, manticore restartet, alle Dienste active ``` Hinweis: Ein naiver `runuser -u archivmail -- sudo ...`-Test aus einer root-Shell schlägt NICHT fehl (exit 0), weil `runuser` den restriktiven Bounding-Set der `.service`-Unit NICHT trägt — dieser Test ist irreführend und darf nicht als Nachweis der Funktionsfähigkeit verwendet werden. Nur der Test im echten Unit-/Capability-Kontext (systemd-run oder echter API-Call durch den Daemon) ist aussagekräftig. **Auswirkung auf AC 4:** `POST /api/admin/services/{name}` bleibt für ALLE Dienste der Whitelist funktionslos, solange der Daemon mit dem aktuellen Bounding-Set läuft. AC 4 ist damit NICHT erfüllt — die sudoers-Regel allein reicht nicht. **Minimale nötige Änderung (NICHT von QA umgesetzt — Backend/DevOps):** In beiden Unit-Templates den Bounding-Set erweitern: - `update.sh:344` und `install.sh:758`: `CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETUID CAP_SETGID` - `AmbientCapabilities` bleibt unverändert (`CAP_NET_BIND_SERVICE CAP_NET_ADMIN`) — SETUID/SETGID dürfen NICHT ambient gesetzt werden, sonst hätte der Go-Daemon selbst dauerhaft diese Rechte; im Bounding-Set genügt die Erlaubnis, damit der setuid-root-sudo-Kindprozess sie nutzen darf. - Danach `systemctl daemon-reload` + `systemctl restart archivmail` nötig (Capability-Änderungen greifen erst nach Neustart des Dienstes). **Alternative Lösungswege (Design-Entscheidung Backend):** - Bounding-Set erweitern (oben) — kleinster Eingriff, aber weicht das Hardening leicht auf (der Daemon-Prozessbaum darf uid/gid wechseln — praktisch nur relevant über den ohnehin durch sudoers eng begrenzten sudo-Pfad). - Alternativ: sudo ganz vermeiden und Dienststeuerung über systemd D-Bus/PolicyKit oder einen dedizierten, minimal privilegierten Helper realisieren (in PROJ-68 ausdrücklich als Non-Goal markiert — größerer Architektur-Schnitt). **Regression/Nebenwirkungen:** Keine. Alle Dienste auf 132 nach den Tests stabil `active` (archivmail, archivmail-web, manticore, postgresql@17-main, nginx). Die mehrfachen Test-Restarts von manticore haben den Index-Dienst nicht beschädigt; manticore kam jedes Mal sauber wieder hoch. Es wurden keine Konfig- oder Passwort-Änderungen vorgenommen; die `systemd-run`-Testunits liefen mit `--collect` und wurden automatisch aufgeräumt. **Fazit AC-Status:** - AC 1 (sudoers-Datei) — erfüllt/eingespielt - AC 2 (visudo-Validierung) — erfüllt - AC 3 (update.sh idempotent) — erfüllt - AC 4 (Service-Aktionen funktionieren über API) — **NICHT erfüllt, durch Capability-Bounding-Set blockiert** (dieser Zusatzbefund) - AC 5 (nft-Doku) — offen (unverändert) **QA-Urteil: NICHT bestanden** — Ticket darf nicht auf „gelöst" gesetzt werden, solange das Capability-Bounding-Set nicht in beiden Unit-Templates erweitert (oder eine sudo-freie Alternative gebaut) und auf 132 verifiziert ist. ## Fix (2026-07-05) `CapabilityBoundingSet` in `update.sh` und `install.sh` (jeweils im `archivmail.service`-Unit-Template) um `CAP_SETUID CAP_SETGID` erweitert — genau der von der QA-Diagnose empfohlene minimale Eingriff. `AmbientCapabilities` bewusst unverändert gelassen (der Go-Daemon selbst soll diese Rechte nie dauerhaft halten, nur der kurzlebige `sudo`-Kindprozess braucht sie im Bounding-Set verfügbar). Beide Skripte schreiben die Unit-Datei bei jedem Deploy neu und rufen danach `systemctl daemon-reload` — kein separater Migrationsschritt nötig, der Fix greift beim nächsten regulären `update.sh`-Lauf (unter Berücksichtigung des bekannten Self-Update-Timing-Effekts, siehe PROJ-67). Re-QA auf 132 steht noch aus (echter API-Call nach Deploy, nicht der irreführende `runuser`-Test). ## Deployment _wird ergänzt._