Automate RHEL Installation with Kickstart

Tested on RHEL 10.2 (Coughlan) — vm1.lab.example (kickstart server), vm2.lab.example (PXE install target)
Package pykickstart 3.52.12-1.el10
Applies to RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Privilege root on the kickstart server; installer boot access on the target host
Scope Kickstart structure, validation with ksvalidator, anaconda-ks.cfg and template generation, command examples by topic, inst.ks boot parameters, lab harness kickstart file, rhsm registration, %pre/%post timing overview, verification, and kickstart-side troubleshooting. PXE server build and netboot logs are in the PXE boot server guide.
Related guides Configure Kickstart PXE boot server
Kickstart %pre script examples
Kickstart %post script examples
Kickstart clearpart not working
Register RHEL with subscription-manager

Kickstart is how you automate Red Hat’s Anaconda installer: a plain-text answer file lists language, network, disk layout, users, packages, and optional % scripts, and the installer applies those answers without manual clicks. This guide walks through that flow on RHEL 10—file structure, command examples, validation with ksvalidator, generating templates from anaconda-ks.cfg, and a lab harness kickstart exercised through PXE on vm2.

You need a valid Red Hat subscription for CDN installs: an organization ID and activation key from the Activation Keys page. The article uses placeholders (ORG_ID, ACTIVATION_KEY_NAME)—set real values only on vm1, never in git or public URLs. To netboot vm2, wire the PXE server first in Configure Kickstart PXE boot server. Deep %pre and %post catalogs live in dedicated articles linked below.


Lab overview

Host Role Lab address What it does
vm1 Kickstart server 192.168.56.116 Publishes kickstart files under /var/www/html/ks/, PXE stack, local inst.stage2
vm2 Install target 192.168.56.156 Netboots, loads kickstart from vm1, becomes a fresh RHEL 10 system

Two kickstart files share the same PXE server:

File URL Use
rhel10-pxe.cfg http://192.168.56.116/ks/rhel10-pxe.cfg PXE boot server guide — minimal @^minimal-environment install and netboot screenshots
rhel10-lab.cfg http://192.168.56.116/ks/rhel10-lab.cfg This guide — full harness (GUI, bond, ignoredisk, %pre/%post) for validation and satellite articles

Flow for the full harness: vm2 PXE netboots (see PXE guide) with inst.ks=…/rhel10-lab.cfg → Anaconda applies the harness → rhsm registers and pulls packages from the CDN → vm2 reboots with hostname vm2.lab.example and user admin.

IMPORTANT
vm2 is the install target. clearpart on the install disk wipes vm2. Replace ORG_ID and ACTIVATION_KEY_NAME on vm1 only. For rhel10-lab.cfg (@^graphical-server-environment, bond, extra scripts), assign 6 GiB+ RAM to vm2. The minimal rhel10-pxe.cfg in the PXE guide needs 4 GiB+.

What is a Kickstart installation?

Piece Role
Kickstart file Automated answers for locale, network, disk, users, packages, and scripts
inst.ks= Boot parameter that points Anaconda at the kickstart URL or path
repo / url / rhsm Where RPMs install from (mirrors or CDN)
inst.stage2= on kernel line Where installer runtime (install.img) loads during network boot
Fully automated No prompts when every required item is valid and reachable
Semi-automated Kickstart leaves gaps; Anaconda stops at the text menu

Understand kickstart file structure

A kickstart file combines top-level commands (one per line) and script sections introduced with % headers. Each section ends with %end.

Section or block Runs when Typical use
#version=RHEL10 Parse time Selects RHEL 10 grammar for ksvalidator and Anaconda
Top-level commands Throughout install planning Locale, repos, disk layout, users, firewall, reboot
%packages Package selection Environment groups (@^…), RPM names, exclusions (-pkg)
%pre After kickstart load, before partitioning Disk detection, wipe scripts, logging
%pre-install After /mnt/sysimage is mounted, before packages Users/groups with fixed IDs, layout tweaks
%post After packages install (chrooted by default) Configuration, markers, services
%post --nochroot After packages, in installer environment Copy installer logs to target
%addon Addon modules com_redhat_kdump, OpenSCAP profiles

