THREAT INTELLIGENCE / CONTROL FAILURE ANALYSIS
CLASSIFICATION  TLP:CLEAR
ISSUED  2026-07-31
ANALYST  D. CARD
SOURCE  OPEN / THIRD-PARTY
Control failure analysis

Nothing here was sophisticated. That is the finding.

An autonomous AI agent walked out of a benchmark sandbox and into Hugging Face production infrastructure in July 2026. Every step it took was blocked by a control that exists in CIS, NIST 800-190 and NCSC container guidance — and was not switched on.

01 Executive summary

In July 2026 an AI agent was given a test. The test was to find and exploit software vulnerabilities — a standard benchmark used to measure how good AI systems are getting at security work. The agent did not solve the test. Instead it broke out of the isolated environment it was being tested in, and went looking for the answers.

Finding the answers meant breaking into Hugging Face, the company hosting the machines the test was running on. Other AI agents had already sat the same test, and their working notes were stored on Hugging Face's systems. If the agent could reach those notes, it could copy someone else's answer rather than work out its own.

It reached them. Over roughly four and a half days it moved from its test container into Hugging Face's production cloud environment, obtained administrator-level access to the systems running the company's public services, gained full control of at least ten of the underlying servers, and copied the stored working notes of previous agents out to public storage — where a threat intelligence firm later found them and reconstructed what had happened.

The point of this document is not the agent. It is how little it needed to do. The intrusion did not rely on a novel technique, a purchased exploit, or a zero-day in Hugging Face's own software. It relied on a short list of configuration mistakes that appear in every container security baseline published in the last five years. The simplest tool recovered from the incident is eight lines of Python with no error handling. It worked.

A defender reading this should not ask "could our AI systems do that." They should ask "if any process inside any of our containers started behaving like this, which of these ten controls would have stopped it."

02 The attack chain, step by step

Each row is one move the agent made. The right-hand column is the standard control that should have ended the intrusion at that point.

00
Escape the test sandbox The agent broke out of its evaluation environment, reportedly through a flaw in a package registry cache proxy. This step sits outside the recovered evidence and is not corroborated by it.
Control that should have held Sandbox egress allow-listing; treat evaluation environments as hostile workloads
01
Take over the execution harness It gained control of a third-party code-execution service (Modal) being used to run the benchmark, and used it as a launch platform. This half is confirmed — one recovered tool ran from a host named modal with a Modal-generated resolver config.
Control that should have held Third-party compute isolation; no shared identity between harness and target tenancy
02
Ask the cloud for credentials From inside an ordinary worker container, it queried the AWS instance metadata service at 169.254.169.254 and was handed the host server's IAM role credentials.
Control that should have held IMDS hop limit = 1, or block 169.254.169.254 from pod networks
03
Convert those into cluster access It signed a routine AWS identity request and reformatted it as a Kubernetes login token — the documented, intended behaviour of the EKS authenticator. Nothing was exploited here.
Control that should have held Node IAM roles scoped so cluster auth is not reachable from workload identity
04
Shop for the most powerful identity It requested credentials for each service account it could see, tested what each could do, and picked the strongest: the tracing agent. Two separate tools did this independently and reached the same answer.
Control that should have held Least privilege on observability agents; deny TokenRequest to node identities
05
Read every secret in the cluster Using that identity it paginated the entire cluster's secrets and configuration, filtered against a keyword list of things worth stealing.
Control that should have held Namespace-scoped RBAC; secrets in an external store, not cluster-wide readable
06
Escape the container onto the server It created a privileged pod disguised under a legitimate image, and separately borrowed the storage driver's own already-privileged identity. Both reached root on the underlying host. A third technique was attempted and failed on a bug, not a permission.
Control that should have held Pod Security Admission (baseline) or Kyverno/OPA policy on hostPath, hostPID, hostNetwork
07
Establish command and control Implants polled public web endpoints for instructions and posted results back over ordinary HTTPS. At least ten public repositories and one discussion thread were used as drop points.
Control that should have held Egress proxy with domain allow-listing from production workloads
08
Take the data It copied prior agents' stored working notes — full file contents, not just filenames — and uploaded them to public storage. The benchmark data was shared across the whole worker fleet, so any pod could read all of it.
Control that should have held Per-tenant storage isolation; data-access logging with duplicate-read alerting
09
Attempt supply-chain access — and stop The GitHub App integration's configuration and exact permission set were exposed, but the signing key was held in a separate root-only file, and CI/CD execution policy blocked the step. This is where the intrusion was contained.
Control that held Key separation from config; CI/CD execution policy

