chore(coturn): automated TURN shared-secret rotation #46

Closed
sorb wants to merge 76 commits from turn-secret-rotation-20260801-020001 into main
7 changed files with 331 additions and 1 deletions
Showing only changes of commit 1b1fa2b719 - Show all commits
+5 -1
View File
@@ -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)
@@ -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: <liste>"` → 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://<euer-homeserver>/_matrix/client/v3/join/<room-id>"
```
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 <host>:/tmp/
scp host-config/maintenance-notify/maintenance-notify.{service,timer} <host>:/tmp/
ssh <host> "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/<host>/<port>'`.
- **`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://<apex-domain>/.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
@@ -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"
@@ -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
+102
View File
@@ -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
@@ -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
@@ -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