Safeguard
Vulnerability Analysis•

CVE-2026-104795: How OpenShift's Insights Runtime Extractor Lets a Pod Take Over Its Node

CVE-2026-104795 is a container escape in OpenShift's insights-runtime-extractor. Any tenant who can schedule a pod can get root with CAP_SYS_ADMIN on the node. No fix has shipped yet.

Safeguard Security Team
Security Research
9 min read

CVE-2026-104795 is a container escape in the insights-runtime-extractor, a privileged DaemonSet that OpenShift's Insights Operator runs on worker nodes to fingerprint the runtimes inside containers. According to Red Hat's CVE record, any tenant who can schedule a pod can get root with CAP_SYS_ADMIN on the node, and with it the host's CRI-O socket. No interaction is needed, because the extractor scans every container on the node by design. Red Hat rates it Important (CVSS 3.1 8.8). As of 14 October 2026 there is no fixed release, and the public source repository hasn't changed since 24 September.

This post is for OpenShift administrators, especially on clusters shared between teams, and for anyone who builds or runs node agents that look inside containers. It explains the bug from the code, what you can do before a fix ships, a script that flags DaemonSets with the same risky shape, and the general rule it breaks: seteuid() is not a privilege drop.

The short answer

The extractor enters each container's namespaces as root, changes only its effective user ID, and then runs the container's own executable with --version to identify its runtime. Because the real and saved user IDs stay 0, that executable can call setuid(0) and become full root on the node. Red Hat's record says the vulnerability "requires a code fix", meaning a complete privilege drop before anything is executed. Until that ships, the practical choices are to disable the runtime extractor if your Insights Operator version supports it, and to treat pod-scheduling rights on affected clusters as node-level access.

