client secret bootstrapping
Some checks failed
Deploy Gitea Pages / build-and-deploy (push) Failing after 39m32s

This commit is contained in:
2026-04-16 13:55:13 +02:00
parent 87ee0588a7
commit 7e10954a8f
5 changed files with 411 additions and 5 deletions

View File

@@ -72,13 +72,17 @@ keycloakConfigCli:
"enabled": true,
"protocol": "openid-connect",
"clientAuthenticatorType": "client-secret",
"secret": "382ed413580cb79d0f54813e5da87007b28fe766a8903d378b9e1c266405a784",
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"publicClient": false,
"redirectUris": ["https://git.forteapps.net/*"],
"webOrigins": ["https://git.forteapps.net"],
"defaultClientScopes": ["openid", "email", "profile"],
"attributes": {
"k8s.secret.sync": "true",
"k8s.secret.namespace": "gitea",
"k8s.secret.name": "gitea-oidc-credentials"
},
"protocolMappers": [
{
"name": "email_verified",
@@ -97,3 +101,210 @@ keycloakConfigCli:
}
]
}
extraDeploy:
# -- ServiceAccount for the secret syncer Job
- apiVersion: v1
kind: ServiceAccount
metadata:
name: keycloak-secret-syncer
namespace: keycloak
# -- ClusterRole granting access to secrets and namespaces
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: keycloak-secret-syncer
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "update", "patch"]
- apiGroups: [""]
resources: ["namespaces"]
verbs: ["get", "list"]
# -- ClusterRoleBinding for the syncer ServiceAccount
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: keycloak-secret-syncer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: keycloak-secret-syncer
subjects:
- kind: ServiceAccount
name: keycloak-secret-syncer
namespace: keycloak
# -- PostSync Job: extracts Keycloak client secrets into K8s Secrets
- apiVersion: batch/v1
kind: Job
metadata:
name: keycloak-secret-syncer
namespace: keycloak
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 3
template:
spec:
serviceAccountName: keycloak-secret-syncer
restartPolicy: Never
containers:
- name: syncer
image: alpine:3.20
command: ["/bin/sh", "-c"]
args:
- |
set -e
apk add --no-cache curl jq > /dev/null 2>&1
KEYCLOAK_URL="http://keycloak:80"
REALM="forte"
# Read admin credentials from the keycloak-credentials secret
ADMIN_USER="admin"
ADMIN_PASS=$(cat /secrets/admin-password)
# Authenticate to Keycloak Admin API
echo "Authenticating to Keycloak..."
TOKEN=$(curl -sf -X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" \
-d "username=${ADMIN_USER}" \
-d "password=${ADMIN_PASS}" \
-d "grant_type=password" | jq -r '.access_token')
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "ERROR: Failed to authenticate to Keycloak"
exit 1
fi
# Get all clients in the realm
echo "Fetching clients from realm '${REALM}'..."
CLIENTS=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients")
# Filter clients with k8s.secret.sync=true
SYNC_CLIENTS=$(echo "$CLIENTS" | jq -c '[.[] | select(.attributes["k8s.secret.sync"] == "true")]')
COUNT=$(echo "$SYNC_CLIENTS" | jq 'length')
echo "Found ${COUNT} client(s) with sync enabled"
K8S_API="https://kubernetes.default.svc"
SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA_CERT="/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
CENTRAL_NS="secrets"
# Upsert a K8s Secret: try PUT (update), fall back to POST (create)
upsert_secret() {
local ns="$1" name="$2" manifest="$3"
local code
code=$(curl -sf -o /dev/null -w "%{http_code}" \
--cacert "$CA_CERT" \
-H "Authorization: Bearer ${SA_TOKEN}" \
-H "Content-Type: application/json" \
-X PUT -d "$manifest" \
"${K8S_API}/api/v1/namespaces/${ns}/secrets/${name}")
if [ "$code" = "200" ]; then
echo " Updated secret '${ns}/${name}'"
elif [ "$code" = "404" ]; then
code=$(curl -sf -o /dev/null -w "%{http_code}" \
--cacert "$CA_CERT" \
-H "Authorization: Bearer ${SA_TOKEN}" \
-H "Content-Type: application/json" \
-X POST -d "$manifest" \
"${K8S_API}/api/v1/namespaces/${ns}/secrets")
if [ "$code" = "201" ]; then
echo " Created secret '${ns}/${name}'"
else
echo " ERROR: Failed to create secret '${ns}/${name}' (HTTP ${code})"
return 1
fi
else
echo " ERROR: Failed to update secret '${ns}/${name}' (HTTP ${code})"
return 1
fi
}
# Build a Secret JSON manifest
build_manifest() {
local ns="$1" name="$2" b64_id="$3" b64_secret="$4"
cat <<MANIFEST
{
"apiVersion": "v1",
"kind": "Secret",
"metadata": {
"name": "${name}",
"namespace": "${ns}",
"labels": {
"app.kubernetes.io/managed-by": "keycloak-secret-syncer"
}
},
"type": "Opaque",
"data": {
"client-id": "${b64_id}",
"client-secret": "${b64_secret}"
}
}
MANIFEST
}
echo "$SYNC_CLIENTS" | jq -c '.[]' | while read -r CLIENT; do
CLIENT_ID=$(echo "$CLIENT" | jq -r '.clientId')
CLIENT_UUID=$(echo "$CLIENT" | jq -r '.id')
TARGET_NS=$(echo "$CLIENT" | jq -r '.attributes["k8s.secret.namespace"]')
TARGET_NAME=$(echo "$CLIENT" | jq -r '.attributes["k8s.secret.name"]')
echo "Processing client '${CLIENT_ID}' -> secret '${TARGET_NS}/${TARGET_NAME}'"
# Get the client secret from Keycloak
SECRET_VALUE=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/client-secret" \
| jq -r '.value')
if [ -z "$SECRET_VALUE" ] || [ "$SECRET_VALUE" = "null" ]; then
echo " WARNING: No secret found for client '${CLIENT_ID}', skipping"
continue
fi
B64_CLIENT_ID=$(printf '%s' "$CLIENT_ID" | base64 | tr -d '\n')
B64_SECRET=$(printf '%s' "$SECRET_VALUE" | base64 | tr -d '\n')
# 1. Write to target namespace (if it exists)
NS_STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \
--cacert "$CA_CERT" \
-H "Authorization: Bearer ${SA_TOKEN}" \
"${K8S_API}/api/v1/namespaces/${TARGET_NS}")
if [ "$NS_STATUS" = "200" ]; then
MANIFEST=$(build_manifest "$TARGET_NS" "$TARGET_NAME" "$B64_CLIENT_ID" "$B64_SECRET")
upsert_secret "$TARGET_NS" "$TARGET_NAME" "$MANIFEST" || exit 1
else
echo " WARNING: Namespace '${TARGET_NS}' does not exist, skipping target"
fi
# 2. Always write a central copy to the secrets namespace
CENTRAL_MANIFEST=$(build_manifest "$CENTRAL_NS" "$TARGET_NAME" "$B64_CLIENT_ID" "$B64_SECRET")
upsert_secret "$CENTRAL_NS" "$TARGET_NAME" "$CENTRAL_MANIFEST" || exit 1
done
echo "Secret sync complete"
volumeMounts:
- name: keycloak-credentials
mountPath: /secrets
readOnly: true
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumes:
- name: keycloak-credentials
secret:
secretName: keycloak-credentials
items:
- key: admin-password
path: admin-password