Note: This is Part 2 of a multi-part series.
- ✅ Part 1: The Network Foundation & Utilities Server
- Part 3: The Deployment Lifecycle & Troubleshooting (Coming Soon)
- Part 4: Post-Install Hardening & User Management (Coming Soon)
In Part 1, we built the Utilities Server—DNS, DHCP, TFTP, HTTP, and HAProxy. Now it's time to prepare the actual OpenShift deployment artifacts.
This part is all about Ignition: what it is, how to generate it, and how to feed it to our VMs so they can boot into Red Hat CoreOS and join the cluster automatically.
We'll also cover a critical—and poorly documented—step: disabling Secure Boot on libvirt VMs to allow iPXE chainloading. Without this, your nodes will simply refuse to boot.
Note that all the tasks mentioned in this blog are performed in the utilities server.
Let's get into it.
1. Download OpenShift RHCOS images
To deploy OpenShift, we need to download the Red Hat CoreOS (formerly Red Hat Enterprise Linux CoreOS) images. It's the immutable, container-optimized operating system that OpenShift uses on all cluster nodes—bootstrap, masters, and workers alike.
RHCOS images (kernel, initramfs, rootfs) are the three core artifacts needed to network-boot RHCOS: the kernel is the Linux kernel that initializes the hardware, the initramfs is a minimal in-memory filesystem that loads early in the boot process and handles loading drivers and fetching the rootfs, and the rootfs is the full RHCOS filesystem that gets loaded into RAM and serves as the base operating system before Ignition takes over.
Those files can be found in this OpenShift mirror site.
At the time I deployed the cluster, I checked what was the latest available 4.18 version, which happened to be 4.18.27. That is the reason you are going to see some folders called "oc41827". But you can name them as you prefer, but ensure you review the commands and configuration files accordantly.
First, let's create the folders to hold those files.
$ sudo mkdir -p /var/www/html/oc41827/{ignition,rhcos}
Define some variables to ease the download process.
$ ocp_maj=4.18;rhcos_ver=4.18.27
$ mirror=https://mirror.openshift.com/pub/openshift-v4
$ baseurl=${mirror}/dependencies/rhcos/${ocp_maj}/${rhcos_ver}
$ cd /var/www/html/oc1827/rhcos/
And download the files
$ sudo wget ${baseurl}/rhcos-${rhcos_ver}-x86_64-live-rootfs.x86_64.img
$ sudo wget ${baseurl}/rhcos-${rhcos_ver}-x86_64-live-kernel-x86_64
$ sudo wget ${baseurl}/rhcos-${rhcos_ver}-x86_64-live-initramfs.x86_64.img
Note that on version 4.20, the kernel file name is rhcos-4.20.0-x86_64-live-kernel.x86_64, while on 4.18 is rhcos-4.18.27-x86_64-live-kernel-x86_64. Note the difference between .x86 and -x86.
2. Downloading the OpenShift Tools
We need the tools to generate the cluster configuration, which are the OpenShift Installer & OpenShift Client.
They can be found in the OpenShift mirror site. At the time I was deploying this cluster, the latest 4.18 version available was 4.18.37.
Let's download them.
$ sudo su
# mirror="https://mirror.openshift.com/pub/openshift-v4/clients"
# wget ${mirror}/ocp/4.18.37/openshift-client-linux-4.18.37.tar.gz
# wget ${mirror}/ocp/4.18.37/openshift-install-linux-4.18.37.tar.gz
Extract both, move binaries to /usr/bin/ and ensure we have some helping aliases
# tar -xvf openshift-client-linux-4.18.37.tar.gz -C /usr/bin/
# tar -xvf openshift-install-linux-4.18.37.tar.gz -C /usr/bin/
# oc completion bash > /etc/bash_completion.d/openshift
# openshift-install completion bash > /etc/bash_completion.d/openshift-install
# exit
$ source /etc/bash_completion.d/openshift
$ source /etc/bash_completion.d/openshift-install
And now you can test them. You should see something similar to the following lines
$ oc version
Client Version: 4.18.37
Kustomize Version: v5.4.2
$ openshift-install version
openshift-install 4.18.37
built from commit 488926dc2c95d96460ee9939929a76ed23e1c596
release image quay.io/openshift-release-dev/ocp-release@sha256:9b7068aa6f6087c2f0a7cefa241c5dbb0ede0efaad783607dff0da98cac432d2
release architecture amd64
$
3. Generating installation configuration file
In order to deploy the OpenShift nodes, we need to generate the Ignition files. Those are JSON-based configuration specifications that tell RHCOS exactly how to configure itself on first boot—partitioning disks, writing network configs, embedding the pull secret, and registering the node with the OpenShift cluster. Think of them as the "first-run setup script" that transforms a blank RHCOS machine into a functioning OpenShift node.
To generate those files, we can runs the OpenShift installer, which asks for the necessary cluster information and then creates the installation configuration file install-config.yaml accordingly.
Instead, I have a install-config.yaml template I used to create the Kubernetes manifests and the ignition configuration files for the bootstrap node bootstrap.ign, control plane nodes master.ign, and compute nodes worker.ign.
install-config.yaml
Note the importance of the correct contents of this file, as it defines the OpenShift cluster.
apiVersion: v1
baseDomain: internal.local
compute:
- hyperthreading: Enabled
name: worker
replicas: 3
controlPlane:
hyperthreading: Enabled
name: master
replicas: 3
metadata:
name: oc41827
networking:
clusterNetwork:
- cidr: 10.128.0.0/14
hostPrefix: 23
networkType: OVNKubernetes
serviceNetwork:
- 172.30.0.0/16
platform:
none: {}
fips: false
pullSecret: 'CHANGEME'
sshKey: 'CHANGEMETOO'
| What It Defines | Why It Matters |
|---|---|
| Cluster Identity (metadata.name, baseDomain) | Determines all DNS names (api.., *.apps..) |
| Networking (clusterNetwork, serviceNetwork) | Ensures pods, services, and node IPs don't conflict with each other or existing infrastructure |
| Topology (node replicas, control plane size) | Sets the minimum resources needed for high availability (3 masters, at least 1 worker) |
| Authentication (pullSecret, sshKey) | Allows nodes to pull container images from Red Hat registries and enables emergency SSH access |
| Platform (platform: none: {}) | Tells the installer we're doing bare metal/external provisioning, not cloud integration |
As you can see, there are two pieces of information are essential before we can use install-config.yaml that we need to change.
Pull Secret
Log into the OpenShift console using this link, or navigate to Red Hat OpenShift, under Get started with OpenShift select Create Cluster, Select Datacenter, then Bare Metal (x86_64), and next select Full Control.
Under Pull secret, press Download the pull secret. This file authenticates your cluster with Red Hat's registries.
Save it and convert it into a one-liner json file. Create a working directory from where the OpenShift deployment will be run, for example:
$ mkdir oc41827upi
and then
$ python3 -m json.tool pull-secret > pull-secret.json
$ cat pull-secret.json | jq . -c > ~/oc41827upi/pull-secret-online.json
SSH key
You'll need an SSH key pair to access the nodes from the utilities server.
$ ssh-keygen -t rsa -b 4096 -N '' -f .ssh/oc41827upi
Make note of the public key path—we'll reference it in the install config.
Update install-config.yaml
Now it is time to get all the information together:
cat <<EOF > ~/oc41827upi/install-config.yaml
apiVersion: v1
baseDomain: internal.local
compute:
- hyperthreading: Enabled
name: worker
replicas: 3
controlPlane:
hyperthreading: Enabled
name: master
replicas: 3
metadata:
name: oc41827
networking:
clusterNetwork:
- cidr: 10.128.0.0/14
hostPrefix: 23
networkType: OVNKubernetes
serviceNetwork:
- 172.30.0.0/16
platform:
none: {}
fips: false
pullSecret: '$(cat ~/oc41827upi/pull-secret.json)'
sshKey: '$(cat ~/.ssh/oc41827upi.pub)'
EOF
⚠️ Important: The metadata.name (oc41827) becomes part of your cluster's FQDN. Make sure this matches the DNS records we created in Part 1 (api.oc41827.internal.local).
🔒 Sensitive Data: The pullSecret and sshKey fields are populated at generation time. Once you run openshift-install create manifests, these values are embedded in the Ignition files. Do not commit install-config.yaml to a public repository without redacting these fields.
Key Fields Explained
| Field | Value | Why |
|---|---|---|
| baseDomain | internal.local | Matches the DNS zone from Part 1 |
| metadata.name | oc41827 | Cluster identifier, part of the FQDN |
| platform | none: {} | Platform-agnostic (bare metal / VM) |
| networkType | OVNKubernetes | Default SDN for 4.18 |
| replicas (master) | 3 | Minimum for HA control plane |
| replicas (worker) | 3 | Adjust as needed for your lab, minimum 1 |
4. Generating Manifests & Ignition Files
Now we transform the blueprint into machine-readable configuration. This is a one-way operation: once you generate manifests, you can make targeted edits, but after Ignition files are created, the config is frozen.
Step 1: Create Manifests
$ cd ~/oc41827upi
$ openshift-install create manifests --dir=.
This creates two directories:
- manifests/ — YAML definitions for cluster resources
- openshift/ — Internal OpenShift configuration files
💡 Optional: At this stage, you can modify files in manifests/ to customize the cluster before Ignition generation. Common modifications include adjusting default node settings or removing the infrastructure components you don't need. For this lab, we'll proceed with defaults.
Step 2: Generate Ignition Files
$ openshift-install create ignition-configs --dir=.
This produces three critical files:
| File | Target |
|---|---|
| bootstrap.ign | The temporary bootstrap node |
| master.ign | Each control plane node |
| worker.ign | Each compute node |
These Ignition files contain the complete initial configuration for each node type: disk partitioning, systemd units, network settings, and the embedded pull secret. They are the single source of truth that tells RHCOS how to become an OpenShift node.
5. Serving Ignition Files via Apache
Copy the Ignition files to the web server
sudo cp ~/oc41827upi/bootstrap.ign /var/www/html/oc41827/ignition/
sudo cp ~/oc41827upi/master.ign /var/www/html/oc41827/ignition/
sudo cp ~/oc41827upi/worker.ign /var/www/html/oc41827/ignition/
Set permissions
$ sudo chmod +r /var/www/html/oc41827/ignition/*
Verify they are accessible:
$ curl -I http://192.168.110.20/oc41827/ignition/bootstrap.ign
Expected: HTTP/1.1 200 OK
6. Creating the boot.ipxe Menu
Now we create the iPXE boot script to include OpenShift entries. Each node type points to the same kernel and rootfs but fetches a different Ignition file via the coreos.inst.ignition_url kernel argument.
Here's the structure for the menu entries in your boot.ipxe (/var/www/html/boot.ipxe):
#!ipxe
dhcp
set base http://192.168.110.20/
set oc41827 http://192.168.110.20/oc41827/
:bootstrap
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/bootstrap.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot
:master
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/master.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot
:worker
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/worker.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot
⚠️ Disk Device Name: coreos.inst.install_dev=vda assumes your VMs use virtio disks. If your VMs use SATA or SCSI, change this to sda accordingly.
7. Testing the environment
Before we continue, I would recommend to create a test virtual machine and to check if it can be installed using iPXE. This step can be bypassed, but it can give us the confidence that all is ready for the OpenShift deployment.
I am adding here two examples, RHEL and Ubuntu
RHEL 10
Previously, I copied the RHEL 10 iso image in the utilities server in a folder called /isos. Let's mount it.
$ ls /isos
rhel-10.1-x86_64-dvd.iso
$ sudo mkdir -v /mnt/dvd
mkdir: created directory '/mnt/dvd'
$ sudo mount -v /isos/rhel-10.1-x86_64-dvd.iso /mnt/dvd
mount: /mnt/dvd: WARNING: source write-protected, mounted read-only.
mount: /dev/loop0 mounted on /mnt/dvd.
$
Let´s copy now the contents of the iso image to our Apache server folder.
$ sudo mkdir /var/www/html/rhel10
$ sudo cp -R /mnt/dvd/* /var/www/html/rhel10/
$ sudo restorecon -Rv /var/www/html
$
Ubuntu 24.04 and 26.04
Similarly as with RHEL, I copied the isos to the server in the /isos folder.
Place their contents into the web server:
$ sudo mkdir -p /var/www/html/ubuntuserver/24.04
$ sudo mkdir -p /var/www/html/ubuntuserver/26.04
$ sudo mount -o loop /isos/ubuntu-24.04.4-live-server-amd64.iso /mnt/dvd
mount: /mnt/dvd: WARNING: source write-protected, mounted read-only.
$ sudo cp -a /mnt/dvd/. /var/www/html/ubuntuserver/24.04/
$ sudo umount /mnt/dvd
$ sudo mount -o loop /isos/ubuntu-26.04-live-server-amd64.iso /mnt/dvd
mount: /mnt/dvd: WARNING: source write-protected, mounted read-only.
$ sudo cp -a /mnt/dvd/. /var/www/html/ubuntuserver/26.04/
$ sudo umount /mnt/dvd
Correct SELinux:
$ sudo restorecon -Rv /var/www/html
Update the iPXE boot file
This is the full contents of the ipxe boot file.
/var/www/html/boot.ipxe
#!ipxe
dhcp
set base http://192.168.110.20/
set rhel10 http://192.168.110.20/rhel10/
set ubuntuserver2404 http://192.168.110.20/ubuntuserver/24.04/
set ubuntuserver2604 http://192.168.110.20/ubuntuserver/26.04/
set oc41827 http://192.168.110.20/oc41827/
menu iPXE menu
item redhat Install RHEL 10 interactively
item ubuntuserver2404 Install Ubuntu Server 24.04.4 LTS interactively
item ubuntuserver2604 Install Ubuntu Server 26.04 LTS interactively
item bootstrap Install OpenShift 4.18.27 Bootstrap
item master Install OpenShift 4.18.27 Master Node
item worker Install OpenShift 4.18.27 Worker Node
item shell Drop to iPXE shell
choose --default shell --timeout 1500000 target && goto ${target}
:redhat
kernel ${rhel10}images/pxeboot/vmlinuz ip=dhcp inst.repo=${rhel10} inst.stage2=${rhel10} quiet
initrd ${rhel10}images/pxeboot/initrd.img
boot
:ubuntuserver2404
kernel ${ubuntuserver2404}casper/vmlinuz ip=dhcp url=${ubuntuserver2404}
initrd ${ubuntuserver2404}casper/initrd
boot
:ubuntuserver2604
kernel ${ubuntuserver2604}casper/vmlinuz ip=dhcp url=${ubuntuserver2604}
initrd ${ubuntuserver2604}casper/initrd
boot
:bootstrap
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/bootstrap.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot
:master
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/master.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot
:worker
kernel ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-kernel-x86_64 initrd=main coreos.live.rootfs_url=${oc41827}rhcos/rhcos-4.18.27-x86_64-live-rootfs.x86_64.img coreos.inst.install_dev=/dev/vda coreos.inst.ignition_url=${oc41827}ignition/worker.ign
initrd --name main ${oc41827}rhcos/rhcos-4.18.27-x86_64-live-initramfs.x86_64.img
boot
:shell
shell
8. Disabling Secure Boot on VMs
This is the step that cost me several hours of debugging. If your libvirt VMs have Secure Boot enabled, iPXE will fail to chainload. The UEFI firmware rejects the unsigned iPXE binary, and your VMs won't boot.
Those steps are to be done in your hypervisor, where all the virtual machines are being hosted (the utilities server, your test virtual machine to boot from ipxe, the OpenShift nodes).
If you are using a different hypervisor, check their documentation to disable Secure Boot. Here I am explaining how I managed to do it for RHEL virtualization.
Dump the VM definition as XML
Replace test by the name of your virtual machine.
$ sudo virsh dumpxml test > test.xml
Edit the VM definition
Using your preferred editor (for example vim or nano), edit the file test.xml.
Find the section and change:
<vcpu placement='static'>2</vcpu>
<os firmware='efi'>
<type arch='x86_64' machine='pc-q35-rhel10.0.0'>hvm</type>
<firmware>
<feature enabled='yes' name='enrolled-keys'/>
<feature enabled='yes' name='secure-boot'/>
</firmware>
<loader readonly='yes' secure='yes' type='pflash' format='raw'>/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd</loader>
<nvram template='/usr/share/edk2/ovmf/OVMF_VARS.secboot.fd' templateFormat='raw' format='raw'>/var/lib/libvirt/qemu/nvram/test_VARS.fd</nvram>
</os>
<features>
<acpi/>
<apic/>
<smm state='on'/>
</features>
to:
<vcpu placement='static'>2</vcpu>
<os>
<type arch='x86_64' machine='pc-q35-rhel10.0.0'>hvm</type>
<loader readonly='yes' type='pflash' format='raw'>/usr/share/edk2/ovmf/OVMF_CODE.fd</loader>
<nvram template='/usr/share/edk2/ovmf/OVMF_VARS.fd' templateFormat='raw' format='raw'>/var/lib/libvirt/qemu/nvram/test_VARS.fd</nvram>
</os>
<features>
<acpi/>
<apic/>
<smm state='off'/>
</features>
🔑 Key Changes:
- Replace
<os firmware='efi'>by<os> - Remove the full section starting with
<firmware>to</firmware> - At the line starting with
<loader, remove secure='yes' and replace OVMF_CODE.secboot.fd by OVMF_CODE.fd - At the line starting with
<nvram, replace OVMF_VARS.secboot.fd by OVMF_VARS.fd - And replace the line
<smm state='on'/>by<smm state='off'/>
Update the VM definition
Update the configuration of the virtual machine in your host:
$ sudo virsh define test.xml
9. Verification
After disabling Secure Boot on the VM, power on the VM and connect to its console:
$ sudo virsh start test --console
or using your Cockpit interface.
You should now see the iPXE menu appear. If it loads the boot.ipxe script correctly, and you can install the OS into your test VM, you're ready for deployment.
Part 2 Recap
Let's review what we accomplished:
✅ Downloaded OpenShift installer and CLI tools
✅ Secured the pull secret and SSH key
✅ Wrote install-config.yaml matching our DNS/DHCP from Part 1
✅ Generated manifests and Ignition files
✅ Published Ignition files and RHCOS images via Apache
✅ Updated boot.ipxe with OpenShift boot entries
✅ Disabled Secure Boot on all VMs to enable iPXE chainloading
Everything is staged. The Ignition files are on the web server, the iPXE menu is updated, and the VMs can boot.
🔜 Coming Up in Part 3: The Deployment Lifecycle
- Powering on the bootstrap node and monitoring logs
- Waiting for bootstrap-complete and removing the bootstrap node
- Approving CSRs for master and worker nodes
- Verifying cluster health with oc commands
Stay tuned!

Top comments (0)