#!/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 <