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).
This commit is contained in:
sysops
2026-07-05 20:10:30 +02:00
parent 7300a696ca
commit af897948be
3 changed files with 119 additions and 5 deletions
@@ -1,7 +1,7 @@
--- ---
id: PROJ-68 id: PROJ-68
title: sudo-Provisionierung für Admin-Dienststeuerung fehlte komplett title: sudo-Provisionierung für Admin-Dienststeuerung fehlte komplett
status: In Review status: In Progress
created: 2026-07-05 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 die Datei schon existiert/aktuell ist), damit auch 132 (und jede
andere Bestandsinstallation) beim nächsten Deploy automatisch andere Bestandsinstallation) beim nächsten Deploy automatisch
nachgerüstet wird — analog zum PROJ-67-Upgrade-Pfad-Muster. 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 `POST /api/admin/services/{name}` funktioniert für alle Dienste in
der Whitelist, verifiziert auf 132. 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, - [ ] `archivmail-nft`-Fehlen ist dokumentiert (nicht Teil dieses Tickets,
siehe Non-Goals), damit es nicht als überraschender Folgefehler siehe Non-Goals), damit es nicht als überraschender Folgefehler
auftaucht. auftaucht.
@@ -109,7 +111,105 @@ Teil des `firewall-security`-Skills und bewusst außerhalb dieses Tickets
_wird ergänzt._ _wird ergänzt._
## QA Test Results ## 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 ## Deployment
_wird ergänzt._ _wird ergänzt._
+8 -1
View File
@@ -755,7 +755,14 @@ User=${AM_USER}
Group=${AM_USER} Group=${AM_USER}
# CAP_NET_ADMIN: required for the admin "enable firewall" action (nft -f /etc/nftables.conf) # CAP_NET_ADMIN: required for the admin "enable firewall" action (nft -f /etc/nftables.conf)
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN 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 ExecStart=${INSTALL_DIR}/archivmail --config ${CONFIG_DIR}/config.yml
ExecReload=/bin/kill -HUP \$MAINPID ExecReload=/bin/kill -HUP \$MAINPID
Restart=on-failure Restart=on-failure
+8 -1
View File
@@ -341,7 +341,14 @@ User=${AM_USER}
Group=${AM_USER} Group=${AM_USER}
# CAP_NET_ADMIN: required for the admin "enable firewall" action (nft -f /etc/nftables.conf) # CAP_NET_ADMIN: required for the admin "enable firewall" action (nft -f /etc/nftables.conf)
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN 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 ExecStart=${INSTALL_DIR}/archivmail --config ${CONFIG_DIR}/config.yml
ExecReload=/bin/kill -HUP \$MAINPID ExecReload=/bin/kill -HUP \$MAINPID
Restart=on-failure Restart=on-failure