Dotfiles/docs/md/freeipa-ansible.md

20 KiB

FreeIPA & Ansible

The FreeIPA/Ansible system provides centralised identity management for a fleet of Arch Linux machines: single sign-on, host-group-driven package and module deployment, policy enforcement, LUKS backup key collection, scan-result aggregation, and automatic Keycloak configuration.

All relevant files live under setup/modules/FreeipaAnsible/.


Architecture

┌──────────────────────────────────────────────────────────────────────┐
│  FreeIPA + Keycloak + Samba container (docker-compose)               │
│                                                                      │
│  • FreeIPA — user/host directory, Kerberos KDC, DNS (optional)       │
│  • Keycloak — OIDC provider federating FreeIPA via LDAP              │
│  • Samba — two SMB shares for scan results and LUKS backup keys      │
│  • cronie — hourly scan-result analyser (ansipa-check-scans)         │
└───────────┬─────────────────────────┬────────────────────────────────┘
            │ SSSD / Kerberos         │ SMB (445)
            ▼                         ▼
┌──────────────────────┐    ┌─────────────────────────────────────────┐
│  Enrolled client     │    │  Ansible controller                     │
│                      │    │                                         │
│  • sssd              │    │  • deploy-ansipa-*.yml playbooks        │
│  • ipa CLI           │    │  • collect-luks-keys.yml                │
│  • Ansible-deployed  │    │    (uploads keys to ansipa-luks-keys    │
│    timers:           │    │     share as luks-upload service acct)  │
│    ├── pkg installer │    └─────────────────────────────────────────┘
│    ├── module install│
│    ├── Flatpak install
│    ├── baseuser sync │
│    └── policy enforcer (every 30 min)
│        ├── binary blocking
│        ├── daemon enable/disable
│        ├── Timeshift backups
│        ├── security scans + SMB upload
│        └── alert fetch + desktop notify
└──────────────────────┘

FreeIPA Server Container

Quick Start

cd setup/modules/FreeipaAnsible/image
cp .env.example .env
$EDITOR .env                        # set all required variables
docker compose up -d
docker compose logs -f freeipa      # watch first-boot (~10 min)
# Once freeipa is healthy:
./keycloak-configure.sh             # wire Keycloak → FreeIPA LDAP

To run FreeIPA without Keycloak:

docker compose up -d freeipa

Environment Variables (.env)

Variable Required Description
IPA_HOSTNAME yes FQDN of the IPA server (e.g. ipa.corp.example.com)
IPA_DOMAIN yes DNS domain (e.g. corp.example.com)
IPA_REALM Kerberos realm; defaults to IPA_DOMAIN uppercased
IPA_ADMIN_PASSWORD yes admin account password
IPA_DM_PASSWORD yes Directory Manager password
IPA_SETUP_DNS true to enable integrated DNS (default false)
IPA_DNS_FORWARDER Upstream DNS when IPA_SETUP_DNS=true
IPA_SETUP_KRA true to enable the Key Recovery Authority
SMB_SCAN_PASSWORD yes Password for the scanupload Samba service account
LUKS_KEY_UPLOAD_PASSWORD yes Password for the luks-upload Samba service account
KC_HOSTNAME yes Public hostname of Keycloak
KC_REALM Keycloak realm name (default corp)
KC_ADMIN Keycloak admin username (default admin)
KC_ADMIN_PASSWORD yes Keycloak admin password
KC_DB_PASSWORD yes PostgreSQL password for Keycloak
IPA_BIND_DN LDAP bind DN for Keycloak federation (default: Directory Manager)
IPA_BIND_PASSWORD LDAP bind password; leave blank to reuse IPA_DM_PASSWORD
IPA_USE_LDAPS true to use LDAPS for Keycloak federation

Exposed Ports

Port Protocol Service
389 TCP LDAP
636 TCP LDAPS
88 TCP + UDP Kerberos
464 TCP + UDP kpasswd
443 TCP HTTPS (IPA web UI)
445 TCP SMB (Samba shares)
139 TCP NetBIOS session (Samba)
137 UDP NetBIOS name service
138 UDP NetBIOS datagram
8080 TCP Keycloak HTTP
8443 TCP Keycloak HTTPS

Container Internals

On first start, ipa-first-boot.service runs ipa-first-boot.sh to initialise the FreeIPA instance. On every start, ansipa-smb.service runs ansipa-smb-setup.sh to configure Samba (the container rootfs is ephemeral — Samba config and users must be re-applied after restarts).

Data persisted to the /data volume:

