Skip to content

Commit f01ff91

Browse files
committed
feat(lab-04): FreeIPA SSO — Keycloak LDAP federation, user sync, OIDC discovery
1 parent 2cbbb38 commit f01ff91

3 files changed

Lines changed: 230 additions & 74 deletions

File tree

.github/workflows/ci.yml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,29 @@ jobs:
154154
- name: ShellCheck test script
155155
run: |
156156
sudo apt-get install -y shellcheck -qq
157-
shellcheck tests/labs/test-lab-01-03.sh
157+
shellcheck tests/labs/test-lab-01-03.sh
158+
159+
lab-04-smoke:
160+
name: Lab 04 — Keycloak LDAP Federation (syntax check only)
161+
runs-on: ubuntu-latest
162+
needs: validate
163+
continue-on-error: true
164+
steps:
165+
- uses: actions/checkout@v4
166+
167+
- name: Validate SSO compose (pull images only — FreeIPA is privileged)
168+
run: |
169+
echo "NOTE: FreeIPA Lab 04 requires privileged + full IPA init (~5min); runs on real VMs"
170+
docker compose -f docker/docker-compose.sso.yml pull --quiet
171+
echo "Images pulled OK"
172+
173+
- name: Validate compose config
174+
run: docker compose -f docker/docker-compose.sso.yml config -q
175+
176+
- name: Verify test script syntax
177+
run: bash -n tests/labs/test-lab-01-04.sh
178+
179+
- name: ShellCheck test script
180+
run: |
181+
sudo apt-get install -y shellcheck -qq
182+
shellcheck tests/labs/test-lab-01-04.sh

docker/docker-compose.sso.yml

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,106 @@
1-
# Lab 04 — SSO Integration: freeipa with Keycloak OIDC authentication
2-
---
31
services:
2+
3+
# ── FreeIPA identity directory ────────────────────────────────────
44
freeipa:
5-
image: freeipa/freeipa-server:rocky-9
6-
container_name: it-stack-freeipa
7-
restart: unless-stopped
5+
image: freeipa/freeipa-server:fedora-41
6+
container_name: freeipa-lab04
7+
hostname: ipa.lab.local
8+
privileged: true
9+
tty: true
10+
stdin_open: true
11+
environment:
12+
IPA_SERVER_IP: 172.20.0.10
13+
IPA_SERVER_HOSTNAME: ipa.lab.local
14+
command:
15+
- ipa-server-install
16+
- --unattended
17+
- --realm=LAB.LOCAL
18+
- --domain=lab.local
19+
- --ds-password=Lab04Password!
20+
- --admin-password=Lab04Password!
21+
- --no-ntp
22+
- --no-host-dns
23+
- --setup-dns
24+
- --auto-forwarders
25+
volumes:
26+
- freeipa-sso:/data
27+
- /sys/fs/cgroup:/sys/fs/cgroup:ro
28+
tmpfs:
29+
- /run
30+
- /tmp
831
ports:
9-
- "389:$firstPort"
32+
- "389:389"
33+
- "636:636"
34+
- "88:88/tcp"
35+
- "88:88/udp"
36+
networks:
37+
ipa-sso-net:
38+
ipv4_address: 172.20.0.10
39+
sysctls:
40+
- net.ipv6.conf.all.disable_ipv6=0
41+
healthcheck:
42+
test: ["CMD", "ipa", "user-find", "--all"]
43+
interval: 30s
44+
timeout: 10s
45+
retries: 20
46+
start_period: 240s
47+
48+
# ── Keycloak backing DB (internal) ────────────────────────────────
49+
kc-db:
50+
image: postgres:16-alpine
1051
environment:
11-
- IT_STACK_ENV=lab-04-sso
12-
- KEYCLOAK_URL=
13-
- KEYCLOAK_REALM=
14-
- KEYCLOAK_CLIENT_ID=freeipa
15-
- KEYCLOAK_CLIENT_SECRET=
52+
POSTGRES_DB: keycloak
53+
POSTGRES_USER: kcadmin
54+
POSTGRES_PASSWORD: Lab04Password!
1655
networks:
17-
- it-stack-net
56+
- kc-db-net
57+
volumes:
58+
- kc-db-sso:/var/lib/postgresql/data
59+
healthcheck:
60+
test: ["CMD-SHELL", "pg_isready -U kcadmin -d keycloak"]
61+
interval: 5s
62+
timeout: 3s
63+
retries: 20
1864

