2026-08-02 02:30:44 -03:00
|
|
|
#!/usr/bin/env sh
|
|
|
|
|
set -eu
|
|
|
|
|
|
|
|
|
|
. /vault/secrets/keycloak-admin-env.sh
|
|
|
|
|
|
|
|
|
|
KC_URL="http://keycloak.sso.svc.cluster.local"
|
2026-08-08 17:59:45 -03:00
|
|
|
VAULT_ADDR="${VAULT_ADDR:-http://vault.vault.svc.cluster.local:8200}"
|
|
|
|
|
VAULT_ROLE="${VAULT_ROLE:-sso-secrets}"
|
2026-08-02 02:30:44 -03:00
|
|
|
|
|
|
|
|
ACCESS_TOKEN=""
|
|
|
|
|
for attempt in 1 2 3 4 5 6 7 8 9 10; do
|
|
|
|
|
if curl -fsS "${KC_URL}/realms/master" >/dev/null 2>&1; then
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
echo "Waiting for Keycloak to be reachable (attempt ${attempt})" >&2
|
|
|
|
|
sleep $((attempt * 2))
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
for attempt in 1 2 3 4 5; do
|
|
|
|
|
token_json="$(curl -sS -X POST "${KC_URL}/realms/master/protocol/openid-connect/token" \
|
|
|
|
|
-H 'Content-Type: application/x-www-form-urlencoded' \
|
|
|
|
|
-d "grant_type=password" \
|
|
|
|
|
-d "client_id=admin-cli" \
|
|
|
|
|
-d "username=${KEYCLOAK_ADMIN}" \
|
|
|
|
|
-d "password=${KEYCLOAK_ADMIN_PASSWORD}" || true)"
|
|
|
|
|
ACCESS_TOKEN="$(printf '%s' "${token_json}" | jq -r '.access_token' 2>/dev/null || true)"
|
|
|
|
|
if [ -n "${ACCESS_TOKEN}" ] && [ "${ACCESS_TOKEN}" != "null" ]; then
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
sleep $((attempt * 2))
|
|
|
|
|
done
|
|
|
|
|
if [ -z "${ACCESS_TOKEN}" ] || [ "${ACCESS_TOKEN}" = "null" ]; then
|
|
|
|
|
echo "Failed to fetch Keycloak admin token" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-08-10 16:31:06 -03:00
|
|
|
ensure_hermes_owner() {
|
|
|
|
|
group_name="hermes-owner"
|
|
|
|
|
groups="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/groups?search=${group_name}" || true)"
|
|
|
|
|
group_id="$(printf '%s' "${groups}" | jq -r --arg name "${group_name}" \
|
|
|
|
|
'[.[] | select(.name == $name)][0].id // empty')"
|
|
|
|
|
if [ -z "${group_id}" ]; then
|
|
|
|
|
status="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
|
|
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
-d "$(jq -nc --arg name "${group_name}" '{name:$name}')" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/groups")"
|
|
|
|
|
if [ "${status}" != "201" ] && [ "${status}" != "204" ] && [ "${status}" != "409" ]; then
|
|
|
|
|
echo "Keycloak ${group_name} group create failed (status ${status})" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
groups="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/groups?search=${group_name}" || true)"
|
|
|
|
|
group_id="$(printf '%s' "${groups}" | jq -r --arg name "${group_name}" \
|
|
|
|
|
'[.[] | select(.name == $name)][0].id // empty')"
|
|
|
|
|
fi
|
|
|
|
|
users="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/users?username=bstein&exact=true&max=1" || true)"
|
|
|
|
|
user_id="$(printf '%s' "${users}" | jq -r '.[0].id // empty')"
|
|
|
|
|
if [ -z "${group_id}" ] || [ -z "${user_id}" ]; then
|
|
|
|
|
echo "Unable to resolve the immutable Hermes owner group or bstein user" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
status="$(curl -sS -o /dev/null -w '%{http_code}' -X PUT \
|
|
|
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/users/${user_id}/groups/${group_id}")"
|
|
|
|
|
if [ "${status}" != "204" ] && [ "${status}" != "200" ]; then
|
|
|
|
|
echo "Unable to assign bstein to ${group_name} (status ${status})" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 17:59:45 -03:00
|
|
|
jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
|
|
|
|
|
login_payload="$(jq -nc --arg jwt "${jwt}" --arg role "${VAULT_ROLE}" '{jwt:$jwt,role:$role}')"
|
|
|
|
|
vault_token="$(curl -sS --request POST --data "${login_payload}" \
|
|
|
|
|
"${VAULT_ADDR}/v1/auth/kubernetes/login" | jq -r '.auth.client_token')"
|
|
|
|
|
if [ -z "${vault_token}" ] || [ "${vault_token}" = "null" ]; then
|
|
|
|
|
echo "Vault login failed" >&2
|
2026-08-02 02:30:44 -03:00
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-08-08 17:59:45 -03:00
|
|
|
ensure_proxy_client() {
|
|
|
|
|
client_id="$1"
|
|
|
|
|
public_url="$2"
|
|
|
|
|
vault_path="$3"
|
hermes: restore legacy chat/triage hosts alongside the renamed ones
PR #34 renamed the public chat/triage hosts in place rather than adding
the new names, so chat.hermes.bstein.dev and triage.hermes.bstein.dev
were dropped from the certificate SANs, the hermes-sites Ingress rules
and the CoreDNS overrides at once. Both legacy hosts now answer 404 with
Traefik's default self-signed certificate, and the renamed hosts cannot
complete a login because the Keycloak clients still carry the old
redirect URIs, so chat and triage are unreachable on every hostname.
Make the rename additive, which is the rollback path the post-merge
runbook asks for when the OIDC step fails:
- put the legacy names back on hermes-sites-tls and on the Ingress,
pointing at the same oauth2-proxy backends
- restore both CoreDNS host overrides for in-cluster resolution
- teach ensure_proxy_client to register an optional legacy origin, so
hermes-chat-proxy and hermes-triage-proxy accept the old and new
redirect URIs, web origins and post-logout origins at the same time
while rootUrl stays on the canonical new host
- bump the immutable ensure Job so Flux reruns the script
Serving both names is deliberate: oauth2-proxy cookies are host-bound,
so redirecting the legacy hosts would silently drop live sessions.
Retiring them stays a separate, explicit change.
Supersedes #36, which only bumped the Job and would have left the
legacy hosts dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:49:09 +00:00
|
|
|
# Optional legacy host kept registered alongside the canonical one during a
|
|
|
|
|
# hostname rename. Keycloak matches redirect_uri exactly, so dropping the old
|
|
|
|
|
# entry turns every in-flight login into "Invalid parameter: redirect_uri".
|
|
|
|
|
legacy_url="${4:-}"
|
|
|
|
|
if [ -n "${legacy_url}" ]; then
|
|
|
|
|
origins="$(jq -nc --arg a "${public_url}" --arg b "${legacy_url}" '[$a,$b]')"
|
|
|
|
|
else
|
|
|
|
|
origins="$(jq -nc --arg a "${public_url}" '[$a]')"
|
|
|
|
|
fi
|
|
|
|
|
redirect_uris="$(printf '%s' "${origins}" | jq -c 'map(. + "/oauth2/callback")')"
|
|
|
|
|
# Keycloak takes post-logout origins as one "##"-delimited string.
|
|
|
|
|
post_logout="$(printf '%s' "${origins}" | jq -r 'join("##")')"
|
2026-08-08 17:59:45 -03:00
|
|
|
payload="$(jq -nc \
|
|
|
|
|
--arg client_id "${client_id}" \
|
hermes: restore legacy chat/triage hosts alongside the renamed ones
PR #34 renamed the public chat/triage hosts in place rather than adding
the new names, so chat.hermes.bstein.dev and triage.hermes.bstein.dev
were dropped from the certificate SANs, the hermes-sites Ingress rules
and the CoreDNS overrides at once. Both legacy hosts now answer 404 with
Traefik's default self-signed certificate, and the renamed hosts cannot
complete a login because the Keycloak clients still carry the old
redirect URIs, so chat and triage are unreachable on every hostname.
Make the rename additive, which is the rollback path the post-merge
runbook asks for when the OIDC step fails:
- put the legacy names back on hermes-sites-tls and on the Ingress,
pointing at the same oauth2-proxy backends
- restore both CoreDNS host overrides for in-cluster resolution
- teach ensure_proxy_client to register an optional legacy origin, so
hermes-chat-proxy and hermes-triage-proxy accept the old and new
redirect URIs, web origins and post-logout origins at the same time
while rootUrl stays on the canonical new host
- bump the immutable ensure Job so Flux reruns the script
Serving both names is deliberate: oauth2-proxy cookies are host-bound,
so redirecting the legacy hosts would silently drop live sessions.
Retiring them stays a separate, explicit change.
Supersedes #36, which only bumped the Job and would have left the
legacy hosts dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:49:09 +00:00
|
|
|
--argjson redirect_uris "${redirect_uris}" \
|
|
|
|
|
--argjson web_origins "${origins}" \
|
2026-08-08 17:59:45 -03:00
|
|
|
--arg web_origin "${public_url}" \
|
hermes: restore legacy chat/triage hosts alongside the renamed ones
PR #34 renamed the public chat/triage hosts in place rather than adding
the new names, so chat.hermes.bstein.dev and triage.hermes.bstein.dev
were dropped from the certificate SANs, the hermes-sites Ingress rules
and the CoreDNS overrides at once. Both legacy hosts now answer 404 with
Traefik's default self-signed certificate, and the renamed hosts cannot
complete a login because the Keycloak clients still carry the old
redirect URIs, so chat and triage are unreachable on every hostname.
Make the rename additive, which is the rollback path the post-merge
runbook asks for when the OIDC step fails:
- put the legacy names back on hermes-sites-tls and on the Ingress,
pointing at the same oauth2-proxy backends
- restore both CoreDNS host overrides for in-cluster resolution
- teach ensure_proxy_client to register an optional legacy origin, so
hermes-chat-proxy and hermes-triage-proxy accept the old and new
redirect URIs, web origins and post-logout origins at the same time
while rootUrl stays on the canonical new host
- bump the immutable ensure Job so Flux reruns the script
Serving both names is deliberate: oauth2-proxy cookies are host-bound,
so redirecting the legacy hosts would silently drop live sessions.
Retiring them stays a separate, explicit change.
Supersedes #36, which only bumped the Job and would have left the
legacy hosts dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:49:09 +00:00
|
|
|
--arg post_logout "${post_logout}" \
|
2026-08-08 17:59:45 -03:00
|
|
|
'{
|
|
|
|
|
clientId:$client_id,
|
|
|
|
|
name:$client_id,
|
|
|
|
|
enabled:true,
|
|
|
|
|
protocol:"openid-connect",
|
|
|
|
|
publicClient:false,
|
|
|
|
|
standardFlowEnabled:true,
|
|
|
|
|
implicitFlowEnabled:false,
|
|
|
|
|
directAccessGrantsEnabled:false,
|
|
|
|
|
serviceAccountsEnabled:false,
|
hermes: restore legacy chat/triage hosts alongside the renamed ones
PR #34 renamed the public chat/triage hosts in place rather than adding
the new names, so chat.hermes.bstein.dev and triage.hermes.bstein.dev
were dropped from the certificate SANs, the hermes-sites Ingress rules
and the CoreDNS overrides at once. Both legacy hosts now answer 404 with
Traefik's default self-signed certificate, and the renamed hosts cannot
complete a login because the Keycloak clients still carry the old
redirect URIs, so chat and triage are unreachable on every hostname.
Make the rename additive, which is the rollback path the post-merge
runbook asks for when the OIDC step fails:
- put the legacy names back on hermes-sites-tls and on the Ingress,
pointing at the same oauth2-proxy backends
- restore both CoreDNS host overrides for in-cluster resolution
- teach ensure_proxy_client to register an optional legacy origin, so
hermes-chat-proxy and hermes-triage-proxy accept the old and new
redirect URIs, web origins and post-logout origins at the same time
while rootUrl stays on the canonical new host
- bump the immutable ensure Job so Flux reruns the script
Serving both names is deliberate: oauth2-proxy cookies are host-bound,
so redirecting the legacy hosts would silently drop live sessions.
Retiring them stays a separate, explicit change.
Supersedes #36, which only bumped the Job and would have left the
legacy hosts dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:49:09 +00:00
|
|
|
redirectUris:$redirect_uris,
|
|
|
|
|
webOrigins:$web_origins,
|
2026-08-08 17:59:45 -03:00
|
|
|
rootUrl:$web_origin,
|
|
|
|
|
baseUrl:"/",
|
|
|
|
|
attributes:{
|
|
|
|
|
"pkce.code.challenge.method":"S256",
|
hermes: restore legacy chat/triage hosts alongside the renamed ones
PR #34 renamed the public chat/triage hosts in place rather than adding
the new names, so chat.hermes.bstein.dev and triage.hermes.bstein.dev
were dropped from the certificate SANs, the hermes-sites Ingress rules
and the CoreDNS overrides at once. Both legacy hosts now answer 404 with
Traefik's default self-signed certificate, and the renamed hosts cannot
complete a login because the Keycloak clients still carry the old
redirect URIs, so chat and triage are unreachable on every hostname.
Make the rename additive, which is the rollback path the post-merge
runbook asks for when the OIDC step fails:
- put the legacy names back on hermes-sites-tls and on the Ingress,
pointing at the same oauth2-proxy backends
- restore both CoreDNS host overrides for in-cluster resolution
- teach ensure_proxy_client to register an optional legacy origin, so
hermes-chat-proxy and hermes-triage-proxy accept the old and new
redirect URIs, web origins and post-logout origins at the same time
while rootUrl stays on the canonical new host
- bump the immutable ensure Job so Flux reruns the script
Serving both names is deliberate: oauth2-proxy cookies are host-bound,
so redirecting the legacy hosts would silently drop live sessions.
Retiring them stays a separate, explicit change.
Supersedes #36, which only bumped the Job and would have left the
legacy hosts dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:49:09 +00:00
|
|
|
"post.logout.redirect.uris":$post_logout,
|
2026-08-08 17:59:45 -03:00
|
|
|
"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
|
2026-08-02 02:30:44 -03:00
|
|
|
status="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
|
|
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
2026-08-08 17:59:45 -03:00
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
-d "${payload}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/clients")"
|
|
|
|
|
if [ "${status}" != "201" ] && [ "${status}" != "204" ] && [ "${status}" != "409" ]; then
|
|
|
|
|
echo "Keycloak client ${client_id} create failed (status ${status})" >&2
|
2026-08-02 02:30:44 -03:00
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-08-08 17:59:45 -03:00
|
|
|
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 client ${client_id} was not found after create" >&2
|
|
|
|
|
exit 1
|
2026-08-02 02:30:44 -03:00
|
|
|
fi
|
|
|
|
|
|
2026-08-08 17:59:45 -03:00
|
|
|
status="$(curl -sS -o /dev/null -w '%{http_code}' -X PUT \
|
2026-08-02 02:30:44 -03:00
|
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
-H 'Content-Type: application/json' \
|
2026-08-08 17:59:45 -03:00
|
|
|
-d "${payload}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/clients/${internal_id}")"
|
|
|
|
|
if [ "${status}" != "204" ]; then
|
|
|
|
|
echo "Keycloak client ${client_id} update failed (status ${status})" >&2
|
2026-08-02 02:30:44 -03:00
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
2026-08-10 16:31:06 -03:00
|
|
|
mapper_payload="$(jq -nc \
|
|
|
|
|
'{name:"groups",protocol:"openid-connect",protocolMapper:"oidc-group-membership-mapper",consentRequired:false,config:{"full.path":"true","id.token.claim":"true","access.token.claim":"true","userinfo.token.claim":"true","claim.name":"groups","jsonType.label":"String"}}')"
|
|
|
|
|
mappers="$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/clients/${internal_id}/protocol-mappers/models" || true)"
|
|
|
|
|
mapper_id="$(printf '%s' "${mappers}" | jq -r '[.[] | select(.name == "groups")][0].id // empty')"
|
|
|
|
|
if [ -n "${mapper_id}" ]; then
|
|
|
|
|
update_payload="$(printf '%s' "${mapper_payload}" | jq -c --arg id "${mapper_id}" '. + {id:$id}')"
|
|
|
|
|
mapper_status="$(curl -sS -o /dev/null -w '%{http_code}' -X PUT \
|
|
|
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
-d "${update_payload}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/clients/${internal_id}/protocol-mappers/models/${mapper_id}")"
|
|
|
|
|
[ "${mapper_status}" = "204" ] || {
|
|
|
|
|
echo "Keycloak ${client_id} groups mapper update failed (status ${mapper_status})" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
mapper_status="$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
|
|
|
|
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
|
|
|
-H 'Content-Type: application/json' \
|
|
|
|
|
-d "${mapper_payload}" \
|
|
|
|
|
"${KC_URL}/admin/realms/atlas/clients/${internal_id}/protocol-mappers/models")"
|
|
|
|
|
[ "${mapper_status}" = "201" ] || [ "${mapper_status}" = "204" ] || {
|
|
|
|
|
echo "Keycloak ${client_id} groups mapper create failed (status ${mapper_status})" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
}
|
|
|
|
|
fi
|
|
|
|
|
|
2026-08-08 17:59:45 -03:00
|
|
|
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 client ${client_id} secret was not returned" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-08-02 02:30:44 -03:00
|
|
|
|
2026-08-08 17:59:45 -03:00
|
|
|
state_file="/tmp/hermes-$(printf '%s' "${client_id}" | tr -c 'a-zA-Z0-9' '-').json"
|
|
|
|
|
read_status="$(curl -sS -o "${state_file}" -w '%{http_code}' \
|
|
|
|
|
-H "X-Vault-Token: ${vault_token}" \
|
|
|
|
|
"${VAULT_ADDR}/v1/kv/data/atlas/${vault_path}" || true)"
|
|
|
|
|
cookie_secret=""
|
|
|
|
|
if [ "${read_status}" = "200" ]; then
|
|
|
|
|
cookie_secret="$(jq -r '.data.data.cookie_secret // empty' "${state_file}")"
|
|
|
|
|
elif [ "${read_status}" != "404" ]; then
|
|
|
|
|
echo "Vault ${vault_path} read failed (status ${read_status})" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-08-02 02:30:44 -03:00
|
|
|
cookie_length="$(printf '%s' "${cookie_secret}" | wc -c | tr -d ' ')"
|
2026-08-08 17:59:45 -03:00
|
|
|
case "${cookie_length}" in 16|24|32) ;; *) cookie_secret="$(openssl rand -hex 16 | tr -d '\n')" ;; esac
|
|
|
|
|
|
|
|
|
|
vault_payload="$(jq -nc \
|
|
|
|
|
--arg client_id "${client_id}" \
|
|
|
|
|
--arg client_secret "${client_secret}" \
|
|
|
|
|
--arg cookie_secret "${cookie_secret}" \
|
|
|
|
|
'{data:{client_id:$client_id,client_secret:$client_secret,cookie_secret:$cookie_secret}}')"
|
|
|
|
|
write_status="$(curl -sS -o "${state_file}.write" -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
|
2026-08-02 02:30:44 -03:00
|
|
|
fi
|
2026-08-08 17:59:45 -03:00
|
|
|
echo "Hermes OIDC client ${client_id} is ready"
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 17:45:09 -03:00
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-08 17:59:45 -03:00
|
|
|
ensure_telegram_config() {
|
|
|
|
|
vault_path="hermes/chat-telegram"
|
|
|
|
|
state_file="/tmp/hermes-chat-telegram.json"
|
|
|
|
|
read_status="$(curl -sS -o "${state_file}" -w '%{http_code}' \
|
|
|
|
|
-H "X-Vault-Token: ${vault_token}" \
|
|
|
|
|
"${VAULT_ADDR}/v1/kv/data/atlas/${vault_path}" || true)"
|
|
|
|
|
bot_token=""
|
|
|
|
|
relay_key=""
|
|
|
|
|
if [ "${read_status}" = "200" ]; then
|
|
|
|
|
bot_token="$(jq -r '.data.data.bot_token // empty' "${state_file}")"
|
|
|
|
|
relay_key="$(jq -r '.data.data.relay_key // empty' "${state_file}")"
|
|
|
|
|
elif [ "${read_status}" != "404" ]; then
|
|
|
|
|
echo "Vault ${vault_path} read failed (status ${read_status})" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
relay_length="$(printf '%s' "${relay_key}" | wc -c | tr -d ' ')"
|
|
|
|
|
if [ "${relay_length}" -lt 64 ]; then
|
|
|
|
|
relay_key="$(openssl rand -hex 32 | tr -d '\n')"
|
|
|
|
|
fi
|
|
|
|
|
vault_payload="$(jq -nc \
|
|
|
|
|
--arg bot_token "${bot_token}" \
|
|
|
|
|
--arg relay_key "${relay_key}" \
|
|
|
|
|
'{data:{bot_token:$bot_token,relay_key:$relay_key}}')"
|
|
|
|
|
write_status="$(curl -sS -o "${state_file}.write" -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 Telegram transport secret is ready"
|
|
|
|
|
}
|
2026-08-02 02:30:44 -03:00
|
|
|
|
2026-08-10 16:31:06 -03:00
|
|
|
ensure_hermes_owner
|
hermes: restore legacy chat/triage hosts alongside the renamed ones
PR #34 renamed the public chat/triage hosts in place rather than adding
the new names, so chat.hermes.bstein.dev and triage.hermes.bstein.dev
were dropped from the certificate SANs, the hermes-sites Ingress rules
and the CoreDNS overrides at once. Both legacy hosts now answer 404 with
Traefik's default self-signed certificate, and the renamed hosts cannot
complete a login because the Keycloak clients still carry the old
redirect URIs, so chat and triage are unreachable on every hostname.
Make the rename additive, which is the rollback path the post-merge
runbook asks for when the OIDC step fails:
- put the legacy names back on hermes-sites-tls and on the Ingress,
pointing at the same oauth2-proxy backends
- restore both CoreDNS host overrides for in-cluster resolution
- teach ensure_proxy_client to register an optional legacy origin, so
hermes-chat-proxy and hermes-triage-proxy accept the old and new
redirect URIs, web origins and post-logout origins at the same time
while rootUrl stays on the canonical new host
- bump the immutable ensure Job so Flux reruns the script
Serving both names is deliberate: oauth2-proxy cookies are host-bound,
so redirecting the legacy hosts would silently drop live sessions.
Retiring them stays a separate, explicit change.
Supersedes #36, which only bumped the Job and would have left the
legacy hosts dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:49:09 +00:00
|
|
|
ensure_proxy_client "hermes-chat-proxy" "https://chat.bstein.dev" "hermes/chat-oidc" \
|
|
|
|
|
"https://chat.hermes.bstein.dev"
|
2026-08-08 17:59:45 -03:00
|
|
|
ensure_proxy_client "hermes-agent-proxy" "https://agent.hermes.bstein.dev" "hermes/agent-oidc"
|
hermes: restore legacy chat/triage hosts alongside the renamed ones
PR #34 renamed the public chat/triage hosts in place rather than adding
the new names, so chat.hermes.bstein.dev and triage.hermes.bstein.dev
were dropped from the certificate SANs, the hermes-sites Ingress rules
and the CoreDNS overrides at once. Both legacy hosts now answer 404 with
Traefik's default self-signed certificate, and the renamed hosts cannot
complete a login because the Keycloak clients still carry the old
redirect URIs, so chat and triage are unreachable on every hostname.
Make the rename additive, which is the rollback path the post-merge
runbook asks for when the OIDC step fails:
- put the legacy names back on hermes-sites-tls and on the Ingress,
pointing at the same oauth2-proxy backends
- restore both CoreDNS host overrides for in-cluster resolution
- teach ensure_proxy_client to register an optional legacy origin, so
hermes-chat-proxy and hermes-triage-proxy accept the old and new
redirect URIs, web origins and post-logout origins at the same time
while rootUrl stays on the canonical new host
- bump the immutable ensure Job so Flux reruns the script
Serving both names is deliberate: oauth2-proxy cookies are host-bound,
so redirecting the legacy hosts would silently drop live sessions.
Retiring them stays a separate, explicit change.
Supersedes #36, which only bumped the Job and would have left the
legacy hosts dark.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:49:09 +00:00
|
|
|
ensure_proxy_client "hermes-triage-proxy" "https://triage.bstein.dev" "hermes/triage-oidc" \
|
|
|
|
|
"https://triage.hermes.bstein.dev"
|
2026-08-13 17:45:09 -03:00
|
|
|
ensure_service_account_client "hermes-automation" "hermes/developer-keycloak"
|
2026-08-08 17:59:45 -03:00
|
|
|
ensure_telegram_config
|