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

1.2. Target Architecture

bind-01  (10.50.1.90) ─── Standalone BIND DNS (authoritative)
ipa-01   (10.50.1.100) ─── FreeIPA IdM (LDAP/Kerberos, NO integrated DNS)

1.3. Environment

Component Value

KVM Hypervisor

kvm-01 / supermicro300-9d1 (Arch Linux)

Guest OS

Rocky Linux 9 GenericCloud

FreeIPA Hostname

ipa-01.inside.domusdigitalis.dev

FreeIPA IP

10.50.1.100

Realm

INSIDE.DOMUSDIGITALIS.DEV

VM Storage

/mnt/onboard-ssd/vms/

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
Expected Output
-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
Expected Output
# 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
Expected Output
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

KVM Network Architecture
Figure 1. KVM Network Diagram

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.3. meta-data

cat > meta-data << 'EOF'
instance-id: ipa-01
local-hostname: ipa-01
EOF

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:

  1. passwd field must be indented - It must be inside the user block (same indentation as shell:)

  2. Use the SHA-512 hash, not the raw password - Must start with $6$…​

  3. Interface name is enp1s0 - Rocky cloud images do NOT use eth0

  4. Use nmcli commands - More reliable than write_files for NetworkManager

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 ~/.ssh/authorized_keys.

4.6. Create cloud-init ISO

# Install genisoimage if needed (Arch)
sudo pacman -S --noconfirm cdrtools

# Create ISO
genisoimage -output /mnt/onboard-ssd/vms/ipa-01-cloud-init.iso \
  -volid cidata -joliet -rock meta-data user-data

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.1. Check VM Status

sudo virsh list --all | grep ipa

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
Expected Output
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
Expected Output
Connection successfully activated

Verify:

ip a show eth0
Expected Output
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
Expected Output
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

7.5. SSH Access

From workstation (exit console with Ctrl+] first):

ssh ipa-01

Or directly:

ssh evanusmodestus@10.50.1.100

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
Expected Output
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
Expected Output
 Static hostname: ipa-01.inside.domusdigitalis.dev
       Icon name: computer-vm
         Chassis: vm
# Verify DNS resolution works
dig ipa-01.inside.domusdigitalis.dev +short
Expected Output
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
Expected Output
{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:

  • --no-ntp - Don’t configure NTP (use existing infrastructure)

  • --no-host-dns - Don’t check DNS before install (we handle DNS separately)

  • No --setup-dns - DNS handled by standalone BIND server (bind-01)

Expected Output (installation takes ~5-10 minutes)
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
Expected Output
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"
Prompts
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
Prompt
Password for admin@INSIDE.DOMUSDIGITALIS.DEV:

(paste admin password from gopass)

10.2. Verify Ticket

klist
Expected Output
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
Expected Output
--------------
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
Expected Output
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

ipa-dnskeysyncd is STOPPED because we’re not using integrated DNS (expected).

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"
Expected Output
---------------------
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
Prompt
Password:
Enter Password again to verify:

(paste password from gopass)

Expected Output
----------------------
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
Expected Output
  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
Expected Output
  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
Expected Response
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}'
Expected Output
{
  "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[]'
Expected Groups
printers
ipausers
admins

12.6. Future: netapi Integration

Add to netapi roadmap:

# Planned commands
netapi ise create-ldap-source --name FreeIPA --host ipa-01.inside.domusdigitalis.dev
netapi ise get-ldap-sources
netapi ise test-ldap-source FreeIPA
netapi ise get-ldap-groups FreeIPA

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

13.2. Network Not Configured

# Check if cloud-init wrote the connection
sudo cat /etc/NetworkManager/system-connections/eth0.nmconnection

# Manually apply
sudo nmcli connection reload
sudo nmcli connection up eth0

13.3. Cannot SSH

# Use virsh console
sudo virsh console ipa-01

# Check IP
ip addr show eth0

# Check SSH service
sudo systemctl status sshd

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

10.50.1.100

10.50.1.101

Hypervisor

kvm-01

kvm-02

Role

Primary

Replica

14.2. Prerequisites

Complete before starting:

  • ipa-01 operational with CA backup in dsec

  • kvm-02 NFS access to NAS (Phase 0 complete)

  • DNS A record for ipa-02 in bind-01

  • Reverse PTR record for 10.50.1.101

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.
Expected completion (5-10 minutes)
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
Expected Output
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

14.10.2. Replication Lag

# Check replication status
ipa-replica-manage list-ruv

# Force replication resync
ipa-replica-manage re-initialize --from=ipa-01.inside.domusdigitalis.dev

14.10.3. DNS Resolution Fails

# Ensure /etc/resolv.conf points to bind-01
cat /etc/resolv.conf
# Should show: nameserver 10.50.1.90

# If using NetworkManager, check connection profile
nmcli -f IP4.DNS connection show eth0

15. Keycloak HA (Future)

Keycloak HA requires PostgreSQL clustering or external database. Current deployment is single instance.

Options for Keycloak HA:

  1. External PostgreSQL cluster - Deploy PostgreSQL HA (Patroni/repmgr) and point both Keycloak instances to it

  2. Keycloak Infinispan clustering - Built-in session replication between Keycloak nodes

  3. Database on shared NAS - Single PostgreSQL with NFS storage (not true HA)