Archived
feat(freeipa): add domain-controller setup — Rocky Linux 9 + FreeIPA 4.13
Installs and documents the FreeIPA identity management server at domain-controller.sweet.home (VMID 108, pve1). Provides Kerberos, LDAP, and integrated DNS for the SWEET.HOME realm. New section: freeipa/ - docs/install.md: full step-by-step reproduction procedure including Proxmox VM prep (Rocky Linux 9 GenericCloud, SeaBIOS, cloud-init), swap setup, static IP, /etc/hosts fix, ipa-server-install flags - docs/pihole-dns.md: how to configure Pi-hole to forward sweet.home queries to the FreeIPA BIND instance - scripts/install.sh: idempotent install script with pre-flight checks; reads passwords from env or interactive prompt (never commits them) - scripts/configure-pihole-dns.sh: idempotent Pi-hole forwarder setup - scripts/verify.sh: read-only health check (13 checks, 0 side effects) - CLAUDE.md: host guardrails for domain-controller Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Jbvxx4xbHVcx1NkK3vtmK
This commit is contained in:
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Configure Pi-hole to forward sweet.home DNS queries to FreeIPA.
|
||||
#
|
||||
# Usage:
|
||||
# bash configure-pihole-dns.sh <pihole-host>
|
||||
# bash configure-pihole-dns.sh pihole.sweet.home
|
||||
#
|
||||
# Idempotent — safe to run multiple times.
|
||||
# Requires SSH access to the Pi-hole host as a user with sudo.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PIHOLE_HOST="${1:-}"
|
||||
IPA_IP="192.168.2.138"
|
||||
IPA_DOMAIN="sweet.home"
|
||||
DNSMASQ_CONF="/etc/dnsmasq.d/10-ipa-${IPA_DOMAIN//./-}.conf"
|
||||
DIRECTIVE="server=/${IPA_DOMAIN}/${IPA_IP}"
|
||||
|
||||
if [[ -z "$PIHOLE_HOST" ]]; then
|
||||
echo "Usage: $0 <pihole-host>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Configuring Pi-hole at $PIHOLE_HOST to forward $IPA_DOMAIN → $IPA_IP..."
|
||||
|
||||
ssh "$PIHOLE_HOST" "
|
||||
set -euo pipefail
|
||||
|
||||
if grep -qF '${DIRECTIVE}' '${DNSMASQ_CONF}' 2>/dev/null; then
|
||||
echo '==> Forwarder already configured, skipping write.'
|
||||
else
|
||||
echo '${DIRECTIVE}' | sudo tee '${DNSMASQ_CONF}'
|
||||
echo '==> Written to ${DNSMASQ_CONF}'
|
||||
fi
|
||||
|
||||
echo '==> Restarting Pi-hole DNS...'
|
||||
sudo pihole restartdns
|
||||
|
||||
echo '==> Verifying SRV record resolution via Pi-hole...'
|
||||
sleep 2
|
||||
dig +short _kerberos._udp.${IPA_DOMAIN} SRV @127.0.0.1 || true
|
||||
"
|
||||
|
||||
echo "==> Done. Pi-hole now forwards ${IPA_DOMAIN} queries to ${IPA_IP}."
|
||||
echo " Verify from a LAN client:"
|
||||
echo " dig +short _kerberos._udp.${IPA_DOMAIN} SRV @${PIHOLE_HOST}"
|
||||
Executable
+130
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env bash
|
||||
# FreeIPA server install script for domain-controller.sweet.home
|
||||
#
|
||||
# Run this on a fresh Rocky Linux 9 VM that already has:
|
||||
# - Correct hostname: domain-controller.sweet.home
|
||||
# - Static IP: 192.168.2.138/24
|
||||
# - Gateway: 192.168.2.254
|
||||
# - DNS: 192.168.2.253 (Pi-hole)
|
||||
# - sudo access for the current user
|
||||
#
|
||||
# The script will prompt for passwords if not set via environment:
|
||||
# IPA_DM_PASSWORD Directory Manager password (store in password manager)
|
||||
# IPA_ADMIN_PASSWORD IPA admin Kerberos password (store in password manager)
|
||||
#
|
||||
# Full procedure: see docs/install.md
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IPA_REALM="SWEET.HOME"
|
||||
IPA_DOMAIN="sweet.home"
|
||||
IPA_HOSTNAME="domain-controller.sweet.home"
|
||||
IPA_IP="192.168.2.138"
|
||||
IPA_DNS_FORWARDER="192.168.2.253"
|
||||
|
||||
# ── Password handling ────────────────────────────────────────────────────────
|
||||
|
||||
if [[ -z "${IPA_DM_PASSWORD:-}" ]]; then
|
||||
read -r -s -p "Directory Manager password (min 8 chars): " IPA_DM_PASSWORD
|
||||
echo
|
||||
fi
|
||||
if [[ -z "${IPA_ADMIN_PASSWORD:-}" ]]; then
|
||||
read -r -s -p "IPA admin password (min 8 chars): " IPA_ADMIN_PASSWORD
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ${#IPA_DM_PASSWORD} -lt 8 || ${#IPA_ADMIN_PASSWORD} -lt 8 ]]; then
|
||||
echo "ERROR: passwords must be at least 8 characters" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Pre-flight checks ────────────────────────────────────────────────────────
|
||||
|
||||
echo "==> Checking hostname..."
|
||||
actual_fqdn=$(hostname -f)
|
||||
if [[ "$actual_fqdn" != "$IPA_HOSTNAME" ]]; then
|
||||
echo "ERROR: hostname -f returned '$actual_fqdn', expected '$IPA_HOSTNAME'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Checking /etc/hosts entry..."
|
||||
if ! grep -q "$IPA_IP $IPA_HOSTNAME" /etc/hosts; then
|
||||
echo "ERROR: /etc/hosts does not have '$IPA_IP $IPA_HOSTNAME'" >&2
|
||||
echo "Fix: sudo sed -i '/$IPA_HOSTNAME/d' /etc/hosts && echo '$IPA_IP $IPA_HOSTNAME ${IPA_HOSTNAME%%.*}' | sudo tee -a /etc/hosts" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Checking Python FQDN resolution..."
|
||||
resolved=$(python3 -c "import socket; print(socket.gethostbyname('$IPA_HOSTNAME'))" 2>/dev/null || true)
|
||||
if [[ "$resolved" != "$IPA_IP" ]]; then
|
||||
echo "ERROR: $IPA_HOSTNAME resolves to '$resolved', expected '$IPA_IP'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Checking internet connectivity..."
|
||||
if ! ping -c1 -W5 8.8.8.8 >/dev/null 2>&1; then
|
||||
echo "ERROR: no internet connectivity (needed for package install)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Swap ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
if ! swapon --show | grep -q .; then
|
||||
echo "==> Creating 2 GB swap file (FreeIPA needs headroom)..."
|
||||
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile
|
||||
sudo swapon /swapfile
|
||||
grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap defaults 0 0' | sudo tee -a /etc/fstab
|
||||
else
|
||||
echo "==> Swap already configured, skipping."
|
||||
fi
|
||||
|
||||
# ── Packages ─────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "==> Installing FreeIPA server packages..."
|
||||
sudo dnf install -y ipa-server ipa-server-dns
|
||||
|
||||
# ── Install ──────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "==> Running ipa-server-install (15–20 min)..."
|
||||
sudo ipa-server-install \
|
||||
--realm="$IPA_REALM" \
|
||||
--domain="$IPA_DOMAIN" \
|
||||
--hostname="$IPA_HOSTNAME" \
|
||||
--ds-password="$IPA_DM_PASSWORD" \
|
||||
--admin-password="$IPA_ADMIN_PASSWORD" \
|
||||
--setup-dns \
|
||||
--forwarder="$IPA_DNS_FORWARDER" \
|
||||
--no-dnssec-validation \
|
||||
--no-ntp \
|
||||
--unattended
|
||||
|
||||
# ── Verify ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "==> Verifying services..."
|
||||
sudo ipactl status
|
||||
|
||||
echo "==> Verifying Kerberos ticket..."
|
||||
echo "$IPA_ADMIN_PASSWORD" | kinit admin
|
||||
klist
|
||||
|
||||
echo "==> Verifying DNS SRV records..."
|
||||
dig +short _kerberos._udp."$IPA_DOMAIN" SRV @127.0.0.1
|
||||
|
||||
echo ""
|
||||
echo "======================================================================"
|
||||
echo "Setup complete. Next steps:"
|
||||
echo ""
|
||||
echo " 1. Save passwords to your password manager (if not already done)."
|
||||
echo ""
|
||||
echo " 2. Configure Pi-hole to forward $IPA_DOMAIN DNS to $IPA_IP:"
|
||||
echo " bash scripts/configure-pihole-dns.sh <pihole-host>"
|
||||
echo ""
|
||||
echo " 3. Back up the CA certificates:"
|
||||
echo " scp root@$IPA_HOSTNAME:/root/cacert.p12 ~/backups/ipa-cacert.p12"
|
||||
echo " (Encrypted with the Directory Manager password)"
|
||||
echo ""
|
||||
echo " 4. Access the Web UI at:"
|
||||
echo " https://$IPA_HOSTNAME/ipa/ui/"
|
||||
echo "======================================================================"
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Read-only health check for the FreeIPA server.
|
||||
# Run locally on domain-controller or remotely:
|
||||
# ssh wayne@domain-controller 'bash -s' < scripts/verify.sh
|
||||
#
|
||||
# Exit code 0 = all checks passed, non-zero = something is wrong.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
local label="$1"
|
||||
shift
|
||||
if "$@" >/dev/null 2>&1; then
|
||||
echo " OK $label"
|
||||
(( PASS++ )) || true
|
||||
else
|
||||
echo "FAIL $label"
|
||||
(( FAIL++ )) || true
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== FreeIPA health check: $(hostname -f) ==="
|
||||
echo ""
|
||||
|
||||
echo "--- Services ---"
|
||||
check "ipactl status" sudo ipactl status
|
||||
check "dirsrv running" systemctl is-active dirsrv.target
|
||||
check "krb5kdc running" systemctl is-active krb5kdc
|
||||
check "named running" systemctl is-active named
|
||||
check "httpd running" systemctl is-active httpd
|
||||
check "pki-tomcatd running" systemctl is-active pki-tomcatd.target
|
||||
|
||||
echo ""
|
||||
echo "--- DNS ---"
|
||||
check "A record: domain-controller.sweet.home" dig +short domain-controller.sweet.home A @127.0.0.1
|
||||
check "SRV: _kerberos._udp.sweet.home" dig +short _kerberos._udp.sweet.home SRV @127.0.0.1
|
||||
check "SRV: _ldap._tcp.sweet.home" dig +short _ldap._tcp.sweet.home SRV @127.0.0.1
|
||||
check "TXT: _kerberos.sweet.home" dig +short _kerberos.sweet.home TXT @127.0.0.1
|
||||
|
||||
echo ""
|
||||
echo "--- LDAP ---"
|
||||
check "LDAP port 389 open" bash -c "exec 3<>/dev/tcp/127.0.0.1/389"
|
||||
check "LDAPS port 636 open" bash -c "exec 3<>/dev/tcp/127.0.0.1/636"
|
||||
|
||||
echo ""
|
||||
echo "--- Kerberos ---"
|
||||
check "KDC port 88 open" bash -c "exec 3<>/dev/tcp/127.0.0.1/88"
|
||||
|
||||
echo ""
|
||||
echo "--- HTTP ---"
|
||||
check "IPA HTTP redirect" curl -sk -o /dev/null -w "%{http_code}" http://localhost/ | grep -qE "^(301|302|200)"
|
||||
check "IPA HTTPS UI" curl -sk -o /dev/null -w "%{http_code}" https://localhost/ipa/ui/ | grep -q "200"
|
||||
|
||||
echo ""
|
||||
if [[ $FAIL -eq 0 ]]; then
|
||||
echo "All $PASS checks passed."
|
||||
else
|
||||
echo "$FAIL check(s) FAILED, $PASS passed."
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user