#!/usr/bin/env bash # Sync Keycloak users into Zot htpasswd. # Intended to be rendered into a ConfigMap/Job; keep real secrets out of git. set -euo pipefail require() { command -v "$1" >/dev/null 2>&1 || { echo "missing required binary: $1" >&2; exit 1; }; } require curl; require jq; require kubectl; require htpasswd : "${KEYCLOAK_URL:=https://sso.bstein.dev}" : "${KEYCLOAK_REALM:=atlas}" : "${KEYCLOAK_CLIENT_ID:?set KEYCLOAK_CLIENT_ID or export via secret}" : "${KEYCLOAK_CLIENT_SECRET:?set KEYCLOAK_CLIENT_SECRET or export via secret}" : "${ZOT_NAMESPACE:=zot}" : "${HTPASSWD_SECRET_NAME:=zot-htpasswd}" : "${DEFAULT_PASSWORD:=TempSsoPass!2025}" : "${UI_PROXY_USER:=zot-ui-proxy}" : "${UI_PROXY_PASSWORD:=TempSsoUiPass!2025}" fetch_token() { curl -fsS -X POST \ -d "grant_type=client_credentials" \ -d "client_id=${KEYCLOAK_CLIENT_ID}" \ -d "client_secret=${KEYCLOAK_CLIENT_SECRET}" \ "${KEYCLOAK_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token" \ | jq -r '.access_token' } pull_users() { local token="$1" curl -fsS -H "Authorization: Bearer ${token}" \ "${KEYCLOAK_URL}/admin/realms/${KEYCLOAK_REALM}/users?max=500" \ | jq -r '.[] | select(.enabled == true) | select(.username | startswith("service-account-") | not) | .username' } decode_secret_file() { local ns="$1" name="$2" key="$3" out="$4" if kubectl -n "${ns}" get secret "${name}" >/dev/null 2>&1; then kubectl -n "${ns}" get secret "${name}" -o "jsonpath={.data.${key}}" | base64 -d > "${out}" || true else : > "${out}" fi } ensure_htpasswd_line() { local user="$1" password="$2" file="$3" if ! grep -q "^${user}:" "${file}" 2>/dev/null; then htpasswd -nbB "${user}" "${password}" >> "${file}" echo "added user ${user} to htpasswd" fi } main() { local token tmp existing tmp="$(mktemp)" existing="$(mktemp)" trap 'rm -f "${tmp}" "${existing}"' EXIT decode_secret_file "${ZOT_NAMESPACE}" "${HTPASSWD_SECRET_NAME}" htpasswd "${existing}" cp "${existing}" "${tmp}" ensure_htpasswd_line "${UI_PROXY_USER}" "${UI_PROXY_PASSWORD}" "${tmp}" token="$(fetch_token)" readarray -t users < <(pull_users "${token}") for user in "${users[@]}"; do ensure_htpasswd_line "${user}" "${DEFAULT_PASSWORD}" "${tmp}" done kubectl create secret generic "${HTPASSWD_SECRET_NAME}" \ --namespace "${ZOT_NAMESPACE}" \ --from-file=htpasswd="${tmp}" \ --dry-run=client -o yaml | kubectl apply -f - } main "$@"