From aa376c0ee9ff8a230b746c37c378a9336d2c54cf Mon Sep 17 00:00:00 2001 From: jenkins Date: Thu, 13 Aug 2026 17:45:09 -0300 Subject: [PATCH] keycloak: add Hermes automation identity --- .../hermes-access-oidc-client-job.yaml | 4 +- .../scripts/hermes_access_oidc_ensure.sh | 84 +++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/services/keycloak/bootstrap-jobs/hermes-access-oidc-client-job.yaml b/services/keycloak/bootstrap-jobs/hermes-access-oidc-client-job.yaml index 054c3949c..e7c56074f 100644 --- a/services/keycloak/bootstrap-jobs/hermes-access-oidc-client-job.yaml +++ b/services/keycloak/bootstrap-jobs/hermes-access-oidc-client-job.yaml @@ -1,9 +1,9 @@ # services/keycloak/bootstrap-jobs/hermes-access-oidc-client-job.yaml -# Purpose: create isolated chat, Brad-only agent, and Brad-only triage clients. +# Purpose: create Hermes browser clients and its automation identity. apiVersion: batch/v1 kind: Job metadata: - name: hermes-access-oidc-client-ensure-6 + name: hermes-access-oidc-client-ensure-7 namespace: sso spec: backoffLimit: 3 diff --git a/services/keycloak/scripts/hermes_access_oidc_ensure.sh b/services/keycloak/scripts/hermes_access_oidc_ensure.sh index ef70257a5..0ab9fbb0d 100755 --- a/services/keycloak/scripts/hermes_access_oidc_ensure.sh +++ b/services/keycloak/scripts/hermes_access_oidc_ensure.sh @@ -208,6 +208,89 @@ ensure_proxy_client() { echo "Hermes OIDC client ${client_id} is ready" } +ensure_service_account_client() { + client_id="$1" + vault_path="$2" + payload="$(jq -nc \ + --arg client_id "${client_id}" \ + '{ + clientId:$client_id, + name:"Hermes Automation", + description:"Machine identity for Hermes development and delivery integrations", + enabled:true, + protocol:"openid-connect", + publicClient:false, + bearerOnly:false, + clientAuthenticatorType:"client-secret", + standardFlowEnabled:false, + implicitFlowEnabled:false, + directAccessGrantsEnabled:false, + serviceAccountsEnabled:true, + fullScopeAllowed:false, + attributes:{"access.token.lifespan":"1200"} + }')" + + query="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${KC_URL}/admin/realms/atlas/clients?clientId=${client_id}" || true)" + internal_id="$(printf '%s' "${query}" | jq -r '.[0].id' 2>/dev/null || true)" + if [ -z "${internal_id}" ] || [ "${internal_id}" = "null" ]; then + status="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H 'Content-Type: application/json' \ + -d "${payload}" \ + "${KC_URL}/admin/realms/atlas/clients")" + if [ "${status}" != "201" ] && [ "${status}" != "204" ] && [ "${status}" != "409" ]; then + echo "Keycloak service account client ${client_id} create failed (status ${status})" >&2 + exit 1 + fi + query="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${KC_URL}/admin/realms/atlas/clients?clientId=${client_id}" || true)" + internal_id="$(printf '%s' "${query}" | jq -r '.[0].id' 2>/dev/null || true)" + fi + if [ -z "${internal_id}" ] || [ "${internal_id}" = "null" ]; then + echo "Keycloak service account client ${client_id} was not found after create" >&2 + exit 1 + fi + + status="$(curl -sS -o /dev/null -w '%{http_code}' -X PUT \ + -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -H 'Content-Type: application/json' \ + -d "${payload}" \ + "${KC_URL}/admin/realms/atlas/clients/${internal_id}")" + if [ "${status}" != "204" ]; then + echo "Keycloak service account client ${client_id} update failed (status ${status})" >&2 + exit 1 + fi + + client_secret="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + "${KC_URL}/admin/realms/atlas/clients/${internal_id}/client-secret" \ + | jq -r '.value' 2>/dev/null || true)" + if [ -z "${client_secret}" ] || [ "${client_secret}" = "null" ]; then + echo "Keycloak service account client ${client_id} secret was not returned" >&2 + exit 1 + fi + + issuer="https://sso.bstein.dev/realms/atlas" + token_url="${KC_URL}/realms/atlas/protocol/openid-connect/token" + state_file="/tmp/hermes-$(printf '%s' "${client_id}" | tr -c 'a-zA-Z0-9' '-').json" + vault_payload="$(jq -nc \ + --arg client_id "${client_id}" \ + --arg client_secret "${client_secret}" \ + --arg issuer "${issuer}" \ + --arg token_url "${token_url}" \ + '{data:{client_id:$client_id,client_secret:$client_secret,issuer:$issuer,token_url:$token_url}}')" + write_status="$(curl -sS -o "${state_file}" -w '%{http_code}' -X POST \ + -H "X-Vault-Token: ${vault_token}" \ + -H 'Content-Type: application/json' \ + -d "${vault_payload}" \ + "${VAULT_ADDR}/v1/kv/data/atlas/${vault_path}")" + if [ "${write_status}" != "200" ] && [ "${write_status}" != "204" ]; then + echo "Vault ${vault_path} write failed (status ${write_status})" >&2 + exit 1 + fi + echo "Hermes machine identity ${client_id} is ready" +} + ensure_telegram_config() { vault_path="hermes/chat-telegram" state_file="/tmp/hermes-chat-telegram.json" @@ -247,4 +330,5 @@ ensure_hermes_owner ensure_proxy_client "hermes-chat-proxy" "https://chat.hermes.bstein.dev" "hermes/chat-oidc" ensure_proxy_client "hermes-agent-proxy" "https://agent.hermes.bstein.dev" "hermes/agent-oidc" ensure_proxy_client "hermes-triage-proxy" "https://triage.hermes.bstein.dev" "hermes/triage-oidc" +ensure_service_account_client "hermes-automation" "hermes/developer-keycloak" ensure_telegram_config