From af897948be47deadc89b67844dca4bb05669629e Mon Sep 17 00:00:00 2001 From: sysops Date: Sun, 5 Jul 2026 20:10:30 +0200 Subject: [PATCH] fix(PROJ-68): CapabilityBoundingSet blockiert sudo trotz korrekter sudoers-Regel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- ...68-sudo-provisionierung-dienststeuerung.md | 106 +++++++++++++++++- install.sh | 9 +- update.sh | 9 +- 3 files changed, 119 insertions(+), 5 deletions(-) diff --git a/features/PROJ-68-sudo-provisionierung-dienststeuerung.md b/features/PROJ-68-sudo-provisionierung-dienststeuerung.md index b2d496e..5adc41c 100644 --- a/features/PROJ-68-sudo-provisionierung-dienststeuerung.md +++ b/features/PROJ-68-sudo-provisionierung-dienststeuerung.md @@ -1,7 +1,7 @@ --- id: PROJ-68 title: sudo-Provisionierung für Admin-Dienststeuerung fehlte komplett -status: In Review +status: In Progress created: 2026-07-05 --- @@ -62,9 +62,11 @@ Teil des `firewall-security`-Skills und bewusst außerhalb dieses Tickets 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. -- [x] Nach der Provisionierung: Start/Stop/Restart über +- [ ] 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. @@ -109,7 +111,105 @@ Teil des `firewall-security`-Skills und bewusst außerhalb dieses Tickets _wird ergänzt._ ## QA Test Results -_wird ergänzt._ + +### 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._ diff --git a/install.sh b/install.sh index cea6f38..95f7e9c 100755 --- a/install.sh +++ b/install.sh @@ -755,7 +755,14 @@ User=${AM_USER} Group=${AM_USER} # CAP_NET_ADMIN: required for the admin "enable firewall" action (nft -f /etc/nftables.conf) AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN -CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN +# PROJ-68: CAP_SETUID/CAP_SETGID are NOT ambient (the Go daemon itself must +# never hold them) but must be in the bounding set so the setuid-root sudo +# child process (spawned by the admin "Dienste"-Tab for systemctl actions) +# can actually change to uid/gid 0. Without this, sudo aborts with +# "unable to change to root gid: Operation not permitted" even though the +# sudoers NOPASSWD rule is correct — verified 2026-07-05, see +# features/PROJ-68-sudo-provisionierung-dienststeuerung.md. +CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETUID CAP_SETGID ExecStart=${INSTALL_DIR}/archivmail --config ${CONFIG_DIR}/config.yml ExecReload=/bin/kill -HUP \$MAINPID Restart=on-failure diff --git a/update.sh b/update.sh index b047a52..0dbb007 100755 --- a/update.sh +++ b/update.sh @@ -341,7 +341,14 @@ User=${AM_USER} Group=${AM_USER} # CAP_NET_ADMIN: required for the admin "enable firewall" action (nft -f /etc/nftables.conf) AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN -CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN +# PROJ-68: CAP_SETUID/CAP_SETGID are NOT ambient (the Go daemon itself must +# never hold them) but must be in the bounding set so the setuid-root sudo +# child process (spawned by the admin "Dienste"-Tab for systemctl actions) +# can actually change to uid/gid 0. Without this, sudo aborts with +# "unable to change to root gid: Operation not permitted" even though the +# sudoers NOPASSWD rule is correct — verified 2026-07-05, see +# features/PROJ-68-sudo-provisionierung-dienststeuerung.md. +CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETUID CAP_SETGID ExecStart=${INSTALL_DIR}/archivmail --config ${CONFIG_DIR}/config.yml ExecReload=/bin/kill -HUP \$MAINPID Restart=on-failure