368 lines
15 KiB
Text
368 lines
15 KiB
Text
#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://<workbench_ip>:8000/alma10-minimal-bigboy.ks
|
|
#
|
|
# Reference: planning/alma-phase1-install-workflow.md
|
|
#
|
|
# BEFORE USE — fill in:
|
|
# BEFORE USE — fill in:
|
|
# <user-name> — administrator account username (appears 4x: user --name,
|
|
# sudoers filename in the cat> line, inside the sudoers
|
|
# file content, and the chmod line's filename)
|
|
# <sha512-hash> — 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=<user-name> --groups=wheel --shell=/bin/bash --password=<sha512-hash> --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 <user-name> 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-<user-name> << '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.
|
|
<user-name> ALL=(ALL) NOPASSWD: ALL
|
|
EOF
|
|
chmod 0440 /etc/sudoers.d/00-bootstrap-<user-name>
|
|
|
|
# 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'
|
|
<?xml version="1.0" encoding="utf-8"?>
|
|
<zone>
|
|
<short>Public</short>
|
|
<description>Default zone. SSH restricted to home LAN and workbench bench LAN only.</description>
|
|
<rule family="ipv4">
|
|
<source address="192.168.1.0/24"/>
|
|
<service name="ssh"/>
|
|
<accept/>
|
|
</rule>
|
|
<rule family="ipv4">
|
|
<source address="192.168.0.0/24"/>
|
|
<service name="ssh"/>
|
|
<accept/>
|
|
</rule>
|
|
</zone>
|
|
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:
|
|
# [ ] <user-name> and <sha512-hash> 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)
|
|
# ============================================================================
|