keycloak client cleanup
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user