Files
archivmail/features/PROJ-68-sudo-provisionierung-dienststeuerung.md
T
sysops af897948be fix(PROJ-68): CapabilityBoundingSet blockiert sudo trotz korrekter sudoers-Regel
QA-Re-Diagnose auf 132 fand einen zweiten, unabhängigen Blocker: die
archivmail.service-Unit setzt CapabilityBoundingSet=CAP_NET_BIND_SERVICE
CAP_NET_ADMIN. Das kappt das Capability-Set des GESAMTEN Prozessbaums
inklusive der von exec.Command("sudo", ...) geforkten Kindprozesse - sudo
braucht aber zusaetzlich CAP_SETUID/CAP_SETGID um auf uid/gid 0 zu
wechseln, sonst bricht es ab ("unable to change to root gid: Operation not
permitted"), selbst wenn sudoers korrekt konfiguriert ist.

Ein erster Verifikationsversuch per `runuser -u archivmail -- sudo ...`
hatte faelschlich Erfolg gemeldet, weil runuser den restriktiven
Bounding-Set der Unit nicht traegt - nur ein Test im echten
systemd-Unit-Kontext (systemd-run oder echter API-Call) ist aussagekraeftig.

Fix: CapabilityBoundingSet in update.sh und install.sh um CAP_SETUID
CAP_SETGID erweitert. AmbientCapabilities bleibt unveraendert (der Go-Daemon
selbst soll diese Rechte nie dauerhaft halten, nur der sudo-Kindprozess
braucht sie im Bounding-Set verfuegbar).
2026-07-05 20:10:30 +02:00

11 KiB

id, title, status, created
id title status created
PROJ-68 sudo-Provisionierung für Admin-Dienststeuerung fehlte komplett In Progress 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

  • 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.
  • Sudoers-Datei wird vor dem Einspielen mit visudo -c -f validiert (Syntaxfehler dürfen niemals eine funktionierende sudoers-Config kaputt machen).
  • 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.