19-
# Local Keycloak for SSO lab (replace with lab-id1 in real env)
65+
# ── Keycloak (will be configured to federate FreeIPA LDAP) ────────
2066
keycloak:
21-
image: quay.io/keycloak/keycloak:24
22-
container_name: it-stack-freeipa-keycloak
67+
image: quay.io/keycloak/keycloak:24.0
2368
command: start-dev
69+
depends_on:
70+
kc-db:
71+
condition: service_healthy
2472
environment:
25-
KEYCLOAK_ADMIN: admin
26-
KEYCLOAK_ADMIN_PASSWORD: admin
73+
KC_BOOTSTRAP_ADMIN_USERNAME: admin
74+
KC_BOOTSTRAP_ADMIN_PASSWORD: Lab04Password!
75+
KC_DB: postgres
76+
KC_DB_URL: "jdbc:postgresql://kc-db:5432/keycloak"
77+
KC_DB_USERNAME: kcadmin
78+
KC_DB_PASSWORD: Lab04Password!
79+
KC_HTTP_PORT: "8080"
80+
KC_HOSTNAME_STRICT: "false"
81+
KC_PROXY: edge
2782
ports:
2883
- "8080:8080"
2984
networks:
30-
- it-stack-net
85+
- ipa-sso-net
86+
- kc-db-net
87+
healthcheck:
88+
test: ["CMD-SHELL", "curl -sf http://localhost:8080/health/ready || exit 1"]
89+
interval: 10s
90+
timeout: 5s
91+
retries: 30
92+
start_period: 60s
3193

3294
networks:
33-
it-stack-net:
95+
ipa-sso-net:
96+
driver: bridge
97+
ipam:
98+
config:
99+
- subnet: 172.20.0.0/24
100+
kc-db-net:
34101
driver: bridge
102+
internal: true
103+
104+
volumes:
105+
freeipa-sso:
106+
kc-db-sso:

tests/labs/test-lab-01-04.sh

Lines changed: 113 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,130 @@
11
#!/usr/bin/env bash
2-
# test-lab-01-04.sh — Lab 01-04: SSO Integration
3-
# Module 01: FreeIPA LDAP/Kerberos identity provider
4-
# freeipa with Keycloak OIDC/SAML authentication
2+
# test-lab-01-04.sh — FreeIPA Lab 04: Keycloak LDAP Federation with FreeIPA
3+
# Tests: FreeIPA LDAP reachable, Keycloak federation component, user sync,
4+
# OIDC discovery, FreeIPA user visible in Keycloak after sync
5+
# NOTE: Requires privileged container — full test runs on real VMs.
6+
# CI runs syntax check + ShellCheck only.
57
set -euo pipefail
68

7-
LAB_ID="01-04"
8-
LAB_NAME="SSO Integration"
9-
MODULE="freeipa"
10-
COMPOSE_FILE="docker/docker-compose.sso.yml"
11-
PASS=0
12-
FAIL=0
9+
PASS=0; FAIL=0
10+
KC_PASS="${KC_PASS:-Lab04Password!}"
11+
KC_URL="http://localhost:8080"
12+
IPA_HOST="localhost"
13+
REALM="it-stack"
1314

14-
# ── Colors ────────────────────────────────────────────────────────────────────
15-
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
16-
CYAN='\033[0;36m'; NC='\033[0m'
15+
pass() { ((++PASS)); echo " [PASS] $1"; }
16+
fail() { ((++FAIL)); echo " [FAIL] $1"; }
17+
warn() { echo " [WARN] $1"; }
18+
header(){ echo; echo "=== $1 ==="; }
1719

18-
pass() { echo -e "${GREEN}[PASS]${NC} $1"; ((PASS++)); }
19-
fail() { echo -e "${RED}[FAIL]${NC} $1"; ((FAIL++)); }
20-
info() { echo -e "${CYAN}[INFO]${NC} $1"; }
21-
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
20+
kc_token() {
21+
curl -sf -X POST "$KC_URL/realms/master/protocol/openid-connect/token" \
22+
-d "client_id=admin-cli&grant_type=password&username=admin&password=${KC_PASS}" \
23+
| grep -o '"access_token":"[^"]*"' | cut -d'"' -f4
24+
}
2225