CVECVE-2026-104795 (Red Hat CNA, published 14 October 2026; reported 30 September)
Componentopenshift4/insights-runtime-extractor-rhel9, OpenShift Container Platform 4
SeverityImportant; CVSS 3.1 8.8 (AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
AttackerAnyone who can schedule a pod on a node running the extractor
Fixed inNo fix released as of 14 October 2026; an upstream fix is open as pull request #95

How the extractor works

The Insights Operator collects anonymised data about clusters for Red Hat. Part of that is which runtimes workloads use, such as Node.js, Python or Java frameworks. To find out, it deploys the insights-runtime-extractor DaemonSet to every Linux worker node. In the operator's manifest, the extractor container is privileged: true, adds CAP_SYS_ADMIN, shares the host PID namespace (hostPID: true), and mounts the host's /run/crio/crio.sock.

When the operator asks for a scan, the extractor, written in Rust, walks the containers on the node. For each process, it forks, joins the target container's PID and mount namespaces, and runs a set of fingerprint helpers. One of them identifies runtimes by running the process's executable with --version. The default configuration applies this to processes named node, MainThread, python and python3. Red Hat's record also lists processes whose name contains java as triggers. In the current source, the Java fingerprints read files such as the Java home's release file rather than running the workload's binary, but follow the record and treat Java workloads as in scope too.

Where it goes wrong

Red Hat's CVE record lays out the chain with code excerpts. We checked each point against the current main branch of openshift/insights-runtime-extractor.

1. Only the effective UID is dropped. switch_user() in insights_runtime_extractor.rs calls seteuid(uid) and nothing else. The real and saved-set user IDs remain 0, and the permitted capability set is untouched. A process in that state can switch back to root at any time.

2. The safety check looks at the wrong thing. check_no_privileged_perms() in perms.rs checks that CAP_SETUID and CAP_SYS_ADMIN are absent from the Effective capability set only, so it passes while the permitted set still holds them. It also isn't called at all when the target container runs as UID 0.

3. It executes a path the workload controls. The version fingerprint passes process.command_line[0], the target's argv[0] read from /proc, to a Go helper that runs exec.Command(executable, "--version"). A process chooses its own argv[0], and it resolves inside the container's mount namespace, so the binary that runs is whatever the tenant put there.

4. The working directory points back at the extractor. Before running the fingerprint, the coordinator changes directory back to a file descriptor for the extractor's own root filesystem, which is where the host's CRI-O socket is mounted. The record says the executed process inherits that working directory.

Put together, the record's attack sequence needs only a pod. The tenant runs a process whose name matches one of the configured runtimes, with a binary at its argv[0] path that calls setuid(0) when started. When the next scan runs, the extractor executes that binary with a real UID of 0 in the host user namespace. The binary takes back full root with CAP_SYS_ADMIN, and the inherited working directory gives it the node's container runtime socket. The record calls the result "full node compromise".

We are not publishing exploit code. The record already describes the chain in full, and there is no fix to point people to.

What to do before a fix ships

  1. Decide whether you need runtime fingerprinting. Current Insights Operator source reads a disableRuntimeExtractor setting under dataReporting in the insights-config ConfigMap (namespace openshift-insights, key config.yaml), and its controller removes the DaemonSet when it's set to "true". Check that your OpenShift version supports the setting and Red Hat's guidance before relying on it. You lose the runtime data in Insights reports, not cluster functionality.
  2. Treat pod creation as node access on affected clusters. Until the extractor is fixed or disabled, anyone who can schedule pods on a worker node can potentially get root on it. Review who has that right, especially in shared clusters, CI namespaces and self-service platforms. Security context constraints don't help here: the attacker's pod can be fully restricted, because the privileged code is the extractor's, not theirs.
  3. Watch the advisory. Track the Red Hat CVE page and the erratum. Upstream, pull request #95, opened on 14 October, replaces the seteuid() call with a full privilege drop, always runs the privilege check, and skips executing workload binaries for UID 0 containers. A merged pull request is not a shipped fix, so update when the erratum lands.
  4. Audit your other node agents. This bug comes from a common pattern, which the next section covers.

Audit DaemonSets with the same shape

The risky combination is a DaemonSet that shares the host PID namespace, runs privileged or with CAP_SYS_ADMIN, and mounts a container runtime socket. That describes many security, monitoring and APM agents, and any of them that executes or parses content from inside containers deserves a closer look. This script flags them from manifests, either files or the output of kubectl get ds -A -o yaml:

#!/usr/bin/env python3
"""Flag DaemonSets that can reach into every container on a node: host PID namespace,
privileged containers or powerful capabilities, and container-runtime sockets mounted
from the host. Reads manifests from files or from `kubectl get ds -A -o yaml`.
Usage: python3 node_agent_audit.py manifest.yaml [...]   (needs: pip install pyyaml)"""
import sys, yaml

RUNTIME_SOCKETS = ("crio.sock", "containerd.sock", "docker.sock", "cri-dockerd.sock", "kubelet")
RISKY_CAPS = {"SYS_ADMIN", "CAP_SYS_ADMIN", "SYS_PTRACE", "CAP_SYS_PTRACE", "ALL"}

def daemonsets(doc):
    if not doc:
        return
    if doc.get("kind") == "List":
        for item in doc.get("items", []):
            yield from daemonsets(item)
    elif doc.get("kind") == "DaemonSet":
        yield doc

for path in sys.argv[1:]:
    for doc in yaml.safe_load_all(open(path)):
        for ds in daemonsets(doc):
            meta, spec = ds["metadata"], ds["spec"]["template"]["spec"]
            vols = {v["name"]: v.get("hostPath", {}).get("path", "") for v in spec.get("volumes", [])}
            flags = []
            if spec.get("hostPID"):
                flags.append("hostPID")
            if spec.get("hostNetwork"):
                flags.append("hostNetwork")
            for c in spec.get("containers", []) + spec.get("initContainers", []):
                sc = c.get("securityContext") or {}
                caps = set((sc.get("capabilities") or {}).get("add") or [])
                if sc.get("privileged"):
                    flags.append(f"{c['name']}: privileged")
                if caps & RISKY_CAPS:
                    flags.append(f"{c['name']}: adds {', '.join(sorted(caps & RISKY_CAPS))}")
                for m in c.get("volumeMounts", []):
                    host = vols.get(m["name"], "")
                    if any(s in host for s in RUNTIME_SOCKETS):
                        flags.append(f"{c['name']}: mounts host {host} at {m['mountPath']}")
            risky = spec.get("hostPID") and any("privileged" in f or "adds" in f for f in flags)
            print(f"{'REVIEW ' if risky else 'ok     '} {meta.get('namespace', '-')}/{meta['name']}  ({path})")
            for f in flags:
                print(f"         - {f}")

Run against the Insights Operator's DaemonSet manifest and a typical log shipper:

$ python3 node_agent_audit.py operator-daemonset.yaml log-shipper.yaml
REVIEW  openshift-insights/insights-runtime-extractor  (operator-daemonset.yaml)
         - hostPID
         - extractor: privileged
         - extractor: adds CAP_SYS_ADMIN
         - extractor: mounts host /run/crio/crio.sock at /crio.sock
ok      logging/log-shipper  (log-shipper.yaml)

A REVIEW result isn't a vulnerability. Node agents often need these privileges. It's a list of components that would need to drop privileges correctly, and avoid executing tenant-controlled files, before you could trust them in a multi-tenant cluster.

The rule it breaks: seteuid is not a privilege drop

Code that runs as root and then acts on behalf of a less-trusted party has to drop privileges completely and irreversibly before touching anything that party controls. On Linux that means, in order:

  • clear supplementary groups with setgroups(), then set all three group IDs with setresgid(),
  • set all three user IDs with setresuid(), so the real and saved IDs can't be used to switch back,
  • clear every capability set: effective, permitted, inheritable, ambient and, where possible, the bounding set,
  • set PR_SET_NO_NEW_PRIVS so nothing executed later can regain privileges through setuid binaries or file capabilities.

The CVE record's remediation note asks for exactly this: setting all three UID and GID fields, clearing all capability sets and setting PR_SET_NO_NEW_PRIVS before running any fingerprint. Checking privileges should mean checking the permitted set and the real UID, not just the effective set.

The deeper fix is not to execute workload binaries at all. Version information can often be read from files instead, and the extractor already does this for Java: its Java fingerprint locates the Java home directory and reads its release file rather than running java -version. Reading a file inside a container is safe with care. Running a binary from inside a container, while holding host privileges, puts the agent's trust boundary in the tenant's hands.

The fair view

This is a local, authenticated attack: the tenant needs pod-creation rights. On a single-team cluster where everyone who can deploy is already trusted with the nodes, the practical risk is lower. On shared platforms it's the boundary that matters most. Red Hat published a detailed record two weeks after the report, which gives administrators what they need to assess exposure, even before a fix exists. The lesson applies well beyond one component: any privileged agent that inspects containers is part of your isolation boundary, and should be reviewed with the same care as the container runtime itself.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.