FreeIPA Server Deployment (Headless)
1. Overview
Deploy FreeIPA identity server on Rocky 9 using cloud images for fully headless, enterprise-pattern deployment. No GUI, no installer - pure cloud-init automation.
1.1. Architecture Decision
| Pattern | Rationale |
|---|---|
Cloud Images |
Enterprise standard (AWS/Azure/GCP pattern) |
cloud-init |
Repeatable, infrastructure-as-code configuration |
Headless |
No GUI overhead, SSH-only management |
Standalone DNS |
FreeIPA for LDAP/Kerberos only, BIND on separate VM |
2. Phase 1: Download Cloud Image
2.1. Rocky 9 GenericCloud Image
cd /var/lib/libvirt/images
sudo curl -LO https://dl.rockylinux.org/pub/rocky/9/images/x86_64/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
Verify download (~620MB):
ls -lh Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
-rw-r--r-- 1 root root 619M Feb 15 08:00 Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
2.2. Verify Checksum (Security)
|
Always verify checksums - HTTPS only proves server identity, not file integrity. If Rocky’s servers were compromised, you’d download malware over valid HTTPS. Checksums prove the file matches what Rocky published. |
Download the checksum file:
sudo curl -LO https://dl.rockylinux.org/pub/rocky/9/images/x86_64/CHECKSUM
Extract and verify (Rocky uses BSD-format checksums):
# Get expected hash from CHECKSUM file
grep "GenericCloud-Base.latest.x86_64.qcow2" CHECKSUM
# Rocky-9-GenericCloud-Base.latest.x86_64.qcow2: 648806400 bytes SHA256 (Rocky-9-GenericCloud-Base.latest.x86_64.qcow2) = 15d81d3434b298142b2fdd8fb54aef2662684db5c082cc191c3c79762ed6360c
# Verify hash matches (converts BSD → GNU format)
EXPECTED=$(grep "GenericCloud-Base.latest.x86_64.qcow2)" CHECKSUM | awk '{print $4}')
echo "$EXPECTED Rocky-9-GenericCloud-Base.latest.x86_64.qcow2" | sha256sum -c
Rocky-9-GenericCloud-Base.latest.x86_64.qcow2: OK
|
If verification fails, DO NOT USE THE IMAGE. Re-download or investigate. |
3. Phase 2: KVM Network Architecture
Network Details:
| Interface | IP Address | Purpose |
|---|---|---|
eno1 |
192.168.1.181/24 (DHCP) |
Host management (SSH to hypervisor) |
eno8np3 |
N/A (bridge member) |
10GbE uplink to physical switch |
virbr0 |
10.50.1.110/24 |
VM bridge - includes eno8np3 as member |
4. Phase 3: Create cloud-init Configuration
4.1. Create cloud-init ISO
cloud-init requires two files: meta-data and user-data
mkdir -p /tmp/ipa-01-cloud-init
cd /tmp/ipa-01-cloud-init
4.2. Generate Login Password (gopass)
Generate a password for console access (emergency use when SSH unavailable):
# On your workstation
gopass generate -s v2/DOMUS/servers/ipa-01/evanusmodestus 24
Generate the password hash for cloud-init:
# Generate SHA-512 hash for cloud-init
gopass show v2/DOMUS/servers/ipa-01/evanusmodestus | openssl passwd -6 -stdin
Copy the hash output (starts with $6$…) for use in user-data below.
4.4. Validate Interface Name (Before Deployment)
Rocky cloud images use enp1s0, not eth0. Verify by inspecting the base image:
# Mount the qcow2 and check udev rules
sudo modprobe nbd max_part=8
sudo qemu-nbd --connect=/dev/nbd0 /var/lib/libvirt/images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2
sudo mount /dev/nbd0p1 /mnt
# Check predictable network interface naming
ls /mnt/etc/udev/rules.d/
cat /mnt/usr/lib/udev/rules.d/80-net-setup-link.rules
# Cleanup
sudo umount /mnt
sudo qemu-nbd --disconnect /dev/nbd0
Or simply boot a test VM and check ip link output before configuring cloud-init.
4.5. user-data
Replace <HASH_FROM_GOPASS> with the output from the openssl passwd command above:
|
Common cloud-init mistakes:
|
cat > user-data << 'EOF'
#cloud-config
hostname: ipa-01
fqdn: ipa-01.inside.domusdigitalis.dev
manage_etc_hosts: true
users:
- name: evanusmodestus
groups: wheel
sudo: ALL=(ALL) NOPASSWD:ALL
shell: /bin/bash
lock_passwd: false
passwd: "<HASH_FROM_GOPASS>"
ssh_authorized_keys:
- sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFHfsGSAFAkqwYj6EGS9sA2MROjs28zM6LJds3gagsCkAAAACHNzaDpkMDAw evanusmodestus@d000-yubikey
- sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIEBZ+kus4aTHzQt1zNnEnGxJs+Lf56vrCdcyvqLhpp9hAAAACHNzaDpkMDAw evanusmodestus@d000-secondary
runcmd:
- nmcli con add type ethernet ifname enp1s0 con-name mgmt ipv4.addresses {ipa-ip}/24 ipv4.gateway {vyos-vip} ipv4.dns {bind-ip} ipv4.method manual
- nmcli con up mgmt
- dnf update -y
- dnf install -y freeipa-server
final_message: "Cloud-init completed. System ready for FreeIPA installation."
EOF
|
YubiKey sk-ssh-ed25519 keys are pre-configured. These match the keys in your |
5. Phase 4: Create VM Disk
Copy and resize the cloud image:
# Copy base image
sudo cp /var/lib/libvirt/images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 \
/mnt/onboard-ssd/vms/ipa-01.qcow2
# Resize to 50GB
sudo qemu-img resize /mnt/onboard-ssd/vms/ipa-01.qcow2 50G
Verify:
sudo qemu-img info /mnt/onboard-ssd/vms/ipa-01.qcow2
6. Phase 5: Create VM with virt-install
sudo virt-install \
--name ipa-01 \
--memory 4096 \
--vcpus 2 \
--disk path=/mnt/onboard-ssd/vms/ipa-01.qcow2,format=qcow2 \
--disk path=/mnt/onboard-ssd/vms/ipa-01-cloud-init.iso,device=cdrom \
--network bridge=virbr0,model=virtio \
--os-variant rocky9 \
--graphics none \
--console pty,target_type=serial \
--import \
--noautoconsole
Key flags:
-
--import- Use existing disk image (no installer) -
--graphics none- Headless, no VNC -
--console pty,target_type=serial- Serial console for debugging
7. Phase 6: Verify VM Boot
7.2. View Console Output
sudo virsh console ipa-01
Press Enter, login with evanusmodestus and password from gopass.
To exit console: Ctrl+]
7.3. Configure Network (Manual)
Cloud-init user creation works, but network config via runcmd may fail on first boot. Configure manually from console:
ip link
1: lo: <LOOPBACK,UP,LOWER_UP>...
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>...
altname enp1s0
Rocky cloud images may use eth0 (with enp1s0 as altname). Check ip link output.
|
Delete any failed cloud-init connections:
sudo nmcli con delete mgmt 2>/dev/null
sudo nmcli con delete eth0 2>/dev/null
Add static IP (use interface name from ip link):
sudo nmcli con add type ethernet ifname eth0 con-name mgmt \
ipv4.addresses 10.50.1.100/24 \
ipv4.gateway 10.50.1.1 \
ipv4.dns 10.50.1.90 \
ipv4.method manual
sudo nmcli con up mgmt
Connection successfully activated
Verify:
ip a show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP>...
inet {ipa-ip}/24 brd 10.50.1.255 scope global noprefixroute eth0
ping -c2 10.50.1.1
2 packets transmitted, 2 received, 0% packet loss
7.4. Configure SSH Keys (If cloud-init failed)
Cloud-init should add SSH keys from user-data. Verify:
cat ~/.ssh/authorized_keys
If empty or missing, add YubiKey SSH keys manually:
mkdir -p ~/.ssh && chmod 700 ~/.ssh
cat >> ~/.ssh/authorized_keys << 'EOF'
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIFHfsGSAFAkqwYj6EGS9sA2MROjs28zM6LJds3gagsCkAAAACHNzaDpkMDAw evanusmodestus@d000-yubikey
sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIEBZ+kus4aTHzQt1zNnEnGxJs+Lf56vrCdcyvqLhpp9hAAAACHNzaDpkMDAw evanusmodestus@d000-secondary
EOF
chmod 600 ~/.ssh/authorized_keys
8. Phase 7: Pre-Installation Setup
8.1. Verify DNS Resolution (from ipa-01)
ping -c2 bind-01.inside.domusdigitalis.dev
dig ipa-01.inside.domusdigitalis.dev +short
10.50.1.100
8.2. Generate Passwords (from workstation)
Generate FreeIPA passwords with metadata:
gopass generate -s v2/DOMUS/servers/ipa-01/directory-manager 32 && \
gopass insert -m v2/DOMUS/servers/ipa-01/directory-manager << 'EOF'
$(gopass show v2/DOMUS/servers/ipa-01/directory-manager | head -1)
---
hostname: ipa-01.inside.domusdigitalis.dev
purpose: FreeIPA Directory Manager (cn=Directory Manager)
created: $(date +%Y-%m-%d)
EOF
gopass generate -s v2/DOMUS/servers/ipa-01/admin 24 && \
gopass insert -m v2/DOMUS/servers/ipa-01/admin << 'EOF'
$(gopass show v2/DOMUS/servers/ipa-01/admin | head -1)
---
hostname: ipa-01.inside.domusdigitalis.dev
purpose: FreeIPA admin user (admin@INSIDE.DOMUSDIGITALIS.DEV)
created: $(date +%Y-%m-%d)
EOF
Or simpler - generate then append metadata:
gopass generate -s v2/DOMUS/servers/ipa-01/directory-manager 32
gopass generate -s v2/DOMUS/servers/ipa-01/admin 24
gopass edit v2/DOMUS/servers/ipa-01/directory-manager
Add below the password:
--- hostname: ipa-01.inside.domusdigitalis.dev purpose: FreeIPA Directory Manager (cn=Directory Manager) created: 2026-02-15
gopass edit v2/DOMUS/servers/ipa-01/admin
Add below the password:
--- hostname: ipa-01.inside.domusdigitalis.dev purpose: FreeIPA admin user (admin@INSIDE.DOMUSDIGITALIS.DEV) created: 2026-02-15
Verify passwords are stored:
gopass show v2/DOMUS/servers/ipa-01/directory-manager
gopass show v2/DOMUS/servers/ipa-01/admin
9. Phase 8: Install FreeIPA (No DNS)
SSH to ipa-01:
ssh ipa-01
9.1. Verify Prerequisites
# Check hostname is set correctly
hostnamectl
Static hostname: ipa-01.inside.domusdigitalis.dev
Icon name: computer-vm
Chassis: vm
# Verify DNS resolution works
dig ipa-01.inside.domusdigitalis.dev +short
10.50.1.100
9.2. Run FreeIPA Installer
Retrieve passwords from gopass (on workstation):
gopass show -c v2/DOMUS/servers/ipa-01/directory-manager
gopass show -c v2/DOMUS/servers/ipa-01/admin
Set passwords in variables (paste from gopass):
# Directory Manager password (LDAP bind DN: cn=Directory Manager)
# Used for: LDAP admin operations, ISE external identity source
# Copy from: gopass show -c v2/DOMUS/servers/ipa-01/directory-manager
read DM_PASS << 'EOF'
<paste directory-manager password here>
EOF
# FreeIPA admin password (Kerberos principal: admin@INSIDE.DOMUSDIGITALIS.DEV)
# Used for: ipa CLI commands, Web UI login, kinit admin
# Copy from: gopass show -c v2/DOMUS/servers/ipa-01/admin
read ADMIN_PASS << 'EOF'
<paste admin password here>
EOF
| Heredoc handles all special characters including quotes, parentheses, and symbols. |
9.3. Fix /etc/hosts (Required)
Cloud-init sets hostname to 127.0.0.1 which breaks FreeIPA. Fix before installing:
# Remove any existing ipa-01 entries (IPv4 and IPv6)
sudo sed -i '/ipa-01/d' /etc/hosts
# Add correct entry with FQDN
echo '{ipa-ip} ipa-01.{domain} ipa-01' | sudo tee -a /etc/hosts
Verify:
grep ipa-01 /etc/hosts
{ipa-ip} ipa-01.{domain} ipa-01
9.4. Run FreeIPA Installer
Run the installer without integrated DNS:
sudo ipa-server-install \
--realm=INSIDE.DOMUSDIGITALIS.DEV \
--domain=inside.domusdigitalis.dev \
--ds-password="$DM_PASS" \
--admin-password="$ADMIN_PASS" \
--hostname=ipa-01.inside.domusdigitalis.dev \
--ip-address=10.50.1.100 \
--no-ntp \
--no-host-dns \
--unattended
|
Key flags:
|
The log file for this installation can be found in /var/log/ipaserver-install.log ============================================================================== This program will set up the IPA Server. ... Setup complete Next steps: 1. You must make sure these network ports are open: TCP Ports: * 80, 443: HTTP/HTTPS * 389, 636: LDAP/LDAPS * 88, 464: kerberos UDP Ports: * 88, 464: kerberos Be sure to back up the CA certificates stored in /root/cacert.p12 These files are required to create replicas.
9.5. Backup CA Certificate to dsec
The /root/cacert.p12 contains the CA private key. Back it up to ~/.secrets immediately.
|
On ipa-01, base64 encode the p12:
sudo base64 /root/cacert.p12
On workstation, create encrypted backup:
# Create freeipa directory
mkdir -p ~/.secrets/certs/d000/freeipa
# Save p12 encrypted with age (paste base64 from ipa-01)
# Uses master.age.pub from dsec key structure
cat << 'EOF' | base64 -d | age -R ~/.secrets/.metadata/keys/master.age.pub -o ~/.secrets/certs/d000/freeipa/cacert.p12.age
<paste base64 output here>
EOF
Extract and store CA public cert (no encryption needed):
# On ipa-01 - extract CA cert from p12 (uses DM password)
# If $DM_PASS is still set:
sudo openssl pkcs12 -in /root/cacert.p12 -clcerts -nokeys -out /tmp/freeipa-ca.crt -passin pass:"$DM_PASS"
# Or paste password directly:
# sudo openssl pkcs12 -in /root/cacert.p12 -clcerts -nokeys -out /tmp/freeipa-ca.crt -passin pass:'<DM password>'
sudo cat /tmp/freeipa-ca.crt
# On workstation - save to dsec certs
cat > ~/.secrets/certs/d000/ca/FREEIPA-CA.crt << 'EOF'
<paste certificate here>
EOF
Store p12 password in gopass:
# The p12 password is the Directory Manager password
gopass show v2/DOMUS/servers/ipa-01/directory-manager
# Document this relationship
gopass edit v2/DOMUS/servers/ipa-01/directory-manager
Add to metadata:
--- hostname: ipa-01.inside.domusdigitalis.dev purpose: FreeIPA Directory Manager (cn=Directory Manager) also-unlocks: /root/cacert.p12 (CA PKCS#12) created: 2026-02-15
Verify backup:
ls -la ~/.secrets/certs/d000/freeipa/
ls -la ~/.secrets/certs/d000/ca/FREEIPA-CA.crt
9.6. Cleanup and Commit
On ipa-01, securely remove temp file:
sudo shred -vuzn 3 /tmp/freeipa-ca.crt
On workstation, commit to ~/.secrets repo:
cd ~/.secrets
git add certs/d000/ca/FREEIPA-CA.crt certs/d000/freeipa/cacert.p12.age
git status
git commit -m "feat(certs): Add FreeIPA CA cert and encrypted p12 backup"
git push
9.7. Rotate Directory Manager Password (If Needed)
If the DM password was exposed, rotate it immediately.
On workstation, generate new password:
gopass generate -s v2/DOMUS/servers/ipa-01/directory-manager 32
gopass show -c v2/DOMUS/servers/ipa-01/directory-manager
On ipa-01, change the password:
# Using dsconf (preferred)
sudo dsconf slapd-INSIDE-DOMUSDIGITALIS-DEV directory_manager password_change
Or via ldappasswd:
ldappasswd -H ldap://localhost -D "cn=Directory Manager" -W -S
# -W prompts for current (bind) password
# -S prompts for new password
9.7.1. Regenerate CA p12 with New Password
The /root/cacert.p12 was created during installation with the old password. It does NOT auto-update when you change the DM password. You must regenerate it.
|
Set the new password:
read DM_PASS << 'EOF'
<paste new DM password>
EOF
List available certs:
sudo certutil -L -d /etc/pki/pki-tomcat/alias
Get NSS Certificate DB password (required for pk12util):
sudo cat /var/lib/pki/pki-tomcat/conf/password.conf
internal=<NSS_DB_PASSWORD> replicationdb=<REPLICATION_PASSWORD>
Use the internal= value when prompted for "NSS Certificate DB" password.
|
Store NSS password in gopass (on workstation):
gopass insert -m v2/DOMUS/servers/ipa-01/nss-db << 'EOF'
<paste internal= password here>
---
hostname: ipa-01.inside.domusdigitalis.dev
purpose: NSS Certificate DB password (pk12util, certutil)
location: /var/lib/pki/pki-tomcat/conf/password.conf
created: 2026-02-15
EOF
Export new p12 with new password:
sudo pk12util -o /root/cacert-new.p12 -n "caSigningCert cert-pki-ca" -d /etc/pki/pki-tomcat/alias -W "$DM_PASS"
Enter Password or Pin for "NSS Certificate DB": <paste internal= value> pk12util: PKCS12 EXPORT SUCCESSFUL
Replace old p12:
sudo mv /root/cacert.p12 /root/cacert.p12.old
sudo mv /root/cacert-new.p12 /root/cacert.p12
sudo shred -vuzn 3 /root/cacert.p12.old
Now go back to "Backup CA Certificate to dsec" section and complete the backup with the new p12.
10. Phase 9: Verify FreeIPA Installation
10.1. Get Kerberos Ticket
kinit admin
Password for admin@INSIDE.DOMUSDIGITALIS.DEV:
(paste admin password from gopass)
10.2. Verify Ticket
klist
Ticket cache: KCM:0 Default principal: admin@INSIDE.DOMUSDIGITALIS.DEV Valid starting Expires Service principal 02/15/2026 08:30:00 02/16/2026 08:30:00 krbtgt/INSIDE.DOMUSDIGITALIS.DEV@INSIDE.DOMUSDIGITALIS.DEV
10.3. Test IPA Commands
ipa user-find admin
-------------- 1 user matched -------------- User login: admin Last name: Administrator Home directory: /home/admin Login shell: /bin/bash Principal alias: admin@INSIDE.DOMUSDIGITALIS.DEV UID: 988200000 GID: 988200000 Account disabled: False ---------------------------- Number of entries returned 1 ----------------------------
10.4. Check Services
sudo ipactl status
Directory Service: RUNNING krb5kdc Service: RUNNING kadmin Service: RUNNING httpd Service: RUNNING ipa-custodia Service: RUNNING pki-tomcatd Service: RUNNING ipa-otpd Service: RUNNING ipa-dnskeysyncd Service: STOPPED (not configured) ipa: INFO: The ipactl command was successful
|
|
10.5. Web UI (Optional)
If you need temporary web access for troubleshooting:
# SSH tunnel from workstation
ssh -L 8443:localhost:443 ipa-01
Then browse to: localhost:8443
Login with: admin / (admin password from gopass)
11. Phase 10: Create Printer Service Account
This creates a service account for the Brother MFC-L2750DW printer to authenticate via EAP-TTLS.
11.1. Create Printers Group
ipa group-add printers --desc="Network Printers - EAP-TTLS Authentication"
--------------------- Added group "printers" --------------------- Group name: printers Description: Network Printers - EAP-TTLS Authentication GID: 988200001
11.2. Generate Printer Password
On workstation:
gopass generate -s v2/DOMUS/printers/brother-mfc-01/ipa-password 24
11.3. Create Printer User Account
ipa user-add brother-mfc \
--first=Brother \
--last=MFC-L2750DW \
--shell=/sbin/nologin \
--password
Password: Enter Password again to verify:
(paste password from gopass)
---------------------- Added user "brother-mfc" ---------------------- User login: brother-mfc First name: Brother Last name: MFC-L2750DW Full name: Brother MFC-L2750DW Display name: Brother MFC-L2750DW Initials: BM Home directory: /home/brother-mfc GECOS: Brother MFC-L2750DW Login shell: /sbin/nologin Principal name: brother-mfc@INSIDE.DOMUSDIGITALIS.DEV Principal alias: brother-mfc@INSIDE.DOMUSDIGITALIS.DEV Email address: brother-mfc@inside.domusdigitalis.dev UID: 988200001 GID: 988200001 Password: True Member of groups: ipausers Kerberos keys available: True
11.4. Add to Printers Group
ipa group-add-member printers --users=brother-mfc
Group name: printers Description: Network Printers - EAP-TTLS Authentication GID: 988200001 Member users: brother-mfc ------------------------- Number of members added 1 -------------------------
11.5. Verify Account
ipa user-show brother-mfc
User login: brother-mfc First name: Brother Last name: MFC-L2750DW Home directory: /home/brother-mfc Login shell: /sbin/nologin Principal name: brother-mfc@INSIDE.DOMUSDIGITALIS.DEV Principal alias: brother-mfc@INSIDE.DOMUSDIGITALIS.DEV Email address: brother-mfc@inside.domusdigitalis.dev UID: 988200001 GID: 988200001 Account disabled: False Password: True Member of groups: ipausers, printers Kerberos keys available: True
12. Phase 11: Configure ISE LDAP Integration
Connect FreeIPA as an external LDAP identity source in Cisco ISE for EAP-TTLS authentication.
12.1. LDAP Configuration Values
| Field | Value |
|---|---|
Name |
FreeIPA |
Description |
FreeIPA LDAP for printer authentication |
Primary Server |
ipa-01.inside.domusdigitalis.dev |
Port |
389 (LDAP) or 636 (LDAPS) |
Admin DN |
uid=admin,cn=users,cn=accounts,dc=inside,dc=domusdigitalis,dc=dev |
Password |
(admin password from gopass) |
User Object Class |
person |
User Name Attribute |
uid |
Subject Name Attribute |
uid |
Group Object Class |
groupOfNames |
Group Name Attribute |
cn |
User Search Base |
cn=users,cn=accounts,dc=inside,dc=domusdigitalis,dc=dev |
Group Search Base |
cn=groups,cn=accounts,dc=inside,dc=domusdigitalis,dc=dev |
12.2. Create LDAP Identity Source via API
Set credentials (on workstation):
# Source ISE credentials from dsec
dsource d000 dev/network
# Verify variables are set
echo "ISE_PAN_IP=${ISE_PAN_IP}, ISE_API_USER=${ISE_API_USER}"
Get FreeIPA admin password:
FREEIPA_ADMIN_PASS=$(gopass show v2/DOMUS/servers/ipa-01/admin | head -1)
Create LDAP identity source XML file:
|
ISE ERS API for LDAP requires XML format (JSON returns schema validation errors). The XML schema was reverse-engineered through iterative API calls - ISE does not document this format. |
cat > /tmp/freeipa-ldap.xml << EOF
<ns4:ersLdap xmlns:ns4="identitystores.ers.ise.cisco.com" name="FreeIPA" description="FreeIPA LDAP for printer EAP-TTLS">
<connectionSettings>
<primaryServer>
<adminDN>uid=admin,cn=users,cn=accounts,dc=inside,dc=domusdigitalis,dc=dev</adminDN>
<adminPassword>${FREEIPA_ADMIN_PASS}</adminPassword>
<enableSecureConnection>false</enableSecureConnection>
<hostName>ipa-01.inside.domusdigitalis.dev</hostName>
<port>389</port>
<serverTimeout>10</serverTimeout>
<useAdminAccess>true</useAdminAccess>
</primaryServer>
</connectionSettings>
<directoryOrganization>
<groupDirectorySubtree>cn=groups,cn=accounts,dc=inside,dc=domusdigitalis,dc=dev</groupDirectorySubtree>
<userDirectorySubtree>cn=users,cn=accounts,dc=inside,dc=domusdigitalis,dc=dev</userDirectorySubtree>
</directoryOrganization>
<enablePasswordChangeLDAP>false</enablePasswordChangeLDAP>
<generalSettings>
<groupMapAttributeName>memberOf</groupMapAttributeName>
<groupNameAttribute>cn</groupNameAttribute>
<groupObjectClass>groupOfNames</groupObjectClass>
<schema>CUSTOM</schema>
<userNameAttribute>uid</userNameAttribute>
<userObjectClass>person</userObjectClass>
</generalSettings>
</ns4:ersLdap>
EOF
Submit to ISE:
curl -k -X POST "https://${ISE_PAN_IP}:9060/ers/config/ldap" \
-H "Content-Type: application/xml" \
-H "Accept: application/xml" \
-u "${ISE_API_USER}:${ISE_API_PASS}" \
-d @/tmp/freeipa-ldap.xml \
-w "\nHTTP_STATUS: %{http_code}\n"
Cleanup (contains password):
shred -vuzn 3 /tmp/freeipa-ldap.xml
HTTP/1.1 201 Created Location: https://ise-02:9060/ers/config/ldap/<uuid>
12.3. Verify LDAP Source Created
curl -k -s "https://${ISE_PAN_IP}:9060/ers/config/ldap" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" | jq '.SearchResult.resources[] | {name, id}'
{
"name": "FreeIPA",
"id": "<uuid>"
}
12.4. Test LDAP Connection
# Get the LDAP source ID
LDAP_ID=$(curl -k -s "https://${ISE_PAN_IP}:9060/ers/config/ldap" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" | jq -r '.SearchResult.resources[] | select(.name=="FreeIPA") | .id')
# Test connection
curl -k -X PUT "https://${ISE_PAN_IP}:9060/ers/config/ldap/${LDAP_ID}/test" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}"
12.5. Retrieve Groups from FreeIPA
curl -k -s "https://${ISE_PAN_IP}:9060/ers/config/ldap/${LDAP_ID}/groups" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" | jq '.groups[]'
printers ipausers admins
12.6. Future: netapi Integration
|
Add to netapi roadmap:
|
12.7. Create ISE Identity Source Sequence
curl -k -X POST "https://${ISE_PAN_IP}:9060/ers/config/idstoresequence" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" \
-d '{
"IdStoreSequence": {
"name": "FreeIPA_Sequence",
"description": "FreeIPA for printer authentication",
"idStoreList": ["FreeIPA"],
"parent": "All_User_ID_Stores"
}
}'
12.8. Create Printer AuthZ Profile
curl -k -X POST "https://${ISE_PAN_IP}:9060/ers/config/authorizationprofile" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" \
-d '{
"AuthorizationProfile": {
"name": "AuthZ_DOMUS_Printers",
"description": "Authorization profile for FreeIPA-authenticated printers",
"accessType": "ACCESS_ACCEPT",
"vlan": {
"nameID": "VLAN_PRINTERS",
"tagID": 60
},
"daclName": "DACL_CORP_PRINTERS"
}
}'
12.9. Create Printer DACL
curl -k -X POST "https://${ISE_PAN_IP}:9060/ers/config/downloadableacl" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" \
-d '{
"DownloadableAcl": {
"name": "DACL_CORP_PRINTERS",
"description": "DACL for authenticated printers",
"dacl": "permit tcp any any eq 9100\npermit tcp any any eq 515\npermit tcp any any eq 631\npermit udp any any eq 161\npermit icmp any any\ndeny ip any any log",
"daclType": "IPV4"
}
}'
12.10. Get Policy Set ID
# Get the Wired 802.1X policy set ID
POLICY_SET_ID=$(curl -k -s "https://${ISE_PAN_IP}:443/api/v1/policy/network-access/policy-set" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" | jq -r '.response[] | select(.name | contains("Wired")) | .id')
echo "Policy Set ID: $POLICY_SET_ID"
12.11. Add Authentication Rule
curl -k -X POST "https://${ISE_PAN_IP}:443/api/v1/policy/network-access/policy-set/${POLICY_SET_ID}/authentication" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" \
-d '{
"rule": {
"name": "Printers_EAP_TTLS",
"rank": 1,
"state": "enabled",
"condition": {
"conditionType": "ConditionAndBlock",
"isNegate": false,
"children": [
{
"conditionType": "ConditionAttributes",
"isNegate": false,
"dictionaryName": "Network Access",
"attributeName": "EapAuthentication",
"operator": "equals",
"attributeValue": "EAP-TTLS"
}
]
},
"identitySourceName": "FreeIPA_Sequence"
}
}'
12.12. Add Authorization Rule
curl -k -X POST "https://${ISE_PAN_IP}:443/api/v1/policy/network-access/policy-set/${POLICY_SET_ID}/authorization" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" \
-d '{
"rule": {
"name": "Printers_FreeIPA_AuthZ",
"rank": 1,
"state": "enabled",
"condition": {
"conditionType": "ConditionAndBlock",
"isNegate": false,
"children": [
{
"conditionType": "ConditionAttributes",
"isNegate": false,
"dictionaryName": "FreeIPA",
"attributeName": "ExternalGroups",
"operator": "contains",
"attributeValue": "printers"
}
]
},
"profile": ["AuthZ_DOMUS_Printers"]
}
}'
12.13. Verify Policies Created
# List auth rules
curl -k -s "https://${ISE_PAN_IP}:443/api/v1/policy/network-access/policy-set/${POLICY_SET_ID}/authentication" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" | jq '.response[] | {name, state}'
# List authz rules
curl -k -s "https://${ISE_PAN_IP}:443/api/v1/policy/network-access/policy-set/${POLICY_SET_ID}/authorization" \
-H "Accept: application/json" \
-u "${ISE_API_USER}:${ISE_API_PASS}" | jq '.response[] | {name, state}'
13. Troubleshooting
13.1. cloud-init Not Running
# Check cloud-init status
sudo cloud-init status
# View logs
sudo cat /var/log/cloud-init-output.log
14. Phase 10: Deploy ipa-02 Replica (HA)
FreeIPA supports multi-master replication for high availability. ipa-02 will be deployed on kvm-02 as a replica.
14.1. Architecture
kvm-01 kvm-02
┌──────────────┐ ┌──────────────┐
│ ipa-01 │ ◄─────► │ ipa-02 │
│ 10.50.1.100 │ Multi │ 10.50.1.101 │
│ PRIMARY │ Master │ REPLICA │
│ (original) │ Repl │ (new) │
└──────────────┘ └──────────────┘
│ │
└────────────────────────┘
389/636 LDAP + 88/464 Kerberos
| Property | ipa-01 | ipa-02 |
|---|---|---|
IP |
|
|
Hypervisor |
kvm-01 |
kvm-02 |
Role |
Primary |
Replica |
14.2. Prerequisites
|
Complete before starting:
|
14.3. 10.1 Create DNS Records
On workstation, add A and PTR records to bind-01:
# Load Vault SSH credentials
ds d000 dev/vault && vault-ssh-sign
# Add forward record
ssh bind-01 "sudo tee -a /var/named/inside.domusdigitalis.dev.zone.local << 'EOF'
ipa-02 IN A 10.50.1.101
EOF"
# Add reverse record (assuming 10.50.1.x → 1.50.10.in-addr.arpa)
ssh bind-01 "sudo tee -a /var/named/1.50.10.in-addr.arpa.local << 'EOF'
101 IN PTR ipa-02.inside.domusdigitalis.dev.
EOF"
# Reload BIND
ssh bind-01 "sudo rndc reload"
Verify DNS:
dig +short ipa-02.inside.domusdigitalis.dev
# Expected: 10.50.1.101
dig +short -x 10.50.1.101
# Expected: ipa-02.inside.domusdigitalis.dev.
14.4. 10.2 Create ipa-02 VM on kvm-02
SSH to kvm-02 and create the VM using the same cloud-init pattern as ipa-01.
ssh kvm-02
Create cloud-init configuration:
cd /var/lib/libvirt/cloud-init
cat > ipa-02-meta-data << 'EOF'
instance-id: ipa-02
local-hostname: ipa-02
EOF
cat > ipa-02-user-data << 'EOF'
#cloud-config
hostname: ipa-02
fqdn: ipa-02.inside.domusdigitalis.dev
manage_etc_hosts: true
users:
- name: evanusmodestus
sudo: ALL=(ALL) NOPASSWD:ALL
groups: wheel
lock_passwd: false
plain_text_passwd: changeme
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICFL6Ub+y8D35TH/iM8e9wQy/rNYk0NB8I1h4MmJTrMX # Your key
write_files:
- path: /etc/NetworkManager/system-connections/eth0.nmconnection
permissions: '0600'
content: |
[connection]
id=eth0
type=ethernet
interface-name=eth0
autoconnect=true
[ipv4]
method=manual
addresses=10.50.1.101/24
gateway=10.50.1.1
dns=10.50.1.90
[ipv6]
method=disabled
runcmd:
- nmcli connection reload
- nmcli connection up eth0
- dnf update -y
- dnf install -y freeipa-client freeipa-server
- systemctl enable --now cockpit.socket
chpasswd:
expire: false
ssh_pwauth: true
EOF
Generate cloud-init ISO:
genisoimage -output ipa-02-cloud-init.iso -volid cidata -joliet -rock ipa-02-user-data ipa-02-meta-data
Create VM disk and install:
cd /mnt/nas/vms
# Create disk from base image (assumes Rocky 9 GenericCloud exists)
sudo cp Rocky-9-GenericCloud-Base.latest.x86_64.qcow2 ipa-02.qcow2
sudo qemu-img resize ipa-02.qcow2 40G
# Create VM
sudo virt-install \
--name ipa-02 \
--vcpus 2 \
--memory 4096 \
--disk /mnt/nas/vms/ipa-02.qcow2,bus=virtio \
--disk /var/lib/libvirt/cloud-init/ipa-02-cloud-init.iso,device=cdrom \
--os-variant rocky9 \
--network bridge=br-mgmt,model=virtio \
--graphics none \
--import \
--noautoconsole
Wait for VM to boot and verify:
# From workstation
ping -c3 10.50.1.101
ssh evanusmodestus@10.50.1.101
14.5. 10.3 Get Admin Credentials
On workstation, retrieve admin password from gopass:
gopass show v2/DOMUS/servers/ipa-01/admin
14.6. 10.4 Install FreeIPA Replica
SSH to ipa-02 and run replica installation:
ssh ipa-02
# Run replica install (prompts for principal password)
sudo ipa-replica-install \
--principal admin \
--admin-password '<admin password from gopass>' \
--domain inside.domusdigitalis.dev \
--server ipa-01.inside.domusdigitalis.dev \
--realm INSIDE.DOMUSDIGITALIS.DEV \
--setup-ca \
--no-ntp \
--unattended
The --setup-ca flag replicates the Certificate Authority. --no-ntp is used if chronyd is managed separately.
|
Configuring client side components ... FreeIPA server installed
14.7. 10.5 Verify Replication
On ipa-02, verify replication is working:
# Get Kerberos ticket
kinit admin
# List replication agreements
ipa-replica-manage list
ipa-01.inside.domusdigitalis.dev: master ipa-02.inside.domusdigitalis.dev: master
# Check replication status
ipa-replica-manage list-ruv
# Create test user on ipa-01, verify appears on ipa-02
# On ipa-01:
ipa user-add testuser --first=Test --last=User
# On ipa-02 (should replicate within seconds):
ipa user-find testuser
# Delete test user
ipa user-del testuser
14.8. 10.6 Remove Cloud-Init CD
After successful installation:
# From kvm-02
sudo virsh change-media ipa-02 sda --eject
14.9. 10.7 Update Client DNS Configuration
Update clients to use both IPA servers for LDAP failover:
# On each LDAP client, update SSSD to include both servers:
# /etc/sssd/sssd.conf
# ipa_server = ipa-01.inside.domusdigitalis.dev, ipa-02.inside.domusdigitalis.dev
14.10. Troubleshooting ipa-02 Replica
14.10.1. Replica Install Fails: "Unable to connect"
# Verify ipa-01 is reachable from ipa-02
ping -c3 ipa-01.inside.domusdigitalis.dev
# Test LDAP
ldapsearch -x -H ldap://ipa-01.inside.domusdigitalis.dev -b "" -s base
# Check firewall on ipa-01
sudo firewall-cmd --list-all
15. Keycloak HA (Future)
Keycloak HA requires PostgreSQL clustering or external database. Current deployment is single instance.
Options for Keycloak HA:
-
External PostgreSQL cluster - Deploy PostgreSQL HA (Patroni/repmgr) and point both Keycloak instances to it
-
Keycloak Infinispan clustering - Built-in session replication between Keycloak nodes
-
Database on shared NAS - Single PostgreSQL with NFS storage (not true HA)