/data/
├── samba/
│   ├── passdb.tdb          # Samba password database (survives restarts)
│   └── ansipa-smb.env      # Persisted SMB passwords (auto-written on first start)
├── scan-results/
│   ├── archive/<host>/     # Client scan logs (written by clients via SMB)
│   └── alerts/<host>/      # Alert files generated by ansipa-check-scans (hourly)
└── luks-keys/              # LUKS backup keys (written by Ansible controller via SMB)

SMB Shares

The container exposes two Samba shares, both configured by ansipa-smb-setup.sh.

ansipa-scans — Scan Result Archive

  • Path: /data/scan-results
  • Authenticated user: scanupload (write-only; no browse)
  • Purpose: Clients enrolled in policy-security-scan upload their daily ClamAV / rkhunter / chkrootkit logs here after each scan run.
  • Analysis: ansipa-check-scans.sh runs hourly via cronie; it reads each host's archive logs and writes *.alert files to the alerts/ subdirectory when concerning patterns are found.
  • Credentials file on clients: /etc/ansipa-smb.creds (deployed by deploy-ansipa-policies.yml)

ansipa-luks-keys — LUKS Backup Key Store

  • Path: /data/luks-keys
  • Permissions: write-only for luks-upload; read-only for members of the KeyAdmin Linux group
  • Purpose: The Ansible controller writes each host's LUKS backup key here after collecting it via collect-luks-keys.yml.
  • Access control: Add an admin Samba user to the KeyAdmin group on the container:
    # On the freeipa container
    useradd -r -G KeyAdmin <username>
    smbpasswd -a <username>
    
    The KeyAdmin group and the luks-upload / scanupload service accounts are created by ansipa-smb-setup.sh on every container start.

Client Enrollment

Via Installer Module

Select freeipa-client during tui-install.sh or install-modules.sh.

Manual Enrollment

Three modes:

# Answerfile mode (unattended)
bash setup/modules/FreeipaAnsible/freeipa-client.sh \
    --answerfile setup/modules/FreeipaAnsible/freeipa-client-answerfile.json

# Interactive prompts
bash setup/modules/FreeipaAnsible/freeipa-client.sh --interactive

# Direct flag passthrough to freeipa-enroll.sh
bash setup/modules/FreeipaAnsible/freeipa-client.sh \
    --domain freeipa.example.com \
    --server ipa.example.com \
    --principal admin

Client Answerfile Schema

{
  "domain":      "freeipa.abdelbaki.eu",
  "realm":       "FREEIPA.ABDELBAKI.EU",
  "server":      "freeipa.abdelbaki.eu",
  "hostname":    "",
  "principal":   "admin",
  "password":    "",
  "mkhomedir":   true,
  "sudo":        true,
  "dns_update":  true,
  "ntp_server":  "",
  "fido2":       false,
  "fido2_users": []
}

Leave hostname blank to use the current machine hostname. Leave password blank to be prompted at enrollment time.


Ansible Playbooks

All playbooks live in setup/modules/FreeipaAnsible/ansible/ and require an inventory of enrolled IPA clients.

Deploy Package Auto-Installer

ansible-playbook -i inventory deploy-ansipa-install.yml

Deploys ansipa-install-packages.sh + a systemd timer (every 30 min). The script queries IPA for host groups named ansipa-install-<package> and installs or removes packages to match.

Group naming convention: ansipa-install-firefox → installs firefox.

Deploy Module Auto-Installer

ansible-playbook -i inventory deploy-ansipa-modules.yml \
    [-e ansipa_user=amir]

