#!/bin/bash # keycloak-configure.sh — wire Keycloak to FreeIPA via LDAP user federation # # Run this AFTER both FreeIPA and Keycloak are fully up. # Reads settings from environment variables or a .env file in the same directory. # # Required env vars: # IPA_SERVER FreeIPA server FQDN # IPA_DOMAIN FreeIPA domain # IPA_DM_PASSWORD Directory Manager password (used as LDAP bind credential # unless IPA_BIND_DN / IPA_BIND_PASSWORD override it) # KC_ADMIN_PASSWORD Keycloak admin password # # Optional env vars (defaults shown): # KC_URL http://localhost:8080 # KC_ADMIN admin # KC_REALM freeipa (realm to create) # KC_REALM_DISPLAY # IPA_REALM # IPA_BIND_DN cn=Directory Manager # IPA_BIND_PASSWORD # IPA_USE_LDAPS false # IPA_LDAP_PORT 389 (or 636 if LDAPS) # SYNC_FULL_PERIOD 604800 (1 week) # SYNC_CHANGED_PERIOD 86400 (1 day) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" [[ -f "$SCRIPT_DIR/.env" ]] && set -a && source "$SCRIPT_DIR/.env" && set +a RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' log() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } error() { echo -e "${RED}[✗]${NC} $*" >&2; } info() { echo -e "${CYAN}[i]${NC} $*"; } : "${IPA_SERVER:?IPA_SERVER is required}" : "${IPA_DOMAIN:?IPA_DOMAIN is required}" : "${IPA_DM_PASSWORD:?IPA_DM_PASSWORD is required}" : "${KC_ADMIN_PASSWORD:?KC_ADMIN_PASSWORD is required}" KC_URL="${KC_URL:-http://localhost:8080}" KC_ADMIN="${KC_ADMIN:-admin}" KC_REALM="${KC_REALM:-freeipa}" KC_REALM_DISPLAY="${KC_REALM_DISPLAY:-$IPA_DOMAIN}" IPA_REALM="${IPA_REALM:-${IPA_DOMAIN^^}}" IPA_BIND_DN="${IPA_BIND_DN:-cn=Directory Manager}" IPA_BIND_PASSWORD="${IPA_BIND_PASSWORD:-$IPA_DM_PASSWORD}" IPA_USE_LDAPS="${IPA_USE_LDAPS:-false}" IPA_LDAP_SCHEME="ldap" IPA_LDAP_PORT=389 [[ "$IPA_USE_LDAPS" == "true" ]] && IPA_LDAP_SCHEME="ldaps" && IPA_LDAP_PORT=636 IPA_LDAP_URL="${IPA_LDAP_URL:-${IPA_LDAP_SCHEME}://${IPA_SERVER}:${IPA_LDAP_PORT}}" IPA_BASEDN="dc=${IPA_DOMAIN/./,dc=}" SYNC_FULL_PERIOD="${SYNC_FULL_PERIOD:-604800}" SYNC_CHANGED_PERIOD="${SYNC_CHANGED_PERIOD:-86400}" # ─── Helpers ────────────────────────────────────────────────────────────────── kc_token() { curl -sf -X POST \ "$KC_URL/realms/master/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=admin-cli&grant_type=password" \ -d "username=$KC_ADMIN" \ --data-urlencode "password=$KC_ADMIN_PASSWORD" \ | jq -r '.access_token' } kc_get() { curl -sf -H "Authorization: Bearer $TOKEN" "$KC_URL$1"; } kc_post() { curl -sf -X POST -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" -d "$2" "$KC_URL$1"; } kc_put() { curl -sf -X PUT -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" -d "$2" "$KC_URL$1"; } kc_status() { curl -sf -o /dev/null -w "%{http_code}" \ -H "Authorization: Bearer $TOKEN" "$KC_URL$1"; } # ─── Wait for Keycloak ──────────────────────────────────────────────────────── info "Waiting for Keycloak at $KC_URL..." for i in $(seq 1 60); do curl -sf "$KC_URL/health/ready" &>/dev/null && break [[ $i -eq 60 ]] && { error "Keycloak not ready after 120s."; exit 1; } sleep 2 done log "Keycloak is ready." # ─── Authenticate ───────────────────────────────────────────────────────────── TOKEN=$(kc_token) [[ -z "$TOKEN" || "$TOKEN" == "null" ]] && { error "Failed to obtain Keycloak token."; exit 1; } log "Admin token obtained." # ─── Create realm ───────────────────────────────────────────────────────────── REALM_STATUS=$(kc_status "/admin/realms/$KC_REALM") if [[ "$REALM_STATUS" == "200" ]]; then warn "Realm '$KC_REALM' already exists — updating." kc_put "/admin/realms/$KC_REALM" \ "{\"realm\":\"$KC_REALM\",\"displayName\":\"$KC_REALM_DISPLAY\",\"enabled\":true, \"ssoSessionMaxLifespan\":36000,\"accessTokenLifespan\":300}" >/dev/null else kc_post "/admin/realms" \ "{\"realm\":\"$KC_REALM\",\"displayName\":\"$KC_REALM_DISPLAY\",\"enabled\":true, \"ssoSessionMaxLifespan\":36000,\"accessTokenLifespan\":300}" >/dev/null log "Realm '$KC_REALM' created." fi TOKEN=$(kc_token) # ─── LDAP user federation ───────────────────────────────────────────────────── log "Configuring FreeIPA LDAP user federation..." LDAP_COMPONENT=$(cat </dev/null LDAP_ID="$EXISTING_ID" else LDAP_ID=$(kc_post "/admin/realms/$KC_REALM/components" "$LDAP_COMPONENT" \ | jq -r '.id // empty') # Keycloak returns 201 with Location header, not a body with id — extract from header or re-query if [[ -z "$LDAP_ID" ]]; then LDAP_ID=$(kc_get "/admin/realms/$KC_REALM/components?type=org.keycloak.storage.UserStorageProvider&name=freeipa-ldap" \ | jq -r '.[0].id') fi log "LDAP provider created (id=$LDAP_ID)." fi # ─── Attribute mappers ──────────────────────────────────────────────────────── log "Adding LDAP attribute mappers..." add_mapper() { local name="$1" type="$2" ldap_attr="$3" user_attr="$4" local payload payload=$(cat </dev/null log " mapper: $name" else warn " mapper '$name' already exists — skipping." fi } add_mapper "email" "user-attribute-ldap-mapper" "mail" "email" add_mapper "first-name" "user-attribute-ldap-mapper" "givenName" "firstName" add_mapper "last-name" "user-attribute-ldap-mapper" "sn" "lastName" add_mapper "uid-number" "user-attribute-ldap-mapper" "uidNumber" "uidNumber" # Group mapper (maps IPA groups to Keycloak groups) GROUP_MAPPER=$(cat </dev/null log " mapper: freeipa-groups" fi # ─── Trigger initial sync ────────────────────────────────────────────────────── log "Triggering initial user sync..." SYNC_RESULT=$(kc_post "/admin/realms/$KC_REALM/user-storage/$LDAP_ID/sync?action=triggerFullSync" "" 2>/dev/null || echo "{}") ADDED=$(echo "$SYNC_RESULT" | jq -r '.added // 0') UPDATED=$(echo "$SYNC_RESULT" | jq -r '.updated // 0') log "Sync complete: $ADDED added, $UPDATED updated." # ─── Enable email login ──────────────────────────────────────────────────────── kc_put "/admin/realms/$KC_REALM" \ '{"loginWithEmailAllowed":true,"duplicateEmailsAllowed":false}' >/dev/null log "Email login enabled on realm '$KC_REALM'." # ─── Summary ───────────────────────────────────────────────────────────────── cat <