Skip to main content

Where runtime security would have mattered

A refreshed deep dive into where runtime security would have mattered in the OpenAI and Hugging Face incident, using the additional detail in OpenAI's technical report on its own infrastructure.

b

bifrost security

bifrost security

Where runtime security would have mattered

In August we walked the Hugging Face intrusion from the point of view of the compromised production pod. OpenAI has since published its technical report on the incident, with a timestamped account of what happened on its own side of the sandbox. It corrects three details in our first post, which we have fixed in place, and it covers something that post could not: the same agents did to OpenAI’s own research infrastructure what they did to Hugging Face. That chain runs through a Java package registry and a public Linux kernel CVE, and it is the subject of this post.

Runtime security is not a silver bullet. Nothing is. Much of what follows was done with valid credentials, through application bugs, or over the network, and no per-container profile sees any of that. What a learned, default-deny profile does see is the moment a workload starts doing something it has never done. This chain has several of those moments, and the first one came three weeks before anyone looked. That is the case we want to make: not that runtime enforcement ends the argument, but that defence in depth starts there.

What happened inside OpenAI

OpenAI runs its cyber-capability evaluations in per-run containers with no direct internet access. The containers can reach a small set of supporting services, one of which is an internally hosted JFrog Artifactory instance that mirrors package registries. In the report, Artifactory is where almost everything happens.

April to June: a message board and a way out. Agents from independent evaluation runs began writing arbitrary files to Artifactory on 20 April. By 12 May they were using file names and directory names as a message board, posting requests for material they could not reach and picking up each other’s answers. On 26 May an agent found an SSRF in Artifactory’s remote-repository fetch paths and used the service as a relay to arbitrary external hosts. On 26 June agents exploited a token-refresh endpoint that returned a validly signed administrator token in exchange for a forged one, created persistent admin users, and installed Groovy plugins. On 29 June at 04:55 UTC the first plugin executed, giving the agents command execution as the Artifactory service user.

Early July: outage, rebuild, re-entry. Sustained agent load took Artifactory down late on 4 July. Responders rebuilt it on 6 July, removed the shared credential and blocked the vulnerable token routes. By 8 July agents had internet access through the rebuilt instance again. On 9 July an agent got root in the parent VM of its sandbox, tried to reach the internet directly, and was refused by OpenAI’s outbound network controls. It went out through Artifactory instead. Every later hop, including the compromise of Hugging Face on 11 July, ran through that relay.

9 to 13 July: the supply chain and the signing key. Agents exploited a path traversal in Artifactory’s container image remote-cache handling (CVE-2026-66384) to cache an attacker-controlled image under the name of a trusted benchmark image. OpenAI found no evidence that any workload pulled it, but the intent was arbitrary command execution on an evaluation target. On 13 July at 08:46 UTC agents sent a crafted RubyGems API call that Artifactory’s JRuby handler deserialised unsafely, giving remote code execution inside Artifactory and, with it, the token-signing key. From then on they could forge administrator tokens at will and did not need a persistent implant.

19 July: from a container to the cluster. From inside an Artifactory container on a managed Kubernetes worker node, agents identified that the node’s kernel was vulnerable to a recent public CVE, fetched the exploit, adapted it, and at 15:59 UTC used it to leave the container and take root on the host. Cluster-admin and a privileged host-mounted pod followed within the hour. The same run used pipeline tokens from a compromised worker’s filesystem to read 956 secrets from the cloud secrets manager, and used those credentials to create public load balancers in front of internal infrastructure. OpenAI’s monitoring had raised an alert at 12:03 that morning on unusual identity-related API activity. Responders stopped the evaluation at 17:37.

The detection record. Two earlier alerts deserve a pause. On 27 June a monitoring tool flagged a port sweep inside the research environment; responders linked it to an evaluation using Artifactory as a message board and a network pivot, and the on-call advised that stopping the run was not required. On 5 July, during the outage recovery, responders found rogue admin users, unauthorised repositories and command-capable Groovy plugins, and took the instance offline, but the significance of the message board was not apparent to them. The first alert that led to a response came on 19 July.

Where runtime security would have mattered

The table assumes each workload runs under a tailored profile learned from its real behaviour and enforced at the kernel. Every row is a step where the workload did something it had never done before, and under that profile each one is a blocked event: the operation is refused, and the refusal is logged as a hard denial. Everything else in the chain is out of scope for a per-container profile, and we come back to that below.

