Status: Draft (2026-10-13), based on official documentation + partial measurements. Security defaults and operational procedures were verified by opening the official documentation on 2026-10-13. "Measured" refers to results experienced in the TT Lab course practice VMs, with the version also noted. We did not run an experiment comparing the number of passed checks by running the same auditing tool (such as kube-bench) across all three distributions (Unmeasured). Writing rules: Measured data is the baseline, items that could not be measured are marked as "Unmeasured", and items not found in the documentation are marked as "Unconfirmed".
Table of Contents
- 1. Security Defaults at a Glance
- 2. k3s: Most Things Are Done, the Rest According to the Guide
- 3. RKE2: Turn It On with a Single Line, But the Host Must Be Prepared
- 4. k0s: Separate Users, Equal Treatment for the Rest
- 5. Installation
- 6. Upgrade
- 7. Air-Gapped Installation
- 8. Backup and Restore
- References
1. Security Defaults at a Glance
Part 1, the structural differences seen in become even wider in security. Let's look at the table first.
| Item | k3s | RKE2 | k0s |
|---|---|---|---|
| CIS Benchmark | Passes many items by default, remaining items configured manually according to the hardening guide | profile: cis applied mostly automatically, host preparation is the operator's responsibility |
kube-bench's k0s-1.0 benchmark is documented to pass with only a few exceptions |
| FIPS 140 | Not found in official documentation (Unconfirmed) | FIPS 140-2, Go BoringCrypto build | Open-source edition out of scope, commercial contracts only |
| SELinux | k3s-selinux policy, --selinux to enable |
rke2-selinux policy, automatic if installed via RPM |
There is a guide to enable it in the worker's containerd |
| Pod Security Admission (PSA) default | Kubernetes default (privileged) if not configured separately | Default is privileged, restricted if cis profile | k0s own default is not in the documentation (unconfirmed) |
| Control plane execution user | Single process | Default root, etcd as etcd user for cis profile |
etcd, apiserver, scheduler, and konnectivity as separate users respectively |
| Others | Audit logs and secret encryption must be enabled manually | CVE scanning with Trivy in the build pipeline | Telemetry enabled by default (measured, v1.36.4) |
Looking at the table alone, RKE2 seems ahead, but the three distributions simply target different things. The key is what each does automatically versus what it leaves to the operator.
2. k3s: Most things are handled, the rest per the guide
The k3s hardening guide starts from the premise that default settings alone pass many CIS controls, but not all. Because k3s does not modify the host OS, host-side configurations are handled by the operator. The guide roughly requires the following:
protect-kernel-defaults: trueis enabled, and kernel values expected by the kubelet (vm.panic_on_oom=0,vm.overcommit_memory=1,kernel.panic=10,kernel.panic_on_oops=1) are placed in a sysctl file.- Create a PSA configuration file, pass it to the API server's
admission-control-config-file, and set the default to restricted. Leave kube-system as an exception. - Enable audit logs, secret encryption (
secrets-encryption: true), and NetworkPolicy per namespace.
The documentation separately notes that even after following all guides, several items still require human judgment. FIPS was not found in the documentation. docs.k3s.io/security/fips resulted in a 404 and was absent from the security section list (2026-10-13). It is safer to read this as meaning that if you need FIPS, you should look at RKE2 rather than k3s.
SELinux is supported. The installation script installs the k3s-selinux RPM on compatible systems and enables it via the --selinux flag or configuration file. However, under SELinux, changing the data directory using --data-dir is not supported.
3. RKE2: Enabled with a single line, but the host must be prepared
RKE2's security defaults are concentrated in a single line: profile: cis . It follows the CIS benchmark matching the Kubernetes version (CIS v2.0 or higher based on the guide), and enabling it triggers the following:
- It checks host requirements upon startup, and if they are not met, it stops.
- etcd to run as the
etcduser, and tightens file permissions. - Applies NetworkPolicy to the default namespaces.
- Changes PSA to restricted (with exceptions such as kube-system).
- Enforces kubelet's
protect-kernel-defaultsflag.
Two tasks are left for the operator: placing the CIS-specific sysctl file included in the installation package into /etc/sysctl.d/ , and creating the etcd system user. The reason for choosing to stop rather than fixing it automatically is that if a distribution secretly changes host settings, it could break other software on the same host.
Measurement: What happens when you enable CIS on a running cluster
In a TT Lab RKE2 practice session (v1.36.4+rke2r1, 2026-09-15), we added profile: cis to a server running the default profile and restarted it. The events unfolded as follows (measured).
missing required: user: unknown user etcdrefused to start with. As expected.systemctl stopstopped with and after preparing the etcd user and sysctl to restart, this time etcd panicked because it could not open the database file.- The cause was the static pod seen in Part 1. Even after
systemctl stop, an old etcd container running as root remained, and that old etcd accepted a defragmentation request sent by the newly started RKE2, rewriting the db file with root ownership. Subsequently, the new etcd started as theetcduser could not open that file. rke2-killall.shOnce we also brought down the container left as
and started over, the API became ready in 9 seconds, and RKE2 adjusted the data directory ownership on its own. already running privileged pods remained Running (measured). This is because PSA only inspects creation requests. The day you enable the profile is quiet, and it gets blocked the day you redeploy that pod.
There are two lessons. You enable the cis profile when first setting up . If you must enable it later, do not just stop the service, but bring down the static pods as well before turning it on.
FIPS and SELinux
RKE2's FIPS 140-2 support comes from binaries built with the Go compiler's BoringCrypto module (NIST Certificate No. 4407). The documentation mentions only 140-2 with no mention of 140-3 (unverified). Canal is listed as the CNI rebuilt for FIPS, but Calico is also in the component list on the same page, presenting a discrepancy within the documentation. If you have an upcoming FIPS audit, it is best to check directly with SUSE on which CNI to choose.
SELinux requires rke2-selinux policies and selinux: true , both of which are handled automatically if installed via RPM. If installing via tarball, you must do it manually.
4. k0s: Segregating Users and Leveling the Rest
k0s approaches security from a different angle. As seen in Part 1, it runs control plane components as distinct system users per process, and control plane nodes do not have kubelet or a container runtime, preventing workloads from sitting alongside the control plane.
The CIS document states that when checked against kube-bench's k0s-1.0 benchmark, it passes in the default state with only a few exceptions. The exceptions noted are EventRateLimit, AlwaysPullImages (which conflicts with air-gapped environments), audit logs and audit policies, encryption providers, protect-kernel-defaults, and kubelet service file entries. The document does not state the exact counts of passes and failures.
FIPS is outside the scope of the open source project. A change merged in 2025-12 (PR #6777) removed mentions of FIPS from the README citing this reason, and the homepage notes that it is provided upon "requests and commercial contracts."
PSA is spec.api.extraArgs describes how to pass AdmissionConfiguration. The default in the example is privileged, but I could not find a sentence explicitly stating the default level for k0s itself (unconfirmed).
One more thing, telemetry is enabled by default. In the TT Lab hands-on VM (v1.36.4+k0s.0), k0s config create 's default output showed that telemetry was enabled (measured). If you are in an environment where metrics must not be sent externally, be sure to disable it in the settings before starting.
5. Installation
| k3s | RKE2 | k0s | |
|---|---|---|---|
| Default installation | get.k3s.io script |
get.rke2.io script (tarball or RPM) |
get.k0s.sh , and k0s install controller |
| multiple nodes at once | script + token per node | script + token per node | k0sctl: installs and joins via SSH using a single configuration file |
| kubeconfig | /etc/rancher/k3s/k3s.yaml |
/etc/rancher/rke2/rke2.yaml |
k0s kubeconfig admin is extracted |
| kubectl | Installed together | /var/lib/rancher/rke2/bin(outside the default PATH) |
k0s kubectl(not installed separately) |
k3s and RKE2 share the same model of "running a script on each node." In the practice session, k3s took 8 seconds to install and 11 seconds for the node to become Ready (measured, v1.35.8+k3s1, timed after the VM was already up).
The differentiator for k0s is k0sctl. With a k0sctl.yaml listing host lists and roles, running k0sctl apply logs into each host via SSH to perform installation and joining. The documentation notes that while adding nodes is supported, removing nodes is not handled by k0sctl.
The RKE2 installation script has one environment-dependent part. If /usr/local is read-only or a separate mount point, it extracts to /opt/rke2 . In the practice VM, the root disk was small, so the installation files (128M, measured) almost filled up the root partition, requiring INSTALL_RKE2_TAR_PREFIX to specify the location.
6. Upgrade
k3s and RKE2 use the same tool. For manual upgrades, you re-run the installation script specifying the version (INSTALL_K3S_VERSION , etc.) or channel. Note that if you do not re-supply the arguments given during the initial installation, they will be lost, so it is safer to put configurations in a configuration file rather than arguments. For automated upgrades, system-upgrade-controller maintains Plans. The server Plan updates servers one by one, and the agent Plan waits for the server Plan to finish at the prepare stage before starting. If you give a Plan a channel , it follows the new version, and if you give it a version , it gets pinned to that version. Both documents state not to skip minor versions.
In our practice, upgrading k3s from v1.34.11 to v1.35.8 via script took 11 seconds, and 12 seconds until nodes were Ready, while running pods kept their UIDs, container IDs, and restart counts intact (measured). However, we also noticed an easy-to-miss point in the same practice. The stable channel already pointed to v1.36 on that day (2026-09-15), so following the channel from 1.34 skips two minor versions at once . Neither the installation script nor the Plan CRD prevented this (measured). For automated upgrades, version pinning is recommended over channels.
k0s primarily relies on replacing binaries and restarting, and automation is handled by autopilot . You specify what to change in a single Plan named strictly autopilot (binaries viak0supdate , air-gap image bundles via airgapupdate ). Autopilot updates controllers one by one, verifying that all controllers are ready each time, and then updates workers, typically one by one. You can also enforce sha256 verification of binaries. If you prefer to perform upgrades externally via SSH, you can use k0sctl.
7. Air-Gap Installation
The method for setting up environments without internet access is similar across all three. You transfer the binaries and image bundlesin advance, placing them in designated folders or using a private registry.
| Image storage location | Other methods | |
|---|---|---|
| k3s | /var/lib/rancher/k3s/agent/images/ |
Private registry + registries.yaml, built-in distributed mirror (Spegel) |
| RKE2 | /var/lib/rancher/rke2/agent/images/ |
Private registry + registries.yaml, Hauler, built-in mirror |
| k0s | Generate a list using worker's /var/lib/k0s/images/ |
k0s airgap list-images and bundle manually |
The amount of data to transfer can be compared using the GitHub release file sizes from the resource table in Part 3. To put it briefly, RKE2 is the heaviest because, in addition to binaries, you must transfer runtime images containing kubelet and containerd, as well as CNI images.
8. Backup and Restore
| k3s | RKE2 | k0s | |
|---|---|---|---|
| Default datastore backup | SQLite: copy server/db/ folder |
Automatic etcd snapshots | k0s backup(etcd or kine/SQLite) |
| Automatic snapshots | Every 12 hours for etcd, retains 5 | Every 12 hours, retains 5 per server | Not found in docs (unverified) |
| Send to S3 | --etcd-s3* |
--etcd-s3* |
- directly stream to stdout using |
| Restore | --cluster-reset --cluster-reset-restore-path |
Same method | k0s restore <파일> |
| To be stored together | Server token | Server token | Include PKI in bundle |
With k3s and RKE2, you must store the tokentogether. Because bootstrap data inside the snapshot (such as CA secrets) is encrypted with the token, you cannot use the snapshot when restoring to a different host without the token.
The k0s backup bundle contains PKI, etcd snapshots (or kine/SQLite snapshots), k0s.yaml, manifests, images, and Helm configurations. What gets excluded is PV data and data in cases where external MySQL or PostgreSQL is used behind kine. That database must be backed up separately on the database side.
Measured: Restore command succeeded but nothing came back
This was the most dangerous moment during the TT Lab k0s practice. If you run k0s stop right after k0s restore <파일> and restart, the command finishes without errors or warnings, and the cluster comes up, but ConfigMaps created after the backup remained intact(measured). This happens because etcd uses the existing data directory if present and ignores the extracted snapshot. The sequence should have been k0s stop → clearing existing etcd data → k0s restore → k0s start . Believing that a restore has occurred when it hasn't is the most dangerous situation. A backup is only a backup after you have tested restoring from it.
The restoration of k3s had much clearer results. Restoring with --cluster-reset reset the cluster in 6 seconds, the API came back up 11 seconds after starting, changes made after the snapshot were gone, and the previous DB remained in the etcd-old-<시각> folder (measured, v1.35.8+k3s1).
TT's Note. Turning on a security switch takes just a single line, but laying the groundwork for that line (host preparation) must be done by humans. And make sure to test restoring from your backup at least once. I've had instances where it printed 「Success」 but nothing actually came back.
References
All documents were accessed on 2026-10-13.
- k3s: Hardening Guide https://docs.k3s.io/security/hardening-guide, Advanced Configuration (SELinux) https://docs.k3s.io/advanced, Quick Start https://docs.k3s.io/quick-start, Manual Upgrades https://docs.k3s.io/upgrades/manual, Automated Upgrades https://docs.k3s.io/upgrades/automated, Air-Gap https://docs.k3s.io/installation/airgap, Backup and Restore https://docs.k3s.io/datastore/backup-restore, etcd-snapshot https://docs.k3s.io/cli/etcd-snapshot
- RKE2: Hardening Guide https://docs.rke2.io/security/hardening_guide, FIPS https://docs.rke2.io/security/fips_support, SELinux https://docs.rke2.io/security/selinux, Pod Security Standards https://docs.rke2.io/security/pod_security_standards, Installation Methods https://docs.rke2.io/install/methods, Upgrades https://docs.rke2.io/upgrades/manual·https://docs.rke2.io/upgrades/automated, Air-Gap https://docs.rke2.io/install/airgap, Backup and Restore https://docs.rke2.io/datastore/backup_restore
- k0s: CIS https://docs.k0sproject.io/stable/cis_benchmark/, Pod Security https://docs.k0sproject.io/stable/podsecurity/, SELinux https://docs.k0sproject.io/stable/selinux/, Installation https://docs.k0sproject.io/stable/install/, k0sctl https://docs.k0sproject.io/stable/k0sctl-install/, Upgrades https://docs.k0sproject.io/stable/upgrade/, autopilot https://docs.k0sproject.io/stable/autopilot/, Air-Gap https://docs.k0sproject.io/stable/airgap-install/, Backup https://docs.k0sproject.io/stable/backup/, PR removing FIPS mention https://github.com/k0sproject/k0s/pull/6777
- Kubernetes PSA Default Level: https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-admission-controller/
- Measured: TT Lab 「Kubernetes Distributions — Building from Scratch」 course practice VM logs (RKE2 CIS Profile, k3s Upgrades, k3s Datastore, k0s Backup/Restore, k0s Configuration, 2026-08~09)