← All TILs · proxmox

A libvirt RHEL Kickstart example ported to Proxmox, minus the one flag with no equivalent

proxmox - 2026-09-20

flozanorht/kickstart is a small, real, MIT-licensed pair of Kickstart files and virt-install scripts written to accompany a Red Hat Developer article on libvirt. Read directly from the repo rather than described secondhand, both variants (boot-iso/ — a minimal netinstall image — and dvd-iso/ — the full install DVD) do the same job: an unattended RHEL 9 install, an unprivileged sudo user with an SSH key instead of root login, and a locally-built httpd serving a demo page. Almost every line of it is exactly as portable to Proxmox as it is to libvirt — Kickstart is an Anaconda concept, not a hypervisor one — except the one mechanism that actually gets the kickstart file in front of the installer, which is virt-install-specific down to the flag.

What's genuinely hypervisor-agnostic in the file itself

rootpw --lock
useradd -g wheel core
echo "core:redhat123" | chpasswd
mkdir /home/core/.ssh
cat > /home/core/.ssh/authorized_keys << EOFSSH
REPLACE_WITH_SSH_PUB_KEY
EOFSSH

Locking root outright and creating a wheel-group user with a key instead isn't just a stylistic choice — it's the recommended shape. rootpw's own option reference documents a --allow-ssh flag that would let root log in over SSH with only the locked password, and describes it as something to "only use as a last resort." The source repo never reaches for it. REPLACE_WITH_SSH_PUB_KEY is a literal placeholder, not a templating variable — filling it in for real means substituting it before the file is served, e.g. sed -i "s|REPLACE_WITH_SSH_PUB_KEY|$(cat ~/.ssh/id_ed25519.pub)|" boot.ks.

rhsm --organization REPLACE_WITH_ORG_ID --activation-key REPLACE_WITH_ACTIVATION_KEY

Only in boot.ks — the minimal netinstall image has no packages of its own, so it needs a subscribed content source before %packages can pull httpd. Per the rhsm command's own docs, added in RHEL 8's kickstart syntax, activation-key registration needs both an organization ID and at least one key — dvd.ks skips this entirely because the full DVD carries its own package set and only declares cdrom as the install source.

systemctl enable httpd.socket

Not httpd.service — this is real, current, and worth knowing precisely rather than assuming it's a typo. Fedora/RHEL's own shipped httpd.socket unit listens on port 80 itself (WantedBy=sockets.target) and only starts httpd.service on the first actual connection — socket activation, not eager startup.

The one thing that's virt-install-specific, and what it's actually doing

virt-install --name rhel-boot --os-variant rhel9.5 \
  --location ~/Downloads/rhel-9.6-x86_64-boot.iso \
  --initrd-inject ./boot.ks \
  --extra-args "console=ttyS0 inst.ks=file:/boot.ks"

--location on an ISO makes virt-install mount it, pull the installer's own kernel and initrd out of it, unpack the initrd, drop boot.ks in at the root (--initrd-inject), repack it, and boot QEMU directly from that modified kernel/initrd pair with console=ttyS0 inst.ks=file:/boot.ks on the command line — all of that from one flag. It's a genuine convenience, and it's also entirely a virt-install behavior, not a libvirt or QEMU one: nothing about kernel/initrd extraction-and-boot is exclusive to libvirt, but nothing in Proxmox wraps it in a single flag either.

Proxmox's qm has no --location/--initrd-inject equivalent — no dedicated kernel/initrd config keys at all, confirmed by their absence from the full qm configuration reference. What it does have is args, documented plainly as "Arbitrary arguments passed to kvm... this option is for experts only" — a generic passthrough for exactly the kind of raw QEMU flag Proxmox's own config keys don't wrap. That's a real, if manual, path to the identical mechanism virt-install automates: extract vmlinuz/initrd.img from the ISO's own images/pxeboot/ directory onto the Proxmox host, repack the initrd with the kickstart file added the same way virt-install does it, and set:

args: -kernel /var/lib/vz/template/iso/rhel9-vmlinuz -initrd /var/lib/vz/template/iso/rhel9-initrd-with-ks.img -append "console=ttyS0 inst.ks=file:/boot.ks"

No convenience flag does this in one step on Proxmox the way --initrd-inject does on libvirt — the extraction and repacking are the reader's own to script.

The simpler alternative, if repacking an initrd is more than the job needs

The kernel argument doesn't have to point at a file baked into the initrd at all. Per pykickstart's own boot-argument reference:

inst.ks=http://<server>/<path>

"The installation program will look for the kickstart file on the HTTP server... The installation program will use DHCP to configure the Ethernet card." Serve the filled-in boot.ks from anything that can run a web server on the same network the VM's virtual NIC reaches — python3 -m http.server on the Proxmox host itself is enough for a lab — and the same args: ... -append "inst.ks=http://..." line above no longer needs a modified initrd, only the stock kernel/initrd extracted straight from the ISO with nothing injected into them. Getting that argument onto the command line still needs args: (or, per pykickstart's own kickstart-boot-CD-ROM documentation, copying ks.cfg into the ISO's own isolinux/ directory before it's attached — a longer-standing, Red-Hat-documented alternative for the boot-media case specifically, not verified here to the same depth as the two paths above).

flowchart TD
    ISO[RHEL boot/DVD ISO attached to the VM] --> METHOD{How does inst.ks=<br/>reach the kernel cmdline?}
    METHOD -->|virt-install| AUTO["--initrd-inject + --extra-args<br/>-- one flag, libvirt-only"]
    METHOD -->|Proxmox| MANUAL["args: -kernel ... -initrd ... -append<br/>-- same mechanism, built by hand"]
    METHOD -->|Either, simpler| HTTP["inst.ks=http://server/boot.ks<br/>-- no initrd surgery, still needs args: to set -append"]
    AUTO --> ANACONDA[Anaconda reads inst.ks, installs unattended]
    MANUAL --> ANACONDA
    HTTP --> ANACONDA

Kickstart versus what this site's other Proxmox entries already use

Every other Proxmox entry here — injecting qemu-guest-agent into a template, debugging cloud-init on first boot — starts from an already-installed cloud image and configures it at first boot. Kickstart is the other half of unattended provisioning: it drives the installer itself, starting from nothing but a stock vendor ISO. Neither replaces the other — cloud images generally don't exist for every OS or every custom partition layout a Kickstart file can express, and Kickstart doesn't help once the OS is already on disk. Which one is the right tool depends on whether there's a suitable cloud image to start from at all.

What to change from the source before using it anywhere real

echo "core:redhat123" | chpasswd is a working demo default, hardcoded in plain text in a file meant to be reused — fine for a scratch VM torn down the same day, a real problem the moment the file is copied into a template that outlives that. The SSH-key placeholder pattern already forces a substitution step before use; the password line is exactly as easy to template the same way, or to drop and rely on the key alone since rootpw --lock plus a wheel-group SSH-only user makes the password unnecessary for actual access in the first place.

Created 2026-09-20T14:43:43+02:00 · Edit