Last verified against: Kairos v3.6.0+, Kairos Hadron v0.0.4, CAPI v1.13.3, CAPM3 v1.13.0, BMO v0.13.0, provider v0.1.0-beta.1. Verified on emulated bare metal (sushy-tools Redfish BMC + libvirt/KVM) and on physical hardware (Hadron whole-disk deploy to bare metal); the same flow applies to physical hardware with a Redfish/IPMI BMC.
This guide walks you through creating a single-node k3s or k0s cluster on Kairos using Cluster API with the Metal3 infrastructure provider (CAPM3). k0s and k3s use the identical flow and differ only in distribution and version fields. For a 3-node HA control plane, see High-Availability control plane below.
Scope of this guide:
- The single-node walkthrough below provisions
replicas: 1. HA (replicas: 3/5) is supported on CAPM3 for both k0s and k3s — see High-Availability control plane. - DHCP-only networking. Static IPAM via Metal3DataTemplate / Metal3IPPool is a future phase.
- No cloud controller manager (
cloudProviderEnabled: false). Node providerID is set by the Kairos cloud-config at first boot — no manual patching required.
Note on controlPlaneEndpoint: both Cluster.spec.controlPlaneEndpoint and Metal3Cluster.spec.controlPlaneEndpoint must be set to the control-plane node's stable IP:6443 before applying the manifest. The provider does not auto-discover the endpoint. In DHCP-only Phase 1, this is the node's DHCP-assigned data-plane IP. To make this stable, set a DHCP reservation tied to the BareMetalHost's MAC address before applying. If you apply without a valid endpoint, KairosControlPlane stalls with Available=False(WaitingForInfrastructureControlPlaneEndpoint).
Understanding the provisioning model prevents the most common failure modes:
- You register a physical server as a
BareMetalHost(BMH) with baremetal-operator. BMO + Ironic inspect the host and mark itavailable. - CAPM3 claims the host when you apply a
Metal3Machinethat matches it. - Ironic performs a whole-disk deploy: it writes the raw disk image you provide directly to the node's boot disk over the provisioning network.
- Ironic appends a config-drive partition (label
config-2) containing the Kairos cloud-config rendered by the bootstrap provider. - The node UEFI-boots from the disk. The Kairos active system reads the cloud-config from the config-drive and provisions k3s/k0s.
There is no installer step. The disk image you provide must already be a fully installed Kairos system. This is different from the CAPK (KubeVirt) 2-disk installer pattern.
This is the most important prerequisite. Ironic deploys a raw disk image. That image must contain an already-installed Kairos OS — not an installer ISO and not an auroraboot auto-reset image.
An auroraboot disk.raw (recovery / auto-reset image) is designed to auto-reset-install on first boot. Its first-boot repartitioning collides with the config-2 partition Ironic appends as the config-drive. The node boots into cos-recovery (an ephemeral overlay) instead of the persistent active system. k3s / k0s then fails with an overlay not supported as upperdir error and never provisions a working node.
Kairos Hadron is the next-generation Kairos OS (musl-libc based). When a Hadron image that has not been fully installed is deployed via Ironic, the failure is different from the standard Kairos cos-recovery case described above:
kairos-agentreportsboot_mode=recovery_booton the node.- k0scontroller stays
disabled;/etc/k0sand/var/lib/k0sare never created. - k3s similarly never starts.
- The CAPI Machine stays
Provisioningindefinitely.
This occurs because an auroraboot build-iso product (or auroraboot disk.raw) produces an image with a populated recovery partition but no state (active) partition. Ironic writes it to disk; the node boots into recovery mode rather than the active system.
The fix is the same as for standard Kairos: build a fully-installed disk. The supported pipeline for Hadron:
- Build the Hadron ISO with
auroraboot:auroraboot build-iso -n <name> docker:<hadron-image> # e.g.: docker:quay.io/kairos/hadron:v0.0.4-standard-amd64-generic-v4.0.3-k3s-v1.35.2-k3s1
- Boot that ISO in QEMU against a blank 20 G raw disk with an install cloud-config (same QEMU procedure described below), allowing Kairos to install and power off.
- Verify with
losetup: thestatepartition (labelstate) must be present alongsiderecovery. - Serve the resulting
<name>-installed.raw+.md5from the Ironic httpd.
Quick verification before serving the image:
sudo losetup -fP <name>-installed.raw
# Note the device printed, e.g. /dev/loop0
sudo blkid /dev/loop0p*
# A correctly-installed Hadron disk has both a "state" and a "recovery" partition.
# If you only see "recovery" (no "state"), the image was not fully installed.
sudo losetup -d /dev/loop0The same verification applies to standard Kairos images: COS_ACTIVE corresponds to state, COS_PERSISTENT is the data partition.
The image you provide must have COS_ACTIVE, COS_PERSISTENT, COS_OEM, and COS_STATE partition labels already present (or for Hadron: efi, oem, persistent, recovery, state). The node boots straight into the active, persistent system and reads the config-drive.
The supported method uses a Kairos installer ISO to install onto a blank raw disk in QEMU. The result is the fully-installed disk image.
Inputs required:
- A Kairos installer ISO at the exact k3s or k0s version you intend to run. Example filename:
kairos-ubuntu-24.04-standard-amd64-generic-v3.6.0-k3sv1.33.5+k3s1.iso qemu-system-x86_64with KVM, OVMF firmware, andgenisoimage.
Step 1: Write a minimal install cloud-config.
Create user-data:
#cloud-config
install:
auto: true
device: /dev/vda
poweroff: true # powers off QEMU when install completes
users:
- name: kairos
groups:
- admin
passwd: kairos # temporary; only used during the install VM session
ssh_authorized_keys: []Create meta-data:
instance-id: kairos-install
local-hostname: kairos-installPack them into a cidata ISO:
genisoimage -output cidata.iso \
-volid cidata \
-joliet -rock \
user-data meta-dataStep 2: Create a blank raw disk.
Size the disk to match or be smaller than the node's physical disk, with margin:
qemu-img create -f raw kairos-installed.raw 20GStep 3: Boot QEMU headless and let the installer run.
qemu-system-x86_64 \
-enable-kvm \
-cpu host \
-machine q35 \
-m 4096 \
-drive file=/path/to/OVMF_CODE.fd,if=pflash,format=raw,readonly=on \
-drive file=kairos-installed.raw,if=virtio,format=raw \
-drive file=/path/to/kairos-installer.iso,media=cdrom \
-drive file=cidata.iso,media=cdrom \
-boot order=cd \
-no-reboot \
-nographicReplace /path/to/OVMF_CODE.fd with your OVMF firmware path (commonly /usr/share/OVMF/OVMF_CODE.fd or /usr/share/edk2/ovmf/OVMF_CODE.fd).
install.poweroff: true causes the Kairos installer to power off QEMU when installation completes. -no-reboot is a safety net. The QEMU process exits when the install finishes; this typically takes 3–10 minutes depending on disk speed.
Step 4: Verify the partition layout.
sudo losetup -fP kairos-installed.raw
# Note the device name printed, e.g. /dev/loop0
sudo blkid /dev/loop0p*
# Standard Kairos — expect labels: COS_ACTIVE, COS_PERSISTENT, COS_OEM, COS_STATE
# Hadron — expect labels: efi, oem, persistent, recovery, state
sudo losetup -d /dev/loop0For standard Kairos: if COS_ACTIVE and COS_PERSISTENT are not present, the install did not complete.
For Hadron: if state is not present (only recovery is visible), the image is an installer/live image, not a fully-installed disk. Do not use it with Ironic — the node will boot into recovery mode and k0s/k3s will never start.
Step 5: Generate checksums and serve over HTTP.
md5sum kairos-installed.raw > kairos-installed.raw.md5
# or for sha256:
# sha256sum kairos-installed.raw > kairos-installed.raw.sha256Serve both files from an HTTP server reachable from the Ironic provisioning network:
# Simple example using Python; use a production-grade server for real deployments.
python3 -m http.server 6180 --directory /path/to/images/The Metal3MachineTemplate.spec.template.spec.image.url and .checksum fields point at these URLs.
The k3s or k0s version is fixed at image-build time. KairosControlPlane.spec.version and KairosConfigTemplate.spec.kubernetesVersion are informational on the Metal3 whole-disk path — they do not change what runs on the node. Set both fields to match the version bundled in the image you built. If there is a mismatch, CAPI may report incorrect version information and future upgrade tooling will not work correctly.
-
Bare-metal environment:
- Physical or emulated servers with IPMI, Redfish, or iDRAC BMC access.
- Ironic and baremetal-operator v0.13+ deployed and accessible from the management cluster.
- A DHCP server on the provisioning network, or pre-assigned IP addresses.
- An HTTP server serving the disk image and its checksum, reachable from the provisioning network.
-
Management cluster: A Kubernetes cluster with network access to the Ironic API and to the workload nodes.
-
Cluster API core: v1.13.3+ required (v1beta2 contract). CAPM3 v1.13 stores its CRDs at the
v1beta2contract, so the management cluster's CAPI core must servev1beta2. (This provider's typed client additionally relies oncluster.x-k8s.io/v1beta1still being served; CAPI continues to serve both, so no action is needed.) The manifests in this guide are authored inv1beta2. Verify:kubectl api-versions | grep cluster.x-k8s.io -
CAPM3: Cluster API Provider Metal3 v1.13+ installed.
kubectl get crd metal3clusters.infrastructure.cluster.x-k8s.io
-
Kairos CAPI Provider: v0.1.0-beta.1+ installed (see INSTALL.md).
-
Fully-installed Kairos disk image: See "Building the disk image" above.
-
BareMetalHost registered: At least one
BareMetalHostinavailablestate. BMO must have inspected it successfully:kubectl get baremetalhosts -A # Look for provisioning state "available"
Install CAPM3 using clusterctl or the upstream manifests. Refer to the Metal3 documentation for the current install procedure. The Kairos CAPI provider is installed separately:
kubectl apply -f https://github.com/kairos-io/cluster-api-provider-kairos/releases/download/v0.1.0-beta.1/kairos-capi-provider.yamlSee INSTALL.md for the full provider install and verification steps.
If you have not already registered the target server, create the BMC credentials Secret and BareMetalHost object. Templates for both are included (commented out) in the sample manifest. The BareMetalHost spec.online: true allows BMO to inspect and then provision the host.
# Edit the commented-out sections at the bottom of the sample manifest,
# then apply:
kubectl apply -f config/samples/capm3/kairos_cluster_k3s_single_node.yamlWait for the host to reach available state before proceeding:
kubectl get baremetalhosts -w
# Provisioning state transitions: registering → inspecting → availablekubectl create secret generic kairos-user-password \
--from-literal=password=$(openssl rand -base64 32)The sample manifests reference this Secret via userPasswordSecretRef. Do not set userPassword inline.
- k3s single node:
config/samples/capm3/kairos_cluster_k3s_single_node.yaml - k0s single node:
config/samples/capm3/kairos_cluster_k0s_single_node.yaml
Open the chosen sample and fill in all values marked TODO.
spec:
controlPlaneEndpoint:
host: "192.168.1.100" # replace with the node's actual or reserved IP
port: 6443Set the same IP in both Cluster.spec.controlPlaneEndpoint.host and Metal3Cluster.spec.controlPlaneEndpoint.host.
spec:
template:
spec:
image:
url: "http://192.168.222.2:6180/images/kairos-k3s-installed.raw"
checksum: "http://192.168.222.2:6180/images/kairos-k3s-installed.raw.md5"
checksumType: md5 # or sha256 / sha512
diskFormat: rawThe checksumType must match the hash algorithm used to produce the checksum file.
# In KairosControlPlane:
spec:
version: "v1.34.3+k3s3" # must match the version bundled in your disk image
distribution: k3s
# In KairosConfigTemplate:
spec:
template:
spec:
distribution: k3s
kubernetesVersion: "v1.34.3+k3s3" # must match spec.version above
# Note: no install: block — Ironic already wrote the OS to diskk3s:
kubectl apply -f config/samples/capm3/kairos_cluster_k3s_single_node.yamlk0s:
kubectl apply -f config/samples/capm3/kairos_cluster_k0s_single_node.yamlkubectl get cluster kairos-m3 -w
kubectl get kairoscontrolplane kairos-m3-cp
kubectl get machines -n default
kubectl get metal3machines -n default
kubectl get baremetalhosts -n defaultExpected BareMetalHost state progression: available → provisioning → provisioned.
Expected Machine state: Provisioning → Running.
Provisioning bare metal typically takes 5–20 minutes depending on disk write speed and network bandwidth for the image download.
kubectl get secret kairos-m3-kubeconfig \
-o jsonpath='{.data.value}' | base64 -d > kairos-m3-kubeconfig.yaml
kubectl --kubeconfig=kairos-m3-kubeconfig.yaml get nodes
kubectl --kubeconfig=kairos-m3-kubeconfig.yaml get pods -n kube-systemNode-push behavior: The control-plane node posts its kubeconfig to a Secret in the management cluster at bootstrap time. The node must have network reachability to the management cluster's API server (<mgmt-api-server-host>:6443). See INSTALL.md for verification steps.
If the node cannot reach the management API server, enable the opt-in Air-gapped fallback (SSHFallback) on the KairosControlPlane.
CAPM3 supports a 3- or 5-node control plane fronted by a kube-vip virtual IP (VIP), using config/samples/capm3/kairos_cluster_k0s_ha.yaml or kairos_cluster_k3s_ha.yaml. Read the single-node walkthrough above first — the disk-image, BareMetalHost, and credentials-Secret steps are identical. This section covers only what's different for HA.
k0s is the fully-supported HA distribution. k3s HA bring-up works the same way, but replacing a k3s control-plane node afterward leaves an orphaned etcd member requiring manual cleanup (KD-5d) — see README.md — Day-2.
In addition to the single-node prerequisites:
- Three (or five) BareMetalHosts registered and
available, instead of one. - A free VIP address on the nodes' routable network, outside any DHCP pool and not otherwise in use.
Cluster.spec.controlPlaneEndpoint.hostandMetal3Cluster.spec.controlPlaneEndpoint.hostmust equal this VIP, not any individual node's IP. - The NIC name on your Kairos image. kube-vip advertises the VIP on a specific interface (
spec.ha.vip.interface); confirm the name withip linkon a provisioned node. - Per-node identity via
hostnamePrefix, nothostname. All control-plane nodes boot from the same disk image; setting an explicithostnamecollides every Node name. UseKairosConfigTemplate.spec.template.spec.hostnamePrefixso each node gets a distinct name.
Open the chosen HA sample and fill in the TODO placeholders — one BMC-credentials Secret and one BareMetalHost per node, plus these HA-specific fields:
apiVersion: cluster.x-k8s.io/v1beta2
kind: Cluster
spec:
controlPlaneEndpoint:
host: "TODO-REPLACE-WITH-VIP-ADDRESS" # MUST equal spec.ha.vip.address below
port: 6443
---
apiVersion: controlplane.cluster.x-k8s.io/v1beta2
kind: KairosControlPlane
spec:
replicas: 3 # 3 or 5 only — odd counts for etcd quorum
distribution: k0s # or k3s — set explicitly for clarity (see note below)
ha:
vip:
address: "TODO-REPLACE-WITH-VIP-ADDRESS"
interface: "TODO-NODE-NIC" # NIC name from `ip link` on a provisioned node
mode: ARPSetting spec.distribution explicitly on the KairosControlPlane is recommended. An explicit value always wins. If left unset, the controller inherits spec.distribution from the referenced KairosConfigTemplate (falling back to k0s only if neither sets one), so a k3s HA manifest that sets distribution: k3s only on the KairosConfigTemplate still provisions k3s. Setting it explicitly on both resources removes any ambiguity and is what both HA sample files do; if an explicit KairosControlPlane value ever disagrees with the template's, the controller emits a DistributionOverride warning Event and the explicit value wins.
kubectl apply -f config/samples/capm3/kairos_cluster_k0s_ha.yaml
kubectl get kairoscontrolplane kairos-m3-ha-cp -wHA comes up successfully when:
kubectl get kairoscontrolplane kairos-m3-ha-cp \
-o jsonpath='{.status.readyReplicas}/{.status.replicas}'
# expect: 3/3Check EtcdHealthy and confirm the VIP answers the API:
kubectl get kairoscontrolplane kairos-m3-ha-cp -o yaml | grep -A4 "type: EtcdHealthy"
curl -k https://<vip-address>:6443/livezSame failure modes as CAPV HA troubleshooting — the KairosControlPlane/etcd behavior is provider-independent. Metal3-specific additions:
| Symptom | Cause | Action |
|---|---|---|
| All three Nodes register with the same name | hostname was set instead of hostnamePrefix in the KairosConfigTemplate |
Switch to hostnamePrefix; each node then derives a distinct name from its machine ID. |
| A control-plane node comes up running the wrong distribution | An explicit KairosControlPlane.spec.distribution disagrees with the KairosConfigTemplate's (explicit KCP value always wins — check for a DistributionOverride warning Event on the KairosControlPlane), or the disk image doesn't match the resolved distribution |
Set spec.distribution explicitly on the KairosControlPlane to match the KairosConfigTemplate, and confirm the Metal3 image is built for that distribution. |
| Field | Resource | Description |
|---|---|---|
spec.controlPlaneEndpoint.host |
Cluster, Metal3Cluster | The control-plane node's stable IP. Set identically in both resources. |
spec.controlPlaneEndpoint.port |
Cluster, Metal3Cluster | API server port. Default: 6443. |
spec.template.spec.image.url |
Metal3MachineTemplate | HTTP URL of the fully-installed Kairos raw disk image. |
spec.template.spec.image.checksum |
Metal3MachineTemplate | HTTP URL of the checksum file (one-line md5sum / sha256sum output). |
spec.template.spec.image.checksumType |
Metal3MachineTemplate | Hash algorithm: md5, sha256, or sha512. Must match the checksum file. |
spec.version |
KairosControlPlane | k3s/k0s version bundled in the disk image. Informational; must match the image. |
spec.template.spec.kubernetesVersion |
KairosConfigTemplate | Same version as above; must match KairosControlPlane.spec.version. |
spec.template.spec.distribution |
KairosConfigTemplate | k3s or k0s. Must match KairosControlPlane.spec.distribution. |
| Field | Values | Notes |
|---|---|---|
diskFormat |
raw |
Ironic whole-disk deploy; only raw is validated for this path. |
checksumType |
md5, sha256, sha512 |
md5 has the widest Ironic version compatibility; sha256 is preferred for new deployments. |
| Field | Notes |
|---|---|
install |
Must be absent. Ironic has already written the OS. Adding an install: block causes Kairos to attempt a second install on the provisioned disk. |
role |
Set to control-plane for the control-plane template. |
distribution |
Must match KairosControlPlane.spec.distribution. |
Metal3Cluster.spec.cloudProviderEnabled: false disables the external cloud controller manager. CAPM3 v1.13 with this setting finds the Kubernetes Node by the metal3.io/uuid label and validates providerID=metal3://<BareMetalHost-UID> — it does not set them. The Kairos cloud-config sets both automatically from the Ironic config-drive metadata (the config-2 drive's meta_data.json .uuid): on k3s via a config.yaml.d kubelet drop-in applied when the node registers; on k0s via a post-bootstrap kubectl patch of the node (setting a previously-empty providerID is permitted by the API server). Either way, no manual providerID patching is required — the node sets it itself.
This section covers behavior specific to Kairos Hadron images deployed via Metal3. Hadron is the musl-libc-based next-generation Kairos OS; it shares the same Metal3 provisioning flow but has a few differences from standard (glibc) Kairos images.
The Metal3 config-drive network_data DHCPs the node's data-plane NIC. For single-node clusters where the controlPlaneEndpoint host must match a fixed IP, the DHCP-assigned address is non-deterministic and will not automatically match what you set in the manifest.
Two approaches, in preference order:
-
DHCP reservation — pin the IP at the network layer, tied to the
BareMetalHost.spec.bootMACAddress. The IP is stable before first boot and requires no cloud-config changes. This is the most reliable approach. -
spec.files+ systemd-networkd — write a.networkunit file matching the data-plane NIC's MAC address and set a static address equal to thecontrolPlaneEndpointhost. The file is delivered via the cloud-configwrite_files:list (seeKairosConfigTemplate.spec.template.spec.files).Example
KairosConfigTemplatesnippet:spec: template: spec: files: - path: /etc/systemd/network/10-cp.network content: | [Match] MACAddress=aa:bb:cc:dd:ee:ff [Network] Address=192.168.1.100/24 Gateway=192.168.1.1 DNS=192.168.1.1 permissions: "0644" owner: "root:root"
Replace
MACAddress,Address,Gateway, andDNSwith your network's values. TheMACAddress=match key is more reliable than an interface name on bare metal where NIC names can vary by firmware.Caveat: this is the same static-IP race condition described in the API reference. On a pre-installed disk image, systemd-networkd may already hold a DHCP lease when the file lands. The static IP takes effect after the next
networkctl reload; if k0s or k3s has already bound the DHCP address, the running process will not switch addresses until the node is rebooted. The DHCP reservation approach avoids this race entirely. See Writing files to nodes — Static IP via systemd-networkd for the full explanation.
Kairos Hadron uses musl-libc, which resolves file ownership by name rather than by numeric UID/GID. The provider emits owner: root (by name) in all write_files: entries, not owner: 0 (by UID). This is the correct behavior for Hadron and is also accepted by glibc-based Kairos images. If you author your own spec.files entries, use owner: "root:root" (name form) — numeric owners such as "0:0" are rejected by the webhook and would fail on Hadron regardless.
kubectl describe baremetalhost kairos-m3-node-0 -n default
kubectl logs -n baremetal-operator-system deployment/baremetal-operatorCommon causes:
- Image server unreachable from Ironic: verify Ironic can reach the HTTP server. The image URL is resolved from the Ironic pod, not the management cluster's API server.
- Checksum mismatch: regenerate with
md5sum <image>and verify the.md5file content is exactly<hash> <filename>(two spaces between hash and name — standardmd5sumoutput format). - Wrong
checksumType:checksumType: md5with a sha256 file (or vice versa) causes Ironic to reject the image silently. Match the type to the algorithm used. - BMC credentials wrong or BMC unreachable: verify IPMI/Redfish connectivity:
ipmitool -I lanplus -H <bmc-ip> -U <user> -P <pass> chassis status
This means an auroraboot auto-reset image was deployed instead of a fully-installed disk. Re-read the "Building the disk image" section. The cos-recovery partition is an ephemeral overlay and cannot host a persistent upper directory. Re-provision with a correctly-built image.
Node provisions (Hadron) but Machine stays Provisioning; kairos-agent reports boot_mode=recovery_boot
This is the Hadron-specific form of the same root cause. On a Hadron image that has not been fully installed, kairos-agent reports boot_mode=recovery_boot at boot. The consequences:
k0scontrollerstaysdisabled;/etc/k0sand/var/lib/k0sare never created.- k3s similarly does not start.
- The CAPI Machine stays in
Provisioningindefinitely with no further error.
Check this condition on the node (via console or SSHFallback):
kairos-agent status | grep boot_mode
# boot_mode=recovery_boot → wrong image
# boot_mode=active_boot → correct imageAlso check the partition table:
lsblk -o NAME,LABEL
# "state" partition absent → image was not fully installedReprovision with a fully-installed Hadron disk. See the "Hadron images: additional failure mode" subsection under "Building the disk image" above for the build pipeline.
The node k3s/k0s is running but the kubeconfig has not reached the management cluster. Causes:
- No network route from the node to the management cluster API server: run
curl -k https://<mgmt-api-server-host>:6443/apifrom the node. If it fails, either open the network path or enable SSHFallback. - bootstrap-Secret naming: if
BareMetalHost.spec.userDatareferences a Secret that does not exist, upgrade the provider to v0.1.0-alpha.2+. The deterministic bootstrap-Secret naming fix shipped in alpha.2 and is required for unattended Metal3 provisioning.
kubectl describe kairosconfig <config-name>
kubectl logs -n kairos-capi-system deployment/kairos-capi-controller-managerIf KairosConfig.status.failureMessage is set, the issue is transient — it clears when resolved. A missing kairos-user-password Secret is the most common first-run cause.
kubectl describe metal3cluster kairos-m3
kubectl logs -n capm3-system deployment/capm3-controller-managerVerify that cloudProviderEnabled: false is set. With cloudProviderEnabled: true (the default), CAPM3 expects a cloud controller manager to be running, which will never be satisfied in this configuration.
If the node's IP changes after provisioning (DHCP lease reassignment), the controlPlaneEndpoint becomes stale. Prevention: set a DHCP reservation tied to the BareMetalHost MAC address. Recovery requires updating both Cluster.spec.controlPlaneEndpoint.host and Metal3Cluster.spec.controlPlaneEndpoint.host and restarting the node's API server with the correct bind address.
- BMC credentials: store in a
kubernetes.io/opaqueSecret in the same namespace as the BareMetalHost. Do not set BMC credentials inline anywhere. The sample manifest shows the correct pattern. - User password: provide via
userPasswordSecretRef. The webhook rejectsKairosConfigobjects with no credential. Do not setuserPasswordinline outside of throwaway testing. - Image integrity: use
checksumType: sha256(or sha512) in production. md5 is shown in samples for compatibility; it is not a security-grade algorithm. Ironic verifies the checksum before deploying. - Image server: serve disk images over HTTPS if the provisioning network is not isolated. An unauthenticated HTTP image server on a shared network allows image substitution.
- SSH access: add
sshPublicKeyorgithubUsertoKairosConfigTemplateif you need direct node access. Password-based SSH should not be the primary access mechanism. - Provisioning network: treat the Ironic provisioning network as a trusted, isolated segment. Ironic PXE-boots nodes on this network; a compromised provisioning network allows arbitrary OS injection.
- Configure worker nodes via
MachineDeploymentwith aMetal3MachineTemplateand a worker-roleKairosConfigTemplate. - Add custom Kubernetes manifests via
spec.template.spec.manifestsinKairosConfigTemplate. - HA control planes (
replicas: 3/5with a kube-vip VIP viaspec.ha.vip) are supported on CAPM3 for both k0s and k3s — see High-Availability control plane above and the README HA section. - Static IPAM via Metal3DataTemplate / Metal3IPPool is a future phase.
When to use: the bare-metal node has no network route to the management cluster API server (<mgmt-api-server-host>:6443). The node-push path — where the node POSTs its kubeconfig to a management cluster Secret — is unreachable. Without this fallback, KubeconfigReadyCondition stays False(WaitingForNodePush) indefinitely.
When NOT to use: the default node-push path works in most networks. Do not enable SSHFallback unless you have confirmed the node cannot reach the management API server. The fallback is an escape hatch, not a replacement for node-push.
Security requirement: host-key verification is mandatory. The controller verifies the workload node's SSH host key against a known_hosts Secret before any data is exchanged. There is no trust-on-first-use mode. activateAfter must be at least 15 minutes (greater than the KubeconfigReadyCondition Info→Warning threshold of 10 minutes).
kubectl create secret generic kairos-ssh-identity \
--type=kubernetes.io/ssh-auth \
--from-file=ssh-privatekey=/path/to/your/private_key \
-n defaultThe corresponding public key must already be installed on the node via KairosConfigTemplate.spec.template.spec.sshPublicKey or githubUser.
Obtain the node's SSH host key while you still have network access (or from the Kairos image's pre-generated host key material):
ssh-keyscan <node-ip> > known_hosts_file
kubectl create secret generic kairos-ssh-known-hosts \
--from-file=known_hosts=known_hosts_file \
-n defaultspec:
sshFallback:
enabled: true
user: kairos # must match the node's SSH user
port: 22
activateAfter: 15m # must exceed kubeconfigReadyTimeout (10m)
identitySecretRef:
name: kairos-ssh-identity
knownHostsSecretRef:
name: kairos-ssh-known-hostsBoth Secrets must be in the same namespace as the KairosControlPlane. The webhook rejects cross-namespace references.
kubectl describe kairoscontrolplane kairos-m3-cp -n defaultThe KubeconfigReadyCondition Reason tells you which path is active:
| Reason | Meaning |
|---|---|
KubeconfigReady |
Node-push succeeded. SSHFallback did not fire. |
KubeconfigReadyViaSSHFallback |
SSH fallback supplied the kubeconfig. |
SSHFallbackDialing |
SSH fallback is in progress; wait up to 30 seconds. |
SSHFallbackFailed |
SSH attempt failed. Check Events for the categorized error. |
SSHFallbackMisconfigured |
A referenced Secret is missing, empty, or unparseable. Fix the Secret; the controller retries automatically. |
kubectl delete -f config/samples/capm3/kairos_cluster_k3s_single_node.yamlDeleting the Cluster object triggers CAPM3 to deprovision the BareMetalHost (power off and wipe the provisioned state). The BareMetalHost object itself is not deleted by CAPI — it returns to available state for reuse. Delete the BareMetalHost manually if you want to deregister the server from BMO:
kubectl delete baremetalhost kairos-m3-node-0 -n default