DEV Community

jamilxt
jamilxt

Posted on

An AI Agent Found Admin Access to a $13B Startup in 25 Minutes. Here's the Free Tool It Used, and the Docker Mistake That Made It Possible

The story that hit the Hacker News front page this week: a security company, Strix, was evaluating Baseten, a $13 billion inference provider, as a vendor. Before handing over their own code and models, they pointed their own tool, an autonomous AI pentesting agent, at *.baseten.co. No credentials. No source code. About 25 minutes later, the agent surfaced a live GitHub personal access token for an account called basetenbot, with admin and push access to Baseten's main product repo, the GitOps repo that drives their production clusters, and their Homebrew tap. The token had been sitting in a Docker image's build history since March 2023. More than three years, exposed in a public Harbor registry, never exploited, until an AI agent pulled the thread end to end, autonomously, in under half an hour.

If you run containers and use GitHub, this incident is a checklist, not just a headline. The vulnerable pattern takes five lines of Dockerfile. The detection is one command. And the tool that found it is free and open source.

What the agent actually did, step by step

The Strix team published the full writeup, and it is worth reading slowly because every step is a generic technique, not some exotic zero-day. Here is the chain, reconstructed from their post:

  • Recon. Strix enumerated subdomains and certificate logs to map the attack surface, and found a Harbor container registry at gcp-us-east4-zlw.registry.baseten.co.
  • Anonymous access. One Harbor project was public. Without any token, the agent could list repositories, mint anonymous pull tokens, and download image manifests and blobs.
  • First credential, dead. Inside an image called baseten/baseten-app, it found a pair of AWS keys. It tested them with a read-only sts:GetCallerIdentity call. The API returned InvalidClientTokenId. Dead key. The agent kept going instead of reporting a false positive.
  • The live token. It ran TruffleHog over the pulled layers and inspected the image config directly. In the config's history[].created_by field, it found a RUN command where the GITHUB_TOKEN build argument had been expanded into plain text. It validated the token with a read-only GET /user call to GitHub: HTTP 200, account basetenbot.
  • Impact check. The token carried repo scope and belonged to the basetenlabs organization. Repository-by-repository, using read-only requests: admin and push on the main baseten product repo, admin and push on flux-cd (the GitOps repo that defines the desired state of their clusters), admin and push on their homebrew-tap, plus read/write on several private repos, including one with a top-level customers/ directory. They stopped there and wrote the disclosure email immediately.

The timeline that matters for defenders: reported July 13 at 11:10 PM. Baseten made the Harbor project private the next morning, confirmed the issue as critical and rotated the token by 4:34 PM the same day. Closed remaining findings by July 17. This is how the process is supposed to work, and Baseten's security team deserves credit for it.

The five-line Dockerfile pattern that leaks tokens

Here is the part that should make you open your own Dockerfiles right now. The underlying mistake is one of the most common patterns in production images:

ARG GITHUB_TOKEN
RUN GITHUB_TOKEN=${GITHUB_TOKEN} bash -c '\
  if [[ "${GITHUB_TOKEN}" != "" ]]; then \
    git config --global --add \
    url."https://${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:"; \
  fi'
Enter fullscreen mode Exit fullscreen mode

This looks reasonable. You need a private dependency during the build, so you pass a token as a build argument, Git authenticates, the image builds. Two things go wrong, and both are documented behavior, not bugs:

  • Build args can end up in the image. Docker explicitly warns that build arguments may leak into the image's metadata. In this case, the shell expansion put the literal token value into the RUN command's history entry, the history[].created_by field of the image config.
  • git config --global persists. Even if you fixed the shell expansion, this command writes the authenticated URL, token included, into .gitconfig inside the image layer. Deleting a credentials file does not remove the second copy in build history.

The critical insight from the incident: an image is not just its filesystem layers. The config blob, downloadable alongside the image, records every build step. Secrets can live in layers, in build history, or both. Most scanning setups check the filesystem and skip the history.

Run the audit on your own images today

You do not need an AI agent to find this class of leak. Three checks, in increasing order of effort:

  • Check build history. docker history --no-trunc <image> prints every build step. Look for anything that resembles a token, password, or expanded variable. On images you have pulled, inspect the config blob directly: docker inspect <image> and read the History section, or pull the config JSON from your registry and grep the history[].created_by fields.
  • Scan layers for secrets. TruffleHog (the same tool the agent used) is the standard: trufflehog docker --image <image> checks image layers for verifiable credentials. Gitleaks works too. The difference that matters: TruffleHog verifies found secrets against the live provider, so you learn whether the key is dead or active.
  • List what is anonymously pullable. For your Harbor instance, check which projects are public and what images they expose, including old tags nobody remembers. The Baseten exposure was not a CVE. It was a project visibility setting.