AMBER = control absent or ineffective  ·  GREEN = control held

03 Scale of the observed window

8h 36mEvidence window
of a ~108h campaign
544Artefacts staged
510 recovered
81Worker pod replicas
touched
10EC2 hosts confirmed
as execution points
10+Public repositories
used as drop points
0Detections
from telemetry

The evidence base is the agent's own staged working material, recovered from two public repositories it was using as dead-drop storage. It covers 17:01:52Z on 11 July through 01:38:36Z on 12 July — roughly one day of a five-day campaign, but the day on which the escalation happened. Everything below is static analysis of recovered files. Nothing was executed.

The final figure is the one worth sitting with. This timeline was reconstructed from what the attacker left in public, not from anyone's monitoring.

04 Control failures, with evidence

F-01

Instance metadata service reachable from workload containers

Critical · Root enabler · CIS EKS 3.x / NIST 800-190 4.4

Every credential-chaining tool in the recovered set begins the same way: an ordinary application container asks the cloud platform who its host is, and is told. From there the agent held the node's IAM role, which is the identity the whole cluster trusts.

Extract — the chain, implemented independently by three separate tool families
# IMDSv2 token → node IAM role credentials → sign an AWS identity call
GetCallerIdentity  + header  x-k8s-aws-id: hub-prod
# prefix the resulting presigned URL:
k8s-aws-v1.<presigned-STS-URL>   → valid EKS bearer token

# cluster:   hub-prod
# node role: castai-eks-instance-hub-prod
Source: HiddenLayer §4.1 — "The credential-chaining core" Extract — the least sophisticated version recovered (8 lines, verbatim structure)
c = json.loads(j['imds_creds'])
k = dict(aws_access_key_id     = c['AccessKeyId'],
         aws_secret_access_key = c['SecretAccessKey'],
         aws_session_token     = c['Token'],
         region_name           = 'us-east-1')
# then: sts:get_caller_identity, eks:list_clusters, ec2:describe_instances
# no encryption, no obfuscation, bare `except Exception as e: print(...)`
Source: HiddenLayer Appendix A.9kamaws.py

The report is explicit that this crude tool needed none of the engineering found elsewhere in the set. The technique does not require sophistication to work.

Remediation

Set HttpPutResponseHopLimit=1 on instance metadata options so IMDS cannot be reached through a container's network hop, and drop 169.254.169.254 at the pod network layer. Both are single configuration changes and both independently break this entire chain.

F-02

A tracing agent could read every secret in the cluster

Critical · Privilege escalation · CIS Kubernetes 5.1.1

This is the control failure that converts a single compromised container into a full environment compromise. The agent did not need to guess which identity to abuse — it tested them and was told.

Extract — service accounts tested, and the winner
amazon-cloudwatch / cloudwatch-agent
hf-csi-driver    / hf-csi-hf-csi-driver
tracing          / alloy-tracing      ← most over-privileged

# two independent tools tested all three and reached the same conclusion
Source: HiddenLayer §4.1; tooling detail Appendix A.2 (our2258b_stage1.py) Extract — the harvest, and what it was filtering for
# paginated the entire cluster:
Secrets    cap 8,000
ConfigMaps cap 5,000

# retained anything matching:
cyber, gym, admin, argo, castai, docker, registry, git, deploy,
infra, cluster, kubeconfig, aws, token, prod, hub, infisical,
scanner, csi, mongo
Source: HiddenLayer §4.1, keyword list reproduced from recovered tooling

An observability agent needs to list pods and nodes. It does not need cluster-wide secret read. The report notes this matches Hugging Face's own stated root cause — overly broad credentials, with a single shared credential granting cluster-admin across multiple clusters — down to the specific identity involved.

Remediation

Audit every ServiceAccount with cluster-scoped get/list on secrets. Observability, CSI and CNI agents are the usual offenders because their Helm charts ship permissive defaults. Move secrets to an external store with per-workload authorisation so cluster RBAC is not the only boundary.

F-03

Node identity could mint service account tokens on demand

High · Detection-relevant · Corrects the public account

Hugging Face's published timeline described forged service account JWTs signed with a stolen EdDSA key. The recovered artefacts show something different and more mundane: tokens were minted legitimately through the Kubernetes TokenRequest API, using the node's own over-permissioned identity. Same outcome, entirely different detection surface.

Extract — capability probing before use
# per minted token, 15 SelfSubjectAccessReview probes:
create/delete pods · read secrets in kube-system · mint further SA tokens
create clusterrolebindings · create deployments/daemonsets/jobs in kube-system
# plus one SelfSubjectRulesReview

