chore(coturn): automated TURN shared-secret rotation #46
+28
-10
@@ -16,11 +16,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
zsh \
|
||||
sudo \
|
||||
openssh-client \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install kubectl
|
||||
RUN curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | tee /etc/apt/sources.list.d/kubernetes.list && \
|
||||
# Install kubectl (apt.kubernetes.io was deprecated/shut down by Google in 2023;
|
||||
# pkgs.k8s.io is the current community-owned repo, versioned per k8s minor release)
|
||||
RUN mkdir -p /etc/apt/keyrings && \
|
||||
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.34/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg && \
|
||||
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.34/deb/ /" | tee /etc/apt/sources.list.d/kubernetes.list && \
|
||||
apt-get update && apt-get install -y kubectl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -30,9 +33,10 @@ RUN curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | b
|
||||
# Install Flux CLI
|
||||
RUN curl -s https://fluxcd.io/install.sh | bash
|
||||
|
||||
# Install sops
|
||||
RUN SOPS_VERSION=$(curl -s https://api.github.com/repos/getsops/sops/releases/latest | grep tag_name | cut -d '"' -f 4) && \
|
||||
curl -sL -o /usr/local/bin/sops https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.amd64 && \
|
||||
# Install sops (arch resolved at build time, same reasoning as the Docker CLI step below)
|
||||
RUN SOPS_ARCH=$(dpkg --print-architecture) && \
|
||||
SOPS_VERSION=$(curl -s https://api.github.com/repos/getsops/sops/releases/latest | grep tag_name | cut -d '"' -f 4) && \
|
||||
curl -sL -o /usr/local/bin/sops https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.${SOPS_ARCH} && \
|
||||
chmod +x /usr/local/bin/sops
|
||||
|
||||
# Install age
|
||||
@@ -40,17 +44,31 @@ RUN apt-get update && apt-get install -y age && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Docker CLI (for interacting with Docker daemon)
|
||||
# arch is resolved at build time so this works on both amd64 (cloud/CI) and arm64 (Apple Silicon) hosts
|
||||
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
|
||||
apt-get update && apt-get install -y docker-ce-cli && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create a non-root user 'vscode' for development
|
||||
RUN useradd -m -s /bin/bash -G docker vscode && \
|
||||
# groupadd is needed because only the Docker CLI (not the daemon) is installed above,
|
||||
# so the 'docker' group is never created as a package side effect
|
||||
RUN groupadd docker && \
|
||||
useradd -m -s /bin/zsh -G docker vscode && \
|
||||
echo "vscode ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/vscode
|
||||
|
||||
# Install oh-my-zsh for better shell experience
|
||||
RUN su - vscode -c "sh -c '$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)' '' --unattended"
|
||||
RUN curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -o /tmp/install-omz.sh && \
|
||||
su - vscode -c "sh /tmp/install-omz.sh --unattended" && \
|
||||
rm /tmp/install-omz.sh
|
||||
|
||||
# Entrypoint runs as root to reconcile the docker group's GID against the mounted
|
||||
# socket (see docker-init.sh), then drops to 'vscode' for the actual session/command.
|
||||
# Stays root-owned at the PID 1 level; VS Code's own `docker exec -u vscode` sessions
|
||||
# and the entrypoint's `gosu vscode` both end up correctly grouped either way.
|
||||
COPY docker-init.sh /usr/local/bin/docker-init.sh
|
||||
RUN chmod +x /usr/local/bin/docker-init.sh
|
||||
|
||||
USER vscode
|
||||
WORKDIR /workspace
|
||||
ENTRYPOINT ["/usr/local/bin/docker-init.sh"]
|
||||
CMD ["/bin/zsh"]
|
||||
|
||||
+40
-2
@@ -91,7 +91,7 @@ Der Container mounted `~/.age` automatisch. Setze die Umgebungsvariable:
|
||||
```bash
|
||||
# Im Container-Terminal (SOPS_AGE_KEY_FILE ist bereits automatisch gesetzt!)
|
||||
# Jetzt kannst du Secrets bearbeiten (wird transparent ver-/entschlüsselt):
|
||||
sops apps/production/custom-configs/mas-secrets.sops.yaml
|
||||
sops apps/production/custom-configs/mas-secret.yaml
|
||||
```
|
||||
|
||||
### Schritt 3: VSCode Integration (optional)
|
||||
@@ -130,7 +130,7 @@ kubectl get pods -n matrix
|
||||
flux get helmreleases -A
|
||||
|
||||
# Secrets bearbeiten (mit verschlüsselung)
|
||||
sops apps/production/custom-configs/mas-secrets.sops.yaml
|
||||
sops apps/production/custom-configs/mas-secret.yaml
|
||||
|
||||
# FluxCD Sync erzwingen
|
||||
flux reconcile kustomization production-apps --with-source
|
||||
@@ -193,6 +193,44 @@ Siehe `README.md` → **Issue 3**. Kurz:
|
||||
- `wellKnownDelegation: enabled: false` setzen
|
||||
- Oder `.well-known/matrix/server` manuell auf `elementWeb` weiterleiten
|
||||
|
||||
## ⚠️ Wartungshinweis: Warum dieser Container regelmäßig getestet werden muss
|
||||
|
||||
Der Dockerfile installiert mehrere Tools über externe apt-Repos und Install-Skripte
|
||||
(`pkgs.k8s.io`, `download.docker.com`, GitHub-Releases, `fluxcd.io`/`ohmyzsh.sh`
|
||||
Installer). **Diese Quellen sind nicht unter unserer Kontrolle und können jederzeit
|
||||
brechen** — genau das ist am 2026-07-28 passiert: der Container konnte seit
|
||||
Fertigstellung nie erfolgreich gebaut werden, ohne dass es jemand bemerkt hat, weil
|
||||
niemand ihn zwischenzeitlich tatsächlich gebaut hat. Gefundene und behobene Probleme:
|
||||
|
||||
| # | Problem | Ursache | Fix |
|
||||
|---|---------|---------|-----|
|
||||
| 1 | `apt.kubernetes.io` → `404 Not Found` | Google hat das alte Kubernetes-apt-Repo 2023 abgeschaltet | Umgestellt auf das offizielle Nachfolge-Repo `pkgs.k8s.io` (versioniert pro k8s-Minor-Version, aktuell `v1.34`) |
|
||||
| 2 | `docker-ce-cli` "has no installation candidate" auf Apple Silicon | Repo-Zeile hatte `arch=amd64` hartkodiert, Build lief aber auf arm64 | `arch=$(dpkg --print-architecture)` zur Build-Zeit ermitteln |
|
||||
| 3 | `useradd: group 'docker' does not exist` | Nur die Docker-**CLI** wird installiert (kein Daemon), daher legt kein Paket die `docker`-Gruppe automatisch an | `groupadd docker` explizit vor `useradd` |
|
||||
| 4 | oh-my-zsh-Install schlägt mit Quoting-Fehler fehl | Verschachtelte `sh -c '...'`-Anführungszeichen in einer Zeile | Install-Skript erst in eine Datei laden, dann sauber mit `su - vscode -c "sh /tmp/install-omz.sh --unattended"` ausführen |
|
||||
| 5 | `sops`-Binary war hart auf `linux.amd64` gepinnt | Lief auf Apple Silicon nur zufällig per QEMU-Emulation von Docker Desktop mit, nicht nativ | Arch dynamisch über `dpkg --print-architecture` auflösen (`linux.arm64` / `linux.amd64`) |
|
||||
| 6 | `docker.sock`-Zugriff im Container: `permission denied` | Der gemountete Host-Socket gehört (je nach Docker-Setup) einer Gruppe/GID, die im Container nicht existiert oder nicht der `docker`-Gruppe entspricht (auf Docker Desktop für Mac/Windows z.B. GID 0/root statt einer eigenen `docker`-Gruppe) | `docker-init.sh`: Root-Entrypoint gleicht beim Container-Start die GID der `docker`-Gruppe an den tatsächlich gemounteten Socket an (bzw. tritt der GID-Inhaber-Gruppe bei, falls die GID schon vergeben ist), wechselt danach per `gosu` zu `vscode` |
|
||||
|
||||
**Konsequenz für die Zukunft:** Vor jeder größeren Änderung an `.devcontainer/` (oder
|
||||
mindestens vierteljährlich) einmal real bauen und laufen lassen:
|
||||
|
||||
```bash
|
||||
docker build -f .devcontainer/Dockerfile -t ess-gitops-devcontainer-test .devcontainer
|
||||
docker run --rm \
|
||||
-v ~/.kube:/home/vscode/.kube \
|
||||
-v ~/.age:/home/vscode/.age \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
ess-gitops-devcontainer-test bash -c '
|
||||
kubectl version --client && helm version --short && flux --version && \
|
||||
sops --version && age --version && docker version --format "{{.Server.Version}}" && \
|
||||
id vscode
|
||||
'
|
||||
```
|
||||
|
||||
Wenn `docker version` hier den echten Server, nicht nur die Client-Version zeigt, und
|
||||
`id vscode` die passende Docker-Gruppe/GID auflistet, funktioniert der Socket-Zugriff
|
||||
tatsächlich — nicht nur der Build.
|
||||
|
||||
## 📚 Weitere Ressourcen
|
||||
|
||||
- [Dev Containers Docs](https://containers.dev)
|
||||
|
||||
@@ -50,8 +50,15 @@
|
||||
"HACK",
|
||||
"NOTE",
|
||||
"XXX",
|
||||
"DONE"
|
||||
"DONE",
|
||||
"[ ]",
|
||||
"[x]"
|
||||
],
|
||||
"todo-tree.regex.regex": "(//|#|<!--|;|/\\*|^|^\\s*(-|\\d+.))\\s*($TAGS)",
|
||||
"todo-tree.highlights.customHighlight": {
|
||||
"[ ]": { "background": "#ff000080", "icon": "issue-opened" },
|
||||
"[x]": { "background": "#00ff0080", "icon": "check" }
|
||||
},
|
||||
"todo-tree.tree.showScanModeButton": true,
|
||||
"todo-tree.filtering.includeGlobs": [
|
||||
"**/docs/TASKS.md",
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/bash
|
||||
# Runs as root at container start (before any `docker exec -u vscode` from VS Code).
|
||||
# The docker.sock's GID is only known once the host socket is actually bind-mounted,
|
||||
# so it can't be baked in at image build time - it must be reconciled here, at runtime.
|
||||
set -e
|
||||
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
|
||||
CURRENT_GID=$(getent group docker | cut -d: -f3)
|
||||
if [ -n "$SOCK_GID" ] && [ "$SOCK_GID" != "$CURRENT_GID" ]; then
|
||||
EXISTING_GROUP=$(getent group "$SOCK_GID" | cut -d: -f1)
|
||||
if [ -n "$EXISTING_GROUP" ]; then
|
||||
# GID is already taken by another group (e.g. GID 0/root - Docker Desktop for
|
||||
# Mac/Windows owns the socket this way inside its VM), so join that group
|
||||
# instead of trying to reassign it to 'docker'.
|
||||
usermod -aG "$EXISTING_GROUP" vscode
|
||||
else
|
||||
groupmod -g "$SOCK_GID" docker
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exec gosu vscode "$@"
|
||||
@@ -26,7 +26,7 @@ echo ""
|
||||
echo "📚 Useful commands:"
|
||||
echo " - kubectl get pods -n matrix (check pod status)"
|
||||
echo " - flux get helmreleases -A (check helm releases)"
|
||||
echo " - sops apps/production/custom-configs/mas-secrets.sops.yaml (edit secrets)"
|
||||
echo " - sops apps/production/custom-configs/mas-secret.yaml (edit secrets)"
|
||||
echo ""
|
||||
echo "🔗 For kubeconfig setup:"
|
||||
echo " - Copy your ~/.kube/config to access the cluster"
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
name: Auto-Deploy on Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/**'
|
||||
- 'clusters/**'
|
||||
- '.gitea/workflows/**'
|
||||
|
||||
jobs:
|
||||
verify-and-notify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check YAML Syntax
|
||||
run: |
|
||||
echo "🔍 Validating YAML files..."
|
||||
find apps clusters -name "*.yaml" -type f | while read file; do
|
||||
if ! grep -q "^apiVersion:" "$file"; then
|
||||
echo "⚠️ Warning: $file may not be a valid K8s manifest"
|
||||
fi
|
||||
done
|
||||
echo "✅ YAML validation passed"
|
||||
|
||||
- name: Check for SOPS Encryption
|
||||
run: |
|
||||
echo "🔐 Checking SOPS status..."
|
||||
for file in $(git diff --name-only origin/main...HEAD -- '**/secret*.yaml' '**/credentials*.yaml'); do
|
||||
if grep -q "ENC\[" "$file"; then
|
||||
echo "✅ $file is encrypted"
|
||||
else
|
||||
echo "⚠️ WARNING: $file may not be encrypted!"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Create Deployment Notification
|
||||
run: |
|
||||
echo "📤 Flux will reconcile changes within 1 minute"
|
||||
echo "🔗 Monitor in Gitea: Projects → Releases (check tags)"
|
||||
|
||||
- name: List Changed Files
|
||||
run: |
|
||||
echo "📋 Files changed in this push:"
|
||||
git diff --name-only origin/main...HEAD
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Create Release on Milestone Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'm*-*-complete'
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Extract Milestone Info
|
||||
id: milestone
|
||||
run: |
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
TITLE=$(git tag -l "$TAG" -n1 | awk '{print substr($0, index($0, $2))}')
|
||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
||||
echo "title=$TITLE" >> $GITHUB_OUTPUT
|
||||
echo "🏷️ Milestone: $TAG"
|
||||
echo "📝 Title: $TITLE"
|
||||
|
||||
- name: Create Release
|
||||
run: |
|
||||
echo "📦 Creating release for milestone: ${{ steps.milestone.outputs.tag }}"
|
||||
echo "${{ steps.milestone.outputs.title }}" > /tmp/release-notes.txt
|
||||
echo "Created: $(date)" >> /tmp/release-notes.txt
|
||||
cat /tmp/release-notes.txt
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
.claude/
|
||||
@@ -0,0 +1,26 @@
|
||||
# Leichter Verifikations-Job, portiert aus .gitea/workflows/deploy-on-push.yml
|
||||
# (Gitea-CI-Rueckbau, siehe Backlogs CFGMON-11). Deployt nichts - Flux reconciled
|
||||
# weiterhin selbststaendig aus dem Gitea-Mirror. Repo-Topologie: git.lab ist
|
||||
# kanonisch, rohana/Gitea ist Push-Mirror und Flux-Quelle.
|
||||
|
||||
verify:
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
changes:
|
||||
- apps/**/*
|
||||
- clusters/**/*
|
||||
- .gitlab-ci.yml
|
||||
script:
|
||||
- apk add --no-cache git >/dev/null
|
||||
- |
|
||||
echo "YAML-Manifest-Check..."
|
||||
find apps clusters -name "*.yaml" -type f | while read f; do
|
||||
grep -q "^apiVersion:" "$f" || echo "WARN: $f enthaelt kein apiVersion - evtl. kein K8s-Manifest"
|
||||
done
|
||||
- |
|
||||
echo "SOPS-Check der in diesem Push geaenderten Secret-Dateien..."
|
||||
for f in $(git diff --name-only HEAD~1..HEAD -- '**/secret*.yaml' '**/credentials*.yaml' 2>/dev/null || true); do
|
||||
if grep -q "ENC\[" "$f"; then echo "OK: $f ist verschluesselt"; else echo "WARNUNG: $f ist moeglicherweise NICHT verschluesselt!"; fi
|
||||
done
|
||||
- echo "Flux reconciled die Aenderungen innerhalb ~1 Minute (Quelle Gitea-Mirror)."
|
||||
@@ -0,0 +1,341 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Overview
|
||||
|
||||
This is a **GitOps-based Kubernetes deployment** of **Element Server Suite (ESS Community v26.4.0)**, a complete Matrix homeserver stack. The repository contains Infrastructure-as-Code using **FluxCD** for GitOps synchronization, with encryption (SOPS/age), service mesh (Traefik), certificate management (Cert-Manager), and auxiliary services like Authentik, TURN/coturn, Draupnir (moderation), ClamAV (content scanning), and Grafana monitoring.
|
||||
|
||||
**Key Stack Components:**
|
||||
- **K3s**: Lightweight Kubernetes distribution running on Hetzner Cloud
|
||||
- **FluxCD**: GitOps controller that watches this repository and auto-syncs changes
|
||||
- **ESS (Matrix Umbrella Chart v26.4.0)**: Synapse, MAS (Matrix Authentication Service), ElementWeb, MatrixRTC
|
||||
- **Authentik**: OIDC-based identity provider for centralized authentication, deployed both via HelmRelease and declarative Blueprints (`apps/authentik/authentik-blueprints.yaml`) for flows/OIDC-provider config that would otherwise only exist as manual admin-UI clicks
|
||||
- **Traefik**: Ingress controller (built into K3s) for routing HTTP/HTTPS traffic
|
||||
- **Cert-Manager**: Automatic TLS certificate provisioning from Let's Encrypt
|
||||
- **SOPS + age**: Transparent encryption/decryption of secrets in Git
|
||||
- **Monitoring**: Grafana Alloy (agent), Prometheus (metrics), Loki (logs)
|
||||
- **coturn**: TURN/STUN server for WebRTC audio/video calls, with monthly automated shared-secret rotation via CronJob + PR workflow
|
||||
- **Draupnir**: Matrix moderation bot (community successor to Mjolnir), ban lists/policy rooms
|
||||
- **ClamAV**: Content scanning — a Synapse module for unencrypted-room uploads, plus a standalone `clamav-http-scanner` service that a patched Element Web client (ThreadNet-Web) calls both on send and on receive, extending coverage to encrypted rooms/DMs
|
||||
- **NetworkPolicies**: default-deny-with-explicit-allow across `matrix` and `authentik` namespaces
|
||||
- **`host-config/`**: the one part of this repo that is deliberately **not** managed by Flux/GitOps — see "Host-Level (non-GitOps) Changes" below
|
||||
|
||||
## Repo Topology (since 2026-07-31)
|
||||
|
||||
Canonical repo is **`git.lab/axion1337.chat/axion1337.chat-gitops`** (homelab GitLab,
|
||||
resolvable only inside the lab) — all pushes go there; a push-mirror updates the Gitea
|
||||
copy on `rohana.axion1337.de`, which remains the **Flux source** (the cluster pulls from
|
||||
Gitea; the mirror delivers). **Never push directly to Gitea** for this repo — the mirror
|
||||
force-overwrites divergent state. Issues/wiki/releases stay on Gitea. The same rule
|
||||
applies to ThreadNet-Web, threadnet-call, thread-net-git and threadnet-operating; only
|
||||
the `Backlogs` repo (and this repo's wiki) are still direct-to-Gitea.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
gitops/
|
||||
├── clusters/matrix/ # Flux GitRepository definition; entry point for reconciliation
|
||||
├── apps/
|
||||
│ ├── base/
|
||||
│ │ ├── infra/ # Core infrastructure (Cert-Manager, Namespaces, etc.)
|
||||
│ │ └── matrix/ # HelmRepository definition for ESS OCI chart
|
||||
│ ├── production/ # Main ESS deployment
|
||||
│ │ ├── element-server-suite.yaml # HelmRelease (ESS chart v26.4.0)
|
||||
│ │ ├── custom-configs/ # Overrides & custom configurations
|
||||
│ │ │ ├── synapse-values.yaml # Synapse customizations (ConfigMap)
|
||||
│ │ │ ├── element-values.yaml # ElementWeb customizations (ConfigMap)
|
||||
│ │ │ └── mas-secret.yaml # MAS secrets (encrypted with SOPS)
|
||||
│ │ ├── cert-issuer.yaml # Let's Encrypt ClusterIssuer
|
||||
│ │ ├── apex-ingress.yaml # Apex-domain IngressRoutes (Element Web, /_scan, etc.)
|
||||
│ │ ├── matrix-postgres-auth.yaml # PostgreSQL credentials
|
||||
│ │ ├── coturn.yaml / coturn-secret.yaml / synapse-turn-secret.yaml
|
||||
│ │ ├── turn-secret-rotation.yaml # Monthly CronJob, rotates coturn shared secret via PR
|
||||
│ │ ├── draupnir.yaml / draupnir-pvc.yaml / draupnir-secret.yaml
|
||||
│ │ ├── clamav.yaml / clamav-pvc.yaml / clamav_spam_checker.py # Synapse-side scan module
|
||||
│ │ ├── clamav-http-scanner.py / -Dockerfile / .yaml # Client-side scan service
|
||||
│ │ ├── synapse-backup.yaml / synapse-backup-secret.yaml
|
||||
│ │ └── networkpolicy.yaml # Default-deny + explicit allow rules
|
||||
│ ├── authentik/ # Identity Provider (separate namespace)
|
||||
│ │ ├── authentik.yaml # HelmRelease
|
||||
│ │ ├── authentik-blueprints.yaml # Flows/OIDC-provider as declarative code
|
||||
│ │ ├── helm-repo.yaml # HelmRepository source
|
||||
│ │ ├── ingress.yaml # Ingress route
|
||||
│ │ ├── networkpolicy.yaml
|
||||
│ │ └── authentik-secret.yaml # Secrets (admin password, OIDC client secret, etc.)
|
||||
│ └── monitoring/ # Observability (Alloy, kube-state-metrics, node-exporter)
|
||||
│ ├── alloy-config.yaml # Grafana Alloy configuration
|
||||
│ └── kube-state-metrics.yaml # K8s metrics exporter
|
||||
├── host-config/ # Host-level (non-GitOps) config, see below
|
||||
│ └── maintenance-notify/ # systemd timer: pre-update mail/Matrix notifications (Issue #24)
|
||||
├── .sops.yaml # SOPS encryption rules (age key definition)
|
||||
├── scripts/
|
||||
│ ├── install-hooks.sh # Installs git hooks for ConfigMap auto-tracking
|
||||
│ └── hooks/ # Git hooks (pre-commit, post-commit, etc.)
|
||||
└── docs/
|
||||
├── README.md # Main deployment guide
|
||||
├── TASKS.md # Task list & milestones (backlog itself lives in Gitea issues)
|
||||
├── install.md # Installation instructions
|
||||
├── ops-configmap-sync.md # ConfigMap syncing with git hooks
|
||||
└── deployment-guides/ # Detailed guides for specific components (01-07)
|
||||
```
|
||||
|
||||
## Host-Level (non-GitOps) Changes
|
||||
|
||||
Almost everything in this repo is reconciled by Flux. `host-config/` is the deliberate
|
||||
exception: it holds scripts/systemd units meant to run **on the bare Hetzner host itself**
|
||||
(not as a Kubernetes pod), for things Flux structurally can't reach — e.g. host package
|
||||
management. There is no SOPS-on-host or Ansible-equivalent mechanism yet; deployment to the
|
||||
host is manual (`scp`/SSH), and instance-specific values live in a config file on the host
|
||||
(`/etc/<name>/config`), not hardcoded in the versioned script, so the pattern is reusable
|
||||
across forks/other communities running this same stack. See
|
||||
`docs/deployment-guides/07-host-maintenance-notifications.md` for the first (and so far only)
|
||||
example of this pattern.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Flux / GitOps Synchronization
|
||||
|
||||
```bash
|
||||
# Force immediate reconciliation (don't wait for 10-min auto-sync)
|
||||
flux reconcile kustomization flux-system --with-source
|
||||
flux reconcile kustomization production-apps --with-source
|
||||
|
||||
# Check reconciliation status
|
||||
flux get kustomizations -A
|
||||
flux get helmreleases -A
|
||||
|
||||
# View Flux logs
|
||||
kubectl logs -n flux-system deployment/source-controller -f
|
||||
kubectl logs -n flux-system deployment/helm-controller -f
|
||||
```
|
||||
|
||||
### Kubernetes Cluster Status
|
||||
|
||||
```bash
|
||||
# Check pod health in Matrix namespace
|
||||
kubectl get pods -n matrix
|
||||
kubectl get pods -n authentik
|
||||
kubectl get pods -n monitoring
|
||||
|
||||
# Detailed pod inspection
|
||||
kubectl describe pod <pod-name> -n matrix
|
||||
kubectl logs <pod-name> -n matrix -f
|
||||
|
||||
# Check all services and ingresses
|
||||
kubectl get svc -n matrix
|
||||
kubectl get ingress -n matrix
|
||||
```
|
||||
|
||||
### Certificate Management (Let's Encrypt / Cert-Manager)
|
||||
|
||||
```bash
|
||||
# View certificate status
|
||||
kubectl get certificate -n matrix
|
||||
kubectl get certificaterequest -n matrix
|
||||
kubectl get challenges -n matrix
|
||||
|
||||
# Debug failed certificate issuance
|
||||
kubectl describe challenge <challenge-name> -n matrix
|
||||
kubectl logs -n cert-manager deployment/cert-manager -f
|
||||
|
||||
# Inspect the issued certificate
|
||||
kubectl get secret <cert-secret-name> -n matrix -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -text -noout
|
||||
```
|
||||
|
||||
### SOPS Secret Editing
|
||||
|
||||
SOPS transparently encrypts/decrypts secrets using the `age` key specified in `.sops.yaml`. The environment variable `SOPS_AGE_KEY_FILE` must point to your age private key.
|
||||
|
||||
```bash
|
||||
# Edit an encrypted secret (decrypted for editing, re-encrypted on save)
|
||||
sops apps/production/custom-configs/mas-secret.yaml
|
||||
sops apps/authentik/authentik-secret.yaml
|
||||
|
||||
# Create a new secret file
|
||||
sops -i --encrypted-regex '^(data|stringData)$' --input-type yaml --output-type yaml new-secret.yaml
|
||||
|
||||
# Decrypt to view
|
||||
sops -d apps/production/custom-configs/mas-secret.yaml
|
||||
```
|
||||
|
||||
Ensure `~/.age/keys.txt` exists and contains your age private key. See `.devcontainer/devcontainer.json` for setup details.
|
||||
|
||||
### Helm Chart Inspection
|
||||
|
||||
```bash
|
||||
# List installed charts
|
||||
helm list -n matrix
|
||||
helm list -n authentik
|
||||
|
||||
# View rendered chart values
|
||||
helm get values matrix-stack -n matrix
|
||||
helm get manifest matrix-stack -n matrix | less
|
||||
```
|
||||
|
||||
### Useful kubectl Shortcuts
|
||||
|
||||
```bash
|
||||
# Port-forward to access services locally
|
||||
kubectl port-forward -n matrix svc/synapse 8008:8008
|
||||
|
||||
# Execute command inside pod (for debugging)
|
||||
kubectl exec -it <pod-name> -n matrix -- bash
|
||||
|
||||
# Stream logs from multiple pods
|
||||
kubectl logs -n matrix -l app=synapse -f
|
||||
|
||||
# Bootstrap a service/bot account via MAS (no registration_shared_secret in this stack)
|
||||
kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \
|
||||
mas-cli manage register-user <name> --yes
|
||||
kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \
|
||||
mas-cli manage issue-compatibility-token <name>
|
||||
```
|
||||
|
||||
## Architecture & Key Concepts
|
||||
|
||||
### FluxCD Reconciliation Flow
|
||||
|
||||
1. **Flux watches** `clusters/matrix/` for a FluxRepository resource pointing to this Git repo
|
||||
2. **Kustomization stages** pull in configurations in order:
|
||||
- `flux-system` (FluxCD itself)
|
||||
- `infra-apps` (Namespaces, RBAC, Cert-Manager, HelmRepository sources)
|
||||
- `production-apps` (Main ESS deployment and related services)
|
||||
3. **HelmReleases** specify which charts to install and what values to use
|
||||
4. **ConfigMaps/Secrets** provide values from files in the repo (e.g., custom Synapse config)
|
||||
5. **Flux auto-reconciles** every 10 minutes, or immediately if Git changes are detected
|
||||
|
||||
### Element Server Suite (ESS) Chart Constraints
|
||||
|
||||
The ESS Helm chart (v26.4.0) has strict validation and specific quirks:
|
||||
|
||||
- **No `config:` blocks for core components** — use ConfigMap overrides instead
|
||||
- **`serverName` must be at root level**, not nested under `synapse`
|
||||
- **TLS in Ingress blocks is forbidden** — use `certManager: true` at root to auto-manage certificates
|
||||
- **`camelCase` for component names**: `elementWeb`, `synapseAdmin`, `matrixAuthenticationService`, etc.
|
||||
- **OCI HelmRepository only** — the chart is distributed via `oci://ghcr.io/element-hq/ess-helm`, not HTTP
|
||||
- **Values must pass JSON schema validation** — invalid configs will cause reconciliation failures with cryptic schema errors
|
||||
|
||||
### NetworkPolicy Convention
|
||||
|
||||
Default-deny-with-explicit-allow across `matrix` and `authentik` namespaces
|
||||
(`apps/production/networkpolicy.yaml`, `apps/authentik/networkpolicy.yaml`). Every new pod
|
||||
needs its own explicit ingress-allow rule; NetworkPolicy matches on named **container ports**,
|
||||
not Service ports — a frequent source of live incidents when a new component is added (wrong
|
||||
port number/name silently blocks all traffic to it).
|
||||
|
||||
### Known Issues & Workarounds
|
||||
|
||||
**Issue: Let's Encrypt ACME Race Condition (Error 403 Order's status is processing)**
|
||||
- Symptom: Certificate provisioning hangs when `elementWeb` and `wellKnownDelegation` are both enabled on the same domain
|
||||
- Cause: Both request certificates for the same domain simultaneously; Let's Encrypt rejects concurrent requests
|
||||
- Fix: Set `wellKnownDelegation: enabled: false` and serve `.well-known/matrix/server` via a separate Ingress route or static file
|
||||
|
||||
**Issue: HelmChart not ready / stat no such file or directory**
|
||||
- Cause: Attempting to use a GitRepository source for the ESS chart (it has sub-charts that don't render correctly)
|
||||
- Fix: Use the OCI HelmRepository source (`oci://ghcr.io/element-hq/ess-helm`) instead
|
||||
|
||||
**Issue: Certificate validation failures (No resources found)**
|
||||
- Cause: Manual Kustomize patches conflict with the Helm chart's built-in certificate management
|
||||
- Fix: Remove manual patches; rely on `certManager: true` at the root level of HelmRelease values
|
||||
|
||||
**Issue: Synapse module can't use asyncio**
|
||||
- Cause: Synapse runs on Twisted's reactor, not a running asyncio event loop — `asyncio.open_connection`/`asyncio.wait_for` inside a Synapse module (e.g. `clamav_spam_checker.py`) fail immediately with `RuntimeError: no running event loop`, and can silently trigger a fail-open path instead of an obvious crash
|
||||
- Fix: use `twisted.internet.reactor`/`HostnameEndpoint`/`connectProtocol` + a custom `Protocol` subclass; Twisted `Deferred`s are natively awaitable from `async def` inside Synapse. Standalone processes outside Synapse (e.g. `clamav-http-scanner.py`) don't have this constraint and can use plain sockets/asyncio.
|
||||
|
||||
### SOPS Encryption & Key Management
|
||||
|
||||
- `.sops.yaml` defines encryption rules (currently using `age` keys)
|
||||
- Secrets matching the regex in `.sops.yaml` are automatically encrypted when committed
|
||||
- The age private key (`~/.age/keys.txt`) must be available in your environment for decryption
|
||||
- In the cluster, Flux decrypts secrets "on the fly" using a secret stored in `flux-system` namespace
|
||||
|
||||
To rotate SOPS keys:
|
||||
```bash
|
||||
# Regenerate and re-encrypt all secrets
|
||||
sops updatekeys -y apps/
|
||||
```
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Before Making Changes
|
||||
|
||||
1. **Understand dependencies** — check `kustomization.yaml` files to see the order of resource creation
|
||||
2. **Verify chart schema** — review ESS chart documentation for constraints on the version being used
|
||||
3. **Test locally if possible** — use `kubectl` port-forwards to verify connectivity before pushing changes
|
||||
|
||||
### Making Changes
|
||||
|
||||
1. **Edit ConfigMap files directly** — for non-secret customizations (Synapse config, Element Web themes, etc.)
|
||||
- Changes are auto-tracked by git hooks installed via `./scripts/install-hooks.sh`
|
||||
2. **Edit secrets with SOPS** — `sops` transparently decrypts/re-encrypts on save
|
||||
3. **Update HelmRelease values** — modify the `values` section in `element-server-suite.yaml` or reference ConfigMap sources
|
||||
|
||||
### After Committing
|
||||
|
||||
1. **Flux auto-detects changes** within ~1 minute (or manually trigger with `flux reconcile kustomization production-apps`)
|
||||
2. **Monitor reconciliation** — watch pod logs and Flux status for errors
|
||||
3. **Test functionality** — verify services are accessible and functioning as expected
|
||||
|
||||
### Git Hooks
|
||||
|
||||
After cloning, run:
|
||||
```bash
|
||||
./scripts/install-hooks.sh
|
||||
```
|
||||
|
||||
This installs hooks that automatically commit ConfigMap changes to `.gitignore`-like tracking. See `docs/ops-configmap-sync.md` for details.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### Local Machine Prerequisites
|
||||
|
||||
- `kubectl` — cluster communication
|
||||
- `flux` — GitOps CLI
|
||||
- `helm` — chart inspection & debugging
|
||||
- `sops` & `age` — secret management
|
||||
- `git` — version control
|
||||
- age key file at `~/.age/keys.txt` (request from team)
|
||||
- kubeconfig at `~/.kube/config` (request from team)
|
||||
|
||||
### DevContainer (Recommended)
|
||||
|
||||
The `.devcontainer/` configuration provides a pre-configured environment:
|
||||
|
||||
```bash
|
||||
# In VS Code: "Reopen in Container"
|
||||
# Or manually:
|
||||
docker build -t ess-devcontainer .devcontainer
|
||||
docker run -it --rm \
|
||||
-v ~/.kube:/home/vscode/.kube \
|
||||
-v ~/.age:/home/vscode/.age \
|
||||
-v ~/.ssh:/home/vscode/.ssh \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
ess-devcontainer
|
||||
```
|
||||
|
||||
DevContainer includes:
|
||||
- All required CLI tools (kubectl, flux, helm, sops, age, git, docker)
|
||||
- VS Code extensions for YAML, Kubernetes, Helm
|
||||
- Proper environment variables (`KUBECONFIG`, `SOPS_AGE_KEY_FILE`)
|
||||
- Git hooks pre-installed
|
||||
|
||||
## Troubleshooting Checklist
|
||||
|
||||
- **Pod not starting?** → `kubectl describe pod <name> -n matrix` (check events)
|
||||
- **Image pull failures?** → Check HelmRelease status: `kubectl get helmrelease -n matrix`
|
||||
- **Secret not found?** → Verify SOPS decryption: `sops -d <secret.sops.yaml>` (must output valid YAML)
|
||||
- **Certificate stuck?** → `kubectl describe certificate <name> -n matrix` (check for ACME errors)
|
||||
- **Config validation error?** → Inspect HelmRelease status: `kubectl describe helmrelease <name> -n matrix` (JSON schema error message)
|
||||
- **Cluster unreachable?** → Verify kubeconfig: `kubectl get nodes` (must connect to K3s)
|
||||
- **NetworkPolicy blocking a new pod?** → Check it matches on container port name, not Service port
|
||||
|
||||
## Resources & References
|
||||
|
||||
- **README.md** — High-level overview and architecture
|
||||
- **docs/TASKS.md** — Task backlog, milestones, and priority list (open backlog lives in [Gitea issues](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues))
|
||||
- **docs/deployment-guides/** — Detailed setup guides for specific components (01-07)
|
||||
- **docs/ops-configmap-sync.md** — Git hook configuration and auto-sync behavior
|
||||
- **ESS Chart Docs** — `https://github.com/element-hq/ess-helm` (official Helm chart repository)
|
||||
- **FluxCD Docs** — `https://fluxcd.io/docs/` (GitOps reconciliation & Kustomization)
|
||||
- **Matrix Spec** — `https://spec.matrix.org/` (Matrix protocol specification)
|
||||
@@ -4,18 +4,23 @@ Dieses Repository enthält die Infrastruktur-as-Code (IaC) für den Matrix-Homes
|
||||
|
||||
## 📑 Inhaltsverzeichnis
|
||||
|
||||
1. [Voraussetzungen & Lokale Tools](https://www.google.com/search?q=%231-voraussetzungen--lokale-tools)
|
||||
2. [Architektur & Logik des Stacks](https://www.google.com/search?q=%232-architektur--logik-des-stacks)
|
||||
3. [Aufbau des Repositories](https://www.google.com/search?q=%233-aufbau-des-repositories)
|
||||
4. [Das Deployment (Aktueller Stand)](https://www.google.com/search?q=%234-das-deployment-aktueller-stand)
|
||||
5. [Nützliche Befehle](https://www.google.com/search?q=%235-n%C3%BCtzliche-befehle)
|
||||
6. [Troubleshooting & Known Issues](https://www.google.com/search?q=%236-troubleshooting--known-issues)
|
||||
1. [Voraussetzungen & Lokale Tools](#1-voraussetzungen--lokale-tools)
|
||||
2. [Architektur & Logik des Stacks](#2-architektur--logik-des-stacks)
|
||||
3. [Aufbau des Repositories](#3-aufbau-des-repositories)
|
||||
4. [Das Deployment (Aktueller Stand)](#4-das-deployment-aktueller-stand)
|
||||
5. [Nützliche Befehle](#5-nützliche-befehle)
|
||||
6. [Troubleshooting & Known Issues](#6-troubleshooting--known-issues)
|
||||
7. [Weitere Ressourcen](#7-weitere-ressourcen)
|
||||
|
||||
-----
|
||||
|
||||
## 1\. Voraussetzungen & Lokale Tools
|
||||
|
||||
Um mit diesem Stack zu interagieren (Konfigurationen anzupassen, Secrets zu verschlüsseln, Fehler zu suchen), müssen folgende Tools lokal installiert sein:
|
||||
**Empfohlen: `.devcontainer/` nutzen** ("Reopen in Container" in VS Code, oder `docker build`
|
||||
+ `docker run` manuell, siehe [`.devcontainer/README.md`](.devcontainer/README.md)) - bringt
|
||||
alle unten genannten Tools bereits fertig eingerichtet mit, ohne sie lokal zu installieren.
|
||||
|
||||
Alternativ, um mit diesem Stack zu interagieren (Konfigurationen anzupassen, Secrets zu verschlüsseln, Fehler zu suchen), müssen folgende Tools lokal installiert sein:
|
||||
|
||||
### 🛠️ Benötigte CLI-Tools
|
||||
|
||||
@@ -70,9 +75,10 @@ Das Setup basiert auf einer modernen, modularen GitOps-Architektur:
|
||||
|
||||
* **K3s**: Die leichtgewichtige Kubernetes-Distribution, die als Fundament dient.
|
||||
* **FluxCD**: Der GitOps-Controller. Er überwacht dieses Git-Repository. Ändert sich hier eine Datei, wendet Flux die Änderung automatisch im Cluster an.
|
||||
* **SOPS**: Erlaubt es, Passwörter (z.B. SMTP) verschlüsselt in Git zu speichern. Flux entschlüsselt diese "on the fly" im Cluster.
|
||||
* **SOPS + age**: Erlaubt es, Secrets verschlüsselt in Git zu speichern. Flux entschlüsselt diese "on the fly" im Cluster. Mehrere Secrets nutzen zusätzlich einen zweiten, eng gescopten age-Key für automatisierte Rotation (siehe coturn TURN-Secret unten).
|
||||
* **Traefik**: Der Ingress-Controller (Standard bei K3s). Er leitet Traffic von Port 80/443 an die richtigen internen Pods weiter.
|
||||
* **Cert-Manager**: Spricht mit Let's Encrypt und stellt automatisch gültige TLS-Zertifikate für alle Ingress-Routen aus.
|
||||
* **NetworkPolicies**: Default-Deny Ingress für die `matrix`- und `authentik`-Namespaces, mit expliziten Allow-Regeln pro Komponente (`apps/production/networkpolicy.yaml`, `apps/authentik/networkpolicy.yaml`).
|
||||
|
||||
### Matrix Stack (ESS Community v26.4.0)
|
||||
|
||||
@@ -80,10 +86,26 @@ Die Suite ist ein "Umbrella Chart", das aus mehreren Microservices besteht:
|
||||
|
||||
* **Synapse (`matrix.`):** Das eigentliche Backend (Homeserver) für die Chat-Nachrichten.
|
||||
* **Matrix Authentication Service (MAS) (`account.`):** Der OIDC-basierte Login-Server. Zwingend erforderlich für moderne Matrix-Clients.
|
||||
* **Element Web (`domain.tld`):** Der Web-Client für die Endnutzer.
|
||||
* **Matrix RTC (`mrtc.`):** Die SFU (Selective Forwarding Unit) für Audio-/Video-Calls.
|
||||
* **Element Web (`domain.tld`):** Eigener Fork (`sorb/threadnet-web`) des Web-Clients für die Endnutzer - Custom Themes, Element Desktop Setup-Seiten, Element-Call-Anpassungen.
|
||||
* **Matrix RTC (`mrtc.`):** Die SFU (Selective Forwarding Unit) für Audio-/Video-Calls, mit eigenem Element-Call-Fork (`sorb/threadnet-call`) für höhere Video-Defaults (bis 1440p/60fps).
|
||||
* **coturn:** TURN/STUN-Server für WebRTC hinter NAT (`hostNetwork: true`, außerhalb der NetworkPolicy-Kontrolle, stattdessen über die Hetzner Cloud Firewall abgesichert). Shared Secret wird monatlich automatisiert rotiert.
|
||||
* **PostgreSQL:** Die relationale Datenbank für Synapse und MAS.
|
||||
|
||||
### Identity & Observability
|
||||
|
||||
* **Authentik** (`auth.`, `account.`): OIDC-Identity-Provider für Matrix-Enrollment, Passwort-Recovery und optionales 2FA/Passkey. Flows/Provider/Application deklarativ als Authentik-Blueprints erfasst (`apps/authentik/authentik-blueprints.yaml`), nicht nur in der UI geklickt.
|
||||
* **Monitoring**: Grafana Alloy sammelt Metriken/Logs, Remote-Write zu einem externen Prometheus/Loki-Stack.
|
||||
* **Backups**: Nächtliche, verschlüsselte & deduplizierte Borg-Backups (Postgres-Dumps + Synapse-`media_store`) zu einer Hetzner Storage Box, getrennt nach Namespace, mit eigenen Repos/Passphrasen.
|
||||
|
||||
### Moderation & Content Scanning
|
||||
|
||||
* **Draupnir**: Moderationsbot (Community-Nachfolger von Mjolnir) für Ban-Listen/Policy-Rooms.
|
||||
* **ClamAV**: Zwei Bausteine für unterschiedliche Räume - ein eigenes Synapse-Modul (`clamav_spam_checker.py`) scannt Uploads in unverschlüsselten Räumen; ein zusätzlicher, eigenständiger `clamav-http-scanner`-Dienst wird vom gepatchten Element-Web-Client (`sorb/threadnet-web`) sowohl beim Senden als auch beim Empfangen aufgerufen und deckt damit auch verschlüsselte Räume/DMs ab. Details: `docs/deployment-guides/06-moderation-content-scanning.md`.
|
||||
|
||||
### Host-Level (nicht-GitOps) Änderungen
|
||||
|
||||
* `host-config/` ist bewusst der einzige Teil dieses Repos, den Flux **nicht** verwaltet - Skripte/systemd-Units, die direkt auf dem nackten Hetzner-Host laufen (z.B. `unattended-upgrades`-Vorab-Benachrichtigungen), für Dinge, die strukturell außerhalb der Reichweite von Flux liegen. Deployment erfolgt manuell per SSH, instanzspezifische Werte liegen in einer Config-Datei auf dem Host, nicht im versionierten Skript. Details: `docs/deployment-guides/07-host-maintenance-notifications.md`.
|
||||
|
||||
-----
|
||||
|
||||
## 3\. Aufbau des Repositories
|
||||
@@ -106,9 +128,23 @@ gitops/
|
||||
│ └── custom-configs/ # Eigene Anpassungen (Themes, Logging)
|
||||
│ ├── synapse-values.yaml # Als ConfigMap
|
||||
│ ├── element-values.yaml # Als ConfigMap
|
||||
│ └── mas-secrets.sops.yaml # Als verschlüsseltes SOPS-Secret
|
||||
│ └── mas-secret.yaml # Als verschlüsseltes SOPS-Secret
|
||||
```
|
||||
|
||||
Weitere Secret-Dateien liegen direkt unter `apps/production/` bzw. `apps/authentik/`
|
||||
(z.B. `coturn-secret.yaml`, `synapse-turn-secret.yaml`, `synapse-backup-secret.yaml`,
|
||||
`authentik-backup-secret.yaml`) - jede einzeln SOPS-verschlüsselt, nicht in `custom-configs/`
|
||||
gebündelt.
|
||||
|
||||
### Repo-Topologie (seit 2026-07-31)
|
||||
|
||||
**Kanonisch ist `git.lab/axion1337.chat/axion1337.chat-gitops`** (Homelab-GitLab, nur im
|
||||
Lab auflösbar) — dort wird gepusht und läuft der CI-Verifikations-Job (`.gitlab-ci.yml`).
|
||||
Die Kopie auf `rohana.axion1337.de` ist ein automatischer **Push-Mirror** und bleibt die
|
||||
**Flux-Quelle**: der Cluster zieht unverändert von Gitea, der Mirror liefert. **Niemals
|
||||
direkt nach rohana pushen** — der Mirror überschreibt divergente Stände. Issues, Wiki und
|
||||
Releases bleiben auf Gitea.
|
||||
|
||||
**Abhängigkeits-Logik:** Flux installiert erst `infra-apps` (damit Namespaces und Repositories existieren) und danach `production-apps` (das eigentliche ESS-Chart).
|
||||
|
||||
-----
|
||||
@@ -156,7 +192,10 @@ spec:
|
||||
name: ess-synapse-custom
|
||||
valuesKey: values.yaml
|
||||
- kind: Secret
|
||||
name: ess-mas-custom-secrets
|
||||
name: ess-mas-values-secret
|
||||
valuesKey: values.yaml
|
||||
- kind: Secret
|
||||
name: synapse-turn-secret
|
||||
valuesKey: values.yaml
|
||||
values:
|
||||
serverName: axion1337.chat
|
||||
@@ -216,7 +255,7 @@ kubectl describe challenge <name> -n matrix
|
||||
Um ein Passwort im GitOps-Repo zu ändern, editiert man die verschlüsselte Datei direkt via SOPS (sie wird transparent entschlüsselt und beim Speichern wieder verschlüsselt):
|
||||
|
||||
```bash
|
||||
sops apps/production/custom-configs/mas-secrets.sops.yaml
|
||||
sops apps/production/custom-configs/mas-secret.yaml
|
||||
```
|
||||
|
||||
-----
|
||||
@@ -244,4 +283,14 @@ sops apps/production/custom-configs/mas-secrets.sops.yaml
|
||||
### Issue 4: Fehlende Zertifikate (`No resources found`)
|
||||
|
||||
* **Ursache:** Manuelle Kustomize-Patches kollidieren mit dem Helm-Chart.
|
||||
* **Lösung:** Manuelle Patches löschen und das native Feature des Charts nutzen: `certManager: true` auf der obersten (Root-)Ebene der `values` setzen. Das Chart erstellt daraufhin die korrekten Ingress-Annotations und Secrets von selbst.
|
||||
* **Lösung:** Manuelle Patches löschen und das native Feature des Charts nutzen: `certManager: true` auf der obersten (Root-)Ebene der `values` setzen. Das Chart erstellt daraufhin die korrekten Ingress-Annotations und Secrets von selbst.
|
||||
|
||||
-----
|
||||
|
||||
## 7\. Weitere Ressourcen
|
||||
|
||||
* **`CLAUDE.md`** (Repo-Root): Technische Referenz für KI-gestützte Arbeit an diesem Repo - Architektur, bekannte Chart-Quirks, Troubleshooting-Checkliste.
|
||||
* **`docs/TASKS.md`**: Backlog-Pointer zu den [Gitea Issues](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues) - Details werden nicht mehr doppelt gepflegt.
|
||||
* **[Gitea Releases](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/releases)**: Versionshistorie (SemVer, `vMAJOR.MINOR.PATCH` als Änderungsgrößen-Konvention, kein Kompatibilitätsvertrag - siehe [[00-TASKS]] Wiki für die Konvention).
|
||||
* **[Wiki](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/wiki)**: Ausführliche Historie, Incident-Notizen, Setup-Guides pro Komponente.
|
||||
* **`docs/deployment-guides/`**: Detaillierte Guides für TURN-Server, Authentik, Monitoring, Element-Customization, Room-Policies, Moderation & Content-Scanning, Host-Wartungsbenachrichtigungen.
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: authentik-backup-credentials
|
||||
namespace: authentik
|
||||
stringData:
|
||||
borg-passphrase: ENC[AES256_GCM,data:5PApz4TqSNN2vVXeFSuomd051nl+cYk+a+STViwddG/Hj7XWQ099vvTlKSE=,iv:MFG04/66YqtOjZWsLpy236MYwR05z91ngOQ0BmLNxzA=,tag:gxD5prW8Ted3Q2ZY3sYSYQ==,type:str]
|
||||
ssh-private-key: ENC[AES256_GCM,data:87PrkqAIH2xRCu3jlAK3Ts24XKRDoXNCWLomRhizCY43hwLeY5fNzfsGnMTKBt5IxL2bIljx+pbh/Y8IO38ISUUbUAczfi1KeN1gg69wKgqNAlckcmD0dkA2N2F0ZnEFX/rkad68YR8Hw1/A5h3qGUd5FwoVWzIFD/diY86s/LiQBYzwD4bYLUACHqIL7+QyJ6jEsFzA3tLwDySpUpM9OAjFQF0EazObChNYqe/qrc8i1gI8CptWOFkqiZ8Pen1E79gg/zgleOBqSmlcefgZk+JFPAfvORTHkDrHW9X/bLygaTQvskmGNzaKHpe2s/bVafWb7G60ROrpdjYIJfS7c0baRCSrd9kwdM4JQC2gSsMkI4v6oeFSvkTrb684B10zacZdiTKfySDV+Ry4hCZofF2RhAnsTbOpo0XANoOi4InoxUqGPfctVfUCVb6X+HIR5XYxpLrNwWqRpscMP2P3RJsaUZOinmQUKrHX/nMtPZ/ltpwjhQeAXZTtdyPxTCjHAh6fzouuHk66vlCdukXsMQDBdIfdA/3jjGbW,iv:yFNKkdegLLq8jq1Ya6v67urSJTdG3Ge4ZbmKizqQhmk=,tag:6l/qMb1d1oD8sfVEebW6gQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBWMllTL2hRcEhCT2MxcWZy
|
||||
dkxndmlZRW5FZ2NiWWdFckxqYTFKS1B6SXlvCjJySXRsZThvczNnWjlyM0N2Snha
|
||||
eitvWUhhdmU4U0V3OG55WjdLbU9KcUEKLS0tIEM3MStzNUJhaGg4M0hKMVF1bnBB
|
||||
RWVqVDRBNmJ0b1Bwd0l3dWxPT3Q0SU0KOEoyejkH4RC0p8ka3FjI7MyzRJg+uu7h
|
||||
j3wf1q+Hgg73djDBSPYJkrB6Bdl4YMwo8SzbtW8O9elDE0qAioR0bQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-07-28T18:20:58Z"
|
||||
mac: ENC[AES256_GCM,data:lwwNbxSxtgDTDmaWMU1uf7TBOEw8gFBFKb982VIsGMeM0fIPHvX12Qts90MNgYIJliOWgAWrwyvAgfXWKuE37RNo+BtyfHCWi4IESKSN/RJrd/yMpRKx+02rifH3nl26ZCAQT1Pa0fjI1SfMhbVzfnD9a/AMARXZMhLRc0OqczY=,iv:SgP2iMtENRtZfw6I9EaOsmvecFNYCIZWqVj+cZ+T7EI=,tag:s4oEyxqyUx6ibqsk4g9xpw==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,85 @@
|
||||
# Nightly Borg backup of the authentik Postgres database to a Hetzner Storage Box
|
||||
# (issues #6 + #15). See apps/authentik/authentik-backup-secret.yaml for the SSH key +
|
||||
# Borg repo passphrase, and apps/production/synapse-backup.yaml for the matrix-side job
|
||||
# (same Storage Box, separate repo/passphrase, offset schedule).
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: authentik-backup-known-hosts
|
||||
namespace: authentik
|
||||
data:
|
||||
known_hosts: |
|
||||
[u641795.your-storagebox.de]:23 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIICf9svRenC/PLKIL9nk6K/pxQgoiFC41wTNvoIncOxs
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: authentik-backup
|
||||
namespace: authentik
|
||||
spec:
|
||||
schedule: "15 3 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: authentik-backup
|
||||
app.kubernetes.io/component: backup
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: backup
|
||||
image: rohana.axion1337.de/sorb/axion-backup:v2
|
||||
env:
|
||||
- name: BORG_REPO
|
||||
value: "ssh://u641795@u641795.your-storagebox.de:23/./authentik-backup"
|
||||
- name: BORG_PASSPHRASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-backup-credentials
|
||||
key: borg-passphrase
|
||||
- name: SSH_PRIVATE_KEY_FILE
|
||||
value: /secrets/ssh/ssh-private-key
|
||||
- name: SSH_KNOWN_HOSTS_FILE
|
||||
value: /secrets/known-hosts/known_hosts
|
||||
- name: DB_HOSTS
|
||||
value: "authentik:authentik-postgresql"
|
||||
- name: PGUSER
|
||||
value: authentik
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-credentials
|
||||
key: pg-password
|
||||
volumeMounts:
|
||||
- name: ssh-key
|
||||
mountPath: /secrets/ssh
|
||||
readOnly: true
|
||||
- name: known-hosts
|
||||
mountPath: /secrets/known-hosts
|
||||
readOnly: true
|
||||
- name: scratch
|
||||
mountPath: /scratch
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
volumes:
|
||||
- name: ssh-key
|
||||
secret:
|
||||
secretName: authentik-backup-credentials
|
||||
items:
|
||||
- key: ssh-private-key
|
||||
path: ssh-private-key
|
||||
mode: 0400
|
||||
- name: known-hosts
|
||||
configMap:
|
||||
name: authentik-backup-known-hosts
|
||||
- name: scratch
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,283 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: authentik-blueprints
|
||||
namespace: authentik
|
||||
data:
|
||||
matrix-invitation-flow.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: matrix-invitation-flow
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
# Reaffirm the flow itself (already created manually; matched by slug)
|
||||
- model: authentik_flows.flow
|
||||
state: present
|
||||
identifiers:
|
||||
slug: matrix-invitation
|
||||
id: matrix_invitation_flow
|
||||
attrs:
|
||||
name: matrix-invitation
|
||||
title: matrix-invitation
|
||||
designation: enrollment
|
||||
|
||||
# The prompt stage had accumulated 16 unrelated system validation_policies
|
||||
# (e.g. default-user-settings-authorization, default-oobe-password-usable)
|
||||
# from manual UI setup, likely a "select all" slip in the policy picker.
|
||||
# These crash on an anonymous enrollment context ('AnonymousUser' object
|
||||
# has no attribute 'group_attributes', etc). A prompt stage needs none here.
|
||||
- model: authentik_stages_prompt.promptstage
|
||||
state: present
|
||||
identifiers:
|
||||
name: matrix-invitation-prompt
|
||||
attrs:
|
||||
validation_policies: []
|
||||
|
||||
# Correct stage chain, mirroring the working matrix-enrollment flow:
|
||||
# Invite -> Prompt (username/email/password) -> Write -> Password -> Login
|
||||
# Root cause of the original bug: only Invite+Prompt were bound, both at
|
||||
# order=0, so the flow never wrote the user to the DB or logged them in.
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_invitation_flow
|
||||
order: 0
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_invitation.invitationstage, [name, matrix-enrollment-invitation]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_invitation_flow
|
||||
order: 1
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_prompt.promptstage, [name, matrix-invitation-prompt]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_invitation_flow
|
||||
order: 2
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_user_write.userwritestage, [name, default-source-enrollment-write]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_invitation_flow
|
||||
order: 3
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_password.passwordstage, [name, default-authentication-password]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_invitation_flow
|
||||
order: 4
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_user_login.userloginstage, [name, default-source-enrollment-login]]
|
||||
|
||||
# Without an explicit destination, the flow falls back to Authentik's own
|
||||
# /if/user/ dashboard, which refuses type=external users ("Die Oberflaeche
|
||||
# kann nur von internen Nutzern geoeffnet werden") - exactly the user type
|
||||
# these Matrix-only accounts correctly have. Send them to Element instead.
|
||||
- model: authentik_stages_redirect.redirectstage
|
||||
state: present
|
||||
identifiers:
|
||||
name: matrix-invitation-redirect
|
||||
id: matrix_invitation_redirect_stage
|
||||
attrs:
|
||||
mode: static
|
||||
target_static: https://axion1337.chat
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_invitation_flow
|
||||
order: 5
|
||||
attrs:
|
||||
stage: !KeyOf matrix_invitation_redirect_stage
|
||||
matrix-recovery-flow.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: matrix-recovery-flow
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
# matrix-recovery existed but had zero stage bindings (dead flow), and the
|
||||
# real login flow (default-authentication-flow, used by the MAS OAuth2
|
||||
# provider's authentication_flow) didn't link to it at all - no "Forgot
|
||||
# password?" link was ever shown. Reuses the same default-recovery-*
|
||||
# stages the built-in default-recovery-flow already uses successfully,
|
||||
# plus our own redirect stage instead of falling back to the authentik
|
||||
# dashboard (blocked for type=external Matrix users).
|
||||
- model: authentik_flows.flow
|
||||
state: present
|
||||
identifiers:
|
||||
slug: matrix-recovery
|
||||
id: matrix_recovery_flow
|
||||
attrs:
|
||||
designation: recovery
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_recovery_flow
|
||||
order: 10
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_identification.identificationstage, [name, default-recovery-identification]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_recovery_flow
|
||||
order: 20
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_email.emailstage, [name, default-recovery-email]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_recovery_flow
|
||||
order: 30
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_prompt.promptstage, [name, "Change your password"]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_recovery_flow
|
||||
order: 40
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_user_write.userwritestage, [name, default-recovery-user-write]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_recovery_flow
|
||||
order: 100
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_user_login.userloginstage, [name, default-recovery-user-login]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf matrix_recovery_flow
|
||||
order: 110
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_redirect.redirectstage, [name, matrix-invitation-redirect]]
|
||||
|
||||
# Wire the "Forgot password?" link on the real login flow used by MAS
|
||||
- model: authentik_stages_identification.identificationstage
|
||||
state: present
|
||||
identifiers:
|
||||
name: default-authentication-identification
|
||||
attrs:
|
||||
recovery_flow: !KeyOf matrix_recovery_flow
|
||||
matrix-mfa-setup-redirect.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: matrix-mfa-setup-redirect
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
# 2FA is optional (default-authentication-mfa-validation has
|
||||
# not_configured_action=skip - login never blocks on missing MFA).
|
||||
# Users who want to opt in use these built-in single-stage setup flows
|
||||
# directly (unreachable via /if/user/, which is blocked for type=external
|
||||
# Matrix accounts). Without a stage after the setup itself, completion
|
||||
# fell back to the same blocked /if/user/ dashboard - append our redirect.
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, default-authenticator-totp-setup]]
|
||||
order: 10
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_redirect.redirectstage, [name, matrix-invitation-redirect]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, default-authenticator-webauthn-setup]]
|
||||
order: 10
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_redirect.redirectstage, [name, matrix-invitation-redirect]]
|
||||
matrix-brand-default-app.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: matrix-brand-default-app
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
# Root cause behind several dead ends: an authenticated user hitting "/"
|
||||
# with no other destination (e.g. after logging in mid-way through the
|
||||
# TOTP/WebAuthn setup flows) falls back to Brand.default_application: if
|
||||
# unset, that's /if/user/, which type=external Matrix accounts can't
|
||||
# open. Only affects the bare "/" fallback - explicit URLs like
|
||||
# /if/admin/ are unaffected, so internal/staff access is unchanged.
|
||||
- model: authentik_brands.brand
|
||||
state: present
|
||||
identifiers:
|
||||
domain: authentik-default
|
||||
attrs:
|
||||
default_application: !Find [authentik_core.application, [slug, matrix]]
|
||||
matrix-oidc-provider.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: matrix-oidc-provider
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
# The OIDC Provider + Application linking Authentik to MAS was originally
|
||||
# clicked together by hand in the UI and existed nowhere as code (issue
|
||||
# #36): losing the Authentik DB would have meant re-creating this from
|
||||
# scratch, including a new client_secret that MAS would then no longer
|
||||
# match. client_secret is read from AUTHENTIK_MAS_OIDC_CLIENT_SECRET
|
||||
# (see authentik.yaml HelmRelease values) rather than inlined here,
|
||||
# since this ConfigMap itself is not SOPS-encrypted - the actual value
|
||||
# lives in the authentik-credentials Secret instead.
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
state: present
|
||||
identifiers:
|
||||
name: Matrix Authentication Service
|
||||
id: matrix_mas_provider
|
||||
attrs:
|
||||
client_type: confidential
|
||||
client_id: dHbTAgAgXvjh3VALh220mB3dxcVXAifiXU2ZO3U6
|
||||
client_secret: !Env AUTHENTIK_MAS_OIDC_CLIENT_SECRET
|
||||
# Path includes MAS's own upstream-provider ID, not Authentik's -
|
||||
# must match MAS's config exactly or the OIDC callback breaks.
|
||||
redirect_uris:
|
||||
- matching_mode: strict
|
||||
url: https://account.axion1337.chat/upstream/callback/01KQDJTR1ZVTG8JQ220F5BNBFZ
|
||||
# Stable across username renames - this is what keeps
|
||||
# upstream_oauth_links rows valid after e.g. the elbojoloco rename.
|
||||
sub_mode: hashed_user_id
|
||||
include_claims_in_id_token: true
|
||||
access_code_validity: minutes=1
|
||||
access_token_validity: minutes=5
|
||||
signing_key: !Find [authentik_crypto.certificatekeypair, [name, "authentik Self-signed Certificate"]]
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||
property_mappings:
|
||||
- !Find [authentik_core.propertymapping, [managed, "goauthentik.io/providers/oauth2/scope-openid"]]
|
||||
- !Find [authentik_core.propertymapping, [managed, "goauthentik.io/providers/oauth2/scope-email"]]
|
||||
- !Find [authentik_core.propertymapping, [managed, "goauthentik.io/providers/oauth2/scope-profile"]]
|
||||
|
||||
- model: authentik_core.application
|
||||
state: present
|
||||
identifiers:
|
||||
slug: matrix
|
||||
attrs:
|
||||
name: aXion1337.chat Accountverwaltung
|
||||
provider: !KeyOf matrix_mas_provider
|
||||
meta_description: Matrixclient tailored for aXionCommunity
|
||||
meta_publisher: aXionGaming
|
||||
policy_engine_mode: any
|
||||
open_in_new_tab: false
|
||||
@@ -7,10 +7,10 @@ stringData:
|
||||
secret_key: ENC[AES256_GCM,data:yIyQapbFtFM11LynFtkV3ffExhaDfN9QHeFbI1T0xkIhgsV+9sjg3qwMVmeBlAe7xZl8gsAM4kDj2Q6O91OdDg==,iv:+Cl8vOcxG9/mgRheaCO0bLWyCJXN+f1F2DD3oeHbPFY=,tag:711ytyKf6/tmXomBLoffGA==,type:str]
|
||||
pg-password: ENC[AES256_GCM,data:3w8R9mRjMXMJDLjrC8QYaXFHsCU3yYZs2PcaFQNp3Z4=,iv:G/aXgoGz3vBOzZ5K3Y+DDJsqer4F5gvcMmtkzRx93CU=,tag:dXPs1pY/APvnMlxdvB1EkA==,type:str]
|
||||
smtp-password: ENC[AES256_GCM,data:JpMgaQFPkBzOg5WjvpmhM0kPwvZkH+4tQjT17RJHjG14WjmWtfG9Bg==,iv:zjQRLIlrxKv5hbd4JZowNUEiibiCUMf79acZY0+dYAc=,tag:ORPafTPyOQJvVvHWQGmqhA==,type:str]
|
||||
mas-oidc-client-secret: ENC[AES256_GCM,data:0yx55FroLSxlnuYgfNwczu3PnbPm1kW74JtiU9oFevVqeQDZc385wU6x5X5TN7owXDO7QaOfGTTMvqIpbwQb6Q5Vt1VMToR+0f44oJcktYoTiDFU9Sy6lR/y6nlvBCNqeJg7vIyVpkIqxwqty15EekyqMpkIMp1fT6Pxmek0SO0=,iv:Ey06ljnqbVARDLVt2sLe8R776VEWpTlzI/+Nka5NocA=,tag:I+GNLHz4V8TFa2ijzK5y2Q==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
enc: |
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRekJuZythYzliTFJ3RlhS
|
||||
R2p6TG9NeFdabFlPRWtpNHJMYVVxTWZEcmlRClk0WUorSzdxNlcyWHYwWFBTMnlq
|
||||
@@ -18,7 +18,8 @@ sops:
|
||||
QXVrY1NTeHZkeTlPRWNlVThzWno3T0kKC0KBoLT64GNqb8Ri9u69G7nqb1KftwwP
|
||||
/24aVHrPxKi9d4ij9n3bvCYDF4rhtfexhrE4n7CfuKn2DcSiuTniuw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-04-29T21:43:59Z"
|
||||
mac: ENC[AES256_GCM,data:Y+dJppkaVZ5NOhlvwbbsF5+vDFqGUI1Ps8IcE4J7FIW4HIdMVf6RKM0EInvPUW1LaBlmelCitcE30w0As7ysNRhLY8yUDaKUvuU6mRejlNUIF8wAHzhciL2jTvAQsArHjybJatEig28+wM9VcY8JEa/d/CmuiB9Nq4WbIV+JXlA=,iv:UQj2rIVLNPjtYp3d/jRyNfJyyyUsZ3+NDCgpI4aztzc=,tag:cwiCzG/A+rfRFfLjXVt82w==,type:str]
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-07-28T15:54:53Z"
|
||||
mac: ENC[AES256_GCM,data:P6IF+jukwzldK92nHl6s4h6sS4ldXLwpyLpwv2tpI3vFWgTLEnGCnowi2k5lmWUlITEVmLLC0HvsBuduTiGI2sIHHt+r3RdqkV88HGn6oYDVq5a+Ax7ESfqti/4B7ClQCSxl/tU6hBUFe812DiBXJgA03UJQZn8uHY/dP/RgRpc=,iv:V8sqhbJcKglkKsQmJBdgoxDaCYJ3Wt/qRa18jEviH60=,tag:EiNotrYAKIzKndgjU/kTFQ==,type:str]
|
||||
version: 3.12.2
|
||||
|
||||
@@ -40,6 +40,15 @@ spec:
|
||||
global:
|
||||
security:
|
||||
allowInsecureImages: true
|
||||
# Read by the matrix-oidc-provider blueprint via !Env, so the OAuth2
|
||||
# Provider's client_secret can be captured as code without ever
|
||||
# inlining the live credential into a plain (non-SOPS) ConfigMap.
|
||||
env:
|
||||
- name: AUTHENTIK_MAS_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-credentials
|
||||
key: mas-oidc-client-secret
|
||||
|
||||
authentik:
|
||||
log_level: info
|
||||
@@ -52,6 +61,10 @@ spec:
|
||||
use_tls: true
|
||||
from: "Authentik <gamemaster@axion1337.chat>"
|
||||
|
||||
blueprints:
|
||||
configMaps:
|
||||
- authentik-blueprints
|
||||
|
||||
server:
|
||||
ingress:
|
||||
enabled: false
|
||||
@@ -85,3 +98,8 @@ spec:
|
||||
memory: 256Mi
|
||||
limits:
|
||||
memory: 512Mi
|
||||
# Chart's own generated policy allows ANY pod in ANY namespace on 5432
|
||||
# (see issue #37) - disabled in favor of our own scoped policy in
|
||||
# apps/authentik/networkpolicy.yaml.
|
||||
networkPolicy:
|
||||
enabled: false
|
||||
|
||||
@@ -4,6 +4,11 @@ resources:
|
||||
- namespace.yaml
|
||||
- helm-repo.yaml
|
||||
- authentik-secret.yaml
|
||||
- authentik-blueprints.yaml
|
||||
- certificate.yaml
|
||||
- authentik.yaml
|
||||
- ingress.yaml
|
||||
- networkpolicy.yaml
|
||||
# Backup zur Hetzner Storage Box (Issues #6 + #15)
|
||||
- authentik-backup-secret.yaml
|
||||
- authentik-backup.yaml
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# Default-deny ingress for the authentik namespace, with explicit allow rules for the
|
||||
# traffic paths that actually need to reach in: Traefik (kube-system) for the public
|
||||
# auth.axion1337.chat endpoint and ACME HTTP-01 challenges, and MAS (matrix namespace)
|
||||
# for upstream OIDC calls. Egress is intentionally untouched (federation-equivalent
|
||||
# outbound calls like SMTP aren't restricted here).
|
||||
#
|
||||
# authentik-postgresql: the Bitnami postgresql subchart's own generated NetworkPolicy
|
||||
# restricted the port (5432) but not the source - any pod in any namespace could reach
|
||||
# it (issue #37). Disabled via postgresql.primary.networkPolicy.enabled: false in
|
||||
# authentik.yaml and replaced below with a policy scoped to authentik-server/-worker.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: default-deny-ingress
|
||||
namespace: authentik
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-authentik-server
|
||||
namespace: authentik
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: authentik
|
||||
app.kubernetes.io/component: server
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: matrix
|
||||
ports:
|
||||
# NetworkPolicy matches the pod's actual container port, not the Service's
|
||||
# external port - the authentik-server Service maps 80->9000, 443->9443.
|
||||
- protocol: TCP
|
||||
port: 9000
|
||||
- protocol: TCP
|
||||
port: 9443
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-authentik-postgresql
|
||||
namespace: authentik
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgresql
|
||||
app.kubernetes.io/component: primary
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: authentik
|
||||
app.kubernetes.io/component: server
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: authentik
|
||||
app.kubernetes.io/component: worker
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: authentik-backup
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-acme-solver
|
||||
namespace: authentik
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
acme.cert-manager.io/http01-solver: "true"
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8089
|
||||
Vendored
BIN
Binary file not shown.
@@ -102,7 +102,7 @@ data:
|
||||
// Scrape Synapse metrics
|
||||
prometheus.scrape "synapse" {
|
||||
targets = [{
|
||||
__address__ = "matrix-stack-synapse-main.matrix.svc.cluster.local:9000",
|
||||
__address__ = "matrix-stack-synapse-main.matrix.svc.cluster.local:9001",
|
||||
}]
|
||||
forward_to = [prometheus.remote_write.selendis.receiver]
|
||||
scrape_interval = "30s"
|
||||
|
||||
@@ -35,6 +35,14 @@ spec:
|
||||
services:
|
||||
- name: element-web-docs
|
||||
port: 80
|
||||
# Client-seitiger ClamAV-Scan-Dienst (Issue #19-Erweiterung: Scanning auch für
|
||||
# verschlüsselte Räume, direkt vom Browser aus aufgerufen)
|
||||
- match: Host(`axion1337.chat`) && PathPrefix(`/_scan`)
|
||||
kind: Rule
|
||||
priority: 50
|
||||
services:
|
||||
- name: clamav-http-scanner
|
||||
port: 8090
|
||||
# Niedrigere Priorität: alles andere -> Element Web
|
||||
- match: Host(`axion1337.chat`)
|
||||
kind: Rule
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
COPY clamav-http-scanner.py /app/clamav-http-scanner.py
|
||||
|
||||
USER nobody
|
||||
EXPOSE 8090
|
||||
|
||||
CMD ["python3", "/app/clamav-http-scanner.py"]
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
# Standalone HTTP wrapper around ClamAV's clamd, reachable from browser JS (unlike clamd's raw
|
||||
# TCP protocol). Used by the ThreadNet-Web client fork to scan files client-side, both before
|
||||
# upload (plaintext, pre-encryption) and after download+decrypt of E2EE attachments - the two
|
||||
# places Synapse's own check_media_file_for_spam module (Issue #19) can never see, since
|
||||
# Synapse never holds the room's decryption key.
|
||||
#
|
||||
# Talks to clamd via its native INSTREAM protocol (docs.clamav.net/manual/Usage/ClamdProtocol.html):
|
||||
# 1. send b"zINSTREAM\0"
|
||||
# 2. send one or more chunks, each framed as a 4-byte big-endian length + that many bytes
|
||||
# 3. send a zero-length chunk to signal end of stream
|
||||
# 4. read the reply: "stream: OK" (clean) or "stream: <name> FOUND" (infected)
|
||||
#
|
||||
# Stdlib only, synchronous/threaded (ThreadingHTTPServer) - no asyncio/Twisted constraints
|
||||
# here since this runs as its own plain process, unlike the Synapse module.
|
||||
#
|
||||
# Auth: requires "Authorization: Bearer <matrix access token>", validated against Synapse's
|
||||
# own /_matrix/client/v3/account/whoami - reuses Synapse's existing auth rather than building
|
||||
# a new one, and stops this becoming an open "test your malware against our AV" oracle for
|
||||
# anyone on the internet. This is a hard failure (401) - unlike scan errors below, this is an
|
||||
# abuse-prevention concern, not a reliability one.
|
||||
#
|
||||
# Fails open on clamd connection errors (treats the file as clean, logs loudly) - matches the
|
||||
# same fail-open design as the Synapse module, so a ClamAV hiccup doesn't block all uploads/
|
||||
# downloads site-wide.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
||||
logger = logging.getLogger("clamav-http-scanner")
|
||||
|
||||
CLAMD_HOST = os.environ["CLAMD_HOST"]
|
||||
CLAMD_PORT = int(os.environ["CLAMD_PORT"])
|
||||
SYNAPSE_WHOAMI_URL = os.environ["SYNAPSE_WHOAMI_URL"]
|
||||
CLAMD_TIMEOUT_SECONDS = 30
|
||||
MAX_BODY_BYTES = 100 * 1024 * 1024 # 100MB, matches typical Synapse upload size limits
|
||||
|
||||
|
||||
def check_auth(authorization_header: "str | None") -> bool:
|
||||
if not authorization_header or not authorization_header.startswith("Bearer "):
|
||||
return False
|
||||
token = authorization_header.removeprefix("Bearer ").strip()
|
||||
request = urllib.request.Request(
|
||||
SYNAPSE_WHOAMI_URL, headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
return response.status == 200
|
||||
except urllib.error.URLError:
|
||||
return False
|
||||
|
||||
|
||||
def scan_bytes(data: bytes) -> "str | None":
|
||||
"""Returns the detected signature name, or None if clean. Raises on connection errors."""
|
||||
with socket.create_connection(
|
||||
(CLAMD_HOST, CLAMD_PORT), timeout=CLAMD_TIMEOUT_SECONDS
|
||||
) as sock:
|
||||
sock.sendall(b"zINSTREAM\0")
|
||||
chunk_size = 2**14
|
||||
for offset in range(0, len(data), chunk_size):
|
||||
chunk = data[offset : offset + chunk_size]
|
||||
sock.sendall(len(chunk).to_bytes(4, "big") + chunk)
|
||||
sock.sendall((0).to_bytes(4, "big"))
|
||||
|
||||
response = b""
|
||||
while True:
|
||||
part = sock.recv(4096)
|
||||
if not part:
|
||||
break
|
||||
response += part
|
||||
|
||||
text = response.decode("utf-8", errors="replace").strip("\x00 \n")
|
||||
if text.endswith("FOUND"):
|
||||
return text.removeprefix("stream:").removesuffix("FOUND").strip()
|
||||
return None
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, format: str, *args: object) -> None:
|
||||
logger.info("%s - %s", self.address_string(), format % args)
|
||||
|
||||
def _send_json(self, status: int, payload: dict) -> None:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_OPTIONS(self) -> None:
|
||||
self.send_response(204)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
|
||||
self.end_headers()
|
||||
|
||||
def do_POST(self) -> None:
|
||||
# matches the ingress PathPrefix(`/_scan`) route as-is (Traefik doesn't strip the
|
||||
# prefix by default) - keep client-facing and internal path identical.
|
||||
if self.path != "/_scan":
|
||||
self._send_json(404, {"error": "not found"})
|
||||
return
|
||||
|
||||
if not check_auth(self.headers.get("Authorization")):
|
||||
self._send_json(401, {"error": "invalid or missing access token"})
|
||||
return
|
||||
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
if length <= 0 or length > MAX_BODY_BYTES:
|
||||
self._send_json(400, {"error": "missing or oversized body"})
|
||||
return
|
||||
data = self.rfile.read(length)
|
||||
|
||||
try:
|
||||
signature = scan_bytes(data)
|
||||
except OSError:
|
||||
logger.exception(
|
||||
"ClamAV scan failed (clamd at %s:%s unreachable?) - "
|
||||
"treating file as clean (fail-open)",
|
||||
CLAMD_HOST,
|
||||
CLAMD_PORT,
|
||||
)
|
||||
self._send_json(200, {"clean": True, "scan_error": "scanner_unavailable"})
|
||||
return
|
||||
|
||||
if signature is None:
|
||||
self._send_json(200, {"clean": True})
|
||||
else:
|
||||
logger.warning("ClamAV flagged an upload/download: %s", signature)
|
||||
self._send_json(200, {"clean": False, "signature": signature})
|
||||
|
||||
|
||||
def main() -> None:
|
||||
server = ThreadingHTTPServer(("0.0.0.0", 8090), Handler)
|
||||
logger.info("Listening on :8090, clamd=%s:%s", CLAMD_HOST, CLAMD_PORT)
|
||||
server.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: clamav-http-scanner
|
||||
namespace: matrix
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: clamav-http-scanner
|
||||
ports:
|
||||
- name: http
|
||||
port: 8090
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: clamav-http-scanner
|
||||
namespace: matrix
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: clamav-http-scanner
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clamav-http-scanner
|
||||
spec:
|
||||
containers:
|
||||
- name: clamav-http-scanner
|
||||
image: rohana.axion1337.de/sorb/clamav-http-scanner:v1.0.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8090
|
||||
env:
|
||||
- name: CLAMD_HOST
|
||||
value: "clamav.matrix.svc.cluster.local"
|
||||
- name: CLAMD_PORT
|
||||
value: "3310"
|
||||
- name: SYNAPSE_WHOAMI_URL
|
||||
value: "http://matrix-stack-synapse.matrix.svc.cluster.local:8008/_matrix/client/v3/account/whoami"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: clamav-data
|
||||
namespace: matrix
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 3Gi
|
||||
@@ -0,0 +1,66 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: clamav
|
||||
namespace: matrix
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: clamav
|
||||
ports:
|
||||
- name: clamd
|
||||
port: 3310
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: clamav
|
||||
namespace: matrix
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: clamav
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: clamav
|
||||
spec:
|
||||
containers:
|
||||
- name: clamav
|
||||
image: clamav/clamav:1.5.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: clamd
|
||||
containerPort: 3310
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/clamav
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 1.5Gi
|
||||
limits:
|
||||
memory: 3Gi
|
||||
# clamd needs the full signature DB downloaded (freshclam, can take several
|
||||
# minutes on first start) before it accepts connections - the image's own
|
||||
# healthcheck script accounts for this via a long StartPeriod.
|
||||
livenessProbe:
|
||||
exec:
|
||||
command: ["clamdcheck.sh"]
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
failureThreshold: 10
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["clamdcheck.sh"]
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
failureThreshold: 20
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: clamav-data
|
||||
@@ -0,0 +1,123 @@
|
||||
# Synapse spam-checker module (Issue #19): scans locally-stored and remote/federated media
|
||||
# through ClamAV's clamd daemon via its native INSTREAM protocol, before Synapse serves it.
|
||||
#
|
||||
# Talks to clamd using Twisted's networking primitives - NOT asyncio's open_connection/
|
||||
# wait_for. Synapse runs on Twisted's reactor, which does not have a running asyncio event
|
||||
# loop underneath it, so raw asyncio socket calls fail immediately with
|
||||
# "RuntimeError: no running event loop" (confirmed live, 2026-07-29 - see git history for
|
||||
# the asyncio-based version that failed this way). Twisted Deferreds are natively awaitable
|
||||
# from an `async def` when Synapse wraps the callback via Deferred.fromCoroutine(), so this
|
||||
# stays plain async/await from the caller's perspective.
|
||||
#
|
||||
# clamd INSTREAM protocol (docs.clamav.net/manual/Usage/ClamdProtocol.html):
|
||||
# 1. send b"zINSTREAM\0"
|
||||
# 2. send one or more chunks, each framed as a 4-byte big-endian length + that many
|
||||
# data bytes
|
||||
# 3. send a zero-length chunk (b"\x00\x00\x00\x00") to signal end of stream
|
||||
# 4. read the reply: "stream: OK\0" (clean) or "stream: <name> FOUND\0" (infected)
|
||||
#
|
||||
# Fails open (allows the file through) on any connection/timeout error against clamd,
|
||||
# so a scanner outage can't take down media uploads for the whole homeserver - logged
|
||||
# loudly so an outage is still visible in the logs.
|
||||
|
||||
import logging
|
||||
from typing import Any, Union
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.defer import Deferred, TimeoutError as TwistedTimeoutError
|
||||
from twisted.internet.endpoints import HostnameEndpoint, connectProtocol
|
||||
from twisted.internet.protocol import Protocol
|
||||
|
||||
from synapse.module_api import ModuleApi, NOT_SPAM
|
||||
from synapse.module_api.errors import Codes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CHUNK_SIZE = 2**14 # matches ReadableFileWrapper.CHUNK_SIZE
|
||||
CLAMD_TIMEOUT_SECONDS = 30
|
||||
|
||||
|
||||
class _ClamdInstreamProtocol(Protocol):
|
||||
"""Speaks clamd's INSTREAM protocol for a single scan, then closes."""
|
||||
|
||||
def __init__(self, data: bytes, result: "Deferred[bytes]"):
|
||||
self._data = data
|
||||
self._result = result
|
||||
self._buffer = bytearray()
|
||||
|
||||
def connectionMade(self) -> None:
|
||||
self.transport.write(b"zINSTREAM\0")
|
||||
for offset in range(0, len(self._data), CHUNK_SIZE):
|
||||
chunk = self._data[offset : offset + CHUNK_SIZE]
|
||||
self.transport.write(len(chunk).to_bytes(4, "big") + chunk)
|
||||
self.transport.write((0).to_bytes(4, "big"))
|
||||
|
||||
def dataReceived(self, data: bytes) -> None:
|
||||
self._buffer.extend(data)
|
||||
if self._buffer.endswith(b"\0") or self._buffer.endswith(b"\n"):
|
||||
self.transport.loseConnection()
|
||||
|
||||
def connectionLost(self, reason: Any = None) -> None:
|
||||
if not self._result.called:
|
||||
self._result.callback(bytes(self._buffer))
|
||||
|
||||
|
||||
class ClamAVSpamChecker:
|
||||
def __init__(self, config: dict, api: ModuleApi):
|
||||
self.api = api
|
||||
self.clamd_host = config["clamd_host"]
|
||||
self.clamd_port = config["clamd_port"]
|
||||
self.api.register_spam_checker_callbacks(
|
||||
check_media_file_for_spam=self.check_media_file_for_spam,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_config(config: dict) -> dict:
|
||||
if "clamd_host" not in config or "clamd_port" not in config:
|
||||
raise ValueError(
|
||||
"clamav_spam_checker config requires 'clamd_host' and 'clamd_port'"
|
||||
)
|
||||
return config
|
||||
|
||||
async def check_media_file_for_spam(
|
||||
self, file_wrapper: Any, file_info: Any
|
||||
) -> Union[Any, Codes, bool]:
|
||||
buffer = bytearray()
|
||||
await file_wrapper.write_chunks_to(buffer.extend)
|
||||
|
||||
try:
|
||||
verdict = await self._scan(bytes(buffer))
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"ClamAV scan failed (clamd at %s:%s unreachable?) - "
|
||||
"allowing file through (fail-open)",
|
||||
self.clamd_host,
|
||||
self.clamd_port,
|
||||
)
|
||||
return NOT_SPAM
|
||||
|
||||
if verdict is None:
|
||||
return NOT_SPAM
|
||||
|
||||
logger.warning("ClamAV rejected an upload: %s", verdict)
|
||||
return Codes.FORBIDDEN
|
||||
|
||||
async def _scan(self, data: bytes) -> "str | None":
|
||||
"""Returns the detected signature name, or None if the file is clean."""
|
||||
result: "Deferred[bytes]" = Deferred()
|
||||
endpoint = HostnameEndpoint(reactor, self.clamd_host.encode(), self.clamd_port)
|
||||
await connectProtocol(endpoint, _ClamdInstreamProtocol(data, result))
|
||||
|
||||
result.addTimeout(CLAMD_TIMEOUT_SECONDS, reactor)
|
||||
try:
|
||||
response = await result
|
||||
except TwistedTimeoutError:
|
||||
raise TimeoutError(
|
||||
f"clamd at {self.clamd_host}:{self.clamd_port} did not respond in time"
|
||||
)
|
||||
|
||||
text = response.decode("utf-8", errors="replace").strip("\x00 \n")
|
||||
# "stream: OK" or "stream: <signature name> FOUND"
|
||||
if text.endswith("FOUND"):
|
||||
return text.removeprefix("stream:").removesuffix("FOUND").strip()
|
||||
return None
|
||||
@@ -4,19 +4,28 @@ metadata:
|
||||
name: coturn-secret
|
||||
namespace: matrix
|
||||
stringData:
|
||||
TURN_SECRET: ENC[AES256_GCM,data:SILIqMB+fmAMFITAL7lG1hOgICec6BJf1mOcK0gdmnCHWYqRuJv7jgjfGylG25xzQKi+zE7Qual9PnkZG2KiOA==,iv:+GZqLGusE4Q0x2jEEtFxj06rryyQmQhXdkTy4eE8ZHw=,tag:OpSZkinPTAi1ZKWyo8OX3A==,type:str]
|
||||
TURN_SECRET: ENC[AES256_GCM,data:Cbu5SoxQp0L9WFZFbEiyK8j0IJuSgoJE9OpRKZVXyF+PcTM+ewD+3TI8xq+g0C34XDWeaJdxaj3B4yyKsqQLIA==,iv:lW8lD3D75Z0b0EfBj5myUrv4GWMvRTBELjhCVm+QJjc=,tag:nC5L/PcYPoLJP1whSSfSTA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
enc: |
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyRk1mK3NWc1l4T0JCOFpF
|
||||
S0RuQ3ViZmo3QTNVL2JvZ0hzMy91R2l0TEhzCk01a1VGdk1sdVg4aWswTzRibXI4
|
||||
ZlJtNFF5MjBONEZOaWVpeU5taHl2bkEKLS0tIGxpUHY3NUFLWFBaWm1QSlZiVFkx
|
||||
MEJleHFnd3oyT3VPL2dsYkpMUlRkOWMKcKUIgsQ/ff49pGGXMnYwJmwqPVC7woAR
|
||||
IEzvhcNX97xx746SnrxZe5t2YadsYMkYIl0nvqBPJhSlvqMNafpQbQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQMncxOEhRZC9jcHpjT3dW
|
||||
Q1RNaW5pc00zVnJhWHRnZHd3TGhURWNZU2dJCk85bXJkbTEyd1ZybjhDT05pL2c3
|
||||
ZU9EUSt1eDlSQWJyVGtsak1oS3FSR2sKLS0tIEFUdXVHL0V1ZW5VMVVBZEJaYUIw
|
||||
U1BrYlJyQVZkZFhBdmdwbDMyK3lTQkkKEaSy1o+IICf2uaT6olapRJa/duXxjOBg
|
||||
OqRS9axnJ71XxEnHjLTsCbkI5b+8Fux08qKaH9sMsJrWOiSHDdTXXQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-04-29T21:06:21Z"
|
||||
mac: ENC[AES256_GCM,data:UhyR5m1HYWrZHwNLW5sg2PxbpaydWbP5cekghGlzSpQg7CYEcvZw3tJ/qB8zA19xZSM7tdSHOXdD+QytRq6qW59M1unqMaumA43B6JxQg1C1NdXAW0mkSc2WiNchvgpVii9P/TVlzSSIRwC3YGCQUsfa3SSfNzI4Z6fMuBnhYLE=,iv:4HYxbrYSRJLe1KcQ6q8bpee8/Lx1m3pPmisb/L2Mu64=,tag:l7n3u+Pg6533OzwtNUZvNw==,type:str]
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwQk82eGdWRmdRNFJQR0xF
|
||||
SWhNRUtNWThZNFM0aXU3V0N4UkhtTy9NTFdnCi9pa3dsVXRja1dTL1pZTnoxQ0JT
|
||||
UUIxekZnVGUvdFgyblFiS0JLMjU3L2cKLS0tIE1IRTJ0M3kvMFZPWVVDYjJlVkk0
|
||||
eGJQOTVUc1NsNE5GdmJtODlmdHp3c2cKHTP6YRMTdYE/iBuSZs/Tjt4TwKCxHEIu
|
||||
f3jTblKIqWwRHKCgOIkC16QDbpMBlNLH3JknJEdIjkB2HIrXrw1MNA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1x4jjwc8nuttwr8us924pvdc6dll5npkc6c8f4zf2hx5d2qu75dtqx0fm0d
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-08-01T02:00:01Z"
|
||||
mac: ENC[AES256_GCM,data:1Tx2/4O/fcv9BPYLXainmsILC2HbBJtJkgVnbkCe9oVSKca/hVf6zAa8Pd6n/tvHHAuU1Ghm2mglk/IxbNKgCDHH+xaEeK5sfhAK0Ot2ffIMLK5chBNMH0DpvTjqP8ttMidgPJ4XYzizB850gq7pSRrZBJCC2oG66F+WPnfKkxQ=,iv:NIDfNbIyWSgvjpXtQFVjnZFjtKx8sV3a45ssiebkHf0=,tag:hNDmHPTu4RyNMvjyfJRCmA==,type:str]
|
||||
version: 3.12.2
|
||||
|
||||
@@ -65,6 +65,17 @@ metadata:
|
||||
namespace: matrix
|
||||
spec:
|
||||
replicas: 1
|
||||
# hostNetwork pods bind directly to the node's ports (3478/5349) - on this single-node
|
||||
# cluster, RollingUpdate's default "bring up the new pod before removing the old one"
|
||||
# can never schedule (port conflict). Recreate kills the old pod first.
|
||||
# Note: switching to Recreate on an existing Deployment that already had the
|
||||
# RollingUpdate defaults recorded required a one-time manual
|
||||
# `kubectl patch --type=merge -p '{"spec":{"strategy":{"rollingUpdate":null,"type":"Recreate"}}}'`
|
||||
# (2026-07-28) - a YAML `rollingUpdate: null` in this file is dropped before reaching the
|
||||
# API server (client-side omits null keys) rather than sent as an explicit field deletion,
|
||||
# so it can't clear an already-set field on its own.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: coturn
|
||||
@@ -74,6 +85,10 @@ spec:
|
||||
app: coturn
|
||||
annotations:
|
||||
prometheus.io/scrape: "false"
|
||||
# Bumped on every TURN_SECRET rotation (Issue #38) to force a new pod, since
|
||||
# Kubernetes doesn't restart running pods when a referenced Secret's content
|
||||
# changes and the initContainer that reads it only runs once at pod start.
|
||||
rotated-at: "2026-08-01T02:00:01Z"
|
||||
spec:
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
@@ -130,11 +145,8 @@ spec:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- "netstat -uln | grep 3478 || exit 1"
|
||||
tcpSocket:
|
||||
port: 3478
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
|
||||
@@ -10,6 +10,22 @@ data:
|
||||
rootLevel: INFO
|
||||
levelOverrides:
|
||||
synapse.media.url_previewer: DEBUG
|
||||
# ClamAV media scanning module (Issue #19) - mounted read-only from a ConfigMap
|
||||
# (synapse-clamav-module) since the container runs with a read-only root filesystem
|
||||
# and we avoid a custom Synapse image; PYTHONPATH picks it up for the `modules:`
|
||||
# block below.
|
||||
extraVolumes:
|
||||
- name: clamav-spam-checker
|
||||
configMap:
|
||||
name: synapse-clamav-module
|
||||
extraVolumeMounts:
|
||||
- name: clamav-spam-checker
|
||||
mountPath: /extra-modules/clamav_spam_checker.py
|
||||
subPath: clamav_spam_checker.py
|
||||
readOnly: true
|
||||
extraEnv:
|
||||
- name: PYTHONPATH
|
||||
value: /extra-modules
|
||||
additional:
|
||||
url-previews:
|
||||
config: |
|
||||
@@ -58,15 +74,13 @@ data:
|
||||
room_list_publication_rules:
|
||||
- user_id: "*"
|
||||
action: allow
|
||||
turn:
|
||||
config: |
|
||||
turn_uris:
|
||||
- "turn:turn.axion1337.chat?transport=udp"
|
||||
- "turn:turn.axion1337.chat?transport=tcp"
|
||||
- "turns:turn.axion1337.chat?transport=tcp"
|
||||
turn_shared_secret: "cab3c8408363515d9b4cdc3384a1f76ca17a973242fdfdc72b67ac4d86158527"
|
||||
turn_user_lifetime: 86400000
|
||||
turn_allow_guests: false
|
||||
oembed:
|
||||
config: |
|
||||
oembed_enabled: true
|
||||
oembed_enabled: true
|
||||
clamav-module:
|
||||
config: |
|
||||
modules:
|
||||
- module: clamav_spam_checker.ClamAVSpamChecker
|
||||
config:
|
||||
clamd_host: "clamav.matrix.svc.cluster.local"
|
||||
clamd_port: 3310
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: draupnir-data
|
||||
namespace: matrix
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: draupnir-config
|
||||
namespace: matrix
|
||||
stringData:
|
||||
default.yaml: ENC[AES256_GCM,data:q8jaWkZtjMQFCokSMVLvjRC53qou0AFtMei0LmXR7X7ync2NYDv9HJcnkxRVH9dfbeYGtUub4H5QYJatbX0nJqS0dii3PxHF+rqxlbnBsrfL6lDU6RBvIZnninPHHx0q6YacIPE1u4bX+9VkHZ4CQCsJfYVCX3zcGk3JjYWwF90FFANLH/DJu6trALml7CH7yZiSrgETFtffSn+ghI/CJqAMB6LRl7xG8xD2duE6HdNTztbJn7s0cntdNtU3MmJik8q45pNnnEZSAAXWqQUJ15IWVAWKpADA1IyUemYT0DcdEW1ZZywUhRaDhnF111RjjixVj+wPPYnkCpEoTAJ0cSBna2BAzBVda9ztvjvYXnVY0tL1eqi+VlqH09JPaozLlcLeje62YH8i05wW8SXlU7ojiXZdHx1JiOxQZu3ec/xuBOq0CWKWBQH/nOOL87NwhUoNMqQLgpf4,iv:SfBDZH67aptbRas5mPlBVsA00EfYJ+evzYtoyzhOH4o=,tag:DOfNLaZ2QJncEKt53ELokA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBORHJrRGUraSs0dXc3Y2Zu
|
||||
emZtZExCbGQ0dkh2d3NTZWdjZlVPSXp3NEdNCmRsL3VpdjJ2Q2NWN2Z4TnBwTm9u
|
||||
T2YvL2ZTejhYVkprdXJQM3F6dlE1NlEKLS0tIFZwaFgwRTA1b2JiU0RMVmQ3clhx
|
||||
ZkdURVljTHJjOE1xRmczV3hic2x3U3cKMVcGRX9NQlLefQrjqhYWPH+DyF9N4nw8
|
||||
RxRVkw7DPbrfP8Bm57oasBeUya73OxVDZAj7UM/B1MXS/vtU3mXFmw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-07-29T11:16:15Z"
|
||||
mac: ENC[AES256_GCM,data:4EvnrSUhi0vQpjaJGHF258qKOc4lO8HjmVP0JaSSpNvD8lVTwdM1E5hFlqOKmgyT6naHL7HMzQ63AGCRGG/YrEvWG/tKgMtSvuyfDOMu8nCy6ksW2qOB6YRDQtTGLHIth9p2lVlSYQLmsSONj8Ve9Ftp4/uiJ4fiR9HpYnvk/II=,iv:hYD3oDv23jcyh/HWV7tsOAin9otzjRtOfLpw1zWrOpA=,tag:8zOO16eubuHoMpUelV20yw==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,67 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: draupnir
|
||||
namespace: matrix
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: draupnir
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: draupnir
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 1000
|
||||
containers:
|
||||
- name: draupnir
|
||||
image: gnuxie/draupnir:v3.1.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
# v3.x dropped NODE_CONFIG_DIR auto-discovery in favour of an explicit CLI flag
|
||||
# (confirmed by extracting dist/config.js from the image - getConfigPath() only
|
||||
# checks --draupnir-config/--mjolnir-config, throws otherwise).
|
||||
args: ["bot", "--draupnir-config", "/data/config/default.yaml"]
|
||||
ports:
|
||||
- name: healthz
|
||||
containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /data/config/default.yaml
|
||||
subPath: default.yaml
|
||||
readOnly: true
|
||||
- name: storage
|
||||
mountPath: /data/storage
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 384Mi
|
||||
# healthz reports 418 (not just a plain failure) until Draupnir finishes its
|
||||
# initial room-state sync with the homeserver - generous initialDelay/failureThreshold
|
||||
# avoids a restart loop while that's still in progress on first boot.
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: healthz
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
failureThreshold: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: healthz
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
failureThreshold: 10
|
||||
volumes:
|
||||
- name: config
|
||||
secret:
|
||||
secretName: draupnir-config
|
||||
- name: storage
|
||||
persistentVolumeClaim:
|
||||
claimName: draupnir-data
|
||||
@@ -4,7 +4,11 @@ metadata:
|
||||
name: matrix-stack
|
||||
namespace: matrix
|
||||
spec:
|
||||
interval: 5m
|
||||
# Shortened from 5m to match production-apps Kustomization's 1m interval - narrows the
|
||||
# window between coturn (Kustomization-only, no Helm indirection) and synapse-main
|
||||
# (behind this HelmRelease) picking up a rotated TURN secret after Issue #38's
|
||||
# automated-rotation PR gets merged. Self-heals either way, just faster now.
|
||||
interval: 1m
|
||||
chart:
|
||||
spec:
|
||||
chart: matrix-stack
|
||||
@@ -25,7 +29,10 @@ spec:
|
||||
- kind: Secret
|
||||
name: ess-mas-values-secret
|
||||
valuesKey: values.yaml
|
||||
|
||||
- kind: Secret
|
||||
name: synapse-turn-secret
|
||||
valuesKey: values.yaml
|
||||
|
||||
values:
|
||||
# Top-Level: serverName – das ist dein Matrix-Homeserver-Name
|
||||
serverName: axion1337.chat
|
||||
@@ -59,6 +66,14 @@ spec:
|
||||
enabled: true
|
||||
ingress:
|
||||
host: mrtc.axion1337.chat
|
||||
# Chart default (20Mi request+limit) OOM-killed the authorisation service after
|
||||
# ~74 days of uptime (2026-07-28) - too tight for a long-running Go service.
|
||||
resources:
|
||||
requests:
|
||||
memory: 64Mi
|
||||
cpu: 50m
|
||||
limits:
|
||||
memory: 128Mi
|
||||
|
||||
# Element Web
|
||||
elementWeb:
|
||||
@@ -66,7 +81,7 @@ spec:
|
||||
image:
|
||||
registry: rohana.axion1337.de
|
||||
repository: sorb/threadnet-web
|
||||
tag: v0.1.0
|
||||
tag: v0.3.0-clientscan
|
||||
ingress:
|
||||
host: axion1337.chat
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ data:
|
||||
<div class="section">
|
||||
<h2>❓ Support</h2>
|
||||
<p>Für weitere Hilfe besuche: <a href="https://element.io/help" target="_blank">element.io/help</a></p>
|
||||
<p>🔐 <a href="security.html">Konto-Sicherheit (Passkey/2FA einrichten)</a></p>
|
||||
</div>
|
||||
|
||||
<div class="support">
|
||||
@@ -203,6 +204,117 @@ data:
|
||||
</body>
|
||||
</html>
|
||||
|
||||
# Security / 2FA setup page
|
||||
"security.html": |
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Konto-Sicherheit - aXion1337.Chat</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
padding: 40px;
|
||||
}
|
||||
h1 { color: #333; margin-bottom: 10px; font-size: 2.5em; }
|
||||
.subtitle { color: #666; margin-bottom: 40px; font-size: 1.1em; }
|
||||
.section { margin-bottom: 40px; }
|
||||
.section h2 {
|
||||
color: #667eea;
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 3px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.download-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.download-card {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
}
|
||||
.download-card:hover {
|
||||
border-color: #667eea;
|
||||
background: #f0f3ff;
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
.download-card .icon { font-size: 2.5em; margin-bottom: 10px; }
|
||||
.download-card .name { font-weight: 600; font-size: 1.1em; margin-bottom: 5px; }
|
||||
.download-card .desc { font-size: 0.9em; color: #666; }
|
||||
.instructions {
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #0066cc;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.support {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
.support a { color: #667eea; text-decoration: none; font-weight: 500; }
|
||||
.support a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 Konto-Sicherheit</h1>
|
||||
<p class="subtitle">Zwei-Faktor-Authentifizierung ist optional - richte sie nur ein, wenn du sie nutzen möchtest.</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>🔑 Einrichten</h2>
|
||||
<div class="download-grid">
|
||||
<a href="https://auth.axion1337.chat/if/flow/default-authenticator-webauthn-setup/" class="download-card" target="_blank">
|
||||
<div class="icon">🔑</div>
|
||||
<div class="name">Passkey</div>
|
||||
<div class="desc">WebAuthn / Sicherheitsschlüssel</div>
|
||||
</a>
|
||||
<a href="https://auth.axion1337.chat/if/flow/default-authenticator-totp-setup/" class="download-card" target="_blank">
|
||||
<div class="icon">📱</div>
|
||||
<div class="name">TOTP</div>
|
||||
<div class="desc">Authenticator-App</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="instructions">
|
||||
<strong>Hinweis:</strong> Du musst bei <code>auth.axion1337.chat</code> eingeloggt sein, damit die
|
||||
Einrichtung funktioniert. Ohne konfiguriertes Gerät wird beim Login einfach kein zweiter Faktor abgefragt -
|
||||
2FA ist nie Voraussetzung zum Anmelden.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="support">
|
||||
<p><a href="index.html">← Zurück zum Setup</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
# README
|
||||
"README-Element-Setup.md": |
|
||||
# Element Desktop Setup Scripts
|
||||
|
||||
@@ -21,6 +21,7 @@ spec:
|
||||
- |
|
||||
mkdir -p /html/docs/setup
|
||||
cp /config/index.html /html/docs/setup/
|
||||
cp /config/security.html /html/docs/setup/
|
||||
cp /config/README-Element-Setup.md /html/docs/setup/
|
||||
cp /config/element-setup-windows.cmd /html/docs/setup/
|
||||
cp /config/element-setup-macos.command /html/docs/setup/
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# Patch: Fügt einen Checksum der element-values.yaml zur HelmRelease hinzu
|
||||
# Damit wird Flux die HelmRelease neu-synced wenn sich die ConfigMap ändert
|
||||
# Patch: Fügt Checksums der element-values.yaml und des turn_shared_secret zur
|
||||
# HelmRelease hinzu. Damit wird Flux die HelmRelease neu-synced (und synapse-main neu
|
||||
# gestartet), wenn sich die jeweilige ConfigMap/Secret ändert - siehe Issue #38's
|
||||
# Rotations-Mechanismus, der turn-secret-checksum bei jeder Rotation bumpt.
|
||||
patches:
|
||||
- target:
|
||||
kind: HelmRelease
|
||||
@@ -12,6 +14,9 @@ patches:
|
||||
- op: add
|
||||
path: /metadata/annotations/element-config-checksum
|
||||
value: "401f8a87d0ef5d91d2e5032d4aede42c"
|
||||
- op: add
|
||||
path: /metadata/annotations/turn-secret-checksum
|
||||
value: "05aad8b742fb02c42f4c1a5629ae31e1"
|
||||
|
||||
resources:
|
||||
- matrix-postgres-auth.yaml
|
||||
@@ -26,7 +31,35 @@ resources:
|
||||
# TURN Server für WebRTC
|
||||
- coturn-secret.yaml
|
||||
- coturn.yaml
|
||||
- synapse-turn-secret.yaml
|
||||
# HelmRelease (muss ganz unten stehen, damit die ConfigMaps vorher da sind!)
|
||||
- element-server-suite.yaml
|
||||
# Custom Apex Ingress für Element Web + Well-Known auf axion1337.chat
|
||||
- apex-ingress.yaml # Custom Apex Ingress für Element Web + Well-Known auf axion1337.chat
|
||||
- apex-ingress.yaml # Custom Apex Ingress für Element Web + Well-Known auf axion1337.chat
|
||||
- networkpolicy.yaml
|
||||
# Backup zur Hetzner Storage Box (Issues #6 + #15)
|
||||
- synapse-backup-secret.yaml
|
||||
- synapse-backup.yaml
|
||||
# Automatisierte TURN-Secret-Rotation (Issue #38)
|
||||
- turn-secret-rotation-secret.yaml
|
||||
- turn-secret-rotation.yaml
|
||||
# Draupnir Moderationsbot (Issue #18)
|
||||
- draupnir-secret.yaml
|
||||
- draupnir-pvc.yaml
|
||||
- draupnir.yaml
|
||||
# ClamAV für Media-Scanning via Synapse-Modul (Issue #19)
|
||||
- clamav-pvc.yaml
|
||||
- clamav.yaml
|
||||
# Client-seitiger Scan-Dienst für verschlüsselte Räume (Issue #19-Erweiterung)
|
||||
- clamav-http-scanner.yaml
|
||||
|
||||
# Synapse-Modul als eigene Datei gepflegt (lintbar/testbar), aber als ConfigMap gemounted -
|
||||
# disableNameSuffixHash, da der Name in synapse-values.yaml's eingebettetem values.yaml
|
||||
# referenziert wird (kustomize kann Referenzen nicht in opaken YAML-Strings umschreiben).
|
||||
configMapGenerator:
|
||||
- name: synapse-clamav-module
|
||||
namespace: matrix
|
||||
files:
|
||||
- clamav_spam_checker.py
|
||||
options:
|
||||
disableNameSuffixHash: true
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
# Default-deny ingress for the matrix namespace, with explicit allow rules per component.
|
||||
# Egress is intentionally untouched (federation to arbitrary Matrix servers, ACME, SMTP,
|
||||
# DNS all stay unrestricted).
|
||||
#
|
||||
# Lesson learned deploying the authentik namespace's equivalent policy: NetworkPolicy
|
||||
# filters on the pod's actual container port, not the Service's external port (e.g.
|
||||
# authentik-server's Service maps 80->9000). Wherever a Service here uses a *named*
|
||||
# targetPort, this file references that name directly instead of guessing a number -
|
||||
# Kubernetes resolves it from the pod spec, which is safer than a hardcoded port.
|
||||
#
|
||||
# matrix-stack-postgres already effectively has no dedicated chart NetworkPolicy of its
|
||||
# own (unlike authentik-postgresql's Bitnami one) - the rules below are the only gate.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: default-deny-ingress
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Ingress
|
||||
---
|
||||
# axion1337.chat (root) -> Element Web
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-element-web
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: element-web
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: element
|
||||
---
|
||||
# admin.axion1337.chat -> Element Admin
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-element-admin
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: element-admin
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: http
|
||||
---
|
||||
# axion1337.chat/docs/setup -> Element desktop setup docs (our own nginx)
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-element-web-docs
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: element-web-docs
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
---
|
||||
# matrix.axion1337.chat AND the well-known delegation both front through haproxy
|
||||
# (matrix-stack-synapse and matrix-stack-well-known Services both target haproxy's
|
||||
# named ports, not synapse-main directly).
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-haproxy
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: haproxy
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: haproxy-synapse
|
||||
- protocol: TCP
|
||||
port: haproxy-403
|
||||
- protocol: TCP
|
||||
port: haproxy-wkd
|
||||
# Draupnir (Issue #18) calls Synapse's client-server API directly, in-namespace -
|
||||
# without this it would be silently blocked by the default-deny policy.
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: draupnir
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: haproxy-synapse
|
||||
# Client-Scan-Dienst (Issue #19-Erweiterung) validiert Access-Tokens gegen Synapses
|
||||
# eigenen /whoami-Endpoint statt eine eigene Auth zu bauen.
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: clamav-http-scanner
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: haproxy-synapse
|
||||
---
|
||||
# account.axion1337.chat (Traefik) + matrix.axion1337.chat (also routes to MAS for some
|
||||
# paths) + synapse-main calling MAS's internal port for session/token introspection.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-mas
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: matrix-authentication-service
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: synapse-main
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: synapse-main
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8081
|
||||
---
|
||||
# Synapse itself: reached via haproxy (same namespace), calls from MAS (provisioning),
|
||||
# metrics scraped by Alloy (monitoring namespace).
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-synapse
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: synapse-main
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: haproxy
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: matrix-authentication-service
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: synapse-http
|
||||
- protocol: TCP
|
||||
port: synapse-health
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: monitoring
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: synapse-metrics
|
||||
---
|
||||
# mrtc.axion1337.chat (Traefik) for the auth handshake, plus Alloy scraping metrics.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-rtc-authorisation-service
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: matrix-rtc-authorisation-service
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: matrix-rtc-sfu
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: http
|
||||
---
|
||||
# The SFU: mrtc.axion1337.chat (Traefik) for signalling, Alloy for metrics, and the
|
||||
# NodePort-exposed WebRTC media ports need to stay open to the internet by design -
|
||||
# that's the actual point of a TURN/SFU media relay, not a mistake.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-rtc-sfu
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: matrix-rtc-sfu
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: http
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: monitoring
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: metrics
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 30001
|
||||
- protocol: UDP
|
||||
port: 30002
|
||||
---
|
||||
# Postgres: only Synapse and MAS need data access; Alloy scrapes the exporter.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-postgres
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: postgres
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: synapse-main
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: matrix-authentication-service
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: synapse-backup
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: monitoring
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9187
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-acme-solver
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
acme.cert-manager.io/http01-solver: "true"
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8089
|
||||
|
||||
# Note: coturn runs with hostNetwork: true, so NetworkPolicy does not apply to it at all -
|
||||
# it's already gated by the Hetzner Cloud Firewall instead. Nothing to write here.
|
||||
---
|
||||
# ClamAV (Issue #19): only Synapse's check_media_file_for_spam module calls this, over
|
||||
# clamd's plain TCP protocol on port 3310. Nothing else needs to reach it.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-clamav
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: clamav
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: synapse-main
|
||||
# Client-seitiger Scan-Dienst (Issue #19-Erweiterung) braucht denselben ClamAV.
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: clamav-http-scanner
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: clamd
|
||||
---
|
||||
# axion1337.chat/_scan (Traefik) - client-seitiger Scan-Dienst, direkt vom Browser aufgerufen.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-clamav-http-scanner
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: clamav-http-scanner
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: http
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: synapse-backup-credentials
|
||||
namespace: matrix
|
||||
stringData:
|
||||
borg-passphrase: ENC[AES256_GCM,data:RRXPwr4UGX30IdozM53abN7ZYztTO3Y1/63dtTh0JSZmU8i8l/ATb4gc3lc=,iv:rIyUr+lOUjo9J53OKZ5ZDmp3d8Nrb9PP2JDK2oCutYU=,tag:MqUmoYgCA03WJQy+RQi04w==,type:str]
|
||||
ssh-private-key: ENC[AES256_GCM,data:320B/lSq7DljCrXZ0BluGv8gLIzYF3KL6VQnR6CM/Vzuf/6qbbZ7lMzrS96XfoubU95OxDeJuOsuNZBQUZNyUGtt4QjQmnl8XHUJzl48xLqh83HLFPtQm2uqU072lscf5OTT7I+JzD2BGks3OIowhrg1q0MdVQdfd4Rhdz8Jphjb+WxvstzNmEX5gxQ9mnBVPmj4DS6ikdXnpe+VDvaCJaVkzD5KgwRmqPy0qbFs9WXziQSo3am5fPeHbwhV3UlRhlok9WrDI40a1T1S5DBhgNhwShq1jAxjr9onuq92REymxAV50oLzsw9ivnH0uimw+3PcplRG1v2xxJ/pimWTCjE17bO7OZ8TyzzwyZ7QA02vSpOMNUdUwVU3N6pSdYQpdTETuVBTVqVc+GKC18Z9fWh2rdwX5eUDTwp0bbDDvPEHdANNxMg3VYD3gwpCgy6/wVnjrO+pVMs8K1CqFn4/H7azhzzeEPkrz27ZBxjlnqvDODsm9tlklr6X4jZDZmTaEmiH+WXuc+1qbNxvsrqtSFfa3CfFGxM1nBOh,iv:sKGsTLsxdQYVUvw7CEARL3YNInSd9LPbFp5Ci5CTgIw=,tag:O1mjNZUsGEqV4uu+LlgD/g==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3ZUdtN3hyNHlyMkIrbTVS
|
||||
aHQ4OGNUWnA4eTFUVkx4UVp5VWlnMDgxTEU0CkU3M2dMYWgyKytlRVFOVWptZEd2
|
||||
NXlIY0JCd24xcGFzaGpIeks0R2U0U3MKLS0tIHlxYVZ1ZTJsRXNaZ25sVzZtSnp0
|
||||
SEhzQ0tUYzZTRXcwMVNwbG85SHpyb0UKOn3nxy6Y7yQkGargXQ9z6O36vUWW4qJZ
|
||||
D/GbFGmoRi94EtVFdmTGALhjy2D4J9QXy6gHsTapvKyMxF8NEtk+FQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-07-28T18:20:58Z"
|
||||
mac: ENC[AES256_GCM,data:Rur32fQdCyM3nr/X+KeSgmPYEi4nKyh8lqTuSW3TPBVDjwTWMDp1I1ZPPyy5syeW6RHbKputFUzBWVnuQmVrfbZaQ6DBBI5kP9InspUAVUjXDRk9XqiWtdg/wYaTMMJ4Nxv/zdwkh6uJQSG2JHQBWce4NZc2hoPokLR0CjcWiZk=,iv:eWTUj48EFjjtuIIuErMltEdDfabLZeolkpInMYtVP5Q=,tag:6DPxkPqLt0ihJ80WnTzHeA==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,96 @@
|
||||
# Nightly Borg backup of the shared Postgres instance (synapse + MAS databases) and the
|
||||
# Synapse media_store PVC to a Hetzner Storage Box (issues #6 + #15). See
|
||||
# apps/production/synapse-backup-secret.yaml for the SSH key + Borg repo passphrase, and
|
||||
# apps/authentik/authentik-backup.yaml for the equivalent authentik-side job.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: synapse-backup-known-hosts
|
||||
namespace: matrix
|
||||
data:
|
||||
# Pinned via `ssh-keyscan -p 23 u641795.your-storagebox.de` (2026-07-28) rather than
|
||||
# trusting the host key on first connect in an unattended job.
|
||||
known_hosts: |
|
||||
[u641795.your-storagebox.de]:23 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIICf9svRenC/PLKIL9nk6K/pxQgoiFC41wTNvoIncOxs
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: synapse-backup
|
||||
namespace: matrix
|
||||
spec:
|
||||
schedule: "0 3 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: synapse-backup
|
||||
app.kubernetes.io/component: backup
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: backup
|
||||
image: rohana.axion1337.de/sorb/axion-backup:v2
|
||||
env:
|
||||
- name: BORG_REPO
|
||||
value: "ssh://u641795@u641795.your-storagebox.de:23/./synapse-backup"
|
||||
- name: BORG_PASSPHRASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: synapse-backup-credentials
|
||||
key: borg-passphrase
|
||||
- name: SSH_PRIVATE_KEY_FILE
|
||||
value: /secrets/ssh/ssh-private-key
|
||||
- name: SSH_KNOWN_HOSTS_FILE
|
||||
value: /secrets/known-hosts/known_hosts
|
||||
- name: DB_HOSTS
|
||||
value: "synapse:matrix-stack-postgres matrixauthenticationservice:matrix-stack-postgres"
|
||||
- name: PGUSER
|
||||
value: postgres
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: matrix-stack-generated
|
||||
key: POSTGRES_ADMIN_PASSWORD
|
||||
- name: MEDIA_PATH
|
||||
value: /media/media_store
|
||||
volumeMounts:
|
||||
- name: ssh-key
|
||||
mountPath: /secrets/ssh
|
||||
readOnly: true
|
||||
- name: known-hosts
|
||||
mountPath: /secrets/known-hosts
|
||||
readOnly: true
|
||||
- name: media
|
||||
mountPath: /media
|
||||
readOnly: true
|
||||
- name: scratch
|
||||
mountPath: /scratch
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
memory: 256Mi
|
||||
volumes:
|
||||
- name: ssh-key
|
||||
secret:
|
||||
secretName: synapse-backup-credentials
|
||||
items:
|
||||
- key: ssh-private-key
|
||||
path: ssh-private-key
|
||||
mode: 0400
|
||||
- name: known-hosts
|
||||
configMap:
|
||||
name: synapse-backup-known-hosts
|
||||
- name: media
|
||||
persistentVolumeClaim:
|
||||
claimName: matrix-stack-synapse-media
|
||||
readOnly: true
|
||||
- name: scratch
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,31 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: synapse-turn-secret
|
||||
namespace: matrix
|
||||
stringData:
|
||||
values.yaml: ENC[AES256_GCM,data:jGoBDvjGymp4qEVEB6hzgRJk1ZUVB+jNxfhqpl7tG7UQmZGskix43rMJIOdDZihBTOUHP4emhlOzTxqx3SCAFCb8Ie78xA+DfjuLnQ6PQ2jqN48+yc7QE2xbNr5eWlYYKzl1u/z0kCQT723ptA+tQzYCzUq5U624HHxki0firgoSJmaF9S/vbwXHLPqtnviYX48Eim0suwMnavWdWGd1X3yvqUoZnWKz3eDQrH3cp4fQAdnRosveIcWwz+1na67Zshczq7hhlNp/WZ95guAOiqlLxGMPwJQJou24UYynwQTtDle5IzCxOww+sCNuQtPp29NE1gZ5pxIM7Ys6Ul7UxtPAww6OCVbkYYGmQxIq0cY4CrFrgLDJYQ2JJPIexc1ooeIDcYiNYts/2TCAj9ck+Vd+6xN1NEYwm0ZTLL9naLJJ1PEzRkfy3bCOTfKWCRNwLzn/3gsRgr+hrf5dJ6Av6VbM0Ae9xhTEQ4d64C8YRjb4G9BnDRGuaHu+5DYAqiFUEMm6cM4IV3E=,iv:ZRGQREujdrDCNj1OcgV7HAjZREArdnodnD2J5BxkGQo=,tag:2FOO9/rv4IteyulLnsR/YA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBuK2xIT3M0cGtVOHdKYklF
|
||||
OHl1VHFRTkVSaHVQNUUyRlhZUjRWM3JUN0hBCnpUS2YzdG1mSjlRaEVvTHdKVkR4
|
||||
L1hrR05IRUdqdGp2aU95aWxRRXdsQWcKLS0tICtLbnFsK3Zza053VXFWdVErRy92
|
||||
WUUzZTBIRzUyWnp5a1ZScUVqb0NyencKvnFyJCR6j1/aH4gJvFmLPNlk5XpC08wF
|
||||
mTmL981uGfz4NULc+O3sDkonJ827glpefgWPgPW2HmKT88d4A9vyJw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVY0NXa1B5c2RzbzRReU1h
|
||||
RHBSbk9aK3d6VjRWZ1hzL3FDZFZJZGFqTVQ0Ck9ib0p3bkR6cU8yc0VFNjEzSFFi
|
||||
T3dWY1luQW1VZ1NjZFNoZFFLSCsvelkKLS0tIHIxV0d6TWhpSEc3d3c0L1VvOWxk
|
||||
eDhTM2pDbTNXZXlWRVYxR2tPQU5iLzgKrLLLSBU/g5ebeRNi7hWYbcuJ/2JOfiUn
|
||||
0DBnzMVJPBfqq/u8THiRYaMajx3k4D9+FN7qc5nBgTd85iGBo+OowA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1x4jjwc8nuttwr8us924pvdc6dll5npkc6c8f4zf2hx5d2qu75dtqx0fm0d
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-08-01T02:00:01Z"
|
||||
mac: ENC[AES256_GCM,data:zk0ivb9asZLHUg819tR8GV5R5ViSiJId0T1o2GqYWjc/AcNK1pBKwIJ+S3TuB0fLz3qaFXUBfzcc+CfztuTLcRzAfu3mF8Hv0boZTe0lGl5XugmozQWjoLjSw+roWZHlXd5CfYbDZsHSO70231NyPlpuUkVr2fqAFZgTEev2A9Y=,iv:IkRYK6PLE74i+Kq5fop4ddS+Q9KW8uNVfABK81uFyIw=,tag:kanZfzBXEYodnQtwWdZ85g==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: turn-secret-rotation-credentials
|
||||
namespace: matrix
|
||||
stringData:
|
||||
age-key.txt: ENC[AES256_GCM,data:4LAs9LLFo38UMHXCo4lun9RHxGnDyp7GWlaNdIqqkSL9lNv7+ILdlc03CxFVobCYxK65xMOn1xdEty+887JBMlawST04am/5MkAnUivKwXCw8OHmbZhCwKHFqSYH/NsgVNf+btZKIIny8XPVQAPj/vQIi+Ity+BQyPkEZ1WUcsqjDoaK9IFhQTePJtHgWivhOY2WpUt/TP7vTfub4TOrgVpzNd9LIpBkwq+zhpVXKwnYUWOuLMXlPe08kazy,iv:9A24HbTl24slj+qTCfyI01+dGqRFVPDUA0wp2kSUHpc=,tag:iQxU0dWD4Noo1m0HXhV6vQ==,type:str]
|
||||
gitea-token: ENC[AES256_GCM,data:cmZ1GCrqRYLtLn+cRVZCrO7UcCIavlQLJPt2PRMtBbgLdhDVTElUKA==,iv:9v29GXHRtDlrL3PoRCdOqYpBepZrX04+6UjoywRZX0E=,tag:ZvYXBOm9qgB1XA5lkV4LsQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5S3BtTHNLVGVVM2ZpTlZS
|
||||
Y3pGMS9CSkNsdUpPbWtkSTRHK0p6U2lwdEJnClVWdXp4SllyM1hvbTZyTU40SDc2
|
||||
QlVtMDduZWpaVENiYnhMNlFXd01QblEKLS0tIGNzTGRZcmoyaFltUHRDSHBPZE1N
|
||||
OCszUkl1VjQ5V3F2cVI4dXJFcER5YXcK+2Eh1JNLuMiCnpQ3cL/I7XTykkIZ3tqp
|
||||
O3c9UwYs1FAZWlMgElTBTqsmut1ShduIYfDFRKGeS0UxPEM4U+tIGw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-07-28T19:21:28Z"
|
||||
mac: ENC[AES256_GCM,data:vx6Bs/L0NXKUvvQdu6aYtuur/CYPkIBZzvFLqTyd08Errw0dGMrg73oHQ/imxpe42HgnO2mGwxNdEx2jYYbtc3RBWHE/yPH5m8y/XLoSL3fauzbkGsDwMSWzKiZXyIuGh7SxuB+CFY9qqFMK+dap1Ofno7a1/Gr1qibVDqscwxw=,iv:9AUQQuTCja09OohzVw73URMHE8xCW7iLLtBg7GSDcPA=,tag:g1ibnBIGqokvs2IEVOYq6Q==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,70 @@
|
||||
# Automated TURN shared-secret rotation (Issue #38). Generates a new secret, re-encrypts
|
||||
# apps/production/coturn-secret.yaml and synapse-turn-secret.yaml using a dedicated,
|
||||
# narrowly-scoped age key (see turn-secret-rotation-secret.yaml - it can only decrypt these
|
||||
# two files, not the repo's master sops-age key), bumps the checksum/rotated-at annotations
|
||||
# so a merge restarts both consumers automatically, and opens a Pull Request rather than
|
||||
# pushing straight to main - a human reviews and merges it.
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: turn-secret-rotation
|
||||
namespace: matrix
|
||||
spec:
|
||||
schedule: "0 4 1 * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: turn-secret-rotation
|
||||
app.kubernetes.io/component: rotation
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
# Public-internet reachability to the Gitea host has been flaky (see Issue #41);
|
||||
# both servers share a private Hetzner network. hostAliases (unlike the node-level
|
||||
# /etc/hosts fix used for image pulls) is actually honored by in-pod processes.
|
||||
hostAliases:
|
||||
- ip: "10.0.0.3"
|
||||
hostnames:
|
||||
- "rohana.axion1337.de"
|
||||
containers:
|
||||
- name: rotate
|
||||
image: rohana.axion1337.de/sorb/axion-secret-rotation:v1
|
||||
env:
|
||||
- name: GITEA_HOST
|
||||
value: "rohana.axion1337.de"
|
||||
- name: GITEA_REPO
|
||||
value: "sorb/axion1337.chat-gitops"
|
||||
- name: GITEA_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: turn-secret-rotation-credentials
|
||||
key: gitea-token
|
||||
- name: SOPS_AGE_KEY_FILE
|
||||
value: /secrets/age/age-key.txt
|
||||
- name: GIT_AUTHOR_NAME
|
||||
value: "turn-secret-rotation"
|
||||
- name: GIT_AUTHOR_EMAIL
|
||||
value: "turn-secret-rotation@axion1337.chat"
|
||||
volumeMounts:
|
||||
- name: age-key
|
||||
mountPath: /secrets/age
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
volumes:
|
||||
- name: age-key
|
||||
secret:
|
||||
secretName: turn-secret-rotation-credentials
|
||||
items:
|
||||
- key: age-key.txt
|
||||
path: age-key.txt
|
||||
mode: 0400
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
+225
-255
@@ -1,7 +1,7 @@
|
||||
# aXion1337.Chat – Task List & Meilensteine
|
||||
|
||||
**Last Updated**: 2026-05-15
|
||||
**Statusübersicht**: [✅ 9 Abgeschlossen] [🔄 0 In Progress] [📋 11+ Pending] [🔒 10 Security]
|
||||
**Last Updated**: 2026-07-28
|
||||
**Statusübersicht**: [✅ 13 Abgeschlossen] [🔄 0 In Progress] [📋 8+ Pending] [🔒 10 Security]
|
||||
|
||||
---
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
|
||||
| Kategorie | Count | Status | Details |
|
||||
|-----------|-------|--------|---------|
|
||||
| **Completed** | 9 | ✅ Done | K3S, Flux, ESS, Themes, Desktop, Monitoring, TURN, Authentik, Firewall, SSH |
|
||||
| **Completed** | 13 | ✅ Done | K3S, Flux, ESS, Themes, Desktop, Monitoring, TURN, Authentik (Deploy+Enrollment/Recovery/2FA), Firewall, SSH, coturn Fix, Element Call Fork, NetworkPolicies |
|
||||
| **In Progress** | 0 | 🔄 — | — |
|
||||
| **Backlog** | 11+ | 📋 Pending | DB Backups, E2E Test, Element Call Fork, PostgreSQL Migration, NetworkPolicies |
|
||||
| **Backlog** | 8+ | 📋 Pending | DB Backups, PostgreSQL Migration, MAS-Template-Link, VP9-Retry |
|
||||
| **Security Tasks** | 5 | 🔒 Pending | auditd, Kernel hardening, CrowdSec, Falco, WAF |
|
||||
|
||||
### Priority Distribution
|
||||
@@ -25,6 +25,63 @@
|
||||
|
||||
---
|
||||
|
||||
## 🗓️ Session-Zusammenfassung 2026-07-27/28 (fortlaufend aktualisiert)
|
||||
|
||||
Nach längerer Pause wiederaufgenommen — Mac war neu aufgesetzt, Zugriff (SSH, Kubeconfig,
|
||||
age-Key, Homebrew/flux/helm/sops/age) komplett wiederhergestellt und dauerhaft in `~/.zshrc`
|
||||
verankert. Was in dieser Session erledigt wurde:
|
||||
|
||||
1. **Authentik Enrollment/Recovery/2FA** (Issue #7 ✅ geschlossen) — siehe Phase 8 unten und
|
||||
`docs/troubleshooting/README.md`. `matrix-invitation`- und `matrix-recovery`-Flows waren
|
||||
kaputt bzw. leer, jetzt als Authentik Blueprint (`apps/authentik/authentik-blueprints.yaml`)
|
||||
deklarativ repariert. E2E mit echten Test-Usern (`clark`, `lucky`) verifiziert.
|
||||
2. **coturn-Crash behoben** — Liveness-Probe nutzte `netstat` (existiert nicht im Image),
|
||||
Server killte einen gesunden Prozess seit 88 Tagen, 36.000+ Restarts. Auf `tcpSocket`-Probe
|
||||
umgestellt, läuft seitdem stabil.
|
||||
3. **Element Call Fork** (Issue #8 ✅ geschlossen, Release `m6-element-call-fork-complete`) —
|
||||
1440p/60fps-Defaults, siehe Kapitel 4 in `docs/deployment-guides/04-element-customization.md`.
|
||||
**Wichtig**: erzwungenes `video_codec: vp9` hat Calls kurzzeitig live komplett kaputt gemacht
|
||||
(kein Bild/Ton) — sofort zurückgerollt, ohne Codec-Zwang läuft's. Root Cause dafür nicht
|
||||
abschließend isoliert, nur umgangen.
|
||||
4. **Identitäts-Aufräumarbeiten**: `sorB`'s Authentik-E-Mail korrigiert (`thorec@hotmail.de`),
|
||||
MAS OIDC-Link (`upstream_oauth_links`) von `sorB` zeigte fest auf den alten MAS-User
|
||||
`akadmin`/`@akadmin:axion1337.chat` (Sub-Hash ist stabil über Username-Renames, daher blieb
|
||||
die Verknüpfung nach dem Rename "akadmin"→"sorB" bestehen) — umgehängt auf `sorb`/
|
||||
`@sorb:axion1337.chat`. Neue Identität `elbojoloco` angelegt (E-Mail `cfx@riot.8shield.net`),
|
||||
verknüpft mit dem alten `akadmin`-MAS-User. **Übrig**: ein leeres, unverknüpftes
|
||||
`@bojeledoggo:axion1337.chat`-Konto (Tippfehler-Artefakt) — User räumt das selbst auf.
|
||||
5. **NetworkPolicies** (Issue #10 ✅ geschlossen) — siehe "Network Security" Abschnitt unten.
|
||||
Zwei Live-Incidents beim Rollout (Port-Verwechslungen), beide binnen Minuten live gepatcht
|
||||
und danach committed. Nebenbei: `matrixRTC`-Authorisation-Service OOM-Fix (20Mi→128Mi).
|
||||
6. **Element Call Qualität nachgeschärft** — 720p-Zwischen-Simulcast-Layer ergänzt (sonst
|
||||
harter Sprung von 1440p auf blockiges 360p bei kleinsten Netzwerkschwankungen), und
|
||||
`video_codec: h264` statt VP8 (klassisches Simulcast wie VP8, kein SVC-Risiko wie bei
|
||||
VP9, oft hardwarebeschleunigt v.a. auf iOS). Live verifiziert: 7/8 Tracks nativ H.264,
|
||||
1 sauberer VP8-Fallback. Deployed als `v0.2.3-elementcall-h264`.
|
||||
7. **Backlog nach Gitea migriert** — restlicher offener Backlog (VP9-Retry, ThreadNet-Web-Bug,
|
||||
MAS-Template-Link, WAF und 17 weitere Security-/Infra-Punkte) als Issues #11–#31 angelegt,
|
||||
veraltete erledigte Punkte (Authentik Stage 2/E2E-Test/Invite-Links, Hetzner-Firewall,
|
||||
SSH-Hardening) aus dieser Datei entfernt bzw. als done markiert.
|
||||
|
||||
### Offene Punkte
|
||||
- **VP9-Retry**: vermutete Ursache jetzt bekannt (LiveKit nutzt SVC für vp9/av1, Fork-Code
|
||||
setzt aber immer Simulcast-Layer) — braucht einen Code-Fix in `buildPublishOptions()`
|
||||
(`src/livekit/options.ts`) bevor erneut versucht wird. Stattdessen H.264 probiert (siehe
|
||||
unten) — läuft gut, kein SVC-Risiko, hardwarebeschleunigt auf mehr Geräten.
|
||||
- **`ThreadNet-Web` Build-Bug**: `scripts/docker-link-repos.sh`/`docker-package.sh` nicht
|
||||
ausführbar committet + veralteter `matrix-js-sdk#develop`-Pin im Lockfile blockiert
|
||||
vollständigen Neu-Build des Web-Forks. Noch nicht gefixt, User hat noch nicht final
|
||||
entschieden ob gewünscht.
|
||||
- **Verwaistes `@bojeledoggo:axion1337.chat`**: leeres Matrix-Konto ohne OIDC-Link, User räumt
|
||||
das selbst auf (braucht dafür seinen eigenen Access-Token für die Admin-API).
|
||||
- **MAS-Template-Link**: 2FA/Passkey-Setup-Links direkt auf `account.axion1337.chat/account/`
|
||||
statt nur über `docs/setup/security.html` — braucht MAS Custom-Template-Override
|
||||
(`templates.path`), größerer separater Task.
|
||||
- Nächste Kandidaten aus den offenen Issues: #6 (DB-Backup, CRITICAL), #9 (PostgreSQL-Migration),
|
||||
#10 (NetworkPolicies).
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Next Steps (Priorisiert)
|
||||
|
||||
### 🔴 **THIS WEEK – CRITICAL**
|
||||
@@ -58,33 +115,53 @@
|
||||
- **Status**: NEXT
|
||||
|
||||
### 🟠 **NEXT 1–2 WEEKS – HIGH**
|
||||
1. **Authentik End-to-End Test**
|
||||
- Test: Login flow Element → MAS → Authentik → Matrix User
|
||||
- Test: Password reset
|
||||
- Create: Test invite links
|
||||
- Est. Time: 2 hours
|
||||
1. ✅ **Authentik End-to-End Test** — erledigt als Teil von Issue #7 (Enrollment/Recovery/2FA,
|
||||
2026-07-27), mit echten Test-Usern verifiziert. **Status**: COMPLETE
|
||||
|
||||
2. **Element Call Fork**
|
||||
- Fork: element-hq/element-call
|
||||
- Feature: Video/audio constraints parameters
|
||||
- Integration: Synapse well-known config
|
||||
- Est. Time: 2–3 days
|
||||
2. ✅ **Element Call Fork** — erledigt, Closes Issue #8 (2026-07-28), siehe
|
||||
`docs/deployment-guides/04-element-customization.md` Kapitel 4. **Status**: COMPLETE
|
||||
|
||||
3. **External PostgreSQL Migration**
|
||||
3. **External PostgreSQL Migration** → [Issue #9](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/9)
|
||||
- Decision: CloudNativePG vs. Hetzner Postgres
|
||||
- Setup: HA + Replication
|
||||
- Migration: Move data from ESS embedded Postgres
|
||||
- Testing: Verify all services work
|
||||
- Est. Time: 1–2 days
|
||||
|
||||
4. **NetworkPolicies Deployment**
|
||||
- Create: Default-Deny for `matrix` namespace
|
||||
- Create: Allow rules (Synapse↔Postgres, MAS↔Postgres, Ingress→Web, etc.)
|
||||
- Test: Ensure no service breakage
|
||||
- Est. Time: 1 day
|
||||
4. ✅ **NetworkPolicies Deployment** — erledigt, Closes Issue #10 (2026-07-28), Default-Deny
|
||||
für `matrix`+`authentik` Namespaces. **Status**: COMPLETE
|
||||
|
||||
---
|
||||
|
||||
## 🗓️ Session-Zusammenfassung 2026-07-30 bis 2026-08-01 (CI/CD-Umzug ins Lab)
|
||||
|
||||
1. **CI/CD komplett ins Homelab-GitLab migriert** (`git.lab`, nur im Lab auflösbar) —
|
||||
nach vier realen Gitea-Actions/act-Bugs und einem verifizierten Webpack-OOM auf dem
|
||||
3,7-GiB-CFGMON-Runner. ThreadNet-Web-Pipeline vollautomatisch grün (Web-Build,
|
||||
Image-Push nach rohana, Desktop-Linux deb/tar.gz). Schlankes Trigger-Schema: Pushes
|
||||
prüfen nur bei relevanten Pfaden, Artefakte entstehen bei `v*`-Tags.
|
||||
(ThreadNet-Web#2 ✅ geschlossen)
|
||||
2. **Repo-Topologie neu**: git.lab ist kanonisch für gitops, ThreadNet-Web,
|
||||
threadnet-call, thread-net-git, threadnet-operating — Push-Mirrors nach Gitea,
|
||||
das Flux-Quelle/Registry/Issues behält. Details: README §3 / CLAUDE.md.
|
||||
3. **Windows-Build-Strecke** (ThreadNet-Web#5): eigene Windows-VM auf Overmind aus
|
||||
selbst gebautem, reviewtem dockur/windows-Stand (Vendor-Repo + Runbook), On-Demand
|
||||
per CI-Jobs; Gast provisioniert, Runner registriert — erster voller Build-Durchlauf
|
||||
stand zum Session-Ende noch aus (Runner-Dienst-Hänger nach VM-Neustart).
|
||||
4. **Lab-Container-Registry** aktiviert (`registry.git.lab`, OVERMIND-01 ✅): lab-interne
|
||||
Build-Images (windows-vm, desktop-build) bleiben im Lab, rohana behält nur, was
|
||||
Prod konsumiert.
|
||||
5. **Gitea-CI-Rückbau** (CFGMON-11, weitgehend): Verifikations-Job hierher portiert
|
||||
(`.gitlab-ci.yml`), `.gitea/workflows/` entfernt, Actions-Toggles deaktiviert,
|
||||
Runner-Entfernung als Commit vorbereitet — drei manuelle Restschritte beim Nutzer.
|
||||
6. **threadnet-call-CI** (threadnet-call#1): build_embedded grün, npm-Registry bleibt
|
||||
evidenzbasiert auf rohana (pnpm-Lockfile-Pin), manueller Publish-Job wartet auf
|
||||
`GITEA_NPM_TOKEN`-Variable.
|
||||
7. **Alerting vorbereitet** (Issue #32): Alertmanager + 6 Alert-Regeln +
|
||||
Matrix-Receiver in `threadnet-operating`, gelintet, Deploy steht aus (CFGMON).
|
||||
8. Nebenbefunde: GitLab-Puma lief mit ~17 Workern (Unicorn-Fossil in der Config,
|
||||
Fix beim Nutzer), Windows-Gast-Provisionierung als idempotenter CI-Job.
|
||||
|
||||
## ✅ Abgeschlossene Aufgaben (Chronologisch)
|
||||
|
||||
### Phase 1: Basis-Setup
|
||||
@@ -206,289 +283,181 @@
|
||||
|
||||
**None** – Alle CRITICAL Tasks erledigt! Nächster Focus: Database Backups
|
||||
|
||||
### Phase 8: Authentik Enrollment/Recovery/MFA Fix (2026-07-27)
|
||||
- [x] **matrix-invitation Flow repariert** – fehlende Write/Password/Login-Stages ergänzt, Reihenfolge korrigiert, als Authentik Blueprint (`apps/authentik/authentik-blueprints.yaml`) reproduzierbar gemacht
|
||||
- [x] **matrix-invitation-prompt** – 16 fehlerhafte `validation_policies` entfernt (crashten mit `AnonymousUser`/`NoneType`-Fehlern)
|
||||
- [x] **Redirect-Stage** – Flow endet jetzt auf `axion1337.chat` statt in der `/if/user/`-Sackgasse (blockiert für `type=external`)
|
||||
- [x] **matrix-recovery Flow gebaut** – war komplett leer (0 Stages); Passwort-Reset funktioniert jetzt, verlinkt von der echten Login-Seite
|
||||
- [x] **Brand.default_application gesetzt** – behebt mehrere Dead-Ends, wenn eingeloggte User `/` ohne Ziel aufrufen
|
||||
- [x] **2FA/Passkey Selbst-Einrichtung** – Links zu `default-authenticator-totp-setup`/`-webauthn-setup` (2FA bleibt optional, `not_configured_action=skip`), dokumentiert unter `axion1337.chat/docs/setup/security.html`
|
||||
- [x] **Backlog**: Issue #13 geschlossen (2026-07-29) - MAS-Template-Override verworfen, MAS
|
||||
unterstützt laut live geprüfter OIDC-Discovery keine 2FA/Passkey-Deep-Link-Action. Jetzt
|
||||
als Client-Änderung nachgehalten: [ThreadNet-Web#4](https://rohana.axion1337.de/sorb/ThreadNet-Web/issues/4)
|
||||
|
||||
---
|
||||
|
||||
## 📋 Backlog (Weitere Aufgaben)
|
||||
|
||||
### Authentik Completion
|
||||
- [ ] **Finish Authentik Stage 2 – MAS Integration**
|
||||
- Prerequisites: Authentik OIDC Provider vollständig konfiguriert
|
||||
- Task: Update `mas-secret.yaml`, enable password login disable
|
||||
- Commit: `enable-authentik-oidc-integration-in-mas`
|
||||
- Est. Effort: 30 min (manual + scripted)
|
||||
|
||||
- [ ] **Test End-to-End Login Flow**
|
||||
- Element Web login → MAS → Authentik → Matrix User Creation
|
||||
- Create test users via Authentik
|
||||
- Verify password reset flow
|
||||
- Commit: (implicit in Stage 2)
|
||||
- Est. Effort: 20 min
|
||||
|
||||
- [ ] **Create Invite Links für neue User**
|
||||
- Authentik Admin UI → Invitations → Create
|
||||
- Set expiry dates (7d) + use limits
|
||||
- Document procedure
|
||||
- Est. Effort: 15 min
|
||||
**Ab 2026-07-28 in Gitea-Issues gepflegt statt hier** (eine Quelle der Wahrheit) — offene Issues:
|
||||
[#6](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/6) DB-Backup-Strategie,
|
||||
[#9](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/9) Externe PostgreSQL-Migration,
|
||||
[#11](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/11)–[#31](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/31)
|
||||
(VP9-Retry, ThreadNet-Web-Build-Bug, MAS-Template-Link, WAF, Media-PVC-Backups, Pod Security
|
||||
Admission, Federation-Allowlist, Mjolnir/Draupnir, Content-Scanner, External-Secrets,
|
||||
Renovate/Trivy, Security-Advisory-Monitoring, automountServiceAccountToken,
|
||||
unattended-upgrades, K3s-API-Security, auditd, Kernel-Hardening, Lynis, CrowdSec, Falco).
|
||||
Die detaillierten Beschreibungen unten sind das historische Original, aus dem die Issues
|
||||
entstanden sind — nicht mehr getrennt pflegen, stattdessen die Issues aktuell halten.
|
||||
|
||||
### Element Call Enhancement
|
||||
- [ ] **Element Call Fork für Custom Constraints**
|
||||
- Repository: Fork `element-hq/element-call`
|
||||
- Feature: Video/Audio constraints parameter im config
|
||||
- Include: Bandwidth limiting, resolution limits, frame rate control
|
||||
- Integration mit Synapse well-known
|
||||
- Est. Effort: 2–3 days (fork + feature + test)
|
||||
- Priority: **HIGH** (user feature)
|
||||
- [x] **Element Call Fork für Custom Constraints** (2026-07-28, Closes #8)
|
||||
- Fork: `rohana.axion1337.de/sorb/threadnet-call` (basiert auf `emmick4/element-call:livekit`,
|
||||
das den noch nicht gemergten Upstream-PR element-hq/element-call#3736 enthält —
|
||||
config-driven `media_quality`, keine Custom-Logik nötig)
|
||||
- Defaults angehoben: Video bis 1440p/60fps (~8 Mbps), Screen-Share 1440p/30fps (~6 Mbps).
|
||||
Das sind Startwerte, keine harten Limits — Nutzer können in den Settings weiter hochdrehen.
|
||||
- **Incident (2026-07-28)**: Erster Deploy (`v0.2.0`, mit `video_codec: vp9` erzwungen) hat
|
||||
Calls komplett kaputt gemacht (kein Bild/Ton), obwohl LiveKit-Server-Logs den
|
||||
Codec-Regression-Fallback auf VP8 als erfolgreich zeigten — Root Cause nicht abschließend
|
||||
isoliert. Sofort auf `v0.1.0` zurückgerollt, dann `v0.2.1` ohne erzwungenen Codec (Standard
|
||||
VP8) mit denselben 1440p/60fps-Werten deployed und vom Nutzer live bestätigt: funktioniert.
|
||||
VP9-Präferenz vorerst fallengelassen, siehe Backlog.
|
||||
- Rauschunterdrückung: nur clientseitige WebRTC-Standardtoggles (echoCancellation/
|
||||
noiseSuppression/autoGainControl), kommt kostenlos mit derselben PR. **Bewusst kein**
|
||||
server-seitiges ML-Noise-Cancellation (LiveKit Agents + DTLN/RNNoise) — laut LiveKits
|
||||
eigener Doku ist das für Mensch-zu-Mensch-Calls der falsche Ansatz (nur für AI-Voice-Agents
|
||||
gedacht, kein Standard-Pfad um bereinigtes Audio an andere Teilnehmer zurückzugeben).
|
||||
- Well-Known/`org.matrix.msc4143.rtc_foci`-Delegation war schon vom ESS-Chart korrekt
|
||||
automatisch konfiguriert — kein Handlungsbedarf trotz anderslautendem Issue-Text.
|
||||
- **Deployment-Ansatz geändert**: `sorb/ThreadNet-Web` (der Element-Web-Fork) hat einen
|
||||
vorbestehenden, unabhängigen Build-Bug (siehe unten) und ließ sich nicht komplett neu
|
||||
bauen. Stattdessen: nur der `/app/widgets/element-call/`-Ordner im bereits laufenden
|
||||
`threadnet-web:v0.1.0`-Image ausgetauscht → neues Image
|
||||
`rohana.axion1337.de/sorb/threadnet-web:v0.2.0-elementcall-mediaquality`.
|
||||
- Verifiziert: `media_quality` live auf `axion1337.chat/widgets/element-call/config.json`.
|
||||
- **Gefunden, nicht gefixt**: `ThreadNet-Web` lässt sich aktuell nicht komplett neu bauen
|
||||
— `scripts/docker-link-repos.sh`/`docker-package.sh` sind im Repo nicht ausführbar
|
||||
committet (Mode 644 statt 755), UND der gepinnte `matrix-js-sdk#develop`-Commit im
|
||||
Lockfile ist zu alt (fehlt `src/oidc/authorize.ts`, das `apps/web` importiert). Beides
|
||||
unabhängig von diesem Fix, blockiert aber jeden zukünftigen vollständigen Rebuild.
|
||||
**Update 2026-07-28 (Issue #12): behoben** — Skript-Rechte korrigiert, matrix-js-sdk auf
|
||||
einen funktionierenden Commit (`d19cb751`, letzter vor einem API-Breaking-Rename) gepinnt,
|
||||
zusätzlich gefunden+gefixt: Element-Call-Referenz zeigte noch auf Upstream statt unseren
|
||||
Fork. Mit echtem Full-Docker-Build verifiziert, siehe [[Element-Customization]] Wiki.
|
||||
- Backlog: MAL-basierte Noise-Cancellation (LiveKit Agents + self-hosted DTLN/RNNoise) als
|
||||
experimentelle Idee, falls später gewünscht — kein etablierter Pfad für Conferencing.
|
||||
- Backlog: VP9-Codec-Präferenz erneut versuchen, sobald PR #3736 upstream gemerged/gereift
|
||||
ist oder Root Cause des Ausfalls isoliert wurde (Browser-Konsolen-Repro nötig).
|
||||
|
||||
### Database Hardening
|
||||
- [ ] **External/Dedicated PostgreSQL Deployment**
|
||||
- Option 1: CloudNativePG Operator (open-source, auf K3S)
|
||||
- Option 2: Managed Hetzner Postgres
|
||||
- Separate aus ESS matrix-stack embedded Postgres
|
||||
- HA + Replication
|
||||
- Est. Effort: 1–2 days
|
||||
- Priority: **HIGH** (reliability)
|
||||
- [ ] **External/Dedizierte PostgreSQL-Migration** → [Issue #9](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/9)
|
||||
|
||||
- [ ] **Database Backup Strategy**
|
||||
- Daily automated backups (PgBackRest oder velero)
|
||||
- Off-site backup storage (S3 / Hetzner Storage Box)
|
||||
- Monthly verified restores (test restore → verify data integrity)
|
||||
- Backup + restore documentation
|
||||
- Est. Effort: 2–3 days
|
||||
- Priority: **CRITICAL** (disaster recovery)
|
||||
|
||||
- [ ] **Synapse Media PVC Backups**
|
||||
- Separate backup pipeline für `/data/media_store` PVC
|
||||
- Reason: Media oft >100GB, sollte nicht im DB-Backup sein
|
||||
- Velero + Restic für block-level backup
|
||||
- Est. Effort: 1 day
|
||||
- Priority: **HIGH** (data preservation)
|
||||
(Database Backup Strategy und Synapse Media PVC Backups waren hier ursprünglich als eigene
|
||||
Punkte gelistet - beide erledigt und geschlossen, siehe [#6](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/6)
|
||||
und [#15](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/15), Details in
|
||||
[[00-TASKS]] Wiki bzw. Release v0.16.0.)
|
||||
|
||||
### Network Security
|
||||
- [ ] **NetworkPolicies – K8s-Layer Segmentation**
|
||||
- Default-Deny Ingress für `matrix` namespace
|
||||
- Allow rules:
|
||||
- Ingress → MAS:443
|
||||
- Ingress → ElementWeb:443
|
||||
- MAS ↔ Synapse:8008
|
||||
- Synapse ↔ Postgres:5432
|
||||
- Authentik → Postgres:5432
|
||||
- Authentik → Loki:3100 (monitoring)
|
||||
- Egress: Matrix-specific (federation, etc.)
|
||||
- Est. Effort: 1 day
|
||||
- Priority: **MEDIUM** (compliance, least-privilege)
|
||||
- [x] **NetworkPolicies – K8s-Layer Segmentation** (2026-07-28, Closes #10)
|
||||
- Default-Deny Ingress (egress left untouched) für `matrix` UND `authentik` namespaces,
|
||||
per-Komponente Allow-Regeln in `apps/authentik/networkpolicy.yaml` und
|
||||
`apps/production/networkpolicy.yaml`. Rollout: authentik zuerst als Pilot, dann matrix.
|
||||
- Empirisch verifiziert, dass K3s' eingebauter NetworkPolicy-Controller tatsächlich
|
||||
durchsetzt (Testnamespace, Timeout- statt Refused-Verhalten unter Deny-Policy).
|
||||
- **Zwei Live-Incidents beim Rollout, beide binnen Minuten behoben**:
|
||||
1. `authentik-server`: Regel erlaubte Service-Port 80/443, aber NetworkPolicy filtert
|
||||
auf dem tatsächlichen Container-Port (9000/9443 nach kube-proxy-DNAT) — 502 auf
|
||||
`auth.axion1337.chat`, sofort korrigiert.
|
||||
2. `matrix-authentication-service`: Regel erlaubte Synapse nur auf Port 8081, aber
|
||||
Synapse ruft `/oauth2/introspect` tatsächlich auf **Port 8080** — jede
|
||||
authentifizierte Anfrage (inkl. `/sync`) scheiterte mit 503, alle Clients zeigten
|
||||
"Verbindung unterbrochen". Live gepatcht, dann committed.
|
||||
- Lehre für zukünftige NetworkPolicies in diesem Repo: wo immer ein Service benannte
|
||||
Ports (`targetPort: <name>`) nutzt, diese direkt in der Policy referenzieren statt
|
||||
Portnummern zu raten — schließt genau diese Fehlerklasse aus.
|
||||
- Nebenbefund (unabhängig von NetworkPolicies): `matrixRTC`-Authorisation-Service hatte
|
||||
ein 20Mi-Memory-Limit (Chart-Default), OOM-gekillt nach ~74 Tagen Uptime während der
|
||||
Verifikations-Calls — auf 64Mi/128Mi angehoben.
|
||||
- `coturn` (hostNetwork) bewusst ausgenommen — NetworkPolicy greift dort nicht.
|
||||
- `authentik-postgresql`'s Bitnami-Chart-Policy (Port 5432, quelloffen) bewusst nicht
|
||||
angefasst/dupliziert, da Helm-verwaltet.
|
||||
|
||||
- [ ] **Pod Security Admission (Restricted)**
|
||||
- Apply to `matrix` & `authentik` namespaces
|
||||
- Enforce: non-root, no privileged, read-only root fs
|
||||
- Test: Ensure no chart breakage
|
||||
- Est. Effort: 1 day
|
||||
- Priority: **MEDIUM** (hardening)
|
||||
- [ ] **Pod Security Admission (Restricted)** → [Issue #16](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/16)
|
||||
|
||||
### Federation & Access Control
|
||||
- [ ] **Federation-Allowlist oder Closed Federation**
|
||||
- Decision: Which servers to federate with?
|
||||
- If allowlist: explicit `federation_domain_whitelist`
|
||||
- If closed: `allow_public_rooms_without_join_rules: false`
|
||||
- Synapse config in `synapse-values.yaml`
|
||||
- Est. Effort: 4 hours
|
||||
- Priority: **MEDIUM** (security policy)
|
||||
- [ ] **Federation-Allowlist oder Closed Federation** → [Issue #17](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/17)
|
||||
|
||||
### Moderation & Anti-Abuse
|
||||
- [ ] **Mjolnir/Draupnir Bot Deployment**
|
||||
- Open-source moderation bot für Matrix
|
||||
- Reason: Invitation-based, aber Federation kann Spam bringen
|
||||
- Auto-ban known bad servers/users
|
||||
- Spam-detection rules
|
||||
- HelmChart oder custom Deployment
|
||||
- Est. Effort: 1–2 days
|
||||
- Priority: **MEDIUM** (ops safety)
|
||||
|
||||
- [ ] **Content Scanner for Media**
|
||||
- matrix-content-scanner + ClamAV antivirus
|
||||
- Scan uploaded media for malware
|
||||
- Block suspicious files
|
||||
- Est. Effort: 1–2 days
|
||||
- Priority: **LOW–MEDIUM** (optional but good practice)
|
||||
- [x] **Draupnir Moderationsbot** → [Issue #18](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/18) (2026-07-29, deployed + live getestet)
|
||||
- [x] **Content Scanner für Media** → [Issue #19](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/19) (2026-07-29, Synapse-Modul + ClamAV für unverschlüsselte Räume, plus client-seitiger Scan im ThreadNet-Web-Fork für verschlüsselte Räume/DMs - live getestet, beide Richtungen; siehe Deployment-Guide)
|
||||
- [ ] **Grafana-Dashboard für ClamAV-Erkennungen** → [Issue #43](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/43)
|
||||
- [ ] **ThreadNet-Web: Electron-Desktop-Build automatisieren** → [Issue #44](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/44) (kein CI-Runner, Fork-Änderungen landen aktuell nicht automatisch im Desktop-Client)
|
||||
|
||||
### Secrets Management
|
||||
- [ ] **External-Secrets Operator oder SOPS für Flux**
|
||||
- Current: SOPS with age encryption
|
||||
- Consideration: External-Secrets for cloud-native (AWS Secrets Manager, Hetzner Vault, etc.)
|
||||
- OR: Improve SOPS rotation strategy
|
||||
- Decision needed: Keep SOPS or upgrade?
|
||||
- Est. Effort: 2–3 days (if switching)
|
||||
- Priority: **LOW** (current SOPS setup working)
|
||||
- [ ] **External-Secrets Operator vs. SOPS-Setup** → [Issue #20](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/20)
|
||||
|
||||
### Image & Dependency Management
|
||||
- [ ] **Renovate / Dependabot Setup**
|
||||
- Auto-update Helm Chart versions
|
||||
- Auto-update Container Image Tags
|
||||
- Monitor for security patches
|
||||
- Est. Effort: 4 hours
|
||||
- Priority: **MEDIUM** (maintenance)
|
||||
|
||||
- [ ] **Trivy Image Scanning**
|
||||
- Scan images in Flux HelmReleases for CVEs
|
||||
- Block deployment if critical CVE found
|
||||
- CI/CD hook in git workflow
|
||||
- Est. Effort: 8 hours
|
||||
- Priority: **LOW–MEDIUM** (security posture)
|
||||
|
||||
- [ ] **Monitor ESS & Element Security Advisories**
|
||||
- Subscribe to `element-hq` security mailing list
|
||||
- Monitor `#matrix-community` security channels
|
||||
- Auto-alerts on new CVEs/patches
|
||||
- Est. Effort: Ongoing (low maintenance)
|
||||
- Priority: **MEDIUM** (security awareness)
|
||||
- [ ] **Renovate/Dependabot Setup** → [Issue #21](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/21)
|
||||
- [ ] **Trivy Image Scanning** → [Issue #31](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/31)
|
||||
- [ ] **Security Advisory Monitoring (ESS/Element)** → [Issue #22](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/22)
|
||||
|
||||
### Container Security
|
||||
- [ ] **Disable automountServiceAccountToken Everywhere**
|
||||
- Audit all Deployments/StatefulSets
|
||||
- Disable for: Synapse, ElementWeb, MAS, Postgres, Authentik (where not needed)
|
||||
- Add `automountServiceAccountToken: false` to spec.template.spec
|
||||
- Test: Ensure no breakage
|
||||
- Est. Effort: 4 hours
|
||||
- Priority: **MEDIUM** (least-privilege)
|
||||
- [ ] **automountServiceAccountToken deaktivieren wo nicht benötigt** → [Issue #23](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/23)
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Security Hardening (Host & Cluster Level)
|
||||
|
||||
### Host OS Layer (Ubuntu/Debian)
|
||||
- [ ] **Hetzner Cloud Firewall**
|
||||
- Default-Deny inbound
|
||||
- Allow: 80/443 (HTTP/HTTPS)
|
||||
- Allow: 22 (SSH) from your IP only (or via WireGuard/Tailscale)
|
||||
- Status: ✅ Can be done in Hetzner UI
|
||||
- Est. Effort: 30 min
|
||||
- Priority: **CRITICAL** (immediate, zero config cost)
|
||||
- [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.**
|
||||
|
||||
- [ ] **SSH Hardening**
|
||||
- Disable password auth (key-only)
|
||||
- Disable root login
|
||||
- PermitRootLogin: no
|
||||
- PasswordAuthentication: no
|
||||
- MaxAuthTries: 3
|
||||
- Optional: Change SSH port (cosmetic, reduces log noise)
|
||||
- Optional: SSH hinter WireGuard/Tailscale (eliminates fail2ban für SSH)
|
||||
- Est. Effort: 2 hours
|
||||
- Priority: **HIGH** (immediate)
|
||||
|
||||
- [ ] **unattended-upgrades**
|
||||
- Enable automatic security updates
|
||||
- Configure: APT::Periodic::Update-Package-Lists "1";
|
||||
- Configure: APT::Periodic::Unattended-Upgrade "1";
|
||||
- Configure: APT::Periodic::AutocleanInterval "7";
|
||||
- Est. Effort: 30 min
|
||||
- Priority: **HIGH** (set & forget)
|
||||
|
||||
- [ ] **K3S API Security**
|
||||
- Current: K3S API listening on :6443 on all interfaces (default)
|
||||
- Hardening:
|
||||
- Option 1: Firewall restrict :6443 to localhost only
|
||||
- Option 2: K3S --bind-address + --advertise-address to WireGuard IP
|
||||
- Option 3: kubectl access only via jumphost/bastion
|
||||
- Est. Effort: 2 hours
|
||||
- Priority: **HIGH** (API is high-value target)
|
||||
|
||||
- [ ] **auditd for File Integrity & Syscall Audit**
|
||||
- Monitor: /etc, ~/.kube, /var/lib/rancher/k3s
|
||||
- Audit rules für sensitive file changes
|
||||
- Low overhead, good signal/noise ratio
|
||||
- Output to syslog / centralized logging
|
||||
- Est. Effort: 2 hours
|
||||
- Priority: **MEDIUM** (forensics + compliance)
|
||||
|
||||
- [ ] **Kernel Hardening (sysctl)**
|
||||
- Apply hardening recommendations from Lynis
|
||||
- Key settings:
|
||||
- kernel.kptr_restrict=2 (hide kernel pointers)
|
||||
- kernel.dmesg_restrict=1 (restrict dmesg)
|
||||
- net.ipv4.tcp_syncookies=1 (SYN flood protection)
|
||||
- net.ipv4.conf.all.rp_filter=1 (reverse path filtering)
|
||||
- net.ipv4.conf.all.send_redirects=0
|
||||
- net.ipv6.conf.all.disable_ipv6=0 (or =1 if no IPv6 needed)
|
||||
- Persist via /etc/sysctl.d/99-hardening.conf
|
||||
- Est. Effort: 2 hours
|
||||
- Priority: **MEDIUM** (defense in depth)
|
||||
|
||||
- [ ] **Lynis Security Baseline**
|
||||
- Run `lynis audit system`
|
||||
- Review recommendations
|
||||
- Implement high-priority findings
|
||||
- Aim for score >80
|
||||
- Re-run quarterly
|
||||
- Est. Effort: 4 hours (initial) + 1 hour quarterly
|
||||
- Priority: **MEDIUM** (baseline verification)
|
||||
- [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)
|
||||
- [ ] **Lynis Security Baseline** → [Issue #28](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/28)
|
||||
|
||||
### Cluster Layer (K3S / Kubernetes)
|
||||
- [ ] **CrowdSec Integration**
|
||||
- Install CrowdSec agent on host
|
||||
- Connect to CrowdSec Hub (commercial platform, free tier available)
|
||||
- Feed auth.log, syslog → CrowdSec for attack detection
|
||||
- Auto-block IPs via local firewall or Hetzner Firewall API
|
||||
- Est. Effort: 4 hours
|
||||
- Priority: **MEDIUM** (proactive threat response)
|
||||
|
||||
- [ ] **Falco Runtime Monitoring**
|
||||
- Install Falco DaemonSet in K3S
|
||||
- Monitor: Shell spawning in containers, suspicious syscalls, privilege escalation
|
||||
- Output to Loki / syslog
|
||||
- Alert on anomalies
|
||||
- Est. Effort: 1 day
|
||||
- Priority: **MEDIUM** (runtime detection)
|
||||
- [ ] **CrowdSec Integration** → [Issue #29](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/29)
|
||||
- [ ] **Falco Runtime Monitoring** → [Issue #30](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/30)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Meilensteine (Milestones)
|
||||
## 🎯 Versionierung
|
||||
|
||||
| Meilenstein | Beschreibung | Status | ETA |
|
||||
|------------|-------------|--------|-----|
|
||||
| **M1: Basis-Setup** | K3S + Flux + ESS deployed | ✅ Done | - |
|
||||
| **M2: Core Matrix** | Themes, Scripts, Policies | ✅ Done | - |
|
||||
| **M3: WebRTC & Monitoring** | TURN + Alloy/Prometheus/Loki | ✅ Done | - |
|
||||
| **M4: Identity Provider** | Authentik Stage 1+2 (pending Stage 2) | 🔄 In Progress | ~1–2 days |
|
||||
| **M5: Production-Ready** | DB Backups, NetworkPolicies, Security Hardening | 📋 Backlog | ~2–3 weeks |
|
||||
| **M6: Advanced Features** | Element Call Fork, Content Scanner, Mjolnir | 📋 Backlog | ~4+ weeks |
|
||||
| **M7: Enterprise-Ready** | Full compliance (DSGVO), HA setup, Disaster Recovery | 🎯 Future | ~8+ weeks |
|
||||
Seit 2026-07-28 SemVer statt der alten m1-m7-Meilensteine - siehe
|
||||
[Releases](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/releases) für die volle,
|
||||
detaillierte Historie (aktuell bis v0.17.0) und [[00-TASKS]] im Wiki für die Konvention
|
||||
(MINOR = neue Fähigkeit, PATCH = Fix/Tuning/Doku).
|
||||
|
||||
---
|
||||
|
||||
## 📊 Prioritäts-Kategorien
|
||||
|
||||
### 🔴 CRITICAL (do immediately)
|
||||
- Hetzner Cloud Firewall setup
|
||||
- Database backup strategy
|
||||
- SSH hardening
|
||||
Alle Punkte hier sind als Gitea-Issues nachgehalten (Nummern siehe oben/Backlog-Verweis) - diese
|
||||
Kategorisierung ist nur eine grobe Einordnung, keine zweite Tracking-Quelle.
|
||||
|
||||
### 🟠 HIGH (do within 1–2 weeks)
|
||||
- Authentik Stage 2 completion
|
||||
- External PostgreSQL migration
|
||||
- NetworkPolicies
|
||||
- Element Call fork
|
||||
### 🟠 HIGH
|
||||
- External PostgreSQL migration (#9)
|
||||
|
||||
### 🟡 MEDIUM (do within 1 month)
|
||||
- CrowdSec + Falco
|
||||
- Mjolnir bot
|
||||
- Renovate/Trivy
|
||||
- PSA restricted mode
|
||||
- Kernel hardening
|
||||
- CrowdSec + Falco (#29, #30)
|
||||
- Renovate/Trivy (#31, #32)
|
||||
- K3s API Hardening, auditd, Kernel Hardening, Lynis (#25-#28)
|
||||
|
||||
### 🟢 LOW (nice-to-have, do if time allows)
|
||||
- Content scanner (ClamAV)
|
||||
- External-Secrets upgrade
|
||||
- SSH port relocation
|
||||
- Advanced federation rules
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes & Decision Points
|
||||
|
||||
### Authentik Stage 2 Blocker
|
||||
⏳ **Waiting for**: User to manually configure Authentik OIDC Provider in Authentik Admin UI.
|
||||
- Once done, provide Client ID + Secret
|
||||
- Then: Commit Stage 2 MAS config
|
||||
|
||||
### Database: CloudNativePG vs. Hetzner Postgres
|
||||
- **CloudNativePG**: Open-source, runs on K3S, full control
|
||||
- **Hetzner Postgres**: Managed, backups included, less ops overhead
|
||||
@@ -509,12 +478,13 @@
|
||||
|
||||
- `docs/deployment-guides/README.md` – Overview
|
||||
- `docs/deployment-guides/01-turn-server-setup.md` – TURN
|
||||
- `docs/deployment-guides/02-authentik-identity-provider.md` – Authentik (Stage 1 + Stage 2 plan)
|
||||
- `docs/deployment-guides/02-authentik-identity-provider.md` – Authentik (Stage 1+2 + Enrollment/Recovery/2FA)
|
||||
- `docs/deployment-guides/03-monitoring-integration.md` – Monitoring
|
||||
- `docs/deployment-guides/04-element-customization.md` – Themes, Desktop
|
||||
- `docs/deployment-guides/04-element-customization.md` – Themes, Desktop, Element Call Fork
|
||||
- `docs/deployment-guides/05-room-policies.md` – Policies
|
||||
- `docs/deployment-guides/06-moderation-content-scanning.md` – Draupnir, ClamAV Content Scanning
|
||||
- `docs/deployment-guides/07-host-maintenance-notifications.md` – Host-Wartungsbenachrichtigungen
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-05-14
|
||||
**Next Review**: 2026-05-21
|
||||
**Last Updated**: 2026-07-30
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Authentik als Identity Provider für Matrix
|
||||
|
||||
**Status**: ✅ Stage 1 Deployed (Authentik läuft)
|
||||
**Pending**: Stage 2 (MAS Integration)
|
||||
**Status**: ✅ Deployed (Stage 1 + Stage 2 + Enrollment/Recovery/2FA, Closes Issue #7)
|
||||
**Domain**: `auth.axion1337.chat`
|
||||
|
||||
## Überblick
|
||||
@@ -41,5 +40,22 @@ Authentik = OIDC Provider für MAS → Zentrales Login + Einladungs-basierte Reg
|
||||
|
||||
Authentik Admin → Flows & Stages → Invitations → Create
|
||||
|
||||
## Enrollment/Recovery/2FA Fix (2026-07-27, Issue #7)
|
||||
|
||||
Der `matrix-invitation`-Flow hatte nur 2 von 5 nötigen Stages (kein Write/Password/Login) -
|
||||
Nutzer wurden nie in Synapse angelegt. Behoben und als Authentik Blueprint
|
||||
(`apps/authentik/authentik-blueprints.yaml`) deklarativ ins Repo übernommen: vollständiger
|
||||
`matrix-invitation`-Flow (Invite → Prompt → Write → Password → Login → Redirect), leerer
|
||||
`matrix-recovery`-Flow ergänzt, `Brand.default_application` gesetzt. 2FA/Passkey-Selbst-
|
||||
Einrichtung optional (`not_configured_action=skip`), auffindbar über
|
||||
`axion1337.chat/docs/setup/security.html`. Details: siehe Wiki
|
||||
[Authentik-OIDC.md](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/wiki/Authentik-OIDC).
|
||||
|
||||
**Issue #13 geschlossen (2026-07-29)**: der ursprünglich hier vorgesehene direkte 2FA-Link auf
|
||||
`account.axion1337.chat/account/` (per MAS Custom-Template-Override) wurde verworfen - live
|
||||
geprüfte OIDC-Discovery zeigt, dass MAS keine 2FA/Passkey-Deep-Link-Action unterstützt. Jetzt
|
||||
als Client-seitige Änderung nachgehalten:
|
||||
[ThreadNet-Web#4](https://rohana.axion1337.de/sorb/ThreadNet-Web/issues/4).
|
||||
|
||||
---
|
||||
**Weitere Details**: Siehe Kapitel 2 in diesem Projekt.
|
||||
|
||||
@@ -44,6 +44,61 @@
|
||||
|
||||
**Konfiguration**: `apps/production/element-server-suite.yaml` (ESS Chart)
|
||||
|
||||
## 4. Element Call Fork (Video/Audio-Qualität)
|
||||
|
||||
**Status**: ✅ Deployed (2026-07-28, Closes Issue #8)
|
||||
|
||||
- Fork: `rohana.axion1337.de/sorb/threadnet-call` (basiert auf `emmick4/element-call:livekit`,
|
||||
enthält den noch nicht gemergten Upstream-PR element-hq/element-call#3736 mit
|
||||
config-driven `media_quality` — kein Custom-Code nötig)
|
||||
- Defaults angehoben: Kamera bis **1440p/60fps** (~8 Mbps), Screen-Share **1440p/30fps**
|
||||
(~6 Mbps). Startwerte, keine harten Limits — Nutzer können in Settings weiter hochdrehen.
|
||||
- Rauschunterdrückung: clientseitige WebRTC-Standardtoggles (Echo/Noise/Gain), passend zu
|
||||
LiveKits eigener Empfehlung für Mensch-zu-Mensch-Calls. Bewusst **kein** server-seitiges
|
||||
ML-Noise-Cancellation (siehe `docs/TASKS.md` Backlog).
|
||||
- **Incident (2026-07-28)**: Erster Versuch mit erzwungenem `video_codec: vp9` hat Calls
|
||||
komplett kaputt gemacht (kein Bild/Ton). Sofort zurückgerollt. Vermutete Ursache: LiveKit
|
||||
nutzt für vp9/av1 SVC statt klassischem Simulcast, `buildPublishOptions()` im Fork setzt
|
||||
aber immer Simulcast-Layer — Code-Fix nötig, bevor vp9 erneut versucht wird (Backlog).
|
||||
- **720p-Zwischen-Layer ergänzt** (`simulcast_layers`) — ohne eigene Definition fiel die
|
||||
Übertragung bei kleinsten Netzwerkschwankungen direkt von 1440p auf blockiges 360p, jetzt
|
||||
sanftere Abstufung über 720p.
|
||||
- **H.264 statt VP8** (2026-07-28) — nutzt wie VP8 klassisches Simulcast (kein SVC-Risiko wie
|
||||
bei VP9), zusätzlich auf vielen Geräten (v.a. iOS/Safari) hardwarebeschleunigt. Live
|
||||
verifiziert: 7 von 8 Video-Tracks liefen über H.264, 1 fiel sauber auf den VP8-Backup-Codec
|
||||
zurück (kein Ausfall). Deployed als `v0.2.3-elementcall-h264`.
|
||||
- Deployt als `rohana.axion1337.de/sorb/threadnet-web:v0.2.1-elementcall-noquotavp9` — nur
|
||||
der `/app/widgets/element-call/`-Ordner im bestehenden `v0.1.0`-Image ausgetauscht, da
|
||||
`ThreadNet-Web` einen vorbestehenden Build-Bug hat (siehe unten).
|
||||
- Config live prüfbar: `https://axion1337.chat/widgets/element-call/config.json`
|
||||
|
||||
**Update 2026-07-28 (Issue #12) — Full-Rebuild-Blocker behoben**: der oben beschriebene
|
||||
Patch-Workaround war nötig, weil `ThreadNet-Web` komplett neu gebaut nicht funktionierte.
|
||||
Drei Bugs gefixt: (1) 7 Skripte nicht ausführbar committet (644 statt 755, betraf auch die
|
||||
GitHub-Actions-Workflows des Forks), (2) `matrix-js-sdk#develop`-Pin auf einen veralteten
|
||||
Commit resolved (fehlte `src/oidc/authorize.ts`) — gepinnt auf `d19cb751` (letzter Commit
|
||||
vor dem Rename `src/oidc/` → `src/oauth/` mit geänderter API), (3) `package.json`/
|
||||
`webpack.config.ts` referenzierten noch upstream `@element-hq/element-call-embedded` statt
|
||||
unseren Fork. Mit echtem, vollständigem `docker build` aus frischem Klon verifiziert.
|
||||
Details: [[Element-Customization]] Wiki-Seite. Produktivumgebung bleibt beim Patch-Image.
|
||||
|
||||
**Update 2026-07-28 (später) — Video-Tab statt Developer-Mode**: Kamera-/Screen-Share-
|
||||
Qualitätseinstellungen (Auflösung, Framerate, Bitrate, Codec) waren im Upstream-PR #3736
|
||||
hinter einem "Developer Mode"-Schalter versteckt — in unseren Fork in den regulären
|
||||
"Video"-Settings-Tab verschoben, für alle Nutzer sichtbar. Deutsche Übersetzungen ergänzt
|
||||
(fehlten komplett). Codec-Dropdown auf die tatsächlich von der SFU akzeptierten Codecs
|
||||
beschränkt (VP8/H.264/H.265 — live per SFU-Logs verifiziert; VP9/AV1 wurden von der SFU
|
||||
ohnehin nur transparent auf VP8 zurückgefallen, boten aber keinen echten Effekt).
|
||||
|
||||
**Update 2026-07-29 — VP9/AV1 live getestet, zurückgerollt (Issue #11)**: SFU-seitige
|
||||
Codec-Freigabe (`matrixRTC.sfu.additional`) + Dropdown-Wiederfreischaltung getestet. Trotz
|
||||
echter Auswahl auf Safari und Desktop-Firefox (mit frischem Call-Rejoin) fiel VP9 immer
|
||||
automatisch auf VP8 zurück. SFU-Logs zeigten: die eigene Codec-Freigabe kam serverseitig nie
|
||||
in der aktiven `enabledPublishCodecs`-Liste an — Ursache nicht abschließend geklärt (möglicher
|
||||
Zusammenhang: `sfu.additional` ersetzt die Chart-eigene `config-overrides.yaml` im Config-Merge,
|
||||
statt sie zu ergänzen). Komplett zurückgerollt auf den bekannt funktionierenden 3-Codec-Stand.
|
||||
Details: Issue #11.
|
||||
|
||||
## Dateien
|
||||
|
||||
| Datei | Ort |
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Moderation Bot & Content Scanning
|
||||
|
||||
**Status**: ✅ Draupnir deployed (2026-07-29, Closes Issue #18) | ✅ Content Scanner deployed + live getestet (2026-07-29, Closes Issue #19)
|
||||
**Konfiguration**: `apps/production/draupnir*.yaml`, `apps/production/clamav*.yaml`, `apps/production/clamav_spam_checker.py`
|
||||
|
||||
## 1. Draupnir (Moderationsbot)
|
||||
|
||||
Community-Nachfolger von Mjolnir. Läuft als eigener Bot-Account (`@draupnir:axion1337.chat`),
|
||||
verwaltet Ban-Listen ("Policy Rooms") und setzt sie in geschützten Räumen durch.
|
||||
|
||||
### Warum Draupnir statt Mjolnir?
|
||||
|
||||
Mjolnir gilt als Vorgänger-Projekt und wird von der Community nicht mehr aktiv weiterentwickelt;
|
||||
Draupnir ist der aktive Fork mit denselben Kernfunktionen plus Erweiterungen (u.a. native
|
||||
Rust-Crypto-Unterstützung, siehe unten).
|
||||
|
||||
### Bot-Account & Zugriff (Bootstrap)
|
||||
|
||||
Da Authentifizierung über MAS läuft (kein klassisches `registration_shared_secret`), wird der
|
||||
Bot-Account über MAS' eigenes CLI-Tool angelegt:
|
||||
|
||||
```bash
|
||||
kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \
|
||||
mas-cli manage register-user draupnir --yes
|
||||
|
||||
kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \
|
||||
mas-cli manage issue-compatibility-token draupnir
|
||||
```
|
||||
|
||||
Der ausgegebene Token wird per `sops apps/production/draupnir-secret.yaml` manuell eingetragen
|
||||
(kein automatisierter Schritt - der Token darf nirgends unverschlüsselt landen).
|
||||
|
||||
### Wichtige Stolpersteine (live gefunden, nicht aus der Doku ableitbar)
|
||||
|
||||
- **Version**: `gnuxie/draupnir:v2.9.0` crasht beim ersten Start mit `initialManager`
|
||||
("Can't join remote room because no servers..."). Das automatische Anlegen des
|
||||
Management-Rooms über `initialManager` funktioniert erst **ab v3.1.0**. Aktuell deployt:
|
||||
`v3.1.0`.
|
||||
- **CLI-Argument statt Env-Var**: v3.x hat die automatische Config-Erkennung über
|
||||
`NODE_CONFIG_DIR` entfernt - der Container braucht jetzt explizit
|
||||
`args: ["bot", "--draupnir-config", "/data/config/default.yaml"]`, sonst
|
||||
`TypeError: No configuration path has been found for Draupnir.` (per Extraktion von
|
||||
`dist/config.js` aus dem Image bestätigt, nicht dokumentiert gefunden).
|
||||
- **NetworkPolicy**: Der Bot muss Synapse direkt anrufen können. Da `matrix-stack-synapse`
|
||||
intern über haproxy geroutet wird und `allow-ingress-haproxy` standardmäßig nur Traefik
|
||||
(`kube-system`) erlaubt, braucht Draupnir eine eigene `podSelector`-Ausnahme in
|
||||
`networkpolicy.yaml` - sonst schlägt jede Anfrage an den Homeserver silent fehl.
|
||||
|
||||
### Verschlüsselter Management-Room
|
||||
|
||||
Standardmäßig unverschlüsselt (Draupnirs zugrundeliegende Bot-Library aktiviert Crypto nicht
|
||||
automatisch). Für einen verschlüsselten Management-Room:
|
||||
|
||||
1. `experimentalRustCrypto: true` in der Config ergänzen (via `sops`) - vom Hersteller selbst
|
||||
als "not considered production safe" gekennzeichnet, in unserem Test aber ohne Fehler
|
||||
gelaufen (Pod stabil, kein Crash, `End-to-end encryption enabled` in den Logs).
|
||||
2. Verschlüsselung ist eine Raum-Eigenschaft, die beim Erstellen gesetzt wird - das Flag allein
|
||||
verschlüsselt einen bereits bestehenden Management-Room **nicht** rückwirkend. Dafür in
|
||||
Element: Raumeinstellungen → Sicherheit & Datenschutz → Verschlüsselung aktivieren.
|
||||
|
||||
### Profilbild setzen
|
||||
|
||||
Erfordert eine `mxc://`-URL (Bild muss zuerst hochgeladen werden, z.B. per Chat an den Bot
|
||||
senden, dann in Element per "View Source" die `mxc://`-URL kopieren):
|
||||
|
||||
```
|
||||
!draupnir avatar mxc://<server>/<media-id>
|
||||
```
|
||||
|
||||
### Befehle (Kurzreferenz)
|
||||
|
||||
Alle Befehle im (verschlüsselten) Management-Room, Präfix `!draupnir`:
|
||||
|
||||
| Befehl | Zweck |
|
||||
|--------|-------|
|
||||
| `status` | Bot-Status, beobachtete Listen, geschützte Räume |
|
||||
| `rooms add <room>` | Raum unter Draupnirs Schutz stellen (Voraussetzung für Bans!) |
|
||||
| `list create <shortcode> <alias>` | Neue Policy-Liste anlegen (wird automatisch beobachtet + geschützt) |
|
||||
| `watch <shortcode>` | Zusätzliche Policy-Liste beobachten |
|
||||
| `ban <user> <liste> <grund>` | **Wichtig**: 2. Argument ist die Policy-Liste, NICHT der Ziel-Raum! Der Ban gilt automatisch in allen Räumen, die diese Liste beobachten und geschützt sind |
|
||||
| `kick <user> <room> <grund>` | Direkter, sofortiger Kick aus einem konkreten Raum (ohne Listen-Umweg) |
|
||||
| `rules` | Zeigt die Regeln einer Policy-Liste an |
|
||||
| `unban <user> <liste>` | Regel wieder entfernen |
|
||||
|
||||
**Live getestet** (2026-07-29): Testraum geschützt, Policy-Liste angelegt, Testnutzer über
|
||||
`ban`+Liste erfolgreich aus dem geschützten Raum entfernt. Kernmechanismus bestätigt
|
||||
funktionsfähig.
|
||||
|
||||
## 2. Content Scanner (Issue #19)
|
||||
|
||||
**Verworfener erster Ansatz**: `matrix-content-scanner-python` ist ein Proxy, den der
|
||||
**Client** explizit statt der normalen Media-Endpunkte aufrufen muss - Synapse selbst leitet
|
||||
nichts automatisch dorthin um. Diese client-seitige Unterstützung existiert nur noch in
|
||||
veralteten, nicht mehr gepflegten Android/iOS-SDKs; weder aktuelles Element Web noch Element X
|
||||
unterstützen das (geprüft: kein `content_scanner`-Hook im offenen `element-x-android`-Repo).
|
||||
Element selbst hat echtes serverseitiges Scanning - aber nur in der kommerziellen
|
||||
**Element Pro** + **ESS Pro**-Kombination, nicht in unserer offenen ESS-Community-Installation.
|
||||
|
||||
**Tatsächlich umgesetzt**: ein eigenes, kleines Synapse-Modul (`clamav_spam_checker.py`),
|
||||
das Synapses echten, dokumentierten Hook `check_media_file_for_spam` nutzt - läuft
|
||||
**serverseitig**, transparent für jeden Client, ganz ohne Mitwirkung des Clients. Kein
|
||||
fertiges Modul dafür existiert (auch das verbreitete `synapse-http-antispam`-Brückenmodul
|
||||
schließt genau diesen Callback explizit aus), daher selbst geschrieben.
|
||||
|
||||
**Architektur**:
|
||||
- ClamAV (`clamav/clamav:1.5.3`) läuft als eigener Pod, PVC für die Signatur-Datenbank.
|
||||
- Das Modul (`apps/production/clamav_spam_checker.py`) wird per ConfigMap gemounted und über
|
||||
`PYTHONPATH` importierbar gemacht (`synapse.extraVolumes`/`extraVolumeMounts`/`extraEnv` -
|
||||
kein Custom-Synapse-Image nötig).
|
||||
- Spricht ClamAVs natives INSTREAM-Protokoll direkt über **Twisted**-Netzwerk-Primitives
|
||||
(`HostnameEndpoint`/`connectProtocol`), nicht über `asyncio` - Synapse läuft auf Twisteds
|
||||
Reactor, nicht auf einer laufenden asyncio-Event-Loop. Ein erster Versuch mit
|
||||
`asyncio.open_connection`/`wait_for` schlug live mit `RuntimeError: no running event loop`
|
||||
fehl und fiel dadurch (durch das eigene Fail-Open-Verhalten) unbemerkt auf "durchlassen"
|
||||
zurück - die EICAR-Testdatei wurde beim ersten Versuch nicht erkannt. Nach Umstellung auf
|
||||
Twisted-Primitives funktioniert es sauber.
|
||||
- **Fail-open** bei Scanner-Fehlern (Verbindungsfehler/Timeout → Datei wird durchgelassen,
|
||||
laut geloggt) - ein ClamAV-Ausfall soll nicht alle Uploads auf dem Homeserver blockieren.
|
||||
|
||||
**Live getestet und bestätigt** (2026-07-29):
|
||||
- Normale Datei in unverschlüsseltem Raum → läuft durch (kein Regressionsschaden).
|
||||
- EICAR-Testdatei in unverschlüsseltem Raum → zuverlässig blockiert
|
||||
(`ClamAV rejected an upload: Eicar-Test-Signature`, Client bekommt `400 Bad content` -
|
||||
Synapse gibt bewusst keine Begründung an den Client zurück, nur in den Server-Logs sichtbar).
|
||||
- EICAR-Testdatei in verschlüsseltem Raum/DM → **läuft durch** - erwartete, strukturelle
|
||||
Grenze: Synapse hat bei E2EE nie den Entschlüsselungsschlüssel, sieht nur Ciphertext. Nur
|
||||
ein kooperierender Client könnte das lösen (siehe oben, existiert nicht offen verfügbar).
|
||||
|
||||
**Bekannte Deckungslücke (Stand vor der Client-Erweiterung unten)**: schützt nur
|
||||
unverschlüsselte Räume/DMs - keine Warnung/Kennzeichnung für Nutzer in verschlüsselten
|
||||
Räumen, dass dort kein Scanning stattfindet. Folgeidee (Issue #43, LOW): Grafana-Dashboard
|
||||
über die bestehenden Loki-Logs, um Erkennungen/Scanner-Ausfälle sichtbar zu machen.
|
||||
|
||||
## 3. Client-seitiges Scanning für verschlüsselte Räume (Issue #19-Erweiterung, 2026-07-29)
|
||||
|
||||
Da Synapse bei E2EE-Räumen strukturell nie den Schlüssel hat, kann nur der **Client**
|
||||
Klartext scannen - einmal beim Senden (vor der Verschlüsselung), einmal beim Empfangen
|
||||
(nach der Entschlüsselung). Umgesetzt in `ThreadNet-Web` (Fork von Element Web).
|
||||
|
||||
### Architektur
|
||||
|
||||
Ein neuer, eigener HTTP-Dienst (`apps/production/clamav-http-scanner.py`, eigenes Image via
|
||||
`clamav-http-scanner-Dockerfile`) macht denselben ClamAV-Pod für Browser-JS erreichbar
|
||||
(clamd spricht nur rohes TCP, das kann ein Browser nicht). Erreichbar unter
|
||||
`https://axion1337.chat/_scan`. Auth über Synapses eigenen
|
||||
`/_matrix/client/v3/account/whoami`-Endpunkt (kein eigenes Auth-System nötig) - verhindert,
|
||||
dass der Dienst zu einem offenen "teste dein Malware gegen unseren Virenscanner"-Orakel für
|
||||
das ganze Internet wird. Fail-open bei Scanner-Fehlern, wie beim Synapse-Modul.
|
||||
|
||||
**Zwei Patch-Stellen im `ThreadNet-Web`-Fork** (im Repo
|
||||
`rohana.axion1337.de/sorb/ThreadNet-Web.git`, nicht in diesem gitops-Repo):
|
||||
|
||||
- **Empfang**: `apps/web/src/utils/DecryptFile.ts`, Funktion `decryptFile()` - der einzige
|
||||
Punkt im ganzen Client, an dem entschlüsselte Klartext-Bytes für *jeden* Anhangstyp
|
||||
entstehen (Bild/Audio/Video/Datei laufen alle über `MediaEventHelper` hier durch). Scan
|
||||
direkt nach dem Entschlüsseln, vor der Rückgabe als `Blob`.
|
||||
- **Versand**: `apps/web/src/ContentMessages.ts`, Funktion `uploadFile()` - die eine
|
||||
gemeinsame Funktion für alle Anhangs-Uploads (Hauptdatei, generierte Thumbnails,
|
||||
Sprachnachrichten), unabhängig davon ob der Zielraum verschlüsselt ist. Scan direkt nach
|
||||
dem Einlesen der Datei, vor Verschlüsselung/Upload.
|
||||
- Gemeinsame Hilfsdatei: `apps/web/src/utils/ContentScanner.ts` (neue `scanContent()`-
|
||||
Funktion + `ContentScanRejectedError`), von beiden Stellen genutzt. Fehlertexte über die
|
||||
bereits bestehenden Error-Rendering-Pfade in `MImageBody.tsx`/`MAudioBody.tsx`/
|
||||
`VideoBodyViewModel.ts`/`FileBodyViewModel.ts` (gleiches Muster wie die schon vorhandenen
|
||||
`DecryptError`/`DownloadError`).
|
||||
|
||||
**Live getestet** (2026-07-29):
|
||||
- EICAR in verschlüsseltem Gruppenraum ("testgruppe") und in 1:1-DMs zwischen zwei echten
|
||||
Accounts - in beiden Fällen zuverlässig **vor dem Upload** blockiert. Vorher (nur
|
||||
Synapse-Modul) lief das durch.
|
||||
- Empfangsseite unabhängig vom Absender bestätigt: EICAR über einen echten, ungepatchten
|
||||
Client (app.element.io) in denselben verschlüsselten Raum geschickt (simuliert einen
|
||||
fremden/föderierten Absender ohne unseren Patch) - beim Download-/Anzeigeversuch im
|
||||
gepatchten `ThreadNet-Web`-Client greift der Scanner zuverlässig. Beweist, dass der
|
||||
Empfangs-Hook unabhängig vom sendenden Client funktioniert, nicht nur als Selbstschutz
|
||||
für eigene Uploads.
|
||||
|
||||
### ⚠️ Wichtig für Desktop-/Electron-Builds (korrigiert, siehe Issue #44)
|
||||
|
||||
**Dieser Fix ist im Web-Client (Browser, das laufende `threadnet-web`-Container-Image)
|
||||
bestätigt live wirksam. Ob er auch im Electron-Client wirkt, hängt am tatsächlichen
|
||||
Build-Prozess - und der ist aktuell nicht automatisiert.**
|
||||
|
||||
Element Desktop (`apps/desktop` im selben Monorepo) baut die Web-App nicht selbst, sondern
|
||||
packt ein fertiges `webapp`-Verzeichnis in ein `webapp.asar`. *Woher* dieses Verzeichnis
|
||||
kommt, hängt vom Aufrufer ab:
|
||||
- **Standard-Fallback** (`pnpm run fetch <version>` ohne Artefakt): lädt ein offiziell von
|
||||
`element-hq/element-web` signiertes Release-Tarball herunter - **Upstream, ohne unsere
|
||||
Patches**.
|
||||
- **Mit eigenem Build** (`webapp-artifact`-Mechanismus in `build_desktop_prepare.yaml`,
|
||||
gedacht für CI): würde unseren eigenen `apps/web`-Output übernehmen, **inklusive** aller
|
||||
Fork-Anpassungen.
|
||||
|
||||
Der zweite Weg ist im Repo als GitHub-Actions-Pipeline (`build-and-test.yaml`) angelegt,
|
||||
läuft aber **nicht automatisch** - kein registrierter Runner, und der vorgelagerte Build-Job
|
||||
checkt zudem noch `element-hq/element-web` (Upstream) statt des eigenen Forks aus, ein Rest
|
||||
der ursprünglichen Upstream-CI. Die bereits existierende Desktop-Build (mit der
|
||||
Discord-Style-Raumliste) entstand nach aktuellem Stand aus einem **manuellen, lokalen**
|
||||
Build-Durchlauf, nicht aus einem reproduzierbaren, automatisierten Prozess.
|
||||
|
||||
**Konsequenz für heute**: die Scan-Patches sind im `ThreadNet-Web`-Fork-Code enthalten und
|
||||
würden in jedem zukünftigen (manuellen oder automatisierten) Desktop-Build aus diesem Fork
|
||||
mitkommen - sie sind aber **nicht automatisch** in einer bereits existierenden
|
||||
Desktop-Installation gelandet, ohne dass jemand den Build-Vorgang erneut manuell durchführt.
|
||||
|
||||
Neues Backlog-Item dafür angelegt:
|
||||
[Issue #44](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/44) - Build-Job
|
||||
auf den eigenen Fork umstellen + funktionierenden Runner aufsetzen, damit Fork-Änderungen
|
||||
zuverlässig und automatisch auch im Desktop-Client landen.
|
||||
|
||||
**Element X (Mobile, iOS/Android)** ist davon komplett unberührt - eigene Codebasis auf
|
||||
Basis von `matrix-rust-sdk`, kein gemeinsamer Code mit `ThreadNet-Web`. Ein Schutz dort
|
||||
wäre ein separates, eigenständiges Projekt.
|
||||
@@ -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
|
||||
@@ -9,10 +9,12 @@ Die Implementierungen wurden in dieser Reihenfolge durchgeführt. Für neue Setu
|
||||
| # | Titel | Datei | Status | Zieldomäne |
|
||||
|---|-------|-------|--------|-----------|
|
||||
| 1 | TURN Server für WebRTC Video-Calls | `01-turn-server-setup.md` | ✅ Deployed | `turn.axion1337.chat` |
|
||||
| 2 | Authentik als Identity Provider | `02-authentik-identity-provider.md` | ✅ Stage 1 Deployed | `auth.axion1337.chat` |
|
||||
| 2 | Authentik als Identity Provider | `02-authentik-identity-provider.md` | ✅ Deployed | `auth.axion1337.chat` |
|
||||
| 3 | Monitoring mit Alloy/Prometheus/Loki | `03-monitoring-integration.md` | ✅ Deployed | lokal (10.0.0.3) |
|
||||
| 4 | Element Web Anpassung & Desktop-Apps | `04-element-customization.md` | ✅ Deployed | `axion1337.chat` |
|
||||
| 5 | Room Policies (Retention, Publication, Auto-Join) | `05-room-policies.md` | ✅ Deployed | Matrix Synapse |
|
||||
| 6 | Moderationsbot (Draupnir) & Content Scanning | `06-moderation-content-scanning.md` | ✅ Deployed | Matrix Synapse |
|
||||
| 7 | Host-Wartungsbenachrichtigungen (unattended-upgrades) | `07-host-maintenance-notifications.md` | ✅ Deployed | Host-Ebene (kein K8s) |
|
||||
|
||||
---
|
||||
|
||||
@@ -85,6 +87,15 @@ Custom Themes, Desktop-Setup-Scripts, Element Admin.
|
||||
### [05-room-policies.md](05-room-policies.md)
|
||||
Message Retention, Room Publication, Auto-Join Policies.
|
||||
|
||||
### [06-moderation-content-scanning.md](06-moderation-content-scanning.md)
|
||||
Draupnir Moderationsbot (Bans, Policy-Listen), Content Scanner via eigenes Synapse-Modul für
|
||||
unverschlüsselte Räume UND client-seitiges Scanning für verschlüsselte Räume/DMs (Issue #19 +
|
||||
Erweiterung) - inkl. Electron/Desktop-Deckungslücke (Issue #44). Beide live getestet.
|
||||
|
||||
### [07-host-maintenance-notifications.md](07-host-maintenance-notifications.md)
|
||||
Erster nicht-GitOps-verwalteter Mechanismus im Repo: systemd-Timer auf dem nackten Host meldet
|
||||
per Mail + Matrix-Thread-Reply anstehende `unattended-upgrades`, bevor sie laufen (Issue #24).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Wartung & Troubleshooting
|
||||
|
||||
+56
-1
@@ -75,4 +75,59 @@ flux get helmreleases -n matrix --watch
|
||||
# Zeigt, wie die Pods hochfahren:
|
||||
kubectl get pods -n matrix -w
|
||||
```
|
||||
Sobald alle Pods auf `Running` stehen und die Zertifikate über Let's Encrypt validiert wurden (`kubectl get certificate -n matrix`), ist dein Matrix-Stack unter `https://axion1337.chat` erreichbar.
|
||||
Sobald alle Pods auf `Running` stehen und die Zertifikate über Let's Encrypt validiert wurden (`kubectl get certificate -n matrix`), ist dein Matrix-Stack unter `https://axion1337.chat` erreichbar.
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Recovery: lokalen age-Key wiederherstellen (Server läuft bereits)
|
||||
|
||||
Anders als Schritt 2 oben (neuen Key **erzeugen**) — falls der Server bereits läuft und nur der
|
||||
lokale Rechner den age-Key verloren hat (z.B. nach einer Neuinstallation), lässt sich der
|
||||
**bestehende** Private Key direkt aus dem Cluster zurückholen, ohne einen neuen zu generieren
|
||||
(das würde `.sops.yaml` und alle bereits verschlüsselten Secrets ungültig machen):
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.age
|
||||
kubectl get secret sops-age -n flux-system -o jsonpath='{.data.age\.agekey}' | base64 -d > ~/.age/keys.txt
|
||||
chmod 600 ~/.age/keys.txt
|
||||
|
||||
# Public Key zur Kontrolle gegen .sops.yaml abgleichen:
|
||||
grep 'public key:' ~/.age/keys.txt
|
||||
grep 'age:' .sops.yaml
|
||||
```
|
||||
|
||||
Voraussetzung: laufender Kubeconfig-Zugriff auf den Cluster (siehe Schritt 1 oben — auch das
|
||||
ist reines Zurückkopieren, kein Neu-Erzeugen).
|
||||
|
||||
**Bekannte Schwachstelle**: Dieser Key existiert aktuell nur an zwei Orten — im
|
||||
`sops-age`-Secret selbst (auf demselben Server) und lokal bei wem auch immer ihn zuletzt
|
||||
zurückgeholt hat. Es gibt kein separates, offsite Backup. Fällt der Server komplett aus
|
||||
(nicht nur der lokale Rechner), sind alle SOPS-verschlüsselten Secrets im Repo unlesbar.
|
||||
Siehe Issue-Backlog für die Entscheidung, ob/wie das abgesichert wird.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Node-Konfiguration: `/etc/hosts`-Eintrag für den Gitea-Host
|
||||
|
||||
Der K3s-Node und der Gitea-Host (`rohana.axion1337.de`, Container-Registry + Git-Remote)
|
||||
teilen sich ein privates Hetzner-Netzwerk (Node `10.0.0.2`, Gitea-Host `10.0.0.3`). Seit
|
||||
2026-07-28 hat der Node dafür einen manuellen `/etc/hosts`-Eintrag:
|
||||
|
||||
```
|
||||
10.0.0.3 rohana.axion1337.de
|
||||
```
|
||||
|
||||
**Warum**: eine Firewall-Fehlkonfiguration hatte den Node zeitweise komplett von
|
||||
`rohana.axion1337.de` über die öffentliche IP abgeschnitten, was Image-Pulls (z.B. für
|
||||
Custom-Images wie `sorb/axion-backup`) mit Timeout scheitern ließ. Der Eintrag macht
|
||||
Image-Pulls unabhängig vom Zustand der öffentlichen Firewall.
|
||||
|
||||
**Wichtig**: Das ist unmanaged Node-Konfiguration (kein GitOps, kein Kubernetes-Objekt) —
|
||||
überlebt einen Node-Neuaufbau **nicht** und muss dann erneut gesetzt werden:
|
||||
|
||||
```bash
|
||||
echo "10.0.0.3 rohana.axion1337.de" | sudo tee -a /etc/hosts
|
||||
```
|
||||
|
||||
Ein sauberer, clusterweiter Ersatz (z.B. CoreDNS-Rewrite, damit auch Pods selbst intern
|
||||
auflösen) ist als Issue #41 nachgehalten.
|
||||
Regular → Executable
@@ -85,8 +85,8 @@ Dieser Ordner enthält detaillierte Troubleshooting- und Reparaturanleitungen f
|
||||
| Problem | Nutzer | Guide | Status |
|
||||
|---------|--------|-------|--------|
|
||||
| Nur Standard Enrollment funktioniert | akadmin ✅ | - | Resolved |
|
||||
| User nur in Authentik, nicht in Synapse | Boje | `DIAGNOSTIK-AUTHENTIK-FLOW.md` | In Progress |
|
||||
| Einladungslink-Fehler: "kein ausstehender benutzer" | Klaus | `AUTHENTIK-CREATE-INVITATION-FLOW.md` | In Progress |
|
||||
| User nur in Authentik, nicht in Synapse | Boje | `DIAGNOSTIK-AUTHENTIK-FLOW.md` | **Resolved (2026-07-27)** — identischer Root Cause wie bei Klaus (fehlende Write/Password/Login-Stages im `matrix-invitation`-Flow), behoben durch denselben Issue-#7-Fix. Nicht erneut mit Boje selbst nachgetestet, aber mit anderen Test-Usern (`clark`, `lucky`) end-to-end verifiziert - der zugrundeliegende Flow ist jetzt für jeden Nutzer korrekt. |
|
||||
| Einladungslink-Fehler: "kein ausstehender benutzer" | Klaus | `AUTHENTIK-CREATE-INVITATION-FLOW.md` | **Fixed (2026-07-27)** — `matrix-invitation` Flow hatte nur Invite+Prompt Stage-Bindings, beide auf `order=0`. Write/Password/Login-Stages fehlten komplett. Live gefixt + als Blueprint (`apps/authentik/authentik-blueprints.yaml`) reproduzierbar gemacht. |
|
||||
| OIDC-Integration unklar | General | `AUTHENTIK-FIX-TEMPLATE.md` | Reference |
|
||||
|
||||
---
|
||||
@@ -108,5 +108,5 @@ Dieser Ordner enthält detaillierte Troubleshooting- und Reparaturanleitungen f
|
||||
|
||||
---
|
||||
|
||||
**Zuletzt aktualisiert**: 2026-05-18
|
||||
**Zuletzt aktualisiert**: 2026-07-30
|
||||
**Verfasser**: Claude Code + Thore
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user