2026-01-19 18:21:24 +01:00
|
|
|
#!/bin/bash
|
|
|
|
|
# Automatic Keycloak OIDC client configuration script
|
|
|
|
|
# This script creates the fail2ban-ui OIDC client in Keycloak automatically
|
|
|
|
|
|
|
|
|
|
set -e
|
|
|
|
|
|
|
|
|
|
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:3000}"
|
|
|
|
|
KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}"
|
|
|
|
|
KEYCLOAK_PASSWORD="${KEYCLOAK_PASSWORD:-admin}"
|
|
|
|
|
REALM="${REALM:-master}"
|
|
|
|
|
CLIENT_ID="${CLIENT_ID:-fail2ban-ui}"
|
|
|
|
|
CLIENT_SECRET="${CLIENT_SECRET:-}"
|
|
|
|
|
# Use PUBLIC_FRONTEND_URL if provided, otherwise default to localhost
|
|
|
|
|
PUBLIC_FRONTEND_URL="${PUBLIC_FRONTEND_URL:-http://localhost:3080}"
|
|
|
|
|
REDIRECT_URI="${REDIRECT_URI:-${PUBLIC_FRONTEND_URL}/auth/callback}"
|
2026-01-19 22:09:54 +01:00
|
|
|
POST_LOGOUT_REDIRECT_URI="${POST_LOGOUT_REDIRECT_URI:-${PUBLIC_FRONTEND_URL}/auth/login}"
|
2026-01-19 18:21:24 +01:00
|
|
|
WEB_ORIGIN="${WEB_ORIGIN:-${PUBLIC_FRONTEND_URL}}"
|
|
|
|
|
|
|
|
|
|
# Extract host and port from KEYCLOAK_URL for health check
|
|
|
|
|
# KEYCLOAK_URL is the internal URL (e.g., http://keycloak:8080)
|
|
|
|
|
# Health endpoint is on management port 9000
|
|
|
|
|
KEYCLOAK_HOST=$(echo "${KEYCLOAK_URL}" | sed -E 's|https?://([^:/]+).*|\1|')
|
|
|
|
|
KEYCLOAK_HEALTH_URL="http://${KEYCLOAK_HOST}:9000/health/ready"
|
|
|
|
|
|
|
|
|
|
echo "Waiting for Keycloak to be ready..."
|
|
|
|
|
echo "Checking health endpoint: ${KEYCLOAK_HEALTH_URL}"
|
|
|
|
|
max_attempts=120 # Increased timeout since Keycloak can take a while
|
|
|
|
|
attempt=0
|
|
|
|
|
while [ $attempt -lt $max_attempts ]; do
|
|
|
|
|
# Check health endpoint on management port 9000
|
|
|
|
|
if curl -s -f "${KEYCLOAK_HEALTH_URL}" > /dev/null 2>&1; then
|
|
|
|
|
echo "Keycloak is ready!"
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
# Also try the main port as fallback
|
|
|
|
|
if curl -s -f "${KEYCLOAK_URL}/health/ready" > /dev/null 2>&1; then
|
|
|
|
|
echo "Keycloak is ready (via main port)!"
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
attempt=$((attempt + 1))
|
|
|
|
|
if [ $((attempt % 10)) -eq 0 ]; then
|
|
|
|
|
echo "Attempt $attempt/$max_attempts: Keycloak not ready yet, waiting..."
|
|
|
|
|
fi
|
|
|
|
|
sleep 2
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
if [ $attempt -eq $max_attempts ]; then
|
|
|
|
|
echo "ERROR: Keycloak did not become ready in time"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Waiting for Keycloak admin API to be available..."
|
|
|
|
|
sleep 5
|
|
|
|
|
|
|
|
|
|
echo "Getting admin access token..."
|
|
|
|
|
ADMIN_TOKEN=$(curl -s -X POST "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \
|
|
|
|
|
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
|
|
|
-d "username=${KEYCLOAK_ADMIN}" \
|
|
|
|
|
-d "password=${KEYCLOAK_PASSWORD}" \
|
|
|
|
|
-d "grant_type=password" \
|
|
|
|
|
-d "client_id=admin-cli" | jq -r '.access_token')
|
|
|
|
|
|
|
|
|
|
if [ -z "$ADMIN_TOKEN" ] || [ "$ADMIN_TOKEN" = "null" ]; then
|
|
|
|
|
echo "ERROR: Failed to get admin token"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo "Checking if client already exists..."
|
|
|
|
|
EXISTING_CLIENT=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=${CLIENT_ID}" \
|
|
|
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
|
|
|
-H "Content-Type: application/json" | jq -r '.[0].id // empty')
|
|
|
|
|
|
|
|
|
|
if [ -n "$EXISTING_CLIENT" ]; then
|
|
|
|
|
echo "Client '${CLIENT_ID}' already exists, updating..."
|
|
|
|
|
CLIENT_UUID="$EXISTING_CLIENT"
|
|
|
|
|
|
|
|
|
|
# Update client configuration
|
|
|
|
|
curl -s -X PUT "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}" \
|
|
|
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d "{
|
|
|
|
|
\"clientId\": \"${CLIENT_ID}\",
|
|
|
|
|
\"enabled\": true,
|
|
|
|
|
\"clientAuthenticatorType\": \"client-secret\",
|
|
|
|
|
\"redirectUris\": [\"${REDIRECT_URI}\"],
|
|
|
|
|
\"webOrigins\": [\"${WEB_ORIGIN}\"],
|
2026-01-19 22:09:54 +01:00
|
|
|
\"attributes\": {
|
|
|
|
|
\"post.logout.redirect.uris\": \"${POST_LOGOUT_REDIRECT_URI}\"
|
|
|
|
|
},
|
2026-01-19 18:21:24 +01:00
|
|
|
\"protocol\": \"openid-connect\",
|
|
|
|
|
\"publicClient\": false,
|
|
|
|
|
\"standardFlowEnabled\": true,
|
|
|
|
|
\"directAccessGrantsEnabled\": true
|
|
|
|
|
}" > /dev/null
|
|
|
|
|
|
|
|
|
|
echo "Client updated successfully"
|
|
|
|
|
else
|
|
|
|
|
echo "Creating new client '${CLIENT_ID}'..."
|
|
|
|
|
|
|
|
|
|
# Create client
|
|
|
|
|
CLIENT_RESPONSE=$(curl -s -X POST "${KEYCLOAK_URL}/admin/realms/${REALM}/clients" \
|
|
|
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d "{
|
|
|
|
|
\"clientId\": \"${CLIENT_ID}\",
|
|
|
|
|
\"enabled\": true,
|
|
|
|
|
\"clientAuthenticatorType\": \"client-secret\",
|
|
|
|
|
\"redirectUris\": [\"${REDIRECT_URI}\"],
|
|
|
|
|
\"webOrigins\": [\"${WEB_ORIGIN}\"],
|
2026-01-19 22:09:54 +01:00
|
|
|
\"attributes\": {
|
|
|
|
|
\"post.logout.redirect.uris\": \"${POST_LOGOUT_REDIRECT_URI}\"
|
|
|
|
|
},
|
2026-01-19 18:21:24 +01:00
|
|
|
\"protocol\": \"openid-connect\",
|
|
|
|
|
\"publicClient\": false,
|
|
|
|
|
\"standardFlowEnabled\": true,
|
|
|
|
|
\"directAccessGrantsEnabled\": true
|
|
|
|
|
}")
|
|
|
|
|
|
|
|
|
|
if [ $? -ne 0 ]; then
|
|
|
|
|
echo "ERROR: Failed to create client"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Get the client UUID
|
|
|
|
|
CLIENT_UUID=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=${CLIENT_ID}" \
|
|
|
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
|
|
|
-H "Content-Type: application/json" | jq -r '.[0].id')
|
|
|
|
|
|
|
|
|
|
echo "Client created successfully with UUID: ${CLIENT_UUID}"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Get or regenerate client secret
|
|
|
|
|
echo "Getting client secret..."
|
|
|
|
|
CLIENT_SECRET=$(curl -s -X GET "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/client-secret" \
|
|
|
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
|
|
|
-H "Content-Type: application/json" | jq -r '.value')
|
|
|
|
|
|
|
|
|
|
if [ -z "$CLIENT_SECRET" ] || [ "$CLIENT_SECRET" = "null" ]; then
|
|
|
|
|
echo "Regenerating client secret..."
|
|
|
|
|
CLIENT_SECRET=$(curl -s -X POST "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/client-secret" \
|
|
|
|
|
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
|
|
|
|
-H "Content-Type: application/json" | jq -r '.value')
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [ -z "$CLIENT_SECRET" ] || [ "$CLIENT_SECRET" = "null" ]; then
|
|
|
|
|
echo "ERROR: Failed to get client secret"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
echo "OIDC Client Configuration Complete!"
|
|
|
|
|
echo "=========================================="
|
|
|
|
|
echo "Client ID: ${CLIENT_ID}"
|
|
|
|
|
echo "Client Secret: ${CLIENT_SECRET}"
|
|
|
|
|
echo "Realm: ${REALM}"
|
|
|
|
|
echo "Redirect URI: ${REDIRECT_URI}"
|
2026-01-19 22:09:54 +01:00
|
|
|
echo "Post Logout Redirect URI: ${POST_LOGOUT_REDIRECT_URI}"
|
2026-01-19 18:21:24 +01:00
|
|
|
echo "=========================================="
|
|
|
|
|
|
|
|
|
|
# Save secret to shared volume for fail2ban-ui to read
|
|
|
|
|
SECRET_FILE="${SECRET_FILE:-/config/keycloak-client-secret}"
|
|
|
|
|
# Create directory if it doesn't exist
|
|
|
|
|
mkdir -p "$(dirname "${SECRET_FILE}")" 2>/dev/null || true
|
|
|
|
|
# Write secret file (running as root, so should have permissions)
|
|
|
|
|
if echo "${CLIENT_SECRET}" > "${SECRET_FILE}" 2>/dev/null; then
|
|
|
|
|
chmod 644 "${SECRET_FILE}" 2>/dev/null || true
|
|
|
|
|
echo "Client secret saved to ${SECRET_FILE} for fail2ban-ui"
|
|
|
|
|
else
|
|
|
|
|
echo "ERROR: Failed to write client secret to ${SECRET_FILE}"
|
|
|
|
|
echo "Client secret: ${CLIENT_SECRET}"
|
|
|
|
|
echo "Please save this secret manually to ${SECRET_FILE}"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|