SELinux
Kairos can boot RHEL and SUSE family images with SELinux on, in permissive or enforcing mode, using the distribution's own targeted policy. SELinux is off unless you turn it on at install time.
SELinux support is on kairos master and ships with the next kairos release after v4.3.0. Images built with v4.3.0 or older always boot with selinux=0. SELinux on Trusted Boot (UKI) images also needs an AuroraBoot release newer than v0.27.1.
Supported bases
| Base | SELinux | Tested in CI |
|---|---|---|
| Fedora, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream | ✅ | Rocky 9 |
| openSUSE Leap, Tumbleweed | ✅ | Leap 16.0 |
| Hadron, Ubuntu, Debian, Alpine | ❌ |
On the RHEL and SUSE families, kairos-init installs policycoreutils and selinux-policy-targeted into the image. Kairos does not ship a policy module of its own. The other bases get no policy packages and always boot with selinux=0, even if you set install.selinux.enabled: true.
CI tests both modes on Rocky 9 and openSUSE Leap 16.0, booted with GRUB on amd64. See Feature sets for the full per-base table.
Turning it on
Add an install.selinux block to the cloud-config you install with:
#cloud-config
install:
selinux:
enabled: true
mode: enforcing
| Field | Values | Default | Notes |
|---|---|---|---|
enabled | true, false | false | Boot the installed system with SELinux on. |
mode | permissive, enforcing | permissive | Ignored when enabled is false. An unknown value falls back to permissive. |
The installer logs a warning if mode is set while enabled is false, and if mode has an unknown value.
There is no selinux block under upgrade or reset. The setting is written once at install time as selinux_enabled and selinux_mode in a GRUB environment file:
/oem/grubenvby default.grubenvat the root of the STATE partition ifCOS_OEMis ininstall.encrypted_partitions, because GRUB cannot read an encrypted OEM. On the running system this is/run/initramfs/cos-state/grubenv.
The setting survives upgrades and resets, because neither rewrites that file. A reset with reset-oem: true formats OEM, so on a default install it removes the setting and the system boots with SELinux off afterwards.
To change the mode on an installed GRUB system, edit the GRUB environment and reboot:
grub2-editenv /oem/grubenv set selinux_enabled=true selinux_mode=enforcing
With COS_OEM encrypted, edit the STATE copy instead. STATE is mounted read-only, so remount it first:
mount -o remount,rw /run/initramfs/cos-state
grub2-editenv /run/initramfs/cos-state/grubenv set selinux_enabled=true selinux_mode=enforcing
mount -o remount,ro /run/initramfs/cos-state
Do not edit /etc/selinux/config. Kairos rewrites its SELINUX= line on every boot to match the kernel cmdline, so a manual edit is lost on the next boot.
How it works
SELinux on an immutable system has one main problem: files need labels, and most of the filesystem is either a read-only image, a tmpfs overlay that is empty on each boot, or a bind mount from the persistent partition. Kairos handles this at install, at boot, and when the persistent layout changes.
At install time
After the active image is written, kairos-agent runs setfiles against the image in a chroot. The persistent partition is bound at /usr/local and OEM at /oem during that chroot, so both are labeled too. The passive image file gets the boot_t label. Reset relabels the same way. Upgrade relabels the new image only when it is not squashfs, since a squashfs image cannot be relabeled after it is built.
If setfiles or the policy's file_contexts is missing, as on bases without SELinux, this step does nothing.
Kernel cmdline
On GRUB, the boot script reads selinux_enabled and selinux_mode from the GRUB environment. On a RHEL or SUSE family image with selinux_enabled=true, it adds:
security=selinux selinux=1 enforcing=0 rd.cos.selinux=<mode>
In every other case it adds selinux=0. The recovery entry always gets selinux=0, so recovery is a safe way back into a system with a broken policy or bad labels.
enforcing=0 is always set, even in enforcing mode. The kernel starts in permissive mode and Kairos moves to enforcing later in boot, after labels are fixed. rd.cos.selinux= carries the requested mode to that step.
At boot
When selinux=1 is on the cmdline, the initramfs stage of the bundled SELinux cloud-config does three things:
- Writes
file_contexts.subsunder/etc/selinux/targeted/contexts/files/from the bind mounts in/etc/fstab. Persistent paths are stored under/usr/local/.state/<name>.bindand bind-mounted to their real path, for example/usr/local/.state/var-lib-rancher.bindto/var/lib/rancher. The substitutions makerestoreconlabel those directories with the policy rules for their real path. - Creates and enables
kairos-selinux-relabel.service. - Sets
SELINUX=in/etc/selinux/configtoenforcing,permissiveordisabled, to match the cmdline. This step also runs with SELinux off.
kairos-selinux-relabel.service is a oneshot unit that runs after cos-setup-fs.service and kairos-agent.service, and before multi-user.target. It runs, in order:
restorecon -Riv /etcrestorecon -Rivx /var /srvselinux-relabel-persistent, which relabels/usr/local /etc /var /home /oem /root /opt /srv. It only does this when the policy version or the set of*.binddirectories changed since the last run. It records both in/usr/local/.state/selinux/.kairos-selinux-relabeledafter a successful run. The first boot after install, after a policy update, or after adding a bind mount does the full walk. Other boots skip it.setenforce 1in enforcing mode,setenforce 0in permissive mode.
If the persistent relabel fails, the stamp is not written, setenforce does not run, and the system stays permissive. The relabel runs again on the next boot.
immucore keeps the extended attributes of the persistent state directories when it syncs them on boot, so their labels are not lost between boots.
Trusted Boot (UKI)
A UKI has its cmdline baked in and signed, and a Trusted Boot system does not read /oem/grubenv. The SELinux mode is therefore chosen when you build the UKI, not when you install it.
Pass a cloud-config with an install.selinux block to build-uki with --cloud-config. AuroraBoot replaces selinux=0 in the UKI cmdline with the same security=selinux selinux=1 enforcing=0 rd.cos.selinux=<mode> fragment GRUB uses:
docker run -ti --rm -v $PWD/build:/result -v $PWD/keys/:/keys -v $PWD/config.yaml:/config.yaml \
quay.io/kairos/auroraboot build-uki -t iso -d /result/ \
--public-keys /keys --tpm-pcr-private-key $PATH_TO_TPM_KEY \
--sb-key $PATH_TO_SB_KEY --sb-cert $PATH_TO_SB_CERT \
--cloud-config /config.yaml $CONTAINER_IMAGE
install.selinux in the install-time cloud-config has no effect on a UKI system, which is why the configuration reference lists it as GRUB-only. Under UKI the same block is read only by build-uki, and the mode is baked into the signed cmdline. To change the mode, build a new signed UKI and upgrade to it.
Known gaps with UKI, both still open:
- AuroraBoot adds the SELinux fragment even when the image family has no SELinux policy, and only logs a warning (kairos-io/AuroraBoot#893). Only set
install.selinuxwhen building UKIs from RHEL or SUSE family images. - UKI recovery does not force SELinux off like GRUB recovery does (kairos-io/kairos#5120).
SELinux on UKI is not tested in CI yet.
Checking and debugging
Check the current mode and the cmdline:
getenforce
cat /proc/cmdline
grub2-editenv /oem/grubenv list # GRUB systems only
grub2-editenv /run/initramfs/cos-state/grubenv list # GRUB, with COS_OEM encrypted
Check that the relabel unit ran:
systemctl status kairos-selinux-relabel.service
journalctl -u kairos-selinux-relabel.service
kairos-agent logs collects this unit's journal, see Troubleshooting.
auditd is installed and enabled as part of the CIS hardening, and /var/log/audit is kept on the persistent partition, so AVC denials survive a reboot:
ausearch -m avc -ts boot
To force a full relabel of the persistent paths on the next boot, remove the stamp file and reboot:
rm /usr/local/.state/selinux/.kairos-selinux-relabeled
Limitations
- Every boot starts in permissive mode. Enforcing mode starts when
kairos-selinux-relabel.servicefinishes, beforemulti-user.target. Anything that runs earlier in boot is not confined. - The first boot after install, after a policy update, or after adding a bind mount runs a full
restoreconover the persistent paths. On a node with a lot of data under/varor/usr/local, this delays boot. - Kubernetes under SELinux is not tested. Kairos skips the
k3s-selinuxRPM when it installs k3s, andcontainer-selinuxis not part of the image. Add the policy packages your workloads need when you build the image, and test in permissive mode first. - Kairos does not ship tooling for custom policy modules. Add them to the image when you build it, for example with
semodule -iin your Dockerfile, so the install-timesetfilesrun already sees them. - SELinux enforcing is not yet part of the CIS L1 hardening. CIS hardening installs audit rules that watch
/etc/selinux/and/usr/share/selinux/, but does not turn SELinux on.