diff --git a/docs/TASKS.md b/docs/TASKS.md index a54f4ca..d7d002e 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -392,7 +392,11 @@ und [#15](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/15), Det - [x] **Hetzner Cloud Firewall** – Default-Deny inbound, siehe "Phase 7" oben. **Done.** - [x] **SSH Hardening** – Key-only, Root-Login disabled, Port 2248, siehe "Phase 7" oben. **Done.** -- [ ] **unattended-upgrades** → [Issue #24](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/24) +- [x] **unattended-upgrades** (2026-07-30) – war bereits aktiv (`APT::Periodic::*` seit + längerem gesetzt, Origins-Pattern deckt Debian+Debian-Security ab), nur nie dokumentiert. + Ergänzt: Pre-Update-Benachrichtigung per Mail+Matrix, fest vor dem 06:00-07:00-Update-Fenster. + Siehe [07-host-maintenance-notifications.md](deployment-guides/07-host-maintenance-notifications.md). + Closes [Issue #24](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/24) - [ ] **K3s API Security Hardening** → [Issue #25](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/25) - [ ] **auditd (File Integrity & Syscall Audit)** → [Issue #26](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/26) - [ ] **Kernel Hardening (sysctl)** → [Issue #27](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/27) diff --git a/docs/deployment-guides/07-host-maintenance-notifications.md b/docs/deployment-guides/07-host-maintenance-notifications.md new file mode 100644 index 0000000..c8d8e52 --- /dev/null +++ b/docs/deployment-guides/07-host-maintenance-notifications.md @@ -0,0 +1,149 @@ +# Host-Wartungsbenachrichtigungen (Pre-Update Mail & Matrix) + +**Status**: ✅ Deployed + live getestet (2026-07-29/30, Closes Issue #24) +**Konfiguration**: `host-config/maintenance-notify/` (nicht via Flux/GitOps deployt - siehe unten warum) + +## Überblick + +Der Host läuft bereits mit aktivem `unattended-upgrades` +(`APT::Periodic::Update-Package-Lists`/`Unattended-Upgrade` in +`/etc/apt/apt.conf.d/20auto-upgrades`, Standard-Origins-Pattern deckt +`Debian`+`Debian-Security` ab). Das ist unabhängig von diesem Dokument und war schon vor +Issue #24 aktiv - nur nie dokumentiert. + +Was hier ergänzt wird: eine Benachrichtigung **vor** dem täglichen Update-Lauf, per E-Mail +und Matrix, damit man weiß "gleich läuft ein Update" und im Störungsfall danach sofort den +Zusammenhang sieht. `Unattended-Upgrade::Mail` (auskommentiert in +`50unattended-upgrades`) wäre keine Alternative gewesen: die feuert nur *nach* dem Lauf und +braucht ohnehin ein lokales `mailx`-Setup. + +Diese Anleitung ist bewusst **generisch** gehalten - sie funktioniert für jeden Fork dieses +Homeserver-Stacks, nicht nur für axion1337.chat. Alle instanzspezifischen Werte (Domain, +Matrix-Raum, Mail-Adressen) stecken in einer separaten Config-Datei, nicht im Skript selbst. +Ein konkretes, reales Beispiel (axion1337.chat) steht am Ende. + +## Warum nicht via Flux/GitOps? + +Alles andere in diesem Repo landet via Flux im Cluster. Diese Automatisierung läuft aber +**auf dem nackten Host** (systemd-Timer, kein Kubernetes-Pod) - dafür existiert in diesem +Repo (noch) kein Deployment-Mechanismus (kein Ansible, kein SOPS-Agent auf dem Host). Das +Skript selbst ist trotzdem hier versioniert (`host-config/maintenance-notify/`), das +Deployment auf den Host erfolgt aber manuell per `scp`/SSH. + +## Architektur + +- **Timing**: `apt-daily-upgrade.timer` führt den echten Update-Lauf aus + (`OnCalendar=*-*-* 6:00`, `RandomizedDelaySec=60m` → tatsächlicher Start irgendwann + zwischen 06:00-07:00, je nach eurer eigenen Konfiguration ggf. abweichend - mit + `systemctl cat apt-daily-upgrade.timer` prüfen). Der neue `maintenance-notify.timer` + feuert **fest** vor diesem Fenster (Default `05:00`, kein Randomize). +- **Prüfung**: `maintenance-notify.sh` ruft `apt-get update` + `unattended-upgrade --dry-run -v` + auf und liest dessen eigene, im Quellcode verifizierte Log-Zeilen (`/usr/bin/unattended-upgrade`): + - `"No packages found that can be upgraded unattended..."` → nichts ansteht, Skript beendet + sich ohne jede Benachrichtigung (kein täglicher Alarm-Spam). + - `"Packages that will be upgraded: "` → genau die Pakete, die der echte Lauf gleich + anfassen wird. +- **Zustellung** (nur wenn Pakete anstehen): + - **Mail** via `msmtp`, Passwort kommt aus `/etc/maintenance-notify/mail-password` + (chmod 600, nie im Repo). + - **Matrix** via `curl` gegen die Client-Server-API, als Reply in einem bestehenden Thread + (`m.relates_to: {rel_type: "m.thread", event_id: ...}`), Bot-Token aus + `/etc/maintenance-notify/matrix-token` (chmod 600, nie im Repo). + +## Voraussetzungen + +- Ein Mail-Provider mit SMTP-Auth (eigenes Postfach zum *Versenden*, nicht zwingend zum + Empfangen - der Empfänger kann eine ganz andere, bereits bestehende Adresse sein). +- Ein Matrix-Raum (und optional ein bestehender Thread darin), in den ein eigener Bot-Account + eingeladen wird. +- Auf dem Host: `msmtp`, `jq`, `uuid-runtime` (`apt-get install -y msmtp jq uuid-runtime`). + +## Deployment + +1. **Bot-Account anlegen** (identisches Muster wie für Draupnir/den Content-Scanner in + [06-moderation-content-scanning.md](06-moderation-content-scanning.md)): + ```bash + kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \ + mas-cli manage register-user maintenance-notify --yes + kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \ + mas-cli manage issue-compatibility-token maintenance-notify + ``` + Der ausgegebene Token wird **manuell** in `/etc/maintenance-notify/matrix-token` auf dem + Host eingetragen (chmod 600) - kein automatisierter Schritt, der Token darf nirgends im + Klartext im Repo landen. + +2. **Bot in den Zielraum einladen UND joinen lassen.** Eine Einladung allein reicht nicht - + der Account muss aktiv beitreten, sonst kann er nicht senden: + ```bash + curl -s -X POST -H "Authorization: Bearer $(cat /etc/maintenance-notify/matrix-token)" \ + "https:///_matrix/client/v3/join/" + ``` + +3. **Skript + systemd-Units auf den Host kopieren** (aus + `host-config/maintenance-notify/` in diesem Repo): + ```bash + scp host-config/maintenance-notify/maintenance-notify.sh :/tmp/ + scp host-config/maintenance-notify/maintenance-notify.{service,timer} :/tmp/ + ssh "sudo install -m 755 /tmp/maintenance-notify.sh /usr/local/bin/maintenance-notify.sh && \ + sudo install -m 644 /tmp/maintenance-notify.service /etc/systemd/system/ && \ + sudo install -m 644 /tmp/maintenance-notify.timer /etc/systemd/system/ && \ + sudo mkdir -p /etc/maintenance-notify && sudo systemctl daemon-reload" + ``` + +4. **Config-Datei anlegen** (`config.example` in diesem Verzeichnis als Vorlage nach + `/etc/maintenance-notify/config` kopieren, alle Werte für eure Instanz anpassen). + **Wichtig**: Matrix-Event-IDs beginnen mit `$` - der `MATRIX_THREAD_EVENT_ID`-Wert muss + single-quoted sein, sonst versucht bash ihn als Variable zu expandieren und schneidet ihn + auf einen leeren String zusammen. + +5. **`msmtprc.template` nach `/etc/msmtprc` kopieren**, Platzhalter ausfüllen, chmod 600. + Passwort selbst kommt nicht hier rein, sondern separat in + `/etc/maintenance-notify/mail-password` (chmod 600, eine Zeile, **kein** SMTP-Passwort + ohne vorheriges eigenes Testen der Zugangsdaten übernehmen - siehe Stolpersteine unten). + +6. **Timer aktivieren**: + ```bash + sudo systemctl enable --now maintenance-notify.timer + ``` + +## Verifikation + +```bash +sudo systemctl start maintenance-notify.service +sudo journalctl -u maintenance-notify.service --no-pager -n 40 +sudo systemctl list-timers maintenance-notify.timer +``` +Bei nichts anstehenden Updates loggt das Skript nur `"No pending upgrades - nothing to notify."` +und beendet sich sauber (kein Fehlerfall). Für einen echten Zustellungstest (Mail + Matrix) +unabhängig vom tatsächlichen Update-Status können die `send_mail`/`send_matrix`-Bausteine aus +dem Skript manuell mit einer Testnachricht nachgestellt werden. + +## Stolpersteine (live gefunden, nicht aus der Doku ableitbar) + +- **Port 465 kann ausgehend blockiert sein, obwohl 587 durchgeht.** Bei axion1337.chat war + ausgehendes SMTPS (465) sowohl zu IONOS als auch testweise zu Gmail dicht (stiller Timeout, + kein aktives Reject - typisch für eine Firewall-Regel auf Cloud-Provider-Ebene), während + 587/STARTTLS problemlos funktionierte. Vor dem Debuggen von Auth-Fehlern erst die reine + TCP-Erreichbarkeit prüfen: `timeout 8 bash -c 'echo > /dev/tcp//'`. +- **`msmtp`'s `passwordeval` nimmt die Ausgabe wörtlich**, inklusive eines eventuellen + Trailing-Newlines aus der Passwort-Datei. `printf %s "$(cat datei)" > datei` entfernt das + zuverlässig. +- **Absender-Domain ≠ Matrix-Server-Domain.** Es ist nicht garantiert, dass das Mail-Postfach + unter derselben Domain läuft wie der Matrix-Homeserver (bei axion1337 z.B. Mail unter + `.de`, Matrix unter `.chat`) - `MAIL_FROM` und der `user`/`from` in `msmtprc` müssen zur + tatsächlichen Mail-Domain passen, nicht zur Matrix-Domain. +- **`MATRIX_HOMESERVER` ist oft eine eigene Subdomain, nicht die Apex-Domain.** Vor dem + Eintragen die eigene `.well-known/matrix/client`-Delegation prüfen + (`curl https:///.well-known/matrix/client`, Feld `m.homeserver.base_url`). +- **535 "Authentication credentials invalid" trotz korrektem Passwort?** Manche + Mail-Provider trennen Postfach-Login und SMTP/IMAP-Zugriff als separaten Schalter in den + Postfach-Einstellungen - vor weiterem Debugging prüfen, ob dieser aktiviert ist. + +## Beispiel: axion1337.chat + +- Homeserver: `https://matrix.axion1337.chat` (nicht die Apex-Domain) +- Matrix-Ziel: Space "operating" → Raum `wartung`, Reply in einem vorab angelegten Thread +- Mail: Absender `wartung@axion1337.de` (eigene Mail-Domain, getrennt von `axion1337.chat`) + über IONOS SMTP (`smtp.ionos.de:587`, STARTTLS), Empfänger die private Hauptadresse des + Betreibers +- Timer: `OnCalendar=*-*-* 05:00` (fest), reales Update-Fenster 06:00-07:00 diff --git a/host-config/maintenance-notify/config.example b/host-config/maintenance-notify/config.example new file mode 100644 index 0000000..78bf2a7 --- /dev/null +++ b/host-config/maintenance-notify/config.example @@ -0,0 +1,28 @@ +# Example for /etc/maintenance-notify/config (host-level, NOT deployed via +# GitOps/Flux - copy manually to the target host and adjust for your own +# instance). Sourced as a plain bash file by maintenance-notify.sh. +# +# The values below are axion1337.chat's own, real configuration - shown as a +# concrete worked example. Replace every value for your own homeserver/room. + +# Your homeserver's base URL (matrix client-server API). This is often a +# dedicated subdomain, NOT your apex domain - check your own +# .well-known/matrix/client delegation (`m.homeserver.base_url`) to be sure. +# For axion1337.chat specifically it's matrix.axion1337.chat, not the apex. +MATRIX_HOMESERVER="https://matrix.axion1337.chat" + +# The room the notification gets posted into. +MATRIX_ROOM_ID="!lmZaajvVboTPxQxXzv:axion1337.chat" + +# The thread to reply into (matrix.to link's event id after the room id). +# IMPORTANT: Matrix event IDs start with "$" - this value MUST be +# single-quoted, otherwise bash will try to expand "$T3MQZgf..." as a +# variable and silently truncate it to an empty string. +MATRIX_THREAD_EVENT_ID='$T3MQZgf-maQwfshCKlCn0bo4DGHn4sZS-8eI9u2V6ZI' + +# Sending identity - must match the "user"/"from" in /etc/msmtprc. +MAIL_FROM="wartung@axion1337.chat" + +# Where the pre-update heads-up actually lands (your everyday inbox, not +# necessarily the sending mailbox above). +MAIL_TO="your-address@example.com" diff --git a/host-config/maintenance-notify/maintenance-notify.service b/host-config/maintenance-notify/maintenance-notify.service new file mode 100644 index 0000000..027ba33 --- /dev/null +++ b/host-config/maintenance-notify/maintenance-notify.service @@ -0,0 +1,8 @@ +[Unit] +Description=Pre-update maintenance notification (Mail + Matrix) +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/maintenance-notify.sh diff --git a/host-config/maintenance-notify/maintenance-notify.sh b/host-config/maintenance-notify/maintenance-notify.sh new file mode 100755 index 0000000..48b0829 --- /dev/null +++ b/host-config/maintenance-notify/maintenance-notify.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Pre-update maintenance notification. Runs as a systemd oneshot service, well +# before apt-daily-upgrade.timer's own window, and tells you (Mail + Matrix) +# which packages are about to be auto-upgraded - so a post-update problem can +# immediately be traced back to "the update ran, that's probably it". +# +# Generic by design: no instance-specific values are hardcoded here. All of +# them live in /etc/maintenance-notify/config - see config.example in this +# same directory, and docs/deployment-guides/07-host-maintenance-notifications.md +# for the full setup guide. +set -euo pipefail + +CONFIG_FILE="/etc/maintenance-notify/config" +MATRIX_TOKEN_FILE="/etc/maintenance-notify/matrix-token" + +if [ ! -f "$CONFIG_FILE" ]; then + echo "Missing $CONFIG_FILE - see docs/deployment-guides/07-host-maintenance-notifications.md" >&2 + exit 1 +fi +# shellcheck source=/dev/null +. "$CONFIG_FILE" + +: "${MATRIX_HOMESERVER:?MATRIX_HOMESERVER not set in $CONFIG_FILE}" +: "${MATRIX_ROOM_ID:?MATRIX_ROOM_ID not set in $CONFIG_FILE}" +: "${MATRIX_THREAD_EVENT_ID:?MATRIX_THREAD_EVENT_ID not set in $CONFIG_FILE}" +: "${MAIL_FROM:?MAIL_FROM not set in $CONFIG_FILE}" +: "${MAIL_TO:?MAIL_TO not set in $CONFIG_FILE}" + +apt-get update -qq + +DRYRUN_OUTPUT="$(unattended-upgrade --dry-run -v 2>&1)" + +# Exact log strings taken from /usr/bin/unattended-upgrade itself (verified +# live on the target host), not guessed - this is the one message emitted +# when there is nothing to do, and the one line emitted with the package +# list otherwise. They're mutually exclusive. +if echo "$DRYRUN_OUTPUT" | grep -q "No packages found that can be upgraded unattended"; then + echo "No pending upgrades - nothing to notify." + exit 0 +fi + +PENDING_PKGS="$(echo "$DRYRUN_OUTPUT" | sed -n 's/^.*Packages that will be upgraded: //p' | tail -1)" + +if [ -z "$PENDING_PKGS" ]; then + echo "No pending upgrade packages parsed - nothing to notify." + exit 0 +fi + +HOST_LABEL="$(hostname -f 2>/dev/null || hostname)" +NOW="$(date '+%Y-%m-%d %H:%M %Z')" + +BODY="Host: ${HOST_LABEL} +Zeitpunkt: ${NOW} + +Im naechsten apt-daily-upgrade.timer-Fenster werden folgende Pakete automatisch aktualisiert: + +${PENDING_PKGS} + +Automatische Vorab-Benachrichtigung, keine Aktion erforderlich." + +send_mail() { + if ! command -v msmtp >/dev/null 2>&1; then + echo "msmtp not installed, skipping mail notification" >&2 + return 1 + fi + { + echo "From: ${MAIL_FROM}" + echo "To: ${MAIL_TO}" + echo "Subject: [${HOST_LABEL}] Anstehendes Update" + echo + echo "$BODY" + } | msmtp -a maintenance-notify -- "${MAIL_TO}" +} + +send_matrix() { + if [ ! -f "$MATRIX_TOKEN_FILE" ]; then + echo "Missing $MATRIX_TOKEN_FILE, skipping Matrix notification" >&2 + return 1 + fi + local token txn_id encoded_room payload + token="$(cat "$MATRIX_TOKEN_FILE")" + txn_id="$(uuidgen)" + encoded_room="$(jq -rn --arg s "$MATRIX_ROOM_ID" '$s|@uri')" + payload="$(jq -n --arg body "$BODY" --arg event_id "$MATRIX_THREAD_EVENT_ID" \ + '{msgtype: "m.text", body: $body, "m.relates_to": {rel_type: "m.thread", event_id: $event_id}}')" + curl -sS -f -X PUT \ + -H "Authorization: Bearer ${token}" \ + -H "Content-Type: application/json" \ + --data "$payload" \ + "${MATRIX_HOMESERVER}/_matrix/client/v3/rooms/${encoded_room}/send/m.room.message/${txn_id}" \ + > /dev/null +} + +mail_ok=0 +matrix_ok=0 +send_mail && mail_ok=1 +send_matrix && matrix_ok=1 + +if [ "$mail_ok" -eq 0 ] && [ "$matrix_ok" -eq 0 ]; then + echo "Both mail and Matrix notification failed" >&2 + exit 1 +fi diff --git a/host-config/maintenance-notify/maintenance-notify.timer b/host-config/maintenance-notify/maintenance-notify.timer new file mode 100644 index 0000000..74f3a26 --- /dev/null +++ b/host-config/maintenance-notify/maintenance-notify.timer @@ -0,0 +1,15 @@ +[Unit] +Description=Daily pre-update maintenance notification, fires before apt-daily-upgrade.timer's window + +[Timer] +# Fixed, unrandomized time - must fire safely before the earliest possible +# start of apt-daily-upgrade.timer. Default apt-daily-upgrade.timer ships as +# OnCalendar=*-*-* 6:00 with RandomizedDelaySec=60m (actual run: 06:00-07:00). +# If your apt-daily-upgrade.timer differs (check with +# `systemctl cat apt-daily-upgrade.timer`), adjust the time below to keep a +# comfortable lead. +OnCalendar=*-*-* 05:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/host-config/maintenance-notify/msmtprc.template b/host-config/maintenance-notify/msmtprc.template new file mode 100644 index 0000000..d672a1f --- /dev/null +++ b/host-config/maintenance-notify/msmtprc.template @@ -0,0 +1,24 @@ +# Template for /etc/msmtprc (host-level, NOT deployed via GitOps/Flux - copy +# manually to the target host and fill in the placeholders yourself). +# +# Copy to /etc/msmtprc, replace the __PLACEHOLDER__ values below with your own +# transactional-mail provider's SMTP details, then: +# chmod 600 /etc/msmtprc +# The password itself is NOT stored here - it's read at send-time from +# /etc/maintenance-notify/mail-password (chmod 600, plain text, one line, no +# trailing newline needed either way). + +account maintenance-notify +host __SMTP_HOST__ +port __SMTP_PORT__ +tls on +# Port 465 = implicit TLS (tls_starttls off, as below). If your provider uses +# port 587/STARTTLS instead, flip this to "tls_starttls on". +tls_starttls off +auth on +user __MAIL_FROM__ +passwordeval "cat /etc/maintenance-notify/mail-password" +from __MAIL_FROM__ +logfile /var/log/msmtp.log + +account default : maintenance-notify