Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d87c432f89 | ||
|
|
de347343b3 | ||
|
|
b319f21133 | ||
|
|
1c3df5badb | ||
|
|
0f45266426 | ||
|
|
d809b3f695 | ||
|
|
1e65f5b3dd | ||
|
|
3935f359f9 | ||
|
|
e7469a36b8 | ||
|
|
8473a1f0e5 | ||
|
|
c3e42be0d1 | ||
|
|
e816d76f60 | ||
|
|
e055ec511d | ||
|
|
dfb88a375e | ||
|
|
ccf04608f5 | ||
|
|
14cf9318ea | ||
|
|
0d9cc03bfd | ||
|
|
55a06bc723 | ||
|
|
625d8fbb17 | ||
|
|
0c9ffe5324 | ||
|
|
450ad51c39 | ||
|
|
b46040cae2 | ||
|
|
4cd23e8990 | ||
|
|
25c5bb6286 | ||
|
|
83a14e1d67 | ||
|
|
82412cf43c | ||
|
|
afc4ad3f20 | ||
|
|
b4650dcef8 | ||
|
|
23533c7899 | ||
|
|
60aaf0e068 | ||
|
|
b61dfd968e | ||
|
|
5f54fbef8e | ||
|
|
2250969e4a | ||
|
|
9f8eed3193 | ||
|
|
56653ffd0c | ||
|
|
026c255a45 | ||
|
|
df72744d55 | ||
|
|
2154f7fe77 | ||
|
|
cc189d3e15 | ||
|
|
acf38107f0 | ||
|
|
dbc7dacf2a | ||
|
|
ece80a8153 | ||
|
|
32ca547be3 | ||
|
|
5d7301c4d2 | ||
|
|
63460e798c | ||
|
|
7164e4f61c | ||
|
|
c4e67244fe | ||
|
|
f034eabdd6 | ||
|
|
81d66c25a0 | ||
|
|
026b499d1d | ||
|
|
3844f42f1f | ||
|
|
3c6d9549fb | ||
|
|
64d60b4428 | ||
|
|
22da5a7910 | ||
|
|
e110918d5f | ||
|
|
ea01c0bc6d | ||
|
|
1744de074f | ||
|
|
3200865bbb | ||
|
|
27a5395e17 | ||
|
|
065b13086b | ||
|
|
cac2b88fb3 | ||
|
|
310bb65b98 | ||
|
|
a661ceda11 | ||
|
|
eb5442e7bf | ||
|
|
ef04d86bc4 | ||
|
|
151ef3c8d0 | ||
|
|
9d9a87b598 | ||
|
|
f6d2761025 | ||
|
|
f7412817c3 | ||
|
|
c341faf521 | ||
|
|
f0174decc1 | ||
|
|
3c6f3645b3 | ||
|
|
1b35766ad3 | ||
|
|
a81ea0dd2e | ||
|
|
f1d732afbe | ||
|
|
34442df75c | ||
|
|
e7f1b5d6f8 | ||
|
|
f5fe27336f | ||
|
|
191981aefe | ||
|
|
ebb1d4e2a7 | ||
|
|
7a1710c78d | ||
|
|
c1ee776070 | ||
|
|
f687b41441 | ||
|
|
d7aca3bd0c | ||
|
|
0f28acb626 | ||
|
|
712cd430bf | ||
|
|
0213b0fe58 | ||
|
|
b51a07b068 | ||
|
|
c7d4b6f376 | ||
|
|
42632d6b2a | ||
|
|
52573634df | ||
|
|
0a07da260f | ||
|
|
316178bd51 | ||
|
|
42da25d0a6 | ||
|
|
bb5778e121 | ||
|
|
b10b607d70 | ||
|
|
ac0afaf3d9 | ||
|
|
b15a9f578c | ||
|
|
cdecb006d4 | ||
|
|
ba9783fa08 | ||
|
|
c8fc91ff1d | ||
|
|
8331bb212d | ||
|
|
018a4b7d83 | ||
|
|
dfe7ff9fc0 | ||
|
|
d4f3fc4c97 | ||
|
|
af63755f1e | ||
|
|
44d3e7dae1 | ||
|
|
11f97b935f | ||
|
|
717638d12c | ||
|
|
3c26cf2504 | ||
|
|
60248bfd92 | ||
|
|
228807fcc8 | ||
|
|
6f761da1a4 | ||
|
|
8ad921519f | ||
|
|
b28ef2c97a | ||
|
|
5e46a24e7d | ||
|
|
5aca738f55 | ||
|
|
932443df63 | ||
|
|
376772078a | ||
|
|
100b46ec91 | ||
|
|
2803fb384c | ||
|
|
11a079db57 | ||
|
|
e094c4ab29 | ||
|
|
5ca8f00d58 | ||
|
|
7b19586179 | ||
|
|
4ca87a68c7 | ||
|
|
7ac584d27b | ||
|
|
9db1ee6f45 | ||
|
|
2e53387650 | ||
|
|
e659242990 | ||
|
|
bb51047e1d | ||
|
|
f58add72ae | ||
|
|
5e16719bfe | ||
|
|
7cd177f744 | ||
|
|
5c3a7b798a | ||
|
|
23c66c97b8 | ||
|
|
9fa88bda5c | ||
|
|
105194d450 | ||
|
|
2aed6e4932 | ||
|
|
174c58001b | ||
|
|
5f7fd8bc46 | ||
|
|
13385b68f2 | ||
|
|
ccf6856f42 | ||
|
|
eabde3747e | ||
|
|
2932ffffc3 | ||
|
|
0f7ff1a896 | ||
|
|
debf980d5f | ||
|
|
802e09cd9c | ||
|
|
d742c99ba5 | ||
|
|
ac7e65100d | ||
|
|
be95bd921c | ||
|
|
7795f89fc9 | ||
|
|
9fa947cc21 | ||
|
|
233b13f972 | ||
|
|
51ea51352b | ||
|
|
9607687e4e | ||
|
|
49005ed325 | ||
|
|
ba270bce71 | ||
|
|
2c5f77686d | ||
|
|
414ada956a | ||
|
|
4a3d7c70a6 | ||
|
|
6973a8dbc7 | ||
|
|
09aaf1b2b5 | ||
|
|
337dbe85ac | ||
|
|
e462980dac | ||
|
|
79db6a8e49 | ||
|
|
25d1742d61 | ||
|
|
736c39a413 | ||
|
|
22a0823b7e | ||
|
|
3054037480 | ||
|
|
8f1d39b7a8 | ||
|
|
edf224e450 | ||
|
|
b32920c48f | ||
|
|
cb2ffa6a08 | ||
|
|
7d352fbf20 | ||
|
|
1bb1bc9610 | ||
|
|
37aea0254b | ||
|
|
fad91b6a05 | ||
|
|
af73cf770b | ||
|
|
09e4225de5 | ||
|
|
c0cb864ca2 | ||
|
|
235306a840 | ||
|
|
e9b24a6d1f | ||
|
|
0274f9316c | ||
|
|
027f567c8b | ||
|
|
d2bcd90291 | ||
|
|
80714fe901 | ||
|
|
5bbb03bc52 | ||
|
|
af13688993 | ||
|
|
f70e77127e | ||
|
|
f658ce2980 | ||
|
|
b29c7516d4 | ||
|
|
c32f951716 | ||
|
|
cdfbf7de98 | ||
|
|
b1247b4720 | ||
|
|
6bcbe9cc9e | ||
|
|
b8da70b14b | ||
|
|
4cf6702f85 | ||
|
|
aee9a34369 | ||
|
|
e6ef29c1e9 | ||
|
|
857c3965ef | ||
|
|
d37a65f4fa | ||
|
|
e6abd0fb3f | ||
|
|
d3362180ea | ||
|
|
5f50a60071 | ||
|
|
174e9721ed | ||
|
|
5f14376bdb | ||
|
|
81a30e21b9 | ||
|
|
f81fda12d4 | ||
|
|
fe0165a509 | ||
|
|
dc17158fe2 | ||
|
|
0c81de057f | ||
|
|
aaa197fbb5 | ||
|
|
e0ed10598f | ||
|
|
d39ce5a3a0 | ||
|
|
68bf4e213c | ||
|
|
644dff9c48 | ||
|
|
8e0f244257 | ||
|
|
b000c1b10b | ||
|
|
24d0cd1201 | ||
|
|
08a38fdfbb | ||
|
|
eda648eaa7 | ||
|
|
575ac0112f | ||
|
|
878687f334 | ||
|
|
7b4def0fe7 | ||
|
|
7b7643cf6e | ||
|
|
a7959474c5 | ||
|
|
3c2a03e9c3 | ||
|
|
8ff438bd24 | ||
|
|
01ad3f2d71 | ||
|
|
34bffe40be | ||
|
|
1a1c8ee2c6 | ||
|
|
78454ba2ef | ||
|
|
c98997f75e | ||
|
|
a323c6bb65 | ||
|
|
47625c05b6 | ||
|
|
42483f32b5 | ||
|
|
ee6cdb7982 | ||
|
|
4a95350c72 | ||
|
|
c85dd39eba | ||
|
|
bdba4f3e34 | ||
|
|
80fa13b63f | ||
|
|
cf8bc00613 | ||
|
|
0c67a249fa | ||
|
|
4bc0e10631 | ||
|
|
3e07d6c613 | ||
|
|
336f2aa1cb | ||
|
|
659acdf601 | ||
|
|
147ac079f7 | ||
|
|
0efe91b1bd | ||
|
|
6aff1f86d6 | ||
|
|
bdca6d8840 | ||
|
|
b6e2c6f00b | ||
|
|
37149b195b | ||
|
|
13e597769c | ||
|
|
c17e8f528f | ||
|
|
9fdac55e25 | ||
|
|
eb21dc9494 |
@@ -0,0 +1,74 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install base tools
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
lsb-release \
|
||||
apt-transport-https \
|
||||
vim \
|
||||
nano \
|
||||
jq \
|
||||
yq \
|
||||
zsh \
|
||||
sudo \
|
||||
openssh-client \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 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/*
|
||||
|
||||
# Install Helm
|
||||
RUN curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
|
||||
|
||||
# Install Flux CLI
|
||||
RUN curl -s https://fluxcd.io/install.sh | bash
|
||||
|
||||
# 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
|
||||
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=$(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
|
||||
# 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 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
|
||||
|
||||
WORKDIR /workspace
|
||||
ENTRYPOINT ["/usr/local/bin/docker-init.sh"]
|
||||
CMD ["/bin/zsh"]
|
||||
@@ -0,0 +1,241 @@
|
||||
# 🐳 DevContainer für ESS Community GitOps
|
||||
|
||||
Dieses DevContainer-Setup ermöglicht dir, auf **macOS, Windows und Linux** einheitlich zu entwickeln.
|
||||
|
||||
## 🚀 Schnelstart
|
||||
|
||||
### VSCode mit Remote Containers Extension
|
||||
|
||||
1. **VSCode Extension installieren:**
|
||||
- Öffne VSCode → Extensions → Suche nach `Dev Containers` (Microsoft)
|
||||
- Installiere sie
|
||||
|
||||
2. **GitOps Verzeichnis öffnen:**
|
||||
```bash
|
||||
cd "april mit Ansible/prod/gitops"
|
||||
code .
|
||||
```
|
||||
|
||||
3. **DevContainer starten:**
|
||||
- Klick auf `><` Symbol unten links in VSCode
|
||||
- Wähle `Reopen in Container`
|
||||
- Warte, bis das Image gebaut wurde (~3-5 Min beim ersten Mal)
|
||||
|
||||
### Alternative: Docker + CLI
|
||||
|
||||
```bash
|
||||
docker build -t ess-gitops .devcontainer
|
||||
docker run -it --rm \
|
||||
-v ~/.kube:/home/vscode/.kube \
|
||||
-v ~/.ssh:/home/vscode/.ssh \
|
||||
-v ~/.age:/home/vscode/.age \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
ess-gitops
|
||||
```
|
||||
|
||||
## 📦 Enthaltene Tools
|
||||
|
||||
- **kubectl** - Kubernetes CLI
|
||||
- **flux** - FluxCD GitOps Controller CLI
|
||||
- **helm** - Kubernetes Package Manager
|
||||
- **sops** - Secret Operations (Verschlüsselung)
|
||||
- **age** - Modern File Encryption
|
||||
- **docker** - Container CLI (über Host-Socket)
|
||||
- **git** - Versionskontrolle
|
||||
- **jq/yq** - JSON/YAML Processing
|
||||
- **zsh + oh-my-zsh** - Shell mit Plugins
|
||||
|
||||
## 🔐 Wichtige Verzeichnis-Binds
|
||||
|
||||
Der Container mountet automatisch:
|
||||
|
||||
| Host | Container | Zweck |
|
||||
|------|-----------|-------|
|
||||
| `~/.kube` | `/home/vscode/.kube` | Kubernetes Config |
|
||||
| `~/.ssh` | `/home/vscode/.ssh` | SSH Keys |
|
||||
| `~/.age` | `/home/vscode/.age` | Age Encryption Keys |
|
||||
| `/var/run/docker.sock` | `/var/run/docker.sock` | Docker Daemon (für `docker` Befehle) |
|
||||
|
||||
## ⚙️ Kubeconfig Einrichten
|
||||
|
||||
1. **Host-Machine (z.B. macOS):**
|
||||
```bash
|
||||
# Stelle sicher, dass ~/.kube/config existiert und den richtigen Cluster enthält
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
2. **Im Container:**
|
||||
```bash
|
||||
kubectl get nodes # Sollte jetzt auch dein Cluster zeigen
|
||||
kubectl config current-context
|
||||
```
|
||||
|
||||
## 🔐 SOPS + Age Setup
|
||||
|
||||
Damit du Secrets bearbeiten kannst, brauchst du den privaten `age`-Key. Dieser ist in `.sops.yaml` konfiguriert.
|
||||
|
||||
### Schritt 1: Age-Key bereitstellen
|
||||
|
||||
```bash
|
||||
# Host-Machine: Key-Datei erstellen
|
||||
mkdir -p ~/.age
|
||||
# Füge deinen privaten Key ein (Format: "age-secret-key-...")
|
||||
echo "age-secret-key-xxx..." > ~/.age/keys.txt
|
||||
chmod 600 ~/.age/keys.txt
|
||||
```
|
||||
|
||||
### Schritt 2: Im Container konfigurieren
|
||||
|
||||
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-secret.yaml
|
||||
```
|
||||
|
||||
### Schritt 3: VSCode Integration (optional)
|
||||
|
||||
Um die Umgebungsvariable beim Start zu setzen, nutze die `.devcontainer/devcontainer.json`:
|
||||
|
||||
```json
|
||||
"remoteEnv": {
|
||||
"KUBECONFIG": "/home/vscode/.kube/config",
|
||||
"SOPS_AGE_KEY_FILE": "/home/vscode/.age/keys.txt"
|
||||
}
|
||||
```
|
||||
|
||||
### Wie es funktioniert
|
||||
|
||||
- `.sops.yaml` definiert, dass Secrets mit `age` verschlüsselt werden
|
||||
- Beim Öffnen mit `sops <datei>` wird die Datei entschlüsselt → du editierst den plaintext in deinem Editor
|
||||
- Beim Speichern wird alles wieder automatisch verschlüsselt
|
||||
- **Wichtig:** Niemals den plaintext-Buffer commiten!
|
||||
|
||||
## 📝 Nach Container-Start: Git Hooks Installieren
|
||||
|
||||
Wichtig für die ConfigMap Auto-Sync (verhindert Merge-Konflikte):
|
||||
|
||||
```bash
|
||||
./scripts/install-hooks.sh
|
||||
```
|
||||
|
||||
Mehr Details: `docs/ops-configmap-sync.md`
|
||||
|
||||
## 📝 Nützliche Befehle
|
||||
|
||||
```bash
|
||||
# Status des Deployments
|
||||
kubectl get pods -n matrix
|
||||
flux get helmreleases -A
|
||||
|
||||
# Secrets bearbeiten (mit verschlüsselung)
|
||||
sops apps/production/custom-configs/mas-secret.yaml
|
||||
|
||||
# FluxCD Sync erzwingen
|
||||
flux reconcile kustomization production-apps --with-source
|
||||
|
||||
# Zertifikate debuggen
|
||||
kubectl get certificate -n matrix
|
||||
kubectl describe certificate matrix-ingress -n matrix
|
||||
|
||||
# HelmRelease Status prüfen
|
||||
flux describe helmrelease matrix-stack -n matrix
|
||||
```
|
||||
|
||||
## 🛠️ Anpassungen für Windows/WSL2
|
||||
|
||||
Falls du Windows nutzt:
|
||||
|
||||
1. **Docker Desktop installieren** (mit WSL2 Backend)
|
||||
2. **VSCode mit WSL Extension öffnen**
|
||||
3. **Im WSL Terminal:**
|
||||
```bash
|
||||
cd /mnt/c/path/to/projekt
|
||||
code .
|
||||
```
|
||||
4. Dann `Dev Containers: Reopen in Container`
|
||||
|
||||
Das funktioniert seamless, weil Docker Desktop unter WSL2 läuft.
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Problem: `SOPS_AGE_KEY_FILE not found`
|
||||
**Lösung:** Key muss in `~/.age/keys.txt` auf der Host-Machine sein:
|
||||
```bash
|
||||
# Host
|
||||
mkdir -p ~/.age
|
||||
echo "your-age-private-key" > ~/.age/keys.txt
|
||||
```
|
||||
Der Container mountet `~/.age` automatisch → sollte dann funktionieren.
|
||||
|
||||
### Problem: `kubectl: connection refused`
|
||||
**Lösung:** `~/.kube/config` muss auf Host vorhanden sein:
|
||||
```bash
|
||||
# Host
|
||||
kubectl get nodes # Test, ob Zugriff existiert
|
||||
# Dann Container neustarten
|
||||
```
|
||||
|
||||
### Problem: `HelmChart is not ready: stat ... no such file or directory`
|
||||
Siehe `README.md` → **Issue 1**. Kontrolliere:
|
||||
- `HelmRepository` nutzt `type: oci`
|
||||
- URL ist `oci://ghcr.io/element-hq/ess-helm`
|
||||
|
||||
### Problem: `values don't meet the specifications of the schema`
|
||||
Siehe `README.md` → **Issue 2**. Häufige Fehler:
|
||||
- `tls:` darf nicht im `ingress:` Block sein
|
||||
- `serverName` muss auf Root-Ebene der `values` stehen
|
||||
- Komponenten-Namen: `camelCase` (z.B. `elementWeb`, `matrixAuthenticationService`)
|
||||
|
||||
### Problem: Let's Encrypt `403 Order's status is processing`
|
||||
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)
|
||||
- [FluxCD Dokumentation](https://fluxcd.io)
|
||||
- [SOPS Anleitung](https://github.com/getsops/sops)
|
||||
- **Projekt-README:** `README.md` (Architektur, Issues, Best Practices)
|
||||
- **Setup-Docs:** `docs/setup/`
|
||||
- **Install-Guide:** `docs/install.md`
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"name": "ESS Community GitOps",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "."
|
||||
},
|
||||
"mounts": [
|
||||
"source=${localEnv:HOME}/.kube,target=/home/vscode/.kube,type=bind,consistency=cached",
|
||||
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
|
||||
"source=${localEnv:HOME}/.age,target=/home/vscode/.age,type=bind,consistency=cached",
|
||||
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
|
||||
],
|
||||
"remoteUser": "vscode",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/git:1": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||
},
|
||||
"remoteEnv": {
|
||||
"KUBECONFIG": "/home/vscode/.kube/config",
|
||||
"SOPS_AGE_KEY_FILE": "/home/vscode/.age/keys.txt"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"ms-kubernetes-tools.vscode-kubernetes-tools",
|
||||
"redhat.vscode-yaml",
|
||||
"redhat.vscode-commons",
|
||||
"monokai.theme-monokai-pro-vscode",
|
||||
"eamodio.gitlens",
|
||||
"gruntfuggly.todo-tree",
|
||||
"ms-vscode.makefile-tools",
|
||||
"GitHub.copilot"
|
||||
],
|
||||
"settings": {
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.tabSize": 2
|
||||
},
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/kustomization.json": "**/kustomization.yaml",
|
||||
"https://json.schemastore.org/helmrelease.json": "**/*helmrelease*.yaml"
|
||||
},
|
||||
"editor.theme": "Monokai Pro",
|
||||
"todo-tree.general.showActivityBarBadge": true,
|
||||
"todo-tree.general.tags": [
|
||||
"TODO",
|
||||
"FIXME",
|
||||
"BUG",
|
||||
"HACK",
|
||||
"NOTE",
|
||||
"XXX",
|
||||
"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",
|
||||
"**/docs/deployment-guides/*.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash .devcontainer/postCreateCommand.sh",
|
||||
"forwardPorts": []
|
||||
}
|
||||
@@ -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 "$@"
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Setting up ESS Community GitOps devcontainer..."
|
||||
|
||||
# Verify all required tools are installed
|
||||
echo "✅ Verifying installed tools..."
|
||||
commands=("kubectl" "flux" "helm" "sops" "age" "git" "docker")
|
||||
|
||||
for cmd in "${commands[@]}"; do
|
||||
if command -v $cmd &> /dev/null; then
|
||||
version=$($cmd version 2>/dev/null | head -1 || echo "installed")
|
||||
echo " ✓ $cmd: $version"
|
||||
else
|
||||
echo " ✗ $cmd: NOT FOUND"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Create necessary directories
|
||||
echo "📁 Creating home directories..."
|
||||
mkdir -p ~/.kube ~/.ssh ~/.age
|
||||
|
||||
# Print useful information
|
||||
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-secret.yaml (edit secrets)"
|
||||
echo ""
|
||||
echo "🔗 For kubeconfig setup:"
|
||||
echo " - Copy your ~/.kube/config to access the cluster"
|
||||
echo " - Run: kubectl get nodes"
|
||||
echo ""
|
||||
echo "✨ Devcontainer setup complete!"
|
||||
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
.claude/
|
||||
@@ -0,0 +1,224 @@
|
||||
# Leichter Verifikations-Job, portiert aus .gitea/workflows/deploy-on-push.yml
|
||||
# (Gitea-CI-Rueckbau, CFGMON-11, erledigt 2026-08-01). Deployt nichts - Flux reconciled
|
||||
# weiterhin selbststaendig aus dem Gitea-Mirror. Repo-Topologie: git.lab ist
|
||||
# kanonisch, rohana/Gitea ist Push-Mirror und Flux-Quelle.
|
||||
|
||||
# Ohne workflow-Block legt GitLab auch dann eine Pipeline an, wenn KEIN Job auf sie
|
||||
# passt - und fuehrt sie als "failed". Das ist rot ohne Fehler, und rot ohne Fehler
|
||||
# gewoehnt einem das Hinsehen ab (management #0104). Real passiert am 2026-08-19,
|
||||
# Pipeline 518. Diese Regeln verhindern die leere Pipeline, statt sie hinterher zu
|
||||
# quittieren.
|
||||
#
|
||||
# Reihenfolge zaehlt: schedule zuerst, sonst faellt die taegliche Kanonisierung mit
|
||||
# heraus - sie aendert keine Pfade und wuerde von der changes-Regel nie erfasst.
|
||||
.pfade_mit_pipeline: &pfade_mit_pipeline
|
||||
- apps/**/*
|
||||
- clusters/**/*
|
||||
- .gitlab-ci.yml
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
- if: $CI_PIPELINE_SOURCE == "web"
|
||||
- if: $CI_PIPELINE_SOURCE == "api"
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
changes: *pfade_mit_pipeline
|
||||
- when: never
|
||||
|
||||
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)."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TURN-Rotation kanonisieren (laeuft NUR als Pipeline-Schedule)
|
||||
#
|
||||
# Der Rotations-CronJob laeuft im Cluster und erreicht git.lab nicht - er pusht
|
||||
# seinen Branch deshalb nach Gitea. Von dort muss die Rotation ueber git.lab
|
||||
# zurueck, sonst ueberschreibt der naechste Mirror-Push sie und Flux spielt still
|
||||
# das ALTE Shared Secret wieder ein. Frueher war das ein monatlicher Handgriff;
|
||||
# dieser Job erledigt ihn.
|
||||
#
|
||||
# Einrichtung:
|
||||
# 1. OFFEN: Project Access Token, Rolle Maintainer, Scope write_repository
|
||||
# -> CI/CD-Variable CANONIZE_TOKEN (masked + protected). Nur sorb legt das
|
||||
# an; ohne das Token laeuft der Leerlauf gruen durch, aber die erste
|
||||
# echte Rotation bricht ab. Siehe gitops#51.
|
||||
# 2. ERLEDIGT: Schedule "TURN-Rotation kanonisieren (taeglich)", 17:05 Europe/
|
||||
# Berlin, angelegt 2026-08-02. Probelaeufe: Pipeline 159 deckte die
|
||||
# fehlende Lab-CA auf, 161 lief gruen durch ("Keine offene Rotation").
|
||||
#
|
||||
# Warum taeglich statt monatlich zum Rotationstermin: ein monatlicher Schedule
|
||||
# hat genau einen Versuch. Faellt der Runner an dem Tag aus, faellt die
|
||||
# Kanonisierung ein Monat lang aus. Taeglich holt der Job jede offene Rotation
|
||||
# spaetestens am Folgetag nach und ist ansonsten in Sekunden fertig.
|
||||
# ---------------------------------------------------------------------------
|
||||
canonize_rotation:
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
variables:
|
||||
GIT_DEPTH: "0" # volle Historie: wir brauchen merge-base
|
||||
script:
|
||||
- apk add --no-cache git ca-certificates >/dev/null
|
||||
- |
|
||||
# Lab-CA. Der Runner stellt sie nur seinem eigenen Klon-Schritt bereit, nicht
|
||||
# dem Job-Script - ein 'git fetch origin' im Script scheiterte deshalb an
|
||||
# "unable to get local issuer certificate" (live gesehen, Pipeline 159).
|
||||
# Deshalb liegt die Kette hier im Repo; sie ist ein oeffentliches Zertifikat,
|
||||
# kein Geheimnis. CI_SERVER_TLS_CA_FILE zuerst, falls der Runner sie doch liefert.
|
||||
if [ -n "$CI_SERVER_TLS_CA_FILE" ] && [ -f "$CI_SERVER_TLS_CA_FILE" ]; then
|
||||
export GIT_SSL_CAINFO="$CI_SERVER_TLS_CA_FILE"
|
||||
else
|
||||
export GIT_SSL_CAINFO="$CI_PROJECT_DIR/ci/lab-ca-chain.crt"
|
||||
fi
|
||||
echo "CA: $GIT_SSL_CAINFO"
|
||||
test -f "$GIT_SSL_CAINFO" || { echo "ABBRUCH: CA-Datei fehlt."; exit 1; }
|
||||
- git config --global user.email "ci@axion1337.chat"
|
||||
- git config --global user.name "TURN-Rotation (automatische Kanonisierung)"
|
||||
- git remote add gitea https://rohana.axion1337.de/sorb/axion1337.chat-gitops.git
|
||||
# --prune ist Pflicht, nicht Kosmetik: der Runner recycelt seinen Workspace
|
||||
# (GIT_STRATEGY=fetch), und ohne prune bleiben remote-tracking-Refs geloeschter
|
||||
# Zweige stehen. Der Job saehe einen laengst entfernten Rotationszweig sonst
|
||||
# dauerhaft weiter - live beobachtet am 2026-08-18, Pipeline 492: beide Remotes
|
||||
# waren nachweislich leer, der Job meldete den Zweig trotzdem.
|
||||
- git fetch --quiet --prune gitea
|
||||
# Kein 'fetch origin' noetig: der Runner hat main schon ausgecheckt (GIT_DEPTH 0),
|
||||
# CI_COMMIT_SHA ist der Stand von main beim Anlegen der Pipeline.
|
||||
- git checkout -B main "$CI_COMMIT_SHA"
|
||||
- |
|
||||
# Zeitpunkt der Rotation aus dem SOPS-Metadatenblock lesen. 'lastmodified'
|
||||
# ist Metadatum, kein Geheimnis - es steht im Klartext neben den ENC[]-Werten.
|
||||
rotationszeit() {
|
||||
git show "$1:apps/production/coturn-secret.yaml" 2>/dev/null \
|
||||
| sed -n 's/^[[:space:]]*lastmodified:[[:space:]]*"\(.*\)".*/\1/p' | head -1
|
||||
}
|
||||
MERGED=0
|
||||
UEBERHOLT=""
|
||||
for ref in $(git for-each-ref --format='%(refname:short)' 'refs/remotes/gitea/turn-secret-rotation-*'); do
|
||||
SHA=$(git rev-parse "$ref")
|
||||
if git merge-base --is-ancestor "$SHA" HEAD; then
|
||||
echo "bereits kanonisiert: $ref"
|
||||
continue
|
||||
fi
|
||||
# Ueberholte Zweige NICHT mergen. Ein Zweig, dessen Rotation aelter ist als
|
||||
# der Stand in main, bringt nichts Neues, konfliktiert aber mit allem, was
|
||||
# seither passiert ist - inklusive coturn-secret.yaml und
|
||||
# synapse-turn-secret.yaml. Eine unbedachte Hand-Aufloesung wuerde das
|
||||
# TURN-Shared-Secret ZURUECKDREHEN; Synapse und coturn waeren dann uneins
|
||||
# und TURN tot. Real passiert: der Zweig vom 2026-07-28 blieb nach dem Merge
|
||||
# auf Gitea liegen und hat diesen Job neun Tage lang taeglich rot gefaerbt -
|
||||
# womit die rote Pipeline als Alarm wertlos wurde (AGENTS.md: sie IST der
|
||||
# einzige Meldeweg). Deshalb ist ein Ueberbleibsel hier Aufraeumarbeit und
|
||||
# kein Vorfall: gemeldet ja, rot nein.
|
||||
NEU=$(rotationszeit "$SHA"); IST=$(rotationszeit HEAD)
|
||||
if [ -n "$NEU" ] && [ -n "$IST" ] \
|
||||
&& [ "$(printf '%s\n%s\n' "$NEU" "$IST" | sort | head -1)" = "$NEU" ]; then
|
||||
echo "ueberholt: $ref traegt die Rotation vom $NEU, main steht auf $IST."
|
||||
UEBERHOLT="$UEBERHOLT ${ref#gitea/}"
|
||||
continue
|
||||
fi
|
||||
echo "kanonisiere: $ref ($SHA)"
|
||||
SUBJ="chore(coturn): TURN-Rotation aus ${ref#gitea/} uebernommen"
|
||||
BODY="Automatisch kanonisiert: der Rotations-CronJob im Cluster erreicht git.lab nicht und pusht nach Gitea; dieser Commit bringt die Rotation auf den kanonischen Weg zurueck, bevor der Mirror sie ueberschreiben kann."
|
||||
if ! git merge --no-ff -m "$SUBJ" -m "$BODY" "$SHA"; then
|
||||
echo "MERGE-KONFLIKT in $ref - es wird nichts gepusht."
|
||||
echo "⚠️ Beim Aufloesen NIEMALS die aeltere Fassung von coturn-secret.yaml oder"
|
||||
echo " synapse-turn-secret.yaml uebernehmen: beide muessen dasselbe Shared"
|
||||
echo " Secret tragen, sonst faellt TURN aus. Im Zweifel main behalten."
|
||||
echo "Siehe CLAUDE.md (Abschnitt Repo Topology)."
|
||||
exit 1
|
||||
fi
|
||||
MERGED=1
|
||||
done
|
||||
if [ -n "$UEBERHOLT" ]; then
|
||||
echo
|
||||
echo "AUFRAEUMEN: diese Zweige sind ueberholt und koennen auf Gitea geloescht werden:"
|
||||
for b in $UEBERHOLT; do echo " - $b"; done
|
||||
echo "(Der Job bleibt gruen - liegengebliebene Zweige sind kein Vorfall.)"
|
||||
fi
|
||||
echo "MERGED=$MERGED" > .canonize_state
|
||||
- |
|
||||
. ./.canonize_state
|
||||
if [ "$MERGED" = "0" ]; then
|
||||
echo "Keine offene Rotation - nichts zu tun."
|
||||
exit 0
|
||||
fi
|
||||
# Token erst hier pruefen, nicht am Jobanfang: solange nichts zu pushen ist,
|
||||
# soll der taegliche Lauf gruen durchgehen und die Gitea-Strecke verifizieren.
|
||||
# Fehlt er, wenn es wirklich etwas zu tun gibt, ist Abbruch richtig.
|
||||
if [ -z "$CANONIZE_TOKEN" ]; then
|
||||
echo "ABBRUCH: Es gibt eine offene Rotation, aber CANONIZE_TOKEN ist nicht gesetzt."
|
||||
echo "Siehe Kopf dieser Datei, Einrichtungsschritt 1."
|
||||
exit 1
|
||||
fi
|
||||
# Sicherheitsnetz: die Rotation fasst genau diese Secrets an. Kaeme hier
|
||||
# etwas Unverschluesseltes durch, waere das Shared Secret im Klartext in Git.
|
||||
for f in apps/production/coturn-secret.yaml apps/production/synapse-turn-secret.yaml; do
|
||||
if ! grep -q "ENC\[" "$f"; then
|
||||
echo "ABBRUCH: $f ist nicht SOPS-verschluesselt - es wird nichts gepusht."
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: $f ist verschluesselt"
|
||||
done
|
||||
git push "https://oauth2:${CANONIZE_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:main
|
||||
echo "Gepusht. Der Mirror traegt es nach Gitea zurueck, der PR schliesst sich dort selbst;"
|
||||
echo "der Rotations-Branch existiert dann nur noch auf Gitea und wird vom Mirror entfernt."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wiki-Inhalt kanonisieren (laeuft NUR als Pipeline-Schedule, ADR-0015)
|
||||
#
|
||||
# Wiki.js laeuft im Cluster und erreicht git.lab nicht - es pusht seinen Inhalt
|
||||
# deshalb nach Gitea (sorb/ThreadNetWiki, oeffentlich lesbar). Dieser Job spiegelt
|
||||
# den Stand von dort nach git.lab axion1337.chat/threadnet-wiki, damit der
|
||||
# kanonische Speicher wieder im Lab liegt. Gleiche Umkehrung wie die TURN-Rotation.
|
||||
#
|
||||
# Einrichtung:
|
||||
# 1. ERLEDIGT: Project Access Token (Rolle Maintainer, Scope write_repository) auf
|
||||
# axion1337.chat/threadnet-wiki -> gitops-CI/CD-Variable WIKI_CANONIZE_TOKEN
|
||||
# (masked + protected).
|
||||
# 2. Schedule: laeuft im bestehenden Tages-Schedule mit (CI_PIPELINE_SOURCE ==
|
||||
# "schedule"). threadnet-wiki wurde leer angelegt -> der erste Push legt main an,
|
||||
# danach nur noch Fast-Forward (kein Force, Branch-Protection bleibt).
|
||||
canonize_wiki:
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "schedule"
|
||||
variables:
|
||||
GIT_DEPTH: "1" # nur fuer ci/lab-ca-chain.crt aus dem ausgecheckten Repo
|
||||
script:
|
||||
- apk add --no-cache git ca-certificates >/dev/null
|
||||
- |
|
||||
# Lab-CA fuer den git.lab-Push; der Gitea-Klon nutzt die System-CA (oeffentliches Zert).
|
||||
if [ -n "$CI_SERVER_TLS_CA_FILE" ] && [ -f "$CI_SERVER_TLS_CA_FILE" ]; then
|
||||
LABCA="$CI_SERVER_TLS_CA_FILE"
|
||||
else
|
||||
LABCA="$CI_PROJECT_DIR/ci/lab-ca-chain.crt"
|
||||
fi
|
||||
test -f "$LABCA" || { echo "ABBRUCH: Lab-CA-Datei fehlt."; exit 1; }
|
||||
if [ -z "$WIKI_CANONIZE_TOKEN" ]; then
|
||||
echo "ABBRUCH: WIKI_CANONIZE_TOKEN nicht gesetzt (siehe Kopf, Einrichtungsschritt 1)."
|
||||
exit 1
|
||||
fi
|
||||
- git config --global user.email "ci@axion1337.chat"
|
||||
- git config --global user.name "ThreadNet-Wiki Kanonisierung"
|
||||
- git clone --bare --quiet https://rohana.axion1337.de/sorb/ThreadNetWiki.git /tmp/wiki.git
|
||||
- GIT_SSL_CAINFO="$LABCA" git -C /tmp/wiki.git push "https://oauth2:${WIKI_CANONIZE_TOKEN}@${CI_SERVER_HOST}/axion1337.chat/threadnet-wiki.git" 'refs/heads/main:refs/heads/main'
|
||||
- echo "git.lab threadnet-wiki spiegelt jetzt Gitea sorb/ThreadNetWiki (main)."
|
||||
@@ -0,0 +1,12 @@
|
||||
# Flux source-controller's default ignore rules strip image files (*.png, *.jpg,
|
||||
# ...) from the build artifact. The platform-branding ConfigMap (configMapGenerator
|
||||
# in apps/production/kustomization.yaml) needs these branding assets in the build,
|
||||
# so re-include them. Negations are evaluated after the defaults.
|
||||
!apps/production/branding/logo.png
|
||||
!apps/production/branding/alpenglow.jpg
|
||||
!apps/production/branding/favicon.ico
|
||||
!apps/production/branding/favicons/favicon-32x32.png
|
||||
!apps/production/branding/favicons/favicon-16x16.png
|
||||
!apps/production/branding/favicons/android-chrome-192x192.png
|
||||
!apps/production/branding/favicons/apple-touch-icon.png
|
||||
!apps/production/branding/favicons/mstile-150x150.png
|
||||
@@ -0,0 +1,467 @@
|
||||
# AGENTS.md — axion1337.chat-gitops
|
||||
|
||||
> **Die Gruppenregeln sind kanonisch im `management`-Repo:**
|
||||
> [`AGENTS.md`](https://git.lab/axion1337.chat/management/-/blob/main/AGENTS.md)
|
||||
> — von außerhalb des Labs über den Gitea-Mirror lesbar:
|
||||
> `https://rohana.axion1337.de/sorb/management`. Dort stehen Repo-Topologie und
|
||||
> Mirror-Regeln, das Kanban-Framework (Status-Labels, WIP-Limit 2, ADR-Pflicht —
|
||||
> auch für dauerhafte Regel-Ausnahmen), Deploy-Übergabe und AAR-Verfahren,
|
||||
> Secrets-Handhabung und die Karpathy-Leitlinien. Sie gelten für **jede** Session
|
||||
> in diesem Repo. Hier steht nur, was für dieses Repository zusätzlich gilt.
|
||||
|
||||
## 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. The same rule applies to ThreadNet-Web, threadnet-call,
|
||||
thread-net-git, threadnet-operating and (since 2026-08-01) `management` (the former
|
||||
`Backlogs` repo, renamed in the PM-framework restructuring — ADRs/vision/roadmap live
|
||||
there). Since 2026-08-02 the wiki lives on git.lab too, so **no repo is authored on
|
||||
Gitea any more**; the one process that still *writes* there is the TURN rotation
|
||||
(see below).
|
||||
|
||||
**Why Gitea is the Flux source, and why that is not a leftover.** git.lab holds the
|
||||
blueprints; Gitea holds a copy the cluster can reach without the lab being up. That
|
||||
separation is deliberate: the Hetzner cluster must be buildable and re-deployable when
|
||||
the homelab is offline, on holiday, or mid-rebuild — it therefore must not depend on a
|
||||
host that only answers inside the lab. **Do not "fix" the Flux source to point at
|
||||
git.lab**: it would look tidier and would couple production availability to the lab,
|
||||
which is exactly what this split avoids.
|
||||
|
||||
**Issues live on git.lab** (migrated 2026-08-01, gitops#48): the old Gitea issues are
|
||||
closed with a pointer to their GitLab counterpart. ⚠️ gitops issue numbers **shifted**
|
||||
in the migration (Gitea had gaps from PRs; e.g. Gitea#48 → GitLab#46) — old references
|
||||
"gitops#N" in commits/docs mean the **Gitea** number; the authoritative mapping is the
|
||||
migration footer in each GitLab issue. Releases stay on Gitea (public download path).
|
||||
**No exceptions left**: deploy-handover issues used to stay on the Gitea
|
||||
`sorb/management` tracker because CFGMON had no route to git.lab. The site-to-site VPN
|
||||
(ADR-0004) removed that reason, and on 2026-08-02 they moved (LABNET-03) — the Gitea
|
||||
tracker is empty and the form now lives as a GitLab issue template in the management
|
||||
repo. Issues live on git.lab, without exception.
|
||||
|
||||
**Wiki** (migrated 2026-08-02): the 15 pages now live in this project's GitLab wiki
|
||||
(`…-gitops.wiki.git`, *Wiki* tab). ⚠️ The **`wiki` branch in this repo is NOT the wiki** —
|
||||
it is a stale May snapshot of `docs/`; don't edit or trust it.
|
||||
|
||||
⚠️ **`wiki.lab` is gone** (2026-08-15). The Docusaurus site that aggregated the doc
|
||||
sources was replaced by **Wiki.js inside this stack** (ADR-0014, `wiki.axion1337.chat`),
|
||||
which holds the operations and user handbooks and is edited in its own UI rather than
|
||||
aggregated read-only from source repos. `homelab/docs` is deliberately out of that
|
||||
scope — sorb's homelab documentation is not part of the platform.
|
||||
|
||||
**The one write that still lands on Gitea**: the monthly TURN-rotation CronJob runs in
|
||||
the cluster (no route to git.lab) and pushes its rotation branch to Gitea. Never merge
|
||||
that PR on Gitea — the rotation has to travel back through git.lab, otherwise the next
|
||||
mirror push overwrites it and Flux silently re-applies the *old* shared secret.
|
||||
|
||||
**This is automated — do not do it by hand.** The scheduled job `canonize_rotation` in
|
||||
`.gitlab-ci.yml` runs daily on git.lab, picks up any `turn-secret-rotation-*` branch
|
||||
from Gitea that is not yet in `main`, merges it, and pushes to git.lab; the mirror
|
||||
carries it back and Gitea auto-closes the PR. Once merged, the branch exists only on
|
||||
Gitea, so the next mirror run deletes it — no cleanup needed.
|
||||
|
||||
If the job fails (merge conflict, or the SOPS check finds an unencrypted secret file),
|
||||
it fails **loudly and changes nothing** — the pipeline stays red until someone looks.
|
||||
That red pipeline is the alarm; there is no separate reminder. Manual fallback, should
|
||||
it ever be needed: fetch the branch, merge into main locally, push to git.lab (worked
|
||||
example: 2026-08-01, commit `717638d`). If the mirror lags, force it via the GitLab API
|
||||
`POST /projects/<id>/remote_mirrors/<mirror_id>/sync`.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
gitops/
|
||||
├── .gitlab-ci.yml # Lab-CI verify job (YAML validation + SOPS-encryption check)
|
||||
├── 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 git.lab 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 [git.lab issues](https://git.lab/axion1337.chat/axion1337.chat-gitops/-/issues); the closed Gitea issues only point there)
|
||||
- **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)
|
||||
|
||||
---
|
||||
name: karpathy-guidelines
|
||||
description: Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Karpathy Guidelines
|
||||
|
||||
Behavioral guidelines to reduce common LLM coding mistakes, derived from [Andrej Karpathy's observations](https://x.com/karpathy/status/2015883857489522876) on LLM coding pitfalls.
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
## 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
Before implementing:
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
## 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
## 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
## 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" → "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" → "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] → verify: [check]
|
||||
2. [Step] → verify: [check]
|
||||
3. [Step] → verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
@@ -0,0 +1 @@
|
||||
Read AGENTS.md — the canonical instruction file for this repository. All rules live there.
|
||||
@@ -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
|
||||
|
||||
@@ -53,6 +58,12 @@ winget install Kubernetes.kubectl FluxCD.Flux Mozilla.sops age-encryption.age He
|
||||
|
||||
1. **Kubeconfig:** Stelle sicher, dass die Datei `~/.kube/config` mit den Zugangsdaten zu deinem K3s-Cluster gefüllt ist. Test: `kubectl get nodes`.
|
||||
2. **SOPS Key:** Du benötigst den privaten `age`-Key (oder GPG-Key), der in der `.sops.yaml` des Repositories hinterlegt ist, um Secrets bearbeiten zu können.
|
||||
3. **Git Hooks installieren:** Nach dem Klonen dieses Repositories müssen Git Hooks installiert werden, um ConfigMap-Änderungen automatisch zu tracken:
|
||||
```bash
|
||||
cd prod/gitops
|
||||
./scripts/install-hooks.sh
|
||||
```
|
||||
Siehe [📖 GitOps ConfigMap Auto-Sync](docs/ops-configmap-sync.md) für Details.
|
||||
|
||||
-----
|
||||
|
||||
@@ -64,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)
|
||||
|
||||
@@ -74,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
|
||||
@@ -100,9 +128,35 @@ 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 und Wiki liegen seit 2026-08-01/02 ebenfalls auf git.lab**
|
||||
([Issues](https://git.lab/axion1337.chat/axion1337.chat-gitops/-/issues) ·
|
||||
[Wiki](https://git.lab/axion1337.chat/axion1337.chat-gitops/-/wikis/home) — der
|
||||
Wiki-Reiter oben im Projekt). ⚠️ Die Issue-Nummern haben sich beim Umzug verschoben;
|
||||
ein alter Verweis „gitops#N" meint die Gitea-Nummer, verbindlich ist der
|
||||
Migrations-Fußtext im jeweiligen Issue. **Releases bleiben auf Gitea** (öffentlicher
|
||||
Download-Pfad). Der `wiki`-Branch in diesem Repo ist ein **überholter Abzug von
|
||||
`docs/` aus dem Mai** und nicht die gepflegte Fassung.
|
||||
|
||||
Alle Dokumentationsquellen zusammen (Plattform, Homelab, Arbeitsweise) gibt es unter
|
||||
**[wiki.lab](https://wiki.lab)** — Konfiguration im Repo
|
||||
[`homelab/wiki`](https://git.lab/homelab/wiki).
|
||||
|
||||
**Abhängigkeits-Logik:** Flux installiert erst `infra-apps` (damit Namespaces und Repositories existieren) und danach `production-apps` (das eigentliche ESS-Chart).
|
||||
|
||||
-----
|
||||
@@ -150,7 +204,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
|
||||
@@ -210,7 +267,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
|
||||
```
|
||||
|
||||
-----
|
||||
@@ -238,4 +295,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://git.lab/axion1337.chat/axion1337.chat-gitops/-/wikis/home)** (auf git.lab, Reiter *Wiki*): Ausführliche Historie, Incident-Notizen, Setup-Guides pro Komponente. Zusammen mit Homelab- und Verfahrensdoku auch unter **[wiki.lab](https://wiki.lab)**.
|
||||
* **`docs/deployment-guides/`**: Detaillierte Guides für TURN-Server, Authentik, Monitoring, Element-Customization, Room-Policies, Moderation & Content-Scanning, Host-Wartungsbenachrichtigungen.
|
||||
@@ -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,509 @@
|
||||
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.
|
||||
# Eindeutigkeit case-insensitiv pruefen (#0043). Authentiks eigene Pruefung
|
||||
# ist case-sensitiv, "Boje" und "boje" gelten ihr also als verschieden --
|
||||
# in Matrix kollidieren sie. Seit ADR-0011 faengt MAS das mit
|
||||
# on_conflict:fail ab, aber erst BEIM LOGIN: der Nutzer registriert sich
|
||||
# erfolgreich und scheitert spaeter ohne Erklaerung. Diese Policy gibt die
|
||||
# Rueckmeldung dort, wo der Fehler entsteht.
|
||||
#
|
||||
# ⚠️ Bewusst ohne Zugriff auf request.user: die Stage laeuft im ANONYMEN
|
||||
# Enrollment-Kontext. Genau daran sind die frueher hier haengenden
|
||||
# System-Policies gescheitert ('AnonymousUser' hat kein group_attributes).
|
||||
# Gelesen wird ausschliesslich prompt_data.
|
||||
- model: authentik_policies_expression.expressionpolicy
|
||||
state: present
|
||||
identifiers:
|
||||
name: matrix-username-eindeutig-ci
|
||||
id: username_unique_ci
|
||||
attrs:
|
||||
execution_logging: false
|
||||
expression: |
|
||||
from authentik.core.models import User
|
||||
|
||||
gewuenscht = (request.context.get("prompt_data") or {}).get("username") or ""
|
||||
gewuenscht = gewuenscht.strip()
|
||||
if not gewuenscht:
|
||||
return True # Pflichtfeld-Pruefung macht der Prompt selbst
|
||||
|
||||
if User.objects.filter(username__iexact=gewuenscht).exists():
|
||||
ak_message("Dieser Benutzername ist bereits vergeben - auch in anderer "
|
||||
"Gross-/Kleinschreibung. Bitte waehle einen anderen.")
|
||||
return False
|
||||
return True
|
||||
|
||||
- model: authentik_stages_prompt.promptstage
|
||||
state: present
|
||||
identifiers:
|
||||
name: matrix-invitation-prompt
|
||||
attrs:
|
||||
validation_policies:
|
||||
- !KeyOf username_unique_ci
|
||||
|
||||
# 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).
|
||||
# ⚠️ Hier steckten ZWEI Fehler uebereinander (gitops#60).
|
||||
#
|
||||
# 1. !KeyOf koppelt jede Bindung daran, dass DIESER Eintrag im selben Lauf
|
||||
# eine Model-Instanz erzeugt. Scheitert er, faellt der ganze Blueprint.
|
||||
# Schlimmer: beim Protokollieren des Fehlers ruft Authentik str() auf das
|
||||
# !KeyOf-Objekt, dessen __repr__ gegen ein LEERES Blueprint aufloest und
|
||||
# dabei dieselbe Ausnahme wirft - die echte Fehlermeldung wird dadurch
|
||||
# ueberdeckt. Man sieht nur noch "KeyOf: failed to find entry".
|
||||
# Deshalb jetzt !Find gegen die Datenbank, wie in den Blueprints darunter
|
||||
# ohnehin ueblich. Der Flow existiert, das ist stabiler und entkoppelt.
|
||||
#
|
||||
# 2. name und title MUESSEN gesetzt sein. Der FlowSerializer verlangt beide
|
||||
# (keine Model-Defaults). Fehlen sie, scheitert die Validierung, der Eintrag
|
||||
# bekommt keine Model-Instanz - und JEDES nachfolgende `!KeyOf
|
||||
# matrix_recovery_flow` laeuft ins Leere. Genau das war der Zustand bis
|
||||
# 2026-08-07: der Blueprint wurde bei jedem Lauf verworfen (gitops#60).
|
||||
# Gegenprobe im selben ConfigMap: matrix-invitation-flow setzt beide und
|
||||
# laeuft durch.
|
||||
#
|
||||
# Die Werte sind der Stand aus der Datenbank, damit sich an der angezeigten
|
||||
# Seite nichts aendert. (Der Tippfehler "mail-adress" ist so gewachsen und
|
||||
# bleibt bewusst stehen - Textaenderung waere eine eigene Entscheidung.)
|
||||
- model: authentik_flows.flow
|
||||
state: present
|
||||
identifiers:
|
||||
slug: matrix-recovery
|
||||
attrs:
|
||||
name: "Welcome to aXion1337! Please provide a username or mail-adress."
|
||||
title: "Welcome to aXion1337! Please provide a username or mail-adress."
|
||||
designation: recovery
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
|
||||
order: 10
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_identification.identificationstage, [name, default-recovery-identification]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
|
||||
order: 20
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_email.emailstage, [name, default-recovery-email]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
|
||||
order: 30
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_prompt.promptstage, [name, "Change your password"]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
|
||||
order: 40
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_user_write.userwritestage, [name, default-recovery-user-write]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
|
||||
order: 100
|
||||
attrs:
|
||||
stage: !Find [authentik_stages_user_login.userloginstage, [name, default-recovery-user-login]]
|
||||
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, matrix-recovery]]
|
||||
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.
|
||||
#
|
||||
# ⚠️ Hier reicht recovery_flow allein NICHT. Der Serializer validiert das
|
||||
# ganze Objekt, nicht nur die angegebenen Felder - ohne user_fields faellt
|
||||
# er mit "When no user fields are selected, at least one source must be
|
||||
# selected". Das war die eigentliche Ursache von gitops#60; sichtbar wurde
|
||||
# sie erst, nachdem die !KeyOf-Verweise weg waren (die haben die Meldung
|
||||
# ueberdeckt).
|
||||
#
|
||||
# Die uebrigen Felder stehen bewusst mit drin, obwohl sie den Model-Defaults
|
||||
# entsprechen: Was der Serializer nicht bekommt, setzt er auf den Default
|
||||
# zurueck. Ein Blueprint, der nur ein Feld nennt, kann so still andere
|
||||
# Einstellungen kippen. Werte sind der Stand aus der Datenbank.
|
||||
- model: authentik_stages_identification.identificationstage
|
||||
state: present
|
||||
identifiers:
|
||||
name: default-authentication-identification
|
||||
attrs:
|
||||
recovery_flow: !Find [authentik_flows.flow, [slug, matrix-recovery]]
|
||||
user_fields:
|
||||
- email
|
||||
- username
|
||||
case_insensitive_matching: true
|
||||
show_matched_user: true
|
||||
pretend_user_exists: true
|
||||
show_source_labels: false
|
||||
enable_remember_me: false
|
||||
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]]
|
||||
admin-mfa-enforcement.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: admin-mfa-enforcement
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
# MFA-Pflicht fuer Admins, ohne sie fuer alle anderen zu erzwingen.
|
||||
#
|
||||
# Warum eine ZWEITE Validate-Stage statt not_configured_action am
|
||||
# bestehenden default-authentication-mfa-validation umzustellen: dieses Feld
|
||||
# haengt an der Stage, nicht an der Bindung - eine Umstellung wuerde alle
|
||||
# Mitglieder treffen. Und die Standard-Stage anzufassen hiesse, ein Objekt
|
||||
# aus Authentiks eigenem Blueprint zu veraendern.
|
||||
#
|
||||
# Diese Loesung fasst KEIN Authentik-Standardobjekt an. Sie haengt eine
|
||||
# eigene Stage hinter die vorhandene und bindet sie an die Admin-Gruppe.
|
||||
- model: authentik_stages_authenticator_validate.authenticatorvalidatestage
|
||||
state: present
|
||||
identifiers:
|
||||
name: admin-mfa-validation
|
||||
id: admin_mfa_stage
|
||||
attrs:
|
||||
# configure statt deny: Wer keinen Faktor hat, wird beim Login durch die
|
||||
# Einrichtung GEFUEHRT. deny wuerde ihn aussperren - und bei einem
|
||||
# Admin-Konto gibt es dann keinen Weg zurueck ausser ueber den Cluster.
|
||||
not_configured_action: configure
|
||||
configuration_stages:
|
||||
- !Find [authentik_stages_authenticator_totp.authenticatortotpstage, [name, default-authenticator-totp-setup]]
|
||||
- !Find [authentik_stages_authenticator_webauthn.authenticatorwebauthnstage, [name, default-authenticator-webauthn-setup]]
|
||||
# Verhindert die doppelte Abfrage: Die Standard-Stage auf Ordnung 30
|
||||
# validiert bereits, wer einen Faktor hat. Diese hier laeuft danach und
|
||||
# ueberspringt sich, wenn das Geraet gerade eben benutzt wurde. Uebrig
|
||||
# bleibt genau der Fall, um den es geht - Admin ohne zweiten Faktor.
|
||||
last_auth_threshold: hours=1
|
||||
|
||||
# Ordnung 31: direkt hinter der Standard-MFA-Stage (30), vor dem Login (100).
|
||||
- model: authentik_flows.flowstagebinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !Find [authentik_flows.flow, [slug, default-authentication-flow]]
|
||||
order: 31
|
||||
id: admin_mfa_binding
|
||||
attrs:
|
||||
stage: !KeyOf admin_mfa_stage
|
||||
|
||||
# Eine PolicyBinding mit gesetztem "group" prueft Gruppenmitgliedschaft
|
||||
# (PolicyResult(group.is_member(user))). Ohne diese Bindung wuerde die
|
||||
# Stage fuer ALLE gelten - dann waere 2FA fuer die ganze Instanz Pflicht.
|
||||
- model: authentik_policies.policybinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf admin_mfa_binding
|
||||
order: 0
|
||||
attrs:
|
||||
group: !Find [authentik_core.group, [name, "authentik Admins"]]
|
||||
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]]
|
||||
|
||||
# ThreadNet-Branding: Wer sich anmeldet, soll nicht auf einer Seite
|
||||
# landen, die sich "authentik" nennt und ein fremdes Logo zeigt.
|
||||
# Assets werden bewusst NICHT in Authentik hochgeladen, sondern von
|
||||
# der Client-Auslieferung referenziert - sie liegen dort ohnehin und
|
||||
# ein zweites Mal gepflegt zu werden ist genau die Quelle, aus der
|
||||
# spaeter Abweichungen entstehen. Ein ConfigMap-Mount scheidet aus:
|
||||
# die drei Dateien sind zusammen ~775 KB, base64-kodiert reissen sie
|
||||
# das 1-MiB-Limit einer ConfigMap.
|
||||
#
|
||||
# Preis dieser Wahl: faellt Element Web aus, zeigt die Anmeldeseite
|
||||
# Platzhalter statt Logo und Hintergrund. Kosmetisch, nicht
|
||||
# funktional - anmelden kann man sich weiterhin.
|
||||
#
|
||||
# ⚠️ alpenglow.jpg existiert erst nach dem naechsten Client-Deploy.
|
||||
# Wird diese Brand vorher ausgerollt, ist der Hintergrund 404 und
|
||||
# Authentik zeigt Grau.
|
||||
branding_title: ThreadNet
|
||||
# Vorerst Authentiks eigenes Logo. Unser 512-px-PNG rendert in der
|
||||
# Anmeldemaske in Naturgroesse und damit viel zu gross: Authentiks
|
||||
# Default ist ein SVG, das sich seiner Box anpasst, ein PNG nicht.
|
||||
#
|
||||
# Explizit auf den Default gesetzt statt die Zeile zu loeschen - ein
|
||||
# Blueprint mit state: present setzt weggelassene Felder NICHT zurueck,
|
||||
# der alte Wert bliebe in der Datenbank stehen.
|
||||
#
|
||||
# ⚠️ SOLANGE DIESE ZEILE HIER STEHT, GEWINNT SIE. Wer das Logo in der
|
||||
# Authentik-Oberflaeche auswaehlt oder hochlaedt, sieht es bis zur
|
||||
# naechsten Blueprint-Reconciliation - danach steht wieder der Wert von
|
||||
# hier. Ein neues Logo gehoert deshalb in diese Zeile, nicht in die UI.
|
||||
#
|
||||
# Was schon oeffentlich erreichbar ist und hier eingetragen werden kann:
|
||||
# https://axion1337.chat/vector-icons/{24,120,144,152,180,512,1024}.png
|
||||
# Alles quadratische Bildmarken. Fuer diesen Slot passt eher eine
|
||||
# Wortmarke im Querformat - die gibt es noch nicht. Die vorhandene
|
||||
# threadnet-logo-wortmarke.png liegt im wiki-Repo (Gruppe homelab) und
|
||||
# ist von Hetzner aus NICHT erreichbar; sie muesste erst mit dem Client
|
||||
# ausgeliefert werden, damit Authentik sie laden kann.
|
||||
branding_logo: /static/dist/assets/icons/icon_left_brand.svg
|
||||
branding_favicon: https://axion1337.chat/vector-icons/favicon.ico
|
||||
branding_default_flow_background: https://axion1337.chat/themes/element/img/backgrounds/alpenglow.jpg
|
||||
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
|
||||
wiki-oidc-provider.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: wiki-oidc-provider
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
state: present
|
||||
identifiers:
|
||||
name: ThreadNet Wiki (Wiki.js)
|
||||
id: wiki_oidc_provider
|
||||
attrs:
|
||||
client_type: confidential
|
||||
# client_id + client_secret generiert Authentik selbst (kein Env-/SOPS-
|
||||
# Aufwand; client_id ist ohnehin nicht geheim). Beide nach dem Apply in
|
||||
# der Authentik-UI ablesen und in Wiki.js eintragen.
|
||||
# redirect_uris: echte Wiki.js-Callback-URL (OIDC-Strategy-Key), 2026-08-12.
|
||||
redirect_uris:
|
||||
- matching_mode: strict
|
||||
url: https://wiki.axion1337.chat/login/d3e7d0e4-adff-4421-b016-7758c44fd697/callback
|
||||
sub_mode: hashed_user_id
|
||||
include_claims_in_id_token: true
|
||||
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"]]
|
||||
# + Gruppen-Claim: entweder das mitgelieferte Groups-Scope-Mapping
|
||||
# (falls vorhanden) oder ein eigenes Scope-Mapping, das "groups"
|
||||
# zurückgibt — nötig für Rollen/Abschottung (#0049).
|
||||
|
||||
- model: authentik_core.application
|
||||
state: present
|
||||
identifiers:
|
||||
slug: wiki-js
|
||||
attrs:
|
||||
name: ThreadNet Wiki
|
||||
provider: !KeyOf wiki_oidc_provider
|
||||
meta_description: Internes Wiki (Betrieb/Anwender), Zugriff nach Gruppe
|
||||
policy_engine_mode: any
|
||||
open_in_new_tab: false
|
||||
|
||||
# Rollen (#0049): Admin = bestehende Gruppe `authentik Admins` (liest+schreibt
|
||||
# alles). `wiki-anwender` = kuratierte Leser von /anwender. Beide kommen über
|
||||
# den profile->groups-Claim in Wiki.js an; Mitglieder pflegt sorb in Authentik.
|
||||
- model: authentik_core.group
|
||||
state: present
|
||||
identifiers:
|
||||
name: wiki-anwender
|
||||
@@ -0,0 +1,25 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: authentik-credentials
|
||||
namespace: authentik
|
||||
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:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRekJuZythYzliTFJ3RlhS
|
||||
R2p6TG9NeFdabFlPRWtpNHJMYVVxTWZEcmlRClk0WUorSzdxNlcyWHYwWFBTMnlq
|
||||
TlM4dENSSit2S3VGSzJCeTRTYU52dmcKLS0tIEF0WkV0M25OSEo1N0FEYXI5Q0Z6
|
||||
QXVrY1NTeHZkeTlPRWNlVThzWno3T0kKC0KBoLT64GNqb8Ri9u69G7nqb1KftwwP
|
||||
/24aVHrPxKi9d4ij9n3bvCYDF4rhtfexhrE4n7CfuKn2DcSiuTniuw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
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
|
||||
@@ -0,0 +1,105 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: authentik
|
||||
namespace: authentik
|
||||
spec:
|
||||
interval: 1h
|
||||
chart:
|
||||
spec:
|
||||
chart: authentik
|
||||
version: "2026.2.3"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: goauthentik
|
||||
namespace: flux-system
|
||||
install:
|
||||
remediation:
|
||||
retries: 3
|
||||
upgrade:
|
||||
remediation:
|
||||
retries: 3
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: authentik-credentials
|
||||
valuesKey: secret_key
|
||||
targetPath: authentik.secret_key
|
||||
- kind: Secret
|
||||
name: authentik-credentials
|
||||
valuesKey: pg-password
|
||||
targetPath: authentik.postgresql.password
|
||||
- kind: Secret
|
||||
name: authentik-credentials
|
||||
valuesKey: pg-password
|
||||
targetPath: postgresql.auth.password
|
||||
- kind: Secret
|
||||
name: authentik-credentials
|
||||
valuesKey: smtp-password
|
||||
targetPath: authentik.email.password
|
||||
values:
|
||||
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
|
||||
error_reporting:
|
||||
enabled: false
|
||||
email:
|
||||
host: smtp.ionos.de
|
||||
port: 587
|
||||
username: gamemaster@axion1337.chat
|
||||
use_tls: true
|
||||
from: "Authentik <gamemaster@axion1337.chat>"
|
||||
|
||||
blueprints:
|
||||
configMaps:
|
||||
- authentik-blueprints
|
||||
|
||||
server:
|
||||
ingress:
|
||||
enabled: false
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
memory: 1Gi
|
||||
|
||||
worker:
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 512Mi
|
||||
limits:
|
||||
memory: 1Gi
|
||||
|
||||
postgresql:
|
||||
enabled: true
|
||||
auth:
|
||||
username: authentik
|
||||
database: authentik
|
||||
primary:
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 8Gi
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
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
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: auth-axion1337-chat-cert
|
||||
namespace: authentik
|
||||
spec:
|
||||
secretName: auth-axion1337-chat-tls
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- auth.axion1337.chat
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: goauthentik
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://charts.goauthentik.io
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: authentik
|
||||
namespace: authentik
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
secretName: auth-axion1337-chat-tls
|
||||
routes:
|
||||
- match: Host(`auth.axion1337.chat`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: authentik-server
|
||||
port: 80
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
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,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: authentik
|
||||
@@ -0,0 +1,149 @@
|
||||
# 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
|
||||
---
|
||||
# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088).
|
||||
#
|
||||
# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei
|
||||
# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy
|
||||
# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea
|
||||
# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen.
|
||||
#
|
||||
# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der
|
||||
# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19:
|
||||
# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und
|
||||
# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist
|
||||
# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse.
|
||||
#
|
||||
# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer
|
||||
# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher
|
||||
# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der
|
||||
# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall.
|
||||
#
|
||||
# Rollback: diese eine Policy im Namespace loeschen.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: egress-block-metadata
|
||||
namespace: authentik
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
# 1. DNS - zuerst, weil ohne DNS nichts geht.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
# 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1).
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.42.0.0/16
|
||||
- ipBlock:
|
||||
cidr: 10.43.0.0/16
|
||||
# 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz -
|
||||
# ausser Link-Local.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 169.254.0.0/16
|
||||
@@ -0,0 +1,130 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: alloy-config
|
||||
namespace: monitoring
|
||||
data:
|
||||
config.alloy: |
|
||||
// Kubernetes pod discovery
|
||||
discovery.kubernetes "k8s_pods" {
|
||||
role = "pod"
|
||||
}
|
||||
|
||||
// Relabel for Prometheus scrape
|
||||
discovery.relabel "prometheus_pods" {
|
||||
targets = discovery.kubernetes.k8s_pods.targets
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_scrape"]
|
||||
action = "keep"
|
||||
regex = "true"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_annotation_prometheus_io_path"]
|
||||
action = "replace"
|
||||
target_label = "__metrics_path__"
|
||||
regex = "(.+)"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__address__", "__meta_kubernetes_pod_annotation_prometheus_io_port"]
|
||||
action = "replace"
|
||||
regex = "([^:]+)(?::\\d+)?;(\\d+)"
|
||||
replacement = "$1:$2"
|
||||
target_label = "__address__"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_namespace"]
|
||||
action = "replace"
|
||||
target_label = "namespace"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_name"]
|
||||
action = "replace"
|
||||
target_label = "pod"
|
||||
}
|
||||
}
|
||||
|
||||
// Scrape Flux controllers (flux-system namespace, port 8080)
|
||||
discovery.kubernetes "flux_pods" {
|
||||
role = "pod"
|
||||
namespaces {
|
||||
names = ["flux-system"]
|
||||
}
|
||||
}
|
||||
|
||||
discovery.relabel "flux_scrape" {
|
||||
targets = discovery.kubernetes.flux_pods.targets
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_container_port_number"]
|
||||
action = "keep"
|
||||
regex = "8080"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_namespace"]
|
||||
action = "replace"
|
||||
target_label = "namespace"
|
||||
}
|
||||
rule {
|
||||
source_labels = ["__meta_kubernetes_pod_name"]
|
||||
action = "replace"
|
||||
target_label = "pod"
|
||||
}
|
||||
}
|
||||
|
||||
// Scrape kube-state-metrics
|
||||
prometheus.scrape "kube_state_metrics" {
|
||||
targets = [{
|
||||
__address__ = "kube-state-metrics.monitoring.svc.cluster.local:8080",
|
||||
}]
|
||||
forward_to = [prometheus.remote_write.selendis.receiver]
|
||||
scrape_interval = "30s"
|
||||
scrape_timeout = "10s"
|
||||
}
|
||||
|
||||
// Scrape Flux controllers
|
||||
prometheus.scrape "flux" {
|
||||
targets = discovery.relabel.flux_scrape.output
|
||||
forward_to = [prometheus.remote_write.selendis.receiver]
|
||||
scrape_interval = "30s"
|
||||
scrape_timeout = "10s"
|
||||
job_name = "flux"
|
||||
}
|
||||
|
||||
// Kein node-exporter-Scrape mehr: Host-Metriken liefert der systemd-Exporter
|
||||
// auf dem Host, den CFGMON direkt via 10.0.0.2:9100 scrapt. Das fruehere
|
||||
// DaemonSet konnte wegen Portkonflikt mit ebendiesem Dienst nie starten
|
||||
// (4883 Restarts) und haette dieselben Metriken doppelt geliefert (gitops#45).
|
||||
|
||||
// Scrape Synapse metrics
|
||||
prometheus.scrape "synapse" {
|
||||
targets = [{
|
||||
__address__ = "matrix-stack-synapse-main.matrix.svc.cluster.local:9001",
|
||||
}]
|
||||
forward_to = [prometheus.remote_write.selendis.receiver]
|
||||
scrape_interval = "30s"
|
||||
scrape_timeout = "10s"
|
||||
}
|
||||
|
||||
// Kubernetes pod logs to Loki
|
||||
loki.source.kubernetes "k8s_logs" {
|
||||
targets = discovery.kubernetes.k8s_pods.targets
|
||||
forward_to = [loki.write.selendis.receiver]
|
||||
}
|
||||
|
||||
// Remote write to Selendis Prometheus
|
||||
prometheus.remote_write "selendis" {
|
||||
endpoint {
|
||||
url = "http://10.0.0.3:9090/api/v1/write"
|
||||
write_relabel_config {
|
||||
source_labels = ["__name__"]
|
||||
regex = "go_.*|process_.*"
|
||||
action = "drop"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remote write logs to Selendis Loki
|
||||
loki.write "selendis" {
|
||||
endpoint {
|
||||
url = "http://10.0.0.3:3100/loki/api/v1/push"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: alloy
|
||||
namespace: monitoring
|
||||
spec:
|
||||
interval: 1h
|
||||
chart:
|
||||
spec:
|
||||
chart: alloy
|
||||
version: "0.x"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: grafana
|
||||
namespace: flux-system
|
||||
values:
|
||||
alloy:
|
||||
configMap:
|
||||
name: alloy-config
|
||||
replicaCount: 1
|
||||
serviceAccount:
|
||||
create: true
|
||||
name: alloy
|
||||
rbac:
|
||||
create: true
|
||||
podAnnotations:
|
||||
prometheus.io/scrape: "false"
|
||||
resources:
|
||||
limits:
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: prometheus-community
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://prometheus-community.github.io/helm-charts
|
||||
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: grafana
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://grafana.github.io/helm-charts
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: kube-state-metrics
|
||||
namespace: monitoring
|
||||
spec:
|
||||
interval: 1h
|
||||
chart:
|
||||
spec:
|
||||
chart: kube-state-metrics
|
||||
version: "5.x"
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: prometheus-community
|
||||
namespace: flux-system
|
||||
values:
|
||||
replicas: 1
|
||||
service:
|
||||
port: 8080
|
||||
prometheus:
|
||||
monitor:
|
||||
enabled: false
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- helm-repos.yaml
|
||||
- kube-state-metrics.yaml
|
||||
- alloy-config.yaml
|
||||
- alloy.yaml
|
||||
- networkpolicy.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: monitoring
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088).
|
||||
#
|
||||
# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei
|
||||
# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy
|
||||
# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea
|
||||
# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen.
|
||||
#
|
||||
# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der
|
||||
# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19:
|
||||
# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und
|
||||
# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist
|
||||
# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse.
|
||||
#
|
||||
# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer
|
||||
# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher
|
||||
# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der
|
||||
# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall.
|
||||
#
|
||||
# Rollback: diese eine Policy im Namespace loeschen.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: egress-block-metadata
|
||||
namespace: monitoring
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
# 1. DNS - zuerst, weil ohne DNS nichts geht.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
# 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1).
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.42.0.0/16
|
||||
- ipBlock:
|
||||
cidr: 10.43.0.0/16
|
||||
# 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz -
|
||||
# ausser Link-Local.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 169.254.0.0/16
|
||||
@@ -28,6 +28,21 @@ spec:
|
||||
services:
|
||||
- name: matrix-stack-well-known
|
||||
port: 8010
|
||||
# Element Desktop Setup Skripte
|
||||
- match: Host(`axion1337.chat`) && PathPrefix(`/docs/setup`)
|
||||
kind: Rule
|
||||
priority: 50
|
||||
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
|
||||
|
||||
|
After Width: | Height: | Size: 391 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 709 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -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
|
||||
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env python3
|
||||
# @concierge - Gaeste-Einladungen mit Ablauf, Freischaltung und begrenzter
|
||||
# Verlaengerung (gitops#48, Design von sorb am 2026-08-01 festgezurrt).
|
||||
#
|
||||
# WARUM EIN EIGENER BOT UND NICHT DRAUPNIR
|
||||
# Draupnir ist ein Moderationsbot ohne Lebenszyklus-Funktionen. Er kann einen
|
||||
# Gast policy-seitig einschraenken, aber Links erzeugen, Ablaeufe verwalten und
|
||||
# Konten deaktivieren kann er nicht. Ihn dafuer zu verbiegen hiesse, Upstream-
|
||||
# Code zu forken, den wir sonst unveraendert mitziehen.
|
||||
#
|
||||
# WARUM AUTHENTIK UND NICHT SYNAPSE-REGISTRATION-TOKENS
|
||||
# In diesem Stack laeuft Registrierung ausschliesslich ueber Authentik (MAS-OIDC).
|
||||
# Synapse kennt gar keinen offenen Registrierungsweg - ein Registration-Token
|
||||
# waere wirkungslos. Der natuerliche Einladungslink ist deshalb ein
|
||||
# Authentik-Invitation-Token: einmalig verwendbar, mit eigenem Ablaufdatum.
|
||||
#
|
||||
# BERECHTIGUNG = GRUPPE **UND** RAUM
|
||||
# Autoritativ ist die Mitgliedschaft in der Authentik-Gruppe (INVITE_GROUP).
|
||||
# Zusaetzlich nimmt der Bot Kommandos nur im Einladungsraum an. Die Gruppe ist
|
||||
# die Kontrolle, der Raum die Transparenz: Jede Einladung hinterlaesst einen
|
||||
# nachlesbaren Eintrag, wer wen eingeladen hat. Beides zusammen, weil eine
|
||||
# Gruppe allein unsichtbar ist und ein Raum allein nicht autorisiert.
|
||||
#
|
||||
# ⚠️ ZUORDNUNG MATRIX -> AUTHENTIK
|
||||
# Der Bot nimmt an, dass der Matrix-Localpart dem Authentik-Benutzernamen
|
||||
# entspricht (@gast:axion1337.chat -> "gast"). Das gilt in diesem Stack, weil
|
||||
# MAS die Konten aus Authentik provisioniert. Stimmt das einmal nicht, findet
|
||||
# der Bot den Nutzer nicht und sagt das - er raet nicht.
|
||||
#
|
||||
# FEHLERVERHALTEN, BEWUSST ASYMMETRISCH
|
||||
# - Einladen/Freischalten scheitert LAUT: lieber keine Einladung als eine, von
|
||||
# der niemand weiss.
|
||||
# - Die Ablaufpruefung deaktiviert NUR, wenn Authentik sauber geantwortet hat.
|
||||
# Ein API-Fehler darf nicht dazu fuehren, dass Konten reihenweise abgeschaltet
|
||||
# werden - im Zweifel bleibt ein Gast einen Durchlauf laenger aktiv.
|
||||
#
|
||||
# Stdlib only, wie die uebrigen Bots dieses Verbunds.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
log = logging.getLogger("concierge")
|
||||
|
||||
MATRIX = os.environ["MATRIX_HOMESERVER"].rstrip("/")
|
||||
ROOM = os.environ["MATRIX_ROOM_ID"]
|
||||
AUTHENTIK = os.environ["AUTHENTIK_URL"].rstrip("/")
|
||||
INVITE_GROUP = os.environ.get("INVITE_GROUP", "invite-berechtigt")
|
||||
MEMBER_GROUP = os.environ.get("MEMBER_GROUP", "members")
|
||||
ADMIN_GROUP = os.environ.get("ADMIN_GROUP", "authentik Admins")
|
||||
INVITE_FLOW = os.environ.get("INVITE_FLOW_SLUG", "matrix-invitation")
|
||||
GUEST_DAYS = int(os.environ.get("GUEST_DAYS", "3"))
|
||||
MAX_RENEWALS = int(os.environ.get("MAX_RENEWALS", "2"))
|
||||
SWEEP_SECONDS = int(os.environ.get("SWEEP_SECONDS", "900"))
|
||||
|
||||
# Attribute am Authentik-Nutzer. Praefix, damit sie nicht mit Feldern anderer
|
||||
# Werkzeuge kollidieren, die sich denselben attributes-Topf teilen.
|
||||
ATTR_EXPIRES = "threadnet_guest_expires_at"
|
||||
ATTR_RENEWALS = "threadnet_guest_renewals"
|
||||
ATTR_INVITED_BY = "threadnet_invited_by"
|
||||
|
||||
|
||||
def _read(path_env, direct_env):
|
||||
"""Token entweder aus einer Datei (Secret-Mount) oder direkt. Dateien sind
|
||||
der Normalfall - ein Wert in der Umgebung steht in jedem Prozess-Dump."""
|
||||
p = os.environ.get(path_env)
|
||||
if p:
|
||||
with open(p) as f:
|
||||
return f.read().strip()
|
||||
return os.environ[direct_env]
|
||||
|
||||
|
||||
MATRIX_TOKEN = _read("MATRIX_TOKEN_FILE", "MATRIX_TOKEN")
|
||||
AUTHENTIK_TOKEN = _read("AUTHENTIK_TOKEN_FILE", "AUTHENTIK_TOKEN")
|
||||
|
||||
|
||||
def _call(url, token, method="GET", body=None, scheme="Bearer"):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(url, data=data, method=method)
|
||||
req.add_header("Authorization", f"{scheme} {token}")
|
||||
if data:
|
||||
req.add_header("Content-Type", "application/json")
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
raw = r.read()
|
||||
return json.loads(raw) if raw else {}
|
||||
|
||||
|
||||
def ak(path, method="GET", body=None):
|
||||
return _call(f"{AUTHENTIK}/api/v3{path}", AUTHENTIK_TOKEN, method, body)
|
||||
|
||||
|
||||
def mx(path, method="GET", body=None):
|
||||
return _call(f"{MATRIX}/_matrix/client/v3{path}", MATRIX_TOKEN, method, body)
|
||||
|
||||
|
||||
def say(text):
|
||||
txn = str(int(time.time() * 1000))
|
||||
room = urllib.parse.quote(ROOM)
|
||||
mx(f"/rooms/{room}/send/m.room.message/{txn}", "PUT",
|
||||
{"msgtype": "m.notice", "body": text})
|
||||
|
||||
|
||||
# --- Authentik ---------------------------------------------------------------
|
||||
|
||||
def find_user(username):
|
||||
r = ak(f"/core/users/?username={urllib.parse.quote(username)}")
|
||||
for u in r.get("results", []):
|
||||
if u["username"] == username:
|
||||
return u
|
||||
return None
|
||||
|
||||
|
||||
def group_uuid(name):
|
||||
r = ak(f"/core/groups/?name={urllib.parse.quote(name)}")
|
||||
for g in r.get("results", []):
|
||||
if g["name"] == name:
|
||||
return g["pk"]
|
||||
return None
|
||||
|
||||
|
||||
def in_group(user, name):
|
||||
return any(g.get("name") == name for g in user.get("groups_obj", []))
|
||||
|
||||
|
||||
def set_attrs(user, **changes):
|
||||
"""attributes ist ein einzelnes JSON-Feld: PATCH ersetzt es komplett. Wer nur
|
||||
einen Schluessel schickt, loescht alle anderen - deshalb immer mischen."""
|
||||
attrs = dict(user.get("attributes") or {})
|
||||
for k, v in changes.items():
|
||||
if v is None:
|
||||
attrs.pop(k, None)
|
||||
else:
|
||||
attrs[k] = v
|
||||
return ak(f"/core/users/{user['pk']}/", "PATCH", {"attributes": attrs})
|
||||
|
||||
|
||||
def localpart(mxid):
|
||||
return mxid.lstrip("@").split(":")[0]
|
||||
|
||||
|
||||
# --- Kommandos ---------------------------------------------------------------
|
||||
|
||||
def darf_einladen(sender):
|
||||
u = find_user(localpart(sender))
|
||||
return u is not None and in_group(u, INVITE_GROUP), u
|
||||
|
||||
|
||||
def ist_admin(sender):
|
||||
u = find_user(localpart(sender))
|
||||
return u is not None and in_group(u, ADMIN_GROUP)
|
||||
|
||||
|
||||
def cmd_einladen(sender, args):
|
||||
ok, _ = darf_einladen(sender)
|
||||
if not ok:
|
||||
say(f"{sender}: du bist nicht in der Gruppe '{INVITE_GROUP}'.")
|
||||
return
|
||||
name = (args or "gast").strip().replace(" ", "-")[:40]
|
||||
expires = datetime.now(timezone.utc) + timedelta(days=GUEST_DAYS)
|
||||
inv = ak("/stages/invitation/invitations/", "POST", {
|
||||
"name": f"gast-{name}-{int(time.time())}",
|
||||
"expires": expires.isoformat(),
|
||||
"single_use": True,
|
||||
"fixed_data": {ATTR_INVITED_BY: sender},
|
||||
})
|
||||
link = f"{AUTHENTIK}/if/flow/{INVITE_FLOW}/?itoken={inv['pk']}"
|
||||
say(f"Einladung von {sender} fuer '{name}':\n{link}\n"
|
||||
f"Einmalig verwendbar, verfaellt {expires:%d.%m.%Y %H:%M} UTC.")
|
||||
|
||||
|
||||
def cmd_freischalten(sender, args):
|
||||
if not ist_admin(sender):
|
||||
say(f"{sender}: Freischalten darf nur die Gruppe '{ADMIN_GROUP}'.")
|
||||
return
|
||||
u = find_user(localpart(args.strip()))
|
||||
if not u:
|
||||
say(f"Kein Authentik-Konto zu '{args.strip()}' gefunden.")
|
||||
return
|
||||
set_attrs(u, **{ATTR_EXPIRES: None, ATTR_RENEWALS: None})
|
||||
gid = group_uuid(MEMBER_GROUP)
|
||||
if gid:
|
||||
ak(f"/core/groups/{gid}/add_user/", "POST", {"pk": u["pk"]})
|
||||
say(f"{u['username']} ist dauerhaft freigeschaltet (von {sender}).")
|
||||
|
||||
|
||||
def cmd_verlaengern(sender, args):
|
||||
ok, _ = darf_einladen(sender)
|
||||
if not ok:
|
||||
say(f"{sender}: du bist nicht in der Gruppe '{INVITE_GROUP}'.")
|
||||
return
|
||||
u = find_user(localpart(args.strip()))
|
||||
if not u:
|
||||
say(f"Kein Authentik-Konto zu '{args.strip()}' gefunden.")
|
||||
return
|
||||
used = int((u.get("attributes") or {}).get(ATTR_RENEWALS, 0))
|
||||
if used >= MAX_RENEWALS:
|
||||
say(f"{u['username']}: {MAX_RENEWALS} Verlaengerungen sind aufgebraucht. "
|
||||
f"Jetzt muss ein Admin freischalten.")
|
||||
return
|
||||
neu = datetime.now(timezone.utc) + timedelta(days=1)
|
||||
set_attrs(u, **{ATTR_EXPIRES: neu.isoformat(), ATTR_RENEWALS: used + 1})
|
||||
if not u.get("is_active"):
|
||||
ak(f"/core/users/{u['pk']}/", "PATCH", {"is_active": True})
|
||||
say(f"{u['username']} um einen Tag verlaengert ({used + 1}/{MAX_RENEWALS}), "
|
||||
f"laeuft {neu:%d.%m.%Y %H:%M} UTC ab.")
|
||||
|
||||
|
||||
def cmd_status(_sender, _args):
|
||||
r = ak("/core/users/?page_size=200")
|
||||
zeilen = []
|
||||
for u in r.get("results", []):
|
||||
exp = (u.get("attributes") or {}).get(ATTR_EXPIRES)
|
||||
if exp:
|
||||
used = (u.get("attributes") or {}).get(ATTR_RENEWALS, 0)
|
||||
zustand = "aktiv" if u.get("is_active") else "deaktiviert"
|
||||
zeilen.append(f" {u['username']}: laeuft {exp[:16]} ab, "
|
||||
f"{used}/{MAX_RENEWALS} verlaengert, {zustand}")
|
||||
say("Gaeste:\n" + ("\n".join(zeilen) if zeilen else " keine offenen Gastkonten"))
|
||||
|
||||
|
||||
def cmd_hilfe(_sender, _args):
|
||||
say("!einladen <name> - Einladungslink erzeugen\n"
|
||||
"!verlaengern @nutzer - um einen Tag verlaengern (begrenzt)\n"
|
||||
"!freischalten @nutzer - dauerhaft freischalten (nur Admins)\n"
|
||||
"!status - offene Gastkonten anzeigen")
|
||||
|
||||
|
||||
BEFEHLE = {
|
||||
"!einladen": cmd_einladen,
|
||||
"!verlaengern": cmd_verlaengern,
|
||||
"!freischalten": cmd_freischalten,
|
||||
"!status": cmd_status,
|
||||
"!hilfe": cmd_hilfe,
|
||||
}
|
||||
|
||||
|
||||
# --- Ablaufpruefung ----------------------------------------------------------
|
||||
|
||||
def sweep():
|
||||
try:
|
||||
r = ak("/core/users/?page_size=200")
|
||||
except Exception as e:
|
||||
# KEIN Deaktivieren bei API-Fehlern - siehe Kopfkommentar.
|
||||
log.warning("Ablaufpruefung uebersprungen, Authentik nicht erreichbar: %s", e)
|
||||
return
|
||||
jetzt = datetime.now(timezone.utc)
|
||||
for u in r.get("results", []):
|
||||
exp = (u.get("attributes") or {}).get(ATTR_EXPIRES)
|
||||
if not exp or not u.get("is_active"):
|
||||
continue
|
||||
try:
|
||||
faellig = datetime.fromisoformat(exp)
|
||||
except ValueError:
|
||||
log.warning("%s: unlesbares Ablaufdatum %r", u["username"], exp)
|
||||
continue
|
||||
if faellig.tzinfo is None:
|
||||
faellig = faellig.replace(tzinfo=timezone.utc)
|
||||
if faellig <= jetzt:
|
||||
ak(f"/core/users/{u['pk']}/", "PATCH", {"is_active": False})
|
||||
say(f"Gastkonto {u['username']} ist abgelaufen und wurde deaktiviert. "
|
||||
f"'!verlaengern @{u['username']}' oder Admin-Freischaltung.")
|
||||
|
||||
|
||||
# --- Hauptschleife -----------------------------------------------------------
|
||||
|
||||
def main():
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s")
|
||||
mx(f"/rooms/{urllib.parse.quote(ROOM)}/join", "POST", {})
|
||||
# Ab jetzt, nicht die Raumhistorie: ein Neustart soll keine alten Kommandos
|
||||
# erneut ausfuehren.
|
||||
since = mx("/sync?timeout=0").get("next_batch")
|
||||
log.info("bereit, Raum %s", ROOM)
|
||||
letzter_sweep = 0.0
|
||||
while True:
|
||||
try:
|
||||
if time.time() - letzter_sweep > SWEEP_SECONDS:
|
||||
sweep()
|
||||
letzter_sweep = time.time()
|
||||
r = mx(f"/sync?since={urllib.parse.quote(since)}&timeout=30000")
|
||||
since = r.get("next_batch", since)
|
||||
raum = r.get("rooms", {}).get("join", {}).get(ROOM, {})
|
||||
for ev in raum.get("timeline", {}).get("events", []):
|
||||
if ev.get("type") != "m.room.message":
|
||||
continue
|
||||
c = ev.get("content", {})
|
||||
if c.get("msgtype") != "m.text":
|
||||
continue
|
||||
text = (c.get("body") or "").strip()
|
||||
wort = text.split(" ", 1)[0].lower()
|
||||
if wort not in BEFEHLE:
|
||||
continue
|
||||
rest = text[len(wort):].strip()
|
||||
try:
|
||||
BEFEHLE[wort](ev["sender"], rest)
|
||||
except Exception as e:
|
||||
log.exception("Kommando %s fehlgeschlagen", wort)
|
||||
say(f"'{wort}' fehlgeschlagen: {e}")
|
||||
except urllib.error.HTTPError as e:
|
||||
log.warning("HTTP %s bei /sync - warte", e.code)
|
||||
time.sleep(10)
|
||||
except Exception:
|
||||
log.exception("Schleifenfehler")
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,85 @@
|
||||
# @concierge - Gaeste-Einladungen (gitops#48). Skript: concierge-bot.py,
|
||||
# als ConfigMap ueber den configMapGenerator in kustomization.yaml.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: concierge-bot
|
||||
namespace: matrix
|
||||
spec:
|
||||
# ⚠️ Genau EINE Instanz. Der Bot haelt eine /sync-Schleife und verarbeitet
|
||||
# Kommandos; zwei Instanzen wuerden jedes Kommando doppelt ausfuehren und
|
||||
# jede Meldung doppelt posten. Deshalb replicas: 1 UND Recreate - bei
|
||||
# RollingUpdate liefen waehrend eines Deploys kurzzeitig zwei.
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: concierge-bot
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: concierge-bot
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: bot
|
||||
image: python:3.12-alpine
|
||||
command: ["python3", "/app/concierge-bot.py"]
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
env:
|
||||
- name: MATRIX_HOMESERVER
|
||||
value: "https://matrix.axion1337.chat"
|
||||
# Der Einladungsraum. ⚠️ Muss invite-only sein - der Bot prueft zwar
|
||||
# zusaetzlich die Authentik-Gruppe, aber ein offener Raum macht
|
||||
# sichtbar, wer eingeladen wurde, und das ist der halbe Zweck.
|
||||
- name: MATRIX_ROOM_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: concierge-credentials
|
||||
key: matrix-room-id
|
||||
# In-Cluster, nicht ueber die oeffentliche Adresse: spart den Umweg
|
||||
# ueber Traefik und funktioniert auch, wenn extern etwas klemmt.
|
||||
- name: AUTHENTIK_URL
|
||||
value: "http://authentik-server.authentik.svc.cluster.local"
|
||||
- name: MATRIX_TOKEN_FILE
|
||||
value: /secrets/matrix-token
|
||||
- name: AUTHENTIK_TOKEN_FILE
|
||||
value: /secrets/authentik-token
|
||||
- name: GUEST_DAYS
|
||||
value: "3"
|
||||
- name: MAX_RENEWALS
|
||||
value: "2"
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /app
|
||||
readOnly: true
|
||||
- name: creds
|
||||
mountPath: /secrets
|
||||
readOnly: true
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
volumes:
|
||||
- name: script
|
||||
configMap:
|
||||
name: concierge-bot-script
|
||||
- name: creds
|
||||
secret:
|
||||
secretName: concierge-credentials
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,31 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: coturn-secret
|
||||
namespace: matrix
|
||||
stringData:
|
||||
TURN_SECRET: ENC[AES256_GCM,data:Cbu5SoxQp0L9WFZFbEiyK8j0IJuSgoJE9OpRKZVXyF+PcTM+ewD+3TI8xq+g0C34XDWeaJdxaj3B4yyKsqQLIA==,iv:lW8lD3D75Z0b0EfBj5myUrv4GWMvRTBELjhCVm+QJjc=,tag:nC5L/PcYPoLJP1whSSfSTA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQMncxOEhRZC9jcHpjT3dW
|
||||
Q1RNaW5pc00zVnJhWHRnZHd3TGhURWNZU2dJCk85bXJkbTEyd1ZybjhDT05pL2c3
|
||||
ZU9EUSt1eDlSQWJyVGtsak1oS3FSR2sKLS0tIEFUdXVHL0V1ZW5VMVVBZEJaYUIw
|
||||
U1BrYlJyQVZkZFhBdmdwbDMyK3lTQkkKEaSy1o+IICf2uaT6olapRJa/duXxjOBg
|
||||
OqRS9axnJ71XxEnHjLTsCbkI5b+8Fux08qKaH9sMsJrWOiSHDdTXXQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
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
|
||||
@@ -0,0 +1,181 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: coturn-config
|
||||
namespace: matrix
|
||||
data:
|
||||
turnserver.conf: |
|
||||
# TURN Server configuration
|
||||
realm=axion1337.chat
|
||||
|
||||
# Listen ports
|
||||
listening-port=3478
|
||||
listening-ip=0.0.0.0
|
||||
alt-listening-port=5349
|
||||
alt-listening-ip=0.0.0.0
|
||||
|
||||
# External IPs (for clients behind NAT)
|
||||
relay-ip=49.13.132.245
|
||||
external-ip=49.13.132.245
|
||||
|
||||
# Relay port range
|
||||
min-bps=0
|
||||
bps-capacity=0
|
||||
|
||||
# Authentication
|
||||
use-auth-secret
|
||||
static-auth-secret=$TURN_SECRET
|
||||
|
||||
# HTTPS/TLS
|
||||
cert=/etc/coturn/tls/tls.crt
|
||||
pkey=/etc/coturn/tls/tls.key
|
||||
|
||||
# Performance tuning
|
||||
max-bps=0
|
||||
bps-capacity=0
|
||||
log-file=stdout
|
||||
|
||||
# Logging
|
||||
verbose
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: coturn
|
||||
namespace: matrix
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: stun-udp
|
||||
port: 3478
|
||||
protocol: UDP
|
||||
- name: stun-tcp
|
||||
port: 3478
|
||||
protocol: TCP
|
||||
- name: turns-tcp
|
||||
port: 5349
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: coturn
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: coturn
|
||||
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
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
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
|
||||
initContainers:
|
||||
- name: init-config
|
||||
# Gepinnt und aktuell gehalten (#0052); 1.36 ist die im Repo bereits
|
||||
# anderswo genutzte Version — ein Stand statt zwei.
|
||||
image: busybox:1.36
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
TURN_SECRET=$(cat /etc/coturn-secret/TURN_SECRET)
|
||||
sed "s|\$TURN_SECRET|$TURN_SECRET|g" /etc/coturn-template/turnserver.conf > /etc/coturn/turnserver.conf
|
||||
chmod 644 /etc/coturn/turnserver.conf
|
||||
resources:
|
||||
limits:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 32Mi
|
||||
volumeMounts:
|
||||
- name: config-template
|
||||
mountPath: /etc/coturn-template
|
||||
- name: config
|
||||
mountPath: /etc/coturn
|
||||
- name: secret
|
||||
mountPath: /etc/coturn-secret
|
||||
readOnly: true
|
||||
containers:
|
||||
- name: coturn
|
||||
# Gepinnt statt ':latest' (#0052). Vorher lief hier 4.10.0, waehrend ':latest'
|
||||
# laengst auf 4.17.2 zeigte: mit imagePullPolicy IfNotPresent haelt der Node das
|
||||
# einmal gezogene Image fest, und der naechste Pod-Neustart auf einem frischen
|
||||
# Node waere still ueber sieben Minor-Versionen gesprungen. Genau deshalb ist
|
||||
# ':latest' weder reproduzierbar noch sinnvoll scanbar.
|
||||
image: coturn/coturn:4.17.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: stun-udp
|
||||
containerPort: 3478
|
||||
protocol: UDP
|
||||
- name: stun-tcp
|
||||
containerPort: 3478
|
||||
protocol: TCP
|
||||
- name: turns-tcp
|
||||
containerPort: 5349
|
||||
protocol: TCP
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/coturn
|
||||
- name: tls
|
||||
mountPath: /etc/coturn/tls
|
||||
readOnly: true
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 3478
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: config
|
||||
emptyDir: {}
|
||||
- name: config-template
|
||||
configMap:
|
||||
name: coturn-config
|
||||
- name: secret
|
||||
secret:
|
||||
secretName: coturn-secret
|
||||
defaultMode: 0400
|
||||
- name: tls
|
||||
secret:
|
||||
secretName: turn-axion1337-chat-tls
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- matrix
|
||||
@@ -0,0 +1,186 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ess-element-custom
|
||||
namespace: matrix
|
||||
data:
|
||||
values.yaml: |
|
||||
elementWeb:
|
||||
additional:
|
||||
config.json: |
|
||||
{
|
||||
"brand": "aXion1337.Chat",
|
||||
"default_theme": "aXion1337 Dark",
|
||||
"show_labs_settings": true,
|
||||
"features": {
|
||||
"feature_qr_code_login": true,
|
||||
"feature_new_room_list": true
|
||||
},
|
||||
"element_call": {
|
||||
"use_exclusively": true
|
||||
},
|
||||
"setting_defaults": {
|
||||
"custom_themes": [
|
||||
{
|
||||
"name": "aXion1337 Dark true",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#ffaf0f",
|
||||
"primary-color": "#ffaf0f",
|
||||
"secondary-color": "#ffaf0f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Deep Purple",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#6503b3",
|
||||
"primary-color": "#368bd6",
|
||||
"warning-color": "#b30356",
|
||||
"sidebar-color": "#15171B",
|
||||
"roomlist-background-color": "#22262E",
|
||||
"roomlist-text-color": "#A1B2D1",
|
||||
"roomlist-text-secondary-color": "#EDF3FF",
|
||||
"roomlist-highlights-color": "#343A46",
|
||||
"roomlist-separator-color": "#a1b2d1",
|
||||
"timeline-background-color": "#181b21",
|
||||
"timeline-text-color": "#EDF3FF",
|
||||
"timeline-text-secondary-color": "#A1B2D1",
|
||||
"timeline-highlights-color": "#22262E"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Discord Dark",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#747ff4",
|
||||
"accent": "#747ff4",
|
||||
"primary-color": "#00aff4",
|
||||
"warning-color": "#faa81ad9",
|
||||
"alert": "#faa81ad9",
|
||||
"sidebar-color": "#202225",
|
||||
"roomlist-background-color": "#2f3136",
|
||||
"roomlist-text-color": "#dcddde",
|
||||
"roomlist-text-secondary-color": "#8e9297",
|
||||
"roomlist-highlights-color": "#4f545c52",
|
||||
"roomlist-separator-color": "#40444b",
|
||||
"timeline-background-color": "#36393f",
|
||||
"timeline-text-color": "#dcddde",
|
||||
"secondary-content": "#dcddde",
|
||||
"tertiary-content": "#dcddde",
|
||||
"timeline-text-secondary-color": "#b9bbbe",
|
||||
"timeline-highlights-color": "#04040512",
|
||||
"reaction-row-button-selected-bg-color": "#4752c4",
|
||||
"menu-selected-color": "#4752c4",
|
||||
"focus-bg-color": "#4752c4",
|
||||
"room-highlight-color": "#4752c4",
|
||||
"other-user-pill-bg-color": "#4752c4",
|
||||
"togglesw-off-color": "#72767d"
|
||||
},
|
||||
"compound": {
|
||||
"--cpd-color-theme-bg": "#0019ff",
|
||||
"--cpd-color-bg-canvas-default": "#2f3136",
|
||||
"--cpd-color-bg-subtle-secondary": "#2f3136",
|
||||
"--cpd-color-bg-subtle-primary": "#4f545c52",
|
||||
"--cpd-color-bg-action-primary-rest": "#dcddde",
|
||||
"--cpd-color-bg-action-secondary-rest": "#2f3136",
|
||||
"--cpd-color-bg-critical-primary": "#fd3f3c",
|
||||
"--cpd-color-bg-critical-subtle": "#745862",
|
||||
"--cpd-color-bg-critical-hovered": "#fd3f3c",
|
||||
"--cpd-color-bg-accent-rest": "#4cb387",
|
||||
"--cpd-color-text-primary": "#dcddde",
|
||||
"--cpd-color-text-secondary": "#b9bbbe",
|
||||
"--cpd-color-text-action-accent": "#b9bbbe",
|
||||
"--cpd-color-text-critical-primary": "#fd3f3c",
|
||||
"--cpd-color-text-success-primary": "#4cb387",
|
||||
"--cpd-color-icon-primary": "#dcddde",
|
||||
"--cpd-color-icon-secondary": "#dcddde",
|
||||
"--cpd-color-icon-tertiary": "#a7a0a7",
|
||||
"--cpd-color-icon-accent-tertiary": "#4cb387",
|
||||
"--cpd-color-border-interactive-primary": "#5d6064",
|
||||
"--cpd-color-border-interactive-secondary": "#5d6064",
|
||||
"--cpd-color-border-critical-primary": "#fd3f3c",
|
||||
"--cpd-color-border-success-subtle": "#4cb387"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Electric Blue",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#3596fc",
|
||||
"primary-color": "#368bd6",
|
||||
"warning-color": "#ff4b55",
|
||||
"sidebar-color": "#27303a",
|
||||
"roomlist-background-color": "#f3f8fd",
|
||||
"roomlist-text-color": "#2e2f32",
|
||||
"roomlist-text-secondary-color": "#61708b",
|
||||
"roomlist-highlights-color": "#ffffff",
|
||||
"roomlist-separator-color": "#e3e8f0",
|
||||
"timeline-background-color": "#ffffff",
|
||||
"timeline-text-color": "#2e2f32",
|
||||
"timeline-text-secondary-color": "#61708b",
|
||||
"timeline-highlights-color": "#f3f8fd",
|
||||
"username-colors": ["#ff0000", "#ff7f00", "#ffff00", "#00ff00", "#0000ff", "#4b0082", "#9400d3", "#ff1493"],
|
||||
"avatar-background-colors": ["#cc0000", "#cc6600", "#cccc00", "#00cc00", "#0000cc", "#3b0066", "#7a00b3", "#cc1077"]
|
||||
},
|
||||
"compound": {
|
||||
"--cpd-color-icon-accent-tertiary": "var(--cpd-color-blue-800)",
|
||||
"--cpd-color-text-action-accent": "var(--cpd-color-blue-900)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Everforest dark hard",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#a7c080",
|
||||
"primary-color": "#a7c080",
|
||||
"warning-color": "#e67e80",
|
||||
"sidebar-color": "#323d43",
|
||||
"roomlist-background-color": "#2f383e",
|
||||
"roomlist-text-color": "#d3c6aa",
|
||||
"roomlist-text-secondary-color": "#d3c6aa",
|
||||
"roomlist-highlights-color": "#4b565c",
|
||||
"roomlist-separator-color": "#4b565c",
|
||||
"timeline-background-color": "#2b3339",
|
||||
"timeline-text-color": "#d3c6aa",
|
||||
"secondary-content": "#d3c6aa",
|
||||
"tertiary-content": "#d3c6aa",
|
||||
"timeline-text-secondary-color": "#a7c080",
|
||||
"timeline-highlights-color": "#4b565c",
|
||||
"reaction-row-button-selected-bg-color": "#4b565c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aXion1337 Dark", #Gruvbox Dark
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#bd93f9",
|
||||
"primary-color": "#fe8019",
|
||||
"warning-color": "#fb4934",
|
||||
"sidebar-color": "#282828",
|
||||
"roomlist-background-color": "#1d2021",
|
||||
"roomlist-text-color": "#a89984",
|
||||
"roomlist-text-secondary-color": "#00ff00",
|
||||
"roomlist-highlights-color": "#00000030",
|
||||
"roomlist-separator-color": "#4d4d4d90",
|
||||
"timeline-background-color": "#282828",
|
||||
"timeline-text-color": "#ebdbb2",
|
||||
"secondary-content": "#928374",
|
||||
"tertiary-content": "#928374",
|
||||
"quinary-content": "#504945",
|
||||
"timeline-text-secondary-color": "#a89984",
|
||||
"timeline-highlights-color": "#00000030",
|
||||
"reaction-row-button-selected-bg-color": "#689d6a",
|
||||
"menu-selected-color": "#504945",
|
||||
"icon-button-color": "#928374",
|
||||
"accent": "#689d6a",
|
||||
"alert": "#cc241d",
|
||||
"username-colors": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#d79921",
|
||||
"#458588",
|
||||
"#b16286",
|
||||
"#689d6a",
|
||||
"#a89984",
|
||||
"#d65d0e"
|
||||
@@ -1,3 +1,8 @@
|
||||
# Themes: die Paletten stammen aus Anthropics theme-factory-Skill und sind dort
|
||||
# woertlich uebernommen — nicht frei erfinden. Herkunft, Werte und Fallen
|
||||
# (u. a.: is_dark steht nicht verlaesslich in den Beschreibungen) stehen in
|
||||
# shared/branding.md im management-Repo. Zweite Kopie derselben Themes liegt in
|
||||
# ThreadNet-Web unter apps/desktop/axion1337/config.json — beim Aendern mitziehen.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
@@ -10,22 +15,627 @@ data:
|
||||
config.json: |
|
||||
{
|
||||
"brand": "aXion1337.Chat",
|
||||
"_kommentar_custom_urls": "Entscheidung sorb 2026-08-19 (management #0099). Blendet den 'Bearbeiten'-Knopf am Servernamen aus (ServerPicker.tsx) - der Homeserver ist damit ueber die Oberflaeche nicht mehr wechselbar, und die Login-Fehlermeldung benennt bei 401/403 den Server statt generisch zu bleiben (ErrorUtils.tsx). Es ist eine OBERFLAECHEN-Sperre: der hs_url-Query-Parameter wird in MatrixChat.tsx (mobile Registrierung, Registrierungs-Bestaetigung) weiterhin ohne diese Pruefung uebernommen. Gegen das offene Advisory GHSA-wrcp-5v3v-3j6v (betrifft < 1.12.22, wir laufen 1.12.17) verengt das die Flaeche, ersetzt aber NICHT das Upstream-Update aus #0099. Dieselbe Zeile muss im Desktop-Client stehen - der hat seine eigene config.json.",
|
||||
"disable_custom_urls": true,
|
||||
"_kommentar_bug_report": "local = der Knopf erscheint als 'Protokolle herunterladen'; die Logs bleiben auf dem Geraet und gehen an KEINEN Server. Ohne diesen Wert ist die ganze Sektion unsichtbar und Nutzer kommen gar nicht an ihre Logs (ThreadNet-Web#9). Der element.io-Endpunkt wurde am 2026-08-01 bewusst entfernt - dieser Wert holt die Faehigkeit zurueck, ohne den Abfluss.",
|
||||
"bug_report_endpoint_url": "local",
|
||||
"_kommentar_report_event": "Weg B (Entscheidung sorb 2026-08-15): Meldungen bleiben im event_reports-Speicher des Servers und werden ueber Element Admin gesichtet - Draupnir bekommt dafuer BEWUSST keine Server-Admin-Rechte. Ohne diesen Text sieht der Melder nach dem Absenden gar nichts und weiss nicht, ob und an wen die Meldung geht (#0045).",
|
||||
"report_event": {
|
||||
"admin_message_md": "Deine Meldung ist bei der Serveradministration eingegangen und wird gesichtet.\n\nFuer Rueckfragen oder wenn es dringend ist, schreib bitte direkt an [@sorb:axion1337.chat](https://matrix.to/#/@sorb:axion1337.chat)."
|
||||
},
|
||||
"branding": {
|
||||
"auth_header_logo_url": "vector-icons/512.png",
|
||||
"logo_link_url": "https://axion1337.chat"
|
||||
},
|
||||
"default_theme": "aXion1337 Dark",
|
||||
"show_labs_settings": true,
|
||||
"features": {
|
||||
"feature_qr_code_login": true
|
||||
"feature_qr_code_login": true,
|
||||
"feature_new_room_list": true,
|
||||
"feature_video_rooms": true,
|
||||
"feature_group_calls": true,
|
||||
"feature_element_call_video_rooms": true,
|
||||
"feature_new_room_decoration_ui": true
|
||||
},
|
||||
"element_call": {
|
||||
"use_exclusively": true
|
||||
},
|
||||
"setting_defaults": {
|
||||
"feature_group_calls": true,
|
||||
"custom_themes": [
|
||||
{
|
||||
"name": "aXion1337 Dark",
|
||||
"name": "aXion1337 Dark true",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#ffaf0f",
|
||||
"primary-color": "#ffaf0f",
|
||||
"secondary-color": "#ffaf0f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Deep Purple",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#6503b3",
|
||||
"primary-color": "#368bd6",
|
||||
"warning-color": "#b30356",
|
||||
"sidebar-color": "#15171B",
|
||||
"roomlist-background-color": "#22262E",
|
||||
"roomlist-text-color": "#A1B2D1",
|
||||
"roomlist-text-secondary-color": "#EDF3FF",
|
||||
"roomlist-highlights-color": "#343A46",
|
||||
"roomlist-separator-color": "#a1b2d1",
|
||||
"timeline-background-color": "#181b21",
|
||||
"timeline-text-color": "#EDF3FF",
|
||||
"timeline-text-secondary-color": "#A1B2D1",
|
||||
"timeline-highlights-color": "#22262E"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Discord Dark",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#747ff4",
|
||||
"accent": "#747ff4",
|
||||
"primary-color": "#00aff4",
|
||||
"warning-color": "#faa81ad9",
|
||||
"alert": "#faa81ad9",
|
||||
"sidebar-color": "#202225",
|
||||
"roomlist-background-color": "#2f3136",
|
||||
"roomlist-text-color": "#dcddde",
|
||||
"roomlist-text-secondary-color": "#8e9297",
|
||||
"roomlist-highlights-color": "#4f545c52",
|
||||
"roomlist-separator-color": "#40444b",
|
||||
"timeline-background-color": "#36393f",
|
||||
"timeline-text-color": "#dcddde",
|
||||
"secondary-content": "#dcddde",
|
||||
"tertiary-content": "#dcddde",
|
||||
"timeline-text-secondary-color": "#b9bbbe",
|
||||
"timeline-highlights-color": "#04040512",
|
||||
"reaction-row-button-selected-bg-color": "#4752c4",
|
||||
"menu-selected-color": "#4752c4",
|
||||
"focus-bg-color": "#4752c4",
|
||||
"room-highlight-color": "#4752c4",
|
||||
"other-user-pill-bg-color": "#4752c4",
|
||||
"togglesw-off-color": "#72767d"
|
||||
},
|
||||
"compound": {
|
||||
"--cpd-color-theme-bg": "#0019ff",
|
||||
"--cpd-color-bg-canvas-default": "#2f3136",
|
||||
"--cpd-color-bg-subtle-secondary": "#2f3136",
|
||||
"--cpd-color-bg-subtle-primary": "#4f545c52",
|
||||
"--cpd-color-bg-action-primary-rest": "#dcddde",
|
||||
"--cpd-color-bg-action-secondary-rest": "#2f3136",
|
||||
"--cpd-color-bg-critical-primary": "#fd3f3c",
|
||||
"--cpd-color-bg-critical-subtle": "#745862",
|
||||
"--cpd-color-bg-critical-hovered": "#fd3f3c",
|
||||
"--cpd-color-bg-accent-rest": "#4cb387",
|
||||
"--cpd-color-text-primary": "#dcddde",
|
||||
"--cpd-color-text-secondary": "#b9bbbe",
|
||||
"--cpd-color-text-action-accent": "#b9bbbe",
|
||||
"--cpd-color-text-critical-primary": "#fd3f3c",
|
||||
"--cpd-color-text-success-primary": "#4cb387",
|
||||
"--cpd-color-icon-primary": "#dcddde",
|
||||
"--cpd-color-icon-secondary": "#dcddde",
|
||||
"--cpd-color-icon-tertiary": "#a7a0a7",
|
||||
"--cpd-color-icon-accent-tertiary": "#4cb387",
|
||||
"--cpd-color-border-interactive-primary": "#5d6064",
|
||||
"--cpd-color-border-interactive-secondary": "#5d6064",
|
||||
"--cpd-color-border-critical-primary": "#fd3f3c",
|
||||
"--cpd-color-border-success-subtle": "#4cb387"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Electric Blue",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#3596fc",
|
||||
"primary-color": "#368bd6",
|
||||
"warning-color": "#ff4b55",
|
||||
"sidebar-color": "#27303a",
|
||||
"roomlist-background-color": "#f3f8fd",
|
||||
"roomlist-text-color": "#2e2f32",
|
||||
"roomlist-text-secondary-color": "#61708b",
|
||||
"roomlist-highlights-color": "#ffffff",
|
||||
"roomlist-separator-color": "#e3e8f0",
|
||||
"timeline-background-color": "#ffffff",
|
||||
"timeline-text-color": "#2e2f32",
|
||||
"timeline-text-secondary-color": "#61708b",
|
||||
"timeline-highlights-color": "#f3f8fd",
|
||||
"username-colors": ["#ff0000", "#ff7f00", "#ffff00", "#00ff00", "#0000ff", "#4b0082", "#9400d3", "#ff1493"],
|
||||
"avatar-background-colors": ["#cc0000", "#cc6600", "#cccc00", "#00cc00", "#0000cc", "#3b0066", "#7a00b3", "#cc1077"]
|
||||
},
|
||||
"compound": {
|
||||
"--cpd-color-icon-accent-tertiary": "var(--cpd-color-blue-800)",
|
||||
"--cpd-color-text-action-accent": "var(--cpd-color-blue-900)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Everforest dark hard",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#a7c080",
|
||||
"primary-color": "#a7c080",
|
||||
"warning-color": "#e67e80",
|
||||
"sidebar-color": "#323d43",
|
||||
"roomlist-background-color": "#2f383e",
|
||||
"roomlist-text-color": "#d3c6aa",
|
||||
"roomlist-text-secondary-color": "#d3c6aa",
|
||||
"roomlist-highlights-color": "#4b565c",
|
||||
"roomlist-separator-color": "#4b565c",
|
||||
"timeline-background-color": "#2b3339",
|
||||
"timeline-text-color": "#d3c6aa",
|
||||
"secondary-content": "#d3c6aa",
|
||||
"tertiary-content": "#d3c6aa",
|
||||
"timeline-text-secondary-color": "#a7c080",
|
||||
"timeline-highlights-color": "#4b565c",
|
||||
"reaction-row-button-selected-bg-color": "#4b565c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aXion1337 Dark",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#bd93f9",
|
||||
"primary-color": "#fe8019",
|
||||
"warning-color": "#fb4934",
|
||||
"sidebar-color": "#282828",
|
||||
"roomlist-background-color": "#1d2021",
|
||||
"roomlist-text-color": "#a89984",
|
||||
"roomlist-text-secondary-color": "#00ff00",
|
||||
"roomlist-highlights-color": "#00000030",
|
||||
"roomlist-separator-color": "#4d4d4d90",
|
||||
"timeline-background-color": "#282828",
|
||||
"timeline-text-color": "#ebdbb2",
|
||||
"secondary-content": "#928374",
|
||||
"tertiary-content": "#928374",
|
||||
"quinary-content": "#504945",
|
||||
"timeline-text-secondary-color": "#a89984",
|
||||
"timeline-highlights-color": "#00000030",
|
||||
"reaction-row-button-selected-bg-color": "#689d6a",
|
||||
"menu-selected-color": "#504945",
|
||||
"icon-button-color": "#928374",
|
||||
"accent": "#689d6a",
|
||||
"alert": "#cc241d",
|
||||
"username-colors": [
|
||||
"#cc241d",
|
||||
"#98971a",
|
||||
"#d79921",
|
||||
"#458588",
|
||||
"#b16286",
|
||||
"#689d6a",
|
||||
"#a89984",
|
||||
"#d65d0e"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "aXion1337 Light",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#8f3f71",
|
||||
"primary-color": "#af3a03",
|
||||
"warning-color": "#9d0006",
|
||||
"sidebar-color": "#ebdbb2",
|
||||
"roomlist-background-color": "#f2e5bc",
|
||||
"roomlist-text-color": "#665c54",
|
||||
"roomlist-text-secondary-color": "#427b58",
|
||||
"roomlist-highlights-color": "#00000012",
|
||||
"roomlist-separator-color": "#bdae9330",
|
||||
"timeline-background-color": "#fbf1c7",
|
||||
"timeline-text-color": "#3c3836",
|
||||
"secondary-content": "#7c6f64",
|
||||
"tertiary-content": "#7c6f64",
|
||||
"quinary-content": "#d5c4a1",
|
||||
"timeline-text-secondary-color": "#665c54",
|
||||
"timeline-highlights-color": "#00000012",
|
||||
"reaction-row-button-selected-bg-color": "#8ec07c",
|
||||
"menu-selected-color": "#d5c4a1",
|
||||
"icon-button-color": "#7c6f64",
|
||||
"accent": "#427b58",
|
||||
"alert": "#9d0006",
|
||||
"username-colors": [
|
||||
"#9d0006",
|
||||
"#79740e",
|
||||
"#b57614",
|
||||
"#076678",
|
||||
"#8f3f71",
|
||||
"#427b58",
|
||||
"#665c54",
|
||||
"#af3a03"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Ocean Depths",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#2d8b8b",
|
||||
"accent": "#2d8b8b",
|
||||
"primary-color": "#a8dadc",
|
||||
"warning-color": "#457b9d",
|
||||
"alert": "#70555a",
|
||||
"sidebar-color": "#e3ebe0",
|
||||
"roomlist-background-color": "#eaf2e7",
|
||||
"roomlist-text-color": "#1a2332",
|
||||
"roomlist-text-secondary-color": "#656e74",
|
||||
"roomlist-highlights-color": "#00000010",
|
||||
"roomlist-separator-color": "#0000001f",
|
||||
"timeline-background-color": "#f1faee",
|
||||
"timeline-text-color": "#1a2332",
|
||||
"secondary-content": "#656e74",
|
||||
"tertiary-content": "#656e74",
|
||||
"timeline-text-secondary-color": "#656e74",
|
||||
"timeline-highlights-color": "#00000010",
|
||||
"reaction-row-button-selected-bg-color": "#99c8c1",
|
||||
"menu-selected-color": "#b6d9d0",
|
||||
"focus-bg-color": "#b6d9d0",
|
||||
"room-highlight-color": "#b6d9d0",
|
||||
"other-user-pill-bg-color": "#d4ede7",
|
||||
"icon-button-color": "#656e74",
|
||||
"username-colors": [
|
||||
"#2d8b8b",
|
||||
"#a8dadc",
|
||||
"#457b9d",
|
||||
"#276c70",
|
||||
"#7da3a9",
|
||||
"#365c78",
|
||||
"#6ab2b4",
|
||||
"#76aabc"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Sunset Boulevard",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#e76f51",
|
||||
"accent": "#e76f51",
|
||||
"primary-color": "#f4a261",
|
||||
"warning-color": "#e9c46a",
|
||||
"alert": "#d6453a",
|
||||
"sidebar-color": "#1b323c",
|
||||
"roomlist-background-color": "#213c47",
|
||||
"roomlist-text-color": "#f6efe6",
|
||||
"roomlist-text-secondary-color": "#adb4b3",
|
||||
"roomlist-highlights-color": "#ffffff14",
|
||||
"roomlist-separator-color": "#ffffff26",
|
||||
"timeline-background-color": "#264653",
|
||||
"timeline-text-color": "#f6efe6",
|
||||
"secondary-content": "#adb4b3",
|
||||
"tertiary-content": "#adb4b3",
|
||||
"timeline-text-secondary-color": "#adb4b3",
|
||||
"timeline-highlights-color": "#ffffff14",
|
||||
"reaction-row-button-selected-bg-color": "#7d5852",
|
||||
"menu-selected-color": "#605252",
|
||||
"focus-bg-color": "#605252",
|
||||
"room-highlight-color": "#605252",
|
||||
"other-user-pill-bg-color": "#786b59",
|
||||
"icon-button-color": "#adb4b3",
|
||||
"username-colors": [
|
||||
"#e76f51",
|
||||
"#f4a261",
|
||||
"#e9c46a",
|
||||
"#ec957e",
|
||||
"#f5b989",
|
||||
"#eed395",
|
||||
"#ee8859",
|
||||
"#eeb366"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Forest Canopy",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#2d4a2b",
|
||||
"accent": "#2d4a2b",
|
||||
"primary-color": "#7d8471",
|
||||
"warning-color": "#a4ac86",
|
||||
"alert": "#703126",
|
||||
"sidebar-color": "#ebeae7",
|
||||
"roomlist-background-color": "#f2f2ef",
|
||||
"roomlist-text-color": "#22301f",
|
||||
"roomlist-text-secondary-color": "#6e766a",
|
||||
"roomlist-highlights-color": "#00000010",
|
||||
"roomlist-separator-color": "#0000001f",
|
||||
"timeline-background-color": "#faf9f6",
|
||||
"timeline-text-color": "#22301f",
|
||||
"secondary-content": "#6e766a",
|
||||
"tertiary-content": "#6e766a",
|
||||
"timeline-text-secondary-color": "#6e766a",
|
||||
"timeline-highlights-color": "#00000010",
|
||||
"reaction-row-button-selected-bg-color": "#9eaa9b",
|
||||
"menu-selected-color": "#bcc4b9",
|
||||
"focus-bg-color": "#bcc4b9",
|
||||
"room-highlight-color": "#bcc4b9",
|
||||
"other-user-pill-bg-color": "#c8cac1",
|
||||
"icon-button-color": "#6e766a",
|
||||
"username-colors": [
|
||||
"#2d4a2b",
|
||||
"#7d8471",
|
||||
"#a4ac86",
|
||||
"#2a4227",
|
||||
"#626b58",
|
||||
"#768162",
|
||||
"#55674e",
|
||||
"#90987c"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Modern Minimalist",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#36454f",
|
||||
"accent": "#36454f",
|
||||
"primary-color": "#708090",
|
||||
"warning-color": "#d3d3d3",
|
||||
"alert": "#752e39",
|
||||
"sidebar-color": "#f0f0f0",
|
||||
"roomlist-background-color": "#f7f7f7",
|
||||
"roomlist-text-color": "#1c252b",
|
||||
"roomlist-text-secondary-color": "#6b7175",
|
||||
"roomlist-highlights-color": "#00000010",
|
||||
"roomlist-separator-color": "#0000001f",
|
||||
"timeline-background-color": "#ffffff",
|
||||
"timeline-text-color": "#1c252b",
|
||||
"secondary-content": "#6b7175",
|
||||
"tertiary-content": "#6b7175",
|
||||
"timeline-text-secondary-color": "#6b7175",
|
||||
"timeline-highlights-color": "#00000010",
|
||||
"reaction-row-button-selected-bg-color": "#a5abb0",
|
||||
"menu-selected-color": "#c3c7ca",
|
||||
"focus-bg-color": "#c3c7ca",
|
||||
"room-highlight-color": "#c3c7ca",
|
||||
"other-user-pill-bg-color": "#c6ccd3",
|
||||
"icon-button-color": "#6b7175",
|
||||
"username-colors": [
|
||||
"#36454f",
|
||||
"#708090",
|
||||
"#d3d3d3",
|
||||
"#2e3b44",
|
||||
"#576572",
|
||||
"#939698",
|
||||
"#536270",
|
||||
"#a2aab2"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Golden Hour",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#f4a900",
|
||||
"accent": "#f4a900",
|
||||
"primary-color": "#c1666b",
|
||||
"warning-color": "#d4b896",
|
||||
"alert": "#dd650e",
|
||||
"sidebar-color": "#352e2a",
|
||||
"roomlist-background-color": "#403732",
|
||||
"roomlist-text-color": "#f7f0e6",
|
||||
"roomlist-text-secondary-color": "#bab2aa",
|
||||
"roomlist-highlights-color": "#ffffff14",
|
||||
"roomlist-separator-color": "#ffffff26",
|
||||
"timeline-background-color": "#4a403a",
|
||||
"timeline-text-color": "#f7f0e6",
|
||||
"secondary-content": "#bab2aa",
|
||||
"tertiary-content": "#bab2aa",
|
||||
"timeline-text-secondary-color": "#bab2aa",
|
||||
"timeline-highlights-color": "#ffffff14",
|
||||
"reaction-row-button-selected-bg-color": "#966f20",
|
||||
"menu-selected-color": "#7d6029",
|
||||
"focus-bg-color": "#7d6029",
|
||||
"room-highlight-color": "#7d6029",
|
||||
"other-user-pill-bg-color": "#7a4f4e",
|
||||
"icon-button-color": "#bab2aa",
|
||||
"username-colors": [
|
||||
"#f4a900",
|
||||
"#c1666b",
|
||||
"#d4b896",
|
||||
"#f5be45",
|
||||
"#d18f90",
|
||||
"#e0ccb2",
|
||||
"#da8836",
|
||||
"#ca8f80"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Arctic Frost",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#4a6fa5",
|
||||
"accent": "#4a6fa5",
|
||||
"primary-color": "#d4e4f7",
|
||||
"warning-color": "#c0c0c0",
|
||||
"alert": "#804569",
|
||||
"sidebar-color": "#ebebeb",
|
||||
"roomlist-background-color": "#f2f2f2",
|
||||
"roomlist-text-color": "#1e2c40",
|
||||
"roomlist-text-secondary-color": "#6b7481",
|
||||
"roomlist-highlights-color": "#00000010",
|
||||
"roomlist-separator-color": "#0000001f",
|
||||
"timeline-background-color": "#fafafa",
|
||||
"timeline-text-color": "#1e2c40",
|
||||
"secondary-content": "#6b7481",
|
||||
"tertiary-content": "#6b7481",
|
||||
"timeline-text-secondary-color": "#6b7481",
|
||||
"timeline-highlights-color": "#00000010",
|
||||
"reaction-row-button-selected-bg-color": "#abbbd4",
|
||||
"menu-selected-color": "#c5d0e0",
|
||||
"focus-bg-color": "#c5d0e0",
|
||||
"room-highlight-color": "#c5d0e0",
|
||||
"other-user-pill-bg-color": "#ebf1f9",
|
||||
"icon-button-color": "#6b7481",
|
||||
"username-colors": [
|
||||
"#4a6fa5",
|
||||
"#d4e4f7",
|
||||
"#c0c0c0",
|
||||
"#3d5b87",
|
||||
"#9dadc0",
|
||||
"#878c93",
|
||||
"#8faace",
|
||||
"#cad2dc"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Desert Rose",
|
||||
"is_dark": true,
|
||||
"colors": {
|
||||
"accent-color": "#d4a5a5",
|
||||
"accent": "#d4a5a5",
|
||||
"primary-color": "#b87d6d",
|
||||
"warning-color": "#e8d5c4",
|
||||
"alert": "#cb6369",
|
||||
"sidebar-color": "#432132",
|
||||
"roomlist-background-color": "#50283c",
|
||||
"roomlist-text-color": "#f6ece4",
|
||||
"roomlist-text-secondary-color": "#c0aaad",
|
||||
"roomlist-highlights-color": "#ffffff14",
|
||||
"roomlist-separator-color": "#ffffff26",
|
||||
"timeline-background-color": "#5d2e46",
|
||||
"timeline-text-color": "#f6ece4",
|
||||
"secondary-content": "#c0aaad",
|
||||
"tertiary-content": "#c0aaad",
|
||||
"timeline-text-secondary-color": "#c0aaad",
|
||||
"timeline-highlights-color": "#ffffff14",
|
||||
"reaction-row-button-selected-bg-color": "#936471",
|
||||
"menu-selected-color": "#815262",
|
||||
"focus-bg-color": "#815262",
|
||||
"room-highlight-color": "#815262",
|
||||
"other-user-pill-bg-color": "#814e56",
|
||||
"icon-button-color": "#c0aaad",
|
||||
"username-colors": [
|
||||
"#d4a5a5",
|
||||
"#b87d6d",
|
||||
"#e8d5c4",
|
||||
"#debab8",
|
||||
"#cb9e91",
|
||||
"#edddcf",
|
||||
"#c69189",
|
||||
"#d0a998"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Tech Innovation",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#0066ff",
|
||||
"accent": "#0066ff",
|
||||
"primary-color": "#00ffff",
|
||||
"warning-color": "#1e1e1e",
|
||||
"alert": "#57409a",
|
||||
"sidebar-color": "#f0f0f0",
|
||||
"roomlist-background-color": "#f7f7f7",
|
||||
"roomlist-text-color": "#1e1e1e",
|
||||
"roomlist-text-secondary-color": "#6d6d6d",
|
||||
"roomlist-highlights-color": "#00000010",
|
||||
"roomlist-separator-color": "#0000001f",
|
||||
"timeline-background-color": "#ffffff",
|
||||
"timeline-text-color": "#1e1e1e",
|
||||
"secondary-content": "#6d6d6d",
|
||||
"tertiary-content": "#6d6d6d",
|
||||
"timeline-text-secondary-color": "#6d6d6d",
|
||||
"timeline-highlights-color": "#00000010",
|
||||
"reaction-row-button-selected-bg-color": "#8cbaff",
|
||||
"menu-selected-color": "#b2d1ff",
|
||||
"focus-bg-color": "#b2d1ff",
|
||||
"room-highlight-color": "#b2d1ff",
|
||||
"other-user-pill-bg-color": "#99ffff",
|
||||
"icon-button-color": "#6d6d6d",
|
||||
"username-colors": [
|
||||
"#0066ff",
|
||||
"#00ffff",
|
||||
"#1e1e1e",
|
||||
"#0950bc",
|
||||
"#09bcbc",
|
||||
"#1e1e1e",
|
||||
"#00b2ff",
|
||||
"#0f8e8e"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Botanical Garden",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#4a7c59",
|
||||
"accent": "#4a7c59",
|
||||
"primary-color": "#f9a620",
|
||||
"warning-color": "#b7472a",
|
||||
"alert": "#804c3f",
|
||||
"sidebar-color": "#e6e4df",
|
||||
"roomlist-background-color": "#eeece6",
|
||||
"roomlist-text-color": "#22331f",
|
||||
"roomlist-text-secondary-color": "#6c7667",
|
||||
"roomlist-highlights-color": "#00000010",
|
||||
"roomlist-separator-color": "#0000001f",
|
||||
"timeline-background-color": "#f5f3ed",
|
||||
"timeline-text-color": "#22331f",
|
||||
"secondary-content": "#6c7667",
|
||||
"tertiary-content": "#6c7667",
|
||||
"timeline-text-secondary-color": "#6c7667",
|
||||
"timeline-highlights-color": "#00000010",
|
||||
"reaction-row-button-selected-bg-color": "#a8bdaa",
|
||||
"menu-selected-color": "#c2cfc1",
|
||||
"focus-bg-color": "#c2cfc1",
|
||||
"room-highlight-color": "#c2cfc1",
|
||||
"other-user-pill-bg-color": "#f7d49b",
|
||||
"icon-button-color": "#6c7667",
|
||||
"username-colors": [
|
||||
"#4a7c59",
|
||||
"#f9a620",
|
||||
"#b7472a",
|
||||
"#3e6648",
|
||||
"#b88420",
|
||||
"#834026",
|
||||
"#a2913c",
|
||||
"#d87625"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Midnight Galaxy",
|
||||
"is_dark": false,
|
||||
"colors": {
|
||||
"accent-color": "#2b1e3e",
|
||||
"accent": "#2b1e3e",
|
||||
"primary-color": "#4a4e8f",
|
||||
"warning-color": "#a490c2",
|
||||
"alert": "#6e1930",
|
||||
"sidebar-color": "#d8d8eb",
|
||||
"roomlist-background-color": "#dfdff2",
|
||||
"roomlist-text-color": "#241a34",
|
||||
"roomlist-text-secondary-color": "#686179",
|
||||
"roomlist-highlights-color": "#00000010",
|
||||
"roomlist-separator-color": "#0000001f",
|
||||
"timeline-background-color": "#e6e6fa",
|
||||
"timeline-text-color": "#241a34",
|
||||
"secondary-content": "#686179",
|
||||
"tertiary-content": "#686179",
|
||||
"timeline-text-secondary-color": "#686179",
|
||||
"timeline-highlights-color": "#00000010",
|
||||
"reaction-row-button-selected-bg-color": "#928ca5",
|
||||
"menu-selected-color": "#aeaac2",
|
||||
"focus-bg-color": "#aeaac2",
|
||||
"room-highlight-color": "#aeaac2",
|
||||
"other-user-pill-bg-color": "#a8a9cf",
|
||||
"icon-button-color": "#686179",
|
||||
"username-colors": [
|
||||
"#2b1e3e",
|
||||
"#4a4e8f",
|
||||
"#a490c2",
|
||||
"#291d3b",
|
||||
"#3f3e74",
|
||||
"#776790",
|
||||
"#3a3666",
|
||||
"#776fa8"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,19 @@ metadata:
|
||||
name: ess-mas-values-secret
|
||||
namespace: matrix
|
||||
stringData:
|
||||
values.yaml: ENC[AES256_GCM,data:K692tDSLoftfKmsd4nIc8BzJw9CwXkOc4qSGTw78qF7L7UolTlH456WYeuwXiajDHmsVJCLhxDjCAhGyYUgbEC1mwuX1ZpkJefJkXvkGtQqnVbpS5MLYWt2SMQo+Yuz5L3Qxkop8Y02/U8jFxu5w7LQT71aAJphI1Po6r0wWjCIP0pDLTSUXd5Tk4eLq7o1a8nCrkJxSDQc95sRPTlZ/cfVhBE4xDzDK/CD/Yajv6yUceqUr7j7aBHOlg82+y0HU8xY44B/NtAzphJJf706z0X80Q5Dboeks5Rj9Na0lld5hwcOQPocYO28Tb6RDYOkNOQUELZKoc64JcpYJNyUkxfJl0hjtC1TmtAy1ZljLytnbnc9abNnHWgKcBt9TCcLIJJbZAI2snSHON8uNYpf7+erXx0wHVGxL/WZWEiFlAhnTfNx/Dqr2l0tKTjzRgibSpeA9rBmEm1/2PnJjexRpyewbS2MASGy2C1lfqaAPfjxf,iv:tguaH5u/67ubPSzhrhk3QMkpsE3flvrEZf50U3bcXvc=,tag:KY3T9RcV9KVBKb1SKXCjmw==,type:str]
|
||||
values.yaml: ENC[AES256_GCM,data:jKArnf1jeZwgTcCoktZozMHJ8wBulzvI90G5yaY800E0wA0Fel14DDVpJ8u1kyRXKdT3A3YGGdDExuEEC5ZWfU+o6GGlkpLzJKu9MzQyDSsU56nrkn+QB90Ky5m3fFipoINaRjlycafuY2JWLQfdZH2is5phLTRW3vrZtLdnujFqkWKoAISPST3IaN4Ked2BxC/ZzrgTK46PAale1JC2vbxFAPtNTb2Qf0sZ4SGGJfO7NYumUtIc5Lq51x5gHHChRDmi2vBD4/EhPIglnodTW3opUOVZ8V/DIt0zev/10PNvn7zoWrCFRgWHcl1wkXL8RKwJohGf8u00Lom7D/YWaXCq4P6/ykMMLyf19MvMNDb0NJwhdPGnjZExkLmFYRL1kfH6yQVijv8Oj3ySVcvIISau9FWxPHQljhRIFTlpAjUPwZFD/mvg4nf9li5dFdU3RgOx9vgZayb5lM7UhP2ubvSvZqaYRrgBipx3jyNoj6CQ3/fhmpDQB2nOxwQx6OLkAgBDm87Pnxv40U+hO40DmLPtuv5h3KVwzf0Sagx44OevkEeNE1iYMgdWLk4In7Mj1gQe0jRBOE0Ys9sVJ3YiKqQEi4Ul8PlfrT+SFrbI/iZ+Q7NjyfiXLYIAvrAU2DW2RzVA6lO5poZ6lP3BhnA3LzolFgPZh1sE21U479fGY+aZwkdwpQCTAl126/CeN1f6SZXjfYUEBl9lOEL1kPZ0ujyt6HjuvzWJM3fMw+F6MkXayrN8KUBoln8Vn1ma1wxYhvRc8XjAUhU8/vrwTFzGK9b9mg/qLGB1G0CJN8ZV/RHEM0JMGW73Hqg/pYs51GmqQfcZGL4lUFKcdQwjEeWA32A4Eb69GZlRtnhtofgHjFTjSlOChMbBzRQFXV4pOCvc2rO4nPYu8FT6yIYmHaKezN2sVWdZ0dz3NUFZD8nHD62EcTnpYAPvtIeDuyL0dqnsiNeTfeYvWZh+562iQkPiIHSUKvwffrC/EnTEHwi4IBHjEYxGv/CQYcCU2BXw/vKKCJVVcZ5FB7l6PbofreruKxyDof5GWjSehhddYj2MItt6wm2G6tLrIm9zBW5QQVsMc+RSBR5XnglyVFsRXCdWbFfKdIOTBRITH+qpgKLozeh4cVGGSFao4QSzgLgIoEd8xUx9sc8tf65Wkb0Z/qNGRKhBOuwzSYKZSlJF9BvAip6mb9befEnGOOkz6NNWi9/mItLochsKru+V/WhjEykVdHRkxZy5uj4o7hyiRlkCax6e2oSHshH/0u0cv9gp4cNH07JFHiqnrr/OzSizyMHelQF6WLoQXliLzxhNdqiSTsFyxn7DfZLjUokJBk7HVKctO7Hg+PeOuQEXydhJAUhxSds4NQw2Gc3ArKF1NK0F2kjqSzNgtuk2MYZ5nLIqM4inkulWuAT3sZKO05k0kbV+gZftI1khKJ4EPD7+X3sOYYsuxYsNPP8afzQkNEpjKi70AI38i9CuZlZFkmaQ8XJpdfQqXYMecy0khWxTDeLP8qa4P6L8H95py5+fhEih/VkFJVYmeJdYbiz7/nPUh7kTKqE3/bCjj0y6TfrdJvzMm8FTCtQRglO5feUg1SKrEckL8VEvaTawDMHI4psuiCukFoDhs0YauAVYUIfYfzCj+KCp3yn09huCl0hZ79PMO5ddHOsLI+t6M16MXAEbQQzXmfVd008RG9/a1WNzYuc+oEq/hgQ/PtlaRRsLqRDWBBR97/MpedpE+cVAZoJs/pyiGepr+wC5mP8Vg1aRUHnetoARFX+cquqJ0N1VbSUE0QxmmJ/mCQTumY1LtRG1nWgw7NuJ/N7wXSXUkwVjLulsZGI6Cc1xt8xrUfgSky8EDR0zRd2ogPAN+Nm5talaHxxnmDau3wMHFufrsvLy8+pqqJHY/PEIBgf/8yZGk9PwLV4FkeeqAelVyYnGXDdqpsYI8WtgKOIr52TAoq4hKSpNGCJsT634nSP8ZNlZBrWpnrdYvNiWe7tqS+hIz+uY53baYNM8xZiRtvqSL9S3JUsqno49B7DreRCLtypKCSYjQlFFgPt6TlaN9rVWof1mSOtKTQnyOn0zlVgdO6+PB0wN4JcwYdYn5TG55/EE2BDZCceDMt9tryUGWmP6Yvhn0T3JD9mcRmeQxH9O1nipuWuPWQek,iv:YhS6z4F2E7EvhpY2A5rN9mnnllmiieDCUQQEBykJxUw=,tag:Z5i5cJ77CuxMt2ChVIYNaw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
enc: |
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJYUEvbDF0azNsM0RSS3pZ
|
||||
eG1sNEZKOXAzUVRZWWVTbjlpazJqaVpBRWtBCkE4SW9qVVhxa1dKSXhXV2pSN0lK
|
||||
VVZET0E2bWZSclkvSzlqdlRxcjVNOEEKLS0tIEZtdm9xRU81WXpkTGQ1UW5HZUl2
|
||||
UERlYmtRc21PSnFGNjFkZFVVcm9nUTgKuxWcqg1MRWq7Yhp61lHHKPZsdiNeHXSd
|
||||
72OCNMaUiz58kTKt+tpthUxXlGUlf3y0VA1/KigeA+L5Z2bk/bdMmw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsWDNpQm90aHZLR1MwendM
|
||||
RC8rTE4rS3laK281THE2Mnp4b2hTdjhQVjFFClRGTExuWVFNVGZGTURWNyt1Qkxy
|
||||
YnBEaytBTjJqS25zeG5rczVLR256OEUKLS0tIEdBdHNsWit6MjlVUDVSWmZJRjJM
|
||||
NGN1S3hpNThQSERDZGFtRjBaWi9mYkkKSiRfp0qHxQ+CAKhAjJRhnHl30iteBQuS
|
||||
Ha8G0sJNHDk/+vg4/3LR+D1hv2i6vbI/Mo0W85GiCpGBu0hajTxgbQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-04-22T13:49:01Z"
|
||||
mac: ENC[AES256_GCM,data:wHDBVBTvNycXTKTVBK1MNrGg2C0XufPpNTx9ksp1V5rtfcPvZNbiw8aUcjHt6hNtT3tPPmUt82jwzAbAm3hnPsAVfnue+bB2BZ91AI11tcqr4o+l3djeJODntF5d6TfdIWchtwMisy7bBMtLPS4Vs++JPbP90p/CtCdOxPwh1CQ=,iv:CQ24u8xSpzG3QU4JzzpZIx5cBrMH5RxVPl/S6gLylxU=,tag:f8FdKvtyxz8ks0gV6ct9iQ==,type:str]
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.12.2
|
||||
lastmodified: "2026-08-11T10:29:33Z"
|
||||
mac: ENC[AES256_GCM,data:TPCj47C4tHcT71r4Y4BsTCFwRnj3/olDJKjpBW6cXUsV4ee10cmITt7NKoHXxkuux62VPMuL5SMv1Y0pETxVFVpXxIAvonedy/VpZkPbA605rQ7G0lNzcz/NRJVQt0mq5KKIhRsDtovRH8SD6/D3CjATNzvP77nfEaJZclvxSik=,iv:A6Y3ZdMYuB0qGTdJv3X4mr9/tva/7HHqzYRFvrorE3E=,tag:bhXI97dA2KSyZcF66R1nqw==,type:str]
|
||||
version: 3.13.3
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ess-mas-custom-secrets
|
||||
namespace: matrix
|
||||
stringData:
|
||||
values.yaml: ENC[AES256_GCM,data:Vk5TvaBui8gK6ftcRvZA8aEmw+QmWhn6pp9/2T5ndw8YPiggAtpqZszms5yoOxPK2Jy+GAVZ3M1whWFrdqy2WPjCAVEKlg5i7qeC1wKfgjMV2txWRlQjYAu2n4IYxi1aOaFcqzxLGYclAlQh6J0PmSYKtbWZO0nsjjcbXt9AXRgmjjZnIcNEBcQg9DoSew4MiHr5BQ0dsN0hJx96UhtTAVwssDgE1eKlTz2K/VD/xz3Odd/WuFKjThpa4E3p66Azc+4WNuQpxFejUgEg5ZzMvup8GGEzH5L3jBtxdBNmNFn6GNwFfYwq/ohWTXafVTj1mEpwTYBcKEtsmks8fl/qzb6QBobOOPqKTD4chRhv72yeYE7rv9x6530TxNXGUd++jxtQm1G+ZOjje05k3x2i9SQv5CNBM2jSyoh2uktoVyaJLpgJMv35VdyLAWY+nNXD4yj0isI=,iv:7GmbMaMDWY+agQC3Xr9SpOzGPyb82RDCSEyPHaWwMhQ=,tag:FUpe4v9BybS0M+8L7yvB4A==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCUWlDYkY4NVpWNFVHb1Bo
|
||||
b3FpR0w5NmttcW9IV1RXWENwTnE2NU9tR0hvCnJyNU9xWEpYZWRXVURtTis3aEp2
|
||||
MmtCZXZXemlKNnZrbW1OL0NlMXlQbzQKLS0tIG81ZXIyd2pGOVlXeEVtRy92RzFp
|
||||
VU9sa0tOdlBqcnZ2VTVaQWpaN3Y3L0UKUel3SRju6rFbMa9Di++EAci8+fsMjx4X
|
||||
X8fM/N4jTHerXOBCT/bnD/Tk/2jAcwg9PC3e906VDUdsIZw+xU+grw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-04-22T10:12:35Z"
|
||||
mac: ENC[AES256_GCM,data:zMIPahMwgSgF9vdEkp7DyECL3s6IuEHWs0JE1G9A+xYmm0mAFYbzMPwgg6oB57gNpX3EaPovHOOvKlTjZVWUbv892mU63GPi4W5kRxsZya5FyC3PxyZlsvMX9U/F8Hwvo9tymbGYpsGGkxv0doRZvTuKDzj1j1+VO0VVcY2i3Gc=,iv:dq6GtQ0vfQWiew6s6gWjPRZPtQ1rD3cO3aQXPDT0Sos=,tag:7UrcQ8Q420HK/vW01SsRkg==,type:str]
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.12.2
|
||||
@@ -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: |
|
||||
@@ -22,4 +38,69 @@ data:
|
||||
- '::1/128'
|
||||
- 'fe80::/10'
|
||||
- 'fc00::/7'
|
||||
max_spider_size: 10M
|
||||
max_spider_size: 10M
|
||||
retention:
|
||||
config: |
|
||||
retention:
|
||||
enabled: true
|
||||
default_policy:
|
||||
min_lifetime: 1d
|
||||
max_lifetime: 1y
|
||||
allowed_lifetime_min: 1d
|
||||
allowed_lifetime_max: 2y
|
||||
purge_jobs:
|
||||
- longest_max_lifetime: 3d
|
||||
interval: 12h
|
||||
- shortest_max_lifetime: 3d
|
||||
longest_max_lifetime: 1w
|
||||
interval: 1d
|
||||
- shortest_max_lifetime: 1w
|
||||
interval: 2d
|
||||
|
||||
media_retention:
|
||||
local_media_lifetime: 365d
|
||||
remote_media_lifetime: 90d
|
||||
|
||||
redaction_retention_period: 7d
|
||||
forgotten_room_retention_period: 28d
|
||||
user_ips_max_age: 90d
|
||||
auto_join:
|
||||
config: |
|
||||
auto_join_rooms:
|
||||
- "#onboarding:axion1337.chat"
|
||||
auto_join_rooms_for_guests: false
|
||||
# Foederation geschlossen (Entscheidung sorb 2026-08-19, management #0060,
|
||||
# ADR-0021). Leere Liste = mit KEINEM Server foederieren; ein Eintrag hier
|
||||
# oeffnet gezielt fuer diese eine Domain.
|
||||
#
|
||||
# Grundlage war eine Messung, keine Meinung: in vier Monaten Betrieb null
|
||||
# Eintraege in `destinations`, null fremde Nutzer, null Raeume mit fremder
|
||||
# Beteiligung - bei gleichzeitig oeffentlich erreichbarer Foederations-API
|
||||
# (die Delegation fuehrt sie ueber 443, 8448 ist ohnehin zu).
|
||||
#
|
||||
# ⚠️ NICHT betroffen und darf es auch nicht sein:
|
||||
# /_matrix/federation/v1/openid/userinfo. Ueber diesen Endpunkt prueft der
|
||||
# MatrixRTC-Authorisation-Service (lk-jwt-service) die OpenID-Tokens, und er
|
||||
# ruft ihn ueber den OEFFENTLICHEN Namen auf - das Deployment hat keine
|
||||
# hostAliases und ClusterFirst-DNS. Synapse bedient ihn ohne
|
||||
# X-Matrix-Signatur (REQUIRE_AUTH=False), die Whitelist greift dort also
|
||||
# nicht. Wer hier auf Pfad-Ebene sperrt statt ueber diese Liste, legt die
|
||||
# Gruppen-Calls lahm - genau der mrtc-Ausfall, nur mit anderer Ursache.
|
||||
federation:
|
||||
config: |
|
||||
federation_domain_whitelist: []
|
||||
room_publish:
|
||||
config: |
|
||||
room_list_publication_rules:
|
||||
- user_id: "*"
|
||||
action: allow
|
||||
oembed:
|
||||
config: |
|
||||
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: 1h
|
||||
# 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
|
||||
@@ -43,6 +50,10 @@ spec:
|
||||
enabled: true
|
||||
ingress:
|
||||
host: matrix.axion1337.chat
|
||||
additional:
|
||||
oembed:
|
||||
config: |
|
||||
oembed_enabled: true
|
||||
|
||||
# Matrix Authentication Service – braucht eine Subdomain
|
||||
matrixAuthenticationService:
|
||||
@@ -55,10 +66,86 @@ 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:
|
||||
enabled: true
|
||||
image:
|
||||
registry: rohana.axion1337.de
|
||||
repository: sorb/threadnet-web
|
||||
# v0.6.0-rc.2 = Anschluss an Element Web v1.12.26 (ADR-0022). Ein echter
|
||||
# Merge-Commit statt Cherry-Picks; die Fork-Patches mussten umziehen,
|
||||
# weil Upstream MImageBody.tsx geloescht und den Raumlisten-Inhalt nach
|
||||
# RoomListItemContent ausgelagert hat. Betroffen sind genau die zwei
|
||||
# Stellen, die die Abnahme pruefen muss: die ClamAV-Fehlermeldung im
|
||||
# Bild-Pfad (ImageBodyViewModel.computeErrorLabel) und die
|
||||
# Call-Teilnehmerliste in der Raumliste.
|
||||
# Am 19.08. ausgerollt und nach wenigen Minuten zurueckgenommen: die
|
||||
# Raumliste stuerzte bei jedem Eintrag ab (react-soft-crash), weil in
|
||||
# RoomListItemViewModel.ts eine getValue-Zeile auf den von Upstream
|
||||
# entfernten Labs-Schalter feature_room_list_sections stehenblieb -
|
||||
# Sektionen laufen dort inzwischen ueber RoomList.showSections. Eine
|
||||
# Leiche aus der Merge-Aufloesung, die kein Build fangen konnte:
|
||||
# getValue nimmt einen String und wirft erst zur Laufzeit.
|
||||
# rc.3 = derselbe Merge ohne den Rest, plus ein typecheck-Job, den
|
||||
# docker_web als needs fuehrt: kein Image mehr ohne Typpruefung. Der
|
||||
# web-Job baut nur, webpack wirft Typen weg - tsc hatte den Fehler die
|
||||
# ganze Zeit gemeldet, gefragt hatte ihn niemand. Massstab ist "kein
|
||||
# Fehler ausserhalb von node_modules", weil Upstream v1.12.26 selbst
|
||||
# nicht typrein ist (matrix-js-sdk 42.2.0, in einem sauberen Checkout
|
||||
# gegengeprueft).
|
||||
# v0.6.0 = Abnahme auf rc.3 bestanden (19.08.), derselbe Quellstand
|
||||
# 8ca03fe unter Release-Nummer. Geprueft am laufenden System, nicht nur
|
||||
# am Build: Raumliste laedt, ClamAV blockt beim Senden (ein Scan-Aufruf,
|
||||
# kein Upload), abgewiesene Datei zeigt die Meldung, und das .png wurde
|
||||
# zugestellt, beim Herunterladen abgewiesen und die Meldung gerendert -
|
||||
# damit ist der portierte Bild-Pfad belegt, nicht nur vermutet. Auch die
|
||||
# Call-Teilnehmerliste, der zweite umgezogene Patch, steht richtig drin.
|
||||
# Rueckhebel bleibt der Tag-Revert auf v0.5.4.
|
||||
# v0.5.4 = Sender-Verifikation (threadnet-call fee9866): auf Safari
|
||||
# uebersprang LiveKit den Track-Tausch stumm (sender?.replaceTrack),
|
||||
# das rohe Mikro blieb auf der Leitung. Der Fork prueft und erzwingt
|
||||
# den Tausch jetzt; die Konsole weist den Sendepfad aus.
|
||||
# v0.5.3 = KI-Geraeuschunterdrueckung freigeschaltet (threadnet-call
|
||||
# e3f8a85): Abnahme im Call zu zweit bestanden 2026-08-17. Checkbox +
|
||||
# Regler in den Call-Einstellungen (Audio-Reiter). Rueckhebel bei
|
||||
# Regression: Feature-Tor im Fork schliessen, kein Deployment-Revert.
|
||||
# v0.5.2 = KI-Filter-Anbindung Weg B (threadnet-call df4e5ee): Filter
|
||||
# haengt sich NACH der Publikation an den Mikrofon-Track, eigener
|
||||
# AudioContext nur dort - kein webAudioMix, kein processor-Schluessel in
|
||||
# den Capture-Defaults in irgendeinem Zustand. Tor geschlossen: fuer
|
||||
# alle Nutzer verhaltensgleich mit v0.5.1; Test-Client per zwei
|
||||
# localStorage-Schluesseln. Tor-Oeffnung erst nach Abnahme im Call.
|
||||
# v0.5.1 = Entmuten-Vorfall aus v0.5.0 behoben (threadnet-call dcc8643):
|
||||
# Aus-Pfad der KI-Geraeuschunterdrueckung wieder identisch mit Upstream
|
||||
# (kein processor-Schluessel), Feature hart stillgelegt bis zur
|
||||
# webAudioMix-Entscheidung - neutralisiert auch Clients mit noch
|
||||
# aktivierter Einstellung im localStorage. Regressionstests decken beide
|
||||
# Faelle ab. Abnahme: Call zu zweit nach dem Rollout.
|
||||
# Historie 2026-08-16: v0.5.0 brach das Entmuten beidseitig und wurde v0.5.0 brach das Entmuten - es wurde nie ein
|
||||
# Audio-Track veroeffentlicht (SFU-Log: kein einziges "published"). Erstes
|
||||
# Produktivimage mit dem KI-Filter im Audio-Pfad; v0.4.3 (embedded .7) ist
|
||||
# der letzte Stand, mit dem Calls nachweislich liefen (09.-11.08.).
|
||||
# Ursache offen - siehe #0054. NICHT wieder anheben ohne Call-Test zu zweit.
|
||||
# v0.5.0 = KI-Geraeuschunterdrueckung im Call-Widget (ADR-0018): erste
|
||||
# Funktionserweiterung seit dem Rebrand, daher Minor statt Patch. Das
|
||||
# Image traegt jetzt 23 MB Modell-Assets unter
|
||||
# /widgets/element-call/assets/dfn3/ - sie werden erst beim Einschalten
|
||||
# des Filters geladen, nicht beim Seitenaufruf.
|
||||
# v0.4.0 = Rebrand sichtbar: zentrierte Icons, Markenfarbe #ed4f4c,
|
||||
# About-Attribution unter der Client-Version (ThreadNet-Web#10).
|
||||
# Die Linie beginnt bei v0.3.0, dem ersten kanonischen CI-Build aus
|
||||
# apps/web/Dockerfile - er loeste die Derivat-Images ab, deren
|
||||
# Entrypoint ohne Exec-Bit /config.json still brach (ThreadNet-Web#8).
|
||||
tag: v0.6.0
|
||||
ingress:
|
||||
host: axion1337.chat
|
||||
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ess-element-web-docs
|
||||
namespace: matrix
|
||||
data:
|
||||
# HTML Index Page
|
||||
"index.html": |
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Element Desktop Setup - 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;
|
||||
}
|
||||
.themes {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #667eea;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.themes h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.theme-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.theme-item {
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
border: 1px solid #667eea;
|
||||
}
|
||||
.instructions {
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #0066cc;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin: 15px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.instructions code {
|
||||
background: #f0f0f0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.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>🎨 Element Desktop Setup</h1>
|
||||
<p class="subtitle">Automatische Konfiguration mit Custom Themes für aXion1337.Chat</p>
|
||||
|
||||
<div class="section">
|
||||
<h2>📥 Download Setup-Script</h2>
|
||||
<div class="download-grid">
|
||||
<a href="element-setup-windows.cmd" class="download-card" download>
|
||||
<div class="icon">🪟</div>
|
||||
<div class="name">Windows</div>
|
||||
<div class="desc">.cmd Datei</div>
|
||||
</a>
|
||||
<a href="element-setup-macos.command" class="download-card" download>
|
||||
<div class="icon">🍎</div>
|
||||
<div class="name">macOS</div>
|
||||
<div class="desc">.command Datei</div>
|
||||
</a>
|
||||
<a href="element-setup-linux.sh" class="download-card" download>
|
||||
<div class="icon">🐧</div>
|
||||
<div class="name">Linux</div>
|
||||
<div class="desc">.sh Datei</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<strong>Anleitung:</strong><br><br>
|
||||
<strong>🪟 Windows:</strong> Datei herunterladen → Doppelklick → Script läuft automatisch<br><br>
|
||||
<strong>🍎 macOS:</strong> Datei herunterladen → Doppelklick im Finder → Script läuft automatisch<br><br>
|
||||
<strong>🐧 Linux:</strong><br>
|
||||
<code>chmod +x element-setup-linux.sh</code><br>
|
||||
<code>./element-setup-linux.sh</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>🎨 Verfügbare Themes</h2>
|
||||
<div class="themes">
|
||||
<h3>Automatisch geladen in Element:</h3>
|
||||
<div class="theme-list">
|
||||
<div class="theme-item">aXion1337 Dark</div>
|
||||
<div class="theme-item">Deep Purple</div>
|
||||
<div class="theme-item">Discord Dark</div>
|
||||
<div class="theme-item">Electric Blue</div>
|
||||
<div class="theme-item">Everforest dark hard</div>
|
||||
<div class="theme-item">Gruvbox Dark</div>
|
||||
<div class="theme-item">Wal</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<p>Element wird automatisch installiert und konfiguriert.<br>
|
||||
<small>Bei Fragen oder Problemen: <a href="https://element.io/help">Element Support</a></small></p>
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
|
||||
Automatische Konfiguration und Installation von Element Desktop mit Custom Themes für aXion1337.Chat
|
||||
|
||||
## 🎨 Verfügbare Themes
|
||||
- aXion1337 Dark
|
||||
- Deep Purple
|
||||
- Discord Dark
|
||||
- Electric Blue
|
||||
- Everforest dark hard
|
||||
- Gruvbox Dark
|
||||
- Wal
|
||||
|
||||
## 🪟 Windows
|
||||
Herunterladen: `element-setup-windows.cmd` → Doppelklick
|
||||
|
||||
## 🍎 macOS
|
||||
Herunterladen: `element-setup-macos.command` → Doppelklick im Finder
|
||||
|
||||
## 🐧 Linux
|
||||
```bash
|
||||
chmod +x element-setup-linux.sh
|
||||
./element-setup-linux.sh
|
||||
```
|
||||
|
||||
Support: https://element.io/help
|
||||
|
||||
# Windows Script
|
||||
"element-setup-windows.cmd": |
|
||||
@echo off
|
||||
REM Element Desktop Setup Script for Windows
|
||||
setlocal enabledelayedexpansion
|
||||
echo ========================================
|
||||
echo Element Desktop Konfiguration Setup
|
||||
echo ========================================
|
||||
echo.
|
||||
set APPDATA_PATH=%APPDATA%\Element
|
||||
set CONFIG_FILE=%APPDATA_PATH%\config.json
|
||||
if not exist "%APPDATA_PATH%" (
|
||||
echo Erstelle Element Verzeichnis...
|
||||
mkdir "%APPDATA_PATH%"
|
||||
)
|
||||
echo Erstelle config.json...
|
||||
(
|
||||
echo {
|
||||
echo "configUrl": "https://axion1337.chat/config.json",
|
||||
echo "brand": "aXion1337.Chat",
|
||||
echo "default_theme": "aXion1337 Dark",
|
||||
echo "show_labs_settings": true,
|
||||
echo "features": {
|
||||
echo "feature_qr_code_login": true
|
||||
echo },
|
||||
echo "setting_defaults": {
|
||||
echo "custom_themes": []
|
||||
echo }
|
||||
echo }
|
||||
) > "%CONFIG_FILE%"
|
||||
echo Config erstellt: %CONFIG_FILE%
|
||||
echo.
|
||||
echo Ueberpruefen Sie ob Element Desktop installiert ist...
|
||||
where element >nul 2>nul
|
||||
if %ERRORLEVEL% == 0 (
|
||||
echo Starte Element Desktop...
|
||||
start element
|
||||
timeout /t 2 >nul
|
||||
echo Done!
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
winget list --name "Element" >nul 2>nul
|
||||
if %ERRORLEVEL% == 0 (
|
||||
echo WinGet gefunden. Installiere Element...
|
||||
winget install Element.Element --silent
|
||||
timeout /t 3 >nul
|
||||
start element
|
||||
pause
|
||||
exit /b 0
|
||||
)
|
||||
echo.
|
||||
echo Element Desktop konnte nicht automatisch installiert werden.
|
||||
echo Bitte installiere Element Desktop manuell:
|
||||
echo https://element.io/download
|
||||
echo.
|
||||
pause
|
||||
|
||||
# macOS Script
|
||||
"element-setup-macos.command": |
|
||||
#!/bin/bash
|
||||
echo "========================================"
|
||||
echo "Element Desktop Konfiguration Setup"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
CONFIG_DIR="$HOME/Library/Application Support/Element"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
if [ ! -d "$CONFIG_DIR" ]; then
|
||||
echo "Erstelle Element Verzeichnis..."
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
fi
|
||||
echo "Erstelle config.json..."
|
||||
cat > "$CONFIG_FILE" << 'EOF'
|
||||
{
|
||||
"configUrl": "https://axion1337.chat/config.json",
|
||||
"brand": "aXion1337.Chat",
|
||||
"default_theme": "aXion1337 Dark",
|
||||
"show_labs_settings": true,
|
||||
"features": {
|
||||
"feature_qr_code_login": true
|
||||
},
|
||||
"setting_defaults": {
|
||||
"custom_themes": []
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "Config erstellt: $CONFIG_FILE"
|
||||
echo ""
|
||||
echo "Ueberpruefen Sie ob Element Desktop installiert ist..."
|
||||
if [ -d "/Applications/Element.app" ]; then
|
||||
echo "Element im Applications Folder gefunden. Starte Element..."
|
||||
open -a Element
|
||||
sleep 2
|
||||
exit 0
|
||||
fi
|
||||
if command -v brew &> /dev/null; then
|
||||
echo "Installiere Element uber Homebrew..."
|
||||
brew install element --cask
|
||||
sleep 2
|
||||
open -a Element
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
echo "Homebrew nicht gefunden. Bitte installiere zuerst:"
|
||||
echo "https://brew.sh"
|
||||
echo ""
|
||||
echo "Deine config.json wurde erstellt unter:"
|
||||
echo "$CONFIG_FILE"
|
||||
echo ""
|
||||
fi
|
||||
read -p "Druecke Enter zum Beenden..."
|
||||
|
||||
# Linux Script
|
||||
"element-setup-linux.sh": |
|
||||
#!/bin/bash
|
||||
echo "========================================"
|
||||
echo "Element Desktop Konfiguration Setup"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
CONFIG_DIR="$HOME/.config/Element"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
if [ ! -d "$CONFIG_DIR" ]; then
|
||||
echo "Erstelle Element Verzeichnis..."
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
fi
|
||||
echo "Erstelle config.json..."
|
||||
cat > "$CONFIG_FILE" << 'EOF'
|
||||
{
|
||||
"configUrl": "https://axion1337.chat/config.json",
|
||||
"brand": "aXion1337.Chat",
|
||||
"default_theme": "aXion1337 Dark",
|
||||
"show_labs_settings": true,
|
||||
"features": {
|
||||
"feature_qr_code_login": true
|
||||
},
|
||||
"setting_defaults": {
|
||||
"custom_themes": []
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "Config erstellt: $CONFIG_FILE"
|
||||
echo ""
|
||||
if command -v apt &> /dev/null; then
|
||||
echo "Installiere Element uber apt..."
|
||||
sudo apt update && sudo apt install -y element-desktop
|
||||
element &
|
||||
exit 0
|
||||
fi
|
||||
if command -v dnf &> /dev/null; then
|
||||
echo "Installiere Element uber dnf..."
|
||||
sudo dnf install -y element-desktop
|
||||
element &
|
||||
exit 0
|
||||
fi
|
||||
if command -v pacman &> /dev/null; then
|
||||
echo "Installiere Element uber pacman..."
|
||||
sudo pacman -S --noconfirm element-web
|
||||
element &
|
||||
exit 0
|
||||
fi
|
||||
echo "Element Desktop konnte nicht automatisch installiert werden."
|
||||
echo "Bitte installiere Element Desktop manuell:"
|
||||
echo "Ubuntu/Debian: sudo apt install element-desktop"
|
||||
echo "Fedora/RHEL: sudo dnf install element-desktop"
|
||||
echo "Arch: sudo pacman -S element-web"
|
||||
echo ""
|
||||
echo "Deine config.json wurde erstellt unter:"
|
||||
echo "$CONFIG_FILE"
|
||||
@@ -0,0 +1,93 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: element-web-docs
|
||||
namespace: matrix
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: element-web-docs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: element-web-docs
|
||||
spec:
|
||||
initContainers:
|
||||
- name: copy-files
|
||||
image: busybox:1.36
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
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/
|
||||
cp /config/element-setup-linux.sh /html/docs/setup/
|
||||
chmod 644 /html/docs/setup/*
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: html
|
||||
mountPath: /html
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.26-alpine
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: nginx-conf
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: nginx.conf
|
||||
- name: html
|
||||
mountPath: /usr/share/nginx/html
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: ess-element-web-docs
|
||||
- name: nginx-conf
|
||||
configMap:
|
||||
name: element-web-docs-nginx
|
||||
- name: html
|
||||
emptyDir: {}
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: element-web-docs
|
||||
namespace: matrix
|
||||
spec:
|
||||
selector:
|
||||
app: element-web-docs
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
type: ClusterIP
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: element-web-docs-nginx
|
||||
namespace: matrix
|
||||
data:
|
||||
nginx.conf: |
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location /docs/setup/ {
|
||||
index index.html;
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,108 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
# 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
|
||||
name: matrix-stack
|
||||
namespace: matrix
|
||||
patch: |-
|
||||
- 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
|
||||
- cert-issuer.yaml
|
||||
- matrix-certificates.yaml
|
||||
# Neue Dateien:
|
||||
- custom-configs/synapse-values.yaml
|
||||
- custom-configs/element-values.yaml
|
||||
- custom-configs/mas-secret.yaml
|
||||
- element-web-docs-configmap.yaml
|
||||
- element-web-docs-server.yaml
|
||||
# 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
|
||||
# Monatliche Restore-Probe: spielt die Sicherungen isoliert zurueck (#0030)
|
||||
- restore-drill.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
|
||||
- concierge-bot.yaml
|
||||
# Wiki.js (Plattform-Wiki, ADR-0014, #0048)
|
||||
- wikijs-postgres-secret.yaml # SOPS, von sorb angelegt
|
||||
- wikijs-admin-secret.yaml # SOPS, randomisiert — Bootstrap durch den Konfig-Job
|
||||
- wikijs-oidc-secret.yaml # SOPS, client_id/secret für die OIDC-Strategy
|
||||
- wikijs-git-secret.yaml # SOPS, Git-Storage-PAT nach Gitea (ADR-0015)
|
||||
- wikijs-postgres.yaml
|
||||
- wikijs.yaml
|
||||
- wiki-ingress.yaml
|
||||
- wikijs-config.yaml # Konfig-Job (headless Setup + OIDC + Rollen)
|
||||
- wikijs-postgres-backup.yaml # Nächtliches Borg-Backup der Wiki-DB (#0048)
|
||||
|
||||
# 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:
|
||||
# ⚠️ Bewusst OHNE disableNameSuffixHash: Der Hash im ConfigMap-Namen aendert
|
||||
# sich mit dem Skript, kustomize zieht die Referenz im Deployment nach, und
|
||||
# der Pod startet dadurch von selbst neu. Ohne das haetten wir wieder den
|
||||
# Fall aus gitops#50 - geaenderte Datei im Repo, alter Stand im laufenden
|
||||
# Prozess, und niemand merkt es.
|
||||
- name: concierge-bot-script
|
||||
namespace: matrix
|
||||
files:
|
||||
- concierge-bot.py
|
||||
- name: synapse-clamav-module
|
||||
namespace: matrix
|
||||
files:
|
||||
- clamav_spam_checker.py
|
||||
options:
|
||||
disableNameSuffixHash: true
|
||||
- name: wikijs-config-script
|
||||
namespace: matrix
|
||||
files:
|
||||
- wikijs-config.py
|
||||
options:
|
||||
disableNameSuffixHash: true
|
||||
# Gemeinsame Branding-Assets (eine Quelle). Binärdateien -> kustomize legt sie als
|
||||
# binaryData ab. MIT Namens-Hash: ändert sich ein Asset, zieht der Deployment-Verweis
|
||||
# nach und der Pod startet mit dem neuen Bild neu. Kann später auch in Authentik/Element
|
||||
# gemountet werden, um dieselbe Datei nicht mehrfach zu pflegen.
|
||||
- name: platform-branding
|
||||
namespace: matrix
|
||||
files:
|
||||
- branding/logo.png
|
||||
- branding/alpenglow.jpg
|
||||
- branding/favicon.ico
|
||||
- branding/favicons/favicon-32x32.png
|
||||
- branding/favicons/favicon-16x16.png
|
||||
- branding/favicons/android-chrome-192x192.png
|
||||
- branding/favicons/apple-touch-icon.png
|
||||
- branding/favicons/mstile-150x150.png
|
||||
|
||||
@@ -62,3 +62,16 @@ spec:
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- admin.axion1337.chat
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: turn-axion1337-chat-cert
|
||||
namespace: matrix
|
||||
spec:
|
||||
secretName: turn-axion1337-chat-tls
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- turn.axion1337.chat
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
# 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
|
||||
---
|
||||
# wiki.axion1337.chat (Traefik) -> Wiki.js (ADR-0014, #0048)
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-wikijs
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: wikijs
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: wikijs-config # Konfig-Job darf Wiki.js erreichen
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: http
|
||||
---
|
||||
# Wiki.js -> sein eigener Postgres (nur von Wiki.js-Pods erreichbar)
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: allow-ingress-wikijs-postgres
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: wikijs-postgres
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: wikijs
|
||||
# Nächtliches Backup (wikijs-postgres-backup.yaml) braucht denselben DB-Zugang.
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: wikijs-backup
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: postgres
|
||||
---
|
||||
# Egress: alles bleibt erlaubt AUSSER dem Metadaten-Dienst (management #0088).
|
||||
#
|
||||
# Warum so schmal? Der Lehrbuch-Schnitt (0.0.0.0/0 except RFC1918) haette hier zwei
|
||||
# Dinge zerrissen, beide ueber 10.0.0.3 (CFGMON im privaten Hetzner-Netz): Alloy
|
||||
# schreibt Metriken und Logs dorthin, und der TURN-Rotations-CronJob erreicht Gitea
|
||||
# ueber einen hostAlias auf dieselbe Adresse. Private Netze bleiben deshalb offen.
|
||||
#
|
||||
# 169.254.0.0/16 ist Link-Local; darin liegt bei Hetzner unter 169.254.169.254 der
|
||||
# Metadaten-Dienst, aus jedem Pod unauthentifiziert abfragbar (nachgemessen 2026-08-19:
|
||||
# HTTP 200 mit instance-id, hostname, region, MAC, Netzkonfiguration). userdata und
|
||||
# public-keys waren LEER - es liegen hier also keine Zugangsdaten offen. Der Gewinn ist
|
||||
# entsprechend bescheiden; der Schnitt kostet aber nichts und schliesst die Klasse.
|
||||
#
|
||||
# ACHTUNG beim Erweitern: Sobald eine Egress-Regel fuer einen Pod existiert, gilt fuer
|
||||
# ihn Default-Deny fuer alles NICHT Aufgefuehrte. Die drei Bloecke unten sind daher
|
||||
# Pflicht, nicht Bequemlichkeit - insbesondere DNS: fehlt es, steht alles, und der
|
||||
# Fehler sieht wie ein Anwendungsproblem aus, nicht wie eine Firewall.
|
||||
#
|
||||
# Rollback: diese eine Policy im Namespace loeschen.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: egress-block-metadata
|
||||
namespace: matrix
|
||||
spec:
|
||||
podSelector: {}
|
||||
policyTypes:
|
||||
- Egress
|
||||
egress:
|
||||
# 1. DNS - zuerst, weil ohne DNS nichts geht.
|
||||
- to:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
ports:
|
||||
- protocol: UDP
|
||||
port: 53
|
||||
- protocol: TCP
|
||||
port: 53
|
||||
# 2. Cluster-intern: Pods und Services (inkl. API-Server 10.43.0.1).
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 10.42.0.0/16
|
||||
- ipBlock:
|
||||
cidr: 10.43.0.0/16
|
||||
# 3. Alles uebrige - Foederation, ACME, SMTP, Registries, privates Netz -
|
||||
# ausser Link-Local.
|
||||
- to:
|
||||
- ipBlock:
|
||||
cidr: 0.0.0.0/0
|
||||
except:
|
||||
- 169.254.0.0/16
|
||||
@@ -0,0 +1,121 @@
|
||||
# Monatliche Restore-Probe (#0030): spielt die Borg-Sicherungen in eine Wegwerf-Postgres
|
||||
# IM POD zurueck und besteht nur, wenn wirklich Zeilen ankommen. Die Produktion wird nicht
|
||||
# angefasst - es wird ausschliesslich aus den Repos gelesen.
|
||||
#
|
||||
# Warum automatisch statt "einmal im Quartal dran denken": eine Sicherung, die nie
|
||||
# zurueckgespielt wurde, ist eine Vermutung (#0030) - ein Pruefrhythmus, den niemand
|
||||
# ausfuehrt, ist genau derselbe Fehler eine Ebene hoeher.
|
||||
#
|
||||
# Abgedeckt sind die unersetzlichen Daten: synapse + matrixauthenticationservice und das
|
||||
# Wiki. Authentik ist bewusst nicht Teil des automatischen Laufs (Flows/Provider liegen
|
||||
# als Blueprints deklarativ im Repo, die DB ist also weitgehend reproduzierbar); dafuer
|
||||
# gibt es die Stufe 3 in notfallhandbuch/notfall.sh auf Zuruf.
|
||||
#
|
||||
# Fehlschlag = der Job schlaegt fehl. Alarmierung dazu: threadnet-operating,
|
||||
# monitoring/prometheus/alerts.yml (BackupJobFailed / RestoreDrillStale).
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: restore-drill
|
||||
namespace: matrix
|
||||
spec:
|
||||
# 4. des Monats, 04:20 - deutlich nach den naechtlichen Backups (03:00/03:15/03:30),
|
||||
# damit die Probe den frischen Stand zieht.
|
||||
schedule: "20 4 4 * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: synapse-backup # NetworkPolicy/Egress wie die Backups
|
||||
app.kubernetes.io/component: restore-drill
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: drill
|
||||
image: rohana.axion1337.de/sorb/axion-backup:v2
|
||||
env:
|
||||
- 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
|
||||
command: ["sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
set -eu
|
||||
export BORG_RSH="ssh -i $SSH_PRIVATE_KEY_FILE -o UserKnownHostsFile=$SSH_KNOWN_HOSTS_FILE -o StrictHostKeyChecking=yes"
|
||||
export PGDATA=/scratch/pgdata
|
||||
mkdir -p "$PGDATA" /scratch/sock
|
||||
chown postgres:postgres "$PGDATA" /scratch/sock
|
||||
echo "[drill] Wegwerf-Postgres starten"
|
||||
gosu postgres initdb -U postgres >/dev/null 2>&1
|
||||
gosu postgres pg_ctl -D "$PGDATA" -o "-k /scratch/sock -h ''" -w start >/dev/null
|
||||
# repo:datenbanken
|
||||
for pair in "synapse-backup:synapse matrixauthenticationservice" "wikijs-backup:wiki"; do
|
||||
repo="${pair%%:*}"; dbs="${pair#*:}"
|
||||
export BORG_REPO="ssh://u641795@u641795.your-storagebox.de:23/./${repo}"
|
||||
cd /scratch && rm -rf scratch
|
||||
latest=$(borg list --last 1 --format '{archive}' "$BORG_REPO")
|
||||
echo "[drill] ${repo}: Archiv ${latest}"
|
||||
borg extract "$BORG_REPO::${latest}" scratch/dumps
|
||||
for db in $dbs; do
|
||||
# pg_restore/psql laufen als root und verbinden per Socket als
|
||||
# postgres (initdb setzt lokal 'trust') - sonst scheitert das Lesen
|
||||
# der root-eigenen Dump-Dateien.
|
||||
dropdb -h /scratch/sock -U postgres --if-exists "$db"
|
||||
createdb -h /scratch/sock -U postgres "$db"
|
||||
rc=0
|
||||
pg_restore -h /scratch/sock -U postgres -d "$db" --no-owner \
|
||||
--no-privileges "scratch/dumps/${db}.dump" >/scratch/restore.log 2>&1 || rc=$?
|
||||
[ "$rc" != "0" ] && { echo "[drill] pg_restore Code $rc:"; tail -3 /scratch/restore.log; }
|
||||
psql -h /scratch/sock -U postgres -d "$db" -q -c 'ANALYZE;' >/dev/null 2>&1
|
||||
rows=$(psql -h /scratch/sock -U postgres -d "$db" -tA \
|
||||
-c 'select coalesce(sum(n_live_tup),0) from pg_stat_user_tables;')
|
||||
echo "[drill] ${db}: ${rows} Zeilen"
|
||||
# Der Exitcode von pg_restore ist kein Beweis (es warnt gern) -
|
||||
# gezaehlte Zeilen sind einer.
|
||||
if [ "$rows" -le 0 ]; then
|
||||
echo "[drill] FEHLER: ${db} ist nach dem Restore LEER - Sicherung unbrauchbar!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
done
|
||||
echo "[drill] OK - alle Sicherungen liessen sich zurueckspielen"
|
||||
volumeMounts:
|
||||
- name: ssh-key
|
||||
mountPath: /secrets/ssh
|
||||
readOnly: true
|
||||
- name: known-hosts
|
||||
mountPath: /secrets/known-hosts
|
||||
readOnly: true
|
||||
- name: scratch
|
||||
mountPath: /scratch
|
||||
resources:
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 1Gi
|
||||
volumes:
|
||||
- name: ssh-key
|
||||
secret:
|
||||
secretName: synapse-backup-credentials
|
||||
defaultMode: 0400
|
||||
items:
|
||||
- key: ssh-private-key
|
||||
path: ssh-private-key
|
||||
- name: known-hosts
|
||||
configMap:
|
||||
name: synapse-backup-known-hosts
|
||||
- name: scratch
|
||||
emptyDir:
|
||||
sizeLimit: 4Gi
|
||||
@@ -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
|
||||
@@ -0,0 +1,33 @@
|
||||
# Öffentlicher Zugang zum Wiki.js unter wiki.axion1337.chat (ADR-0014, #0048).
|
||||
# Muster wie apps/authentik/{certificate,ingress}.yaml. DNS-A-Record auf
|
||||
# 49.13.132.245 ist von sorb angelegt. Login läuft nativ über Authentik-OIDC
|
||||
# (kein Forward-Auth) — siehe Guide 10 / #0049.
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: wiki-axion1337-chat-cert
|
||||
namespace: matrix
|
||||
spec:
|
||||
secretName: wiki-axion1337-chat-tls
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- wiki.axion1337.chat
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: wikijs
|
||||
namespace: matrix
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
secretName: wiki-axion1337-chat-tls
|
||||
routes:
|
||||
- match: Host(`wiki.axion1337.chat`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: wikijs
|
||||
port: 3000
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
data:
|
||||
email: ENC[AES256_GCM,data:VXwOHfp9spepzaQ6HrXyeDTITxKWSiEdV3u9pA==,iv:KC6SzHMOELHlFIcM0xW8wXZ+lG61y3arFt8DZMTtm9g=,tag:C5KxglXJzfG86+J4O+6y6g==,type:str]
|
||||
password: ENC[AES256_GCM,data:+r73YasvUGbuEYrcqR1WiVzXunnk7QzTnemu2wtzoQb9T30PA1+Ea/Lap9chSmmR,iv:UT1nDAdoi8jDRG0GN1Tr+ZJbDyHhV0X09HvLxkmOwxQ=,tag:i1+ogn8IjW33hbfKl4t5gQ==,type:str]
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: wikijs-admin-secret
|
||||
namespace: matrix
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKMklFSTZyTTNuREExaW5h
|
||||
ZXFoeS8yMFZvSjFpOXhjQ3JpdmtqNVh4VDN3Ci9jNnd5dFpUdGtlVFFubkRmUjRD
|
||||
WnBEaVVSbEtsakJQMzBJOFI5bDVSR1EKLS0tIEo2NjU0SldVMGg0LzZBaXR2OXFy
|
||||
MzZTRURZYWZ6VDlaSTVramI5aGlSU0kKvY0+xfsUknIlMxTT4zjM40P0LK05Dp2S
|
||||
ufEZU3+IamAdxVhdwaJmT8eEJvd/Q5off35D6OzylZPzdlsrqn5wGg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-08-12T21:07:47Z"
|
||||
mac: ENC[AES256_GCM,data:O/w/lIrlbyclnk6MiGcsg0OgMEjMoHwqYjKLCU53G+mlk+wecuL13MeMTbHW1yf03/dieCcVzYwSspRZyQGmZevO4/QCefN5mVWlzxLPSLmRmOeEA7IJxGqiLXn/GliVfdpvSlifRM0CxfrCWGZ1iBWQqxPbJD9tZRhmaytfHXY=,iv:2adSCqQHm9KBCOHAH0EJ0Un42mx42CqIgELfe7NgP4I=,tag:no+CjJ0Fs2y58sava9Ieyw==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,480 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wikijs-config.py — idempotenter Bootstrap + Konfiguration von Wiki.js.
|
||||
|
||||
Deploybares Äquivalent des manuellen Setup-Assistenten (ADR-0014, #0048/#0049).
|
||||
Läuft als k8s-Job, nachdem Wiki.js oben ist; treibt die Admin-GraphQL-API. Nur
|
||||
stdlib. Alle Verträge wurden am 2026-08-12 live gegen die laufende Instanz geprüft.
|
||||
|
||||
Ablauf:
|
||||
warten -> (falls Setup) /finalize mit SOPS-Admin -> warten auf Normalmodus
|
||||
-> login -> Gruppen + Seitenregeln (Abschottung) -> OIDC-Strategy -> hideLocal
|
||||
-> Theming/Branding -> Navigation (Custom-Sidebar) -> HTML-Rendering (allowHTML)
|
||||
-> Git-Storage.
|
||||
Idempotent: jeder Schritt prüft erst den Ist-Zustand. Seiten-Inhalt (Startseite,
|
||||
Anwender-/Betriebsseiten) liegt in git-storage, nicht in diesem Job.
|
||||
|
||||
Variante B ("kein manuelles Login"): local wird NICHT deaktiviert, sondern nur per
|
||||
Wiki.js-eigenem hideLocal aus der Login-Seite ausgeblendet. Grund: (a) local ist
|
||||
nicht löschbar und Wiki.js braucht eine Formular-Strategie, sonst rendert die
|
||||
Login-Seite leer; (b) der Job selbst loggt sich über local ein; (c) Break-Glass
|
||||
bleibt über /login?all erreichbar.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
WIKI = os.environ.get("WIKI_URL", "http://wikijs:3000")
|
||||
ADMIN_EMAIL = os.environ["WIKI_ADMIN_EMAIL"]
|
||||
ADMIN_PW = os.environ["WIKI_ADMIN_PASSWORD"]
|
||||
SITE_URL = os.environ.get("WIKI_SITE_URL", "https://wiki.axion1337.chat")
|
||||
CLIENT_ID = os.environ["OIDC_CLIENT_ID"]
|
||||
CLIENT_SECRET = os.environ["OIDC_CLIENT_SECRET"]
|
||||
AUTH = os.environ.get("AUTHENTIK_URL", "https://auth.axion1337.chat")
|
||||
APP_SLUG = os.environ.get("OIDC_APP_SLUG", "wiki-js")
|
||||
# Der Strategy-Key bestimmt die Callback-URL (/login/<key>/callback) und MUSS mit
|
||||
# der redirect_uri im Authentik-Provider übereinstimmen.
|
||||
STRATEGY_KEY = os.environ.get("OIDC_STRATEGY_KEY", "d3e7d0e4-adff-4421-b016-7758c44fd697")
|
||||
|
||||
ADMIN_PERMS = [
|
||||
"manage:system", "manage:users", "manage:groups", "manage:navigation",
|
||||
"manage:pages", "write:pages", "read:pages", "manage:assets", "write:assets",
|
||||
"read:assets", "manage:comments", "write:comments", "read:comments",
|
||||
"read:history", "read:source", "write:styles", "write:scripts",
|
||||
"manage:theme", "manage:api",
|
||||
]
|
||||
READER_PERMS = ["read:pages", "read:assets", "read:comments"]
|
||||
|
||||
# App-/Inhalts-UI-CSS (#0050 + UX-Fixes). Gilt NICHT für die Login-Seite (die rendert
|
||||
# Wiki.js ohne Custom-CSS).
|
||||
# - Content-Links: Akzent + Unterstreichung, damit klar als Link erkennbar. Die frühere
|
||||
# breite .primary--text-Einfärbung ließ Nicht-Links wie Links aussehen -> raus.
|
||||
# - Startseiten-Kacheln + Schnellzugriff-Buttons (die Startseite nutzt HTML, s. allowHTML).
|
||||
ACCENT_CSS = (
|
||||
".v-application .contents a{color:#2b6cb0;text-decoration:underline}"
|
||||
".theme--dark.v-application .contents a{color:#63b3ed}"
|
||||
".v-application .contents a.toc-anchor{color:inherit!important;text-decoration:none!important}"
|
||||
".wiki-hero{background:rgba(255,255,255,.05);border-radius:10px;padding:26px 24px;margin:8px 0 20px}"
|
||||
".wiki-hero h1{border:none!important;margin:0 0 6px}.wiki-hero p{margin:0;color:#9ca3af}"
|
||||
".wiki-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin:10px 0 26px}"
|
||||
".wiki-card{display:flex!important;flex-direction:column;gap:6px;padding:20px;border-radius:10px;"
|
||||
"background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.10);text-decoration:none!important}"
|
||||
".wiki-card:hover{background:rgba(99,179,237,.12);border-color:#63b3ed}"
|
||||
".wiki-card .wc-t{font-size:1.15rem;font-weight:600;color:#e5e7eb}"
|
||||
".wiki-card .wc-d{color:#9ca3af;font-size:.9rem}"
|
||||
".wiki-btns{display:flex;flex-wrap:wrap;gap:10px;margin:6px 0 22px}"
|
||||
".wiki-btn{display:inline-block;padding:9px 16px;border-radius:6px;background:#2b6cb0;color:#fff!important;"
|
||||
"text-decoration:none!important;font-size:.92rem;font-weight:500}"
|
||||
".wiki-btn:hover{background:#63b3ed}"
|
||||
)
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
print(f"[wikijs-config] {msg}", flush=True)
|
||||
|
||||
|
||||
def req(path: str, data=None, headers=None, method=None):
|
||||
body = json.dumps(data).encode() if data is not None else None
|
||||
r = urllib.request.Request(WIKI + path, data=body, method=method or ("POST" if body else "GET"))
|
||||
r.add_header("Content-Type", "application/json")
|
||||
for k, v in (headers or {}).items():
|
||||
r.add_header(k, v)
|
||||
with urllib.request.urlopen(r, timeout=30) as resp:
|
||||
raw = resp.read().decode()
|
||||
return resp.status, (json.loads(raw) if raw else {})
|
||||
|
||||
|
||||
def gql(query: str, jwt: str | None = None, variables: dict | None = None):
|
||||
h = {"Authorization": f"Bearer {jwt}"} if jwt else {}
|
||||
_, d = req("/graphql", {"query": query, "variables": variables or {}}, h)
|
||||
if d.get("errors"):
|
||||
raise RuntimeError(json.dumps(d["errors"]))
|
||||
return d["data"]
|
||||
|
||||
|
||||
def wait_reachable():
|
||||
# Startseite ist HTML, nicht JSON -> nicht über req()/json.loads prüfen.
|
||||
for _ in range(60):
|
||||
try:
|
||||
with urllib.request.urlopen(WIKI + "/", timeout=10) as r:
|
||||
if r.status < 500:
|
||||
return
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code < 500:
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(3)
|
||||
sys.exit("Wiki.js nicht erreichbar")
|
||||
|
||||
|
||||
def in_setup_mode() -> bool:
|
||||
# Normalmodus: die (öffentliche) login-Mutation ist beantwortbar. Setup-Modus:
|
||||
# das Haupt-Schema ist nicht geladen -> die Mutation wirft.
|
||||
try:
|
||||
gql('mutation{authentication{login(username:"probe@invalid",password:"x",'
|
||||
'strategy:"local"){responseResult{succeeded}}}}')
|
||||
return False
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def finalize():
|
||||
log("Setup-Modus -> finalize mit SOPS-Admin")
|
||||
_, d = req("/finalize", {
|
||||
"adminEmail": ADMIN_EMAIL,
|
||||
"adminPassword": ADMIN_PW,
|
||||
"adminPasswordConfirm": ADMIN_PW,
|
||||
"siteUrl": SITE_URL,
|
||||
"telemetry": False,
|
||||
})
|
||||
if not d.get("ok"):
|
||||
sys.exit(f"finalize fehlgeschlagen: {d}")
|
||||
log("finalize ok, warte auf Normalmodus (Neustart)")
|
||||
time.sleep(10)
|
||||
for _ in range(40):
|
||||
if not in_setup_mode():
|
||||
return
|
||||
time.sleep(3)
|
||||
sys.exit("Normalmodus nicht erreicht")
|
||||
|
||||
|
||||
def login():
|
||||
# Gibt JWT zurück. local bleibt bewusst aktiviert (Break-Glass + der Job loggt sich
|
||||
# damit ein) — None nur, falls local doch mal deaktiviert wurde, dann kann der Job
|
||||
# nichts tun.
|
||||
d = gql(
|
||||
'mutation($u:String!,$p:String!){authentication{login(username:$u,password:$p,'
|
||||
'strategy:"local"){responseResult{succeeded message} jwt}}}',
|
||||
variables={"u": ADMIN_EMAIL, "p": ADMIN_PW},
|
||||
)["authentication"]["login"]
|
||||
return d["jwt"] if d["responseResult"]["succeeded"] else None
|
||||
|
||||
|
||||
def _set_strategies(jwt: str, oidc_config: list, auto_enroll: list | None = None):
|
||||
strategies = [
|
||||
{ # local bleibt aktiviert: der Konfig-Job loggt sich damit ein und es ist der
|
||||
# Break-Glass-Zugang (/login?all). Auf der Login-Seite wird es per hideLocal
|
||||
# ausgeblendet (siehe set_hide_local) — kein manuelles Login im Alltag.
|
||||
"key": "local", "strategyKey": "local", "displayName": "Local",
|
||||
"order": 0, "isEnabled": True, "selfRegistration": False,
|
||||
"domainWhitelist": [], "autoEnrollGroups": [], "config": [],
|
||||
},
|
||||
{
|
||||
"key": STRATEGY_KEY, "strategyKey": "oidc", "displayName": "Authentik",
|
||||
"order": 1, "isEnabled": True, "selfRegistration": True,
|
||||
# autoEnrollGroups: jeder Authentik-Login landet in wiki-anwender
|
||||
# (#0049: "Betrieb = Admin", Anwender lesen /anwender + Startseite).
|
||||
# Ohne das legt selfRegistration zwar ein Konto an, es bekommt aber
|
||||
# KEINE Gruppe - und weil Guests unten alle Rechte entzogen bekommt,
|
||||
# sieht der Nutzer dann gar nichts und erfaehrt auch nicht warum
|
||||
# (#0103, real passiert). Admins bleiben Handarbeit in Authentik:
|
||||
# Mitgliedschaft in "authentik Admins" wird ueber den groups-Claim
|
||||
# gemappt und ueberschreibt diese Grundausstattung nicht.
|
||||
"domainWhitelist": [], "autoEnrollGroups": auto_enroll or [],
|
||||
"config": oidc_config,
|
||||
},
|
||||
]
|
||||
r = gql(
|
||||
'mutation($s:[AuthenticationStrategyInput]!){authentication{updateStrategies(strategies:$s)'
|
||||
'{responseResult{succeeded message}}}}',
|
||||
jwt, {"s": strategies},
|
||||
)["authentication"]["updateStrategies"]["responseResult"]
|
||||
if not r["succeeded"]:
|
||||
sys.exit(f"updateStrategies fehlgeschlagen: {r['message']}")
|
||||
|
||||
|
||||
def ensure_oidc(jwt: str):
|
||||
# Quellcode-verifiziert: Wiki.js liest jeden Config-Wert via
|
||||
# _.get(JSON.parse(value), 'v', null) -> der Wert MUSS als {"v": …} kodiert sein.
|
||||
cfg = {
|
||||
"clientId": CLIENT_ID, "clientSecret": CLIENT_SECRET,
|
||||
"authorizationURL": f"{AUTH}/application/o/authorize/",
|
||||
"tokenURL": f"{AUTH}/application/o/token/",
|
||||
"userInfoURL": f"{AUTH}/application/o/userinfo/",
|
||||
"issuer": f"{AUTH}/application/o/{APP_SLUG}/",
|
||||
"logoutURL": f"{AUTH}/application/o/{APP_SLUG}/end-session/",
|
||||
"emailClaim": "email", "displayNameClaim": "name",
|
||||
"groupsClaim": "groups", "mapGroups": True,
|
||||
}
|
||||
oidc_config = [{"key": k, "value": json.dumps({"v": v})} for k, v in cfg.items()]
|
||||
# Die Gruppe existiert hier sicher: main() legt sie vor ensure_oidc an.
|
||||
anwender = group_id(jwt, "wiki-anwender")
|
||||
if anwender is None:
|
||||
sys.exit("ABBRUCH: Gruppe 'wiki-anwender' nicht gefunden — ohne sie liefe "
|
||||
"jeder neue Login wieder ins Leere (#0103).")
|
||||
_set_strategies(jwt, oidc_config, [anwender])
|
||||
log(f"OIDC-Strategy gesetzt (local bleibt aktiv als Break-Glass; "
|
||||
f"Auto-Enrollment in wiki-anwender, id {anwender})")
|
||||
|
||||
|
||||
def set_hide_local(jwt: str):
|
||||
# Variante B ohne local zu deaktivieren: Wiki.js' eingebautes hideLocal entfernt den
|
||||
# local-Provider aus der Login-Seite (nur noch "Login with Authentik" sichtbar).
|
||||
# Break-Glass bleibt über /login?all. authHideLocal ist ein Site-Config-Feld.
|
||||
r = gql('mutation{site{updateConfig(authHideLocal:true){responseResult{succeeded message}}}}',
|
||||
jwt)["site"]["updateConfig"]["responseResult"]
|
||||
if not r["succeeded"]:
|
||||
sys.exit(f"authHideLocal setzen fehlgeschlagen: {r['message']}")
|
||||
log("Login-Seite: local ausgeblendet (hideLocal=true; Break-Glass via /login?all)")
|
||||
|
||||
|
||||
def ensure_theming(jwt: str):
|
||||
# Branding kommt aus gemounteten statischen Dateien (/_assets/img/branding/, siehe
|
||||
# wikijs.yaml + platform-branding ConfigMap): öffentlich ausgeliefert, kein read:assets
|
||||
# für Guests, keine externe URL. Dark als Default (#0050).
|
||||
logo = "/_assets/img/branding/logo.png"
|
||||
bg = "/_assets/img/branding/alpenglow.jpg"
|
||||
tc = gql('{theming{config{theme iconset darkMode tocPosition injectCSS injectHead injectBody}}}',
|
||||
jwt)["theming"]["config"]
|
||||
gql('mutation($t:String!,$i:String!,$d:Boolean!,$tp:String,$c:String,$h:String,$b:String){'
|
||||
'theming{setConfig(theme:$t,iconset:$i,darkMode:$d,tocPosition:$tp,injectCSS:$c,injectHead:$h,'
|
||||
'injectBody:$b){responseResult{succeeded message}}}}',
|
||||
jwt, {"t": tc["theme"], "i": tc["iconset"], "d": True, "tp": "right",
|
||||
"c": ACCENT_CSS, "h": tc["injectHead"], "b": tc["injectBody"]})
|
||||
# Site-Titel (oben links / Browser-Tab) aus der Deployment-Variable — nicht mehr
|
||||
# das hartkodierte "Wiki.js". Reproduzierbar über WIKI_SITE_TITLE.
|
||||
title = os.environ.get("WIKI_SITE_TITLE", "Wiki").strip() or "Wiki"
|
||||
gql('mutation($bg:String!,$logo:String!,$ttl:String!){site{updateConfig('
|
||||
'authLoginBgUrl:$bg,logoUrl:$logo,title:$ttl){responseResult{succeeded message}}}}',
|
||||
jwt, {"bg": bg, "logo": logo, "ttl": title})
|
||||
log(f"Theming gesetzt (Titel '{title}', Dark-Default, Logo + Hintergrund aus Mounts)")
|
||||
|
||||
|
||||
def ensure_git_storage(jwt: str):
|
||||
# Git-Storage: Wiki.js-Inhalt nach Gitea syncen (ADR-0015). Der Cluster erreicht
|
||||
# git.lab nicht -> Gitea ist das Ziel, ein CI-Job kanonisiert Gitea->git.lab.
|
||||
# Nur der PAT (GIT_STORAGE_TOKEN) ist ein Secret; Repo/User/Branch sind Klartext.
|
||||
# Fehlt der Token, wird der Schritt übersprungen -> der Job bleibt lauffähig, bevor
|
||||
# der Storage eingerichtet ist. Config-Werte brauchen die {"v":…}-Kodierung.
|
||||
token = os.environ.get("GIT_STORAGE_TOKEN", "").strip()
|
||||
repo = os.environ.get("GIT_STORAGE_REPO", "").strip()
|
||||
user = os.environ.get("GIT_STORAGE_USER", "").strip()
|
||||
if not (token and repo and user):
|
||||
log("Git-Storage übersprungen (GIT_STORAGE_TOKEN/REPO/USER nicht gesetzt)")
|
||||
return
|
||||
cfg = {
|
||||
"authType": "basic", "repoUrl": repo,
|
||||
"branch": os.environ.get("GIT_STORAGE_BRANCH", "main"),
|
||||
"basicUsername": user, "basicPassword": token, "verifySSL": True,
|
||||
"defaultEmail": os.environ.get("GIT_STORAGE_EMAIL", "wiki@localhost"),
|
||||
"defaultName": os.environ.get("GIT_STORAGE_NAME", "Wiki"),
|
||||
"sshPrivateKeyMode": "path", "localRepoPath": "./data/repo",
|
||||
"alwaysNamespace": False, "gitBinaryPath": "",
|
||||
}
|
||||
config = [{"key": k, "value": json.dumps({"v": v})} for k, v in cfg.items()]
|
||||
target = {"isEnabled": True, "key": "git", "mode": "sync",
|
||||
"syncInterval": "PT5M", "config": config}
|
||||
r = gql('mutation($t:[StorageTargetInput]!){storage{updateTargets(targets:$t)'
|
||||
'{responseResult{succeeded message}}}}',
|
||||
jwt, {"t": [target]})["storage"]["updateTargets"]["responseResult"]
|
||||
if not r["succeeded"]:
|
||||
sys.exit(f"Git-Storage fehlgeschlagen: {r['message']}")
|
||||
log(f"Git-Storage aktiviert (Gitea, sync): {repo}")
|
||||
|
||||
|
||||
def set_navigation(jwt: str):
|
||||
# Eigenes Sidebar-Menü (STATIC) statt des Browse-Baums: Home + Anwender + Betrieb.
|
||||
# Betrieb-Einträge nur für Admin-Gruppen sichtbar (1 Administrators, 3 authentik
|
||||
# Admins); die Seiten sind ohnehin per checkAccess für Anwender gesperrt.
|
||||
admin = [1, 3]
|
||||
def lnk(i, label, icon, path, g=None):
|
||||
# Das Default-Theme rendert den Sidebar-Link wortwörtlich als href=target (ohne
|
||||
# targetType-/Slash-Behandlung). Page-Targets MUESSEN daher absolut sein, sonst
|
||||
# loesen sie relativ auf (von /betrieb/x aus -> /betrieb/<target> -> 404). Wiki.js'
|
||||
# eigener Editor speichert /<locale>/<path>; hier /<path> (namespacing=false), passend
|
||||
# zu allen Inhalts-Links.
|
||||
return {"id": i, "kind": "link", "label": label, "icon": icon,
|
||||
"targetType": "page", "target": "/" + path,
|
||||
"visibilityMode": "restricted" if g else "all", "visibilityGroups": g or []}
|
||||
def hdr(i, label, g=None):
|
||||
return {"id": i, "kind": "header", "label": label, "icon": "",
|
||||
"targetType": "external", "target": "",
|
||||
"visibilityMode": "restricted" if g else "all", "visibilityGroups": g or []}
|
||||
items = [
|
||||
{"id": "home", "kind": "link", "label": "Startseite", "icon": "mdi-home",
|
||||
"targetType": "home", "target": "/", "visibilityMode": "all", "visibilityGroups": []},
|
||||
hdr("hanw", "Anwenderdokumentation"),
|
||||
lnk("a1", "Registrierung", "mdi-account-plus", "anwender/registrierung"),
|
||||
lnk("a2", "Anmelden & Konto", "mdi-login", "anwender/anmelden-und-konto"),
|
||||
lnk("a3", "Erste Schritte", "mdi-rocket-launch-outline", "anwender/erste-schritte"),
|
||||
lnk("a4", "Nachrichten", "mdi-message-text", "anwender/nachrichten"),
|
||||
lnk("a5", "Anrufe & Screensharing", "mdi-phone", "anwender/anrufe"),
|
||||
lnk("a6", "Verschlüsselung", "mdi-lock", "anwender/verschluesselung"),
|
||||
lnk("a7", "Benachrichtigungen", "mdi-bell", "anwender/benachrichtigungen"),
|
||||
lnk("a8", "ThreadNet Desktop", "mdi-desktop-classic", "anwender/threadnet-desktop-setup"),
|
||||
hdr("hbet", "Betriebsdokumentation", admin),
|
||||
lnk("b0", "Einführung & Architektur", "mdi-sitemap", "betrieb/einfuehrung", admin),
|
||||
lnk("b1", "Installation", "mdi-download", "betrieb/installation", admin),
|
||||
lnk("b2", "ConfigMap-Sync", "mdi-sync", "betrieb/configmap-sync", admin),
|
||||
lnk("b3", "Authentik OIDC", "mdi-shield-account", "betrieb/authentik-oidc", admin),
|
||||
lnk("b4", "Element / ThreadNet", "mdi-palette", "betrieb/element-customization", admin),
|
||||
lnk("b5", "Element Call & MatrixRTC", "mdi-video", "betrieb/element-call", admin),
|
||||
lnk("b6", "TURN Server", "mdi-server-network", "betrieb/turn-server", admin),
|
||||
lnk("b7", "Moderation & Scanning", "mdi-shield-check", "betrieb/moderation-content-scanning", admin),
|
||||
lnk("b8", "Monitoring", "mdi-chart-line", "betrieb/monitoring", admin),
|
||||
lnk("b9", "Backups", "mdi-backup-restore", "betrieb/backups", admin),
|
||||
lnk("b10", "Host-Wartung", "mdi-wrench", "betrieb/host-maintenance-notifications", admin),
|
||||
lnk("b11", "Room Policies", "mdi-gavel", "betrieb/room-policies", admin),
|
||||
lnk("b12", "Upgrades", "mdi-update", "betrieb/upgrades", admin),
|
||||
lnk("b13", "Sicherheit", "mdi-shield-lock", "betrieb/sicherheit", admin),
|
||||
]
|
||||
gql('mutation($m:NavigationMode!){navigation{updateConfig(mode:$m){responseResult{succeeded}}}}',
|
||||
jwt, {"m": "STATIC"})
|
||||
# Nav-Baum unter der Standard-Locale 'de' (die Seiten liegen nach der Migration unter
|
||||
# de; getTree wird mit der Seiten-Locale abgefragt — ein en-Baum bliebe unsichtbar).
|
||||
gql('mutation($t:[NavigationTreeInput]!){navigation{updateTree(tree:$t)'
|
||||
'{responseResult{succeeded message}}}}', jwt, {"t": [{"locale": "de", "items": items}]})
|
||||
log("Navigation gesetzt (Custom-Sidebar; Betrieb nur für Admins)")
|
||||
|
||||
|
||||
def ensure_html_rendering(jwt: str):
|
||||
# Die Startseite nutzt HTML-Kacheln -> markdownCore.allowHTML muss an sein. Wiki.js'
|
||||
# updateRenderers-Resolver liest jeden Wert via _.get(JSON.parse(value),'v',null),
|
||||
# also {"v":…}-kodiert; die übrigen Renderer-Werte müssen mitgesendet werden, sonst
|
||||
# werden sie genullt.
|
||||
# ⚠️ Wiki.js lädt die Renderer-Config beim START. Auf einer FRISCHEN Instanz greift
|
||||
# allowHTML erst nach einem Wiki.js-Neustart; die Startseite rendert dann beim
|
||||
# nächsten git-Sync korrekt (sonst erscheint sie kurz als HTML-Text). Idempotent.
|
||||
rs = gql('{rendering{renderers{isEnabled key config{key value}}}}', jwt)["rendering"]["renderers"]
|
||||
def actual(v):
|
||||
try:
|
||||
p = json.loads(v)
|
||||
return p["value"] if isinstance(p, dict) and "value" in p else p
|
||||
except Exception:
|
||||
return v
|
||||
out = []
|
||||
for r in rs:
|
||||
cfg = []
|
||||
for c in (r["config"] or []):
|
||||
a = actual(c["value"])
|
||||
if r["key"] == "markdownCore" and c["key"] == "allowHTML":
|
||||
a = True
|
||||
cfg.append({"key": c["key"], "value": json.dumps({"v": a})})
|
||||
out.append({"isEnabled": r["isEnabled"], "key": r["key"], "config": cfg})
|
||||
gql('mutation($r:[RendererInput]){rendering{updateRenderers(renderers:$r)'
|
||||
'{responseResult{succeeded message}}}}', jwt, {"r": out})
|
||||
log("HTML-Rendering aktiviert (markdownCore.allowHTML) — Neustart nötig auf frischer Instanz")
|
||||
|
||||
|
||||
def ensure_locale(jwt: str):
|
||||
# Deutsch als Standardsprache: die Inhalte sind deutsch, hingen aber an 'en'.
|
||||
# updateLocale lädt live (setCurrentLocale + refreshNamespaces, KEIN Neustart);
|
||||
# namespacing bleibt aus (eine Sprache, saubere /<pfad>-URLs), 'en' bleibt als
|
||||
# Namespace registriert.
|
||||
loc = gql('{localization{locales{code isInstalled} config{locale namespaces}}}',
|
||||
jwt)["localization"]
|
||||
installed = {l["code"] for l in loc["locales"] if l["isInstalled"]}
|
||||
if "de" not in installed:
|
||||
# de muss in der locales-Tabelle liegen (updateLocale liest dort isRTL). Auf
|
||||
# frischen Instanzen ggf. herunterladen (braucht Egress) — weich, kein Hard-Fail.
|
||||
try:
|
||||
gql('mutation{localization{downloadLocale(locale:"de")'
|
||||
'{responseResult{succeeded message}}}}', jwt)
|
||||
log("Locale 'de' heruntergeladen")
|
||||
except Exception as e: # noqa: BLE001
|
||||
log(f"WARN: downloadLocale(de) fehlgeschlagen ({e}) — Locale-Umstellung übersprungen")
|
||||
return
|
||||
if loc["config"]["locale"] != "de":
|
||||
ns = sorted(set((loc["config"].get("namespaces") or []) + ["de", "en"]))
|
||||
gql('mutation($l:String!,$a:Boolean!,$n:Boolean!,$ns:[String]!){localization{'
|
||||
'updateLocale(locale:$l,autoUpdate:$a,namespacing:$n,namespaces:$ns)'
|
||||
'{responseResult{succeeded message}}}}',
|
||||
jwt, {"l": "de", "a": True, "n": False, "ns": ns})
|
||||
log("Standardsprache auf Deutsch gesetzt (de)")
|
||||
# Inhalte en->de migrieren (patcht nur die pages-Tabelle; das ist auch Wiki.js'
|
||||
# eigener Locale-Migrationsweg, kollisions-sicher via whereNotExists). Danach Tree
|
||||
# + Suchindex neu bauen, aber NUR wenn wirklich migriert wurde — sonst teurer No-op
|
||||
# bei jedem Job-Lauf. (git-storage-Frontmatter der Altdateien bleibt 'en' und heilt
|
||||
# beim nächsten Bearbeiten; dormante Dateien werden nicht re-importiert.)
|
||||
m = gql('mutation{pages{migrateToLocale(sourceLocale:"en",targetLocale:"de")'
|
||||
'{responseResult{succeeded message} count}}}', jwt)["pages"]["migrateToLocale"]
|
||||
if (m.get("count") or 0) > 0:
|
||||
gql('mutation{pages{rebuildTree{responseResult{succeeded}}}}', jwt)
|
||||
gql('mutation{search{rebuildIndex{responseResult{succeeded}}}}', jwt)
|
||||
log(f"Inhalte nach de migriert ({m['count']} Seiten) + Tree/Suchindex neu gebaut")
|
||||
else:
|
||||
log("Locale-Migration: keine en-Seiten offen (bereits de)")
|
||||
|
||||
|
||||
def ensure_timezones(jwt: str):
|
||||
# Systemkonten auf Europe/Berlin statt Wiki.js-Default America/New_York: guest
|
||||
# (id 2, treibt die anonyme/Standard-Anzeige) + der Admin. Menschliche OIDC-Nutzer
|
||||
# setzen ihre TZ im Profil; NEUE Nutzer erben aber weiter den im Core hardcodierten
|
||||
# New_York-Default (server/core/auth.js) — nur per Fork-Patch änderbar, nicht per
|
||||
# Config. update patcht nur das timezone-Feld (kein Nulling, groups nur bei Array).
|
||||
ids = {2} # guest
|
||||
for u in gql("{users{list{id email}}}", jwt)["users"]["list"]:
|
||||
if u["email"] == ADMIN_EMAIL:
|
||||
ids.add(u["id"])
|
||||
for uid in sorted(ids):
|
||||
gql('mutation($id:Int!){users{update(id:$id,timezone:"Europe/Berlin")'
|
||||
'{responseResult{succeeded message}}}}', jwt, {"id": uid})
|
||||
log(f"Zeitzone Europe/Berlin für Systemkonten gesetzt (ids {sorted(ids)})")
|
||||
|
||||
|
||||
def group_id(jwt: str, name: str):
|
||||
for g in gql("{groups{list{id name}}}", jwt)["groups"]["list"]:
|
||||
if g["name"] == name:
|
||||
return g["id"]
|
||||
return None
|
||||
|
||||
|
||||
def ensure_group(jwt: str, name: str, perms: list, rules: list):
|
||||
gid = group_id(jwt, name)
|
||||
if gid is None:
|
||||
gql('mutation($n:String!){groups{create(name:$n){responseResult{succeeded message}}}}',
|
||||
jwt, {"n": name})
|
||||
gid = group_id(jwt, name)
|
||||
log(f"Gruppe '{name}' angelegt (id {gid})")
|
||||
gql(
|
||||
'mutation($id:Int!,$n:String!,$p:[String]!,$r:[PageRuleInput]!){groups{update('
|
||||
'id:$id,name:$n,redirectOnLogin:"/",permissions:$p,pageRules:$r){responseResult{succeeded message}}}}',
|
||||
jwt, {"id": gid, "n": name, "p": perms, "r": rules},
|
||||
)
|
||||
log(f"Gruppe '{name}' -> Rechte+Seitenregeln gesetzt")
|
||||
|
||||
|
||||
def rule(rid: str, deny: bool, perms: list, path: str, match: str = "START"):
|
||||
return {"id": rid, "deny": deny, "match": match, "roles": perms,
|
||||
"path": path, "locales": []}
|
||||
|
||||
|
||||
def main():
|
||||
log(f"Ziel: {WIKI}")
|
||||
wait_reachable()
|
||||
if in_setup_mode():
|
||||
finalize()
|
||||
jwt = login()
|
||||
if jwt is None:
|
||||
log("lokaler Login nicht möglich (local wurde deaktiviert) — kann nicht konfigurieren, Ende.")
|
||||
return
|
||||
log("eingeloggt")
|
||||
# Erst Gruppen (Schreibvorgänge), DANN OIDC — ensure_oidc deaktiviert zuletzt local.
|
||||
# authentik Admins: alles lesen+schreiben. wiki-anwender: nur /anwender lesen.
|
||||
ensure_group(jwt, "authentik Admins", ADMIN_PERMS, [rule("adm", False, ["read:pages", "write:pages", "manage:pages"], "")])
|
||||
# wiki-anwender: nur den Anwender-Bereich lesen + die Startseite (sonst wäre "/"
|
||||
# für sie gesperrt). betrieb/* matcht keine Regel -> Default-Deny (Abschottung).
|
||||
ensure_group(jwt, "wiki-anwender", READER_PERMS, [
|
||||
rule("anw", False, READER_PERMS, "anwender"),
|
||||
rule("anwhome", False, READER_PERMS, "home", match="EXACT"),
|
||||
])
|
||||
# Guests (id 2) alle Rechte entziehen — Login-Pflicht, keine öffentliche Sicht.
|
||||
gql('mutation{groups{update(id:2,name:"Guests",redirectOnLogin:"/",permissions:[],pageRules:[]){responseResult{succeeded}}}}', jwt)
|
||||
ensure_oidc(jwt)
|
||||
set_hide_local(jwt)
|
||||
ensure_theming(jwt)
|
||||
ensure_locale(jwt) # Deutsch als Default + Inhalte en->de migrieren (vor der Nav)
|
||||
set_navigation(jwt)
|
||||
ensure_html_rendering(jwt)
|
||||
ensure_git_storage(jwt)
|
||||
ensure_timezones(jwt) # Systemkonten auf Europe/Berlin
|
||||
log("fertig — Wiki.js konfiguriert")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
# Bootstrap-/Konfig-Job für Wiki.js (ADR-0014, #0048/#0049). Ersetzt den manuellen
|
||||
# Setup-Assistenten: finalize (Admin aus SOPS) -> OIDC-Strategy -> Gruppen +
|
||||
# Seitenregeln. Idempotent (live verifiziert 2026-08-12). Läuft einmal beim Deploy;
|
||||
# erneut anstoßen = Job löschen, Flux legt ihn neu an (Skript ist re-runnable).
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: wikijs-config
|
||||
namespace: matrix
|
||||
spec:
|
||||
backoffLimit: 10
|
||||
ttlSecondsAfterFinished: 86400
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: wikijs-config
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: config
|
||||
image: python:3.12-alpine
|
||||
command: ["python3", "/script/wikijs-config.py"]
|
||||
env:
|
||||
- name: WIKI_URL
|
||||
value: http://wikijs:3000
|
||||
- name: WIKI_SITE_URL
|
||||
value: https://wiki.axion1337.chat
|
||||
# Site-Titel (oben links + Browser-Tab), konfigurierbar pro Deployment.
|
||||
- name: WIKI_SITE_TITLE
|
||||
value: wiki.axion1337.chat
|
||||
- name: WIKI_ADMIN_EMAIL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-admin-secret
|
||||
key: email
|
||||
- name: WIKI_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-admin-secret
|
||||
key: password
|
||||
- name: OIDC_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-oidc-secret
|
||||
key: client_id
|
||||
- name: OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-oidc-secret
|
||||
key: client_secret
|
||||
# Git-Storage (ADR-0015): Inhalt nach Gitea syncen. Klartext-Koordinaten;
|
||||
# nur der PAT liegt im SOPS-Secret wikijs-git-secret. optional=true, damit
|
||||
# der Job auch läuft, bevor das Secret existiert (Skript überspringt dann).
|
||||
- name: GIT_STORAGE_REPO
|
||||
value: https://rohana.axion1337.de/sorb/ThreadNetWiki.git
|
||||
- name: GIT_STORAGE_USER
|
||||
value: sorb
|
||||
- name: GIT_STORAGE_BRANCH
|
||||
value: main
|
||||
- name: GIT_STORAGE_EMAIL
|
||||
value: wiki@axion1337.chat
|
||||
- name: GIT_STORAGE_NAME
|
||||
value: ThreadNet Wiki
|
||||
- name: GIT_STORAGE_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-git-secret
|
||||
key: token
|
||||
optional: true
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /script
|
||||
volumes:
|
||||
- name: script
|
||||
configMap:
|
||||
name: wikijs-config-script
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: v1
|
||||
data:
|
||||
token: ENC[AES256_GCM,data:Z+oD7NDgHTTuMyNLFY+2PhZmTJzN4eIAGgmCx2TnI73BQ2v40Um/ppioLY4kyow56lfj23jpI6w=,iv:/Y7Oti9e7+Se4DjxI1nT4J0f+l73PKNbgbeSiyIpdU8=,tag:ilW/M/uSTn8qOUcUfOhtwA==,type:str]
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: wikijs-git-secret
|
||||
namespace: matrix
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4c3pFV3gxeEgrVjkzVnZ3
|
||||
TkZHbThJV0dqY3BXanhtMExzUTV3aTdXTlVVCjQxZjN2cjRiMjQzTG1XM3JyVzZt
|
||||
Si9GL1ZEVFArd2FxR3ltaDJhS21GeFkKLS0tIHhKVUtKaUlFOGRkOGJNUkR0Ymwy
|
||||
dzREc0lNMlowMXdNa1I3ODhsN2k4WUkKFRTpU/cQq0+EcIldJn3/77P6HYaxH0uK
|
||||
Xd6CT+WdX83vcmwX4d1Brh6y3GEX+cKeNAaxUL4raZdkjCOz0n8bnQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-08-13T09:46:49Z"
|
||||
mac: ENC[AES256_GCM,data:STpYfsCDwZA1OIZLfTrO3Ww6Nuo8h/lIufrnQj8yeeuYcT1vEev0Tr/c2+8fNRW4yLEfRkqNMhP4p6akh9G2BChQN4FnwuUqb9yA7s+una83IXmn4UOCvwchxhY2c95lLKMNr45f3fUx6bw4tlENRor3ZrF3+jvvbX7b6c1XlC8=,iv:LVZdHBWSbwUgc4/JQSbw6twIVsa6QdSzAW7LnQn81I0=,tag:tTfFxm1URN1U1bbOroKHUA==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
data:
|
||||
client_id: ENC[AES256_GCM,data:QhTMVvbKj+cNAgU7oXHsnyM3y5w88lo3HVf7eQ6a/RWlaVFcbwuNm6LFaeV2fyv+mUCFJ2HtLKc=,iv:9J90hdC728jAqJ4htRGeEnd0mcrQqF7VvzOGE4NKrzg=,tag:ACgs5IAgcoKFy7noFHZHoA==,type:str]
|
||||
client_secret: ENC[AES256_GCM,data:uix6U6Ve60OMUWU1+WsW5ueytELs890A3JJdCad6BoQvcrjnDq9a0z1dAzLjzY9BQqBRo8Bzyd7JBwZVM2EXiJ/cMGgTvNNmMsPlpWcWg4Wu3sUl3gl5UkszcBOlWVXx6BQ0IHSKFOZw7vQYZxFLc5fiCveD4QWwGj+5AMZyNCWJtaxVFDMpno5WFW0Ht2aBWwqHCTK0irXjfeCuxEubmlMd4zzdri5PVn1vlA==,iv:jA4qw1RVDIgk0irFMz744N8LQEM+2lI/nW8jtYXKgyk=,tag:7rrzGejQ025cRnQ+MSHSYA==,type:str]
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: wikijs-oidc-secret
|
||||
namespace: matrix
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYYVdFUXVOQmRqTEJ6Zlhx
|
||||
MG9aUmxaUGZYWjg5YmNYWVBZYUdIbEtGL1ZFCk9tUG1ZTGE4QzBOdzRzT0dkVy90
|
||||
VSsyaFNTK21hb0VlWVhvN0JjeTBKZ2cKLS0tIFB1cGVnZktsczRsQ0NGcmhZNWE3
|
||||
K0l2SUtFanc3UE1WRlo5TUk3OXRJVEkKWE+LvhI8MGNCy54ylmRujV+I6IywurDy
|
||||
DHO71yXy2xnwDfWEV7Mcg02D2lGgGfdFqpY9ulWuFqh/qDM6CMZ4ig==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-08-12T22:01:16Z"
|
||||
mac: ENC[AES256_GCM,data:m+FzQOlNKAzByFxtrC0PPzSsE7T3gDH2Y+OvBLHEl7mcxVoB376ZsRi7op2g456NlgxX/9kD0X+t33RYzqLMe0Q5b3jzQgYPV0SfsIqgpmfid6uhHxGhzpQWGfTIImx9MP2oQqqW93uicKYi64SxbVMcC2xo27gvRpeMTvEALR8=,iv:DrZs65lJ2iSbNtXr/p637Kf0Cxuxs1UfBpgE1kAEED8=,tag:cpzPBw2a9YfllWh8+HiPxg==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,82 @@
|
||||
# Nightly Borg backup der Wiki.js-Postgres-DB auf die Hetzner Storage Box (#0048).
|
||||
# Der Inhalt liegt bereits versioniert in git (git-storage -> Gitea -> git.lab); dieses
|
||||
# Backup sichert den Laufzeit-Zustand, den git NICHT hält: Kommentare, lokale Konten,
|
||||
# OIDC-Nutzer und den Suchindex. Muster: apps/authentik/authentik-backup.yaml (DB-only).
|
||||
# Wiederverwendet die synapse-backup-Credentials (SSH-Key + Borg-Passphrase) und das
|
||||
# -known-hosts (gleicher Namespace, gleiche Storage Box), aber einen EIGENEN Borg-Repo-
|
||||
# Pfad (wikijs-backup) und einen versetzten Zeitplan.
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: wikijs-backup
|
||||
namespace: matrix
|
||||
spec:
|
||||
schedule: "30 3 * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
backoffLimit: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: wikijs-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/./wikijs-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: "wiki:wikijs-postgres"
|
||||
- name: PGUSER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-postgres-secret
|
||||
key: username
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-postgres-secret
|
||||
key: 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: synapse-backup-credentials
|
||||
items:
|
||||
- key: ssh-private-key
|
||||
path: ssh-private-key
|
||||
mode: 0400
|
||||
- name: known-hosts
|
||||
configMap:
|
||||
name: synapse-backup-known-hosts
|
||||
- name: scratch
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
data:
|
||||
password: ENC[AES256_GCM,data:ALT8e6Z6nZXHWkB6w3ZtkW7z14VAN0LpFITlCQBiJ9Up0O0dVhikfseCygo=,iv:Pn6U3t5QCqnYZA2TAk/fQBnE7FITTVoNzyCzMA/Wo+w=,tag:KAHpwDDRN9bIzMkWFBooqQ==,type:str]
|
||||
username: ENC[AES256_GCM,data:KJQbdaJXroo=,iv:0ZVry78Bg9WRW0+vR7Opib9McGC5pyHtaxXExyh9dAk=,tag:PJvI4tigyIDZc0ASLEvUFA==,type:str]
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: wikijs-postgres-secret
|
||||
namespace: matrix
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDY2xSYVl1R3M5SVI4UnR1
|
||||
cFpZcXBLL2thdDdWRmtSa3kxM3FkRWIwRW1ZCk8xa25GbzVDQjlFNk0rcmUwcldJ
|
||||
Z0JNVWhXQTY3VWlDYjFPQkFQM1FFRzgKLS0tIDkzME02K3hYTUFWNGpLY3N3emcr
|
||||
OXhrd2pUSnpTU2ZUWHV2MnMzdUFVVlUKyrvbkRz4sM5FjdHbP0vdkqYlQZxr33c1
|
||||
HDScKVKJ++RBXpP4+x7B6rjSiMsV0I8eT678IO3wz/qGugvrTTZt0w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age14l0hwfqylwpemz5y2ghh2yxk0phszlnj3qlejhue0fw0kz3tmfgqdsjzdh
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-08-12T19:16:30Z"
|
||||
mac: ENC[AES256_GCM,data:3uz4TmPnSwNK3vmBmRzGtwl2qMxPMQDWEqxpwQ0AbTOpyCiLmhqh1/eIvH1qOQoV5RQMkfcGa0XF2dxb7Wr4S52m8E19zyOmw2HNHvUj9gt2IUGHBUm5nZ2Qd0fO38VTdRX+Eyv7y/brbz1+guXMOTAwAbEHh51oV7rc7cyRCQo=,iv:8Kl7qOyfF1AlEMIFTaBM7nXZDE6RhsPJjj9QbzIJ8ns=,tag:BtvI0TpoY339XfXcknTVDw==,type:str]
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,79 @@
|
||||
# Dedizierter Postgres für Wiki.js (ADR-0014, #0048). Bewusst eigenständig statt in
|
||||
# den Synapse-/Authentik-DBs, damit das Wiki als Suite-Baustein reproduzierbar bleibt.
|
||||
# git ist die Inhalts-Quelle (Wiki.js Git-Storage), diese DB ist Laufzeit-Index/Cache.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: wikijs-postgres-data
|
||||
namespace: matrix
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: wikijs-postgres
|
||||
namespace: matrix
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate # RWO-Volume: kein RollingUpdate mit zwei Pods
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: wikijs-postgres
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: wikijs-postgres
|
||||
spec:
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- name: postgres
|
||||
containerPort: 5432
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
value: wiki
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-postgres-secret
|
||||
key: username
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-postgres-secret
|
||||
key: password
|
||||
- name: PGDATA
|
||||
value: /var/lib/postgresql/data/pgdata
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/postgresql/data
|
||||
readinessProbe:
|
||||
exec:
|
||||
command: ["pg_isready", "-U", "wiki", "-d", "wiki"]
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: wikijs-postgres-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: wikijs-postgres
|
||||
namespace: matrix
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: wikijs-postgres
|
||||
ports:
|
||||
- name: postgres
|
||||
port: 5432
|
||||
targetPort: postgres
|
||||
@@ -0,0 +1,140 @@
|
||||
# Wiki.js — Plattform-Wiki (ADR-0014, #0048). Öffentlich unter wiki.axion1337.chat
|
||||
# (Ingress/Cert in wiki-ingress.yaml), Login nativ via Authentik-OIDC (#0049, Guide 10).
|
||||
# Git-Storage (Inhalt in git) wird nach dem ersten Start in der Admin-UI eingerichtet.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: wikijs-data
|
||||
namespace: matrix
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: local-path
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: wikijs
|
||||
namespace: matrix
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate # RWO-Volume (Git-Storage-Klon)
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: wikijs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: wikijs
|
||||
spec:
|
||||
containers:
|
||||
- name: wikijs
|
||||
image: ghcr.io/requarks/wiki:2.5
|
||||
# Fork-Patch (ThreadNet): neue OIDC-Nutzer sollen Europe/Berlin statt des
|
||||
# Wiki.js-Defaults America/New_York bekommen. Wiki.js legt SSO-Nutzer in
|
||||
# processProfile (server/models/users.js) OHNE timezone an -> es greift der
|
||||
# DB-Spalten-Default (New_York). Wir setzen timezone bei der Neuanlage explizit.
|
||||
# Startup-Overlay statt Custom-Image (es gibt keine Wiki.js-Build-Pipeline):
|
||||
# idempotent (grep-Guard), failt open (node startet auch wenn der sed nicht
|
||||
# greift). localeCode kommt bereits aus WIKI.config.lang.code (= de).
|
||||
# ⚠️ Bei Wiki.js-Upgrade den Anker "localeCode: WIKI.config.lang.code," prüfen.
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
grep -q "timezone: 'Europe/Berlin'" server/models/users.js ||
|
||||
sed -i "s#localeCode: WIKI.config.lang.code,#localeCode: WIKI.config.lang.code, timezone: 'Europe/Berlin',#" server/models/users.js;
|
||||
exec node --no-deprecation server
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
env:
|
||||
- name: DB_TYPE
|
||||
value: postgres
|
||||
- name: DB_HOST
|
||||
value: wikijs-postgres
|
||||
- name: DB_PORT
|
||||
value: "5432"
|
||||
- name: DB_NAME
|
||||
value: wiki
|
||||
- name: DB_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-postgres-secret
|
||||
key: username
|
||||
- name: DB_PASS
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: wikijs-postgres-secret
|
||||
key: password
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /wiki/data
|
||||
# Branding-Assets (Logo + Login-Hintergrund) als statische Dateien in den
|
||||
# öffentlichen /_assets-Baum (express.static /wiki/assets). Dadurch ohne
|
||||
# read:assets erreichbar (Guests bleiben gesperrt) und ohne externe URL —
|
||||
# eine Quelle (ConfigMap platform-branding), Config zeigt auf /_assets/img/branding/.
|
||||
- name: branding
|
||||
mountPath: /wiki/assets/img/branding
|
||||
readOnly: true
|
||||
# Favicon (#0050): serve-favicon liest /wiki/assets/favicon.ico; der
|
||||
# Browser-Tab nutzt zusätzlich die PNGs unter /_assets/favicons/. Einzelne
|
||||
# Dateien per subPath überlagern, ohne die restlichen Default-Favicons zu verdecken.
|
||||
- name: branding
|
||||
mountPath: /wiki/assets/favicon.ico
|
||||
subPath: favicon.ico
|
||||
readOnly: true
|
||||
- name: branding
|
||||
mountPath: /wiki/assets/favicons/favicon-32x32.png
|
||||
subPath: favicon-32x32.png
|
||||
readOnly: true
|
||||
- name: branding
|
||||
mountPath: /wiki/assets/favicons/favicon-16x16.png
|
||||
subPath: favicon-16x16.png
|
||||
readOnly: true
|
||||
- name: branding
|
||||
mountPath: /wiki/assets/favicons/android-chrome-192x192.png
|
||||
subPath: android-chrome-192x192.png
|
||||
readOnly: true
|
||||
- name: branding
|
||||
mountPath: /wiki/assets/favicons/apple-touch-icon.png
|
||||
subPath: apple-touch-icon.png
|
||||
readOnly: true
|
||||
- name: branding
|
||||
mountPath: /wiki/assets/favicons/mstile-150x150.png
|
||||
subPath: mstile-150x150.png
|
||||
readOnly: true
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 25
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 20
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: wikijs-data
|
||||
- name: branding
|
||||
configMap:
|
||||
name: platform-branding
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: wikijs
|
||||
namespace: matrix
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: wikijs
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: http
|
||||
@@ -0,0 +1,24 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBrzCCAVWgAwIBAgIQMZ1JRdPpesdAzJqMZOa9tDAKBggqhkjOPQQDAjA2MRUw
|
||||
EwYDVQQKEwxhWGlvbkxhYnMgQ0ExHTAbBgNVBAMTFGFYaW9uTGFicyBDQSBSb290
|
||||
IENBMB4XDTI1MTIwMjIyMDEyMVoXDTM1MTEzMDIyMDEyMVowNjEVMBMGA1UEChMM
|
||||
YVhpb25MYWJzIENBMR0wGwYDVQQDExRhWGlvbkxhYnMgQ0EgUm9vdCBDQTBZMBMG
|
||||
ByqGSM49AgEGCCqGSM49AwEHA0IABFuhwykAsSg2BWI3wqWHjNuL+5xlGjhHyiSt
|
||||
6YmoSd1tJGa7WA5pdRRm2JZriA3IWuhdkZVTUl8nLduRJVTb5KSjRTBDMA4GA1Ud
|
||||
DwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBRGPIQeqG1S
|
||||
CyHYLwSQbe4jQ9RcZDAKBggqhkjOPQQDAgNIADBFAiEA1Z0Dxtj9hJOYkLJ8FLNT
|
||||
96eatVP/OsKyaRk/MxReIwMCICrCeNSSOGUbiZ8Hd89jtlkCpWiLH1pCzZPUtYDX
|
||||
R7gr
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB2DCCAX6gAwIBAgIQLv6q8XwRjaYo8l0gB9NdyjAKBggqhkjOPQQDAjA2MRUw
|
||||
EwYDVQQKEwxhWGlvbkxhYnMgQ0ExHTAbBgNVBAMTFGFYaW9uTGFicyBDQSBSb290
|
||||
IENBMB4XDTI1MTIwMjIyMDEyMloXDTM1MTEzMDIyMDEyMlowPjEVMBMGA1UEChMM
|
||||
YVhpb25MYWJzIENBMSUwIwYDVQQDExxhWGlvbkxhYnMgQ0EgSW50ZXJtZWRpYXRl
|
||||
IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECj764yT9QNMsRjEfmHnuDI1o
|
||||
11faTeMl5kcwZlnDetHF5O+Ey5d+kKuysqIOF1YX8bPt6o61aJYRmc4RxRDU/aNm
|
||||
MGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYE
|
||||
FGpRTHbW0eTgBMJep8w+Qyvy9oWzMB8GA1UdIwQYMBaAFEY8hB6obVILIdgvBJBt
|
||||
7iND1FxkMAoGCCqGSM49BAMCA0gAMEUCIEAbqm/Y14dyM7h+I5en2l596eDDh3cS
|
||||
2GwVO3CKuBY2AiEAkjPO/8M8PaFEaoncc/JDFxk9KR2P9IY24BvUA7YVsAU=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: authentik-apps
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1m
|
||||
path: ./apps/authentik
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
decryption:
|
||||
provider: sops
|
||||
secretRef:
|
||||
name: sops-age
|
||||
dependsOn:
|
||||
- name: infra-apps
|
||||
@@ -4,4 +4,6 @@ resources:
|
||||
- gotk-components.yaml
|
||||
- gotk-sync.yaml
|
||||
- infra-sync.yaml
|
||||
- monitoring-sync.yaml
|
||||
- production-sync.yaml
|
||||
- authentik-sync.yaml
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: monitoring-apps
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./apps/monitoring
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
dependsOn:
|
||||
- name: infra-apps
|
||||
@@ -0,0 +1,490 @@
|
||||
# aXion1337.Chat – Task List & Meilensteine
|
||||
|
||||
**Last Updated**: 2026-07-28
|
||||
**Statusübersicht**: [✅ 13 Abgeschlossen] [🔄 0 In Progress] [📋 8+ Pending] [🔒 10 Security]
|
||||
|
||||
---
|
||||
|
||||
## 📊 Status Summary (Quick View)
|
||||
|
||||
| Kategorie | Count | Status | Details |
|
||||
|-----------|-------|--------|---------|
|
||||
| **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** | 8+ | 📋 Pending | DB Backups, PostgreSQL Migration, MAS-Template-Link, VP9-Retry |
|
||||
| **Security Tasks** | 5 | 🔒 Pending | auditd, Kernel hardening, CrowdSec, Falco, WAF |
|
||||
|
||||
### Priority Distribution
|
||||
|
||||
| Priority | Count | Timeline |
|
||||
|----------|-------|----------|
|
||||
| 🔴 **CRITICAL** | 3 | This week |
|
||||
| 🟠 **HIGH** | 4 | 1–2 weeks |
|
||||
| 🟡 **MEDIUM** | 8 | ~1 month |
|
||||
| 🟢 **LOW** | 4+ | Nice-to-have |
|
||||
|
||||
---
|
||||
|
||||
## 🗓️ 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**
|
||||
1. ✅ **Authentik Stage 2 abschließen**
|
||||
- ✅ Manual: OIDC Provider + Application in Authentik UI erstellt
|
||||
- ✅ Code: `upstream_oauth2_config` in `mas-secret.yaml` eingefügt
|
||||
- ✅ Code: `passwords: enabled: false` aktiviert
|
||||
- ✅ Commit: `cdfbf7d` - Enable Authentik OIDC integration in MAS
|
||||
- ✅ Verified: Login mit Authentik funktioniert (2026-05-15)
|
||||
- **Status**: COMPLETE
|
||||
|
||||
2. ✅ **Hetzner Cloud Firewall – Optimierte Konfiguration**
|
||||
- ✅ Ingress: 80/443 + TURN/STUN + RTC Services
|
||||
- ✅ SSH: Spezifische IPs (port 2248, nicht 22)
|
||||
- ✅ Default-Deny für nicht-definierte Traffic
|
||||
- **Status**: COMPLETE (optimiert über Plan)
|
||||
|
||||
3. ✅ **SSH Hardening**
|
||||
- ✅ PasswordAuthentication: no (key-only)
|
||||
- ✅ PermitRootLogin: no (root disabled)
|
||||
- ✅ MaxAuthTries: 3 (verified 2026-05-15)
|
||||
- **Status**: COMPLETE
|
||||
|
||||
4. **Database Backup Strategy – Decision & First Backup**
|
||||
- ⏳ Decision: CloudNativePG (on K3S) or Hetzner Postgres (managed)?
|
||||
- ⏳ Setup: Daily automated backups
|
||||
- ⏳ Setup: Off-site storage (S3 / Storage Box)
|
||||
- ⏳ Setup: Monthly verified restores
|
||||
- Est. Time: 2–3 days
|
||||
- Priority: CRITICAL (disaster recovery)
|
||||
- **Status**: NEXT
|
||||
|
||||
### 🟠 **NEXT 1–2 WEEKS – HIGH**
|
||||
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** — erledigt, Closes Issue #8 (2026-07-28), siehe
|
||||
`docs/deployment-guides/04-element-customization.md` Kapitel 4. **Status**: COMPLETE
|
||||
|
||||
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** — 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
|
||||
- [x] **K3S Cluster aufsetzen** – Single-Node auf Hetzner Cloud (49.13.132.245)
|
||||
- Commit: `initial-setup` (vor Projekt)
|
||||
- Status: ✅ Läuft
|
||||
|
||||
- [x] **Flux CD Installation**
|
||||
- SOPS + age Encryption
|
||||
- GitOps Repository konfigurieren
|
||||
- Commit: `setup-flux` (vor Projekt)
|
||||
- Status: ✅ Läuft
|
||||
|
||||
- [x] **Element Server Suite v26.4.0 Deployment**
|
||||
- Synapse Homeserver (`matrix.axion1337.chat`)
|
||||
- Matrix Authentication Service (`account.axion1337.chat`)
|
||||
- Element Web (`axion1337.chat`)
|
||||
- Element Admin (`admin.axion1337.chat`)
|
||||
- MatrixRTC/Element Call (`mrtc.axion1337.chat`)
|
||||
- Commit: `deploy-ess-matrix-stack`
|
||||
- Status: ✅ Running
|
||||
|
||||
### Phase 2: Core Features
|
||||
- [x] **7 Custom Element Web Themes**
|
||||
- aXion1337 Dark, Deep Purple, Discord Dark, Electric Blue, Everforest, Gruvbox, Wal
|
||||
- Alphabetisch sortiert
|
||||
- Commit: `add-custom-element-themes`
|
||||
- Status: ✅ Deployed
|
||||
|
||||
- [x] **Element Desktop Setup Scripts** (Windows/macOS/Linux)
|
||||
- Auto-Download + Install + Config
|
||||
- Hosted auf `axion1337.chat/docs/setup/`
|
||||
- Commits: `add-element-desktop-setup-scripts`, `fix-element-setup-script-hosting`
|
||||
- Status: ✅ Deployed
|
||||
|
||||
- [x] **Room Policies**
|
||||
- Message Retention (1d–1y lifecycle)
|
||||
- Room Publication Rules (allow all)
|
||||
- Auto-Join Rooms für Onboarding
|
||||
- Commit: `add-synapse-retention-publication-autojoin`
|
||||
- Status: ✅ Deployed
|
||||
|
||||
### Phase 3: WebRTC & Medienübertragung
|
||||
- [x] **TURN Server (coturn) für Video-Calls**
|
||||
- Domain: `turn.axion1337.chat`
|
||||
- HMAC-Auth mit Shared Secret
|
||||
- Ports: 3478/udp, 3478/tcp, 5349/tcp, 49152-65535/udp
|
||||
- Commit: `implement-turn-server-coturn-for-webrtc-video-calls`
|
||||
- Status: ✅ Deployed
|
||||
- Manual: DNS A-Record + Firewall-Ports öffnen (noch erforderlich)
|
||||
|
||||
### Phase 4: Monitoring & Observability
|
||||
- [x] **Monitoring Stack Integration**
|
||||
- Alloy (Grafana Agent) als Collector
|
||||
- Remote Write zu Selendis (10.0.0.3:9090 Prometheus, :3100 Loki)
|
||||
- kube-state-metrics, node-exporter DaemonSet
|
||||
- Commits: `integrate-monitoring-alloy-prometheus-loki`, `fix-prometheus-remote-write-docker`
|
||||
- Status: ✅ Deployed
|
||||
|
||||
### Phase 5: Identity Provider (Authentik)
|
||||
- [x] **Authentik Stage 1 Deployment**
|
||||
- HelmRelease v2026.x in `authentik` namespace
|
||||
- Embedded PostgreSQL + Alloy-compatible
|
||||
- Cert-Manager für TLS
|
||||
- Commit: `deploy-authentik-as-identity-provider-for-matrix-stage-1`
|
||||
- Status: ✅ Deployed
|
||||
- Manual: Admin-Passwort + OIDC Provider + Application + Enrollment Flow erstellt ✅
|
||||
|
||||
- [x] **Authentik Stage 2 – MAS Integration**
|
||||
- ✅ Authentik Admin UI: OIDC Provider erstellt (Authentik)
|
||||
- ✅ Authentik Admin UI: Application mit Slug `matrix` erstellt
|
||||
- ✅ Authentik Admin UI: Enrollment Flow mit Invitation Stage konfiguriert
|
||||
- ✅ Client ID + Secret kopiert
|
||||
- ✅ MAS `upstream_oauth2_config` mit Client Credentials aktualisiert
|
||||
- ✅ `passwords: enabled: false` aktiviert
|
||||
- ✅ Commit: `cdfbf7d` - Enable Authentik OIDC integration in MAS
|
||||
- ✅ Verified: Login mit Authentik funktioniert (2026-05-15)
|
||||
- Status: ✅ Deployed & Verified
|
||||
|
||||
### Phase 6: Dokumentation
|
||||
- [x] **Deployment Guides erstellen**
|
||||
- 5 Markdown-Dateien in `docs/deployment-guides/`
|
||||
- Chronologisch geordnet
|
||||
- Troubleshooting + Best Practices
|
||||
- Commit: `add-comprehensive-deployment-configuration-documentation`
|
||||
- Status: ✅ Deployed
|
||||
|
||||
- [x] **Gitea Wiki erstellen**
|
||||
- Home.md mit Navigation
|
||||
- Alle Deployment Guides in Root
|
||||
- Operations + Archive Dokumentation
|
||||
- Wiki Branch gepusht zu rohana.axion1337.de
|
||||
- Status: ✅ Live
|
||||
|
||||
- [x] **Gitea Issues & Project Board**
|
||||
- 8 Issues erstellt (#3-#10): 4 CRITICAL + 4 HIGH
|
||||
- Priority Labels: critical, high
|
||||
- Area Labels: authentik, security, database, infrastructure, element
|
||||
- Status: ✅ Tracking
|
||||
|
||||
### Phase 7: Infrastructure Security (Critical)
|
||||
- [x] **Hetzner Cloud Firewall Configuration**
|
||||
- SSH: Spezifische IPs (port 2248)
|
||||
- HTTP/HTTPS: Any IPv4/IPv6
|
||||
- TURN/STUN: WebRTC Ports
|
||||
- RTC Services: SFU + Auth Ports
|
||||
- Status: ✅ Optimiert & Deployed
|
||||
|
||||
- [x] **SSH Hardening**
|
||||
- PasswordAuthentication: no (key-only)
|
||||
- PermitRootLogin: no
|
||||
- MaxAuthTries: 3
|
||||
- Verified: 2026-05-15
|
||||
- Status: ✅ Complete
|
||||
|
||||
---
|
||||
|
||||
## 🔄 In Progress / Blocked
|
||||
|
||||
**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)
|
||||
|
||||
**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
|
||||
- [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/Dedizierte PostgreSQL-Migration** → [Issue #9](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/9)
|
||||
|
||||
(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
|
||||
- [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)** → [Issue #16](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/16)
|
||||
|
||||
### Federation & Access Control
|
||||
- [ ] **Federation-Allowlist oder Closed Federation** → [Issue #17](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/17)
|
||||
|
||||
### Moderation & Anti-Abuse
|
||||
- [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 vs. SOPS-Setup** → [Issue #20](https://rohana.axion1337.de/sorb/axion1337.chat-gitops/issues/20)
|
||||
|
||||
### Image & Dependency Management
|
||||
- [ ] **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
|
||||
- [ ] **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)
|
||||
- [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.**
|
||||
|
||||
- [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** → [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)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Versionierung
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
- External PostgreSQL migration (#9)
|
||||
|
||||
### 🟡 MEDIUM (do within 1 month)
|
||||
- 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)
|
||||
- External-Secrets upgrade
|
||||
- Advanced federation rules
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes & Decision Points
|
||||
|
||||
### Database: CloudNativePG vs. Hetzner Postgres
|
||||
- **CloudNativePG**: Open-source, runs on K3S, full control
|
||||
- **Hetzner Postgres**: Managed, backups included, less ops overhead
|
||||
- **Decision**: Recommend CloudNativePG for now (cost-effective), migrate to Hetzner later if operational overhead too high
|
||||
|
||||
### Federation: Allowlist vs. Closed?
|
||||
- **Allowlist**: Default federation with all public servers, can be attacked
|
||||
- **Closed**: Only federate with trusted servers (higher security, lower interop)
|
||||
- **Decision**: Depends on user intent. For now: allow all, add Mjolnir for abuse protection
|
||||
|
||||
### Security Framework
|
||||
- **Layers**: Perimeter (Firewall) → Host (SSH, auditd, hardening) → Cluster (NetworkPolicies, PSA, Falco) → App (Rate-limits, Mjolnir)
|
||||
- **Approach**: Implement incrementally, test after each layer
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Related Documentation
|
||||
|
||||
- `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+2 + Enrollment/Recovery/2FA)
|
||||
- `docs/deployment-guides/03-monitoring-integration.md` – Monitoring
|
||||
- `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-07-30
|
||||
@@ -0,0 +1,54 @@
|
||||
# TURN Server (coturn) für WebRTC Video-Calls
|
||||
|
||||
**Status**: ✅ Vollständig deployed
|
||||
**Domain**: `turn.axion1337.chat`
|
||||
**Public IP**: `49.13.132.245`
|
||||
|
||||
## Problem & Lösung
|
||||
|
||||
Videocalls scheitern mit DTLS-Timeout bei Clients hinter NAT/Firewall. **Lösung**: coturn als TURN-Relay.
|
||||
|
||||
## Architektur
|
||||
|
||||
Client A ──→ coturn (turn.axion1337.chat) ──→ Client B
|
||||
|
||||
- **Ports**: 3478/udp, 3478/tcp, 5349/tcp, 49152-65535/udp
|
||||
- **Auth**: HMAC-basiert mit Shared Secret zwischen coturn + Synapse
|
||||
- **Deployment**: K3S Deployment mit `hostNetwork: true`
|
||||
|
||||
## Dateien (in `apps/production/`)
|
||||
|
||||
| Datei | Inhalt |
|
||||
|-------|--------|
|
||||
| `coturn.yaml` | ConfigMap + Deployment + Service |
|
||||
| `coturn-secret.yaml` | SOPS-Secret: `TURN_SECRET` |
|
||||
| `custom-configs/synapse-values.yaml` | TURN URIs + shared secret |
|
||||
| `matrix-certificates.yaml` | cert-manager Cert für `turn.axion1337.chat` |
|
||||
|
||||
## DNS & Firewall (manuell)
|
||||
|
||||
```
|
||||
DNS A-Record: turn.axion1337.chat → 49.13.132.245
|
||||
|
||||
Firewall (K3S Host):
|
||||
ufw allow 3478/udp
|
||||
ufw allow 3478/tcp
|
||||
ufw allow 5349/tcp
|
||||
ufw allow 49152:65535/udp
|
||||
```
|
||||
|
||||
## Verifikation
|
||||
|
||||
```bash
|
||||
# Pod läuft?
|
||||
kubectl get pods -n matrix -l app=coturn
|
||||
|
||||
# Certificate ready?
|
||||
kubectl get certificate -n matrix | grep turn
|
||||
|
||||
# Extern testen
|
||||
docker run -it instrumentisto/coturn \
|
||||
turnutils_uclient -v -T -u test -w test turn.axion1337.chat
|
||||
```
|
||||
|
||||
**Weitere Details**: Siehe vollständige Dokumentation oben.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Authentik als Identity Provider für Matrix
|
||||
|
||||
**Status**: ✅ Deployed (Stage 1 + Stage 2 + Enrollment/Recovery/2FA, Closes Issue #7)
|
||||
**Domain**: `auth.axion1337.chat`
|
||||
|
||||
## Überblick
|
||||
|
||||
Authentik = OIDC Provider für MAS → Zentrales Login + Einladungs-basierte Registrierung.
|
||||
|
||||
## Stage 1: Authentik Deployment
|
||||
|
||||
**Dateien** (in `apps/authentik/`):
|
||||
- `namespace.yaml`, `helm-repo.yaml`, `authentik-secret.yaml` (SOPS)
|
||||
- `authentik.yaml` (HelmRelease v2026.x + embedded Postgres)
|
||||
- `certificate.yaml`, `ingress.yaml`
|
||||
|
||||
**Flux Kustomization**: `clusters/matrix/flux-system/authentik-sync.yaml`
|
||||
|
||||
## Deployment-Schritte
|
||||
|
||||
1. **DNS A-Record**: `auth.axion1337.chat → 49.13.132.245`
|
||||
2. **Pods hochfahren**: `kubectl get pods -n authentik -w`
|
||||
3. **Authentik UI**: `https://auth.axion1337.chat/if/flow/initial-setup/` → Admin-Passwort setzen
|
||||
4. **OIDC Provider**: Admin UI → OIDC Provider erstellen
|
||||
5. **Application**: Slug `matrix` (wichtig für Issuer URL!)
|
||||
6. **Redirect URIs**:
|
||||
- `https://account.axion1337.chat/upstream/callback/01KQDJTR1ZVTG8JQ220F5BNBFZ`
|
||||
- Post-logout: `https://axion1337.chat`
|
||||
7. **Client ID + Secret kopieren**
|
||||
|
||||
## Stage 2: MAS Integration
|
||||
|
||||
1. Decrypt: `sops --decrypt --in-place apps/production/custom-configs/mas-secret.yaml`
|
||||
2. `upstream_oauth2_config` + `passwords-config` Blöcke hinzufügen
|
||||
3. Encrypt: `sops --encrypt --in-place ...`
|
||||
4. Commit & Push
|
||||
5. **WICHTIG**: `passwords: enabled: false` erst nach OIDC-Test!
|
||||
|
||||
## Einladungs-Links
|
||||
|
||||
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`) — ⚠️ **gilt seit 2026-08-06 nur noch
|
||||
für Mitglieder, für Admins ist MFA Pflicht**, siehe unten. 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.
|
||||
|
||||
## MFA-Pflicht für Admins (2026-08-06, gitops#57)
|
||||
|
||||
**Für Mitglieder bleibt 2FA freiwillig, für die Gruppe `authentik Admins` ist sie Pflicht.**
|
||||
|
||||
Umgesetzt als eigener Blueprint `admin-mfa-enforcement.yaml` in
|
||||
`apps/authentik/authentik-blueprints.yaml`.
|
||||
|
||||
### Warum eine zweite Stage statt einer Umstellung
|
||||
|
||||
`not_configured_action` hängt an der **Stage**, nicht an der Bindung. Die vorhandene
|
||||
`default-authentication-mfa-validation` umzustellen hätte deshalb **alle Mitglieder**
|
||||
getroffen — und wäre zugleich eine Änderung an einem Objekt aus Authentiks eigenem
|
||||
Blueprint gewesen.
|
||||
|
||||
Stattdessen: eine eigene `admin-mfa-validation` auf Ordnung **31**, direkt hinter der
|
||||
Standard-Stage (30) und vor dem Login (100), eingeschränkt über eine `PolicyBinding`
|
||||
mit gesetztem `group`. Eine solche Bindung wertet Gruppenmitgliedschaft aus
|
||||
(`PolicyResult(group.is_member(user))`). **Kein Authentik-Standardobjekt wird
|
||||
angefasst.**
|
||||
|
||||
### Die drei Einstellungen, auf die es ankommt
|
||||
|
||||
| Feld | Wert | Warum |
|
||||
|---|---|---|
|
||||
| `not_configured_action` | `configure` | führt durch die Einrichtung, statt auszusperren |
|
||||
| `configuration_stages` | TOTP + WebAuthn | sonst kann `configure` nichts anbieten |
|
||||
| `last_auth_threshold` | `hours=1` | verhindert die doppelte Abfrage |
|
||||
|
||||
⚠️ **`configure`, niemals `deny`.** `deny` weist Admins ohne zweiten Faktor ab — und
|
||||
danach gibt es keinen Weg zurück außer über den Cluster. `configure` erzwingt
|
||||
genauso, führt aber durch die Einrichtung.
|
||||
|
||||
⚠️ **`last_auth_threshold` ist kein Beiwerk.** Die Standard-Stage auf Ordnung 30
|
||||
validiert bereits, wer einen Faktor besitzt. Ohne Schwelle (`seconds=0`, der Default)
|
||||
würde unsere Stage direkt danach ein zweites Mal fragen. Mit `hours=1` überspringt
|
||||
sie sich, wenn das Gerät gerade benutzt wurde — übrig bleibt genau der Zielfall:
|
||||
Admin ohne zweiten Faktor.
|
||||
|
||||
### Die Falle beim Ändern
|
||||
|
||||
**Der Gruppenname ist die ganze Wirkung.** Stimmt er nicht, greift die Regel für
|
||||
**niemanden** — und wirft dabei keinen Fehler. Das ist schlechter als keine Regel,
|
||||
weil es sich sicher anfühlt. Wer die Gruppe umbenennt, muss den Blueprint mitziehen.
|
||||
|
||||
Prüfen lässt sich die Wirkung nur an einem Konto, das in der Gruppe ist: anmelden und
|
||||
sehen, ob nach dem Passwort die Einrichtung kommt.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Monitoring: Alloy → Prometheus/Loki auf Selendis
|
||||
|
||||
**Status**: ✅ Vollständig deployed
|
||||
**Remote Storage**: `10.0.0.3:9090` (Prometheus), `10.0.0.3:3100` (Loki)
|
||||
|
||||
## Überblick
|
||||
|
||||
Alloy (Grafana Agent) sammelt Metriken & Logs vom K3S-Cluster und schickt sie zu Prometheus/Loki auf Selendis.
|
||||
|
||||
## Komponenten
|
||||
|
||||
| Komponente | Rolle |
|
||||
|-----------|-------|
|
||||
| **Alloy** | Metrics & Logs Collector |
|
||||
| **kube-state-metrics** | Kubernetes Object Status |
|
||||
| **node-exporter** | Host Metrics (CPU, Memory, Disk) |
|
||||
| **Prometheus** (Selendis) | Metrics Ingestion |
|
||||
| **Loki** (Selendis) | Logs Ingestion |
|
||||
|
||||
## Dateien (in `apps/monitoring/`)
|
||||
|
||||
- `namespace.yaml`
|
||||
- `helm-repos.yaml` (prometheus-community, grafana)
|
||||
- `kube-state-metrics.yaml`, `node-exporter.yaml`
|
||||
- `alloy-config.yaml` (River config with scrape targets + remote write)
|
||||
- `alloy.yaml` (HelmRelease)
|
||||
|
||||
## Scrape Targets
|
||||
|
||||
Alloy scraped:
|
||||
- **Flux Controllers** (flux-system ns, port 8080)
|
||||
- **kube-state-metrics** (monitoring:8080)
|
||||
- **node-exporter** (monitoring:9100)
|
||||
- **Synapse** (matrix.axion1337.chat:9000)
|
||||
|
||||
Alle Remote Write zu `10.0.0.3:9090` (Prometheus) + `10.0.0.3:3100` (Loki).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
```bash
|
||||
# Check Alloy Logs
|
||||
kubectl logs -n monitoring -l app.kubernetes.io/name=alloy
|
||||
|
||||
# Check Prometheus remote write
|
||||
curl http://10.0.0.3:9090/api/v1/query?query=up
|
||||
|
||||
# Loki test
|
||||
curl -s http://10.0.0.3:3100/loki/api/v1/query_range?query=%7B%7D | jq .
|
||||
```
|
||||
|
||||
---
|
||||
**Weitere Details**: Siehe Kapitel 3.
|
||||
@@ -0,0 +1,113 @@
|
||||
# Element Web Customization: Themes, Desktop-Apps, Admin
|
||||
|
||||
**Status**: ✅ Vollständig deployed
|
||||
**Domains**: `axion1337.chat` (Web), `/docs/setup` (Scripts)
|
||||
|
||||
## 1. Custom Themes (17 Stück, Stand 2026-08-06)
|
||||
|
||||
⚠️ Diese Zahl und die Namen waren hier bis 2026-08-09 veraltet (stand auf „7
|
||||
Stück", nannte u. a. „Gruvbox Dark" und „Wal", die es in der Config so nicht
|
||||
gibt) — korrigiert, nachdem der tatsächliche Bestand gegen `element-values.yaml`
|
||||
geprüft wurde.
|
||||
|
||||
Die Liste **mit Farbwerten und hell/dunkel-Kennzeichnung** lebt bewusst nur an
|
||||
einer Stelle, um genau dieses Auseinanderlaufen nicht zu wiederholen:
|
||||
[`management/shared/branding.md`](https://git.lab/axion1337.chat/management/-/blob/main/shared/branding.md#theme-factory--die-zehn-benannten-themes)
|
||||
(Abschnitt „theme-factory" für die zehn neueren, „Stammschema" für die sieben
|
||||
älteren).
|
||||
|
||||
**Konfiguration**: `apps/production/custom-configs/element-values.yaml`
|
||||
|
||||
**Anwendung (User)**: Settings → Appearance → Colour theme
|
||||
|
||||
## 2. Desktop-Setup-Scripts
|
||||
|
||||
| System | Datei |
|
||||
|--------|-------|
|
||||
| Windows | `element-setup-windows.cmd` (Doppelklick) |
|
||||
| macOS | `element-setup-macos.command` (Doppelklick) |
|
||||
| Linux | `element-setup-linux.sh` (bash) |
|
||||
|
||||
**Was die Scripts tun**:
|
||||
1. config.json erstellen mit `configUrl: "https://axion1337.chat/config.json"`
|
||||
2. Element installieren (WinGet / Homebrew / apt/dnf/pacman)
|
||||
3. Element starten (auto-config laden)
|
||||
|
||||
**Download**: `https://axion1337.chat/docs/setup/`
|
||||
|
||||
## 3. Element Admin-Panel
|
||||
|
||||
**URL**: `https://admin.axion1337.chat`
|
||||
|
||||
- User verwalten
|
||||
- Room durchsuchen
|
||||
- Server-Statistiken
|
||||
|
||||
**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 |
|
||||
|-------|-----|
|
||||
| Custom Themes | `element-values.yaml` ConfigMap |
|
||||
| Setup-Scripts | `element-web-docs-configmap.yaml` |
|
||||
| Docs Server | `element-web-docs-server.yaml` (nginx) |
|
||||
| Ingress | `apex-ingress.yaml` (`/docs/setup/` route) |
|
||||
|
||||
---
|
||||
**Weitere Details**: Siehe Kapitel 4.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Room Policies: Retention, Publication, Auto-Join
|
||||
|
||||
**Status**: ✅ Vollständig deployed
|
||||
**Konfiguration**: `apps/production/custom-configs/synapse-values.yaml`
|
||||
|
||||
## 1. Message Retention
|
||||
|
||||
Alte Nachrichten automatisch löschen (Speicher sparen, DSGVO).
|
||||
|
||||
```yaml
|
||||
retention:
|
||||
enabled: true
|
||||
default_policy:
|
||||
min_lifetime: 1d # Messages bleiben ≥1d
|
||||
max_lifetime: 1y # Messages gelöscht nach 1 Jahr
|
||||
|
||||
media_retention:
|
||||
local_media_lifetime: 365d # 1 Jahr
|
||||
remote_media_lifetime: 90d # 90 Tage
|
||||
|
||||
redaction_retention_period: 7d # Gelöschte Messages noch 7d sichtbar
|
||||
```
|
||||
|
||||
## 2. Room Publication Rules
|
||||
|
||||
Kontrollieren welche Rooms im öffentlichen Directory sichtbar sind.
|
||||
|
||||
```yaml
|
||||
room_list_publication_rules:
|
||||
- user_id: "*" # Alle User
|
||||
action: allow # dürfen ihre Rooms publishen
|
||||
```
|
||||
|
||||
**Alternativ (restrictiv)**: Nur Admins publishen
|
||||
```yaml
|
||||
room_list_publication_rules:
|
||||
- user_id: "@admin:axion1337.chat"
|
||||
action: allow
|
||||
- user_id: "*"
|
||||
action: deny
|
||||
```
|
||||
|
||||
## 3. Auto-Join Rooms
|
||||
|
||||
Neue User automatisch in bestimmte Rooms hinzufügen (Onboarding).
|
||||
|
||||
```yaml
|
||||
auto_join_rooms:
|
||||
- "!announcements:axion1337.chat"
|
||||
- "!rules:axion1337.chat"
|
||||
auto_join_rooms_for_guests: false # Nur registered users
|
||||
```
|
||||
|
||||
**Room ID finden**: Element Settings → Advanced → Room ID
|
||||
|
||||
## Deployment
|
||||
|
||||
```bash
|
||||
# Edit ConfigMap
|
||||
kubectl apply -f apps/production/custom-configs/synapse-values.yaml
|
||||
|
||||
# Synapse neustarten
|
||||
kubectl rollout restart deployment -n matrix matrix-stack-synapse
|
||||
|
||||
# Verify
|
||||
kubectl logs -n matrix -l app.kubernetes.io/name=synapse | grep -i "retention\|publication"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Privater Server**:
|
||||
- max_lifetime: 1y (großzügig)
|
||||
- action: allow (alle publishen)
|
||||
- auto_join_rooms: announcements + rules
|
||||
|
||||
**Öffentlicher Server (DSGWR)**:
|
||||
- max_lifetime: 90d (kurz)
|
||||
- action: deny (nur Admins)
|
||||
- auto_join_rooms: [] (keine Zwangs-Rooms)
|
||||
|
||||
---
|
||||
**Weitere Details**: Siehe Kapitel 5.
|
||||
@@ -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
|
||||
@@ -0,0 +1,107 @@
|
||||
# @concierge — Gäste-Einladungen mit Ablauf
|
||||
|
||||
**Status**: gebaut, wartet auf Zugangsdaten · Issue [#48](https://git.lab/axion1337.chat/axion1337.chat-gitops/-/issues/48)
|
||||
|
||||
Ein kleiner Bot, der Einladungslinks erzeugt, Gastkonten nach drei Tagen ablaufen
|
||||
lässt und die dauerhafte Freischaltung an eine bewusste Admin-Handlung bindet.
|
||||
|
||||
## Warum es diesen Bot gibt
|
||||
|
||||
Registrierung läuft in diesem Stack **ausschließlich über Authentik**. Bis jetzt
|
||||
hieß das: Wer jemanden einladen will, klickt in der Authentik-Oberfläche einen
|
||||
Invitation-Token zusammen. Das können nur Admins, es hinterlässt keine Spur, wer
|
||||
wen eingeladen hat, und ein Gastkonto bleibt für immer bestehen.
|
||||
|
||||
Der Bot macht daraus einen Vorgang, den ein festgelegter Kreis selbst auslösen
|
||||
kann — nachvollziehbar und mit eingebautem Ablauf.
|
||||
|
||||
## Wie es funktioniert
|
||||
|
||||
```
|
||||
!einladen <name> → Authentik-Invitation (einmalig, 3 Tage) + Link im Raum
|
||||
Gast registriert sich → Konto trägt threadnet_guest_expires_at
|
||||
!verlaengern @gast → +1 Tag, höchstens 2×
|
||||
!freischalten @gast → Ablauf entfernen, in members-Gruppe (nur Admins)
|
||||
(nichts davon) → Bot deaktiviert das Konto nach Ablauf
|
||||
```
|
||||
|
||||
### Berechtigung ist zweiteilig — und das ist Absicht
|
||||
|
||||
**Authentik-Gruppe UND Einladungsraum.** Die Gruppe entscheidet, der Raum macht
|
||||
sichtbar. Eine Gruppe allein ist unsichtbar: Niemand sähe, wer eingeladen hat.
|
||||
Ein Raum allein autorisiert nicht: Wer hineinkommt, dürfte alles. Zusammen ergibt
|
||||
sich beides, und jede Einladung hinterlässt einen nachlesbaren Eintrag.
|
||||
|
||||
### Zwei Dinge, die beim Umbauen leicht kaputtgehen
|
||||
|
||||
⚠️ **Genau eine Instanz.** `replicas: 1` **und** `strategy: Recreate`. Der Bot
|
||||
hält eine `/sync`-Schleife; zwei Instanzen führen jedes Kommando doppelt aus. Bei
|
||||
`RollingUpdate` liefen während eines Deploys kurzzeitig zwei.
|
||||
|
||||
⚠️ **Die ConfigMap trägt bewusst einen Namens-Hash.** Anders als beim
|
||||
ClamAV-Modul steht hier **kein** `disableNameSuffixHash: true`. Dadurch ändert
|
||||
sich der ConfigMap-Name mit dem Skript, kustomize zieht die Referenz nach, und
|
||||
der Pod startet von selbst neu. Ohne das hätten wir den Fall aus gitops#50:
|
||||
geänderte Datei im Repo, alter Stand im laufenden Prozess.
|
||||
|
||||
### Fehlerverhalten ist absichtlich unsymmetrisch
|
||||
|
||||
- **Einladen und Freischalten scheitern laut.** Lieber keine Einladung als eine,
|
||||
von der niemand weiß.
|
||||
- **Die Ablaufprüfung deaktiviert nur, wenn Authentik sauber geantwortet hat.**
|
||||
Ein API-Fehler darf nicht dazu führen, dass Konten reihenweise abgeschaltet
|
||||
werden; im Zweifel bleibt ein Gast einen Durchlauf länger aktiv.
|
||||
|
||||
## Was zur Inbetriebnahme fehlt
|
||||
|
||||
Der Bot ist ausgerollt, **startet aber nicht**, solange das Secret fehlt — der Pod
|
||||
meldet `secret "concierge-credentials" not found`. Das ist gewollt sichtbar; ein
|
||||
Bot, der still nichts tut, wäre schlechter.
|
||||
|
||||
### 1. Matrix-Konto anlegen
|
||||
|
||||
```bash
|
||||
kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \
|
||||
mas-cli manage register-user concierge --yes
|
||||
kubectl exec -it -n matrix deploy/matrix-stack-matrix-authentication-service -- \
|
||||
mas-cli manage issue-compatibility-token concierge
|
||||
```
|
||||
|
||||
### 2. Authentik-Token
|
||||
|
||||
*Admin → Verzeichnis → Tokens*. Braucht Schreibrechte auf Nutzer, Gruppen und
|
||||
Invitations. Ein eigenes Dienstkonto ist sauberer als ein Admin-Token.
|
||||
|
||||
### 3. Einladungsraum
|
||||
|
||||
Invite-only anlegen, `@concierge` einladen. Die Raum-ID ist Teil des Secrets, weil
|
||||
sie zusammen mit den Token gepflegt wird und sich beim Neuanlegen ändert.
|
||||
|
||||
### 4. Gruppen in Authentik
|
||||
|
||||
`invite-berechtigt` (wer einladen darf) und `members` (wohin Freigeschaltete
|
||||
kommen). Namen sind über `INVITE_GROUP` / `MEMBER_GROUP` änderbar.
|
||||
|
||||
### 5. Secret
|
||||
|
||||
```bash
|
||||
kubectl create secret generic concierge-credentials -n matrix \
|
||||
--from-literal=matrix-token='…' \
|
||||
--from-literal=authentik-token='…' \
|
||||
--from-literal=matrix-room-id='!….:axion1337.chat' \
|
||||
--dry-run=client -o yaml > /tmp/s.yaml
|
||||
sops -e /tmp/s.yaml > apps/production/concierge-secret.yaml
|
||||
```
|
||||
|
||||
⚠️ Über SOPS ins Repo, nicht mit `kubectl apply` von Hand — sonst kennt Flux das
|
||||
Secret nicht und es fehlt nach einem Wiederaufbau des Clusters.
|
||||
|
||||
## Grenzen
|
||||
|
||||
- **Matrix-Localpart = Authentik-Benutzername.** Gilt hier, weil MAS aus
|
||||
Authentik provisioniert. Stimmt es einmal nicht, findet der Bot den Nutzer
|
||||
nicht und sagt das — er rät nicht.
|
||||
- **Ein deaktiviertes Konto ist nicht gelöscht.** Räume und Nachrichten bleiben.
|
||||
Löschen ist bewusst nicht Sache des Bots.
|
||||
- **Draupnir-Verzahnung** (Gast-Label → eingeschränkte Räume) ist Stufe 2 und
|
||||
nicht Teil dieser Fassung.
|
||||
@@ -0,0 +1,268 @@
|
||||
# Docusaurus-Wiki hinter Authentik (Forward-Auth)
|
||||
|
||||
**Status**: vorbereitet, **nicht** ausgerollt · Host: **`axionwiki.lab`**
|
||||
(#0024 entschieden 2026-08-12) · Entscheidung: bei Docusaurus bleiben, Zugang per
|
||||
Authentik (DOC-03/#0020)
|
||||
|
||||
Docusaurus ist ein **statischer** Site-Generator — kein Nutzermodell, kein Login.
|
||||
Der Zugang wird deshalb **im Reverse-Proxy davor** erzwungen: Traefik fragt bei
|
||||
jedem Request einen Authentik-**Outpost**, ob eine gültige Sitzung vorliegt, und
|
||||
leitet sonst zu `auth.axion1337.chat` um. Erst nach Login gibt Traefik die
|
||||
statischen Seiten frei. Es ist ein **Alles-oder-nichts-Tor** (wer in der Gruppe
|
||||
`wiki-zugang` ist, sieht das ganze Wiki; sonst nichts) — für Bereichs-Rechte
|
||||
bräuchte es BookStack (ADR-0007), das war aber ausdrücklich nicht gewollt.
|
||||
|
||||
## ⚠️ Nur für die Entwicklung — das hier ist ein Zwischenstand
|
||||
|
||||
Diese Fassung (Docusaurus auf Overmind unter `axionwiki.lab`, Forward-Auth über
|
||||
den Prod-Authentik) trägt **während der Entwicklung**. Zwei Dinge stehen bewusst
|
||||
noch aus und dürfen dabei nicht in Vergessenheit geraten:
|
||||
|
||||
1. **Das Wiki zieht in die ThreadNet Server Suite um.** Es soll Teil des
|
||||
reproduzierbaren Stacks werden (Vision „reproduzierbar für Dritte"), nicht ein
|
||||
Einzelstück auf dem Lab-Host. Dann ändern sich Host, Proxy und ggf. die
|
||||
Auth-Anbindung erneut. → **[management-Issue: Wiki in die ThreadNet Server
|
||||
Suite umziehen]**
|
||||
2. **Danach werden Oberflächen-Alternativen über BookStack hinaus geprüft.** Die
|
||||
Docusaurus-Entscheidung gilt für jetzt; die breitere Evaluation (nicht nur
|
||||
Docusaurus vs. BookStack) kommt nach dem Umzug. → **[management-Issue:
|
||||
Wiki-Oberfläche über BookStack hinaus prüfen]**
|
||||
|
||||
Alles unten ist deshalb so gebaut, dass es **jetzt** funktioniert und beim Umzug
|
||||
**sauber ablösbar** ist (eigener Outpost, keine Verdrahtung in fremde Stacks).
|
||||
|
||||
## Topologie
|
||||
|
||||
Das Wiki läuft als Dokploy-Stack **auf Overmind** (`git.lab/homelab/wiki`,
|
||||
`axionwiki.lab`), Authentik im **K3s-Cluster auf Hetzner**. Deshalb ein
|
||||
**eigener Proxy-Outpost als Container auf Overmind**, statt des eingebetteten
|
||||
Outposts in Hetzner: So bleiben die Auth-Subrequests lokal auf Overmind — nur die
|
||||
**Login-Umleitung** quert den Site-to-Site-VPN zu `auth.axion1337.chat`. Das passt
|
||||
zur Leitlinie „das Lab hängt nicht am Prod-Host": ist eine Sitzung erst gesetzt,
|
||||
liest sich das Wiki im Lab auch ohne ständige Rückfrage nach Hetzner.
|
||||
|
||||
```
|
||||
Browser ──▶ Traefik (Overmind) ──forwardAuth──▶ authentik-proxy (Overmind, :9000)
|
||||
│ │ Sitzung? nein ──▶ 302
|
||||
└────────────── Login-Redirect ─────────────▶ auth.axion1337.chat (Hetzner, via VPN)
|
||||
```
|
||||
|
||||
## Teil 1 — Authentik-Blueprint (deklarativ)
|
||||
|
||||
Als neues Dokument in `apps/authentik/authentik-blueprints.yaml` einfügen
|
||||
(gleiche ConfigMap wie `matrix-oidc-provider.yaml`). Host ist entschieden
|
||||
(`axionwiki.lab`), also apply-fertig — trotzdem erst mit Teil 2/3 zusammen scharf
|
||||
schalten.
|
||||
|
||||
```yaml
|
||||
wiki-forward-auth.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: wiki-forward-auth
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
# Proxy-Provider im Forward-Auth-Modus. Kein internal_host (das wäre der
|
||||
# Proxy-Modus) — forward_single reicht Traefik nur das Ja/Nein zurück.
|
||||
- model: authentik_providers_proxy.proxyprovider
|
||||
state: present
|
||||
identifiers:
|
||||
name: ThreadNet Wiki
|
||||
id: wiki_proxy_provider
|
||||
attrs:
|
||||
mode: forward_single
|
||||
external_host: https://axionwiki.lab
|
||||
authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
|
||||
invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
|
||||
access_token_validity: hours=24
|
||||
|
||||
- model: authentik_core.application
|
||||
state: present
|
||||
identifiers:
|
||||
slug: wiki
|
||||
id: wiki_app
|
||||
attrs:
|
||||
name: ThreadNet Wiki
|
||||
provider: !KeyOf wiki_proxy_provider
|
||||
meta_description: Internes Docusaurus-Wiki, nur Gruppe wiki-zugang
|
||||
policy_engine_mode: any
|
||||
open_in_new_tab: true
|
||||
|
||||
# Zugangsbeschränkung: nur Mitglieder dieser Gruppe passieren das Tor.
|
||||
- model: authentik_core.group
|
||||
state: present
|
||||
identifiers:
|
||||
name: wiki-zugang
|
||||
id: wiki_group
|
||||
|
||||
- model: authentik_policies.policybinding
|
||||
state: present
|
||||
identifiers:
|
||||
target: !KeyOf wiki_app
|
||||
group: !KeyOf wiki_group
|
||||
order: 0
|
||||
attrs:
|
||||
enabled: true
|
||||
negate: false
|
||||
```
|
||||
|
||||
⚠️ **Vor dem Merge von Hand verifizieren**, nicht auf Flux vertrauen — ein
|
||||
Blueprint-Fehler scheitert still (Lehre aus dem `matrix-recovery`-Fix). Nach dem
|
||||
Einspielen prüfen, dass der Lauf `successful` ist:
|
||||
|
||||
```bash
|
||||
kubectl exec -n authentik authentik-postgresql-0 -- sh -c \
|
||||
'PGPASSWORD="$(cat "$POSTGRES_PASSWORD_FILE")" psql -U authentik -d authentik -At -c \
|
||||
"SELECT name, status FROM authentik_blueprints_blueprintinstance WHERE name='"'"'wiki-forward-auth'"'"'"'
|
||||
```
|
||||
|
||||
## Teil 2 — Outpost + Token in Authentik (**sorb**, Schritt für Schritt)
|
||||
|
||||
Der Outpost ist der Prozess, den Traefik fragt; sein **Token** ist ein Credential
|
||||
und kommt **nicht** ins Repo.
|
||||
|
||||
**2.1 — Laufende Authentik-Version feststellen** (der Outpost-Container muss
|
||||
*exakt* dieselbe Version tragen, sonst verweigert Authentik die Anmeldung des
|
||||
Outposts):
|
||||
```bash
|
||||
kubectl -n authentik get deploy authentik-server \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].image}'; echo
|
||||
# -> ghcr.io/goauthentik/server:2024.x.y --> merke dir 2024.x.y
|
||||
```
|
||||
|
||||
**2.2 — Outpost anlegen:** Authentik-Admin → *Applications → **Outposts*** (die
|
||||
Liste der Outposts) → **Create**.
|
||||
|
||||
⚠️ **Nicht „Outpost-Integrationen".** Das ist ein *anderer* Menüpunkt: dort legt
|
||||
man eine Docker-/K8s-**Service-Verbindung** an, über die Authentik einen Outpost
|
||||
selbst ausrollt — der Dialog „Neue Outpost-Integration" bietet deshalb nur
|
||||
*Docker* oder *Kubernetes* und lässt sich nicht leer lassen. **Brauchen wir nicht**
|
||||
(Prod würde sonst in Overminds Docker greifen). Wenn du dort gelandet bist:
|
||||
Abbrechen und in *Outposts* wechseln.
|
||||
|
||||
Im **Outpost**-Formular:
|
||||
- **Name**: `wiki-forward-auth`
|
||||
- **Type**: `Proxy`
|
||||
- **Integration**: das **Dropdown auf „No integration"/leer stehen lassen** (der
|
||||
Standard) — der Container läuft extern auf Overmind (Teil 3) und meldet sich per
|
||||
Token zurück. Nur wenn du dieses Feld auf Docker/K8s stellst, verlangt es eine
|
||||
Service-Verbindung.
|
||||
- **Applications**: **ThreadNet Wiki** auswählen.
|
||||
- *Advanced settings → `authentik_host`*: `https://auth.axion1337.chat` (die
|
||||
URL, die der Container **und** der Browser fürs Login erreichen).
|
||||
|
||||
**2.3 — Token abgreifen:** beim neuen Outpost auf *View Deployment Info* (bzw.
|
||||
*Directory → Tokens*, Eintrag `ak-outpost-<id>-api`) → **Token kopieren**. Dieser
|
||||
Wert wird in Teil 3 als `WIKI_OUTPOST_TOKEN` gesetzt — sonst nirgends hin, nicht
|
||||
loggen, nicht committen.
|
||||
|
||||
**2.4 — Wer rein darf:** *Directory → Groups → `wiki-zugang`* (vom Blueprint
|
||||
angelegt) → die internen Nutzer hinzufügen. Wer nicht drin ist, bekommt nach dem
|
||||
Login **403**.
|
||||
|
||||
## Teil 3 — Overmind: Outpost-Container + Traefik (`git.lab/homelab/wiki`)
|
||||
|
||||
Beides gehört in den Dokploy-Stack des Wikis. **Kernpunkte zuerst**, dann das
|
||||
Fragment:
|
||||
|
||||
- **Gemeinsames Netz.** Outpost- und Wiki-Container müssen im selben von Traefik
|
||||
beobachteten Docker-Netz liegen (bei Dokploy i. d. R. `dokploy-network`) — sonst
|
||||
findet die `forwardAuth`-Adresse den Outpost nicht.
|
||||
- **Version pinnen** auf die aus 2.1 ermittelte (`proxy:2024.x.y` == `server`).
|
||||
- **`AUTHENTIK_INSECURE: "false"`** — `auth.axion1337.chat` hat ein gültiges
|
||||
öffentliches Zertifikat; kein Lab-CA-Trust nötig, weil der Outpost *nach Hetzner*
|
||||
spricht, nicht ins Lab.
|
||||
- **Der `/outpost.goauthentik.io/`-Router bekommt die Auth-Middleware NICHT** —
|
||||
sonst schützt sich der Login-Callback selbst aus und es entsteht eine
|
||||
Redirect-Schleife.
|
||||
- **Das Token** kommt als Dokploy-Environment/Secret `WIKI_OUTPOST_TOKEN`, nicht
|
||||
im Klartext in die committete Compose-Datei.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# 1) Der Proxy-Outpost — er beantwortet Traefiks forwardAuth-Frage.
|
||||
authentik-proxy:
|
||||
image: ghcr.io/goauthentik/proxy:2024.x.y # == laufende Authentik-Version (2.1)
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
AUTHENTIK_HOST: https://auth.axion1337.chat
|
||||
AUTHENTIK_INSECURE: "false"
|
||||
AUTHENTIK_TOKEN: ${WIKI_OUTPOST_TOKEN} # aus Teil 2.3 (Dokploy-Secret)
|
||||
networks: [dokploy-network]
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: dokploy-network
|
||||
# Router NUR für den Outpost-Callback-Pfad — OHNE Auth-Middleware:
|
||||
traefik.http.routers.wiki-authentik.rule: "Host(`axionwiki.lab`) && PathPrefix(`/outpost.goauthentik.io/`)"
|
||||
traefik.http.routers.wiki-authentik.entrypoints: websecure
|
||||
traefik.http.routers.wiki-authentik.tls: "true"
|
||||
traefik.http.routers.wiki-authentik.service: wiki-authentik
|
||||
traefik.http.services.wiki-authentik.loadbalancer.server.port: "9000"
|
||||
|
||||
# 2) Der bestehende Docusaurus-Service — nur um die Auth-Middleware erweitert.
|
||||
wiki:
|
||||
# ... bestehendes image/build/volumes ...
|
||||
networks: [dokploy-network]
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: dokploy-network
|
||||
traefik.http.routers.wiki.rule: "Host(`axionwiki.lab`)"
|
||||
traefik.http.routers.wiki.entrypoints: websecure
|
||||
traefik.http.routers.wiki.tls: "true"
|
||||
traefik.http.routers.wiki.middlewares: "wiki-auth@docker"
|
||||
# Die forwardAuth-Middleware:
|
||||
traefik.http.middlewares.wiki-auth.forwardauth.address: "http://authentik-proxy:9000/outpost.goauthentik.io/auth/traefik"
|
||||
traefik.http.middlewares.wiki-auth.forwardauth.trustForwardHeader: "true"
|
||||
traefik.http.middlewares.wiki-auth.forwardauth.authResponseHeaders: "X-authentik-username,X-authentik-groups,X-authentik-entitlements,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version"
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
```
|
||||
|
||||
**Ablauf einer Anfrage** (zum Nachvollziehen beim Debuggen):
|
||||
1. Browser → `axionwiki.lab`; Traefik ruft `wiki-auth` → `authentik-proxy:9000/.../auth/traefik`.
|
||||
2. Keine Sitzung → Outpost antwortet 302 auf `auth.axion1337.chat` (Login).
|
||||
3. Nach Login kommt der Browser auf `axionwiki.lab/outpost.goauthentik.io/callback`
|
||||
zurück (der Router aus Service 1, **ohne** Middleware), Outpost setzt das Cookie.
|
||||
4. Erneuter Request trägt das Cookie → `auth/traefik` gibt 200 + die
|
||||
`X-authentik-*`-Header → Traefik reicht an Docusaurus durch.
|
||||
|
||||
## Abhängigkeiten
|
||||
|
||||
- **#0024 (Wiki-Hostname)** — **entschieden: `axionwiki.lab`** (2026-08-12).
|
||||
`external_host`, der Traefik-`Host()` und der Cookie-Scope zeigen alle darauf.
|
||||
- **#0018 (Wiki-Rollout)** — sollte abgeschlossen sein, bevor das Tor davor kommt.
|
||||
- **Site-to-Site-VPN** — die Login-Umleitung und die Outpost-Konfigsync brauchen
|
||||
`auth.axion1337.chat` erreichbar; interne Nutzer im Lab/über VPN erreichen beides.
|
||||
|
||||
## Verifikation
|
||||
|
||||
1. `curl -sI https://axionwiki.lab` (unauthentifiziert) → **302** auf
|
||||
`auth.axion1337.chat`.
|
||||
2. Login als `wiki-zugang`-Mitglied → Wiki lädt.
|
||||
3. Login als Nicht-Mitglied → **403** (Policy greift).
|
||||
4. Outpost-Gesundheit: im Authentik-Admin zeigt der Outpost `wiki-forward-auth`
|
||||
**grün/last seen** und die passende Version.
|
||||
5. Resilienz-Gegenprobe: `auth.axion1337.chat` kurz nicht erreichbar → bestehende
|
||||
Sitzung liest weiter (lokaler Outpost), nur neuer Login blockiert.
|
||||
|
||||
## Fehlerbild
|
||||
|
||||
- **„Anfrage wurde verweigert — Regelwerk Bindung … resultiert in False"** nach
|
||||
dem Login: Du bist **nicht in `wiki-zugang`** (Schritt 2.4 übersprungen). Das
|
||||
`policy=None` in der Meldung ist normal — es ist eine *Gruppen*-Bindung. Fix:
|
||||
*Directory → Groups → wiki-zugang* → Nutzer hinzufügen, neu einloggen.
|
||||
- **Redirect-Schleife**: der `/outpost.goauthentik.io/`-Router hat versehentlich
|
||||
die Auth-Middleware (Teil 3) — entfernen.
|
||||
- **Outpost bleibt in Authentik „offline"/rot**: Version des `proxy`-Containers
|
||||
passt nicht zur Server-Version (2.1) oder Token/`AUTHENTIK_HOST` falsch.
|
||||
|
||||
## Was hier bewusst offen bleibt
|
||||
|
||||
- **Nichts ist live geschaltet** — der Blueprint liegt als Vorlage hier, nicht in
|
||||
der angewandten ConfigMap; der Outpost-Token ist sorbs Schritt.
|
||||
- **Kein Bereichs-Schutz** — Grenze von Docusaurus + Forward-Auth.
|
||||
- **Zwischenstand** — siehe „Nur für die Entwicklung" oben: Umzug in die ThreadNet
|
||||
Server Suite und die breitere Oberflächen-Evaluation stehen noch aus.
|
||||
@@ -0,0 +1,155 @@
|
||||
# Wiki.js: Zugang (wiki.axion1337.chat) + Authentik-OIDC
|
||||
|
||||
**Status**: vorbereitet, **nicht** ausgerollt · gehört zu #0048 (Ingress/Cert) und
|
||||
#0049 (OIDC) · **löst den Forward-Auth-Zwischenstand aus Guide 09 ab** (der war nur
|
||||
für das statische Docusaurus nötig)
|
||||
|
||||
## Wie das Wiki aufgerufen wird
|
||||
|
||||
**Anwender und Admin öffnen dieselbe URL: `https://wiki.axion1337.chat`.** Wiki.js
|
||||
zeigt seine eigene Login-Seite → „Login with Authentik" → Redirect zu
|
||||
`auth.axion1337.chat` → zurück. **Natives OIDC, kein Forward-Auth/Outpost** —
|
||||
Wiki.js authentifiziert selbst. Nach dem Login entscheidet die Authentik-Gruppe
|
||||
über Sicht und Bearbeiten (Rollen/Abschottung, #0049). Öffentlich wie der Rest der
|
||||
Plattform; Abschottung über Login + Rolle, nicht übers Netz.
|
||||
|
||||
## Teil 1 — Ingress + Zertifikat (`apps/production/wiki-ingress.yaml`)
|
||||
|
||||
Muster: `apps/authentik/certificate.yaml` + `apps/authentik/ingress.yaml`.
|
||||
Namespace = der der Wiki.js-Deployment (#0048); hier als `matrix` angenommen.
|
||||
Service-Name/Port (`wikijs:3000`) müssen zum Deployment passen.
|
||||
|
||||
```yaml
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: wiki-axion1337-chat-cert
|
||||
namespace: matrix
|
||||
spec:
|
||||
secretName: wiki-axion1337-chat-tls
|
||||
issuerRef:
|
||||
name: letsencrypt-prod
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- wiki.axion1337.chat
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: wikijs
|
||||
namespace: matrix
|
||||
spec:
|
||||
entryPoints: [websecure]
|
||||
tls:
|
||||
secretName: wiki-axion1337-chat-tls
|
||||
routes:
|
||||
- match: Host(`wiki.axion1337.chat`)
|
||||
kind: Rule
|
||||
services:
|
||||
- name: wikijs
|
||||
port: 3000
|
||||
```
|
||||
|
||||
Neue Datei in die Kustomization des `production`-Ordners aufnehmen. DNS: A-Record
|
||||
`wiki.axion1337.chat → 49.13.132.245` ist von sorb angelegt (Propagation läuft).
|
||||
|
||||
## Teil 2 — Authentik-OIDC-Provider (Blueprint)
|
||||
|
||||
Als Dokument in `apps/authentik/authentik-blueprints.yaml`, Muster wie
|
||||
`matrix-oidc-provider`. **Bewusst eigener Name/Slug** (`wiki-js`), damit es
|
||||
**parallel** zum Guide-09-Proxy-Provider „ThreadNet Wiki"/App `wiki` läuft; beim
|
||||
Umschalten auf die Suite werden die Guide-09-Objekte (Proxy-Provider, App `wiki`,
|
||||
Outpost, ggf. Gruppe `wiki-zugang`) **stillgelegt**.
|
||||
|
||||
```yaml
|
||||
wiki-oidc-provider.yaml: |
|
||||
# yaml-language-server: $schema=https://goauthentik.io/blueprints/schema.json
|
||||
version: 1
|
||||
metadata:
|
||||
name: wiki-oidc-provider
|
||||
labels:
|
||||
blueprints.goauthentik.io/instantiate: "true"
|
||||
entries:
|
||||
- model: authentik_providers_oauth2.oauth2provider
|
||||
state: present
|
||||
identifiers:
|
||||
name: ThreadNet Wiki (Wiki.js)
|
||||
id: wiki_oidc_provider
|
||||
attrs:
|
||||
client_type: confidential
|
||||
client_id: !Env AUTHENTIK_WIKI_OIDC_CLIENT_ID
|
||||
client_secret: !Env AUTHENTIK_WIKI_OIDC_CLIENT_SECRET
|
||||
# Aus Wiki.js übernehmen (Teil 3 zeigt die Callback-URL nach Anlegen
|
||||
# der Strategy). Erst danach hier eintragen:
|
||||
redirect_uris:
|
||||
- matching_mode: strict
|
||||
url: https://wiki.axion1337.chat/login/PROVIDERKEY/callback
|
||||
sub_mode: hashed_user_id
|
||||
include_claims_in_id_token: true
|
||||
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"]]
|
||||
# + Gruppen-Claim: entweder das mitgelieferte Groups-Scope-Mapping
|
||||
# (falls vorhanden) oder ein eigenes Scope-Mapping, das "groups"
|
||||
# zurückgibt — nötig für Rollen/Abschottung (#0049).
|
||||
|
||||
- model: authentik_core.application
|
||||
state: present
|
||||
identifiers:
|
||||
slug: wiki-js
|
||||
attrs:
|
||||
name: ThreadNet Wiki
|
||||
provider: !KeyOf wiki_oidc_provider
|
||||
meta_description: Internes Wiki (Betrieb/Anwender), Zugriff nach Gruppe
|
||||
policy_engine_mode: any
|
||||
open_in_new_tab: false
|
||||
```
|
||||
|
||||
`client_id`/`client_secret` sind Credentials → in die `authentik-credentials`-
|
||||
Secret (wie `AUTHENTIK_MAS_OIDC_CLIENT_SECRET`), **nicht** in die ConfigMap. Der
|
||||
ConfigMap-Blueprint liest sie per `!Env`.
|
||||
|
||||
## Teil 3 — Wiki.js-Seite (Authentication → OpenID Connect)
|
||||
|
||||
In Wiki.js *Administration → Authentication → + OpenID Connect*:
|
||||
- **Client ID / Client Secret**: dieselben Werte wie in Teil 2.
|
||||
- **Authorization Endpoint URL**: `https://auth.axion1337.chat/application/o/authorize/`
|
||||
- **Token Endpoint URL**: `https://auth.axion1337.chat/application/o/token/`
|
||||
- **User Info Endpoint URL**: `https://auth.axion1337.chat/application/o/userinfo/`
|
||||
- **Issuer**: `https://auth.axion1337.chat/application/o/wiki-js/`
|
||||
- **Logout URL**: `https://auth.axion1337.chat/application/o/wiki-js/end-session/`
|
||||
- **Scopes**: `openid email profile` (+ das Groups-Scope aus Teil 2).
|
||||
- **Groups Claim**: `groups` → auf Wiki.js-Gruppen/Rollen mappen (Admin schreibt,
|
||||
Anwender read-only, Betrieb/Anwender abgeschottet — #0049).
|
||||
|
||||
⚠️ **Reihenfolge:** Wiki.js zeigt beim Anlegen der Strategy die **Callback-URL**
|
||||
(`https://wiki.axion1337.chat/login/<providerKey>/callback`). Diese `<providerKey>`
|
||||
in die `redirect_uris` von Teil 2 eintragen — vorher passt die Umleitung nicht.
|
||||
|
||||
## Reihenfolge / Abhängigkeiten
|
||||
|
||||
1. Wiki.js-Deployment + Service `wikijs` (#0048) — sonst zeigt die IngressRoute
|
||||
ins Leere.
|
||||
2. Ingress + Cert (Teil 1) — Cert wird erst ausgestellt, wenn der ACME-Challenge
|
||||
über den Ingress erreichbar ist.
|
||||
3. OIDC (Teil 2/3) — Callback-URL aus Wiki.js, dann Blueprint scharf.
|
||||
4. Rollen/Abschottung + Gruppen (#0049) — Gruppen legt sorb an.
|
||||
|
||||
## Braucht sorb
|
||||
|
||||
- **OIDC Client-ID + Secret** (Credential; in `authentik-credentials`).
|
||||
- **Authentik-Gruppen** für #0049 (Admin/Betrieb/Anwender).
|
||||
- Beim Cutover: die **Guide-09-Objekte stilllegen** (Proxy-Provider „ThreadNet
|
||||
Wiki", App `wiki`, Outpost `wiki-forward-auth`).
|
||||
|
||||
## Verifikation
|
||||
|
||||
1. `dig wiki.axion1337.chat` → `49.13.132.245`.
|
||||
2. `curl -sI https://wiki.axion1337.chat` → 200/302 (nicht Cert-Fehler).
|
||||
3. Aufruf → Wiki.js-Login → „Login with Authentik" → `auth.axion1337.chat` →
|
||||
zurück, eingeloggt.
|
||||
4. **Admin**: editieren, sieht Betrieb + Anwender. **Anwender**: read-only,
|
||||
`/betrieb` nicht in Navigation/Suche.
|
||||
@@ -0,0 +1,125 @@
|
||||
# aXion1337.Chat – Deployment & Konfiguration Dokumentation
|
||||
|
||||
Diese Dokumentation beschreibt die Einrichtung und Konfiguration des Matrix-Homeservers für **axion1337.chat** mit Element Server Suite (ESS) v26.4.0 auf K3S mit Flux CD GitOps.
|
||||
|
||||
## 📋 Übersicht Deployment-Reihenfolge
|
||||
|
||||
Die Implementierungen wurden in dieser Reihenfolge durchgeführt. Für neue Setups sollten Sie dieser Abfolge folgen:
|
||||
|
||||
| # | 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` | ✅ 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) |
|
||||
| 8 | @concierge — Gäste-Einladungen mit Ablauf | `08-concierge-gaeste-einladungen.md` | ⏳ Wartet auf Zugangsdaten | Matrix Synapse |
|
||||
| 9 | Docusaurus-Wiki hinter Authentik (Forward-Auth) | `09-wiki-forward-auth.md` | 📝 Vorbereitet, nicht ausgerollt · abgelöst durch 10 (Suite) | Authentik + Traefik (Overmind) |
|
||||
| 10 | Wiki.js: Zugang (wiki.axion1337.chat) + Authentik-OIDC | `10-wiki-js-zugang-und-oidc.md` | 📝 Vorbereitet, nicht ausgerollt | Authentik + Traefik (Suite/k8s) |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start für neue Deployment
|
||||
|
||||
Siehe die einzelnen Dokumentationen für detaillierte Anleitung.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architektur-Übersicht
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Element Web (Apex) │
|
||||
│ axion1337.chat (HTTP/TLS) │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┼─────────────┐
|
||||
│ │ │
|
||||
┌────▼────┐ ┌─────▼──────┐ ┌──▼────────┐
|
||||
│ MAS │ │ Well-Known │ │Docs/Setup │
|
||||
│account. │ │matrix/* │ │/setup │
|
||||
│axion1337 │ │ │ │ │
|
||||
└────┬────┘ └────────────┘ └───────────┘
|
||||
│
|
||||
┌────▼────────────────┐
|
||||
│ Authentik OIDC │
|
||||
│ auth.axion1337.chat │
|
||||
│ (Identity Provider) │
|
||||
└─────────────────────┘
|
||||
│
|
||||
┌────▼────────────────┐
|
||||
│ Synapse Matrix │
|
||||
│ matrix.axion1337.chat│
|
||||
│ (Homeserver) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Kritische Werte & Konfigurationen
|
||||
|
||||
### Domains
|
||||
- **Apex**: `axion1337.chat` (Element Web)
|
||||
- **Matrix Synapse**: `matrix.axion1337.chat`
|
||||
- **MAS**: `account.axion1337.chat`
|
||||
- **Authentik**: `auth.axion1337.chat`
|
||||
- **TURN Server**: `turn.axion1337.chat`
|
||||
|
||||
### Externe Services
|
||||
- **K3S Host IP**: `49.13.132.245`
|
||||
- **Monitoring Host**: `10.0.0.3` (Selendis)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Dokumente im Detail
|
||||
|
||||
### [01-turn-server-setup.md](01-turn-server-setup.md)
|
||||
STUN/TURN Server für WebRTC Media Relay (Video-Calls).
|
||||
|
||||
### [02-authentik-identity-provider.md](02-authentik-identity-provider.md)
|
||||
Authentik als OIDC Provider für Matrix. Registrierung via Einladungs-Links.
|
||||
|
||||
### [03-monitoring-integration.md](03-monitoring-integration.md)
|
||||
Alloy → Prometheus/Loki Monitoring Integration.
|
||||
|
||||
### [04-element-customization.md](04-element-customization.md)
|
||||
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).
|
||||
|
||||
### [08-concierge-gaeste-einladungen.md](08-concierge-gaeste-einladungen.md)
|
||||
@concierge-Bot: Einladungslinks mit 3-Tage-Ablauf, begrenzte Selbstverlängerung, dauerhafte
|
||||
Freischaltung nur durch Admin-Kommando im Matrix-Raum (Issue #48). Deployt, wartet auf
|
||||
Zugangsdaten (Matrix-Konto, Authentik-Token, Secret).
|
||||
|
||||
### [09-wiki-forward-auth.md](09-wiki-forward-auth.md)
|
||||
Statisches Docusaurus-Wiki hinter Authentik: Proxy-Provider (Forward-Auth) + Anwendung +
|
||||
Gruppe `wiki-zugang` als Blueprint, Proxy-Outpost-Container plus Traefik-Middleware auf
|
||||
Overmind. Vorbereitet, nicht ausgerollt — Blueprint als Vorlage im Guide, Outpost-Token
|
||||
und `wiki-zugang`-Mitglieder sind sorbs Schritt. **Entwicklungs-Zwischenstand**, wird beim
|
||||
Suite-Umzug durch Guide 10 (Wiki.js, natives OIDC) abgelöst — ADR-0014.
|
||||
|
||||
### [10-wiki-js-zugang-und-oidc.md](10-wiki-js-zugang-und-oidc.md)
|
||||
Zugang zum Suite-Wiki (Wiki.js, ADR-0014): öffentlich `wiki.axion1337.chat` mit
|
||||
**nativem** Authentik-OIDC (kein Forward-Auth). Fertiges Ingress+Certificate-Manifest
|
||||
(Muster wie authentik), Authentik-OIDC-Provider-Blueprint und die Wiki.js-OIDC-Einstellungen.
|
||||
Vorbereitet, nicht ausgerollt — Client-Secret und Gruppen sind sorbs Schritt; gehört zu
|
||||
#0048 (Ingress/Cert) und #0049 (OIDC/Rollen).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Wartung & Troubleshooting
|
||||
|
||||
Alle Dokumentationen enthalten Troubleshooting-Sektionen für häufige Probleme.
|
||||
@@ -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.
|
||||