keycloak client cleanup

This commit is contained in:
2026-06-03 17:28:08 +02:00
parent 428de7af78
commit 376d81a5ac
4 changed files with 150 additions and 7 deletions

View File

@@ -602,3 +602,146 @@ extraDeploy:
items:
- key: admin-password
path: admin-password
# -- ServiceAccount for the client cleanup CronJob
- apiVersion: v1
kind: ServiceAccount
metadata:
name: keycloak-client-cleanup
namespace: keycloak
# -- CronJob: cleans up stale dynamically registered Keycloak clients
- apiVersion: batch/v1
kind: CronJob
metadata:
name: keycloak-client-cleanup
namespace: keycloak
spec:
schedule: "0 3 * * 0"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 3
template:
spec:
serviceAccountName: keycloak-client-cleanup
restartPolicy: Never
containers:
- name: cleanup
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"
ADMIN_USER="admin"
ADMIN_PASS=$(cat /secrets/admin-password)
DRY_RUN="${DRY_RUN:-true}"
MIN_AGE_DAYS="${MIN_AGE_DAYS:-7}"
CLIENT_ID_PATTERN="${CLIENT_ID_PATTERN:-^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$}"
echo "=== Keycloak DCR client cleanup ==="
echo "Dry run: ${DRY_RUN}"
echo "Min age: ${MIN_AGE_DAYS} days"
echo "Pattern: ${CLIENT_ID_PATTERN}"
# Authenticate to Keycloak Admin API
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
NOW_SEC=$(date +%s)
MIN_AGE_SEC=$((MIN_AGE_DAYS * 86400))
# Hardcoded protected clients (never delete these)
PROTECTED_JSON='["gitea","grafana","argocd","vaultwarden","account","account-console","admin-cli","broker","realm-management","security-admin-console"]'
echo "Fetching clients from realm '${REALM}'..."
CLIENTS=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients")
CANDIDATES=$(echo "$CLIENTS" | jq -c --argjson protected "$PROTECTED_JSON" --argjson now "$NOW_SEC" --argjson min_age "$MIN_AGE_SEC" --arg pattern "$CLIENT_ID_PATTERN" '
[
.[]
| select(.clientId as $cid | $protected | index($cid) | not)
| select(.attributes["k8s.secret.sync"] != "true")
| select(.clientId | test($pattern; "i"))
| select((.createdTimestamp // 0) / 1000 < ($now - $min_age))
]
')
COUNT=$(echo "$CANDIDATES" | jq 'length')
echo "Found ${COUNT} candidate client(s) matching pattern and age threshold"
if [ "$COUNT" -eq 0 ]; then
echo "Nothing to clean up."
exit 0
fi
while IFS= read -r CLIENT; do
CLIENT_ID=$(echo "$CLIENT" | jq -r '.clientId')
CLIENT_UUID=$(echo "$CLIENT" | jq -r '.id')
CREATED=$(echo "$CLIENT" | jq -r '.createdTimestamp')
# Check active sessions
SESSION_RESPONSE=$(curl -s -H "Authorization: Bearer ${TOKEN}" \
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/session-count" || true)
SESSION_COUNT=$(echo "$SESSION_RESPONSE" | jq -r '.count // 0')
if [ "$SESSION_COUNT" -gt 0 ]; then
echo " SKIP: '${CLIENT_ID}' has ${SESSION_COUNT} active session(s)"
continue
fi
AGE_DAYS=$(( (NOW_SEC - (CREATED / 1000)) / 86400 ))
if [ "$DRY_RUN" = "true" ]; then
echo " DRY-RUN: would delete '${CLIENT_ID}' (uuid: ${CLIENT_UUID}, age: ${AGE_DAYS}d)"
else
echo " DELETE: '${CLIENT_ID}' (uuid: ${CLIENT_UUID}, age: ${AGE_DAYS}d)"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer ${TOKEN}" \
-X DELETE \
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}" || echo "000")
if [ "$HTTP_CODE" != "204" ] && [ "$HTTP_CODE" != "200" ]; then
echo " ERROR: Failed to delete '${CLIENT_ID}' (HTTP ${HTTP_CODE})"
fi
fi
done < <(echo "$CANDIDATES" | jq -c '.[]')
echo "Cleanup run complete."
env:
- name: DRY_RUN
value: "true"
- name: MIN_AGE_DAYS
value: "7"
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