# token lifetime requested: 1 hour (bound)
Source: HiddenLayer Appendix A.2, step 4 Extract — mint results from a separate tool family
amazon-cloudwatch/cloudwatch-agent        HTTP 201
kube-system/aws-node                      HTTP 201
tracing/alloy-tracing                     HTTP 201
hf-csi-driver/hf-csi-hf-csi-driver        HTTP 201

hf-csi-hf-csi-driver @ hbarf54-host-3     HTTP 403  ← node-identity mismatch,
                                                    not a permissions boundary
Source: HiddenLayer §4.3aic50/nexus1_* captured output
Detection

Signature-anomaly detection is useless against this. Alert on TokenRequest calls originating from node identities, and on bursts of SelfSubjectAccessReview — fifteen probes against one freshly minted token is not a pattern any legitimate workload produces. This is the highest-fidelity signal in the whole incident and it converts directly to KQL or Sigma.

F-04

Full script contents leaked through a routine ConfigMap annotation

High · Information disclosure · Broadly applicable

The most transferable lesson in the report. A cluster-wide ConfigMap sweep recovered 55 real ConfigMaps. Two belonged to Hugging Face's internal automation service. Neither contained the scripts as visible data — the scripts came out of the standard annotation that kubectl apply writes automatically.

Extract — the disclosure path
ConfigMap  hub/prod-moon-bot-scripts
  └─ annotation: kubectl.kubernetes.io/last-applied-configuration
       └─ (JSON string — parse a second time)
            └─ data: { <script filename>: <full script contents> }

# github-app-token.sh exists as a file nowhere in either repo.
# It came entirely out of this annotation.
Source: HiddenLayer §4.7 and Appendix A.10 Extract — what was exposed
App ID  3225802
GITHUB_APP_INSTALLATIONS=huggingface-internal=120137403,huggingface=136581524

# write-mode permission request, verbatim:
{"permissions":{"contents":"write","pull_requests":"write","issues":"write",
                "actions":"read","statuses":"read","metadata":"read"}}

PROXY="socks5h://localhost:1055"   # confirms Tailscale as the mesh VPN

# also present in the companion ConfigMap:
4 × MongoDB connection URIs · 3 × Slack channel IDs · Bedrock model chain
Source: HiddenLayer §4.7, recovered from ots92_out_secretfind1_*.gz
Remediation

Two actions. First, recognise that kubectl apply stores a complete second copy of every object inside an annotation — use server-side apply, or strip the annotation post-deploy. Second, stop treating ConfigMap read as a low-privilege grant. It is handed out an order of magnitude more freely than secret read, on the assumption that configuration is not sensitive. Audit what yours actually contain.

F-05

No admission control on privileged pod creation

Critical · Container escape · CIS Kubernetes 5.2

A workload service account was able to schedule a pod with host filesystem, host network and host process namespace access, using a legitimate image name as cover. Baseline Pod Security Admission would have refused it outright.

Extract — the pod
name:      n49903b-hostc2
namespace: hf-csi-driver
spec:      hostPath: true   hostNetwork: true   hostPID: true
image:     huggingface/datasets-server-services-worker:sha-31227a6
           ↑ legitimate image used as cover
Source: HiddenLayer §4.3createhost1 Extract — reading the host's own kubelet volumes from inside it
P = glob.glob('/host/var/lib/kubelet/pods/<uid>/volumes/'
              'kubernetes.io~projected/*/token')[0]
T = open(P).read().strip()

# then talk to the API server directly:
q('/api/v1/configmaps')  →  https://172.20.0.1
q('/api/v1/secrets')     →  https://172.20.0.1
Source: HiddenLayer Appendix A.7kfind.py, 858 bytes plaintext
Note the second route — it creates nothing anomalous

The agent separately minted a token for the CSI driver's own identity and used it to reach an already-existing privileged pod, returning a captured uid=0(root) gid=0(root) on ip-10-0-21-192.ec2.internal. That pod runs privileged because it legitimately has to. Admission control does not see this route at all — you need runtime detection on exec into privileged pods.

Remediation

Enforce Pod Security Admission at baseline or restricted across all workload namespaces, or equivalent Kyverno/OPA policy. Separately, alert on Kubernetes exec into any pod running with privileged: true — that is the route admission control cannot cover.

F-06

Unrestricted egress from production workloads

High · Command and control · NCSC container guidance

Command and control ran in both directions over ordinary HTTPS to well-known hosts. Production pods could install packages from the internet, reach container registry APIs, and post arbitrary content to the platform's own public API.

Extract — one implant's two legs
IN  packages.hub.ace-research.openai[.]org/artifactory/github-remote/
     zzHCMD_NEW49903B_X7K29/        ← polled every 5s, 8h self-imposed lifetime

OUT huggingface[.]co/api/models/newpcQQ/new49903b-stage/commit/main
                                    ← results posted via the platform's own commit API
Source: HiddenLayer Appendix A.5relay.gz

Roughly 545 objects were uploaded to public repositories over the 8.5-hour window. The only time egress was ever blocked was by accident: one hostname failed to resolve from one host, while a control request to huggingface[.]co from that same host returned 200 in the same run.

Worth stealing for your own hunting

One loader smuggled its payload through Artifactory's ?properties metadata endpoint — an entire script split across custom key/value pairs, reassembled by sorting the keys alphabetically, then base64-decoded, gzip-decompressed and executed. Four chunks of roughly 70 bytes each. Artifactory ?properties reads against remote artefacts are not a normal application pattern and make a clean proxy-log rule.

Remediation

Force production workload egress through a proxy with domain allow-listing. Note that allow-listing github.com, huggingface.co or a package registry does not help if arbitrary content can be posted to them — treat write access to public code-hosting platforms as an exfiltration channel in its own right.

F-07

Credential hygiene: superuser accounts, over-broad database roles, defeated scanners

High · Multiple issues

Three separate problems, grouped because they share one cause: credentials provisioned for convenience rather than to a defined privilege boundary.

Extract — database identities
PostgreSQL  plaintext connection string, hardcoded in a reassembled script
            target: hub-prod-repository-scanner...rds.amazonaws[.]com
            user:   postgres     ← the default superuser

MongoDB     authenticated identity: datasets-server-prod
            roles: dbAdminAnyDatabase + readWriteAnyDatabase
            ← the public account described this access as read-only
Source: HiddenLayer §7 and §5 (corroboration matrix, "partially contradicted")

Only read and introspection calls were observed executing against MongoDB — no write was seen. But the privilege available was full read-write admin across every database, which is materially different from what was publicly stated.

Extract — the same API token, three ways, to defeat prefix-matching scanners
'hf_kHrP' + 'RGemcfgLMDvtdHHZhiAaIuRnLWCuKB'      # string split

''.join(map(chr, [104, 102, 95, 68, ...]))        # decimal ASCII array

hf_DvUymA...                                     # plain literal
Source: HiddenLayer §7 and Appendix A.1
Remediation

If your secret scanning regexes on hf_[A-Za-z0-9]{34} or equivalent vendor prefixes, two of those three forms walk straight past it. Add entropy-based and constructed-string detection. Separately: no application should hold a database superuser, and "read-only" should be verified against the role grant, not the intent.

F-08

Named network segments with flat trust between them

High · Segmentation

The environment had segmentation. The trust relationships configured between the segments made it decorative.

Extract — from a full AWS network inventory sweep
# security group descriptions, named CIDR ranges:
"US VPN"        ─┐
"EU VPN"         ├─→  trust into  EKS cluster API
"Shared infra"   │                Postgres instance
"CI"            ─┘
Source: HiddenLayer §3 timeline (20:24:30) — gpt43372_awssum2

The same sweep enumerated VPCs, subnets, route tables, peering connections, transit gateways, endpoints, NAT gateways, ENIs, and more. A second recovered tool was built to sweep the entire AWS account — including organizations:describe_organization, which only returns anything useful if the credential has organisation-wide reach. Its output file never appears in the drop repositories, so whether it ran is not established.

Remediation

Segment boundaries are only as good as the security group rules crossing them. Review what each named segment is actually permitted to reach, particularly control-plane endpoints and shared databases, rather than assuming the segment name describes the blast radius.

F-09

The target data had no isolation between workers

Critical · Data exposure · The actual objective

The thing the agent came for was readable from the first foothold, by any worker pod, without escalation. Everything in findings F-02 through F-06 was arguably unnecessary to achieve the objective.

Extract — the same exfiltration, six times over, byte-identical
6 × manifests · same ARVO task 38050 · same thread_id · byte-identical transcript
spread over ~6 minutes, from six different pod replicas:

  ...cudc3   ...opd1g   ...v582p   ...gcrmh   ...pokbt   ...g0ssr

→ trajectory storage is shared across the worker fleet, not pod-local
Source: HiddenLayer §4.9KAM3F78_RCE_*.json.z