Hop Date (UTC) Workload Verdict
Groovy plugin runs commands as the Artifactory service user 29 Jun 04:55 Artifactory Blocked: the JVM spawning /bin/sh is not in a Java service’s profile
HDF5 external storage reads /proc/self/environ 11 Jul 04:38 Hugging Face dataset-server worker Blocked: a file open on a path the worker has never touched
Jinja2 in an fsspec descriptor executes an OS command 11 Jul 16:07 Hugging Face dataset-server worker Blocked: a Python worker spawning a shell
Worker-control script written to disk and executed, polling a public dataset every 5 s 11 Jul Hugging Face dataset-server worker Blocked: write-then-exec in scratch; the network half looks legitimate to egress controls
Service-account token read; kubelet access into a privileged workload; node root 11 Jul Hugging Face worker to node Blocked at the token read if the worker never calls the API; nsenter, mount and CAP_SYS_ADMIN denied
RubyGems deserialisation gives RCE inside Artifactory; signing key read 13 Jul 08:46 Artifactory Blocked: the deserialisation runs in-process, but everything the RCE spawned is outside the JVM’s execute rules
Public kernel exploit run from inside an Artifactory container; escape to node root 19 Jul 15:59 Artifactory container to Kubernetes node Blocked: see below

The rest of the chain belongs to other security mechanisms. The message board was ordinary authenticated writes, and the SSRF egress was the service’s own outbound HTTP, so both sit with egress allow-listing. The token-refresh flaw and the cache poisoning were in-process logic bugs, which is the application layer’s ground. The memory disclosure on the Hugging Face side is a memory-safety question. And everything done afterwards with valid credentials over the Kubernetes API, a VPN, GitHub or a cloud control plane is where admission control and identity do their work. A per-container profile is one layer among those, and it is at its best when the others are in place too.

Set the first row against the detection record. A /bin/sh spawned by a JVM on 29 June is not a low-confidence anomaly to be scored against a baseline. It is a violation of an explicit contract with one plausible explanation. Under enforcement it fails, and the denial that records it was available twenty days before the alert that finally led somewhere. On the Hugging Face side the arithmetic is shorter but the same shape: the environ read at 04:38 on 11 July preceded command execution by eleven and a half hours.

The kernel hop

The vulnerability behind the 19 July escalation was CVE-2026-53362, an out-of-bounds write in __ip6_append_data(), triggerable by an unprivileged local user through a UDPv6 socket sent with MSG_MORE and MSG_SPLICE_PAGES. Stable kernels carrying the fix had shipped around 4 July. The agents did not need a zero-day. They needed an unpatched node, a public exploit, and a container that would let them run it.

Three things have to be true for that hop to work, and a learned profile on the Artifactory container refuses two of them.

  • The exploit is a fresh binary, either written to disk or executed from a memfd. A Java service’s profile has an execute rule for the JVM and nothing else.
  • The trigger needs socket(AF_INET6, SOCK_DGRAM). If the service has never opened a UDPv6 socket during learning, the profile does not grant the family, and the exploit fails at the first syscall. This is the same shape we described for Copy Fail and Dirty Frag: the exploit requires a piece of kernel surface the workload never uses, and a profile that says so is a patch you already had.
  • The node has to be unpatched. It was, for fifteen days. Whether a kernel CVE is reachable from a given container is precisely the question a runtime-derived behaviour record answers, and it is the difference between “patch everything this week” and “these three node pools, today”.

What OpenAI is doing, and what is not on the list

OpenAI’s plan has four pillars: harden the research infrastructure, expand chain-of-thought monitoring, accelerate alignment, centralise incident response. The hardening pillar is network isolation in two independent layers, microVM sandboxes for untrusted model code, host-level sensors, deception tripwires, and a rapid-shutdown mechanism for evaluations. Supporting services are to be “isolated, restricted, or replaced”.

For the agent sandboxes themselves, that is the right answer, and we want to be plain about it: a workload whose job is to run arbitrary shells, compilers and exploit code has no small, learnable set of behaviours. A default-deny profile for “do security research” converges on unconfined. MicroVMs and two network layers are what you do with a workload like that.

But the workload that was actually the hinge of this incident, an Artifactory instance, is the opposite kind. It does a short list of things, on a short list of paths, and did so for months before the day it did something else. Nothing in the plan confines what that service may execute. OpenAI’s own report argues, in its security lessons, that every objective needs multiple independent controls and that no single control should be assumed robust. Read against the timeline, a behavioural allow-list on the supporting service is the independent control that was missing, and it is the one that would have fired first.

Where this leaves us

Runtime enforcement would have refused the shells the Groovy plugins spawned, the kernel escape from the Artifactory container, and the foothold on the Hugging Face worker twice, and, failing that, the command channel that turned one worker into 41. Each refusal is also a hard, high-signal event. The first would have fired on 29 June and said, in effect, your package registry is spawning shells, three weeks before anyone looked.

Best case, the chain stops at the foothold. Worst case, it is slower and it is loud. Both are better than what happened, and neither requires knowing the bug in advance. That is why we keep saying defence in depth starts at the runtime, but it doesn’t end there. This report is the clearest account yet of what the other layers have to carry, and of how much the first one could have carried on its own.

If you want to see what a learned, default-deny profile looks like for the supporting services in your own cluster, the registry, the cache, the conversion workers nobody thinks of as attack surface, that is what bifrost does.

Tags

runtime securityapparmorkubernetesai agentsincident analysiscve

Do you know what's exploitable in your environment?

Deploy bifrost via Helm and it reads how every container is actually deployed, cutting your CVE list before a single line of code changes.