diff --git a/features/INDEX.md b/features/INDEX.md index 3096613..b9fe53b 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -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-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-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 | -## Next Available ID: PROJ-68 +## Next Available ID: PROJ-69 diff --git a/features/PROJ-68-sudo-provisionierung-dienststeuerung.md b/features/PROJ-68-sudo-provisionierung-dienststeuerung.md new file mode 100644 index 0000000..b2d496e --- /dev/null +++ b/features/PROJ-68-sudo-provisionierung-dienststeuerung.md @@ -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._ diff --git a/install.sh b/install.sh index 5e15186..cea6f38 100755 --- a/install.sh +++ b/install.sh @@ -16,6 +16,45 @@ info() { echo -e "${BLUE}[..]${NC} $*"; } warn() { echo -e "${YELLOW}[!!]${NC} $*"; } 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 # wertlos — der Store bleibt Chiffretext. Der Inhalt wird EINMALIG bei der # Generierung ausgegeben, damit er sofort in einen Passwort-Safe (z.B. @@ -493,6 +532,15 @@ install_native() { && log "Benutzer '$AM_USER' existiert bereits" \ || { 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 ──────────────────────────────────────────────── info "Erstelle Verzeichnisstruktur..." mkdir -p "$STORE_DIR/store" "$STORE_DIR/astore" diff --git a/update.sh b/update.sh index f1a0e75..b047a52 100755 --- a/update.sh +++ b/update.sh @@ -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" +# 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}" INSTALL_DIR="/opt/archivmail" BUILD_DIR="/opt/archivmail/_build"