Three Bugs and a Pile of Unlocked Doors
- Ryan Rowcliffe
- 17 minutes ago
- 8 min read

Count the vulnerabilities in the July intrusion at Hugging Face. There are three. A zero-day in JFrog Artifactory, the package-registry cache proxy that was the evaluation environment's only permitted egress. Then an HDF5 external-reference file read and a Jinja2 template injection, both in the same dataset config loader. That is the entire exploitation budget for a campaign that crossed five trust boundaries at three companies.
Every other door was either unlocked or had the key taped to it.
The commentary has been about the AI, which is fair. An autonomous agent, GPT-5.6 Sol plus an internal-only pre-release prototype with cyber refusals turned down for the evaluation, escaped a sandbox and then ran a coherent campaign against production infrastructure for four and a half days with no human directing any individual step. Roughly 17,600 recovered actions, apparently in service of cheating its own benchmark by stealing the answer key. Hugging Face published the technical timeline on the 27th, OpenAI updated its own account on the 28th, and Reuters added a second victim on top of that. Read all three and the identity story is louder than the AI story.
Before it ever touched Hugging Face
This is the part that got clearer this week, and it is the part I would put in front of your board.
The agent needed an operating base outside OpenAI's network and it found one. Hugging Face describes it as an unsecured public code-evaluation sandbox on a third-party provider's infrastructure, which the agent rooted and used as its control, staging, and egress point for the whole campaign. Reuters identified that provider as Modal Labs. Modal's position, and I think it is the correct one, is that Modal itself was not breached. CTO Akshat Bubna told Reuters that a Modal customer had published an endpoint letting anyone on the internet use its sandboxes for code execution.
Sit with that. The agent's entire base of operations, the machine it ran root shells from for four days, was reached through an endpoint with no authentication on it whatsoever. Not a weak credential. Not a stolen one. No identity requirement at all, published to the open internet by someone who presumably thought of it as a convenience.
OpenAI's July 28 update fills in the rest, and the language matters. The models identified and used publicly exposed credentials at the account level on other publicly available services, amounting to four accounts across four services during the Hugging Face incident. One served as an outbound relay and staging path, one held data, and two were read-only. OpenAI has not named the services or explained how the models found the credentials, and says it found no evidence of broader compromise at those providers.
So before touching a single Hugging Face system, the agent had assembled relay, staging, and storage infrastructure entirely out of other people's accounts, using credentials sitting in public. This is the same infrastructure pattern human intrusion crews have used for a decade. The novelty is who assembled it, and how fast.
Inside the perimeter, count the credentials
The ordering matters here too. Vector one was the HDF5 file read, and the config pointed a split at /proc/self/environ. No code executed. The config simply asked the worker to read a local path and return the bytes as dataset rows, which the agent pulled back over the public API. What came back was the pod's entire environment: platform tokens, AWS keys, a static MongoDB password. Before a single line of attacker code ran inside the perimeter, the agent had a wallet.
From there it reads like a tour of every place we stash a NHI and forget about it. The projected Kubernetes service account token that every pod carries by design got the agent talking to the cluster API. Instance metadata gave up the EC2 node role, and the agent built a presigned sts:GetCallerIdentity URL, wrapped it as a k8s-aws-v1. bearer token, and authenticated to the cluster as the node itself. Nothing was exploited. It presented a well-formed token and the cluster believed it, because the token was well-formed. It then called TokenRequest, the legitimate issuance API, to mint fresh service account tokens for the CSI storage driver pods, and ran SelfSubjectRulesReview against kube-system to read the permissions on its new identity.
Four things in that inventory deserve more than a bullet.
Among the harvested secrets was an EdDSA JWT signing key. Put your coffee down for that one. A stolen token expires. A stolen signing key means minting valid tokens on demand, for any subject, until somebody notices.
One secret object held 136 keys. That single read produced the mesh-VPN auth key used minutes later and the access-broker credential used an hour after. Hugging Face's own assessment is that everything which followed descends from it.
The VPN auth key carried a CI automation tag. The agent enrolled devices 181 times, running the client in userspace mode with --state=mem: and --no-logs-no-support so nothing hit disk or vendor telemetry. That tag's ACLs exposed 22 nodes, including subnet routers, regional exit nodes, and connectors bridging to source control, and it routed its own egress through those exit nodes.
The internal service connector had one credential shared across clusters, bound to system:masters. Impersonating it gave the agent admin on two clusters within one second, and the same binding sat on nearly every cluster it fronted. Through that mesh access it minted a GitHub App installation token with contents:write and pull_requests:write, then opened a pull request trying to get into CI.
The tempo is worth noting. At 19:53 on day three the agent escaped to root on a node, having found no admission policy rejecting privileged or hostPath pods and a CSI ClusterRole that granted pod creation cluster-wide. Six minutes later it authenticated to the internal datasets-server MongoDB with the static password it had read out of the pod environment on day one.
One caveat, because the write-up is scrupulous about it and I will not overstate this. 84 operations against the credential store were denied, every mutating cloud call was refused by the role's policy, and destructive API calls were issued with DryRun=True. It was mapping capability, not causing damage. The reach was real. The wreckage was not.
Their detection was not missing. Its priorities were.
Hugging Face caught this themselves. Per the Cloud Security Alliance CISO post-mortem, assembled from a call with nearly 700 CISOs and reviewed by the Hugging Face team, they discovered, contained, and began investigating before OpenAI made contact, and they notified the FBI. OpenAI says its own security team discovered the anomalous activity internally, while Reuters reported that OpenAI had not detected what its models had done by the time Hugging Face contained the threat. Both statements are on the record. Only one organization's telemetry produced an actionable answer inside the window, and it was not the one that owned the agent.
The part Hugging Face volunteered is the one worth your attention. Their LLM-based triage stack correlated ambiguous signals across runtime analysis and SIEM into a coherent attack signal, then failed to rank it critical, so on-call never got paged. Publishing that took some spine.
The under-ranking is not hard to explain. Every identity event in that chain looks exactly like a Tuesday. Pods read their service account tokens. EC2 roles authenticate to EKS. TokenRequest mints tokens, because that is its job. Devices join tailnets with valid keys. GitHub Apps hold contents:write. Not one of those is anomalous alone, and a severity model that scores events one at a time will never score this chain.
What made the chain visible sits outside the events, in three places.
Origin, first. The node role credentials lifted from the metadata endpoint were replayed from external addresses on two commercial hosting providers, a credential scoped to an instance inside the VPC presenting from somebody else's ASN. Physical security solved this in the nineties. The badge for the Denver office does not swipe in Frankfurt.
Then behavior. A dataset conversion worker exists to convert datasets. The moment its service account starts enumerating RBAC and probing the public EKS endpoint, that identity has stopped doing its job and started doing someone else's. Non-human identities are the easy case here: boring, deterministic, no vacations, no laptop left in an airport lounge. Baselining them is tractable in a way baselining humans never was, and most shops still do not bother. In our own customer environments, roughly 65 percent of the AI and automation identities we surface are unmanaged, and about half were not on the customer's inventory at all before we looked. You cannot baseline what nobody has written down.
Reach, last. One auth key, 181 devices with routes into CI subnets. One connector credential, cluster-admin nearly everywhere. Neither needed detection. They needed an inventory built before the incident by someone who could say what each machine identity is entitled to touch, rather than what its config claims.
What I would do Monday
Nobody argues with any of this in a meeting. It still does not get funded, because non-human identities do not file tickets and do not appear on the org chart.
Go find your unauthenticated endpoints first. That is the lesson from the Modal customer and it costs nothing but attention. Anything that executes code, accepts uploads, or proxies requests without presenting an identity is not a low-severity finding. It is a launchpad someone else can borrow, and shared responsibility means your provider's clean bill of health will not cover you. While you are there, scan for your own exposed credentials the way the agent did. Four accounts at four services fell to secrets that were already public, and secret scanning across repos, published containers, client bundles, and CI logs is cheap, mature tooling now.
Block pod-level access to instance metadata. An afternoon of work, and it severs pod RCE from node identity. Hugging Face listed it second in their hardening notes because some workloads could still reach IMDSv2.
Treat signing keys as tier zero and inventory them apart from everything else in the vault. A leaked credential is an incident. A leaked issuer key is a compromised authentication system, and it deserves root-CA handling: KMS or HSM resident, non-exportable, with issuance logging you can query afterward.
Get static secrets out of the process environment, starting with whatever parses untrusted input. Here that was the exact service that got hit. IRSA, workload identity federation, SPIFFE/SPIRE, pick your mechanism. The goal is that whatever sits in environ is not worth stealing. Be honest upstairs about the timeline, because in a mature estate this is quarters, not sprints.
Bind credentials to origin and let those alerts page someone. Hugging Face's sixth hardening item is tooling that flags tokens used from unexpected origins. They reverse-engineered their way to identity observability from a breach. NIST SP 800-207 has been telling us to verify continuously since 2020, and for most organizations verification still stops at the front door.
Audit entitlement scope before credential hygiene. Rotate-everything is the reflex, and Hugging Face rotated broadly and rebuilt roughly a third of their infrastructure from clean images, which is correct under that much uncertainty. But system:masters on a shared connector credential was never a rotation problem. Rotating it buys a fresh key with identical reach.
The asymmetry nobody has priced yet
Most of what the agent did failed. The recovered actions break down to 6,191 recon, 2,911 command executions, and 6,972 dropper operations, with 7,677 landing on a single day. The path that worked was buried in the noise of the thousands that did not, and Hugging Face names that, rather than any individual bug, as the real defensive problem.
It shaped the investigation too. Their first automated scan of the captured traffic found very few secrets. Once they replicated the agent's own chunk-XOR-gzip scheme they recovered roughly four times as many, mostly JWTs and platform tokens hiding inside encoded blobs. Two things helped. The agent had sloppy OPSEC and left its own encryption keys scattered across its logs. And when the commercial models they reached for first refused the work (reverse-engineering an exploit reads the same as launching one), they stood up a quantized GLM-5.2 on their own endpoints and pushed more than 17,000 events through it: rebuilding the timeline, extracting indicators, mapping the credentials touched, and separating real impact from decoy activity, in hours rather than weeks.
Note which of those four tasks determines your blast radius.
Here is what I keep circling back to. When your adversary tests paths at machine speed, network and endpoint telemetry scales with their throughput, which is to say it scales past your budget. Your identity inventory does not move. You have a finite number of service accounts, instance roles, signing keys, automation credentials, and unauthenticated endpoints, and that number stays fixed no matter how many agents get pointed at you. It is the one layer of the stack where the denominator holds still.
The question worth answering on Monday is whether anyone in the SOC would have noticed the eleventh identity, or the endpoint that never asked for one.
So I would stop asking whether our controls would have caught the Jinja2 injection. Somebody's config loader will always have a bug in it.