From 9093b52e008597e853166ef5ea236d8fbe8c7073 Mon Sep 17 00:00:00 2001 From: atlas-bot Date: Sun, 4 Oct 2026 21:41:10 +0000 Subject: [PATCH 1/2] feat(matrix): deploy matrix-bridge build.1791062345 as a Synapse appservice (REN-587) Adds the matrix-bridge Deployment, Service and ExternalSecret in the matrix namespace, loads its appservice registration in Synapse, and opens the bridge -> Paperclip API path (:3100) in both namespaces' policies. Tokens and the board key come from OpenBao homelab/apps/matrix-bridge. Co-Authored-By: Claude Opus 5.5 --- kubernetes/apps/matrix/bridge.yaml | 183 ++++++++++++++++++ kubernetes/apps/matrix/kustomization.yaml | 1 + kubernetes/apps/matrix/synapse.yaml | 18 +- kubernetes/apps/paperclip/network-policy.yaml | 10 + 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 kubernetes/apps/matrix/bridge.yaml diff --git a/kubernetes/apps/matrix/bridge.yaml b/kubernetes/apps/matrix/bridge.yaml new file mode 100644 index 00000000..58bada95 --- /dev/null +++ b/kubernetes/apps/matrix/bridge.yaml @@ -0,0 +1,183 @@ +# matrix-bridge (REN-569, REN-587): Matrix appservice between Element and the +# Paperclip agents. One room per agent with the owner; owner messages become +# comments on the agent's chat card, the agent's comments come back to the +# room. Source: gitea.renner.uno/konairius/matrix-bridge. +# +# No volume: the bridge keeps its state in the bot's account data on Synapse. +# No ingress: only Synapse talks to it (appservice transactions on :8080). +# +# OpenBao homelab/apps/matrix-bridge (seeded by the owner): +# as_token random; bridge -> Synapse, MATRIX_AS_TOKEN +# hs_token random; Synapse -> bridge, MATRIX_HS_TOKEN +# paperclip_api_key Paperclip board key "matrix-bridge (board)" (REN-582) +# +# The same Secret carries the appservice registration that Synapse loads +# from app_service_config_files (synapse.yaml), so both sides always get the +# same tokens. Synapse cannot start without it: seed OpenBao before merge. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: matrix-bridge + namespace: matrix +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: openbao + target: + name: matrix-bridge + template: + engineVersion: v2 + data: + as_token: "{{ .as_token }}" + hs_token: "{{ .hs_token }}" + paperclip_api_key: "{{ .paperclip_api_key }}" + # Agent users are @agent_:matrix.renner.uno; the bot is + # @paperclip:matrix.renner.uno. + registration.yaml: | + id: matrix-bridge + url: http://matrix-bridge:8080 + as_token: "{{ .as_token }}" + hs_token: "{{ .hs_token }}" + sender_localpart: paperclip + rate_limited: false + namespaces: + users: + - exclusive: true + regex: "@agent_.*:matrix\\.renner\\.uno" + aliases: [] + rooms: [] + dataFrom: + - extract: + key: apps/matrix-bridge +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: matrix-bridge + namespace: matrix + annotations: + secret.reloader.stakater.com/reload: matrix-bridge + labels: + app.kubernetes.io/name: matrix-bridge + app.kubernetes.io/part-of: matrix +spec: + replicas: 1 + # One poller only: two pods would bridge every comment twice. + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: matrix-bridge + template: + metadata: + labels: + app.kubernetes.io/name: matrix-bridge + app.kubernetes.io/part-of: matrix + spec: + automountServiceAccountToken: false + securityContext: + runAsNonRoot: true + # `node` in node:24-alpine. + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: matrix-bridge + image: gitea.renner.uno/konairius/matrix-bridge:build.1791062345 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: ["ALL"] + env: + - name: MATRIX_HOMESERVER_URL + value: http://synapse:8008 + - name: MATRIX_SERVER_NAME + value: matrix.renner.uno + - name: MATRIX_OWNER + value: "@k.renner:matrix.renner.uno" + - name: MATRIX_AS_TOKEN + valueFrom: + secretKeyRef: + name: matrix-bridge + key: as_token + - name: MATRIX_HS_TOKEN + valueFrom: + secretKeyRef: + name: matrix-bridge + key: hs_token + - name: PAPERCLIP_API_URL + value: http://paperclip.paperclip.svc.cluster.local:3100 + - name: PAPERCLIP_API_KEY + valueFrom: + secretKeyRef: + name: matrix-bridge + key: paperclip_api_key + - name: PAPERCLIP_COMPANY_ID + value: 72f7b75a-10b0-4084-ad5e-a0cdf31c7caf + - name: PAPERCLIP_PROJECT_ID + value: 67143c0b-e9a1-44c1-b012-0f8fa25625aa + ports: + - name: http + containerPort: 8080 + resources: + requests: + cpu: 10m + memory: 64Mi + limits: + memory: 256Mi + # /health is 503 when the Paperclip comment poll fails, so it gates + # readiness only; a Paperclip outage must not restart the bridge. + readinessProbe: + httpGet: + path: /health + port: http + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + periodSeconds: 30 + failureThreshold: 4 + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: matrix-bridge + namespace: matrix +spec: + selector: + app.kubernetes.io/name: matrix-bridge + ports: + - name: http + port: 8080 + targetPort: http +--- +# The bridge calls the Paperclip API in-cluster. Synapse <-> bridge and DNS +# are covered by matrix-app-egress (part-of: matrix); the Paperclip side of +# this flow is in apps/paperclip/network-policy.yaml. +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: matrix-bridge-paperclip-egress + namespace: matrix +spec: + endpointSelector: + matchLabels: + app.kubernetes.io/name: matrix-bridge + egress: + - toEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": paperclip + app.kubernetes.io/name: paperclip + toPorts: + - ports: + - port: "3100" + protocol: TCP diff --git a/kubernetes/apps/matrix/kustomization.yaml b/kubernetes/apps/matrix/kustomization.yaml index 94a5282b..b45c240a 100644 --- a/kubernetes/apps/matrix/kustomization.yaml +++ b/kubernetes/apps/matrix/kustomization.yaml @@ -5,6 +5,7 @@ resources: - synapse.yaml - mas.yaml - element.yaml + - bridge.yaml - ingress.yaml # mx-pg CNPG Postgres + offsite Barman backup. The media store and signing # key are on synapse-data, backed up by Velero (matrix-daily). diff --git a/kubernetes/apps/matrix/synapse.yaml b/kubernetes/apps/matrix/synapse.yaml index cb7ec752..ada370ad 100644 --- a/kubernetes/apps/matrix/synapse.yaml +++ b/kubernetes/apps/matrix/synapse.yaml @@ -90,9 +90,10 @@ data: push: include_content: false - # Appservice registrations (bridges) go here, one file each, mounted - # from a Secret. Empty until REN-565 Part 2 is decided. - app_service_config_files: [] + # Appservice registrations (bridges), one file each, mounted from a + # Secret. matrix-bridge: bridge.yaml (REN-587). + app_service_config_files: + - /appservices/matrix-bridge/registration.yaml --- # Media store and the server signing key. Not reconstructable: lost media # is gone, and a lost signing key invalidates the server's past signatures. @@ -119,7 +120,7 @@ metadata: namespace: matrix annotations: configmap.reloader.stakater.com/reload: synapse-config,mas-config - secret.reloader.stakater.com/reload: synapse-secrets + secret.reloader.stakater.com/reload: synapse-secrets,matrix-bridge labels: app.kubernetes.io/name: synapse app.kubernetes.io/part-of: matrix @@ -267,6 +268,9 @@ spec: mountPath: /data - name: tmp mountPath: /tmp + - name: appservice-matrix-bridge + mountPath: /appservices/matrix-bridge + readOnly: true volumes: - name: config configMap: @@ -299,6 +303,12 @@ spec: claimName: synapse-data - name: tmp emptyDir: {} + - name: appservice-matrix-bridge + secret: + secretName: matrix-bridge + items: + - key: registration.yaml + path: registration.yaml --- apiVersion: v1 kind: Service diff --git a/kubernetes/apps/paperclip/network-policy.yaml b/kubernetes/apps/paperclip/network-policy.yaml index 889ec05c..7a8c18e3 100644 --- a/kubernetes/apps/paperclip/network-policy.yaml +++ b/kubernetes/apps/paperclip/network-policy.yaml @@ -25,6 +25,16 @@ spec: - ports: - port: "3100" protocol: TCP + # matrix-bridge (REN-587), board-key API calls; egress side in + # apps/matrix/bridge.yaml. + - fromEndpoints: + - matchLabels: + "k8s:io.kubernetes.pod.namespace": matrix + app.kubernetes.io/name: matrix-bridge + toPorts: + - ports: + - port: "3100" + protocol: TCP --- # Covers the server and every agent it spawns (same pod). apiVersion: cilium.io/v2 -- 2.54.0 From b052fc2576fdde6e476c89524f0017a8a55238b5 Mon Sep 17 00:00:00 2001 From: atlas-bot Date: Sun, 4 Oct 2026 21:45:07 +0000 Subject: [PATCH 2/2] feat(matrix): generate the appservice tokens in-cluster (REN-587) as_token and hs_token come from an ESO Password generator (CreatedOnce) instead of OpenBao, so no one has to seed them by hand. Synapse mounts only the generated Secret and no longer depends on the OpenBao path; OpenBao homelab/apps/matrix-bridge now holds only paperclip_api_key. Co-Authored-By: Claude Opus 5.5 --- kubernetes/apps/matrix/bridge.yaml | 74 +++++++++++++++++++++-------- kubernetes/apps/matrix/synapse.yaml | 4 +- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/kubernetes/apps/matrix/bridge.yaml b/kubernetes/apps/matrix/bridge.yaml index 58bada95..b8e763a9 100644 --- a/kubernetes/apps/matrix/bridge.yaml +++ b/kubernetes/apps/matrix/bridge.yaml @@ -6,32 +6,47 @@ # No volume: the bridge keeps its state in the bot's account data on Synapse. # No ingress: only Synapse talks to it (appservice transactions on :8080). # -# OpenBao homelab/apps/matrix-bridge (seeded by the owner): -# as_token random; bridge -> Synapse, MATRIX_AS_TOKEN -# hs_token random; Synapse -> bridge, MATRIX_HS_TOKEN -# paperclip_api_key Paperclip board key "matrix-bridge (board)" (REN-582) -# -# The same Secret carries the appservice registration that Synapse loads -# from app_service_config_files (synapse.yaml), so both sides always get the -# same tokens. Synapse cannot start without it: seed OpenBao before merge. +# Secrets: +# matrix-bridge-tokens as_token (bridge -> Synapse) and hs_token (Synapse +# -> bridge), generated once in-cluster by ESO, plus +# the appservice registration Synapse loads from +# app_service_config_files (synapse.yaml). Both sides +# read the same Secret, so the tokens cannot drift. +# Deleting the Secret regenerates both; Reloader +# restarts Synapse and the bridge. +# matrix-bridge paperclip_api_key from OpenBao +# homelab/apps/matrix-bridge: the Paperclip board key +# "matrix-bridge (board)" (REN-582). +apiVersion: generators.external-secrets.io/v1alpha1 +kind: Password +metadata: + name: matrix-bridge-tokens + namespace: matrix +spec: + length: 64 + digits: 16 + symbols: 0 + allowRepeat: true + noUpper: false + secretKeys: + - as_token + - hs_token +--- apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: - name: matrix-bridge + name: matrix-bridge-tokens namespace: matrix spec: - refreshInterval: 1h - secretStoreRef: - kind: ClusterSecretStore - name: openbao + # Generate once; Periodic would rotate the tokens every refresh. + refreshPolicy: CreatedOnce target: - name: matrix-bridge + name: matrix-bridge-tokens template: engineVersion: v2 data: as_token: "{{ .as_token }}" hs_token: "{{ .hs_token }}" - paperclip_api_key: "{{ .paperclip_api_key }}" # Agent users are @agent_:matrix.renner.uno; the bot is # @paperclip:matrix.renner.uno. registration.yaml: | @@ -48,8 +63,29 @@ spec: aliases: [] rooms: [] dataFrom: - - extract: + - sourceRef: + generatorRef: + apiVersion: generators.external-secrets.io/v1alpha1 + kind: Password + name: matrix-bridge-tokens +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: matrix-bridge + namespace: matrix +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: openbao + target: + name: matrix-bridge + data: + - secretKey: paperclip_api_key + remoteRef: key: apps/matrix-bridge + property: paperclip_api_key --- apiVersion: apps/v1 kind: Deployment @@ -57,7 +93,7 @@ metadata: name: matrix-bridge namespace: matrix annotations: - secret.reloader.stakater.com/reload: matrix-bridge + secret.reloader.stakater.com/reload: matrix-bridge,matrix-bridge-tokens labels: app.kubernetes.io/name: matrix-bridge app.kubernetes.io/part-of: matrix @@ -101,12 +137,12 @@ spec: - name: MATRIX_AS_TOKEN valueFrom: secretKeyRef: - name: matrix-bridge + name: matrix-bridge-tokens key: as_token - name: MATRIX_HS_TOKEN valueFrom: secretKeyRef: - name: matrix-bridge + name: matrix-bridge-tokens key: hs_token - name: PAPERCLIP_API_URL value: http://paperclip.paperclip.svc.cluster.local:3100 diff --git a/kubernetes/apps/matrix/synapse.yaml b/kubernetes/apps/matrix/synapse.yaml index ada370ad..27c6fce8 100644 --- a/kubernetes/apps/matrix/synapse.yaml +++ b/kubernetes/apps/matrix/synapse.yaml @@ -120,7 +120,7 @@ metadata: namespace: matrix annotations: configmap.reloader.stakater.com/reload: synapse-config,mas-config - secret.reloader.stakater.com/reload: synapse-secrets,matrix-bridge + secret.reloader.stakater.com/reload: synapse-secrets,matrix-bridge-tokens labels: app.kubernetes.io/name: synapse app.kubernetes.io/part-of: matrix @@ -305,7 +305,7 @@ spec: emptyDir: {} - name: appservice-matrix-bridge secret: - secretName: matrix-bridge + secretName: matrix-bridge-tokens items: - key: registration.yaml path: registration.yaml -- 2.54.0