#version=DEVEL # AlmaLinux 10.2 Unattended Installation Kickstart — BigBoy Sovereign AI Server # Phase 1 (Base OS Installation & Foundation) # 5-drive architecture: 1x NVMe (OS) + 4x SATA (data) # # Usage: # USB/OEMDRV: label a second USB "OEMDRV", place this file at its root as ks.cfg # HTTP: at boot, edit kernel line: inst.ks=http://:8000/alma10-minimal-bigboy.ks # # Reference: planning/alma-phase1-install-workflow.md # # BEFORE USE — fill in: # BEFORE USE — fill in: # — administrator account username (appears 4x: user --name, # sudoers filename in the cat> line, inside the sudoers # file content, and the chmod line's filename) # — output of `openssl passwd -6` (never a plaintext password) # # Disk targeting uses /dev/disk/by-id, not /dev/sdX — sdX assignment isn't # guaranteed stable across boots. by-id values confirmed against drive # labels (BACKUPDRV/RAGLIB/AILOGS/PROMPTLIB) via blkid on 2026-07-16. # # ============================================================================ text firstboot --disable keyboard --xlayouts='us' lang en_US.UTF-8 timezone Europe/Rome --utc network --bootproto=dhcp --device=link --activate --hostname=bigboy --noipv6 # ============================================================================ # SECURITY & AUTHENTICATION # ============================================================================ rootpw --lock # Administrator account. Password auth enabled (hash only — never plaintext, # this file is committed to git). Phase 9 disables PasswordAuthentication # and moves to key-only. # WARNING: the line below MUST remain a single unbroken line, however long # it looks in your editor. Kickstart has no line-continuation syntax — if # an editor or copy/paste wraps the hash onto a second line, pykickstart # reads the wrapped portion as a separate, unrecognized top-level command # ("Unknown command: --iscrypted" is exactly this failure mode). user --name= --groups=wheel --shell=/bin/bash --password= --iscrypted # Permissive during bring-up; Phase 9 sets Enforcing. selinux --disabled # Enabled from first boot (required since password auth is active — see # %post below for the SSH-only rich rules that scope this). firewall --enabled # ============================================================================ # REPOSITORY CONFIGURATION # ============================================================================ url --url="https://repo.almalinux.org/almalinux/10/BaseOS/x86_64/kickstart/" repo --name="almalinux10-appstream" --mirrorlist="https://mirrors.almalinux.org/mirrorlist/10/appstream" repo --name="almalinux10-crb" --mirrorlist="https://mirrors.almalinux.org/mirrorlist/10/crb/" repo --name="epel10" --mirrorlist="https://mirrors.fedoraproject.org/mirrorlist?repo=epel-10&arch=x86_64" # ============================================================================ # BOOTLOADER # ============================================================================ bootloader --location=mbr --boot-drive=/dev/disk/by-id/nvme-Samsung_SSD_980_500GB_S64DNL0TC07019Z # ============================================================================ # DISK PARTITIONING # ============================================================================ # # NVMe (500GB): EFI (1GB) + xfs OS (~499GB) # SATA x4: single xfs partition each, mounted by Phase 3 Ansible # BACKUPDRV=Lexar ...1730S340, RAGLIB=Samsung EVO 500GB, AILOGS=Samsung EVO # 250GB, PROMPTLIB=Lexar ...0370S340 # # NOTE: fstype is xfs, not btrfs. RHEL10's kickstart removed btrfs support # entirely from the part/logvol/autopart commands and retired the dedicated # btrfs command — Anaconda cannot create btrfs volumes at install time on # this OS version. Phase 3 Ansible reformats these to real btrfs # (subvolumes, compression) post-install; this was already the plan for # root regardless, this just extends the same pattern to the SATA drives. clearpart --none --initlabel # All five drives were fully zeroed (dd if=/dev/zero) on 2026-07-16 — no # partition table exists on any of them. clearpart --initlabel is # documented as only meaningful combined with --all; on a genuinely # blank disk with no recognizable label, Anaconda can otherwise prompt # interactively to ask whether to initialize it, which would stall an # unattended install. zerombr removes that prompt for all such disks. zerombr part /boot/efi --fstype=efi --size=1024 --ondrive=/dev/disk/by-id/nvme-Samsung_SSD_980_500GB_S64DNL0TC07019Z part / --fstype=xfs --size=1 --grow --ondrive=/dev/disk/by-id/nvme-Samsung_SSD_980_500GB_S64DNL0TC07019Z part /srv/backup-raw --fstype=xfs --size=1 --grow --ondrive=/dev/disk/by-id/ata-Lexar_SSD_NS100_256GB_QM9370R0291730S340 part /srv/rag-raw --fstype=xfs --size=1 --grow --ondrive=/dev/disk/by-id/ata-Samsung_SSD_870_EVO_500GB_S6PYNL0T620757H part /srv/ai-raw --fstype=xfs --size=1 --grow --ondrive=/dev/disk/by-id/ata-Samsung_SSD_870_EVO_250GB_S61WNJ0R458541M part /srv/prompt-raw --fstype=xfs --size=1 --grow --ondrive=/dev/disk/by-id/ata-Lexar_SSD_NS100_256GB_QM9370R0290370S340 # ============================================================================ # PACKAGE SELECTION # ============================================================================ %packages @core # Kernel + headers (Phase 4 GPU driver compilation) kernel kernel-devel kernel-headers # UEFI bootloader grub2-efi-x64 shim-x64 efibootmgr # Build essentials (Phase 4 NVIDIA driver) gcc make patch perl # Utilities curl wget git tmux htop # vim intentionally omitted — hit a repo mirror inconsistency # (vim-enhanced -> vim-common -> vim-data, "nothing provides vim-data") # during install on 2026-07-17. @core already includes vim-minimal; # install full vim post-install via dnf once on the network normally. # SSH + sudo openssh-clients openssh-server sudo # Storage/monitoring (Phase 3, Phase 11) btrfs-progs smartmontools util-linux # Baseline utilities confirmed missing during Phase 5 (2026-07-19) — neither # tar nor which are part of coreutils, and this minimal install's original # package list didn't request either explicitly. tar which # Full utility-recon.sh scan run 2026-07-19 — these nine were the complete # remaining gap against a reasonable admin/dev baseline. bc bind-utils bzip2 lsof nmap-ncat rsync strace traceroute tree # vim-enhanced was originally dropped due to a cross-repo vim-data mismatch # during the initial install — re-added since it installs cleanly once on # the live network. vim-enhanced -kde-l10n-* -kde-l10n-common -network-manager-applet -nm-connection-editor %end services --enabled=sshd,NetworkManager --disabled=avahi-daemon # ============================================================================ # POST-INSTALL # ============================================================================ %post --log=/root/anaconda-post.log #!/bin/bash mkdir -p /srv/deployment-log chmod 0755 /srv/deployment-log { echo "=== Kickstart Installation Completed ===" echo "Timestamp: $(date -Iseconds)" echo "Hostname: $(hostname)" echo "Kernel: $(uname -r)" echo "AlmaLinux version: $(cat /etc/almalinux-release)" echo "" echo "Installed packages:" rpm -qa | wc -l echo "" echo "Disk layout:" lsblk echo "" echo "Network configuration:" ip addr show enp4s0 echo "" echo "Repositories:" dnf repolist } >> /srv/deployment-log/kickstart.log 2>&1 # Blacklist nouveau before Phase 4 NVIDIA driver install cat >> /etc/modprobe.d/blacklist-nouveau.conf << 'EOF' blacklist nouveau options nouveau modeset=0 EOF dracut --force 2>&1 >> /srv/deployment-log/kickstart.log # ============================================================================ # TEMPORARY PASSWORDLESS SUDO — READ BEFORE ASSUMING THIS IS A BUG # ============================================================================ # # This grants full NOPASSWD sudo, with no restriction on which # commands. That is intentionally broad, and intentionally temporary — not # an oversight and not a permanent security posture for this machine. # # WHY IT EXISTS: Phase 2 (Ansible) is what establishes this box's real, # permanent sudoers policy. But Phase 2 itself needs privileged access to # do that configuration in the first place — and immediately after first # boot, there is no other path to privilege escalation at all (root is # locked via rootpw --lock above, and no sudoers policy exists yet). This # bootstrap grant is the only way to close that chicken-and-egg gap. # # WHAT MUST HAPPEN NEXT: Phase 2 must replace this file with a properly # scoped sudoers policy before this system is considered done bootstrapping # — not layer on top of it, not leave it in place indefinitely. If you are # reading this file's content on a running system, confirm this file has # actually been superseded (check /etc/sudoers.d/ for what Phase 2 left # behind) rather than assuming Phase 2 handled it just because it ran. cat > /etc/sudoers.d/00-bootstrap- << 'EOF' # Temporary bootstrap exception — superseded by Phase 2 Ansible sudoers policy. # If you are reading this after Phase 2 has run, something did not get # cleaned up correctly; check ansible/roles/phase-2-*/ for the real policy. ALL=(ALL) NOPASSWD: ALL EOF chmod 0440 /etc/sudoers.d/00-bootstrap- # Explicit password auth (Phase 9 sets to 'no' once key-based login works) sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config systemctl enable sshd # NOTE: no systemctl start/restart here — systemd isn't running inside # the kickstart %post chroot, so start/restart are silently ignored # ("Running in chroot, ignoring request"). enable works fine (it's just # a symlink), and sshd will actually start normally on first real boot. { echo "SSH daemon enabled, password auth explicitly set in sshd_config" systemctl is-enabled sshd } >> /srv/deployment-log/kickstart.log 2>&1 # Restrict SSH to LAN ranges only (home LAN + workbench bench LAN) # # NOTE: firewall-cmd is NOT used here deliberately. firewalld is not # running inside the kickstart %post chroot, and firewall-cmd requires # a live D-Bus connection to the daemon — it fails with "FirewallD is # not running" in this exact context, silently leaving these rules # never applied. Writing the zone XML directly is the reliable # mechanism instead: no daemon needed, and firewalld reads this file # normally the first time it actually starts after reboot. # /etc/firewalld/zones/ takes precedence over the packaged default at # /usr/lib/firewalld/zones/ for a same-named zone. mkdir -p /etc/firewalld/zones cat > /etc/firewalld/zones/public.xml << 'EOF' Public Default zone. SSH restricted to home LAN and workbench bench LAN only. EOF { echo "Firewalld zone file written: SSH restricted to 192.168.1.0/24, 192.168.0.0/24" } >> /srv/deployment-log/kickstart.log 2>&1 { echo "" echo "=== Kickstart Post-Installation Complete ===" echo "Timestamp: $(date -Iseconds)" echo "Next phase: Ansible Phase 2 (System Configuration)" } >> /srv/deployment-log/kickstart.log 2>&1 exit 0 %end %addon com_redhat_kdump --disable %end reboot --eject # ============================================================================ # Pre-boot checklist: # [ ] and replaced (4 occurrences of the # former — user line, sudoers cat> line, sudoers content, chmod line) # [ ] The user line (search "iscrypted") is one unbroken line — no wrap # [ ] by-id paths still match — re-run `ls -la /dev/disk/by-id/` if any # drive has been physically moved/replaced since 2026-07-16 # [ ] Network interface (enp4s0), hostname, timezone correct # [ ] Repos reachable; real outbound internet confirmed (not just bench LAN) # # Issues found and fixed during 2026-07-17 install attempts (for reference — # if a *new*, different error appears, it is likely unrelated to these): # 1. %anaconda section — not recognized by this Anaconda version, removed # 2. bootloader --location=partition — GRUB2 doesn't support this, # changed to --location=mbr (correct even on UEFI/GPT) # 3. --iscrypted "Unknown command" — the user line had wrapped across # two lines; kickstart has no line-continuation syntax # 4. --ipv6=off — not a valid value ('off' isn't an IPv6 address); # changed to --noipv6 # 5. --fstype=btrfs on all partitions — RHEL10 kickstart removed btrfs # support from part/logvol/autopart and retired the btrfs command # entirely; changed to --fstype=xfs, Phase 3 Ansible converts to real # btrfs post-install # 6. vim-enhanced dependency chain unresolvable ("nothing provides # vim-data") — repo mirror inconsistency between BaseOS/AppStream; # dropped vim from %packages, @core's vim-minimal suffices for now # # Found on deeper review before a fresh attempt (not yet hit live, but # confirmed against pykickstart docs and known chroot behavior): # 7. firewall-cmd in %post cannot work — firewalld isn't running in # the kickstart chroot, so --permanent calls silently fail # ("FirewallD is not running"). Replaced with a direct write of # /etc/firewalld/zones/public.xml, which needs no running daemon. # 8. systemctl start/restart sshd in %post are no-ops in a chroot # (systemd isn't running there either) — removed; enable alone is # sufficient and reliable, sshd starts normally on first real boot. # 9. Considered adding a second, mirrorlist-based BaseOS repo entry to # match the other three (rather than the single static --url). On # reflection, reverted this — it would create a second source # serving the same package set as the existing --url source, risking # a genuine duplicate/conflicting-provider error, without actually # addressing the real cause (BaseOS and AppStream are always # separate repos, and can legitimately sync errata at different # moments regardless of how either is selected). The vim fix (#6) # remains the correct, sufficient fix for the error actually hit. # 10. zerombr added — the five drives are genuinely blank (dd-zeroed), # and clearpart --initlabel alone is only documented to matter # with --all, not --none; zerombr prevents a possible interactive # "initialize this disk?" prompt that would stall the unattended # install. # # Temporary states, both closed by Phase 9: # 1. Password-based SSH (-> key-only) # 2. Blanket NOPASSWD sudo bootstrap (-> Phase 2's real sudoers policy) # # Next: Phase 2 (System Configuration & Secondary Drive Preparation) # ============================================================================