If you find a leaked token, the fix has three parts

Finding the leak is step one. The Strix and Baseten resolution shows the full remediation, and all three parts are mandatory:

  • Rotate first, always. Cleaning the Dockerfile does nothing about images already pulled or pushed elsewhere. The token stays valid until revoked. Rotation was Baseten's first real fix.
  • Fix the build with a secret mount. BuildKit secret mounts pass credentials at build time without persisting them in layers or history:
# syntax=docker/dockerfile:1
FROM alpine:3.20
RUN --mount=type=secret,id=github_token \
  GITHUB_TOKEN=$(cat /run/secrets/github_token) && \
  if [ -n "$GITHUB_TOKEN" ]; then \
    git config --global \
    url."https://${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:"; \
  fi && \
  git clone https://github.com/your-org/private-repo /app && \
  rm /root/.gitconfig
Enter fullscreen mode Exit fullscreen mode

Wait, that --global write is still the bug. Two correct options: scope the config locally and remove it in the same layer, or skip the git config entirely and use an authenticated URL only for the clone command:

# syntax=docker/dockerfile:1
FROM alpine:3.20
RUN --mount=type=secret,id=github_token \
  TOKEN=$(cat /run/secrets/github_token) && \
  git clone "https://x-access-token:${TOKEN}@github.com/your-org/private-repo.git" /app
Enter fullscreen mode Exit fullscreen mode

The token exists only in the build environment, never in a layer, never in build history. Same fix applies to pip install from private indexes, go mod download, and Maven dependency fetching.

  • Shrink the token's blast radius. The most damning detail in this incident is not that the token leaked. It is what the token could do. Fetching private dependencies needs read access to those dependencies. This one had admin on the product repo, the GitOps repo, and the distribution channel, for three years. Minimum requirements: least-privilege scopes (fine-grained PATs let you scope per-repo and read-only), an expiration date, and a build bot account that owns nothing else.

Should you run an AI pentest agent yourself?

Strix is open source (Apache 2.0) and runs locally: you install it, point it at a target, and it runs recon, exploitation attempts, and validation in a Docker sandbox, producing proof-of-concept findings instead of the usual scanner noise. It supports any major LLM provider through LiteLLM, including local models, so your traffic and your code need not leave your machine.

Full disclosure: I have not run Strix myself. Everything above about what it did comes from the Strix team's own disclosure, so treat their claims about their own tool with appropriate skepticism. The incident details, though, are corroborated by Baseten's own response and by the HN thread where the Strix CEO answered questions, so the core finding is not in dispute.

Two cautions worth stating plainly:

  • Authorization. Strix actively exploits targets. Only point it at systems you own or have explicit written permission to test. Unauthorized scanning is illegal in most jurisdictions. Strix's own docs are blunt about this.
  • Cost and noise. An agentic scan burns real LLM tokens, and agentic tools generate findings that need human triage. For continuous scanning, the GitHub Actions integration (scanning changed files on each PR) is the more realistic entry point than full autonomous runs.

The defensive logic, though, is sound regardless of the tool. If a 25-minute autonomous scan can find a three-year-old admin token, assume attackers with the same tools are scanning too. Running offensive tooling against your own infrastructure regularly is becoming table stakes, not a luxury.

The takeaway checklist

  • Grep your Dockerfiles for ARG followed by token-like variable names, and for git config --global inside RUN steps.
  • Run docker history --no-trunc on your production images, especially old ones. Check history[].created_by in the config blob.
  • Scan layers with TruffleHog, which verifies whether found secrets are still live.
  • Audit which registry projects are anonymously pullable, including stale tags.
  • Move build authentication to BuildKit secret mounts with short-lived, least-privilege tokens.
  • Set expirations on every machine account token, and scope build tokens to read-only on exactly the repos they need.

The Baseten incident is a story about an impressive AI agent, but the vulnerability itself required no AI to create and none to find. A 2023 Dockerfile pattern sat in public view for three years. The AI agent just collapsed the time between "exposed" and "found" from years to minutes. Whichever side of that collapse you are on, your images and your token scopes decide which side you land on.

I write about developer tools, security, and AI systems every week. Subscribe, it's free.

Have you audited your own images for secrets in build history? Found anything surprising? I'd like to hear about it in the comments.

Top comments (0)