Skip to main content
BlogSecurity

containerd runAsNonRoot Bypass: CVE-2026-46680 Fix and Audit

How a crafted image defeats runAsNonRoot in containerd, which versions fix it across 1.7, 2.0, 2.2 and 2.3, and the Pod mitigation and audit steps to run now.

Matthews Wong

Software Engineer

October 202610 min read
containerd runAsNonRoot Bypass: CVE-2026-46680 Fix and Audit

Key Takeaway

CVE-2026-46680 lets a crafted image start as root under containerd even when a Kubernetes Pod sets runAsNonRoot, because an oversized numeric User is resolved as a username. Patch to 1.7.32, 2.0.9, 2.2.4 or 2.3.1 or later; the 2.1 line gets no fix. Until then, set an explicit numeric runAsUser in every Pod.

I read GHSA-fqw6-gf59-qr4w looking for one thing, whether my clusters were in scope, and the answer sat in a single sentence. A container started with a numeric User too large for 32 bits is treated as a username, and a crafted image can map that string to root in its own /etc/passwd. The Pod spec still says runAsNonRoot: true. The container runs as UID 0 anyway.

This post covers the advisory published on 20 May 2026 and tracked as CVE-2026-46680, rated Moderate by GitHub. Everything below comes from the advisory, the containerd release policy and releases page, the earlier related advisory, and Kubernetes' own API and Pod Security Standards sources, all re-read on 17 October 2026. I did not build or run a malicious image, and nothing here is an exploit; it is defender material.

What the advisory actually says

The advisory is titled containerd user ID handling bypass allows runAsNonRoot evasion. Its claim is narrow: containers started with a numeric User value that cannot be parsed as a 32-bit integer are treated as a username instead. If the image's /etc/passwd maps that large numeric string to root, the container runs as UID 0, which can bypass the Kubernetes runAsNonRoot restriction.

Three scoping details matter. GitHub rates it Moderate. The reporter credited is Lei Wang (ssst0n3), who used the containerd security policy. And the crafted image has to be the one you actually run: the advisory's own workaround begins with using only trusted images and limiting image import to trusted users, which suggests the realistic threat is a poisoned or look-alike image reaching a cluster that treats runAsNonRoot as its safety net.

Why a policy check and a runtime can disagree

Kubernetes documents runAsNonRoot as a promise about the image: if true, the kubelet validates the image at runtime to ensure it does not run as UID 0, and fails to start the container if it does. runAsUser is different; it is the UID for the container entrypoint and defaults to the user in the image metadata when unspecified. As I read it, two components therefore interpret the image's User field, one to validate and one to start the process, and the advisory describes the runtime side resolving an oversized number as a name.

That is why the Pod-level fix works. When runAsUser is set explicitly, the image's USER directive no longer decides the UID; the advisory lists exactly this as a workaround. Here are the vulnerable shape and the safer one side by side.

# Wrong: relies on the image's USER directive being honest. On an affected
# containerd, an oversized numeric USER can resolve to root through the
# image's own /etc/passwd, and runAsNonRoot alone did not stop it.
apiVersion: v1
kind: Pod
metadata:
  name: api-wrong
spec:
  securityContext:
    runAsNonRoot: true
  containers:
    - name: api
      image: registry.example.com/team/api:1.4.2
---
# Right: an explicit numeric UID overrides the image metadata (the advisory's
# workaround), and runAsNonRoot stays on as a second, independent check.
apiVersion: v1
kind: Pod
metadata:
  name: api-right
spec:
  securityContext:
    runAsUser: 10001
    runAsGroup: 10001
    runAsNonRoot: true
  containers:
    - name: api
      image: registry.example.com/team/api:1.4.2
      securityContext:
        allowPrivilegeEscalation: false

Affected and fixed versions across four release lines

The advisory gives affected and fixed versions for the 1.7 module and the v2 module. I merged them with the support status from containerd's RELEASES.md and the latest patch numbers shown on the releases page, because a fixed version is only useful if the line is still maintained.

Release lineAffected fromFixed inStatus and latest patch
1.71.7.271.7.32End of life since 30 September 2026. Releases page shows 1.7.36.
2.02.0.42.0.9LTS. Extended support to March 2027 from two named maintainers. Latest 2.0.14.
2.1Inside the v2 range from 2.0.4No fixEnd of life since 3 July 2026.
2.2Inside the v2 range from 2.0.42.2.4Active. End of life 6 November 2026. Latest 2.2.10.
2.3Inside the v2 range from 2.0.42.3.1LTS until 30 April 2028. Latest 2.3.7.

