From 0ef14c67fd4b92ecc51d35a5a9f0dee1a61ba593 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Tue, 27 Jan 2026 04:48:44 -0300 Subject: [PATCH] comms: add synapse admin ensure job --- services/comms/kustomization.yaml | 1 + services/comms/synapse-admin-ensure-job.yaml | 177 ++++++++++++++++++ services/maintenance/ariadne-deployment.yaml | 3 + .../vault/scripts/vault_k8s_auth_configure.sh | 4 +- 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 services/comms/synapse-admin-ensure-job.yaml diff --git a/services/comms/kustomization.yaml b/services/comms/kustomization.yaml index 410f2a6..01d7be5 100644 --- a/services/comms/kustomization.yaml +++ b/services/comms/kustomization.yaml @@ -25,6 +25,7 @@ resources: - mas-admin-client-secret-ensure-job.yaml - mas-db-ensure-job.yaml - comms-secrets-ensure-job.yaml + - synapse-admin-ensure-job.yaml - synapse-signingkey-ensure-job.yaml - synapse-seeder-admin-ensure-job.yaml - synapse-user-seed-job.yaml diff --git a/services/comms/synapse-admin-ensure-job.yaml b/services/comms/synapse-admin-ensure-job.yaml new file mode 100644 index 0000000..be9e0fd --- /dev/null +++ b/services/comms/synapse-admin-ensure-job.yaml @@ -0,0 +1,177 @@ +# services/comms/synapse-admin-ensure-job.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: synapse-admin-ensure-1 + namespace: comms +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 3600 + template: + spec: + serviceAccountName: comms-secrets-ensure + restartPolicy: OnFailure + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role.kubernetes.io/worker + operator: Exists + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: kubernetes.io/arch + operator: In + values: ["arm64"] + containers: + - name: ensure + image: python:3.11-slim + env: + - name: VAULT_ADDR + value: http://vault.vault.svc.cluster.local:8200 + - name: VAULT_ROLE + value: comms-secrets + - name: SYNAPSE_ADMIN_URL + value: http://othrys-synapse-matrix-synapse.comms.svc.cluster.local:8008 + command: + - /bin/sh + - -c + - | + set -euo pipefail + python - <<'PY' + import base64 + import hashlib + import hmac + import json + import os + import secrets + import string + import urllib.error + import urllib.request + + VAULT_ADDR = os.environ.get("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200").rstrip("/") + VAULT_ROLE = os.environ.get("VAULT_ROLE", "comms-secrets") + SYNAPSE_ADMIN_URL = os.environ.get( + "SYNAPSE_ADMIN_URL", + "http://othrys-synapse-matrix-synapse.comms.svc.cluster.local:8008", + ).rstrip("/") + SA_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token" + + def log(msg: str) -> None: + print(msg, flush=True) + + def request_json(url: str, payload: dict | None = None) -> dict: + data = None + headers = {"Content-Type": "application/json"} + if payload is not None: + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request(url, data=data, headers=headers, method="POST" if data else "GET") + with urllib.request.urlopen(req, timeout=30) as resp: + return json.loads(resp.read().decode("utf-8")) + + def vault_login() -> str: + with open(SA_TOKEN_PATH, "r", encoding="utf-8") as f: + jwt = f.read().strip() + payload = {"jwt": jwt, "role": VAULT_ROLE} + resp = request_json(f"{VAULT_ADDR}/v1/auth/kubernetes/login", payload) + token = resp.get("auth", {}).get("client_token") + if not token: + raise RuntimeError("vault login failed") + return token + + def vault_get(token: str, path: str) -> dict: + req = urllib.request.Request( + f"{VAULT_ADDR}/v1/kv/data/atlas/{path}", + headers={"X-Vault-Token": token}, + ) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + payload = json.loads(resp.read().decode("utf-8")) + return payload.get("data", {}).get("data", {}) + except urllib.error.HTTPError as exc: + if exc.code == 404: + return {} + raise + + def vault_put(token: str, path: str, data: dict) -> None: + payload = {"data": data} + req = urllib.request.Request( + f"{VAULT_ADDR}/v1/kv/data/atlas/{path}", + data=json.dumps(payload).encode("utf-8"), + headers={"X-Vault-Token": token, "Content-Type": "application/json"}, + method="POST", + ) + with urllib.request.urlopen(req, timeout=30) as resp: + resp.read() + + def random_password(length: int = 32) -> str: + alphabet = string.ascii_letters + string.digits + return "".join(secrets.choice(alphabet) for _ in range(length)) + + def ensure_registration_secret(token: str) -> str: + data = vault_get(token, "comms/synapse-registration") + secret = (data.get("registration_shared_secret") or "").strip() + if not secret: + secret = secrets.token_urlsafe(32) + data["registration_shared_secret"] = secret + vault_put(token, "comms/synapse-registration", data) + log("registration secret created") + return secret + + def ensure_admin_creds(token: str) -> dict: + data = vault_get(token, "comms/synapse-admin") + username = (data.get("username") or "").strip() or "synapse-admin" + password = (data.get("password") or "").strip() + if not password: + password = random_password() + data["username"] = username + data["password"] = password + vault_put(token, "comms/synapse-admin", data) + return data + + def register_admin(secret: str, username: str, password: str) -> str: + nonce_payload = request_json(f"{SYNAPSE_ADMIN_URL}/_synapse/admin/v1/register") + nonce = nonce_payload.get("nonce") + if not nonce: + raise RuntimeError("synapse register nonce missing") + admin_flag = "admin" + user_type = "" + mac_payload = "\x00".join([nonce, username, password, admin_flag, user_type]) + mac = hmac.new(secret.encode("utf-8"), mac_payload.encode("utf-8"), hashlib.sha1).hexdigest() + payload = { + "nonce": nonce, + "username": username, + "password": password, + "admin": True, + "mac": mac, + } + req = urllib.request.Request( + f"{SYNAPSE_ADMIN_URL}/_synapse/admin/v1/register", + data=json.dumps(payload).encode("utf-8"), + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=30) as resp: + payload = json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + body = exc.read().decode("utf-8") + raise RuntimeError(f"synapse admin register failed: {exc.code} {body}") from exc + access_token = payload.get("access_token") + if not access_token: + raise RuntimeError("synapse admin token missing") + return access_token + + vault_token = vault_login() + reg_secret = ensure_registration_secret(vault_token) + admin_data = ensure_admin_creds(vault_token) + if admin_data.get("access_token"): + log("synapse admin token already present") + raise SystemExit(0) + access_token = register_admin(reg_secret, admin_data["username"], admin_data["password"]) + admin_data["access_token"] = access_token + vault_put(vault_token, "comms/synapse-admin", admin_data) + log("synapse admin user ensured") + PY diff --git a/services/maintenance/ariadne-deployment.yaml b/services/maintenance/ariadne-deployment.yaml index 6fa638d..fce1ded 100644 --- a/services/maintenance/ariadne-deployment.yaml +++ b/services/maintenance/ariadne-deployment.yaml @@ -69,6 +69,9 @@ spec: export COMMS_BOT_PASSWORD="{{ index .Data.data "bot-password" }}" export COMMS_SEEDER_PASSWORD="{{ index .Data.data "seeder-password" }}" {{ end }} + {{ with secret "kv/data/atlas/comms/synapse-admin" }} + export COMMS_SYNAPSE_ADMIN_TOKEN="{{ .Data.data.access_token }}" + {{ end }} {{ with secret "kv/data/atlas/comms/synapse-db" }} export COMMS_SYNAPSE_DB_PASSWORD="{{ .Data.data.POSTGRES_PASSWORD }}" {{ end }} diff --git a/services/vault/scripts/vault_k8s_auth_configure.sh b/services/vault/scripts/vault_k8s_auth_configure.sh index 21132c7..0212180 100644 --- a/services/vault/scripts/vault_k8s_auth_configure.sh +++ b/services/vault/scripts/vault_k8s_auth_configure.sh @@ -231,7 +231,7 @@ write_policy_and_role "crypto" "crypto" "crypto-vault-sync" \ write_policy_and_role "health" "health" "health-vault-sync" \ "health/*" "" write_policy_and_role "maintenance" "maintenance" "ariadne,maintenance-vault-sync" \ - "maintenance/ariadne-db portal/atlas-portal-db portal/bstein-dev-home-keycloak-admin mailu/mailu-db-secret mailu/mailu-initial-account-secret nextcloud/nextcloud-db nextcloud/nextcloud-admin health/wger-admin finance/firefly-secrets comms/mas-admin-client-runtime comms/atlasbot-credentials-runtime comms/synapse-db vault/vault-oidc-config shared/harbor-pull" "" + "maintenance/ariadne-db portal/atlas-portal-db portal/bstein-dev-home-keycloak-admin mailu/mailu-db-secret mailu/mailu-initial-account-secret nextcloud/nextcloud-db nextcloud/nextcloud-admin health/wger-admin finance/firefly-secrets comms/mas-admin-client-runtime comms/atlasbot-credentials-runtime comms/synapse-db comms/synapse-admin vault/vault-oidc-config shared/harbor-pull" "" write_policy_and_role "finance" "finance" "finance-vault" \ "finance/* shared/postmark-relay" "" write_policy_and_role "finance-secrets" "finance" "finance-secrets-ensure" \ @@ -253,4 +253,4 @@ write_policy_and_role "crypto-secrets" "crypto" "crypto-secrets-ensure" \ write_policy_and_role "comms-secrets" "comms" \ "comms-secrets-ensure,mas-db-ensure,mas-admin-client-secret-writer,othrys-synapse-signingkey-job" \ "" \ - "comms/turn-shared-secret comms/livekit-api comms/synapse-redis comms/synapse-macaroon comms/atlasbot-credentials-runtime comms/synapse-db comms/mas-db comms/mas-admin-client-runtime comms/mas-secrets-runtime comms/othrys-synapse-signingkey" + "comms/turn-shared-secret comms/livekit-api comms/synapse-redis comms/synapse-macaroon comms/atlasbot-credentials-runtime comms/synapse-db comms/synapse-admin comms/synapse-registration comms/mas-db comms/mas-admin-client-runtime comms/mas-secrets-runtime comms/othrys-synapse-signingkey"