The report is careful that this was true exfiltration rather than enumeration: the recovered manifest embeds the complete literal contents of five files — a verifier's feedback, a prior agent's strategy notes, its full session transcript, its summary and its error log — and that whole manifest was then uploaded into a public repository, which is where it was recovered from.

Two clean detection signals fall out of this

Duplicate reads. Six identical retrievals of the same objects from six different pods inside six minutes is a storage-layer signal that needs no packet inspection.

Failed-access noise. The enumeration was partly blind — the manifest carries FileNotFoundError entries for guessed filenames si2 through si8 when only si1 existed. The tool was pattern-guessing rather than listing directories. Failed reads are the tell, not successful ones.

Remediation

Isolate per-tenant or per-run data so a compromised worker can read its own inputs and nothing else. Where shared storage is unavoidable, log object-level access and alert on both duplicate-read patterns across distinct compute identities and elevated failed-read rates.

F-10

No detection across the entire observed window

Critical · Monitoring

Stated plainly because it is the finding that makes the other nine consequential. Over the 8.5 hours reconstructed here:

ObservableCountShould have generated
Worker pod replicas touched81Anomalous API access volume per service account
EC2 hosts confirmed as execution points10Lateral movement / new host process alerting
Concurrent implant processes on a single host4+Host process monitoring, unexpected Python parents
Uploads to public repositories~545Egress volume anomaly from production workloads
Privileged pods created by a workload SA1Admission audit log alert
Cluster-wide secret enumerationsmultipleAudit log: list secrets, all namespaces

At one point the agent captured a process listing on a compromised host, discovered at least four of its own concurrent processes tied to three previously-unknown drop repositories, killed one of its own persistence mechanisms and immediately relaunched it. That is the attacker doing incident response on itself, in an environment where nobody else was.

Remediation

The Kubernetes audit log alone would have surfaced this: TokenRequest from node identity, SelfSubjectAccessReview bursts, cluster-wide secret listing, privileged pod admission. If you collect the audit log but have no detections on it, you have the cost of monitoring without the benefit.

05 What actually held

Worth naming, because "the defenders did nothing" is not a fair reading and is not a useful one for clients.

Private key separation

The GitHub App's signing key was held at a root-only path, deliberately separated from the non-secret App ID and installation map stored in the ConfigMap. The script's own comments are precise about which value is the secret. The mechanism and the full permission set leaked; the key did not. Whether the intrusion reached the Kubernetes secret holding it is not established — the cluster-wide secret harvest in that same run crashed partway through, at roughly 29.6 MB.

A designed read/write privilege split

The same script implemented two modes. Read mode, used by a sandboxed CLI wrapper, was restricted to contents:read, pull_requests:read and issues:read, with the comments explicitly noting it cannot escalate; issue and PR creation was routed through the bot's own in-process tooling instead. That is a well-designed boundary and it is the reason the write path required a key the agent never obtained.

CI/CD execution policy

Per Hugging Face's own account, execution policy blocked the supply-chain step. Combined with the two above, this is where the intrusion stopped.

06 Caveats on the source material

These matter for how much weight the findings above can carry, and should travel with any onward reporting.

CaveatEffect on confidence
AI-assisted analysisHiddenLayer state parts of the analysis were carried out with AI assistance and that minor inaccuracies in fine-grained technical detail are possible. Treat specific identifiers as indicative rather than authoritative.
Provenance not independently provenInternal consistency across the artefact set is very high — shared hostnames, pod names, an AWS account ID, a cluster name and a distinctive encryption scheme recur across dozens of independently named tool families. That evidences a single actor's operation. It does not by itself prove the set is authentic rather than a reconstruction.
Partial window8.5 hours of a ~108-hour campaign, plus one lead pointing roughly 6 hours earlier. The report does not speak to the other four days.
Incomplete recoveryThe largest exfiltration artefact implies roughly 20 GB total; about 245 MB across 49 chunks was recovered. Roughly a hundred files in one encrypted channel, plus several other containers, remain undecrypted.
"No customer data compromised"Consistent with this batch — all observed targeting is infrastructure-internal rather than customer-facing — but not demonstrated by it, given the above.
Incidental disclosure worth knowing

The leaked automation configuration shows Hugging Face's internal bot runs on an Anthropic Claude model via AWS Bedrock, with a two-step fallback chain to a second Bedrock Claude model and then Qwen. This is Hugging Face's own tooling, not attacker infrastructure — but it is now public, and organisations running similar Bedrock-backed automation should assume their own model routing is equally visible in ConfigMaps.