23-
echo -e "${CYAN}======================================${NC}"
24-
echo -e "${CYAN} Lab ${LAB_ID}: ${LAB_NAME}${NC}"
25-
echo -e "${CYAN} Module: ${MODULE}${NC}"
26-
echo -e "${CYAN}======================================${NC}"
27-
echo ""
26+
header "1. FreeIPA LDAP Port Reachable"
27+
if nc -z -w 5 "$IPA_HOST" 389 2>/dev/null; then
28+
pass "FreeIPA LDAP port 389 reachable"
29+
else
30+
fail "FreeIPA LDAP port 389 not reachable"
31+
fi
2832

29-
# ── PHASE 1: Setup ────────────────────────────────────────────────────────────
30-
info "Phase 1: Setup"
31-
docker compose -f "${COMPOSE_FILE}" up -d
32-
info "Waiting 30s for ${MODULE} to initialize..."
33-
sleep 30
33+
header "2. FreeIPA Anonymous LDAP Bind"
34+
if ldapsearch -x -H "ldap://$IPA_HOST:389" -b "dc=lab,dc=local" "(objectClass=organizationalUnit)" dn 2>/dev/null | grep -q "numEntries"; then
35+
pass "Anonymous LDAP query returns OUs"
36+
else
37+
warn "Anonymous bind may be disabled (normal for hardened IPA)"
38+
fi
3439

35-
# ── PHASE 2: Health Checks ────────────────────────────────────────────────────
36-
info "Phase 2: Health Checks"
40+
header "3. FreeIPA Admin LDAP Bind"
41+
if ldapwhoami -x -H "ldap://$IPA_HOST:389" \
42+
-D "uid=admin,cn=users,cn=accounts,dc=lab,dc=local" \
43+
-w "$KC_PASS" 2>/dev/null | grep -q "dn:"; then
44+
pass "Admin LDAP bind successful"
45+
else
46+
warn "Admin LDAP bind failed (FreeIPA may not be fully initialised)"
47+
fi
3748

38-
if docker compose -f "${COMPOSE_FILE}" ps | grep -q "running\|Up"; then
39-
pass "Container is running"
49+
header "4. Users OU Exists in FreeIPA"
50+
USERS_OU=$(ldapsearch -x -H "ldap://$IPA_HOST:389" \
51+
-D "uid=admin,cn=users,cn=accounts,dc=lab,dc=local" -w "$KC_PASS" \
52+
-b "cn=users,cn=accounts,dc=lab,dc=local" "(objectClass=person)" uid 2>/dev/null | grep "uid:" | head -5)
53+
[[ -n "$USERS_OU" ]] && pass "FreeIPA users found in cn=users,cn=accounts" || fail "Users OU empty or unreachable"
54+
55+
header "5. Keycloak Health"
56+
if curl -sf "$KC_URL/health/ready" | grep -q '"status":"UP"'; then
57+
pass "Keycloak /health/ready UP"
4058
else
41-
fail "Container is not running"
59+
fail "Keycloak not ready"; exit 1
4260
fi
4361

44-
# ── PHASE 3: Functional Tests ─────────────────────────────────────────────────
45-
info "Phase 3: Functional Tests (Lab 04 — SSO Integration)"
62+
header "6. Keycloak Admin Auth + Realm Setup"
63+
TOKEN=$(kc_token)
64+
[[ -n "$TOKEN" ]] && pass "Admin token obtained" || { fail "Admin auth failed"; exit 1; }
65+
curl -sf -X POST "$KC_URL/admin/realms" \
66+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
67+
-d "{\"realm\":\"$REALM\",\"enabled\":true}" -o /dev/null \
68+
&& pass "Realm '$REALM' created/exists" || warn "Realm may already exist"
4669

47-
# TODO: Add module-specific functional tests here
48-
# Example:
49-
# if curl -sf http://localhost:389/health > /dev/null 2>&1; then
50-
# pass "Health endpoint responds"
51-
# else
52-
# fail "Health endpoint not reachable"
53-
# fi
70+
header "7. Create LDAP Federation Component (Keycloak → FreeIPA)"
71+
TOKEN=$(kc_token)
72+
LDAP_BODY="{
73+
\"name\": \"freeipa-ldap\",
74+
\"providerId\": \"ldap\",
75+
\"providerType\": \"org.keycloak.storage.UserStorageProvider\",
76+
\"config\": {
77+
\"vendor\": [\"rhds\"],
78+
\"connectionUrl\": [\"ldap://freeipa:389\"],
79+
\"bindDn\": [\"uid=admin,cn=users,cn=accounts,dc=lab,dc=local\"],
80+
\"bindCredential\": [\"$KC_PASS\"],
81+
\"usersDn\": [\"cn=users,cn=accounts,dc=lab,dc=local\"],
82+
\"usernameLDAPAttribute\": [\"uid\"],
83+
\"rdnLDAPAttribute\": [\"uid\"],
84+
\"uuidLDAPAttribute\": [\"ipaUniqueID\"],
85+
\"userObjectClasses\": [\"inetOrgPerson,organizationalPerson\"],
86+
\"editMode\": [\"READ_ONLY\"],
87+
\"syncRegistrations\": [\"false\"],
88+
\"enabled\": [\"true\"]
89+
}
90+
}"
91+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$KC_URL/admin/realms/$REALM/components" \
92+
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$LDAP_BODY")
93+
[[ "$STATUS" =~ ^(201|409)$ ]] && pass "LDAP federation component created (HTTP $STATUS)" || fail "LDAP federation failed (HTTP $STATUS)"
5494

55-
warn "Functional tests for Lab 01-04 pending implementation"
95+
header "8. Trigger User Sync from FreeIPA"
96+
TOKEN=$(kc_token)
97+
COMP_ID=$(curl -sf "$KC_URL/admin/realms/$REALM/components?type=org.keycloak.storage.UserStorageProvider" \
98+
-H "Authorization: Bearer $TOKEN" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
99+
if [[ -n "$COMP_ID" ]]; then
100+
SYNC=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
101+
"$KC_URL/admin/realms/$REALM/user-storage/$COMP_ID/sync?action=triggerFullSync" \
102+
-H "Authorization: Bearer $TOKEN")
103+
[[ "$SYNC" =~ ^(200|204)$ ]] && pass "Full sync triggered (HTTP $SYNC)" || warn "Sync returned HTTP $SYNC"
104+
else
105+
fail "LDAP federation component not found for sync"
106+
fi
56107

57-
# ── PHASE 4: Cleanup ──────────────────────────────────────────────────────────
58-
info "Phase 4: Cleanup"
59-
docker compose -f "${COMPOSE_FILE}" down -v --remove-orphans
60-
info "Cleanup complete"
108+
header "9. FreeIPA Users Visible in Keycloak"
109+
TOKEN=$(kc_token)
110+
sleep 5 # allow sync to complete
111+
USERS=$(curl -sf "$KC_URL/admin/realms/$REALM/users?max=50" -H "Authorization: Bearer $TOKEN")
112+
ADMIN_USER=$(echo "$USERS" | grep -c '"username":"admin"' || true)
113+
[[ "$ADMIN_USER" -gt 0 ]] && pass "FreeIPA admin user synced into Keycloak" || fail "FreeIPA users not synced"
114+
TOTAL=$(echo "$USERS" | grep -o '"username"' | wc -l)
115+
[[ "$TOTAL" -gt 0 ]] && pass "Keycloak has $TOTAL users after LDAP sync" || fail "No users after sync"
61116

62-
# ── Results ───────────────────────────────────────────────────────────────────
63-
echo ""
64-
echo -e "${CYAN}======================================${NC}"
65-
echo -e " Lab ${LAB_ID} Complete"
66-
echo -e " ${GREEN}PASS: ${PASS}${NC} | ${RED}FAIL: ${FAIL}${NC}"
67-
echo -e "${CYAN}======================================${NC}"
117+
header "10. OIDC Discovery for it-stack Realm"
118+
DISC=$(curl -sf "$KC_URL/realms/$REALM/.well-known/openid-configuration")
119+
echo "$DISC" | grep -q '"token_endpoint"' && pass "OIDC discovery: token_endpoint present" || fail "OIDC discovery failed"
120+
echo "$DISC" | grep -q '"authorization_endpoint"' && pass "OIDC discovery: authorization_endpoint present" || fail "Missing authorization_endpoint"
68121

69-
if [ "${FAIL}" -gt 0 ]; then
70-
exit 1
71-
fi
122+
header "11. JWKS Endpoint"
123+
JWKS_URL=$(echo "$DISC" | grep -o '"jwks_uri":"[^"]*"' | cut -d'"' -f4)
124+
curl -sf "$JWKS_URL" | grep -q '"keys"' && pass "JWKS endpoint returns signing keys" || fail "JWKS endpoint failed"
125+
126+
echo
127+
echo "═══════════════════════════════════════"
128+
echo " Lab 01-04 Results: $PASS passed, $FAIL failed"
129+
echo "═══════════════════════════════════════"
130+
[[ "$FAIL" -eq 0 ]]

0 commit comments

Comments
 (0)