Deep %pre / %post libraries: Kickstart %pre script examples and Kickstart %post script examples.


Kickstart command examples by topic

Reference shapes from Red Hat documentation and lab testing—not every variant is netbooted in the lab. Validate with ksvalidator before production.

text
#version=RHEL10
lang en_US.UTF-8
keyboard us
timezone Asia/Kolkata --utc
eula --agreed

Installation source

For RHEL 10 CDN installs, register during install with rhsm. Do not add conflicting url, repo, or inst.repo= when you rely on CDN-only delivery.

text
rhsm --organization=ORG_ID --activation-key=ACTIVATION_KEY_NAME

Create the activation key on the Activation Keys page. Restrict kickstart file permissions on the server (chmod 640) because it contains registration credentials.

On the PXE kernel line, set inst.stage2= to a local HTTP tree with install.img (see PXE boot server guide). Omit inst.repo= so rhsm controls package sources.

NOTE
Rocky Linux or AlmaLinux: Skip rhsm and point repo lines at public mirrors. Use @^minimal-environment or the group your tree publishes. Stage2 and boot media must match that distribution, not RHEL DVD paths.

Network

Static install NIC by MAC (lab harness—avoids wrong DHCP lease):

text
network --bootproto=static --device=08:00:27:7b:9e:bd --ip=192.168.56.156 --netmask=255.255.255.0 --gateway=192.168.56.116 --nameserver=192.168.56.116,8.8.8.8 --activate
network --bootproto=dhcp --device=eth1 --no-activate
network --hostname=vm2.lab.example

Bond and VLAN with --nodefroute (lab harness—extra NICs do not steal default route):

text
network --device=bond0 --bootproto=static --ip=192.168.56.220 --netmask=255.255.255.0 --bondslaves=eth2,eth3 --bondopts=mode=active-backup,miimon=100 --nodefroute --activate --vlanid=56 --interfacename=bond0.56
network --device=bond0.56 --bootproto=static --ip=192.168.56.221 --netmask=255.255.255.0 --nodefroute --activate

Storage

Scope disks on multi-disk hosts with ignoredisk and clearpart --drives=. See Kickstart clearpart not working when old LVM blocks autopart.

Lab harness (sda install, sdb ignored):

text
ignoredisk --drives=sdb
zerombr
clearpart --all --initlabel --drives=sda
autopart --type=lvm --nohome

Users and authentication

text
authselect select sssd with-fingerprint --force
rootpw --lock
user --name=admin --groups=wheel --password=CHANGEME --plaintext
sshkey --username=admin "ssh-ed25519 AAAA...comment"

Packages

text
%packages --ignoremissing
@^graphical-server-environment
openssh-server
chrony
-vim-enhanced
%end

--ignoremissing avoids interactive prompts when an optional package (for example htop) is absent from entitled repos. Confirm groups on a reference host:

bash
dnf group list --hidden | grep -iE 'minimal|graphical|server'

Bootloader, firewall, and services

text
bootloader --location=mbr --append="crashkernel=auto"
firewall --enabled --ssh
selinux --enforcing
services --enabled=chronyd,sshd
firstboot --disable
reboot

Scripts and addons

text
%pre --log=/tmp/kickstart_pre.log
#!/bin/bash
lsblk >> /tmp/kickstart_pre.log
%end

%post --erroronfail --log=/var/log/kickstart_post.log
echo "kickstart-post-ok" > /root/ks-lab-marker.txt
%end

%addon com_redhat_kdump --enable --reserve-mb=auto
%end

Generate kickstart templates

You do not have to write every line by hand. Start from a template Anaconda or a GUI tool produced, then edit hardware-specific lines and run ksvalidator.

anaconda-ks.cfg after a successful install

After any successful interactive or kickstart install, Anaconda writes a template on the new system:

text
/root/anaconda-ks.cfg

That file reflects the machine it was generated on—disk names, interface names, package set. Edit away hardware-specific lines, validate, and reuse on similar hosts.

On vm2 after the lab run:

bash
sudo ls -l /root/anaconda-ks.cfg

Compare that file to your hand-written /var/www/html/ks/rhel10-lab.cfg on vm1 to see how Anaconda recorded the install.

Scratch files like /root/anaconda-tb-* on a build host come from Image Builder, Cockpit, or incomplete installs—they are not the standard post-install template name.

Legacy kickstart example generator (RHEL/CentOS 8)

The old rhel-centos-8-kickstart-example-generator article is consolidated here. Prefer these sources instead of copying RHEL 7/8 syntax:

Source When to use
#version=RHEL10 + official command reference New files for RHEL 10
/root/anaconda-ks.cfg from a reference host Cloning similar hardware
Cockpit or Image Builder exports Image-mode or custom builds—review for retired commands
system-config-kickstart Legacy GUI on older releases—not available on minimal RHEL 10 server images

Workflow after generating a template:

  1. Set #version=RHEL10 at the top if missing.
  2. Remove or update retired commands (auth, old timezone flags, etc.).
  3. Replace disk and network lines with values for the target hardware.
  4. Run ksvalidator on the edited file.
  5. Test on a disposable VM before production.

Validate with ksvalidator

Syntax checking catches misspelled commands before you netboot:

bash
ksvalidator /var/www/html/ks/rhel10-lab.cfg
output
Checking kickstart file /var/www/html/ks/rhel10-lab.cfg

No stderr means the parser accepted the file. ksvalidator cannot prove disks exist, URLs respond, or package groups are published.


Lab harness kickstart file

vm1 publishes the full harness at http://192.168.56.116/ks/rhel10-lab.cfg. Point inst.ks= at that URL when you test GUI environment, multi-disk ignoredisk, bond/VLAN networking, %pre/%post logging, and CDN install via rhsm. Replace placeholders before netboot.

The minimal kickstart used for PXE walkthrough screenshots (@^minimal-environment, no script sections) is rhel10-pxe.cfg in Configure Kickstart PXE boot server.

text
#version=RHEL10
# GoLinuxCloud lab harness — full PXE test (RHEL 10.2 vm2)
# PXE: bootif (IPAPPEND 2) + net.ifnames=0 -> eth0=lab, eth1=NAT, eth2+eth3=bond slaves
# Disk: sda 20GB LVM autopart; sdb 10GB ignored
# GUI: @^graphical-server-environment — vm2 RAM 6 GiB+ recommended

lang en_US.UTF-8
eula --agreed
keyboard us
timezone Asia/Kolkata --utc
timesource --ntp-server 0.rhel.pool.ntp.org

network --bootproto=static --device=08:00:27:7b:9e:bd --ip=192.168.56.156 --netmask=255.255.255.0 --gateway=192.168.56.116 --nameserver=192.168.56.116,8.8.8.8 --activate
network --bootproto=dhcp --device=eth1 --no-activate
network --device=bond0 --bootproto=static --ip=192.168.56.220 --netmask=255.255.255.0 --bondslaves=eth2,eth3 --bondopts=mode=active-backup,miimon=100 --nodefroute --activate --vlanid=56 --interfacename=bond0.56
network --device=bond0.56 --bootproto=static --ip=192.168.56.221 --netmask=255.255.255.0 --nodefroute --activate
network --hostname=vm2.lab.example

authselect select sssd with-fingerprint --force
rootpw CHANGEME_ROOT --plaintext
user --name=admin --groups=wheel --password=CHANGEME --plaintext
sshkey --username=admin "ssh-ed25519 AAAA...admin-key-comment"
sshkey --username=admin "ssh-ed25519 AAAA...second-key-comment"
sshkey --username=root "ssh-ed25519 AAAA...admin-key-comment"
sshkey --username=root "ssh-ed25519 AAAA...second-key-comment"

ignoredisk --drives=sdb
zerombr
clearpart --all --initlabel --drives=sda
autopart --type=lvm --nohome

bootloader --location=mbr --append="crashkernel=auto"
firewall --enabled --ssh
selinux --enforcing
services --enabled=chronyd,sshd
firstboot --disable

