323 lines
15 KiB
Bash
Executable File
323 lines
15 KiB
Bash
Executable File
#!/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" <<AFEOF
|
|
{
|
|
"domain": "$DEF_DOMAIN",
|
|
"realm": "$DEF_REALM",
|
|
"server": "$DEF_SERVER",
|
|
"hostname": "",
|
|
"principal": "admin",
|
|
"password": "",
|
|
"mkhomedir": true,
|
|
"sudo": true,
|
|
"dns_update": true,
|
|
"ntp_server": "",
|
|
"fido2": false,
|
|
"fido2_users": []
|
|
}
|
|
AFEOF
|
|
msg " Created: $AF_PATH\n\n Edit it to match your environment,\n then re-run the FreeIPA Client module to enroll."
|
|
exit 0
|
|
else
|
|
msg " Provide an existing answerfile and re-run."
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# Clear the dialog UI before running ipa-client-install (which prints to stdout)
|
|
clear
|
|
run_enroll --answerfile "$AF_PATH"
|
|
fi
|
|
|
|
# ── Manual mode ───────────────────────────────────────────────────────────────
|
|
# Collect all enrolment parameters interactively via dialog prompts.
|
|
if [[ "$CHOICE" == "manual" ]]; then
|
|
DOMAIN=$(input "IPA Domain" "FreeIPA domain:" "$DEF_DOMAIN") \
|
|
|| { msg " Enrollment cancelled."; exit 0; }
|
|
DOMAIN="${DOMAIN:-$DEF_DOMAIN}"
|
|
|
|
# Kerberos realm is conventionally the domain uppercased; pre-fill that guess
|
|
DEF_REALM_CALC="${DOMAIN^^}"
|
|
REALM=$(input "Kerberos Realm" "Kerberos realm (usually domain uppercased):" \
|
|
"$DEF_REALM_CALC") || REALM="$DEF_REALM_CALC"
|
|
REALM="${REALM:-$DEF_REALM_CALC}"
|
|
|
|
SERVER=$(input "IPA Server" "FreeIPA server FQDN:" "$DOMAIN") \
|
|
|| { msg " Enrollment cancelled."; exit 0; }
|
|
SERVER="${SERVER:-$DOMAIN}"
|
|
|
|
# Default to the current machine's FQDN so the user can just press Enter
|
|
DEF_HOST=$(hostname -f 2>/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=<domain> --server=<server> --principal=admin"
|
|
fi
|
|
echo ""
|
|
echo " After enrollment:"
|
|
echo " authselect select sssd with-mkhomedir --force"
|