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

LevelMeaning
ModelledA first-class field in the corium: schema. Validated, with defaults, and documented in the configuration reference
PassthroughNot modelled, but reachable verbatim through k0s.patch. Corium neither validates nor interferes
Out of scopeDeliberately 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

FeatureFieldNotes
Single-node clusterrole: singleImplies SQLite storage; cannot gain nodes later
Control plane onlyrole: controllerRuns no workloads
Control plane plus workloadsrole: controller+workerCorium passes --no-taints, so the node actually schedules
Workerrole: workerRequires a join token
Cluster namecluster.nameReaches generated kubeconfig contexts
API endpointcluster.endpointBecomes spec.api.externalAddress, and is added to the certificate
Extra certificate namescluster.subjectAltNamesAppended to spec.api.sans

Networking

FeatureFieldNotes
Pod address rangenetwork.podCIDRDefault 10.244.0.0/16, matching k0s
Service address rangenetwork.serviceCIDRDefault 10.96.0.0/12, matching k0s
kube-routernetwork.cni: kuberouterk0s’s default: networking, policy and service proxy in one component
Caliconetwork.cni: calico
Bring your own CNInetwork.cni: customk0s deploys nothing; the node stays NotReady until you install one. Worked example with Cilium

Storage

FeatureFieldNotes
Embedded etcdstorage.type: etcdDefault for every multi-node role
SQLitestorage.type: sqliteRendered as k0s’s kine. Single controller only, and Corium rejects it with role: controller

High availability

FeatureFieldNotes
Virtual IP for the control planeha.enabled, ha.virtualIPk0s control plane load balancing with keepalived. No external load balancer needed
VRRP groupha.virtualRouterIDMust be unique in the broadcast domain
VRRP passwordha.authPass, ha.authPassFromCapped at eight characters, because keepalived silently truncates beyond that
Interfaceha.interfaceDefaults to the one holding the default route
Non-multicast networksha.unicastPeersRequired on most clouds

Nodes

FeatureFieldNotes
Node namenode.nameDerived from the machine ID when unset
Labelsnode.labelsSorted before reaching the command line, so identical input gives an identical command
Taintsnode.taintskey, value, effect
Node addresswireguard[].nodeAddressSet automatically with HA (excluding the virtual IP), or pinned to a WireGuard overlay interface

Joining

FeatureFieldNotes
Inline tokenjoin.token
Token from a URL or filejoin.tokenFromHTTPS only
Waiting for a token to appearjoin.tokenFrom.waitForLets every node start at once instead of in sequence
Bearer auth for that URLjoin.tokenFrom.authFile

Add-ons

FeatureFieldNotes
Helm charts at bootstrapaddons[]Rendered into spec.extensions.helm. No Helm binary, no in-cluster operator
Chart repositoriesaddons[].repository
Chart valuesaddons[].valuesConverted to the YAML string k0s expects

Upgrades

FeatureFieldNotes
Unattended stagingupgrades.automatic: downloadStages a new image, never reboots on its own
Unattended rebootupgrades.automatic: applyDrains the node, reboots, uncordons. A drain a PDB refuses cancels the upgrade
Check scheduleupgrades.schedulesystemd OnCalendar, default daily
Version ladder0.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 failuregreenboot health check; a node that cannot run k0s returns to its previous image

Disks

FeatureFieldNotes
Software RAID on spare disksraid[]Levels 0, 1, 5, 6, 10; created before k0s starts
Hot sparesraid[].sparesPulled in automatically when a member fails
Filesystemraid[].filesystemext4 (default), xfs, or none for a raw device
Mount and persistraid[].mountPointWritten to /etc/fstab by UUID
Refuses to destroy dataraid[].wipeOff by default; a device holding data stops the bootstrap
Root filesystem on RAIDWorks, but install-time only: hand-written Kickstart, a second ESP, and an fstab edit. See software RAID

Overlay networking

FeatureFieldNotes
Host WireGuard interfacewireguard[]Brought up before k0s so a cluster can run over an encrypted overlay between hosts. See ADR 6
Overlay as the node addresswireguard[].nodeAddressThe kubelet registers this address, not the physical NIC — the field that makes an overlay a cluster transport
Peerswireguard[].peersPublic key, endpoint, allowed IPs, keepalive, optional preshared key
Private key as a secretwireguard[].privateKeyFromResolved at first boot, written 0600, never logged — like a join token
k0s waits for the overlayThe 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.

