From 7f42a456eb4df85ea5affe78b2bb75e356337bf3 Mon Sep 17 00:00:00 2001 From: Thore Cimbal Date: Wed, 29 Jul 2026 08:47:07 +0200 Subject: [PATCH] feat: deploy Draupnir moderation bot (Issue #18) Bot account registered via mas-cli, compatibility token issued and stored via sops. Adds an explicit NetworkPolicy allow-rule since Draupnir needs to reach Synapse's client-server API in-namespace, which the existing default-deny policy would otherwise silently block. --- apps/production/draupnir-pvc.yaml | 12 ++++++ apps/production/draupnir-secret.yaml | 22 ++++++++++ apps/production/draupnir.yaml | 63 ++++++++++++++++++++++++++++ apps/production/kustomization.yaml | 4 ++ apps/production/networkpolicy.yaml | 9 ++++ 5 files changed, 110 insertions(+) create mode 100644 apps/production/draupnir-pvc.yaml create mode 100644 apps/production/draupnir-secret.yaml create mode 100644 apps/production/draupnir.yaml diff --git a/apps/production/draupnir-pvc.yaml b/apps/production/draupnir-pvc.yaml new file mode 100644 index 0000000..13de4c0 --- /dev/null +++ b/apps/production/draupnir-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: draupnir-data + namespace: matrix +spec: + accessModes: + - ReadWriteOnce + storageClassName: local-path + resources: + requests: + storage: 2Gi diff --git a/apps/production/draupnir-secret.yaml b/apps/production/draupnir-secret.yaml new file mode 100644 index 0000000..4e22256 --- /dev/null +++ b/apps/production/draupnir-secret.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Secret +metadata: + name: draupnir-config + namespace: matrix +stringData: + default.yaml: ENC[AES256_GCM,data:PlGEBWmpdzpeiiTP6fk7CJpeLGZHoT20Qp6tPKUOF9NE9/tTucK4qngo4YUx/+HHvk3EeZ26k+FInWABpLtQ+aYs60eecyeUPOyrJ7VNvTbgZLfGvJQJRwqjPauWKv8ODXlK2ABg5olP89k5BQIpBoqUr+p5WghnYSuQ48JkzEYSku6cLufprVfdZjbqexa93vYqy2RUapQgmR4cH8mjUg92ZF2u6d1u8OtblQe+vdAgSN0s1N0bOhwsCXeUKExuYuyfiAErc7MIr/eCcxZRBMx+snxqs8wnOKEKsxmrEvDGdjcqf2VH0TO1MECmsxc6oTkXAruRcJg1RTYNdQ2rpptrMj5pkqaJZdHNWQ1/NQ5vcEmRlsoPvSiaG+z5ohFLfQ5bqyZpUo2dmv0rWWyTY1eh66cmKBEC8YDIKg==,iv:/JT0/yL62jz+ZfDiDIsANiXdY6OnLe6U9Gn7UNVXNpo=,tag:0f9UzmLnXEb+3Mvq1o27Ug==,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-29T06:45:33Z" + mac: ENC[AES256_GCM,data:RhzGNv1r0WcZb18PNAWpCfFCE6FJA8JijRpwZGZwLTBwJ2jdJVrypBEfkCBCPI3BpJNhomX6VMTAPoyfybMzK+s3GYnMgB3Sgju8vio+DsdpvFdu1rXmzHdVNkvvXgCOzxIo0ojoJHG6RwMRvcdcDqR+TvmrHYDFFqDz9DlXTKI=,iv:TwwH61WTRsqB3JyraHru8XDQsedNWU1v0cAV+lkKwas=,tag:0oxbdkPxE//vyrZkJNTTLw==,type:str] + version: 3.13.3 diff --git a/apps/production/draupnir.yaml b/apps/production/draupnir.yaml new file mode 100644 index 0000000..b2c254c --- /dev/null +++ b/apps/production/draupnir.yaml @@ -0,0 +1,63 @@ +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:v2.9.0 + imagePullPolicy: IfNotPresent + 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 diff --git a/apps/production/kustomization.yaml b/apps/production/kustomization.yaml index 85b5941..335e91c 100644 --- a/apps/production/kustomization.yaml +++ b/apps/production/kustomization.yaml @@ -43,3 +43,7 @@ resources: # 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 diff --git a/apps/production/networkpolicy.yaml b/apps/production/networkpolicy.yaml index 4e3bcb4..645afec 100644 --- a/apps/production/networkpolicy.yaml +++ b/apps/production/networkpolicy.yaml @@ -109,6 +109,15 @@ spec: 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 --- # 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.