#!/bin/bash # ============================================================ # freeipa-client.sh — FreeIPA client enrolment installer # ============================================================ # Installs the packages needed to join this machine to a # FreeIPA domain and then guides the user through three # enrolment paths via a dialog(1) TUI: # # answerfile — reads a pre-filled JSON file produced by the # freeipa-server.sh installer (recommended for # scripted/repeatable deployments) # manual — interactive dialog prompts for every option # skip — installs packages only; enrol later manually # # Packages installed: # sssd — System Security Services Daemon; provides # PAM/NSS integration for IPA-managed users # cyrus-sasl-gssapi — SASL GSSAPI mechanism for Kerberos auth # openldap — LDAP utilities (ldapsearch, etc.) # krb5 — Kerberos 5 libraries and kinit/klist tools # oddjob — D-Bus service that auto-creates home dirs # on first login (via pam_oddjob_mkhomedir) # freeipa-client — AUR package providing ipa-client-install # # This is an optional module because FreeIPA client enrolment is # only relevant on managed/enterprise machines that are members # of a FreeIPA domain. # ============================================================ set -euo pipefail # Load shared logging helpers from the dotfiles lib source "$(dirname "${BASH_SOURCE[0]}")/../../lib/logging.sh" log "Starting FreeIPA client installer..." # Resolve the absolute path of this script's directory so all relative # references (to FreeipaAnsible scripts and answerfiles) remain correct # regardless of where the installer TUI calls this module from. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FREEIPA_DIR="$SCRIPT_DIR/../../FreeipaAnsible" # Optional server-generated enrolment wrapper script CLIENT_SCRIPT="$FREEIPA_DIR/freeipa-client.sh" # Default answerfile path — the server installer writes this file with # the domain/realm/server already filled in, so clients only need to # add the admin password. DEFAULT_AF="$FREEIPA_DIR/freeipa-client-answerfile.json" # Default values matching the home lab domain; the server installer # sed-replaces these in the distributed client scripts so clients # always get the correct values without any manual editing. DEF_DOMAIN="freeipa.abdelbaki.eu" DEF_REALM="FREEIPA.ABDELBAKI.EU" DEF_SERVER="freeipa.abdelbaki.eu" # ── Packages ────────────────────────────────────────────────────────────────── echo "[+] Installing FreeIPA client packages..." # pacman (without sudo) because this script may be run as root by the TUI # sssd : provides NSS/PAM lookups against IPA LDAP + Kerberos # cyrus-sasl-gssapi : GSSAPI mechanism; needed for LDAP binds with Kerberos tickets # openldap : ldapsearch and other LDAP client utilities # krb5 : Kerberos 5 runtime; kinit, klist, kdestroy # oddjob : D-Bus service that runs pam_oddjob_mkhomedir on first login pacman -S --noconfirm --needed sssd cyrus-sasl-gssapi openldap krb5 oddjob # freeipa-client provides ipa-client-install (the official FreeIPA enrolment # tool). It is AUR-only on Arch; gracefully degrade if yay is absent. if command -v yay &>/dev/null; then echo "[+] Installing freeipa-client (AUR)..." yay -S --noconfirm --needed freeipa-client else echo "[!] yay not found — skipping freeipa-client AUR package." echo " Install yay then run: yay -S --needed freeipa-client" fi # Enable SSSD now so it starts at boot after enrolment. # "|| true" suppresses the error if the service unit doesn't exist yet # (it will exist after freeipa-client is installed). systemctl enable sssd.service 2>/dev/null || true # Ensure dialog is available for the TUI; install it on-the-fly if missing. command -v dialog &>/dev/null || pacman -S --noconfirm --needed dialog # ── Dialog theme ────────────────────────────────────────────────────────────── # Apply the same magenta-on-black cyberqueer colour scheme used throughout # the dotfiles TUI installers so all dialog screens look consistent. # We only write a temporary dialogrc if one isn't already set in the environment. if [[ -z "${DIALOGRC:-}" ]] || [[ ! -f "${DIALOGRC:-/dev/null}" ]]; then _TMP_D=$(mktemp -d) trap 'rm -rf "$_TMP_D"' EXIT export DIALOGRC="$_TMP_D/dialogrc" cat > "$DIALOGRC" <<'RCEOF' use_shadow = ON use_colors = ON screen_color = (BLACK,BLACK,ON) shadow_color = (BLACK,BLACK,ON) title_color = (MAGENTA,BLACK,ON) border_color = (MAGENTA,BLACK,ON) button_active_color = (BLACK,MAGENTA,ON) button_inactive_color = (WHITE,BLACK,OFF) button_key_active_color = (BLACK,CYAN,ON) button_key_inactive_color = (CYAN,BLACK,ON) button_label_active_color = (BLACK,MAGENTA,ON) button_label_inactive_color = (WHITE,BLACK,OFF) inputbox_color = (WHITE,BLACK,OFF) inputbox_border_color = (MAGENTA,BLACK,ON) menubox_color = (WHITE,BLACK,OFF) menubox_border_color = (MAGENTA,BLACK,ON) item_color = (WHITE,BLACK,OFF) item_selected_color = (BLACK,MAGENTA,ON) tag_color = (CYAN,BLACK,ON) tag_selected_color = (BLACK,CYAN,ON) tag_key_color = (CYAN,BLACK,ON) tag_key_selected_color = (BLACK,CYAN,ON) check_color = (WHITE,BLACK,OFF) check_selected_color = (BLACK,MAGENTA,ON) uarrow_color = (MAGENTA,BLACK,ON) darrow_color = (MAGENTA,BLACK,ON) RCEOF fi BT="FreeIPA Client Setup" T=$(mktemp); trap 'rm -f "$T"' EXIT # ── dialog helper wrappers ──────────────────────────────────────────────────── # These thin wrappers normalise the fd-swap trick needed to capture dialog # output (dialog writes to stderr, so we swap stdout/stderr with 3>&1 1>&2 2>&3). d() { dialog --backtitle "$BT" "$@" 3>&1 1>&2 2>&3; } input(){ d --title " $1 " --inputbox "$2" 10 64 "$3"; } pass() { d --title " $1 " --passwordbox "$2" 10 64; } yn() { d --title " $1 " --yesno "$2" 7 64; } msg() { d --title " FreeIPA Client " --msgbox "$1" 10 64; } # ── Enrollment choice ───────────────────────────────────────────────────────── # Present three options: answerfile, manual, or skip. # Default to "skip" if the user presses Escape or cancels. CHOICE=$(d --title " FreeIPA Client Enrollment " \ --menu "\n Packages installed.\n How would you like to enroll this host?\n" \ 13 64 3 \ "answerfile" "Use a JSON answerfile" \ "manual" "Enter enrollment data manually" \ "skip" "Skip — enroll later") || CHOICE="skip" # ── run_enroll helper ───────────────────────────────────────────────────────── # Runs the enrolment either via the server-generated freeipa-client.sh wrapper # (preferred, because it already has the correct domain/realm baked in) or by # falling back to calling ipa-client-install directly with the same arguments. run_enroll() { local args=("$@") if [[ -x "$CLIENT_SCRIPT" ]]; then # Preferred path: use the pre-generated client script from the server exec "$CLIENT_SCRIPT" "${args[@]}" else # Fall back to ipa-client-install directly local cmd=(ipa-client-install --unattended) local dom="" rlm="" srv="" hst="" pri="admin" pwd="" ntp="" local mkhomedir=true sudo_=true dns=true fido2=false declare -a fido2_users=() # Parse our internal argument format and translate to ipa-client-install flags for ((i=0; i<${#args[@]}; i++)); do case "${args[$i]}" in --domain) dom="${args[$((i+1))]}"; ((i++)) ;; --realm) rlm="${args[$((i+1))]}"; ((i++)) ;; --server) srv="${args[$((i+1))]}"; ((i++)) ;; --hostname) hst="${args[$((i+1))]}"; ((i++)) ;; --principal) pri="${args[$((i+1))]}"; ((i++)) ;; --password) pwd="${args[$((i+1))]}"; ((i++)) ;; --ntp-server) ntp="${args[$((i+1))]}"; ((i++)) ;; --no-mkhomedir) mkhomedir=false ;; --no-sudo) sudo_=false ;; --no-dns-update) dns=false ;; --fido2) fido2=true ;; --fido2-user) fido2_users+=("${args[$((i+1))]}"); ((i++)) ;; esac done # Build the ipa-client-install command from parsed values [[ -n "$dom" ]] && cmd+=(--domain "$dom") [[ -n "$rlm" ]] && cmd+=(--realm "$rlm") [[ -n "$srv" ]] && cmd+=(--server "$srv") [[ -n "$hst" ]] && cmd+=(--hostname "$hst") [[ -n "$ntp" ]] && cmd+=(--ntp-server "$ntp") cmd+=(--principal "$pri" --password "$pwd") $mkhomedir && cmd+=(--mkhomedir) || cmd+=(--no-mkhomedir) $sudo_ && cmd+=(--enable-dns-updates) || true ! $dns && cmd+=(--no-dns-update) || true exec "${cmd[@]}" fi } # ── Answerfile mode ─────────────────────────────────────────────────────────── # The answerfile is a JSON file pre-generated by freeipa-server.sh with domain, # realm, and server already filled in. The user only needs to add the password. if [[ "$CHOICE" == "answerfile" ]]; then AF_PATH=$(input "Answerfile path" \ "Path to the JSON answerfile:" \ "$DEFAULT_AF") || { msg " Enrollment cancelled."; exit 0; } # Use the default path if the user pressed Enter without typing a path [[ -z "$AF_PATH" ]] && AF_PATH="$DEFAULT_AF" if [[ ! -f "$AF_PATH" ]]; then # Offer to create a template answerfile so the user can fill it in if yn "Create answerfile" \ " '$AF_PATH' does not exist.\n Create it with default values?"; then mkdir -p "$(dirname "$AF_PATH")" cat > "$AF_PATH" </dev/null || hostname) HOST=$(input "This Host FQDN" "Hostname to register (leave blank = current):" \ "$DEF_HOST") || HOST="$DEF_HOST" PRINCIPAL=$(input "Admin Principal" "IPA admin principal:" "admin") || PRINCIPAL="admin" PRINCIPAL="${PRINCIPAL:-admin}" # Use passwordbox so the password is not echoed to the terminal PASSWORD=$(pass "Admin Password" "Password for $PRINCIPAL@$REALM:") \ || { msg " Enrollment cancelled."; exit 0; } [[ -z "$PASSWORD" ]] && { msg " Password is required."; exit 1; } NTP=$(input "NTP Server" "NTP server (blank to skip):" "") || NTP="" ARGS=( --domain "$DOMAIN" --realm "$REALM" --server "$SERVER" --hostname "$HOST" --principal "$PRINCIPAL" --password "$PASSWORD" ) [[ -n "$NTP" ]] && ARGS+=(--ntp-server "$NTP") # Optional features: each yes/no dialog adds or omits the corresponding flag yn "Home Directories" " Auto-create home directories on first login?" \ && true || ARGS+=(--no-mkhomedir) yn "Sudo via SSSD" " Configure sudo rules via SSSD?" \ && true || ARGS+=(--no-sudo) yn "DNS Update" " Register this host's IP in IPA DNS?" \ && true || ARGS+=(--no-dns-update) # FIDO2/WebAuthn hardware token support (requires libfido2 + pamu2fcfg) if yn "FIDO2" " Enable FIDO2/WebAuthn authentication?"; then ARGS+=(--fido2) FIDO2_USERS=$(input "FIDO2 Users" \ "Usernames to enable FIDO2 for (comma-separated, blank = all):" "") \ || FIDO2_USERS="" if [[ -n "$FIDO2_USERS" ]]; then # Split comma-separated list and strip whitespace from each username IFS=',' read -ra _U <<< "$FIDO2_USERS" for u in "${_U[@]}"; do u="${u// /}" [[ -n "$u" ]] && ARGS+=(--fido2-user "$u") done fi fi clear run_enroll "${ARGS[@]}" fi # ── Skip ────────────────────────────────────────────────────────────────────── # Packages are installed; print instructions for later manual enrolment. echo "" echo "[✓] FreeIPA client packages installed." echo "" echo " To enroll later, run one of:" if [[ -x "$CLIENT_SCRIPT" ]]; then echo " $CLIENT_SCRIPT --interactive" echo " $CLIENT_SCRIPT --answerfile $DEFAULT_AF" else echo " ipa-client-install --domain= --server= --principal=admin" fi echo "" echo " After enrollment:" echo " authselect select sssd with-mkhomedir --force"