273 lines
12 KiB
Bash
Executable File
273 lines
12 KiB
Bash
Executable File
#!/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_DOMAIN>
|
|
# IPA_REALM <IPA_DOMAIN uppercased>
|
|
# IPA_BIND_DN cn=Directory Manager
|
|
# IPA_BIND_PASSWORD <IPA_DM_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 <<JSON
|
|
{
|
|
"name": "freeipa-ldap",
|
|
"providerId": "ldap",
|
|
"providerType": "org.keycloak.storage.UserStorageProvider",
|
|
"config": {
|
|
"enabled": ["true"],
|
|
"priority": ["1"],
|
|
"editMode": ["READ_ONLY"],
|
|
"syncRegistrations": ["false"],
|
|
"vendor": ["rhds"],
|
|
"usernameLDAPAttribute": ["uid"],
|
|
"rdnLDAPAttribute": ["uid"],
|
|
"uuidLDAPAttribute": ["ipaUniqueID"],
|
|
"userObjectClasses": ["inetOrgPerson, organizationalPerson"],
|
|
"connectionUrl": ["$IPA_LDAP_URL"],
|
|
"usersDn": ["cn=users,cn=accounts,$IPA_BASEDN"],
|
|
"authType": ["simple"],
|
|
"bindDn": ["$IPA_BIND_DN"],
|
|
"bindCredential": ["$IPA_BIND_PASSWORD"],
|
|
"searchScope": ["1"],
|
|
"validatePasswordPolicy": ["false"],
|
|
"trustEmail": ["false"],
|
|
"useTruststoreSpi": ["ldapsOnly"],
|
|
"connectionPooling": ["true"],
|
|
"pagination": ["true"],
|
|
"batchSizeForSync": ["1000"],
|
|
"fullSyncPeriod": ["$SYNC_FULL_PERIOD"],
|
|
"changedSyncPeriod": ["$SYNC_CHANGED_PERIOD"],
|
|
"importEnabled": ["true"],
|
|
"cachePolicy": ["DEFAULT"],
|
|
"kerberosRealm": ["$IPA_REALM"],
|
|
"serverPrincipal": ["HTTP/$IPA_SERVER@$IPA_REALM"],
|
|
"useKerberosForPasswordAuthentication": ["false"],
|
|
"allowKerberosAuthentication": ["false"],
|
|
"debug": ["false"]
|
|
}
|
|
}
|
|
JSON
|
|
)
|
|
|
|
EXISTING_ID=$(kc_get "/admin/realms/$KC_REALM/components?type=org.keycloak.storage.UserStorageProvider&name=freeipa-ldap" \
|
|
| jq -r '.[0].id // empty')
|
|
|
|
if [[ -n "$EXISTING_ID" ]]; then
|
|
warn "LDAP provider already exists (id=$EXISTING_ID) — updating."
|
|
kc_put "/admin/realms/$KC_REALM/components/$EXISTING_ID" "$LDAP_COMPONENT" >/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 <<JSON
|
|
{
|
|
"name": "$name",
|
|
"providerId": "$type",
|
|
"providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
|
|
"parentId": "$LDAP_ID",
|
|
"config": {
|
|
"ldap.attribute": ["$ldap_attr"],
|
|
"user.model.attribute": ["$user_attr"],
|
|
"read.only": ["true"],
|
|
"always.read.value.from.ldap": ["false"],
|
|
"is.mandatory.in.ldap": ["false"]
|
|
}
|
|
}
|
|
JSON
|
|
)
|
|
local exists
|
|
exists=$(kc_get "/admin/realms/$KC_REALM/components?parent=$LDAP_ID&name=$name" \
|
|
| jq -r '.[0].id // empty')
|
|
if [[ -z "$exists" ]]; then
|
|
kc_post "/admin/realms/$KC_REALM/components" "$payload" >/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 <<JSON
|
|
{
|
|
"name": "freeipa-groups",
|
|
"providerId": "group-ldap-mapper",
|
|
"providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
|
|
"parentId": "$LDAP_ID",
|
|
"config": {
|
|
"groups.dn": ["cn=groups,cn=accounts,$IPA_BASEDN"],
|
|
"group.name.ldap.attribute": ["cn"],
|
|
"group.object.classes": ["groupOfNames"],
|
|
"preserve.group.inheritance": ["false"],
|
|
"membership.ldap.attribute": ["member"],
|
|
"membership.attribute.type": ["DN"],
|
|
"mode": ["READ_ONLY"],
|
|
"user.roles.retrieve.strategy": ["LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"],
|
|
"mapped.group.attributes": [""],
|
|
"drop.non.existing.groups.during.sync": ["false"]
|
|
}
|
|
}
|
|
JSON
|
|
)
|
|
exists=$(kc_get "/admin/realms/$KC_REALM/components?parent=$LDAP_ID&name=freeipa-groups" \
|
|
| jq -r '.[0].id // empty')
|
|
if [[ -z "$exists" ]]; then
|
|
kc_post "/admin/realms/$KC_REALM/components" "$GROUP_MAPPER" >/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 <<EOF
|
|
|
|
${GREEN}Keycloak ↔ FreeIPA configuration complete.${NC}
|
|
|
|
Keycloak URL: $KC_URL
|
|
Realm: $KC_REALM (display: $KC_REALM_DISPLAY)
|
|
LDAP provider: $IPA_LDAP_URL
|
|
Users DN: cn=users,cn=accounts,$IPA_BASEDN
|
|
Groups DN: cn=groups,cn=accounts,$IPA_BASEDN
|
|
Sync schedule: full=${SYNC_FULL_PERIOD}s / changed=${SYNC_CHANGED_PERIOD}s
|
|
|
|
Admin console: $KC_URL/admin/$KC_REALM/console
|
|
User login: $KC_URL/realms/$KC_REALM/account
|
|
|
|
Next steps:
|
|
• Verify users are visible: Admin console → Users
|
|
• Set up client applications (OIDC/SAML) in this realm
|
|
• For production: switch Keycloak to 'start' mode with a TLS cert
|
|
• For Kerberos/SPNEGO: supply an HTTP service keytab and set
|
|
allowKerberosAuthentication=true in the LDAP provider config
|
|
|
|
EOF
|