Build History Keeps Its Own Copy of Your Token

TV
Thiago Victorino
6 min read
Build History Keeps Its Own Copy of Your Token

An autonomous scan began with a domain name. No credentials, no source code, no account inside the target. About twenty-five minutes later it was holding a live GitHub personal access token with admin rights on the GitOps repository that drives Baseten’s production clusters.

That twenty-five minutes is a vendor-instrumented number. Strix, which published the writeup, builds the scanner that produced the finding, and the figure is its own instrumentation of its own product. The disclosure timeline, the screenshots and the permission table in the post are checkable. The clock is not. I am carrying the number as reported and the mechanism as proven, which are two different levels of confidence.

The mechanism is the part worth your morning.

The token was never in a layer

A container image is two kinds of content. Layers hold the filesystem: files, directories, permissions. A separate config blob holds metadata about how the image was assembled, including a history[] array, one entry per build step. Each entry carries a created_by field holding the command text for that step.

The token lived in history[].created_by. Not in a file inside the image. In the recorded text of the command that once used it.

The published pattern that put it there:

ARG GITHUB_TOKEN
RUN GITHUB_TOKEN=${GITHUB_TOKEN} bash -c '... git config ... url."https://${GITHUB_TOKEN}@github.com/".insteadOf "git@github.com:" ...'

The secret arrives as a build argument, gets expanded into a shell command, and the expanded command line is what the image records about itself. Unpacking layers and walking the filesystem with a secret scanner returns nothing, because there is nothing on disk to find. The credential is in the metadata, sitting next to the tag and the entrypoint, one API call away from anyone who can fetch the manifest.

That is why filesystem scanning was not enough here. It was looking in the wrong half of the artifact.

Three years is a long time for a build argument

The build step containing the token ran on 2023-03-03. The token still authenticated in July 2026. Over three years of a build argument outliving every rotation cycle it should have been caught by.

Baseten is cited in the writeup at a $13 billion valuation, and the post notes the company already used AI security tooling. The token survived anyway. The question that leaves for the rest of us is whether our own scanners read the config blob at all, and whether they treat a RUN line as a place where secrets live.

The registry door was already open

The registry gcp-us-east4-zlw.registry.baseten.co had one public project. Anonymous listing worked. Anonymous pull tokens were issued. Manifests and blobs were downloadable. Once you can pull a manifest you can pull the config blob, and the config blob is where the history[] array lives.

One detail in the run deserves attention independent of the outcome. An earlier credential pair, AWS keys, turned out to be dead: sts:GetCallerIdentity returned InvalidClientTokenId. The run continued past it. A pipeline that stops at the first interesting string would have filed a dead-key report and closed the file.

What the credential actually opened

GitHub answered with X-OAuth-Scopes: repo for the account basetenbot in the org basetenlabs. From there the permission table showed admin plus push on three repositories, described in prose as the main product repository, the GitOps repository driving their clusters, and their Homebrew tap. Read and write on four additional private repositories, including per-customer repositories.

The repository names are redacted on the source page as basetenlabs/b***. They stay redacted here.

Admin on a GitOps repository is the entry that should make a platform engineer stop reading and go check something. The repository is the desired state. Whoever can write to it can change what the clusters converge to, and the change arrives through the same pipeline that legitimate changes use.

Private-ing the registry did not close it

The disclosure ran like this. The report landed 13 July at 11:10 PM. On the morning of 14 July the Harbor project was made private. The token was still live. At 4:34 PM on 14 July the finding was confirmed critical and the token was rotated. The issue closed on 17 July. The bounty was paid in T-shirts and sweatshirts.

Between the morning and the afternoon of 14 July sits the lesson. Making the registry private closed the distribution channel. It did nothing to the credential, because every copy of that image already pulled still carried the same string in its config blob, and the credential itself was unchanged. Closing the door after the copies have walked out is housekeeping. Rotation is remediation. The order matters, and the incident shows the cost of getting it backwards.

Baseten confirmed and closed this through a coordinated disclosure, with the draft shared in advance. That is the behavior you want from the company on the receiving end, and it is why this reads as a mechanism worth studying rather than a company worth scolding.

The agent stopped at proof

There is a second thing in this run that I keep coming back to, and it has nothing to do with Docker.

The agent found a working admin credential on the repository that controls production and then did nothing with it. Read-only calls to establish scope. Stop at proof. No clone, no push, no configuration change. The writeup is explicit that the access was demonstrated and not exercised.

Set that against the failure mode most of us have actually been bitten by: the agent that reports a fix it never ran, or a reproduction it assembled out of plausible-looking text. We have argued before that agent security is an architecture problem and that a non-human identity needs the isolation you would give an insider. This run is a data point on the other side of that argument. An offensive agent operating at the most tempting moment in its whole run, holding admin on a production control plane, held a boundary narrow enough to check afterwards.

My reading is that a boundary holds when it is expressed as a stopping condition rather than a preference. Prove the access, record the evidence, halt. The shape of an authorization model an agent can obey looks like that: a list of verbs it may issue and a point at which it is done. Not a tone of voice about being careful.

Do this now

Two checks, in this order, before lunch.

Pull the config blob for one image your build pipeline pushed in the last year and read its build history. docker history with truncation turned off, or a read of the config JSON straight from your registry, will show you created_by for every step. You are looking for any RUN line containing a token, a URL with credentials in it, or a git config that embeds one. Do this on an image whose Dockerfile uses ARG for anything sensitive, because that is the shape that produced this finding.

Then take any long-lived GitHub token your automation holds and ask GitHub what it can do. The response header X-OAuth-Scopes tells you the scope, and the repository listing tells you the blast radius. If a bot account has admin on the repository your cluster state comes from, that is the finding, and it is yours to close without anyone else having to report it.

If the first check turns up a token, rotate before you touch access controls on the registry. Copies of an image are not recallable. A rotated credential is dead everywhere at once.

And while you are in there, write down which artifact halves your scanners actually read. That list belongs next to the AI bill of materials inventory, because a component you have catalogued and a component you have actually scanned are not the same claim.


This analysis synthesizes We wanted to use Baseten for inference. We ended up with admin access to Baseten GitHub repos (Strix, September 2026).

Victorino Group helps engineering teams audit what their agents and build pipelines can reach, and define authorization boundaries an agent can actually be held to. Let’s talk.

All articles on The Thinking Wire are written with the assistance of Anthropic's Opus LLM. Each piece goes through multi-agent research to verify facts and surface contradictions, followed by human review and approval before publication. If you find any inaccurate information or wish to contact our editorial team, please reach out at editorial@victorinollc.com . About The Thinking Wire →

If this resonates, let's talk

We help companies implement AI without losing control.

Schedule a Conversation