Demo: FreeIPA/Kerberos deployment
Location: contrib/demo/ipa/
Ansible playbooks that install a FreeIPA cluster and deploy ahdapa on every node behind IPA’s Apache httpd. This demo describes a production-representative deployment rather than a local script: it requires real infrastructure (Fedora 44 hosts with DNS) and takes 15–30 minutes to complete.
Architecture
Browser / OAuth2 client
|
| HTTPS :443
v
Apache httpd (IPA-managed)
/ipa/* → mod_wsgi (IPA API)
/ca/* → AJP → Dogtag PKI
/idp/* → mod_proxy → ahdapa (Unix socket)
|
v
ahdapa process (plain HTTP over Unix socket)
GSSAPI SASL → IPA Directory Server (slapd, ldapi://)
SPNEGO → KDC
ahdapa listens on a Unix domain socket at /run/ahdapa/ahdapa.sock. Apache
proxies all /idp/* traffic there. TLS is terminated by Apache; ahdapa needs
no [tls] section.
After installation, ahdapa is available at https://<node-fqdn>/idp/.
What the demo shows
- IPA-integrated authentication — SPNEGO single sign-on for domain members; password, OTP, and passkey login for others.
- Automatic peer discovery via IPA topology — ahdapa reads the IPA replication topology from LDAP and gossips with all directly connected replicas automatically. No static peer list is required.
- Kerberos key bootstrapping — each node seeds its ML-KEM-768 key on
peers via
POST /api/gossip/register-kemauthenticated with the node’s Kerberos machine credential. No manual key seeding is needed. - FreeIPA IdP auto-discovery — IdPs registered with
ipa idp-add …are discovered at startup and refreshed every 300 s. No[[federation.upstream_idps]]entries are needed inahdapa.toml. ipauserauthtypeenforcement — users withipauserauthtype=idpset in FreeIPA are automatically redirected to the correct upstream IdP; password, OTP, and passkey flows are blocked for those users.- SSSD
id_provider = idpsecretless deployment — IPA-enrolled machines usekerberos_client_auth(no per-machine secret) to obtain tokens and call the/api/identity/directory API for user and group lookups. - HBAC policies — Identity HBAC rules restrict which users may obtain tokens for which OAuth2 clients, enforced at the token endpoint.
Prerequisites
-
Fedora 44 on all target hosts.
-
Working forward and reverse DNS for every FQDN (IPA requires this).
-
SSH key-based access with passwordless sudo on all hosts.
-
On the Ansible control node:
pip install ansible ansible-galaxy collection install freeipa.ansible_freeipa ansible.posixOr install from the distribution package:
dnf install ansible-freeipa ansible-galaxy collection install ansible.posix -
ahdapa packages are installed from the abbra/synta COPR.
Running
# 1. Copy and edit the inventory.
cp contrib/demo/ipa/ansible/inventory.ini.example \
contrib/demo/ipa/ansible/inventory.ini
$EDITOR contrib/demo/ipa/ansible/inventory.ini
# 2. (Recommended) encrypt passwords with ansible-vault.
ansible-vault encrypt_string 'SomeAdminPass1!' --name ipa_admin_password
ansible-vault encrypt_string 'SomeDSPass1!' --name ipa_ds_password
# Paste the output into inventory.ini.
# 3. Run the full site playbook.
ansible-playbook -i contrib/demo/ipa/ansible/inventory.ini \
contrib/demo/ipa/ansible/site.yml
Individual phases
# Install and configure the IPA primary server.
ansible-playbook -i inventory.ini playbooks/ipa_server.yml
# Enroll IPA replicas (serial).
ansible-playbook -i inventory.ini playbooks/ipa_replica.yml
# Deploy ahdapa on all IPA nodes.
ansible-playbook -i inventory.ini playbooks/ahdapa.yml
# Grant IPA permissions to all ahdapa service principals.
ansible-playbook -i inventory.ini playbooks/ipa_permissions.yml
What gets installed
| Phase | Playbook | Target group | Action |
|---|---|---|---|
| 1 | ipa_server.yml | ipa_server | Runs ipa-server-install |
| 2 | ipa_replica.yml | ipa_replicas | Runs ipa-replica-install, serial |
| 3 | ahdapa.yml | ipa_nodes (all) | Installs COPR packages, drops config and service files |
| 4 | ipa_permissions.yml | ipa_server (once) | Grants required IPA privileges to all node service principals |
IPA privilege grants
ahdapa authenticates to the FreeIPA LDAP directory as its HTTP service
principal (HTTP/<fqdn>@<REALM>). Three privileges are required:
| Privilege | Permissions | Purpose |
|---|---|---|
Ahdapa Topology Read | System: Read Topology Segments | Peer discovery via IPA replication topology |
Ahdapa IdP Read | Ahdapa - Read user IdP attributes | Read ipauserauthtype, ipaidpconfiglink, ipaidpsub on user objects |
Ahdapa IdP Read | System: Read External IdP server | Read all ipaIdP entries for automatic IdP discovery |
ipa_permissions.yml creates and assigns these privileges idempotently for
all node HTTP principals.
Inventory variables
| Variable | Example | Description |
|---|---|---|
ipa_domain | ipa.example.com | IPA DNS domain |
ipa_realm | IPA.EXAMPLE.COM | Kerberos realm (usually the domain uppercased) |
ipa_admin_password | — | IPA admin account password |
ipa_ds_password | — | LDAP Directory Manager password |
ahdapa_issuer_path | /idp | URL path prefix for ahdapa behind Apache |
Additional defaults are in contrib/demo/ipa/ansible/group_vars/all.yml.
Post-installation verification
After site.yml completes:
# Verify OIDC discovery endpoint.
curl -s https://ipa1.example.com/idp/.well-known/openid-configuration \
| python3 -m json.tool | head -20
# Obtain a Kerberos ticket and test SPNEGO login.
kinit alice@IPA.EXAMPLE.COM
curl -s --negotiate -u: -c /tmp/alice.jar \
https://ipa1.example.com/idp/api/auth/info | python3 -m json.tool
# Register an OAuth2 client via Kerberos-authenticated dynamic registration.
kinit -k -t /etc/http.keytab HTTP/client.ipa.example.com@IPA.EXAMPLE.COM
curl -s -o /dev/null -w "%{http_code}\n" \
--negotiate -u: -c /tmp/session.jar \
https://ipa1.example.com/idp/authorize
# → 400 (expected; no OAuth2 params given but session cookie is set)
curl -s -b /tmp/session.jar \
-X POST -H 'Content-Type: application/json' \
-d '{
"redirect_uris": ["https://client.ipa.example.com/callback"],
"client_name": "My App",
"scope": "openid profile email"
}' \
https://ipa1.example.com/idp/register | python3 -m json.tool
HBAC demo
Restrict which users can obtain tokens for a specific OAuth2 client:
# 1. Create the demo OAuth2 client.
curl -s -b /tmp/admin.jar \
-X POST -H 'Content-Type: application/json' \
-d '{
"client_id": "demo-app",
"client_name": "Demo Application",
"redirect_uris": ["https://demo.ipa.example.com/callback"],
"scope": "openid profile email"
}' \
https://ipa1.example.com/idp/api/admin/clients | python3 -m json.tool
# 2. Create an HBAC policy: only alice may use demo-app.
curl -s -b /tmp/admin.jar \
-X POST -H 'Content-Type: application/json' \
-d '{
"name": "alice can use demo-app",
"enabled": true,
"users": ["alice"],
"clients": ["demo-app"],
"allowed_scopes": ["openid", "profile"]
}' \
https://ipa1.example.com/idp/api/admin/hbac | python3 -m json.tool
# 3. List active HBAC policies.
curl -s -b /tmp/admin.jar \
https://ipa1.example.com/idp/api/admin/hbac | python3 -m json.tool
After step 2, any user other than alice who attempts to obtain a token for
demo-app receives an access_denied error at the token endpoint. The
gossip protocol replicates the rule to all cluster nodes within one gossip
interval.
Configuration notes
The static reference configuration is at contrib/demo/ipa/ahdapa.toml.
Key points:
[server]
issuer = "https://ipa.example.com/idp"
listen = "unix:/run/ahdapa/ahdapa.sock"
[gssapi]
gssproxy = true
initiator_principal = "HTTP/ipa.example.com@EXAMPLE.COM"
[ipa]
uri = "ldapi://%2Fvar%2Frun%2Fdirsrv%2Fslapd-EXAMPLE-COM.socket"
cache_ttl_secs = 60
[gossip]
ipa_topology = true
interval_secs = 5
listen = "unix:/run/ahdapa/ahdapa.sock"— Apache proxies to this socket.gssproxy = true— uses gssproxy to obtain the HTTP service credential; no separate keytab extraction is needed.ipa_topology = true— peers are discovered from the IPA replication topology; no staticpeerslist is required.
Troubleshooting
| Symptom | Check |
|---|---|
| ahdapa not starting | journalctl -u ahdapa — often a config parse error or missing DB dir |
| 502 Bad Gateway from Apache | ls -la /run/ahdapa/ — socket must be group-accessible by apache |
| gssproxy errors | journalctl -u gssproxy — verify /etc/gssproxy/20-ahdapa.conf |
| Gossip not discovering peers | Verify the HTTP principal has “Ahdapa Topology Read” (ipa role-show "Ahdapa Services") |
| Kerberos self-registration failing | Check journalctl -u ahdapa on the peer for register-kem 403/503 errors |
| IPA IdPs not discovered at startup | Run ipa_permissions.yml — the service principal needs “Ahdapa IdP Read” |
upstream_id="ipa-unknown" in logs | Same as above — ipaidpconfiglink unreadable. Re-run ipa_permissions.yml and restart ahdapa |
| Federated user hits passkey/OTP flow | Confirm ipauserauthtype: idp on the user: ipa user-show <uid> --all |
| SELinux AVC denial for outbound HTTPS | Load the ahdapa SELinux module: semodule -i ahdapa.pp |
Federated login slow (notes=U in 389-ds) | LDAP indexes on ipaIdpConfigLink and ipaIdpSub are missing — re-run ipa_permissions.yml |
Files
| File | Description |
|---|---|
ahdapa.toml | Reference ahdapa config for IPA co-deployment |
ahdapa-gssproxy.conf | gssproxy config fragment for the HTTP service credential |
ipa-idp-proxy.conf | Apache conf.d fragment that proxies /idp/* to the Unix socket |
ansible/site.yml | Full site playbook (runs all four phases in order) |
ansible/inventory.ini.example | Inventory template |
ansible/group_vars/all.yml | Default variable values |
ansible/playbooks/ipa_server.yml | Phase 1: IPA primary server |
ansible/playbooks/ipa_replica.yml | Phase 2: IPA replicas |
ansible/playbooks/ahdapa.yml | Phase 3: ahdapa install |
ansible/playbooks/ipa_permissions.yml | Phase 4: IPA privilege grants |
ansible/templates/ahdapa.toml.j2 | Jinja2 template for the deployed ahdapa config |
ansible/templates/ahdapa-gssproxy.conf.j2 | Jinja2 template for the gssproxy config |
ansible/templates/ipa-idp-proxy.conf.j2 | Jinja2 template for the Apache proxy config |
See also
- FreeIPA Co-deployment — full IPA co-deployment guide.
- Multi-node Cluster — cluster configuration reference.
- Identity HBAC Policy — HBAC policy reference.