rhsm --organization=ORG_ID --activation-key=ACTIVATION_KEY_NAME
reboot

%packages --ignoremissing
@^graphical-server-environment
htop
vim-enhanced
tree
bind-utils
tmux
bash-completion
%end

%addon com_redhat_kdump --enable --reserve-mb=auto
%end

%pre --log=/tmp/kickstart_pre.log
#!/bin/bash
set -x
RUN_TAG="ks-lab-$(date +%Y%m%d-%H%M%S)"
echo "RUN_TAG=${RUN_TAG}"
echo "=== %pre $(date -Is) ==="
echo "--- lsblk ---"
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
for n in /sys/class/net/*; do
  name=$(basename "$n")
  [[ "$name" == "lo" ]] && continue
  mac=$(cat "$n/address")
  drv=$(basename "$(readlink -f "$n/device/driver" 2>/dev/null)" 2>/dev/null || echo "?")
  echo "NIC ${name} mac=${mac} driver=${drv}"
done
ip -br link
ip -br addr
echo "BOOTIF=${BOOTIF:-unset}"
df -Th
free -m
touch /tmp/pre-no-log-marker
echo "${RUN_TAG}" > /tmp/ks-lab-run-tag
INSTALL_DISK=sda
udevadm settle
if [[ -b /dev/${INSTALL_DISK} ]]; then
  vgchange -an 2>/dev/null || true
  dmsetup remove_all 2>/dev/null || true
  wipefs -af "/dev/${INSTALL_DISK}" || true
  for part in /dev/${INSTALL_DISK}*; do
    [[ -b "$part" ]] && wipefs -af "$part" || true
  done
  lsblk "/dev/${INSTALL_DISK}"
fi
%end

%pre-install --log=/tmp/kickstart_pre_install.log
#!/bin/bash
echo "=== %pre-install $(date -Is) ==="
df -Th
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
mount | grep sysimage || true
%end

%post --erroronfail --log=/var/log/kickstart_post.log
#!/bin/bash
echo "=== %post chroot $(date -Is) ==="
df -Th
hostname -f
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
ip -br addr
nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null || true
echo "kickstart-post-ok" > /root/ks-lab-marker.txt
echo "graphical-server-environment" > /root/ks-lab-environment.txt
echo "bond+bond0.56 vlan" > /root/ks-lab-network.txt
systemctl enable chronyd
systemctl enable gdm
systemctl set-default graphical.target
cat > /etc/ssh/sshd_config.d/01-lab-ssh.conf << 'SSHEOF'
# Lab harness: rootpw plus sshkey for password and key login (not for production)
PermitRootLogin yes
PasswordAuthentication yes
PubkeyAuthentication yes
SSHEOF
cat > /root/ks-lab-verify.sh << 'VERIFY'
#!/bin/bash
echo "=== ks-lab verification $(date -Is) ==="
ls -la /var/log/kickstart* /var/log/pre-no-log-marker 2>/dev/null
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
ip -br addr
nmcli con show 2>/dev/null | grep -E "bond|vlan|NAME" || true
ip -d link show bond0 2>/dev/null || true
cat /root/ks-lab-marker.txt /root/ks-lab-environment.txt /root/ks-lab-network.txt 2>/dev/null
cat /etc/redhat-release
VERIFY
chmod +x /root/ks-lab-verify.sh
%end

%post --nochroot --log=/mnt/sysimage/var/log/kickstart_post_nochroot.log
#!/bin/bash
echo "=== %post --nochroot $(date -Is) ==="
INSTALLER_LOGS="/tmp/anaconda.log /tmp/storage.log /tmp/packaging.log /tmp/program.log /tmp/syslog /tmp/ks-lab-run-tag"
for f in $INSTALLER_LOGS /tmp/kickstart_pre.log /tmp/kickstart_pre_install.log /tmp/pre-no-log-marker /tmp/pre-without-log-marker; do
  [[ -f "$f" ]] && cp -av "$f" /mnt/sysimage/var/log/
done
[[ -d /tmp/anaconda ]] && cp -av /tmp/anaconda /mnt/sysimage/var/log/anaconda-installer-tmp 2>/dev/null || true
df -Th
ip -br link
%end

%post --interpreter=/usr/bin/bash --log=/var/log/kickstart_bash_post.log
echo "bash interpreter %post on $(hostname -f) at $(date -Is)"
%end

%post --interpreter=/usr/libexec/platform-python --log=/var/log/kickstart_python_post.log
print("python interpreter %post at", __import__("datetime").datetime.now().isoformat())
%end

%pre
#!/bin/bash
touch /tmp/pre-without-log-marker
%end

%post --log=/var/log/kickstart_post_no_erroronfail.log
#!/bin/bash
echo "post section without --erroronfail $(date -Is)"
if [[ -f /var/log/pre-no-log-marker ]]; then echo "pre-no-log-marker copied"; fi
if [[ -f /var/log/pre-without-log-marker ]]; then echo "pre-without-log-marker copied"; fi
%end

Full interpreter walkthroughs and extra log-copy patterns: Kickstart %pre script examples and Kickstart %post script examples.

PXE kernel parameters for the full harness (set inst.ks= to rhel10-lab.cfg in PXE boot files; the PXE article default is rhel10-pxe.cfg):

text
inst.ks=http://192.168.56.116/ks/rhel10-lab.cfg inst.text ip=dhcp net.ifnames=0 inst.stage2=http://192.168.56.116/rhel10/BaseOS/x86_64/os/
Parameter Role
inst.ks= Kickstart answer file URL
inst.stage2= Installer runtime (install.img) tree
inst.text Text-mode Anaconda during PXE install only. Does not set the post-install boot target. Use @^graphical-server-environment plus systemctl set-default graphical.target in %post for a GUI login after reboot.
ip=dhcp Early boot DHCP on PXE NIC before kickstart network lines apply
net.ifnames=0 Predictable eth0 naming for bond slaves in the harness

Verify the installed system

After a PXE kickstart run, confirm the install on the target. Netboot and install progress screenshots are in Configure Kickstart PXE boot server.

SSH as the kickstart user:

bash
ssh admin@vm2.lab.example

On the new system, confirm the release:

bash
cat /etc/redhat-release
output
Red Hat Enterprise Linux release 10.2 (Coughlan)

Confirm %post left the lab marker:

bash
test -f /root/ks-lab-marker.txt && cat /root/ks-lab-marker.txt
output
kickstart-post-ok

Run the harness verification script when present:

bash
sudo /root/ks-lab-verify.sh
output
=== ks-lab verification 2026-08-09T21:16:35+05:30 ===
  ├─rhel_vm200-root   16G lvm  xfs         /
  └─rhel_vm200-swap    2G lvm  swap        [SWAP]
sdb                   10G disk
└─sdb1                10G part LVM2_member
eth0             UP             192.168.56.156/24
bond0.56@bond0   UP             192.168.56.220/24
--- markers ---
kickstart-post-ok
graphical-server-environment
bond+bond0.56 vlan
--- release ---
Red Hat Enterprise Linux release 10.2 (Coughlan)

Confirm the GUI default target after first boot:

bash
systemctl get-default
output
graphical.target

Confirm RHSM registration and the GUI environment group:

bash
sudo subscription-manager status
sudo dnf grouplist --installed
output
Overall Status: Registered
Installed Environment Groups:
   Server with GUI

Confirm the system boots to the graphical target (not only multi-user):

bash
systemctl get-default
rpm -q gdm
output
graphical.target
gdm-47.0-22.el10_2.x86_64

rootpw in kickstart sets the root password, but RHEL still blocks root SSH by default. The lab harness adds PermitRootLogin yes in %post. Test with ssh root@vm2.lab.example after install, or log in as admin and use su -.

Compare /root/anaconda-ks.cfg on vm2 with your hand-written file on vm1.

Installer-side logs for failed runs:

text
/tmp/anaconda.log
/tmp/storage.log
/tmp/packaging.log
/tmp/pre.log
/tmp/post.log

On the installed system, copies land under /var/log/ when %post --nochroot copied them.


Review important RHEL 10 kickstart notes

  • Confirm removed commands against the RHEL 10 kickstart documentation—do not copy auth or old timezone syntax from retired guides.
  • Use rhsm for registration instead of hand-written %post when activation keys fit your process.
  • inst.text keeps automated installs predictable on serial or VGA consoles while %packages still installs a GUI environment group.
  • Image-mode (bootc) kickstart options apply only when you intentionally build image-mode systems.

Troubleshoot kickstart installs

Kickstart file and Anaconda

Symptom Likely cause Fix
inst.ks not found HTTP URL wrong or firewall blocks target curl -sI the URL from the lab LAN; fix Apache path and firewalld
ksvalidator passes but install prompts Missing command or unavailable package Fill gaps; use %packages --ignoremissing or remove missing packages
Wrong disk wiped ignoredisk missing or clearpart too broad Scope clearpart --drives=; see clearpart troubleshooting
Not enough free space Old LVM on install disk Aggressive clearpart plus %pre wipefs on the target disk
Repository / CDN error Bad rhsm credentials or inst.repo= overrides CDN Fix org ID and activation key; remove conflicting url/repo/inst.repo=
rhsm Errno -2 (name not known) DNS blocked on PXE server Add dns to firewalld; configure dnsmasq forwarders—see PXE guide
rhsm Errno 101 (network unreachable) Bond or static NIC stole default route --nodefroute on extra interfaces; explicit gateway on install NIC
%pre failed Script exit non-zero Inspect /tmp/pre.log or /tmp/kickstart_pre.log
%post never ran Script error before reboot Check /tmp/post.log; use --erroronfail while testing

PXE and network boot

PXE-specific symptoms (wrong NIC, TFTP failures, Exec format error, stage2 URLs) are covered in Configure Kickstart PXE boot server.


References


Summary

Kickstart automates Anaconda: publish an answer file, point the installer at it with inst.ks=, and on RHEL use rhsm with a valid activation key so packages install from the CDN. Start templates from anaconda-ks.cfg, validate with ksvalidator, and wire PXE delivery through Configure Kickstart PXE boot server.

The lab harness on vm1/vm2 validates GUI environment install, multi-disk ignoredisk, bond networking, and %pre/%post logging. Archive /root/anaconda-ks.cfg from vm2 after a successful run as the baseline for the next iteration.


Frequently Asked Questions

1. What is the difference between inst.ks and inst.repo?

inst.ks tells Anaconda where to load the kickstart answer file. inst.repo or url/repo lines tell the installer where packages live. You need both for a fully unattended network install unless the kickstart file already defines repository URLs or rhsm registers CDN access.

2. Does ksvalidator prove my Kickstart install will succeed?

No. ksvalidator checks syntax and known command shapes. It cannot verify disk names, repository reachability, password policy, or package group availability on the target hardware.

3. When does a %pre script run compared to %post?

%pre runs in the installer environment after the kickstart file is loaded but before partitioning and package installation finish. %post runs after installation completes on the target system, normally inside the installed system root unless you use --nochroot.

4. Can I register RHEL during Kickstart without a %post script?

On RHEL 10 you can use the rhsm kickstart command with an activation key and organization instead of hand-written subscription-manager logic in %post. Keep activation keys out of public kickstart files.

5. Why did my Kickstart install stop and ask interactive questions?

Usually a missing or invalid command, unreachable inst.ks URL, wrong disk selection, unavailable package group, or missing package when %packages lacks --ignoremissing. Check /var/log/anaconda/ on the installer and search the journal for kickstart parser errors.

6. Why is my kickstart install still a text console after @^graphical-server-environment?

inst.text only selects the Anaconda installer UI during PXE boot. Installing the GUI environment group does not switch the default boot target. Add systemctl set-default graphical.target and systemctl enable gdm in %post, then reboot.

7. Why does rootpw not let me SSH as root?

rootpw sets the root password but RHEL still defaults to blocking root SSH login. Add a drop-in under /etc/ssh/sshd_config.d with PermitRootLogin yes in %post if your lab needs direct root SSH. Console login and su - from a wheel user still work with the kickstart rootpw password.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)