From 40856b2e5eddeb753a8f7955b0f756590389f422 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Mon, 27 Jul 2026 07:35:13 +1000 Subject: [PATCH] feat(pxe-boot): add FreeIPA Server (Rocky Linux 9) iPXE menu entry Adds an unattended install option to the PXE boot menu that installs Rocky Linux 9 and configures FreeIPA on the domain-controller.sweet.home host without any operator interaction after selecting the menu entry. How it works: - fetch-rocky-pxeboot.service downloads the Rocky 9 Anaconda pxeboot kernel and initrd from the Rocky mirror on first pxe-boot deploy (idempotent, same pattern as fetch-debian-netboot) - rocky-freeipa.ipxe boots Anaconda with inst.ks pointing at the hosted Kickstart and net.ifnames=0 biosdevname=0 for stable eth0 - rocky-freeipa.ks (generated, includes vars.adminSshKey) performs: - Minimal Rocky 9 install with ipa-server + ipa-server-dns - Static IP 192.168.2.138 via NM connection file written in %post - /etc/hosts fixed for FreeIPA FQDN requirement - Random DM + admin passwords generated and saved to /root/ipa-credentials.txt (chmod 600, never hardcoded) - freeipa-first-boot.service oneshot enabled to run ipa-server-install on the first real boot (~20 min) Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_015Jbvxx4xbHVcx1NkK3vtmK --- docs/pxe-boot.md | 42 ++++++ modules/build-types/pxe-boot.nix | 212 ++++++++++++++++++++++++++++++- 2 files changed, 248 insertions(+), 6 deletions(-) diff --git a/docs/pxe-boot.md b/docs/pxe-boot.md index 4bb3347..bc381af 100644 --- a/docs/pxe-boot.md +++ b/docs/pxe-boot.md @@ -73,6 +73,7 @@ The generated menu currently exposes entries for: - NixOS Auto-Installer - NixOS Minimal - Debian Minimal +- FreeIPA Server (Rocky Linux 9) - SystemRescue environment - iPXE shell - Reboot @@ -126,6 +127,43 @@ systemctl restart fetch-debian-netboot.service To update to a different Debian release, change `debianRelease` in `modules/build-types/pxe-boot.nix` and redeploy. +The **FreeIPA Server (Rocky Linux 9)** entry chains +`http:///rocky-freeipa.ipxe`, which boots the Rocky Linux 9 +Anaconda installer with a Kickstart file (`rocky-freeipa.ks`) hosted on the +same server. The `fetch-rocky-pxeboot.service` oneshot downloads the pxeboot +kernel and initrd from the Rocky Linux mirror on first boot (idempotent): + +```text +/srv/pxe/http/rocky/vmlinuz (Rocky Linux 9 Anaconda pxeboot kernel) +/srv/pxe/http/rocky/initrd.img (Rocky Linux 9 Anaconda pxeboot initrd) +``` + +The Kickstart file is generated from the NixOS module and staged at +`/srv/pxe/http/rocky-freeipa.ks`. It performs a fully unattended install: + +1. Installs Rocky Linux 9 with `ipa-server` + `ipa-server-dns` packages +2. Configures static IP `192.168.2.138`, hostname `domain-controller.sweet.home` +3. Creates user `wayne` with the `adminSshKey` from `variables.nix` +4. Generates random IPA passwords and writes them to `/root/ipa-credentials.txt` +5. Creates a `freeipa-first-boot.service` oneshot that runs `ipa-server-install` + on first reboot (~20 minutes) + +After the install completes: +- SSH in as `wayne@domain-controller` using the admin key +- Monitor FreeIPA install progress: `sudo tail -f /root/freeipa-install.log` +- Retrieve credentials: `sudo cat /root/ipa-credentials.txt` (save to password manager) +- Configure Pi-hole: `server=/sweet.home/192.168.2.138` in dnsmasq + +To refresh the pxeboot files (e.g. after a Rocky point release): + +```bash +rm /srv/pxe/http/rocky/vmlinuz /srv/pxe/http/rocky/initrd.img +systemctl restart fetch-rocky-pxeboot.service +``` + +To update to a different Rocky release, change `rockyRelease` in +`modules/build-types/pxe-boot.nix` and redeploy. + The SystemRescue entry expects the source ISO at: ```text @@ -161,6 +199,10 @@ curl http://pxe-boot/menu.ipxe curl http://pxe-boot/debian.ipxe curl -I http://pxe-boot/debian/linux curl -I http://pxe-boot/debian/initrd.gz +curl http://pxe-boot/rocky-freeipa.ipxe +curl http://pxe-boot/rocky-freeipa.ks +curl -I http://pxe-boot/rocky/vmlinuz +curl -I http://pxe-boot/rocky/initrd.img curl http://pxe-boot/systemrescue.ipxe curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/vmlinuz curl -I http://pxe-boot/systemrescue/sysresccd/boot/x86_64/sysresccd.img diff --git a/modules/build-types/pxe-boot.nix b/modules/build-types/pxe-boot.nix index 52a80dd..1f59723 100644 --- a/modules/build-types/pxe-boot.nix +++ b/modules/build-types/pxe-boot.nix @@ -25,6 +25,11 @@ let debianMirror = "https://deb.debian.org/debian"; debianNetbootBase = "${debianMirror}/dists/${debianRelease}/main/installer-amd64/current/images/netboot/debian-installer/amd64"; + rockyRelease = "9"; + rockyArch = "x86_64"; + rockyMirror = "https://dl.rockylinux.org/pub/rocky/${rockyRelease}"; + rockyPxebootBase = "${rockyMirror}/BaseOS/${rockyArch}/os/images/pxeboot"; + debianIpxe = pkgs.writeText "debian.ipxe" '' #!ipxe @@ -54,6 +59,178 @@ let echo "Debian ${debianRelease} netboot files staged." ''; + # Rocky Linux 9 iPXE script — boots vmlinuz+initrd.img from the staged + # /rocky/ directory and hands Anaconda the hosted Kickstart URL. + # net.ifnames=0 biosdevname=0 ensures the NIC is eth0 in both the + # installer and the installed system (matches the Kickstart NM config). + rockyFreeIpaIpxe = pkgs.writeText "rocky-freeipa.ipxe" '' + #!ipxe + + set base ${pxeBaseUrl} + + kernel ''${base}/rocky/vmlinuz inst.ks=''${base}/rocky-freeipa.ks inst.repo=${rockyMirror}/BaseOS/${rockyArch}/os/ net.ifnames=0 biosdevname=0 ip=dhcp quiet + initrd ''${base}/rocky/initrd.img + boot + ''; + + # Kickstart file for domain-controller.sweet.home. + # Installs Rocky Linux 9, sets a static IP, creates wayne with the + # admin SSH key, then on first reboot runs ipa-server-install via a + # systemd oneshot service. Passwords are generated at %post time, + # written to /root/ipa-credentials.txt (chmod 600), and read back by + # the first-boot script — never hardcoded here or in the repo. + rockyFreeIpaKs = pkgs.writeText "rocky-freeipa.ks" '' + #version=RHEL9 + # Unattended Rocky Linux 9 + FreeIPA install + # Target: domain-controller.sweet.home 192.168.2.138 + + url --url=${rockyMirror}/BaseOS/${rockyArch}/os/ + repo --name=appstream --baseurl=${rockyMirror}/AppStream/${rockyArch}/os/ + + lang en_US.UTF-8 + keyboard us + timezone UTC --utc + + # DHCP during install; static IP configured in %post via NM config file + network --bootproto=dhcp --device=link --activate + network --hostname=domain-controller.sweet.home + + selinux --enforcing + firewall --enabled --service=ssh + + rootpw --lock + user --name=wayne --groups=wheel --shell=/bin/bash + sshkey --username=wayne "${vars.adminSshKey}" + + zerombr + clearpart --all --initlabel --drives=sda + # Keep net.ifnames=0 biosdevname=0 in the installed GRUB so the NIC + # stays eth0 after reboot (matches the NM connection file below). + bootloader --location=mbr --boot-drive=sda --append="net.ifnames=0 biosdevname=0" + + part /boot --fstype=xfs --size=1024 --ondisk=sda + part swap --fstype=swap --size=2048 --ondisk=sda + part / --fstype=xfs --grow --size=1 --ondisk=sda --asprimary + + %packages + @^minimal-environment + ipa-server + ipa-server-dns + %end + + reboot + + %post --log=/root/ks-post.log + set -euo pipefail + + # -- Static IP: write NM connection file directly (NM not running in chroot) -- + mkdir -p /etc/NetworkManager/system-connections + cat > /etc/NetworkManager/system-connections/eth0.nmconnection << 'NMCONN' + [connection] + id=eth0 + type=ethernet + interface-name=eth0 + autoconnect=true + + [ethernet] + + [ipv4] + method=manual + addresses=192.168.2.138/24 + gateway=192.168.2.254 + dns=192.168.2.253; + dns-search=sweet.home; + + [ipv6] + method=auto + NMCONN + chmod 600 /etc/NetworkManager/system-connections/eth0.nmconnection + + # -- /etc/hosts: FQDN must resolve to the real IP (not loopback) for IPA -- + sed -i '/domain-controller/d' /etc/hosts + echo '192.168.2.138 domain-controller.sweet.home domain-controller' >> /etc/hosts + + # -- Generate IPA passwords and store securely -- + DM_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24) + ADMIN_PASS=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 24) + printf 'Directory Manager: %s\nIPA Admin: %s\n' "$DM_PASS" "$ADMIN_PASS" \ + > /root/ipa-credentials.txt + chmod 600 /root/ipa-credentials.txt + + # -- First-boot script: reads passwords back, runs ipa-server-install -- + cat > /usr/local/sbin/freeipa-first-boot.sh << 'FIRSTBOOT' + #!/bin/bash + set -euo pipefail + exec >> /root/freeipa-install.log 2>&1 + echo "=== FreeIPA first-boot install started at $(date) ===" + + DM_PASS=$(grep '^Directory Manager:' /root/ipa-credentials.txt | awk '{print $NF}') + ADMIN_PASS=$(grep '^IPA Admin:' /root/ipa-credentials.txt | awk '{print $NF}') + + ipa-server-install \ + --realm=SWEET.HOME \ + --domain=sweet.home \ + --hostname=domain-controller.sweet.home \ + --ds-password="$DM_PASS" \ + --admin-password="$ADMIN_PASS" \ + --setup-dns \ + --forwarder=192.168.2.253 \ + --no-dnssec-validation \ + --no-ntp \ + --unattended + + echo "=== FreeIPA install complete at $(date) ===" + echo "Credentials: /root/ipa-credentials.txt (save to password manager)" + echo "CA backup: /root/cacert.p12 (encrypted with Directory Manager password)" + systemctl disable freeipa-first-boot.service + FIRSTBOOT + chmod 700 /usr/local/sbin/freeipa-first-boot.sh + + # -- Systemd oneshot service: runs freeipa-first-boot.sh on first real boot -- + cat > /etc/systemd/system/freeipa-first-boot.service << 'UNIT' + [Unit] + Description=FreeIPA first-boot installation + After=network-online.target + Wants=network-online.target + ConditionPathExists=/root/ipa-credentials.txt + + [Service] + Type=oneshot + ExecStart=/usr/local/sbin/freeipa-first-boot.sh + TimeoutStartSec=1800 + RemainAfterExit=yes + + [Install] + WantedBy=multi-user.target + UNIT + + mkdir -p /etc/systemd/system/multi-user.target.wants + ln -sf /etc/systemd/system/freeipa-first-boot.service \ + /etc/systemd/system/multi-user.target.wants/freeipa-first-boot.service + + echo "Kickstart %post complete. FreeIPA installs on first reboot (~20 min)." + %end + ''; + + fetchRockyPxeboot = pkgs.writeShellScript "fetch-rocky-pxeboot" '' + set -eu + + dir="${httpRoot}/rocky" + base="${rockyPxebootBase}" + + if [ -f "$dir/vmlinuz" ] && [ -f "$dir/initrd.img" ]; then + echo "Rocky Linux ${rockyRelease} pxeboot files already present; skipping download." + exit 0 + fi + + echo "Downloading Rocky Linux ${rockyRelease} pxeboot kernel and initrd from $base ..." + ${pkgs.curl}/bin/curl -fsSL -o "$dir/vmlinuz.tmp" "$base/vmlinuz" + ${pkgs.curl}/bin/curl -fsSL -o "$dir/initrd.img.tmp" "$base/initrd.img" + mv "$dir/vmlinuz.tmp" "$dir/vmlinuz" + mv "$dir/initrd.img.tmp" "$dir/initrd.img" + echo "Rocky Linux ${rockyRelease} pxeboot files staged." + ''; + systemRescueIpxe = pkgs.writeText "systemrescue.ipxe" '' #!ipxe @@ -101,12 +278,13 @@ let set base ${pxeBaseUrl} menu PXE Boot Menu - item auto-installer NixOS Auto-Installer - item nixos-minimal NixOS Minimal - item debian Debian Minimal - item rescue Rescue Environment - item shell iPXE Shell - item reboot Reboot + item auto-installer NixOS Auto-Installer + item nixos-minimal NixOS Minimal + item debian Debian Minimal + item rocky-freeipa FreeIPA Server (Rocky Linux 9) + item rescue Rescue Environment + item shell iPXE Shell + item reboot Reboot choose target && goto ''${target} @@ -119,6 +297,9 @@ let :debian chain ''${base}/debian.ipxe + :rocky-freeipa + chain ''${base}/rocky-freeipa.ipxe + :rescue chain ''${base}/systemrescue.ipxe @@ -178,10 +359,13 @@ in "d ${httpRoot}/debian 0755 root root -" "d ${httpRoot}/ubuntu 0755 root root -" "d ${httpRoot}/rescue 0755 root root -" + "d ${httpRoot}/rocky 0755 root root -" "d ${tftpRoot} 0755 root root -" "C+ ${httpRoot}/boot.ipxe 0644 root root - ${bootIpxe}" "C+ ${httpRoot}/menu.ipxe 0644 root root - ${menuIpxe}" "C+ ${httpRoot}/debian.ipxe 0644 root root - ${debianIpxe}" + "C+ ${httpRoot}/rocky-freeipa.ipxe 0644 root root - ${rockyFreeIpaIpxe}" + "C+ ${httpRoot}/rocky-freeipa.ks 0644 root root - ${rockyFreeIpaKs}" "C+ ${httpRoot}/systemrescue.ipxe 0644 root root - ${systemRescueIpxe}" "C+ ${tftpRoot}/autoexec.ipxe 0644 root root - ${autoexecIpxe}" "C+ ${tftpRoot}/ipxe.efi 0644 root root - ${pkgs.ipxe}/ipxe.efi" @@ -205,6 +389,22 @@ in }; }; + fetch-rocky-pxeboot = { + description = "Download Rocky Linux ${rockyRelease} pxeboot kernel and initrd for HTTP PXE boot"; + after = [ + "local-fs.target" + "systemd-tmpfiles-setup.service" + "network-online.target" + ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = fetchRockyPxeboot; + RemainAfterExit = true; + }; + }; + stage-systemrescue = { description = "Stage SystemRescue ISO contents for HTTP PXE boot"; after = [