diff --git a/setup/install-modules.sh b/setup/install-modules.sh index da11899..67b1cf2 100755 --- a/setup/install-modules.sh +++ b/setup/install-modules.sh @@ -119,6 +119,7 @@ count_steps() { [[ "$sel" == *"podman"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"cockpit"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"ssh-server"* ]] && TOTAL=$(( TOTAL + 1 )) + [[ "$sel" == *"freeipa-server"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"python"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"zfs"* ]] && TOTAL=$(( TOTAL + 1 )) [[ "$sel" == *"wprs"* ]] && TOTAL=$(( TOTAL + 1 )) @@ -183,6 +184,7 @@ SELECTED=$(dialog --backtitle "$BACKTITLE" \ "podman" "Podman rootless containers · buildah" off \ "cockpit" "Cockpit web UI · machines · podman" off \ "ssh-server" "SSH server openssh · key-auth · enabled" off \ + "freeipa-server" "FreeIPA Server interactive server setup + client gen" off \ "python" "Python tools pyright · pipx · pynvim" off \ "zfs" "ZFS zfs-dkms kernel module" off \ "wprs" "WPRS wprs-git (AUR)" off \ @@ -226,6 +228,7 @@ SUMMARY="" [[ "$SELECTED" == *"podman"* ]] && SUMMARY+=" ✦ Podman\n" [[ "$SELECTED" == *"cockpit"* ]] && SUMMARY+=" ✦ Cockpit\n" [[ "$SELECTED" == *"ssh-server"* ]] && SUMMARY+=" ✦ SSH server\n" +[[ "$SELECTED" == *"freeipa-server"* ]] && SUMMARY+=" ✦ FreeIPA Server\n" [[ "$SELECTED" == *"python"* ]] && SUMMARY+=" ✦ Python tools\n" [[ "$SELECTED" == *"zfs"* ]] && SUMMARY+=" ✦ ZFS\n" [[ "$SELECTED" == *"wprs"* ]] && SUMMARY+=" ✦ WPRS\n" @@ -272,6 +275,7 @@ count_steps "$SELECTED" [[ "$SELECTED" == *"podman"* ]] && run_module "Podman" "$APPS/podman.sh" [[ "$SELECTED" == *"cockpit"* ]] && run_module "Cockpit" "$APPS/cockpit.sh" [[ "$SELECTED" == *"ssh-server"* ]] && run_module "SSH Server" "$APPS/ssh-server.sh" +[[ "$SELECTED" == *"freeipa-server"* ]] && run_module "FreeIPA Server" "$APPS/freeipa-server.sh" [[ "$SELECTED" == *"python"* ]] && run_module "Python Tools" "$MODULES/optional-Modules/python.sh" [[ "$SELECTED" == *"zfs"* ]] && run_module "ZFS" "$MODULES/optional-Modules/zfs.sh" [[ "$SELECTED" == *"wprs"* ]] && run_module "WPRS" "$MODULES/optional-Modules/wprs.sh" diff --git a/setup/modules/FreeipaAnsible/freeipa-client-answerfile.json b/setup/modules/FreeipaAnsible/freeipa-client-answerfile.json new file mode 100644 index 0000000..45fa579 --- /dev/null +++ b/setup/modules/FreeipaAnsible/freeipa-client-answerfile.json @@ -0,0 +1,14 @@ +{ + "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": [] +} diff --git a/setup/modules/FreeipaAnsible/freeipa-client.sh b/setup/modules/FreeipaAnsible/freeipa-client.sh new file mode 100755 index 0000000..7643e91 --- /dev/null +++ b/setup/modules/FreeipaAnsible/freeipa-client.sh @@ -0,0 +1,159 @@ +#!/bin/bash +# freeipa-client.sh — FreeIPA client enrollment +# +# Three modes: +# --answerfile FILE Read settings from a JSON file (jq required) +# --interactive Prompt for every setting +# [flags] Pass any flags directly to freeipa-enroll.sh +# +# All flags accepted by freeipa-enroll.sh work here too and override +# any values loaded from an answerfile or interactive prompts. +# +# JSON answerfile schema: +# { +# "domain": "freeipa.abdelbaki.eu", +# "realm": "FREEIPA.ABDELBAKI.EU", +# "server": "freeipa.abdelbaki.eu", +# "hostname": "", <- leave blank to use current hostname +# "principal": "admin", +# "password": "", <- will prompt if blank +# "mkhomedir": true, +# "sudo": true, +# "dns_update": true, +# "ntp_server": "", +# "fido2": false, +# "fido2_users": [] +# } + +set -euo pipefail + +SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENROLL="$SELF_DIR/freeipa-enroll.sh" + +[[ ! -x "$ENROLL" ]] && { echo "Error: freeipa-enroll.sh not found at $ENROLL" >&2; exit 1; } + +# Defaults (match freeipa-enroll.sh hardcoded values; freeipa-server.sh will +# sed-replace these when generating customized copies) +IPA_DOMAIN="freeipa.abdelbaki.eu" +IPA_REALM="FREEIPA.ABDELBAKI.EU" +IPA_SERVER="freeipa.abdelbaki.eu" + +MODE="" +ANSWERFILE="" +PASSTHROUGH=() + +while [[ $# -gt 0 ]]; do + case $1 in + --answerfile) MODE="answerfile"; ANSWERFILE="$2"; shift 2 ;; + --interactive) MODE="interactive"; shift ;; + *) PASSTHROUGH+=("$1"); shift ;; + esac +done + +# ─── Answerfile mode ────────────────────────────────────────────────────────── +if [[ "$MODE" == "answerfile" ]]; then + [[ ! -f "$ANSWERFILE" ]] && { echo "Error: answerfile not found: $ANSWERFILE" >&2; exit 1; } + command -v jq &>/dev/null || { echo "Error: jq is required for answerfile mode" >&2; exit 1; } + + get() { jq -r "$1 // empty" "$ANSWERFILE"; } + get_bool() { jq -r "$1 // empty" "$ANSWERFILE"; } + + AF_DOMAIN=$(get '.domain') + AF_REALM=$(get '.realm') + AF_SERVER=$(get '.server') + AF_HOSTNAME=$(get '.hostname') + AF_PRINCIPAL=$(get '.principal') + AF_PASSWORD=$(get '.password') + AF_NTP=$(get '.ntp_server') + AF_MKHOMEDIR=$(get_bool '.mkhomedir') + AF_SUDO=$(get_bool '.sudo') + AF_DNS_UPDATE=$(get_bool '.dns_update') + AF_FIDO2=$(get_bool '.fido2') + mapfile -t AF_FIDO2_USERS < <(jq -r '.fido2_users // [] | .[]' "$ANSWERFILE") + + ARGS=() + [[ -n "$AF_DOMAIN" ]] && ARGS+=(--domain "$AF_DOMAIN") + [[ -n "$AF_REALM" ]] && ARGS+=(--realm "$AF_REALM") + [[ -n "$AF_SERVER" ]] && ARGS+=(--server "$AF_SERVER") + [[ -n "$AF_HOSTNAME" ]] && ARGS+=(--hostname "$AF_HOSTNAME") + [[ -n "$AF_PRINCIPAL" ]] && ARGS+=(--principal "$AF_PRINCIPAL") + [[ -n "$AF_NTP" ]] && ARGS+=(--ntp-server "$AF_NTP") + + if [[ -z "$AF_PASSWORD" ]]; then + printf '[?] Password for %s@%s: ' "${AF_PRINCIPAL:-admin}" "${AF_REALM:-REALM}" + read -rs AF_PASSWORD; echo + fi + ARGS+=(--password "$AF_PASSWORD") + + [[ "$AF_MKHOMEDIR" == "false" ]] && ARGS+=(--no-mkhomedir) + [[ "$AF_SUDO" == "false" ]] && ARGS+=(--no-sudo) + [[ "$AF_DNS_UPDATE" == "false" ]] && ARGS+=(--no-dns-update) + [[ "$AF_FIDO2" == "true" ]] && ARGS+=(--fido2) + for U in "${AF_FIDO2_USERS[@]}"; do + [[ -n "$U" ]] && ARGS+=(--fido2-user "$U") + done + + exec "$ENROLL" "${ARGS[@]}" "${PASSTHROUGH[@]}" +fi + +# ─── Interactive mode ───────────────────────────────────────────────────────── +if [[ "$MODE" == "interactive" ]]; then + p() { printf '\033[0;35m[?]\033[0m %s ' "$*"; } + + p "IPA Domain [$IPA_DOMAIN]:"; read -r I; IPA_DOMAIN="${I:-$IPA_DOMAIN}" + IPA_REALM="${IPA_DOMAIN^^}" + p "Kerberos Realm [$IPA_REALM]:"; read -r I; IPA_REALM="${I:-$IPA_REALM}" + p "IPA Server [$IPA_SERVER]:"; read -r I; IPA_SERVER="${I:-$IPA_SERVER}" + + CURRENT_HOST=$(hostname -f 2>/dev/null || hostname) + p "This host's FQDN [$CURRENT_HOST]:"; read -r I; CLIENT_HOSTNAME="${I:-$CURRENT_HOST}" + + p "Admin principal [admin]:"; read -r PRINCIPAL; PRINCIPAL="${PRINCIPAL:-admin}" + p "Admin password (no echo):"; read -rs PASSWORD; echo + [[ -z "$PASSWORD" ]] && { echo "Error: password is required" >&2; exit 1; } + + p "Enable home directory creation? [Y/n]:"; read -r I + MKHOMEDIR=true; [[ "${I,,}" == "n"* ]] && MKHOMEDIR=false + + p "Configure sudo via SSSD? [Y/n]:"; read -r I + SUDO=true; [[ "${I,,}" == "n"* ]] && SUDO=false + + p "Update DNS record? [Y/n]:"; read -r I + DNS_UPDATE=true; [[ "${I,,}" == "n"* ]] && DNS_UPDATE=false + + p "NTP server (blank to skip):"; read -r NTP_SERVER + + p "Enable FIDO2 authentication? [y/N]:"; read -r I + FIDO2=false; [[ "${I,,}" == "y"* ]] && FIDO2=true + + FIDO2_USER_LIST=() + if [[ "$FIDO2" == true ]]; then + p "FIDO2 users (comma-separated, blank to skip):"; read -r I + if [[ -n "$I" ]]; then + IFS=',' read -ra FIDO2_USER_LIST <<< "$I" + fi + fi + + ARGS=( + --domain "$IPA_DOMAIN" + --realm "$IPA_REALM" + --server "$IPA_SERVER" + --hostname "$CLIENT_HOSTNAME" + --principal "$PRINCIPAL" + --password "$PASSWORD" + ) + [[ "$MKHOMEDIR" == false ]] && ARGS+=(--no-mkhomedir) + [[ "$SUDO" == false ]] && ARGS+=(--no-sudo) + [[ "$DNS_UPDATE" == false ]] && ARGS+=(--no-dns-update) + [[ "$FIDO2" == true ]] && ARGS+=(--fido2) + [[ -n "$NTP_SERVER" ]] && ARGS+=(--ntp-server "$NTP_SERVER") + for U in "${FIDO2_USER_LIST[@]}"; do + [[ -n "${U// /}" ]] && ARGS+=(--fido2-user "${U// /}") + done + + exec "$ENROLL" "${ARGS[@]}" "${PASSTHROUGH[@]}" +fi + +# ─── Direct passthrough ─────────────────────────────────────────────────────── +# No --answerfile or --interactive: forward all args directly to freeipa-enroll.sh +exec "$ENROLL" "${PASSTHROUGH[@]}" diff --git a/setup/modules/optional-Modules/apps/freeipa-server.sh b/setup/modules/optional-Modules/apps/freeipa-server.sh new file mode 100755 index 0000000..6a24732 --- /dev/null +++ b/setup/modules/optional-Modules/apps/freeipa-server.sh @@ -0,0 +1,399 @@ +#!/bin/bash +# freeipa-server.sh — Interactive FreeIPA server installer +# Collects all configuration, installs the IPA server, and outputs customized +# client enrollment scripts (freeipa-enroll.sh, freeipa-client.sh, answerfile, +# and optionally auto-enroll-ansible.sh) ready for distribution. + +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +BLUE='\033[0;34m'; CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; NC='\033[0m' + +log() { echo -e "${GREEN}[+]${NC} $*"; } +warn() { echo -e "${YELLOW}[!]${NC} $*"; } +error() { echo -e "${RED}[✗]${NC} $*" >&2; } +info() { echo -e "${CYAN}[i]${NC} $*"; } +section() { echo -e "\n${BLUE}━━━ $* ━━━${NC}"; } +ask() { printf "${MAGENTA}[?]${NC} %s " "$*"; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IPA_BASE="$SCRIPT_DIR/../../FreeipaAnsible" + +# ─── Root check ─────────────────────────────────────────────────────────────── +[[ $EUID -ne 0 ]] && { error "Must be run as root."; exit 1; } + +# ─── OS detection ───────────────────────────────────────────────────────────── +source /etc/os-release +OS_ID="${ID}"; OS_PRETTY="${PRETTY_NAME}" + +section "System check" +log "OS: $OS_PRETTY" + +case "$OS_ID" in + rhel|centos|rocky|almalinux) + PKG_MGR="dnf" + IPA_SERVER_PKGS="freeipa-server freeipa-server-dns freeipa-server-trust-ad" + ANSIBLE_PKGS="ansible-core python3-netaddr" + ;; + fedora) + PKG_MGR="dnf" + IPA_SERVER_PKGS="freeipa-server freeipa-server-dns" + ANSIBLE_PKGS="ansible-core python3-netaddr" + ;; + arch) + PKG_MGR="pacman" + IPA_SERVER_PKGS="freeipa" + ANSIBLE_PKGS="ansible python-netaddr" + warn "Arch: FreeIPA server support is community-maintained. RHEL/Rocky recommended for production." + ;; + *) + error "FreeIPA server is not supported on $OS_PRETTY." + error "Supported: RHEL, Rocky, AlmaLinux, CentOS, Fedora, Arch." + exit 1 + ;; +esac + +# ─── Conflict pre-flight ────────────────────────────────────────────────────── +section "Conflict checks" + +if [[ -f /etc/ipa/default.conf ]]; then + if grep -q "mode=production" /etc/ipa/default.conf 2>/dev/null || \ + grep -q "^basedn=" /etc/ipa/default.conf 2>/dev/null; then + error "/etc/ipa/default.conf exists — this host may already be an IPA server or enrolled client." + error "Uninstall first: ipa-server-install --uninstall or ipa-client-install --uninstall" + exit 1 + fi +fi + +for svc in named dirsrv krb5kdc; do + if systemctl is-active "$svc" &>/dev/null; then + warn "Service '$svc' is already running and may conflict with FreeIPA." + ask "Continue anyway? [y/N]:"; read -r C + [[ "${C,,}" != "y"* ]] && exit 1 + fi +done + +for port in 389 636 88; do + if ss -tlnp 2>/dev/null | grep -q ":${port} " || \ + ss -ulnp 2>/dev/null | grep -q ":${port} "; then + warn "Port $port is already in use. FreeIPA requires this port." + ask "Continue anyway? [y/N]:"; read -r C + [[ "${C,,}" != "y"* ]] && exit 1 + fi +done + +log "Conflict checks passed." + +# ─── Gather configuration ───────────────────────────────────────────────────── +section "Configuration" +info "Leave any field blank to accept the value shown in [brackets]." +echo + +CURRENT_FQDN=$(hostname -f 2>/dev/null || hostname) +ask "Server FQDN [$CURRENT_FQDN]:"; read -r I +SERVER_HOSTNAME="${I:-$CURRENT_FQDN}" + +GUESSED_DOMAIN="${SERVER_HOSTNAME#*.}" +ask "IPA Domain [$GUESSED_DOMAIN]:"; read -r I +IPA_DOMAIN="${I:-$GUESSED_DOMAIN}" + +GUESSED_REALM="${IPA_DOMAIN^^}" +ask "Kerberos Realm [$GUESSED_REALM]:"; read -r I +IPA_REALM="${I:-$GUESSED_REALM}" + +GUESSED_IP=$(ip route get 1 2>/dev/null | awk '{print $7; exit}') +ask "Server IP address [$GUESSED_IP]:"; read -r I +SERVER_IP="${I:-$GUESSED_IP}" + +while true; do + ask "IPA admin password (min 8 chars, no echo):"; read -rs ADMIN_PASSWORD; echo + ask "Confirm admin password:"; read -rs ADMIN_PASSWORD2; echo + [[ "$ADMIN_PASSWORD" == "$ADMIN_PASSWORD2" && ${#ADMIN_PASSWORD} -ge 8 ]] && break + warn "Passwords do not match or are too short." +done + +while true; do + ask "Directory Manager password (min 8 chars, no echo):"; read -rs DM_PASSWORD; echo + ask "Confirm Directory Manager password:"; read -rs DM_PASSWORD2; echo + [[ "$DM_PASSWORD" == "$DM_PASSWORD2" && ${#DM_PASSWORD} -ge 8 ]] && break + warn "Passwords do not match or are too short." +done + +ask "Set up integrated DNS (bind) with FreeIPA? [Y/n]:"; read -r I +SETUP_DNS=true; [[ "${I,,}" == "n"* ]] && SETUP_DNS=false + +DNS_FORWARDER="" +AUTO_REVERSE=true +if [[ "$SETUP_DNS" == true ]]; then + ask "DNS forwarder IP (blank for no forwarding):"; read -r DNS_FORWARDER + ask "Auto-create reverse DNS zone? [Y/n]:"; read -r I + [[ "${I,,}" == "n"* ]] && AUTO_REVERSE=false +fi + +ask "NTP server (blank for system default):"; read -r NTP_SERVER + +ask "Install KRA (Key Recovery Authority)? [y/N]:"; read -r I +SETUP_KRA=false; [[ "${I,,}" == "y"* ]] && SETUP_KRA=true + +echo +ask "Enable Ansible/AWX auto-enrollment integration? [y/N]:"; read -r I +SETUP_ANSIBLE=false; [[ "${I,,}" == "y"* ]] && SETUP_ANSIBLE=true + +AWX_URL=""; AWX_TOKEN=""; AWX_INVENTORY="" +if [[ "$SETUP_ANSIBLE" == true ]]; then + ask "AWX/Controller URL (e.g. https://awx.corp.example.com):"; read -r AWX_URL + ask "AWX API token:"; read -r AWX_TOKEN + ask "AWX inventory name:"; read -r AWX_INVENTORY +fi + +DEFAULT_OUTDIR="$(eval echo ~"${SUDO_USER:-root}")/freeipa-output" +ask "Output directory for client scripts [$DEFAULT_OUTDIR]:"; read -r I +OUTPUT_DIR="${I:-$DEFAULT_OUTDIR}" + +# ─── Confirm ────────────────────────────────────────────────────────────────── +section "Confirm" +echo +printf " %-22s %s\n" "Hostname:" "$SERVER_HOSTNAME" +printf " %-22s %s\n" "Domain:" "$IPA_DOMAIN" +printf " %-22s %s\n" "Realm:" "$IPA_REALM" +printf " %-22s %s\n" "IP:" "$SERVER_IP" +printf " %-22s %s\n" "Integrated DNS:" "$SETUP_DNS" +[[ -n "$DNS_FORWARDER" ]] && printf " %-22s %s\n" "DNS Forwarder:" "$DNS_FORWARDER" +printf " %-22s %s\n" "Install KRA:" "$SETUP_KRA" +printf " %-22s %s\n" "Ansible/AWX:" "$SETUP_ANSIBLE" +[[ "$SETUP_ANSIBLE" == true ]] && printf " %-22s %s\n" "AWX URL:" "$AWX_URL" +printf " %-22s %s\n" "Output dir:" "$OUTPUT_DIR" +echo +ask "Proceed with installation? [y/N]:"; read -r CONFIRM +[[ "${CONFIRM,,}" != "y"* ]] && { echo "Aborted."; exit 0; } + +# ─── Hostname ───────────────────────────────────────────────────────────────── +section "Configuring hostname" +hostnamectl set-hostname "$SERVER_HOSTNAME" +SHORT_NAME="${SERVER_HOSTNAME%%.*}" +sed -i "/\b${SERVER_HOSTNAME}\b/d" /etc/hosts +sed -i "/\b${SHORT_NAME}\b/d" /etc/hosts +echo "$SERVER_IP $SERVER_HOSTNAME $SHORT_NAME" >> /etc/hosts +log "Hostname: $SERVER_HOSTNAME → $SERVER_IP" + +# ─── Packages ───────────────────────────────────────────────────────────────── +section "Installing packages" +case "$PKG_MGR" in + dnf) + dnf install -y $IPA_SERVER_PKGS + [[ "$SETUP_ANSIBLE" == true ]] && dnf install -y $ANSIBLE_PKGS + ;; + pacman) + pacman -Sy --noconfirm $IPA_SERVER_PKGS + [[ "$SETUP_ANSIBLE" == true ]] && pacman -Sy --noconfirm $ANSIBLE_PKGS + ;; +esac +log "Packages installed." + +# ─── Firewall ──────────────────────────────────────────────────────────────── +section "Configuring firewall" +if command -v firewall-cmd &>/dev/null && systemctl is-active firewalld &>/dev/null; then + for svc in freeipa-ldap freeipa-ldaps kerberos kpasswd https ntp; do + firewall-cmd --permanent --add-service="$svc" + done + [[ "$SETUP_DNS" == true ]] && firewall-cmd --permanent --add-service=dns + firewall-cmd --reload + log "firewalld rules applied." +elif command -v ufw &>/dev/null; then + for rule in 80/tcp 443/tcp 389/tcp 636/tcp 88/tcp 88/udp 464/tcp 464/udp 123/udp; do + ufw allow "$rule" + done + [[ "$SETUP_DNS" == true ]] && { ufw allow 53/tcp; ufw allow 53/udp; } + log "UFW rules applied." +else + warn "No firewall manager found — manually open: 80,443,389,636,88,464,123$([ "$SETUP_DNS" == true ] && echo ",53")" +fi + +# ─── ipa-server-install ─────────────────────────────────────────────────────── +section "Installing FreeIPA server (this takes several minutes)" + +IPA_ARGS=( + --realm="$IPA_REALM" + --domain="$IPA_DOMAIN" + --admin-password="$ADMIN_PASSWORD" + --ds-password="$DM_PASSWORD" + --hostname="$SERVER_HOSTNAME" + --ip-address="$SERVER_IP" + --mkhomedir + --unattended +) + +if [[ "$SETUP_DNS" == true ]]; then + IPA_ARGS+=(--setup-dns) + [[ -n "$DNS_FORWARDER" ]] && IPA_ARGS+=(--forwarder="$DNS_FORWARDER") || IPA_ARGS+=(--no-forwarders) + [[ "$AUTO_REVERSE" == true ]] && IPA_ARGS+=(--auto-reverse) || IPA_ARGS+=(--no-reverse) +fi +[[ -n "$NTP_SERVER" ]] && IPA_ARGS+=(--ntp-server="$NTP_SERVER") +[[ "$SETUP_KRA" == true ]] && IPA_ARGS+=(--setup-kra) + +ipa-server-install "${IPA_ARGS[@]}" +log "FreeIPA server installed." + +# ─── Post-install ──────────────────────────────────────────────────────────── +section "Post-install verification" +if echo "$ADMIN_PASSWORD" | kinit admin &>/dev/null; then + log "Kerberos: admin ticket obtained." + kdestroy &>/dev/null +else + warn "Could not obtain Kerberos ticket — check connectivity." +fi + +# ─── Ansible integration ───────────────────────────────────────────────────── +if [[ "$SETUP_ANSIBLE" == true ]]; then + section "Setting up Ansible auto-enrollment" + cp "$IPA_BASE/ansible/ansipa-install-packages.sh" /usr/local/bin/ + cp "$IPA_BASE/ansible/auto-add-baseuser.sh" /usr/local/bin/ + chmod +x /usr/local/bin/ansipa-install-packages.sh /usr/local/bin/auto-add-baseuser.sh + + for f in ansipa-install.service ansipa-install.timer \ + baseuser-sync.path baseuser-sync.service; do + cp "$IPA_BASE/ansible/$f" /etc/systemd/system/ + done + systemctl daemon-reload + systemctl enable --now ansipa-install.timer + systemctl enable --now baseuser-sync.path + log "Ansible auto-enrollment services enabled." +fi + +# ─── Generate output scripts ────────────────────────────────────────────────── +section "Generating client scripts → $OUTPUT_DIR" +REAL_OUTDIR="${OUTPUT_DIR/#\~/"$(eval echo ~"${SUDO_USER:-root}")"/}" +mkdir -p "$REAL_OUTDIR" + +# 1. Customized freeipa-enroll.sh (server defaults baked in) +sed \ + -e "s|^IPA_DOMAIN=.*|IPA_DOMAIN=\"$IPA_DOMAIN\"|" \ + -e "s|^IPA_REALM=.*|IPA_REALM=\"$IPA_REALM\"|" \ + -e "s|^IPA_SERVER=.*|IPA_SERVER=\"$SERVER_HOSTNAME\"|" \ + "$IPA_BASE/freeipa-enroll.sh" > "$REAL_OUTDIR/freeipa-enroll.sh" +chmod +x "$REAL_OUTDIR/freeipa-enroll.sh" +log "freeipa-enroll.sh" + +# 2. Customized freeipa-client.sh (same server defaults) +sed \ + -e "s|^IPA_DOMAIN=.*|IPA_DOMAIN=\"$IPA_DOMAIN\"|" \ + -e "s|^IPA_REALM=.*|IPA_REALM=\"$IPA_REALM\"|" \ + -e "s|^IPA_SERVER=.*|IPA_SERVER=\"$SERVER_HOSTNAME\"|" \ + "$IPA_BASE/freeipa-client.sh" > "$REAL_OUTDIR/freeipa-client.sh" +chmod +x "$REAL_OUTDIR/freeipa-client.sh" +log "freeipa-client.sh" + +# 3. JSON answerfile (pre-filled; password intentionally left blank) +cat > "$REAL_OUTDIR/freeipa-client-answerfile.json" < \n\n' + printf 'CONTROLLER_URL="${CONTROLLER_URL:-${1:-%s}}"\n' "$AWX_URL" + printf 'API_TOKEN="${API_TOKEN:-${2:-%s}}"\n' "$AWX_TOKEN" + printf 'INVENTORY_NAME="${INVENTORY_NAME:-${3:-%s}}"\n\n' "$AWX_INVENTORY" + # Include body of original (skip shebang + old arg block) + awk '/^INVENTORY_NAME=/{found=1; next} found{print}' \ + "$IPA_BASE/auto-enroll-ansible.sh" + } > "$REAL_OUTDIR/auto-enroll-ansible.sh" + chmod +x "$REAL_OUTDIR/auto-enroll-ansible.sh" + log "auto-enroll-ansible.sh" +fi + +# 5. README +cat > "$REAL_OUTDIR/README.txt" < + +freeipa-enroll.sh + Direct enrollment with server defaults baked in. + Accepts full set of CLI flags for one-liner use: + sudo ./freeipa-enroll.sh --password + +$([ "$SETUP_ANSIBLE" == "true" ] && cat <<'AWXEOF' +auto-enroll-ansible.sh + Registers this client in AWX/Controller inventory after enrollment. + AWX URL and inventory are pre-configured; API token can be overridden: + ./auto-enroll-ansible.sh + API_TOKEN=mytoken ./auto-enroll-ansible.sh + +AWXEOF +)─── Quick start on a client machine ──────────────────────── + + 1. Copy this directory to the client (scp, croc, etc.) + 2. Fill in "password" in freeipa-client-answerfile.json + 3. sudo ./freeipa-client.sh --answerfile freeipa-client-answerfile.json + 4. (optional) ./auto-enroll-ansible.sh + +─── Manual registration after enrollment ─────────────────── + + • List FIDO2 credentials: cat /etc/u2f_mappings + • Register a FIDO2 key: pamu2fcfg -u -o pam://\$(hostname -f) >> /etc/u2f_mappings + • Check SSSD: sssctl domain-status $IPA_DOMAIN + • Get Kerberos ticket: kinit admin@$IPA_REALM + +READMEEOF +log "README.txt" + +# ─── Summary ───────────────────────────────────────────────────────────────── +section "Done" +cat <