FeatureFieldNotes
The API itselfapi.enabledOff by default. A node with no api: block runs no daemon and binds no port
Operator CA, inlineapi.operatorCAThe certificate of the CA that signs operator client certificates. Public material, so it is safe in cloud-init in clear
Operator CA, resolvedapi.operatorCAFromThe same SecretSource as join.tokenFrom, waitFor included
Maintenance modeapi.enabled: true, no CAThe node holds its bootstrap and prints a pairing code on the console until an operator claims it
Open enrolmentapi.insecureDrops 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
TransportJSON over HTTP and mutual TLS on 7443, on the standard library alone: no dependency ships to a node for it
RolesCarried in the client certificate’s organisation: corium:readonly, corium:operator, corium:admin. Every route names the lowest role that may call it
Node statecctl status: role, cluster, booted and staged image digests, kernel, k0s version and service, greenboot verdict, uptime. Read-only
Servicescctl services lists what the API knows about. Units come from a fixed list, never passed through
Journalscctl logs, per unit or across all of them, with --since, --follow and --unit kernel. Bounded: 10000 records, one hour of following
Restarting k0scctl restart --unit k0sworker, at corium:operator. Units that run once — the bootstrap above all — are readable and not restartable
Upgradescctl upgrade <nodes...> --image, one node at a time, stopping at the first that does not come back on the digest it was sent
Image validationA node refuses an image its own signing policy would accept unsigned. Not a label check: labels are forgeable
Rollbackcctl rollback <node> marks the previous image as next to boot, and does not reboot
Cordon and draincctl 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 shutdowncctl reboot / cctl shutdown, at corium:admin. The node answers before it goes
Kubeconfigcctl kubeconfig <node>, at corium:admin. Points at the cluster’s virtual IP where there is one; --server overrides
Resetcctl 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 rotationcctl 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 keycorium-agent api set-ca --file, run as root on the node. Local only; the node keeps its cluster membership
SELinuxThe 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 PKIcctl 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

FeatureNotes
cloud-initNoCloud, ConfigDrive, OpenStack, EC2, Azure, GCE, Hetzner, VMware, OVF
A file on the machine/etc/corium/config.yaml
The kernel command linecorium.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: 8132

Commonly reached this way:

Areak0s configurationReference
kube-proxy mode, or disabling itspec.network.kubeProxynetworking
Node-local load balancingspec.network.nodeLocalLoadBalancingNLLB
IPv6 and dual-stackspec.network.dualStackdual-stack
kube-router tuningspec.network.kuberouternetworking
Calico tuningspec.network.caliconetworking
Cluster domainspec.network.clusterDomainconfiguration
API server arguments and portsspec.api.extraArgs, port, k0sApiPortconfiguration
Scheduler and controller-manager argumentsspec.scheduler, spec.controllerManagerconfiguration
etcd tuning and external etcdspec.storage.etcdconfiguration
Kubelet profilesspec.workerProfilesworker profiles
Registry mirroringspec.imagesairgap
Feature gatesspec.featureGatesconfiguration
Konnectivity portsspec.konnectivityconfiguration
Certificate lifetimesspec.api.ca, spec.storage.etcd.caconfiguration
Multiple controllers on kinespec.storage.kine.dataSource pointing at MySQL or PostgreSQLconfiguration

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 supportedWhy
k0s AutopilotAutopilot 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 managementCorium 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 APIThe 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 APIThere 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 k0sCorium configures upstream k0s. A fork would mean owning Kubernetes bugs, which is not a business worth being in
Removing add-onsk0s’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 distributionsOnly 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.

MissingNotes
Air-gapped bundlesk0s 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 fieldReachable 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.

SettingCoriumk0s
podCIDR10.244.0.0/16same
serviceCIDR10.96.0.0/12same
CNI providerkuberoutersame
Storage, multi-nodeetcdsame
Storage, role: singlesqlite (kine)implied by --single
Telemetryoffoff
Cluster namecoriumk0s
Add-on namespacedefaultsame

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.