fix(PROJ-68): sudo-Provisionierung für Admin-Dienststeuerung ergänzen

Root Cause für den HTTP-500-Bug beim Manticore-Neustart über den Dienste-Tab:
sudo war auf 132 gar nicht installiert, admin_services_handlers.go ruft aber
für JEDE Dienst-Aktion sudo systemctl auf. Betraf nicht nur Manticore,
sondern alle Dienste in der Whitelist - PROJ-67 hat es nur erstmals sichtbar
gemacht.

install.sh/update.sh installieren jetzt sudo (falls fehlend) und legen
/etc/sudoers.d/archivmail mit NOPASSWD-Regeln für genau die Service-
Whitelist an (archivmail, archivmail-web, manticore, postgresql@17-main,
postfix, nginx) - validiert per visudo -c vor dem Einspielen, damit ein
Bug im Generator nie eine kaputte sudoers-Datei live schaltet. update.sh
zieht das als Backfill für Bestandsinstallationen nach, analog zum
PROJ-67-Upgrade-Pfad-Muster.

archivmail-nft-Helper (externe Zugriffskontrolle) fehlt auf 132 ebenfalls,
aber bewusst nicht Teil dieses Tickets (vermutlich firewall-security-Skill-
Verantwortung).
This commit is contained in:
sysops
2026-07-05 19:49:56 +02:00
parent 4af0e4a280
commit 7300a696ca
4 changed files with 200 additions and 2 deletions
+3 -2
View File
@@ -82,8 +82,9 @@
| PROJ-64 | Session-Invalidation bei Passwort-Change + Datei-Permissions-Härtung (Security-Audit) | Deployed | [PROJ-64](PROJ-64-session-invalidation-file-permissions.md) | 2026-07-03 | | PROJ-64 | Session-Invalidation bei Passwort-Change + Datei-Permissions-Härtung (Security-Audit) | Deployed | [PROJ-64](PROJ-64-session-invalidation-file-permissions.md) | 2026-07-03 |
| PROJ-65 | Physische Tenant-Trennung im Storage-Layer | Deployed | [PROJ-65](PROJ-65-physische-tenant-trennung.md) | 2026-07-04 | | PROJ-65 | Physische Tenant-Trennung im Storage-Layer | Deployed | [PROJ-65](PROJ-65-physische-tenant-trennung.md) | 2026-07-04 |
| PROJ-66 | Backup-Strategie für Store, Keyfile, PostgreSQL (Produktiv + Teilproduktiv) | Deployed | [PROJ-66](PROJ-66-backup-strategie.md) | 2026-07-04 | | PROJ-66 | Backup-Strategie für Store, Keyfile, PostgreSQL (Produktiv + Teilproduktiv) | Deployed | [PROJ-66](PROJ-66-backup-strategie.md) | 2026-07-04 |
| PROJ-67 | Manticore Search Upgrade 25.0.0 → 27.1.5 + Auto-Upgrade-Pfad | In Review | [PROJ-67](PROJ-67-manticore-upgrade-25-zu-27.md) | 2026-07-05 | | PROJ-67 | Manticore Search Upgrade 25.0.0 → 27.1.5 + Auto-Upgrade-Pfad | Deployed | [PROJ-67](PROJ-67-manticore-upgrade-25-zu-27.md) | 2026-07-05 |
| PROJ-68 | sudo-Provisionierung für Admin-Dienststeuerung fehlte komplett | In Review | [PROJ-68](PROJ-68-sudo-provisionierung-dienststeuerung.md) | 2026-07-05 |
<!-- Add features above this line --> <!-- Add features above this line -->
## Next Available ID: PROJ-68 ## Next Available ID: PROJ-69
@@ -0,0 +1,115 @@
---
id: PROJ-68
title: sudo-Provisionierung für Admin-Dienststeuerung fehlte komplett
status: In Review
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.
- [x] Nach der Provisionierung: Start/Stop/Restart über
`POST /api/admin/services/{name}` funktioniert für alle Dienste in
der Whitelist, verifiziert auf 132.
- [ ] `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
_wird ergänzt._
## Deployment
_wird ergänzt._
+48
View File
@@ -16,6 +16,45 @@ info() { echo -e "${BLUE}[..]${NC} $*"; }
warn() { echo -e "${YELLOW}[!!]${NC} $*"; } warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; } die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; }
# PROJ-68: Provisioniert sudo-NOPASSWD-Rechte für die Admin-Dienststeuerung
# (Start/Stop/Restart/Enable/Disable über den "Dienste"-Tab). Muss mit
# `allowedServices` in internal/api/admin_services_handlers.go synchron
# gehalten werden. Installiert `sudo` falls fehlend, validiert die generierte
# Datei per `visudo -c` VOR dem Einspielen — eine kaputte sudoers-Datei würde
# sonst den gesamten sudo-Mechanismus auf dem Server lahmlegen. Idempotent:
# überschreibt die Datei bei jedem Lauf mit demselben, deterministischen
# Inhalt (kein Diff-Merge nötig, da wir hier die einzige Quelle dieser Regeln
# sind — ein manuell abweichend konfiguriertes System würde überschrieben,
# das ist so beabsichtigt, damit Code und Server nie auseinanderlaufen).
provision_archivmail_sudoers() {
local am_user="${1:-$AM_USER}"
command -v sudo >/dev/null 2>&1 || apt-get install -y -qq sudo
local services=("archivmail" "archivmail-web" "manticore" "postgresql@17-main" "postfix" "nginx")
local tmpfile
tmpfile="$(mktemp)"
{
echo "# Verwaltet von install.sh/update.sh (PROJ-68) — nicht manuell editieren,"
echo "# Änderungen gehen beim nächsten Deploy verloren. Whitelist muss mit"
echo "# allowedServices in internal/api/admin_services_handlers.go übereinstimmen."
for svc in "${services[@]}"; do
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl start ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl stop ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl restart ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl enable ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl disable ${svc}.service"
done
} > "$tmpfile"
if visudo -c -f "$tmpfile" >/dev/null 2>&1; then
install -m 0440 -o root -g root "$tmpfile" /etc/sudoers.d/archivmail
log "sudo-Rechte für Dienststeuerung eingerichtet (/etc/sudoers.d/archivmail)"
else
warn "Generierte sudoers-Regel ist ungültig — /etc/sudoers.d/archivmail NICHT verändert"
fi
rm -f "$tmpfile"
}
# PROJ-66: Ein Backup (App-Backup oder PBS/Snapshot) ist ohne dieses Keyfile # PROJ-66: Ein Backup (App-Backup oder PBS/Snapshot) ist ohne dieses Keyfile
# wertlos — der Store bleibt Chiffretext. Der Inhalt wird EINMALIG bei der # wertlos — der Store bleibt Chiffretext. Der Inhalt wird EINMALIG bei der
# Generierung ausgegeben, damit er sofort in einen Passwort-Safe (z.B. # Generierung ausgegeben, damit er sofort in einen Passwort-Safe (z.B.
@@ -493,6 +532,15 @@ install_native() {
&& log "Benutzer '$AM_USER' existiert bereits" \ && log "Benutzer '$AM_USER' existiert bereits" \
|| { useradd --system --shell /bin/false --home "$STORE_DIR" --create-home "$AM_USER"; log "Benutzer angelegt"; } || { useradd --system --shell /bin/false --home "$STORE_DIR" --create-home "$AM_USER"; log "Benutzer angelegt"; }
# ── 2b. sudo-Rechte für die Admin-Dienststeuerung (PROJ-68) ────────────────
# internal/api/admin_services_handlers.go ruft für Start/Stop/Restart/
# Enable/Disable IMMER `sudo systemctl ...` auf — ohne diese Provisionierung
# schlägt jede Dienst-Aktion aus dem Admin-"Dienste"-Tab mit einem
# schwer diagnostizierbaren HTTP 500 fehl (sudo: command not found).
# Whitelist MUSS mit `allowedServices` in admin_services_handlers.go
# synchron bleiben.
provision_archivmail_sudoers
# ── 3. Verzeichnisstruktur ──────────────────────────────────────────────── # ── 3. Verzeichnisstruktur ────────────────────────────────────────────────
info "Erstelle Verzeichnisstruktur..." info "Erstelle Verzeichnisstruktur..."
mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore" mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore"
+34
View File
@@ -18,6 +18,40 @@ die() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen: sudo bash update.sh" [[ $EUID -eq 0 ]] || die "Bitte als root ausführen: sudo bash update.sh"
# PROJ-68: Backfill für Bestandsinstallationen, bei denen sudo-Rechte für die
# Admin-Dienststeuerung nie provisioniert wurden (siehe install.sh für den
# ausführlichen Kommentar zum "Warum"). Whitelist MUSS mit `allowedServices`
# in internal/api/admin_services_handlers.go synchron gehalten werden.
provision_archivmail_sudoers() {
local am_user="archivmail"
command -v sudo >/dev/null 2>&1 || apt-get install -y -qq sudo
local services=("archivmail" "archivmail-web" "manticore" "postgresql@17-main" "postfix" "nginx")
local tmpfile
tmpfile="$(mktemp)"
{
echo "# Verwaltet von install.sh/update.sh (PROJ-68) — nicht manuell editieren,"
echo "# Änderungen gehen beim nächsten Deploy verloren. Whitelist muss mit"
echo "# allowedServices in internal/api/admin_services_handlers.go übereinstimmen."
for svc in "${services[@]}"; do
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl start ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl stop ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl restart ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl enable ${svc}.service"
echo "${am_user} ALL=(root) NOPASSWD: /usr/bin/systemctl disable ${svc}.service"
done
} > "$tmpfile"
if visudo -c -f "$tmpfile" >/dev/null 2>&1; then
install -m 0440 -o root -g root "$tmpfile" /etc/sudoers.d/archivmail
log "sudo-Rechte für Dienststeuerung aktuell (/etc/sudoers.d/archivmail)"
else
warn "Generierte sudoers-Regel ist ungültig — /etc/sudoers.d/archivmail NICHT verändert"
fi
rm -f "$tmpfile"
}
provision_archivmail_sudoers
REPO_URL="${REPO_URL:-https://gitea.perlbach24.de/scripte/archivmail.git}" REPO_URL="${REPO_URL:-https://gitea.perlbach24.de/scripte/archivmail.git}"
INSTALL_DIR="/opt/archivmail" INSTALL_DIR="/opt/archivmail"
BUILD_DIR="/opt/archivmail/_build" BUILD_DIR="/opt/archivmail/_build"