Read the last column before the third. By the policy page, 1.7 reached end of life on 30 September 2026, so a 1.7 node can be patched to 1.7.32 or later but is not a line to stay on. The 2.1 line ended on 3 July 2026 and the advisory says it gets no fix, so the only route is to move to 2.2 or 2.3. The policy says upgrades are supported between sequential minor releases and directly between sequential LTS releases, for example 1.7 to 2.3.

I would patch to the newest patch in your line rather than the minimum fixed version. The releases page shows 2.0.14, 2.2.10 and 2.3.7 on 13 October, each with a hardening change that filters ID-mapping labels from image annotations during unpack, which 1.7.36 does not include. I could not confirm that change is related to this CVE, so treat it as a reason to take the latest patch, not as part of the fix.

The same bug class appeared two years ago

This is not the first advisory in this corner. GHSA-265r-hfxg-fhmg (CVE-2024-40635), also Moderate, says containers launched with a User set as a UID:GID larger than the maximum 32-bit signed integer could overflow, so the container ultimately runs as root. It was fixed in 1.6.38, 1.7.27 and 2.0.4, with the same workaround: trusted images only, and only trusted users importing them.

The new advisory's affected ranges start at 1.7.27 and 2.0.4, the same releases that fixed the earlier one, and it lists the earlier advisory as a reference. The sources do not say whether the earlier fix opened the new path or merely left a neighbouring one open, and I will not guess. The practical reading is simpler: turning a User string into a UID has been wrong twice, so it should never be your only line of defence.

Four mitigations and what each one really covers

The advisory offers three workarounds and the maintainers add a Kubernetes note. I put them next to the permanent fix so the trade is visible.

MitigationWhat it relies onLimit
Patch the runtimecontainerd at or above the fixed version on every nodeNeeds a node rollout, and 2.1 has no fixed release to roll to.
Explicit numeric runAsUserThe Pod securityContext, which the advisory says overrides the image USERMust be set on every workload; an unset one stays exposed on an affected node.
Kubernetes 1.34 or laterThe advisory says these versions appear to enforce runAsNonRoot regardlessHedged by the maintainers themselves; not something to plan around.
Trusted images and restricted importRegistry and image import permissionsReduces who can supply a crafted image; it is not a fix.

Only the first two change what happens on a node you control. The third is an observation, and the fourth is hygiene you should already have.

Do both of the first two. The Pod change costs a manifest edit and protects you in the window while nodes drain and roll; the patch then removes your dependence on that edit being present everywhere.

Auditing a cluster for exposure

Three questions decide your exposure: which nodes run which containerd, which containers leave the UID to the image, and which images carry an oversized numeric User. The commands below answer each from kubectl and, for the third, crane.

# 1. Runtime version per node. Compare against the fixed versions:
#    1.7.32, 2.0.9, 2.2.4, 2.3.1. A node on containerd://1.7.31 is affected.
kubectl get nodes -o custom-columns=NAME:.metadata.name,RUNTIME:.status.nodeInfo.containerRuntimeVersion

# 2. Containers with no runAsUser at pod or container level, so the image's
#    own USER decides the UID. These are the workloads this advisory touches.
kubectl get pods -A -o json | jq -r '
  .items[] | . as $p
  | $p.spec.containers[]
  | select($p.spec.securityContext.runAsUser == null and .securityContext.runAsUser == null)
  | [$p.metadata.namespace, $p.metadata.name, .name, .image] | @tsv'

# 3. Images whose configured User is numeric and above the signed 32-bit limit.
#    The advisory says "cannot be parsed as a 32-bit integer" and the 2024 one
#    says "signed", so 2147483647 is the conservative line. Names and empty
#    values are normal and skipped. Init containers are not covered here.
kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.image}{"\n"}{end}' | sort -u |
while read -r img; do
  user=$(crane config "$img" 2>/dev/null | jq -r '.config.User // ""')
  uid=${user%%:*}                      # drop an optional :GID suffix
  case "$uid" in
    ''|*[!0-9]*) ;;                     # empty or a name: not this check
    *) if [ "${#uid}" -gt 10 ] || [ "$uid" -gt 2147483647 ]; then
         echo "SUSPECT $img User=$user"
       fi ;;
  esac
