If you stand up a Kubernetes cluster from scratch in 2024, you are almost certainly choosing between containerd and CRI-O. Docker Engine is out as a CRI runtime, Mirantis's cri-dockerd is a migration aid rather than a destination, and the exotic options — Kata, gVisor — live on top of one of the two mainstream choices anyway.
Most teams pick based on what their distribution ships. EKS, GKE, and AKS default to containerd. OpenShift, MicroShift, and several of the CIS-hardened distributions default to CRI-O. Both are CNCF graduated projects with serious engineering behind them. But the security stories they tell are different enough that it is worth understanding before you commit a large fleet to either one.
Different Design Philosophies, Different Attack Surfaces
containerd started life as the runtime layer extracted from Docker. It was designed to be a general-purpose container manager with the CRI plugin bolted on as one of several frontends. You can use containerd without Kubernetes, and its ctr and nerdctl tools reflect that.
CRI-O started the other way around. It was built specifically to implement the Kubernetes Container Runtime Interface and nothing else. There is no general-purpose command-line tool, no non-Kubernetes use case, no legacy surface area from the Docker days. The entire project exists to be the smallest possible layer between a kubelet and OCI-compliant workloads.
This philosophical difference shows up in the code. containerd's binary is around 70 MB and its CRI plugin is one of several. CRI-O's binary is closer to 50 MB and every line of it is there to serve the kubelet. Attack surface follows size, and by any reasonable measurement CRI-O exposes less of it.
The trade-off is ecosystem depth. containerd has more integrations, more snapshotters, and more escape hatches for unusual deployments. CRI-O is easier to reason about but less forgiving when you want to do something off the paved road.
CVE History Through 2024
Both runtimes have clean track records relative to Docker Engine's historical bug count, but the specific CVEs reveal how the attack surface differs.
containerd's significant 2023-2024 findings included CVE-2024-21626 (the runc fd leak, which affected containerd because it bundles runc), CVE-2023-25153 (OCI image decompression DoS), and CVE-2022-23648 (host path disclosure via crafted images). The pattern is that containerd vulnerabilities tend to live in the image-handling path or in the runc binary it ships with.
CRI-O's 2023-2024 findings included CVE-2024-3154, disclosed in April 2024, which allowed a pod with a carefully crafted ExecSyncRequest to escape confinement through the conmon process. CVE-2022-0811 from early 2022 allowed kernel parameter injection through the pinns helper — that one was bad, and it is the reason CRI-O versions older than 1.23.2 are considered deprecated for any exposed cluster. CVE-2024-7006 was a null pointer dereference in the streaming RPC handler that enabled denial of service.
The pattern in CRI-O's history is that bugs cluster around the helper processes — conmon, pinns — rather than in image handling. Both runtimes have had serious issues. Neither has had a year without one. The mitigation in both cases is the same: track versions, patch fast, and do not assume your distro has done the work for you.
Seccomp, AppArmor, and SELinux Posture
CRI-O has a noticeably stronger default posture on SELinux. Because it is the runtime OpenShift ships with, it has had years of production pressure to make SELinux-by-default actually work without breaking workloads. The container_t and spc_t types are integrated into the runtime's assumptions, and CRI-O will fail closed if SELinux is misconfigured rather than silently running unconfined.
containerd works fine with SELinux but does not assume it. On Ubuntu hosts where AppArmor is the default MAC, containerd is the more natural fit. On RHEL, Fedora, and anything derived from them, CRI-O's tighter integration shows.
Both runtimes support seccomp equally well and both will use Kubernetes' RuntimeDefault profile when asked. The difference is in what happens when you do not ask: CRI-O's bundled default is slightly more restrictive than containerd's, blocking a handful of additional syscalls like perf_event_open that containerd permits by default.
User Namespace Support
Kubernetes 1.30, released in April 2024, promoted user namespaces to beta. Both runtimes support the feature, but the integration quality differs.
CRI-O implemented user namespace support in 1.25 and has iterated on it through the 1.29 and 1.30 releases. The feature is considered stable by the maintainers and is what OpenShift uses to drop default pod privileges.
containerd's user namespace support landed in 1.7 and improved substantially in 1.7.12 and the early 2.0 betas. Through mid-2024 it is still considered best-effort, and several edge cases — particularly around idmapped mounts on kernel 6.1 and earlier — continue to show up in issue trackers.
If user namespaces matter to your threat model, CRI-O is the more mature choice today. containerd will close the gap, but as of the current releases it is not there yet.
Supply Chain and Provenance
Both projects sign releases. Both publish SBOMs. Both participate in the SLSA framework.
CRI-O publishes to its own GitHub releases and through Red Hat's advanced packaging system. Releases are signed with cosign since 1.25, and the SBOMs are attached as GitHub release artifacts.
containerd has been signing releases with cosign since 1.6.0 and publishes SLSA Level 3 provenance attestations for all 1.7 and 2.0 releases. The provenance is verifiable against the GitHub Actions workflow that built the release, which is the strongest public chain of custody among the two.
In practical terms, containerd's supply chain posture is slightly ahead of CRI-O's today. Both are far ahead of where the ecosystem was even two years ago.
Which One to Pick
If you are running OpenShift or a Red Hat derivative, CRI-O is the path of least friction and its SELinux story is genuinely better. If you are on EKS, GKE, or AKS, containerd is what your cloud provider hands you and the quality of their integration is hard to beat. For a green-field cluster on vanilla Linux, I lean toward CRI-O when the threat model prioritises minimal attack surface and user namespaces, and toward containerd when you need ecosystem breadth or plan to use exotic snapshotters.
Neither is a wrong answer. Both reward operators who pay attention to configuration and punish operators who do not.
How Safeguard Helps
Safeguard scans clusters regardless of which runtime they use and normalises the findings so you can compare container security posture across a heterogeneous fleet. We track CVEs for both containerd and CRI-O, flag clusters running versions older than the latest security release, and correlate runtime findings with the workloads that would be affected by a compromise. If you operate a mixed environment — say, OpenShift on CRI-O for regulated workloads and EKS on containerd for everything else — we give you one view of where the runtime-layer risk actually sits. Our policy gates can block deployments to clusters running unsupported runtime versions, which is the kind of automated guardrail that keeps decade-old CVEs from showing up in a new cluster built from last year's playbook.