Provisioning of EU OS
WARNING
This section is work in progress. Related issue: #39
Anaconda allows for attended and unattended (hands-off) provisioning of Linux on bare-metal devices and virtual machines. The specific configuration of default values happens in so-called Kickstart configuration files (hereafter named config.toml).
References:
- Redhat Anaconda Manual
- Redhat Anaconda Manual on Kickstart Installations
- Redhat Anaconda Manual Kickstart Examples
Provisioning to Bare Metal machines (desktops/laptops)
The automated Provisioning to Bare Metal or Virtual Machines uses Foreman. It allows the Lifecycle-Management (creation, operation/maintenance, deletion) of physical and virtual machines.
For the initial setup of Foreman, follow the steps described in the official documentation at https://docs.theforeman.org/3.16/Quickstart/index-foreman-el.html
This guide assumes you have at least a single Foreman Host that is accessible via HTTP(S). For network based provisioning that uses DHCP/PXE/TFTP, Foreman should be configured to orchestrate authoritative DNS and DHCP Services. These services can run on the Foreman Host itself (installed and configured by the foreman-installer) or on external systems. See https://docs.theforeman.org/3.16/Integrating_Provisioning_Infrastructure_Services/index-foreman-el.html for further guidance.
Manual Provisioning via ISO Image
The custom OCI image can be transfered on a USB installation medium (e.g. a USB pen drive) using several methods:
- with
bluebuild generate-iso(see documentation; no support forconfig.toml) - with the GUI Podman Desktop and its bootc extension (here, also a Kickstart
config.tomlcan be used) - with the OCI image
bootc-image-builder(see documentation)
The last option offers the most flexibility and can be scripted. Find an example script build-iso.sh and Kickstart config.toml here below.
IMPORTANT
Change the local admin user password, the disk encryption password and the remote OCI image registry!
#!/bin/bash
# Run this script to generate an ISO from the OS container
set -euxo pipefail
cd "$(dirname "$0")"
TYPE="anaconda-iso"
# Set IMAGE to $1 if provided; otherwise fall back to a default
IMAGE=${1:-registry.gitlab.com/eu-os/workspace-images/eu-os-base-demo/eu-os-demo}
mkdir -p output
sudo podman pull "${IMAGE}"
sudo podman run \
--rm \
-it \
--privileged \
--pull=newer \
--security-opt label=type:unconfined_t \
-v /var/lib/containers/storage:/var/lib/containers/storage \
-v ./config.toml:/config.toml:ro \
-v ./output:/output \
quay.io/centos-bootc/bootc-image-builder:latest \
--type "${TYPE}" \
--rootfs btrfs \
"${IMAGE}"[customizations.installer.kickstart]
contents = """
reboot --eject
%post --erroronfail
bootc switch --mutate-in-place --transport registry registry.gitlab.com/eu-os/workspace-images/eu-os-base-demo/eu-os-demo:10
# used during automatic image testing as finished marker
if [ -c /dev/ttyS0 ]; then
# continue on errors here, because we used to omit --erroronfail
echo "Install finished" > /dev/ttyS0 || true
fi
%end
# System language
lang en_UK.UTF-8
# Keyboard layout
keyboard de
# OSTree container setup
ostreecontainer --transport="oci" --url="/run/install/repo/container"
# Generated using Blivet version 3.12.1
ignoredisk --only-use=sda
# Erase all partitions and initialize the disk label
clearpart --all --initlabel
#region fde
# Disk partitioning information
autopart --encrypted --passphrase master-passphrase --type btrfs
#endregion fde
# System Timezone
timezone "Europe/Brussels" --utc
# Prohibit login with root
rootpw --lock
# Setup User with sudo permission
user --groups=wheel --name=admin --password=admin-passphrase --plaintext --gecos="EU OS Local Admin"
"""References:
- Fedora 36 Kickstart Synax Reference (could be outdated!)
- osbuild documentation on Anaconda
- Podman Desktop bootc extension on Github
- Podman Desktop Blog post on bootc extension
Full-Disk Encryption LUKS2
WARNING
This section is work in progress. Related issue: #35
Full-Disk Encryption (FDE) protects configuration data and user data in case the device is lost or stolen. Most Linux distributions as well as EU OS rely for FDE on the software LUKS2.
LUKS2 relies on a specific partitition setup during the provisioning process. The setup is enabled in the config.toml available in full above.
# Disk partitioning information
autopart --encrypted --passphrase master-passphrase --type btrfsUnlocking LUKS2 Volumes
LUKS2 volumes can be unlocked by a passphrase or hardware security tokens. By default, it can be unlocked using a passphrase. The default passpharse of the LUKS2 volume is euos. The default LUKS2 passphrase can be changed after installation, however as the project progresses, a strong passphrase could be generated during partitioning. Hadware security keys serve as an alternative to passphrases and are very convenient. systemd-cryptenroll is used to enroll hardware security tokens, such as TPM, FIDO2 and PKCS#11 devices. Currently, FIDO2 is supported to unlock the LUKS2 FDE volume at boot.
Enrolling FIDO2 Devices
sudo blkid # look out for the crypt luks type and copy the UUID value in below
sudo systemd-cryptenroll --fido2-device auto /dev/<device, i.e. nvme0n1p3>Configuring Kernel Arguments for FIDO2
sudo rpm-ostree kargs --append rd.luks.options=<LUKS device ID>=discard,fido2-device=autoProvisioning with Foreman (PXE and Stub ISO)
- with foreman you can manage hardware models and sets of Kickstart files (requires then the Kartello plugin)
- find more information on foreman on the fleet management page
References:
- Foreman Documentation (nightly) on Provisioning Hosts
- Foreman: Provisioning hosts with NetBoot ISO - Leos Stejskal - CfgMgmtCamp 2025 Ghent
- Tutorial on PXE Provisioning in the Foreman Community Forum
In order to provision EU OS with foreman, we need to create some objects in foreman. Let's begin with the provisioning template, which will be rendered to a kickstart file (download):
<%#
kind: provision
name: EU-OS Kickstart default
model: ProvisioningTemplate
oses:
- EUOS
description: |
EU-OS Provisioning Template using Red Hat anaconda installer
This Template accepts the following parameters:
- lang: string (default="en_US.UTF-8")
- keyboard: string (default="en")
- time-zone: string (default="UTC")
-%>
<%
install_disk = host_param('install-disk') ? host_param('install-disk') : "nvme0n1"
ostree_image_repo = host_param('ostree-image-repo')
os_major = @host.operatingsystem.major.to_i
os_name = @host.operatingsystem.name
-%>
# License agreement
eula --agreed
# Use graphical install
graphical
# Keyboard layouts
keyboard <%= host_param('keyboard') || 'us' %>
<% if host_enc['parameters']['realm'] && @host.realm && (@host.realm.realm_type == 'FreeIPA' || @host.realm.realm_type == 'Red Hat Identity Management') -%>
%pre-install --erroronfail
curl -kL -o /etc/pki/ca-trust/source/anchors/IPA_CA.crt http://ipa-ca.<%= @host.realm.name.downcase %>/ipa/config/ca.crt
update-ca-trust
%end
<% end -%>
# System language
lang <%= host_param('lang') || 'en_US.UTF-8' %>
# Network
network --bootproto=dhcp
# Firewall configuration
firewall --use-system-defaults --ssh
# OSTree setup
ostreecontainer --no-signature-verification --transport=registry --stateroot=fedora-<%= host_param('fedora_flavor') || 'eu-os-demo' %> --remote=fedora-<%= host_param('fedora_flavor') || 'eu-os-demo' %> --url=<%= ostree_image_repo %>/<%= ostree_image %>
# Don't run the setup agent on first boot
firstboot --disable
# Reboot after installation
reboot
# Generated using Blivet version 3.5.0
<%= @host.diskLayout %>
# System timezone
timezone --utc <%= host_param('time-zone') || 'UTC' %>
# Root password
rootpw --iscrypted <%= root_pass %>
<%#
Main post script, if it fails the last post is still executed.
%>
%post
exec < /dev/tty3 > /dev/tty3
chvt 3
(
logger "Starting anaconda <%= @host %> postinstall"
<% if host_enc['parameters']['realm'] && @host.realm && (@host.realm.realm_type == 'FreeIPA' || @host.realm.realm_type == 'Red Hat Identity Management') -%>
<%= snippet 'freeipa_register_ostree' %>
<% end -%>
<%= snippet "blacklist_kernel_modules" %>
<%= snippet 'efibootmgr_netboot' %>
<%= snippet 'vconsole_lang' %>
chvt 6
touch /tmp/foreman_built
) 2>&1 | tee /root/install.post.log
%end
# The last post section halts Anaconda to prevent endless loop in case HTTP request fails
%post --erroronfail --log=/root/install-callhome.post.log
if test -f /tmp/foreman_built; then
echo "calling home: build is done!"
<%= indent(2, skip1: true) { snippet('built', :variables => { :endpoint => 'built', :method => 'POST', :body_file => '/root/install.post.log' }) } -%>
else
echo "calling home: build failed!"
<%= indent(2, skip1: true) { snippet('built', :variables => { :endpoint => 'failed', :method => 'POST', :body_file => '/root/install.post.log' }) } -%>
fi
sync
%endThis template uses a snippet to perform an unattended FreeIPA Realm join. The snippet freeipa_register_ostree.erb here below (download) is based on the upstream snippet freeipa_register. The latter does not support yet (upstream issue report) ostree hosts, because it attempts to install software locally with yum, which ostree/bootc hosts do not support.
<%#
kind: snippet
name: freeipa_register_ostree
model: ProvisioningTemplate
snippet: true
%>
# FreeIPA Registration Snippet
#
# Optional parameters:
#
# freeipa_server IPA server
#
# freeipa_sudo Enable sudoers
# Default: true
#
# freeipa_ssh Enable ssh integration
# Default: true
#
# freeipa_automount Enable automounter
# Default: false
#
# freeipa_automount_location Location for automounts
#
# freeipa_mkhomedir Enable automatically making home directories
# Default: true
#
# freeipa_opts Additional options to pass directly to installer
#
# freeipa_automount_server Override automount server if freeipa_automount is true and the server differs from freeipa_server
#
<% if @host.operatingsystem.family == 'Redhat' -%>
<% if @host.operatingsystem.name == 'Fedora' -%>
freeipa_client=freeipa-client
<% else -%>
freeipa_client=ipa-client
<% end -%>
<% os_major = @host.operatingsystem.major.to_i %>
<% if os_major == 7 -%>
/usr/sbin/sshd-keygen
<% elsif os_major > 7 %>
/usr/libexec/openssh/sshd-keygen rsa
<% end -%>
<% else -%>
freeipa_client=freeipa-client
<% end -%>
##
## IPA Client Installation
##
<% domain = host_param('freeipa_domain') || @host.realm.name.downcase -%>
freeipa_domain="--domain <%= domain %>"
<% if host_param('freeipa_server') -%>
freeipa_server="--server <%= host_param('freeipa_server') %>"
<% end -%>
<% unless host_param_false?('freeipa_mkhomedir') %>
freeipa_mkhomedir="--mkhomedir"
<% end -%>
<% if host_param_false?('freeipa_ssh') %>
freeipa_ssh="--no-ssh"
<% end -%>
<% if host_param('freeipa_opts') -%>
freeipa_opts="<%= host_param('freeipa_opts') %>"
<% end -%>
# One-time password will be requested at install time. Otherwise, $HOST[OTP] is used as a placeholder value.
mkdir -pv /var/lib/ipa-client/sysrestore /var/lib/ipa-client/pki /var/log/certmonger
rm -f /etc/krb5.conf.d/*
env DBUS_SYSTEM_BUS_ADDRESS=unix:path=/dev/null /usr/sbin/ipa-client-install -w '<%= @host.otp || "$HOST[OTP]" %>' --realm=<%= @host.realm %> -U $freeipa_mkhomedir $freeipa_opts $freeipa_server $freeipa_domain $freeipa_ssh
##
## Automounter
##
<% if host_param('freeipa_automount_location') -%>
automount_location="--location <%= host_param('freeipa_automount_location') %>"
<% end -%>
<% if host_param_true?('freeipa_automount') -%>
if [ -f /usr/sbin/ipa-client-automount ]
then
automount_server=$freeipa_server
<%- if automount_server = host_param('freeipa_automount_server') || host_param('freeipa_server') -%>
automount_server="--server <%= automount_server %>"
<%- end -%>
/usr/sbin/ipa-client-automount $automount_server $automount_location --unattended
fi
<% end -%>
##
## Sudoers
##
<% unless host_param_false?('freeipa_enable_sudo') %>
freeipa_client_version=$(ipa-client-install --version)
freeipa_client_version_major=$(echo $freeipa_client_version | cut -f1 -d.)
freeipa_client_version_minor=$(echo $freeipa_client_version | cut -f2 -d.)
freeipa_realm=$(grep default_realm /etc/krb5.conf | cut -d"=" -f2 | tr -d ' ')
freeipa_domain=$(grep -A 2 domain_realm /etc/krb5.conf | tail -n1 | awk '{print $1}')
freeipa_dn=$(for word in $(echo $freeipa_domain | sed 's/\./ /g'); do echo -n dc=$word,; done)
sssd_version=$(sssd --version)
sssd_major=$(echo $sssd_version | cut -f1 -d.)
sssd_minor=$(echo $sssd_version | cut -f2 -d.)
LDAP_CONFIG=$(mktemp)
# >=ipa-client-4.1.0 automatically configures sssd for sudo
# =<ipa-client-3 requires manual configuration which this snippet takes care of
if [ $freeipa_client_version_major -lt 4 ]
then
# Modify sssd.conf
sed -i -e "s/services = .*/\0, sudo/" /etc/sssd/sssd.conf
# Modify sssd.conf for sssd <1.11 (RHEL <6.6)
if [ $sssd_minor -lt 11 ] || [ $sssd_major -lt 1 ]
then
<% if host_param('freeipa_server') -%>
ldap_uri=", ldap://<%= host_param('freeipa_server') %>"
krb5_server=<%= host_param('freeipa_server') %>
<% else -%>
krb5_server="_srv_"
<% end -%>
cat <<EOF > $LDAP_CONFIG
sudo_provider = ldap
ldap_uri = _srv_ $ldap_uri
ldap_sudo_search_base = ou=SUDOers,${freeipa_dn%?}
ldap_sasl_mech = GSSAPI
ldap_sasl_authid = host/$HOSTNAME
ldap_sasl_realm = $freeipa_realm
krb5_server = $krb5_server
EOF
sed -i -e "/\[domain\/.*\]/ r $LDAP_CONFIG" /etc/sssd/sssd.conf
fi
# Modify nsswitch.conf
grep -q sudoers /etc/nsswitch.conf
if [[ $? -eq 0 ]];
then
sed -i -e "s/^sudoers.*/sudoers: files sss/" /etc/nsswitch.conf
else
echo "sudoers: files sss" >> /etc/nsswitch.conf
fi
# Configure nisdomain
<% if @host.operatingsystem.family == 'Redhat' -%>
authconfig --nisdomain ${freeipa_domain} --update
chkconfig sssd on
if [[ $(rpm -qa systemd | wc -l) -gt 0 ]];
then
domain_service=/usr/lib/systemd/system/*-domainname.service
# Workaround for BZ1071969 on RHEL 7.0
grep -q "DefaultDependencies=no" $domain_service
if [[ $? -ne 0 ]]
then
sed -i -e "s/\[Unit\]/\[Unit\]\nDefaultDependencies=no/" $domain_service
fi
systemctl start $(basename $domain_service)
systemctl enable $(basename $domain_service)
fi
<% else -%>
# OS is not RedHat
sed -i -e '/^exit /d' /etc/rc.local
echo "nisdomainname ${freeipa_domain}" >> /etc/rc.local
echo "exit 0" >> /etc/rc.local
nisdomainname ${freeipa_domain}
<% end -%>
fi
<% end -%>
# Modify sssd.conf, add 'selinux_provider = none'
sed -i 's/\(\[domain\/.*\]\)/\1\nselinux_provider = none/' /etc/sssd/sssd.conf
# Remove 04-ipa.conf, file will get substituted on first boot
rm -f /etc/ssh/ssh_config.d/04-ipa.conf || /bin/true
# Set static hostname
hostnamectl set-hostname --static <%= @host.name %>In order for the boot procedure to work via PXE/TFTP we need the pxegrub2 template fedora_atomic_pxegrub2 (download):
<%#
kind: PXEGrub2
name: EU-OS PXEGrub2 Template
model: ProvisioningTemplate
oses:
- EU-OS
- Fedora Atomic Desktops
description: |
The template to render Grub2 bootloader configuration for kickstart based distributions.
The output is deployed on the host's subnet TFTP proxy.
-%>
<%
os_major = @host.operatingsystem.major.to_i
os_name = @host.operatingsystem.name
%>
# This file was deployed via '<%= template_name %>' template
<%
linuxcmd = "linuxefi"
initrdcmd = "initrdefi"
-%>
set default=<%= host_param('default_grub_install_entry') || 3 %>
set timeout=<%= host_param('loader_timeout') || 10 %>
menuentry '<%= template_name %>' {
<%= linuxcmd %> <%= @kernel %> inst.debug inst.stage2=<%= medium_uri %> inst.ks=<%= foreman_url('provision') %> inst.ks.sendmac ip=dhcp
<%= initrdcmd %> <%= @initrd %>
}Partition Table
The disk should be formatted with BTRFS using LUKS Full Disk Encryption. The included Partition Table Kickstart default encrypted is sufficient for that. It allows us to give the type (--type=btrfs) and passphrase using the parameters autopart_options and disk_enc_passphrase.
Architectures
Ensure that the Architecture x86_64 is present in Provisioning Setup → Architectures
Operating System
To create EU-OS as Operating System, use Hosts → Provisioning Setup → Operating Systems → Create Operating System
Operating System
| Option name | Value |
|---|---|
| Name | EU-OS |
| Major Version | 42 |
| Minor Version | |
| Description | EU-OS 42 |
| Family | Red Hat |
| Root Password Hash | SHA512 |
| Architectures | x86_64 |
Partition Table
Selected Items:
- Kickstart default encrypted
Installation Media
Selected Items:
- Fedora mirror
Templates
NOTE
Can only be assigned after creation of the Operating System.
To be able to choose the templates in the Operating System Properties, the templates need to be associated with the Operating System:
- Use the association tab of the template to assign EU-OS as supported operating system.
- Set the template as active template in the Templates tab of the operating system.
| Template Type | Value |
|---|---|
| Host initial configuration template | Linux host_init_config default |
| Provisioning template | EU-OS Desktop |
| PXEGrub2 template | Kickstart fedora atomic PXEGrub2 |
The provisioning template consumes Parameters, they need to be created on the Parameters Tab:
Parameters
| Name | Type | Value |
|---|---|---|
install-disk | string | nvme0n1 |
ostree-image-repo | string | registry.gitlab.com/eu-os/workspace-images/eu-os-base-demo |
fedora_flavor | string | eu-os-demo |
autopart_options | string | --type=btrfs |
disk_enc_passphrase | string | temporarypassphrase |
lang | string | de_DE.UTF-8 |
keyboard | string | de |
time-zone | string | Europe/Berlin |
Provision via PXE/DHCP
The EU-OS specific tasks are done. To provision a Host via PXE/DHCP/TFTP, follow the guide on https://docs.theforeman.org/3.16/Provisioning_Hosts/index-foreman-el.html
Create a Host, assign a Network Interface using an IP Address of a Network with assigned DHCP and TFTP Proxies (Smart Proxies, not HTTP Proxies). The Host will be automatically set to build after Creation. It should have a static DHCP Leases and a grub2 configuration file.
When the Host attempts a network boot, it will get its IP Configuration from the DHCP Server and furthermore the BOOTP/PXE Configuration: Next Server (IP of TFTP Smart Proxy) and Filename to download via TFTP. The downloaded file will try to download machine specific configuration files, Foreman has placed a configuration file named after the hardware ethernet address of the client (PXEGRUB2 Template). The client fetches kernel and initramfs via TFTP and boots anaconda, which will download the kickstart file via HTTP. The installation takes place and notifies Foreman at the end of the installation process, that the installation is done. Foreman places a new PXEGRUB2 configuration file for the machine, that does local boot to avoid an installation loop.
For testing and development, manual provisioning is easier to setup and offers more flexibility. In a production environment, provisioning using Foreman/PXE/netboot images is likely faster for many machines.
Provisioning via Discovery Image
References:
Provisioning to Virtual Machines (VMs)
WARNING
This section is work in progress. Investigation is currently on-going (state of 2025-07-20)