done

Treat the third check as a lead, not proof. A numeric User that looks too large is only dangerous with a matching entry in the image's /etc/passwd, and I did not test for one. It also skips init containers and reads whichever platform crane resolves, so extend it if your fleet is multi-architecture.

Where this goes wrong

The expensive mistake is treating runAsNonRoot: true as a control on its own. The Restricted profile of the Pod Security Standards requires runAsNonRoot to be true and allows runAsUser to be any non-zero value or undefined. A Pod that leaves runAsUser unset therefore passes admission, and admission only sees the spec, not the image's /etc/passwd. On an affected runtime that Pod can start as root with every check green.

The second trap is the cost of the fix. Pinning runAsUser means the process no longer runs as the user the image was built for, so files owned by the image's own user may stop being readable or writable. That is my reasoning from how UIDs work, not a statement in the sources, so test each workload before rolling the change across the fleet, and prefer images built to run as a known numeric UID.

A node reporting containerd 2.1.x is past end of life and, per the advisory, gets no fix. Patching in place is not possible. Plan the move to 2.2 or 2.3 and, until then, set runAsUser explicitly on every workload scheduled to those nodes.

Do not read the Kubernetes 1.34 line as a mitigation either. The advisory only says versions from 1.34 appear to enforce runAsNonRoot correctly regardless of this bug. Appear is the maintainers' own hedge, and I could not find the underlying change in the sources I read.

Finally, severity labels can mislead in both directions. GitHub rates this Moderate, and other trackers publish different scores, so I would not rank it from a number. A cluster that runs third-party images and relies on runAsNonRoot for isolation should treat it as a priority; one that runs only images it builds can reasonably schedule it with the next runtime upgrade.

What to do on Monday

In order, each step small enough to finish in a day.

  1. Run the node query and list every node below a fixed version for its line, noting any on 2.1.x.
  2. Add an explicit numeric runAsUser to workloads that lack one, starting with namespaces that pull from public registries or accept third-party images.
  3. Roll nodes to the newest patch in the line, which the releases page showed as 1.7.36, 2.0.14, 2.2.10 and 2.3.7, and schedule the move off 1.7 and 2.1.
  4. Run the image audit, check where each suspect image came from, and restrict who can import images into your registry and node runtime.
  5. Re-run the node query after the rollout and record the date and the result so the next audit has a baseline.

Version numbers and dates here were read from the sources on 17 October 2026. The releases page moves, so re-check the latest-patch column before relying on it.

A security context field states an intention; the container runtime is what carries it out. Pin the UID explicitly, patch containerd to a fixed release in your line, and retire the node pools you cannot upgrade, the 2.1 ones first.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is CVE-2026-46680 in containerd?

It is a user ID handling flaw tracked as GHSA-fqw6-gf59-qr4w. A numeric User that does not fit in 32 bits is treated as a username, so a crafted image whose /etc/passwd maps that string to root starts as UID 0. That can bypass the Kubernetes runAsNonRoot restriction. GitHub rates it Moderate.

Which containerd versions fix the runAsNonRoot bypass?

The advisory lists 1.7.32, 2.0.9, 2.2.4 and 2.3.1 as patched versions. The 2.1 line is end of life and gets no fix. Taking the newest patch in your line is safer than the minimum, and the releases page showed 1.7.36, 2.0.14, 2.2.10 and 2.3.7 when I checked.

Can I mitigate it without upgrading containerd?

Yes, partly. Set an explicit numeric runAsUser in the Pod securityContext, which the advisory says overrides the image USER directive. It must be set on every workload, so it protects only the Pods you edit. Using only trusted images and limiting who can import images also reduces risk.

Does Kubernetes 1.34 or later protect me from this?

The advisory says Kubernetes 1.34 and later appear to enforce runAsNonRoot correctly regardless of the bug. That wording is a hedge from the maintainers, so do not treat it as a guarantee. Patch containerd and set runAsUser explicitly instead of relying on it.

Why does the Restricted Pod Security Standard not catch this?

The Restricted profile checks the Pod spec: runAsNonRoot must be true and runAsUser must be undefined or any non-zero value. It never inspects the image's /etc/passwd. A Pod that leaves runAsUser unset can therefore pass admission and still start as root on an affected runtime.