keycloak client cleanup
This commit is contained in:
@@ -10,7 +10,7 @@ metadata:
|
|||||||
policies.kyverno.io/severity: medium
|
policies.kyverno.io/severity: medium
|
||||||
policies.kyverno.io/subject: Pod
|
policies.kyverno.io/subject: Pod
|
||||||
policies.kyverno.io/description: >-
|
policies.kyverno.io/description: >-
|
||||||
Injects an auth sidecar container into Pods annotated with policies.forteapps.io/auth: "true". Supports three auth modes controlled by the policies.forteapps.io/auth-type annotation: "token" (default), "oidc", and "mcp". In token mode the sidecar reads credentials from a mounted Secret volume. In OIDC mode the sidecar uses OpenID Connect with authority and client-id provided via required annotations (policies.forteapps.io/auth-oidc-authority and policies.forteapps.io/auth-oidc-client-id) and secrets from an auth-oidc Secret. In MCP mode the sidecar implements OAuth 2.0 for MCP servers per RFC 9728 (Protected Resource Metadata) and RFC 7591 (Dynamic Client Registration), configured via policies.forteapps.io/auth-mcp-resource and policies.forteapps.io/auth-mcp-authority annotations. The sidecar port defaults to 9001 and can be overridden via the policies.forteapps.io/auth-port annotation. A NetworkPolicy is generated to restrict ingress to the sidecar port only.
|
Injects an auth sidecar container into Pods annotated with policies.forteapps.io/auth: "true". Supports three auth modes controlled by the policies.forteapps.io/auth-type annotation: "token" (default), "oidc", and "mcp". In token mode the sidecar reads credentials from a mounted Secret volume. In OIDC mode the sidecar uses OpenID Connect with authority and client-id provided via required annotations (policies.forteapps.io/auth-oidc-authority and policies.forteapps.io/auth-oidc-client-id) and secrets from an auth-oidc Secret. In MCP mode the sidecar implements OAuth 2.0 for MCP servers per RFC 9728 (Protected Resource Metadata); Dynamic Client Registration (RFC 7591) is handled natively by Keycloak and consumed directly by MCP clients. Configured via policies.forteapps.io/auth-mcp-resource and policies.forteapps.io/auth-mcp-authority annotations. The sidecar port defaults to 9001 and can be overridden via the policies.forteapps.io/auth-port annotation. A NetworkPolicy is generated to restrict ingress to the sidecar port only.
|
||||||
spec:
|
spec:
|
||||||
background: false
|
background: false
|
||||||
rules:
|
rules:
|
||||||
|
|||||||
@@ -772,7 +772,7 @@ Internet → Traefik → Service:8080 → Auth Sidecar:8080 → localhost → Yo
|
|||||||
Three authentication modes are supported:
|
Three authentication modes are supported:
|
||||||
1. **Token-based**: Static tokens (simple, good for service-to-service or internal apps)
|
1. **Token-based**: Static tokens (simple, good for service-to-service or internal apps)
|
||||||
2. **OIDC**: OpenID Connect (full SSO, good for user-facing apps)
|
2. **OIDC**: OpenID Connect (full SSO, good for user-facing apps)
|
||||||
3. **MCP**: OAuth 2.0 for MCP servers via RFC 9728 / RFC 7591 (good for MCP tool servers requiring OAuth-based access control)
|
3. **MCP**: OAuth 2.0 for MCP servers via RFC 9728 (Protected Resource Metadata); Keycloak provides native RFC 7591 Dynamic Client Registration (good for MCP tool servers requiring OAuth-based access control)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1013,7 +1013,7 @@ auth:
|
|||||||
scopes: "openid,profile,email" # OIDC scopes (optional)
|
scopes: "openid,profile,email" # OIDC scopes (optional)
|
||||||
callbackPath: /auth/callback # OAuth callback path (optional)
|
callbackPath: /auth/callback # OAuth callback path (optional)
|
||||||
|
|
||||||
# MCP mode configuration (RFC 9728 / RFC 7591)
|
# MCP mode configuration (RFC 9728)
|
||||||
mcp:
|
mcp:
|
||||||
resource: "" # Protected resource URL (required for MCP)
|
resource: "" # Protected resource URL (required for MCP)
|
||||||
authority: "" # Authorization server URL (required for MCP)
|
authority: "" # Authorization server URL (required for MCP)
|
||||||
@@ -1161,7 +1161,7 @@ ingress:
|
|||||||
host: mcp-server.forteapps.net
|
host: mcp-server.forteapps.net
|
||||||
```
|
```
|
||||||
|
|
||||||
The MCP auth mode implements RFC 9728 (OAuth 2.0 Protected Resource Metadata) for authorization server discovery and RFC 7591 (OAuth 2.0 Dynamic Client Registration) for automatic client registration. MCP clients discover the authorization server and scopes from the `/.well-known/oauth-protected-resource` endpoint served by the sidecar.
|
The MCP auth mode implements RFC 9728 (OAuth 2.0 Protected Resource Metadata) for authorization server discovery. Dynamic Client Registration (RFC 7591) is handled natively by Keycloak; MCP clients discover the authorization server and scopes from the `/.well-known/oauth-protected-resource` endpoint served by the sidecar and then register directly with Keycloak.
|
||||||
|
|
||||||
#### Example 4: Disabling Authentication
|
#### Example 4: Disabling Authentication
|
||||||
|
|
||||||
|
|||||||
@@ -1736,7 +1736,7 @@ spec:
|
|||||||
2. `generate-auth-oidc-secret` - Creates Secret for OIDC mode
|
2. `generate-auth-oidc-secret` - Creates Secret for OIDC mode
|
||||||
3. `inject-sidecar-token` - Injects auth sidecar for token mode
|
3. `inject-sidecar-token` - Injects auth sidecar for token mode
|
||||||
4. `inject-sidecar-oidc` - Injects auth sidecar for OIDC mode
|
4. `inject-sidecar-oidc` - Injects auth sidecar for OIDC mode
|
||||||
5. `inject-sidecar-mcp` - Injects auth sidecar for MCP OAuth mode (RFC 9728 / RFC 7591)
|
5. `inject-sidecar-mcp` - Injects auth sidecar for MCP OAuth mode (RFC 9728)
|
||||||
6. `generate-auth-network-policy` - Creates NetworkPolicy to restrict ingress
|
6. `generate-auth-network-policy` - Creates NetworkPolicy to restrict ingress
|
||||||
|
|
||||||
#### Trigger Annotation
|
#### Trigger Annotation
|
||||||
@@ -1776,7 +1776,7 @@ policies.forteapps.io/auth-image: "ghcr.io/fortedigital/auth-sidecar"
|
|||||||
policies.forteapps.io/auth-image-version: "latest"
|
policies.forteapps.io/auth-image-version: "latest"
|
||||||
```
|
```
|
||||||
|
|
||||||
**MCP Mode** (OAuth 2.0 for MCP servers, implements RFC 9728 / RFC 7591):
|
**MCP Mode** (OAuth 2.0 for MCP servers, implements RFC 9728; MCP clients use Keycloak's native RFC 7591 endpoint for Dynamic Client Registration):
|
||||||
```yaml
|
```yaml
|
||||||
# Annotations (required)
|
# Annotations (required)
|
||||||
policies.forteapps.io/auth: "true"
|
policies.forteapps.io/auth: "true"
|
||||||
@@ -2004,7 +2004,7 @@ Pod: Auth Sidecar (port 8080)
|
|||||||
├─ Validate credentials
|
├─ Validate credentials
|
||||||
│ • Token mode: Check Bearer token
|
│ • Token mode: Check Bearer token
|
||||||
│ • OIDC mode: Validate session or redirect to IdP
|
│ • OIDC mode: Validate session or redirect to IdP
|
||||||
│ • MCP mode: OAuth 2.0 via RFC 9728 discovery / RFC 7591 dynamic registration
|
│ • MCP mode: OAuth 2.0 via RFC 9728 discovery; Keycloak handles RFC 7591 dynamic registration natively
|
||||||
↓
|
↓
|
||||||
Forward to Application (localhost:3000)
|
Forward to Application (localhost:3000)
|
||||||
↓
|
↓
|
||||||
|
|||||||
@@ -602,3 +602,146 @@ extraDeploy:
|
|||||||
items:
|
items:
|
||||||
- key: admin-password
|
- key: admin-password
|
||||||
path: 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