Feature support
What Corium models, what it passes through, and what it deliberately does not do.
This page exists so you can tell, before committing to Corium, whether it will get in your way. The short answer: Corium models the settings most clusters need, and everything else in k0s stays reachable through a passthrough patch. There is no feature you can configure in k0s that Corium prevents you from configuring — only features it does not give a shorter name to.
Anything describing k0s behaviour here links to the official k0s documentation. Corium does not fork or patch k0s; when the two disagree, k0s is right and this page is stale.
Three levels of support
| Level | Meaning |
|---|---|
| Modelled | A first-class field in the corium: schema. Validated, with defaults, and documented in the
configuration reference |
| Passthrough | Not modelled, but reachable verbatim through k0s.patch. Corium neither validates nor interferes |
| Out of scope | Deliberately not supported. Each has a reason below |
Passthrough is not a lesser tier for things that were forgotten. It is where settings belong when they are rarely needed, cluster-specific, or change faster upstream than a schema should. A modelled field is a promise to support that spelling forever; that promise is expensive, so it is made sparingly.
Modelled
Cluster shape
| Feature | Field | Notes |
|---|---|---|
| Single-node cluster | role: single | Implies SQLite storage; cannot gain nodes later |
| Control plane only | role: controller | Runs no workloads |
| Control plane plus workloads | role: controller+worker | Corium passes --no-taints, so the node actually schedules |
| Worker | role: worker | Requires a join token |
| Cluster name | cluster.name | Reaches generated kubeconfig contexts |
| API endpoint | cluster.endpoint | Becomes spec.api.externalAddress, and is added to the certificate |
| Extra certificate names | cluster.subjectAltNames | Appended to spec.api.sans |
Networking
| Feature | Field | Notes |
|---|---|---|
| Pod address range | network.podCIDR | Default 10.244.0.0/16, matching k0s |
| Service address range | network.serviceCIDR | Default 10.96.0.0/12, matching k0s |
| kube-router | network.cni: kuberouter | k0s’s default: networking, policy and service proxy in one component |
| Calico | network.cni: calico | |
| Bring your own CNI | network.cni: custom | k0s deploys nothing; the node stays NotReady until you install one.
Worked example with Cilium |
Storage
| Feature | Field | Notes |
|---|---|---|
| Embedded etcd | storage.type: etcd | Default for every multi-node role |
| SQLite | storage.type: sqlite | Rendered as k0s’s kine. Single controller only, and Corium rejects it with role: controller |
High availability
| Feature | Field | Notes |
|---|---|---|
| Virtual IP for the control plane | ha.enabled, ha.virtualIP | k0s control plane load balancing with keepalived. No external load balancer needed |
| VRRP group | ha.virtualRouterID | Must be unique in the broadcast domain |
| VRRP password | ha.authPass, ha.authPassFrom | Capped at eight characters, because keepalived silently truncates beyond that |
| Interface | ha.interface | Defaults to the one holding the default route |
| Non-multicast networks | ha.unicastPeers | Required on most clouds |
Nodes
| Feature | Field | Notes |
|---|---|---|
| Node name | node.name | Derived from the machine ID when unset |
| Labels | node.labels | Sorted before reaching the command line, so identical input gives an identical command |
| Taints | node.taints | key, value, effect |
| Node address | wireguard[].nodeAddress | Set automatically with HA (excluding the virtual IP), or pinned to a WireGuard overlay interface |
Joining
| Feature | Field | Notes |
|---|---|---|
| Inline token | join.token | |
| Token from a URL or file | join.tokenFrom | HTTPS only |
| Waiting for a token to appear | join.tokenFrom.waitFor | Lets every node start at once instead of in sequence |
| Bearer auth for that URL | join.tokenFrom.authFile |
Add-ons
| Feature | Field | Notes |
|---|---|---|
| Helm charts at bootstrap | addons[] | Rendered into
spec.extensions.helm. No Helm binary, no in-cluster operator |
| Chart repositories | addons[].repository | |
| Chart values | addons[].values | Converted to the YAML string k0s expects |
Upgrades
| Feature | Field | Notes |
|---|---|---|
| Unattended staging | upgrades.automatic: download | Stages a new image, never reboots on its own |
| Unattended reboot | upgrades.automatic: apply | Drains the node, reboots, uncordons. A drain a PDB refuses cancels the upgrade |
| Check schedule | upgrades.schedule | systemd OnCalendar, default daily |
| Version ladder | — | 0.1.0, 0.1 and latest per release. No major rung below 1.0, because a 0.x minor may break. See
upgrades |
| Automatic rollback on failure | — | greenboot health check; a node that cannot run k0s returns to its previous image |
Disks
| Feature | Field | Notes |
|---|---|---|
| Software RAID on spare disks | raid[] | Levels 0, 1, 5, 6, 10; created before k0s starts |
| Hot spares | raid[].spares | Pulled in automatically when a member fails |
| Filesystem | raid[].filesystem | ext4 (default), xfs, or none for a raw device |
| Mount and persist | raid[].mountPoint | Written to /etc/fstab by UUID |
| Refuses to destroy data | raid[].wipe | Off by default; a device holding data stops the bootstrap |
| Root filesystem on RAID | — | Works, but install-time only: hand-written Kickstart, a second ESP, and an fstab edit. See software RAID |
Overlay networking
| Feature | Field | Notes |
|---|---|---|
| Host WireGuard interface | wireguard[] | Brought up before k0s so a cluster can run over an encrypted overlay between hosts. See ADR 6 |
| Overlay as the node address | wireguard[].nodeAddress | The kubelet registers this address, not the physical NIC — the field that makes an overlay a cluster transport |
| Peers | wireguard[].peers | Public key, endpoint, allowed IPs, keepalive, optional preshared key |
| Private key as a secret | wireguard[].privateKeyFrom | Resolved at first boot, written 0600, never logged — like a join token |
| k0s waits for the overlay | — | The k0s service is ordered to require the interface, so a node whose overlay failed does not half-join |
This is a host concern, distinct from the pod-traffic encryption a CNI offers:
the overlay is what lets hosts on different sites or providers reach each other at
all. wireguard-tools ships present but inert until an interface is declared.
Management API
Off unless asked for. The decision is ADR 4; the commands are cctl.
| Feature | Field | Notes |
|---|---|---|
| The API itself | api.enabled | Off by default. A node with no api: block runs no daemon and binds no port |
| Operator CA, inline | api.operatorCA | The certificate of the CA that signs operator client certificates. Public material, so it is safe in cloud-init in clear |
| Operator CA, resolved | api.operatorCAFrom | The same SecretSource as join.tokenFrom, waitFor included |
| Maintenance mode | api.enabled: true, no CA | The node holds its bootstrap and prints a pairing code on the console until an operator claims it |
| Open enrolment | api.insecure | Drops the pairing code: the first client to reach the node claims it. For benches and controlled provisioning networks. The node says so on its console and records that its claim was unauthenticated |
| Transport | — | JSON over HTTP and mutual TLS on 7443, on the standard library alone: no dependency ships to a node for it |
| Roles | — | Carried in the client certificate’s organisation: corium:readonly, corium:operator, corium:admin. Every route names the lowest role that may call it |
| Node state | — | cctl status: role, cluster, booted and staged image digests, kernel, k0s version and service, greenboot verdict, uptime. Read-only |
| Services | — | cctl services lists what the API knows about. Units come from a fixed list, never passed through |
| Journals | — | cctl logs, per unit or across all of them, with --since, --follow and --unit kernel. Bounded: 10000 records, one hour of following |
| Restarting k0s | — | cctl restart --unit k0sworker, at corium:operator. Units that run once — the bootstrap above all — are readable and not restartable |
| Upgrades | — | cctl upgrade <nodes...> --image, one node at a time, stopping at the first that does not come back on the digest it was sent |
| Image validation | — | A node refuses an image its own signing policy would accept unsigned. Not a label check: labels are forgeable |
| Rollback | — | cctl rollback <node> marks the previous image as next to boot, and does not reboot |
| Cordon and drain | — | cctl cordon / cctl drain, at corium:operator, on controllers only: a worker holds kubelet credentials, which cannot evict pods. Draining a worker is kubectl drain with the kubeconfig cctl kubeconfig fetches. A drain a pod disruption budget refuses is not forced, and the node stays cordoned |
| Reboot and shutdown | — | cctl reboot / cctl shutdown, at corium:admin. The node answers before it goes |
| Kubeconfig | — | cctl kubeconfig <node>, at corium:admin. Points at the cluster’s virtual IP where there is one; --server overrides |
| Reset | — | cctl reset --confirm <node name>. Leaves the cluster, erases the bootstrap, forgets its owner, reboots unclaimed — in that order. The reboot takes a staged image if one is waiting |
| CA rotation | — | cctl ca rotate <nodes...> --to <dir>, at corium:admin. A node refuses a CA the caller cannot show a signed certificate for, so rotating cannot lock you out |
| Losing the CA key | — | corium-agent api set-ca --file, run as root on the node. Local only; the node keeps its cluster membership |
| SELinux | — | The daemon runs unconfined, as the agent always has. /var/lib/corium/api carries a type of its own so a confined domain has an anchor; the parent keeps var_lib_t because greenboot and systemd read it |
| Operator PKI | — | cctl pki init and cctl pki issue create the CA and sign client certificates. The CA key stays on your machine |
A node in maintenance mode is not a cluster member: it validates its configuration and waits, rather than joining first and being claimed later. Enrolment carries no configuration — it sends a CA certificate and nothing else, and the node’s role and join token still come from cloud-init.
Where configuration comes from
| Feature | Notes |
|---|---|
| cloud-init | NoCloud, ConfigDrive, OpenStack, EC2, Azure, GCE, Hetzner, VMware, OVF |
| A file on the machine | /etc/corium/config.yaml |
| The kernel command line | corium.config=<url or path>, for PXE |
| An image default | /usr/share/corium/config.yaml |
Passthrough
Everything else k0s exposes is set through k0s.patch, applied verbatim to the
rendered k0s.yaml after Corium is done. Maps merge key by key; lists are
replaced wholesale.
This is verified, not asserted — the example below renders with every field intact alongside the values Corium computed:
corium:
role: controller+worker
k0s:
patch:
spec:
network:
kubeProxy:
mode: ipvs # or nftables, userspace, or disabled
nodeLocalLoadBalancing:
enabled: true # Envoy on each worker, no external LB
type: EnvoyProxy
dualStack:
enabled: true
IPv6podCIDR: fd00::/108
workerProfiles:
- name: default
values:
maxPods: 150
images:
repository: registry.example.com/mirror
featureGates:
- name: SomeGate
enabled: true
konnectivity:
agentPort: 8132Commonly reached this way:
| Area | k0s configuration | Reference |
|---|---|---|
| kube-proxy mode, or disabling it | spec.network.kubeProxy | networking |
| Node-local load balancing | spec.network.nodeLocalLoadBalancing | NLLB |
| IPv6 and dual-stack | spec.network.dualStack | dual-stack |
| kube-router tuning | spec.network.kuberouter | networking |
| Calico tuning | spec.network.calico | networking |
| Cluster domain | spec.network.clusterDomain | configuration |
| API server arguments and ports | spec.api.extraArgs, port, k0sApiPort | configuration |
| Scheduler and controller-manager arguments | spec.scheduler, spec.controllerManager | configuration |
| etcd tuning and external etcd | spec.storage.etcd | configuration |
| Kubelet profiles | spec.workerProfiles | worker profiles |
| Registry mirroring | spec.images | airgap |
| Feature gates | spec.featureGates | configuration |
| Konnectivity ports | spec.konnectivity | configuration |
| Certificate lifetimes | spec.api.ca, spec.storage.etcd.ca | configuration |
| Multiple controllers on kine | spec.storage.kine.dataSource pointing at MySQL or PostgreSQL | configuration |
One passthrough field is worth avoiding: spec.extensions.storage is a
deprecated no-op from k0s 1.31 onwards. It still parses, so a configuration
using it looks accepted and does nothing. Install storage as an add-on instead.
A patch can also override anything Corium computed, including values it treats as load-bearing. That is what makes it an escape hatch rather than a suggestion. Corium checks only that the result is valid YAML.
The second escape hatch is that your document stays an ordinary cloud-config:
write_files, runcmd, users and every other module keep working. Corium is
a guest in that document, not its owner.
Static configuration, not dynamic
k0s can run in two modes. By default it reads k0s.yaml from disk on every
controller. With --enable-dynamic-config it instead stores a ClusterConfig
resource in the cluster, and controllers reconcile against that.
Corium writes a static configuration file. It does not pass
--enable-dynamic-config, which means:
- Changing the configuration means reprovisioning the node, in keeping with the rest of the design: a node is rebuilt from an image, not adjusted in place.
- Mixing controllers that use dynamic config with ones that do not is a documented conflict in k0s. If you enable it, enable it everywhere.
If you want dynamic configuration, three parts of the configuration stay
file-local and per-controller regardless — spec.api, spec.storage, and
spec.network.controlPlaneLoadBalancing — so they must still be identical
across controllers. That is precisely what Corium’s rendering already
guarantees, since every controller in an HA cluster renders the same file.
See k0s: dynamic configuration.
Out of scope
These are decisions, not gaps. Each would be straightforward to add; none should be.
| Not supported | Why |
|---|---|
| k0s Autopilot | Autopilot upgrades Kubernetes by replacing the k0s binary on disk. Corium’s binary lives in the read-only /usr, because Kubernetes ships with the OS. Two upgrade mechanisms, each able to move the version independently, is a worse position than one |
| Fleet management | Corium provisions a node and stops. It does not track, group or reconcile machines. That is a control plane’s job, and there are good ones. The management API does not change this: it is one daemon per node answering for that node, with no registry, no inventory and no desired state. A rolling upgrade is a cctl loop over addresses you supplied |
| A bespoke configuration API | The value of cloud-init is that every cloud, hypervisor and PXE setup already speaks it. The management API is not one: it cannot write a corium: block, and there is no cctl apply-config |
| Arbitrary command execution over the API | There is no exec endpoint and no endpoint taking a command line. One would make the API an SSH with a worse client, and every argument for keeping its surface small would stop applying |
| Forking or patching k0s | Corium configures upstream k0s. A fork would mean owning Kubernetes bugs, which is not a business worth being in |
| Removing add-ons | k0s’s Helm extensions install charts; Corium does not model uninstalling one. Deleting a chart from the configuration leaves the release in place — remove it with kubectl delete chart <name> -n kube-system, per
k0s: Helm charts |
| Multiple Kubernetes distributions | Only k0s. Supporting k3s or RKE2 as well would mean an abstraction that fits none of them properly |
Not supported yet
Honest about the difference: these are absent because nobody has needed them enough, not because they are wrong.
| Missing | Notes |
|---|---|
| Air-gapped bundles | k0s supports
air-gap installs by dropping an image bundle in <data-dir>/images/. Corium can already bake one into the image with a COPY, but there is no corium: field for it and it is untested |
| Worker profiles as a modelled field | Reachable through the patch today |
Defaults
Corium’s defaults match k0s’s wherever k0s has one, so that a Corium node and a hand-configured k0s node agree unless there is a reason to differ.
| Setting | Corium | k0s |
|---|---|---|
podCIDR | 10.244.0.0/16 | same |
serviceCIDR | 10.96.0.0/12 | same |
| CNI provider | kuberouter | same |
| Storage, multi-node | etcd | same |
Storage, role: single | sqlite (kine) | implied by --single |
| Telemetry | off | off |
| Cluster name | corium | k0s |
| Add-on namespace | default | same |
The one deliberate difference is the cluster name, which only affects generated kubeconfig contexts.
Values k0s sets that Corium does not touch — API port 6443, join API port
9443, konnectivity ports 8132/8133, cluster domain cluster.local,
certificate lifetimes — keep their k0s defaults. Run k0s config create on a
node to see the full set as your version defines it.