diff --git a/kubernetes/apps/matrix/bridge.yaml b/kubernetes/apps/matrix/bridge.yaml new file mode 100644 index 00000000..b8e763a9 --- /dev/null +++ b/kubernetes/apps/matrix/bridge.yaml @@ -0,0 +1,219 @@ +# 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). +# +# 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-tokens + namespace: matrix +spec: + # Generate once; Periodic would rotate the tokens every refresh. + refreshPolicy: CreatedOnce + target: + name: matrix-bridge-tokens + template: + engineVersion: v2 + data: + as_token: "{{ .as_token }}" + hs_token: "{{ .hs_token }}" + # 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: + - 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 +metadata: + name: matrix-bridge + namespace: matrix + annotations: + secret.reloader.stakater.com/reload: matrix-bridge,matrix-bridge-tokens + 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-tokens + key: as_token + - name: MATRIX_HS_TOKEN + valueFrom: + secretKeyRef: + name: matrix-bridge-tokens + 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..27c6fce8 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-tokens 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-tokens + 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