Deploys ansipa-install-modules.sh + timer. Queries for groups named ansipa-module-<name> and runs the matching script from /usr/local/lib/ansipa-modules/<name>.sh. Module scripts are copied from setup/modules/optional-Modules/apps/*.sh.

Each module is applied once and stamped in /var/lib/ansipa-modules/<name>.done.

Deploy BaseUser Sync

ansible-playbook -i inventory deploy-baseuser-sync.yml

Deploys a systemd.path unit that triggers on login. Users who are members of the IPA BaseUser group are automatically added to the local baseusers group.

Deploy Policy Enforcer

ansible-playbook -i inventory deploy-ansipa-policies.yml \
    -e smb_scan_password=<SMB_SCAN_PASSWORD>

Deploys ansipa-enforce-policies.sh, ansipa-fetch-alerts.sh, ansipa-scan-notify.sh, a systemd timer (every 30 min), and /etc/ansipa-smb.creds. Use ansible-vault for the password in production.

Collect LUKS Backup Keys

ansible-playbook -i inventory collect-luks-keys.yml \
    -e luks_smb_server=ipa.corp.example.com \
    -e luks_upload_password=<LUKS_KEY_UPLOAD_PASSWORD>

For each enrolled host:

  1. Fetches /_LUKS_BACKUP_KEY from the client to a local staging directory.
  2. Uploads the staged key to //ipa-server/ansipa-luks-keys/<hostname>_LUKS_BACKUP_KEY via smbclient using a temporary credentials file (no_log, deleted in post_tasks).
  3. Removes the local staging copy after a successful upload.

Keys on the SMB share are accessible only to KeyAdmin group members (see SMB Shares).

Schedule automatic collection:

# Add to crontab on the Ansible controller
0 3 * * * cd /path/to/FreeipaAnsible/ansible && \
    ansible-playbook -i inventory collect-luks-keys.yml \
    -e luks_smb_server=ipa.corp.example.com \
    -e luks_upload_password=<LUKS_KEY_UPLOAD_PASSWORD>

Host Group Reference

Device policies (host groups — applied machine-wide)

Group prefix Handled by Effect
ansipa-install-<pkg> ansipa-install-packages.sh Install/remove native package
ansipa-module-<name> ansipa-install-modules.sh Run module script once
fp_install-<app> ansipa-install-flatpaks.sh Install Flatpak app
BaseUser auto-add-baseuser.sh Add user to local baseusers group
policy-daemon-enable-<unit> ansipa-enforce-policies.sh systemctl enable --now <unit>; reverted on leave
policy-daemon-disable-<unit> ansipa-enforce-policies.sh systemctl disable --now <unit>; reverted on leave
policy-timeshift-backup ansipa-enforce-policies.sh Daily Timeshift snapshot at 03:00
policy-security-scan ansipa-enforce-policies.sh Daily ClamAV + rkhunter + chkrootkit scan + SMB upload
no_local_users ansipa-enforce-policies.sh Lock passwords for root and all local users (UID ≥ 1000); reverted on leave
local_sudo_<username> ansipa-enforce-policies.sh Grant <username> full sudo on this specific device; reverted on leave

User policies (user groups — follow the user across all enrolled devices)

Group prefix Handled by Effect
policy-block-binary-<name> ansipa-enforce-policies.sh Prevent group members from running <name> on any enrolled host; use __ for . in Flatpak app IDs
policy-scan-notify ansipa-enforce-policies.sh Fetch server alerts and notify group members every 10 min until acknowledged; follows the user across all enrolled devices

Policy Enforcement

ansipa-enforce-policies.sh runs every 30 minutes on each enrolled client (deployed by deploy-ansipa-policies.yml). All policies are idempotent and reversible — leaving a host/user group undoes the policy on the next run.

Binary Blocking (per user)

policy-block-binary-<name> is a FreeIPA user group, not a host group. Membership follows the user to every enrolled machine: a blocked user cannot run <name> regardless of which device they log into.

The enforcer queries all policy-block-binary-* user groups from FreeIPA on every run and installs a PATH-priority wrapper in /usr/local/bin/<name> for each one. The wrapper checks the caller's group membership at runtime via id(1) / SSSD and:

  • blocks the command if the caller is a group member (exits 1 with a policy message);
  • passes through to the real binary for all other users (searches native PATH dirs, then falls back to flatpak run <name>).

Flatpak support: use __ in place of . in the group name. For example, policy-block-binary-org__gimp__Gimp blocks the Flatpak org.gimp.Gimp for group members while transparently invoking flatpak run org.gimp.Gimp for everyone else.

Deleting the IPA user group causes the wrapper to be removed on the next enforcer run. State is tracked in /var/lib/ansipa-policies/blocked-binaries.

Scan Notifications (per user)

policy-scan-notify is a FreeIPA user group, not a host group. Membership follows the user to every enrolled machine: group members receive scan alert notifications regardless of which device they log into.

The enforcer checks whether the policy-scan-notify group exists in FreeIPA on every run. If it does, it installs the ansipa-fetch-alerts systemd timer fleet-wide (root service, fires every 10 min) and a /etc/profile.d/ansipa-notify.sh snippet. The profile.d snippet checks group membership at login via id(1) / SSSD and starts ansipa-scan-notify.sh as a background user daemon only for members. Non-members log in unaffected.

Deleting the policy-scan-notify IPA user group causes the timer and profile.d snippet to be removed on the next enforcer run.

Daemon Enable / Disable

Group Effect on join Effect on leave
policy-daemon-enable-<unit> systemctl enable --now <unit> systemctl disable --now <unit>
policy-daemon-disable-<unit> systemctl disable --now <unit> systemctl enable --now <unit>

The .service suffix is optional — it is appended automatically when the unit name contains no dot, so any systemd unit type works. If a unit appears in both enable and disable groups simultaneously, it is skipped with a warning.

State is tracked in /var/lib/ansipa-policies/daemon-enabled and daemon-disabled so revert actions are applied correctly when a host leaves a group.

Security Scans & Alert Pipeline

Client (policy-security-scan)
  └── daily 02:00: clamscan + rkhunter + chkrootkit
       └── smbclient → //ipa-server/ansipa-scans/archive/<host>/<date>.log

IPA container (hourly cron)
  └── ansipa-check-scans.sh
       └── grep for FOUND / Warning / Possible rootkit / etc.
            └── writes /data/scan-results/alerts/<host>/<date>.alert

Client (policy-scan-notify, every 10 min via systemd timer)
  └── ansipa-fetch-alerts.sh (root)
       └── downloads *.alert files → ~/administration/<host>/ per active user
            └── ansipa-scan-notify.sh (user daemon, started on login via profile.d)
                 └── notify-send every 10 min while *.alert files remain
                      └── delete file to acknowledge → removed from server on next fetch

Prerequisites for scan policies:

  • Add host to ansipa-module-anti-malware before policy-security-scan (installs ClamAV, rkhunter, chkrootkit).
  • samba-client is installed automatically by deploy-ansipa-policies.yml.
  • SMB credentials are written to /etc/ansipa-smb.creds (root-only, 0600).

Local User Lockdown

Adding a host to the no_local_users group locks the password of every local account — root (UID 0) and all regular users (UID ≥ 1000) — using passwd -l. Accounts whose passwords are already locked (! or * prefix in shadow) are left untouched and are not tracked.

State is persisted in /var/lib/ansipa-policies/no-local-users (one username per line). Only accounts that were actively unlocked at apply time are written to this file, so the revert step only unlocks what was changed.

Action Effect
Join no_local_users passwd -l on root + all UID ≥ 1000 local accounts
Leave no_local_users passwd -u on every account listed in the state file

Interaction with FreeIPA sudo: Domain accounts in the sudoers or sudo-nopasswd FreeIPA groups retain full sudo access via SSSD — local password lockdown does not affect them. Ensure at least one domain admin has sudo before adding a host to this group.

Per-device sudo grants

local_sudo_<username> is a host group that grants a specific user full sudo on that particular machine, independently of FreeIPA-wide sudo rules. This is useful for giving a user admin rights on their own workstation while keeping them unprivileged on shared servers.

Action Effect
Join local_sudo_alice Creates /etc/sudoers.d/ansipa-local-sudo-alice with alice ALL=(ALL) ALL
Leave local_sudo_alice Removes the drop-in on the next enforcer run

State is tracked in /var/lib/ansipa-policies/local-sudo-users. Drop-ins are mode 0440 and validated by visudo syntax rules automatically.


LUKS Key Flow

  Install time (arch-autoinstall.sh or archbaseos-guided-install.sh)
  ─────────────────────────────────────────────────────────────────
  1. User sets primary LUKS passphrase interactively
  2. 64-byte random key generated from /dev/urandom
  3. Key enrolled in second LUKS slot
  4. Key written to /_LUKS_BACKUP_KEY (mode 0400, root-only)
     inside the encrypted Btrfs volume

  Post-install (Ansible controller)
  ──────────────────────────────────
  5. collect-luks-keys.yml runs
  6. Fetches /_LUKS_BACKUP_KEY from each client (become: yes)
  7. Uploads to //ipa-server/ansipa-luks-keys/<host>_LUKS_BACKUP_KEY
     as 'luks-upload' service account (write-only, no_log)
  8. Local staging copy deleted after successful upload

  Recovery (KeyAdmin member)
  ───────────────────────────
  9. Connect to //ipa-server/ansipa-luks-keys with a KeyAdmin Samba account
  10. Retrieve <host>_LUKS_BACKUP_KEY and use with cryptsetup

The backup key lives inside the encrypted partition and is only accessible when the disk is already unlocked. Its purpose is to allow an admin to unlock the disk for recovery without knowing the user's passphrase.


Keycloak

keycloak-configure.sh performs the initial wiring after both FreeIPA and Keycloak containers are healthy:

  1. Creates the configured realm in Keycloak.
  2. Sets up an LDAP federation pointing at the FreeIPA LDAP/LDAPS endpoint.
  3. Configures user and group mappers.

Run it once after the first docker compose up:

cd setup/modules/FreeipaAnsible/image
./keycloak-configure.sh

The Keycloak admin console is available at http://<KC_HOSTNAME>:8080 (dev mode) or https://<KC_HOSTNAME>:8443 (requires TLS cert for production start command).


Auto Enrollment + Ansible

bash setup/modules/FreeipaAnsible/auto-enroll-ansible.sh

Combines FreeIPA client enrollment and Ansible deployment in one shot. Useful for provisioning scripts that run on first boot.