Skip to content

Build1 publisher3 min readPublished

A CI leak drill measures the time between spotting a key in the log and revoking it

A dev.to post puts the containment work in the token itself, giving each CI job one audience, one action set and one expiry. It comes with a drill an engineer can run on a disposable key without production access.

The Engineer · Build desk

Illustration accompanying A CI leak drill measures the time between spotting a key in the log and revoking it

What happened

  • A dev.to post on CI credentials argues for a short-lived, narrowly scoped key per job, kept out of build output, with revocation exercised as a tested path.
  • Its drill creates a disposable key, runs the smallest pipeline, inspects the rendered log, revokes the key and verifies that the next API call is denied, with the detection-to-revocation time recorded.
  • The post rejects one permanent organization key copied into every workflow, allowing it only for a throwaway sandbox with synthetic data, a tiny scope and an automated expiry.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Teams have to write the fork rule themselves. Environment approvals gate protected deployments, and repository and organization secrets stay reachable by whatever branch policy is already in place.
  • cost The detection-to-revocation figure is a number a team generates and then has to live with, and the price of producing it is a disposable key plus one run of the smallest pipeline.
  • exposure Access left behind after a branch or repository is deleted keeps a subject authorised while the rotation schedule still looks current, so who can still call the API is a separate question from what a green rotation dashboard shows.

The containment happens in the API that accepts the token. The test job gets a read-only fixture token, the deployment job gets a deploy token, and the post's requirement is that neither is accepted by the other API surface [6]. Each token carries one audience, one action set and one expiry, with the workflow identity bound to the repository and ref where that is available [5]. This only holds if the issuing service will mint per-job tokens and check the audience on every route. Where one key is honoured everywhere, the scoping in the workflow file leaves a printed string exactly as powerful as it was.

The example pipeline publishes a worker, runs integration tests and uploads a diagnostic bundle. The post calls those different trust boundaries [3]. One account-wide key spans three of them in a single run [2]. A line printed by a Node.js dependency becomes access to billing data, or transcripts, or unrelated queues [4].

"I treat the build log as public until proven otherwise," the author wrote [7]. "Masking helps, but it is not a security boundary: a transformed value, an error object, or a verbose child process can evade a simple exact-string masker" [8]. Masking stays. The change sits upstream of the log, by keeping the secret out of commands that do not need it and passing it through the runner's secret mechanism only at the step that uses it [9]. For a Node.js job that means holding the key in process memory and not interpolating it into a shell command [12].

The sample revocation step is Python: a DELETE to /v1/account/keys/revoke/{key_id} with a Bearer token and an explicit ten second timeout [13]. It treats 200 and 204 as success and raises on anything else [14], so a gateway that answers 202 Accepted for an asynchronous revoke would fail the drill on a successful revocation [1]. A 429 raises with an instruction to wait for the server-provided Retry-After delay [15]. Revocation is idempotent by nature, and repeating it for the same key ID leaves the key revoked; for other writes the post asks for an idempotency key and backoff instead of an immediate retry loop [16].

A cancellation halfway through a deployment, a retry after revocation, and a forked pull request that must not inherit write credentials are all in the drill [17]. GitHub Actions environments can require approval for protected deployments, while repository and organization secrets still need a policy that separates trusted branches from untrusted forks [18]. The post also asks teams to document what the runner can read and what it can persist between jobs [23]. On the negative path, a revoked key must fail clearly and the pipeline must stop before publishing artifacts that depend on an authenticated call, without retrying a 401 forever [22].

Two of the three failure modes the post lists survive a perfect rotation schedule. A rotated key that retains access to every project is still overbroad, and access left behind after a branch or repository is deleted stays live [20]. "Rotation does not fix a policy that never removes old subjects," the author wrote [21]. This is a design note written against a fictional endpoint, api.example.test, with the control flow and the explicit timeout offered as the point [24], and it does not include measurements from a real leak [3].

What to watch

  • Whether the API vendors in this path will mint per-job tokens with an audience claim, since the scoping stays advisory until they do.
  • A published detection-to-revocation time from a real leak, which would test whether the drill's number holds outside a disposable key.
  • Whether GitHub Actions gains a fork-safe secret scope, so the trusted-branch policy stops being written by hand per repository.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories