When the secret leaks  / field guide
Practitioner field guide · 2026-09-20

When the secret leaks: the hour after exposure

Four supply-chain incidents from December 2024 to August 2025 left their primary records on GitHub: advisory texts, live incident threads, remediation commits, and the deleted tags where the attacker's releases used to be. Read together with the revocation machinery that PyPI, AWS and GitHub run in code and policy, they answer a question most security programmes never test: once a machine credential is public, who is allowed to kill it, how fast, and what breaks when they do.

26 primary sources 13 organisations 4 incidents, 1 rejected PR Evidence through September 2026 Read: ~25 min
01

The territory

A string that grants access has escaped your control. Everything that happens next is an argument between three clocks: the attacker's, the detector's, and the revoker's.

23,000+
Repositories whose CI secrets one rewritten action tag exposed, March 2025
98
Actions AWS denies a quarantined key, up from 28 in 2020; the key otherwise keeps working
5h 25m
From first malicious npm publish to every Nx publishing token revoked, August 2025
5 yrs
From Kubernetes naming its token flaw (2019) to the legacy token's removal reaching stable (v1.30, 2024)

State the problem without naming a product: a machine credential, a CI token, a publishing key, a bot account's password, has been seen by someone who should not hold it. It is now a race. The attacker needs to use the credential before it dies; you need to find it, kill it, and re-issue it before the use matters. Every mechanism in this guide, scanners, push protection, quarantine policies, honeytokens, short-lived tokens, is a move in that race, and each one was built by an organisation that lost the race at least once first.

The surprise in this corpus is where the race is actually lost. In all four incidents the revocation machinery, once pointed at the right credential, worked in minutes to hours: npm had every Nx publishing token dead five and a half hours after the first malicious publish [Nx advisory]; PyPI deletes a disclosed token in code before a human reads the email [warehouse source]. What failed, repeatedly, was enumeration: knowing which credentials existed and where they were still trusted. Ultralytics fixed its workflow on December 5, 2024, and was republished by the attacker on December 7 through a registry token the first response had not rotated; the poisoned versions 8.3.45 and 8.3.46 exist on PyPI and nowhere in the repository's tag history [tag record]. The reviewdog attacker's opening move was a contributor PAT that, in the maintainer's words, "originated from an attack on another repository" [reviewdog #2079]. You do not lose in the hour after exposure. You lose months earlier, when a credential was created that nobody wrote down.

Figure 1 · Leaks chain: each incident's output is the next one's input

contributor PAT stolen

suspected route to
tj-actions bot PAT

runner memory dumped
to public logs

PyPI token stolen
and not rotated

npm token sent to
attacker webhook

postinstall harvests
victims' tokens

Attack on an
unnamed repository

reviewdog v1 tag
rewritten, Mar 11 2025

tj-actions tags
rewritten, Mar 14 2025

CI secrets of 23,000+
repositories exposed

Ultralytics workflow
injection, Dec 4 2024

Second wave: 8.3.45, 8.3.46
published Dec 7

Nx PR-title injection
Aug 2025

8 malicious versions

Stolen credentials posted as
repos on victims' accounts

contributor PAT stolen

suspected route to
tj-actions bot PAT

runner memory dumped
to public logs

PyPI token stolen
and not rotated

npm token sent to
attacker webhook

postinstall harvests
victims' tokens

Attack on an
unnamed repository

reviewdog v1 tag
rewritten, Mar 11 2025

tj-actions tags
rewritten, Mar 14 2025

CI secrets of 23,000+
repositories exposed

Ultralytics workflow
injection, Dec 4 2024

Second wave: 8.3.45, 8.3.46
published Dec 7

Nx PR-title injection
Aug 2025

8 malicious versions

Stolen credentials posted as
repos on victims' accounts

Three 2024–25 chains reconstructed from the GitHub record. Solid arrows are documented in the linked advisories; the dashed arrow is the reviewdog maintainer's own stated suspicion, not a proven link. Sources: reviewdog #2079, GHSA-mrrh-fwg8-r2c3, Nx advisory, ultralytics #18027.
Diagram source
Scope, and the corpus limit

This guide covers machine credentials, tokens, keys and bot passwords, leaking through code, CI and package registries, and what production systems do in response. It does not cover human phishing, session hijacking, password managers, or the internals of vault products. One constraint shaped the evidence: this research session's network reached github.com and nothing else, so the corpus is the GitHub record: advisories, incident threads, remediation commits, KEPs, policy trackers and platform documentation read from its source repository. The Heroku (2022), CircleCI (2023) and Cloudflare (2023–24) credential-theft postmortems exist and are directly relevant, but could not be fetched and are cited nowhere in this page; treat their absence as a hole in this corpus, not a judgement of relevance. No engineering blog, conference talk or paper is cited, for the same reason.

02

How the response machinery is actually built

Reconstructed across GitHub, PyPI, AWS and the scanner ecosystem: the same pipeline everywhere, with one fork that each platform resolves differently.

Strip away the product names and every leaked-credential defence in this corpus is the same five-stage pipeline. First, make the secret findable: GitHub's tokens and PyPI's tokens carry distinctive prefixes precisely so a regex can find them in a haystack; PyPI's disclosure matcher is one pattern, pypi-[A-Za-z0-9-_=]+ [warehouse source], and TruffleHog ships more than 800 of them [trufflehog README]. Second, detect, at three possible points: before the leak lands (push protection "blocks pushes that contain secrets before they reach your repository" [GitHub docs]), after it lands (scanning of repositories, and also issues, pull request comments, wikis and gists, which is where secrets actually travel [GitHub docs]), or at runtime, which is how the tj-actions compromise was caught: an outside monitor noticed a tag move and an anomalous outbound fetch, three hours in [issue #2463].

Third, verify: decide whether the finding is live. GitHub does this by testing the secret against the issuer's own API [validity checks], and TruffleHog's design premise is the same: "for every secret TruffleHog can classify, it can also log in to confirm if that secret is live or not." Fourth, dispose, and here the pipeline forks three ways; section 3 treats the fork as the central decision of the whole topic. Fifth, close the class: the durable fixes in this corpus are not better rotation but credential removal. Nx's final remediation line is that its packages "CANNOT be published with npm tokens any longer" [Nx advisory]; GitHub's OIDC issues a deployment credential that "is only valid for a single job, and then automatically expires" [GitHub docs]; Kubernetes spent KEP-1205 and KEP-2799 replacing tokens that were "valid for as long as the service account exists" with bound, expiring ones [KEP-1205].

Figure 2 · The leak-response pipeline, and the fork inside it

Dispose - the fork

Verify

Detect

Make it findable

Distinct token formats
ghp_..., pypi-...

Push protection
pre-receive block

Post-receive scan
code, issues, gists

Runtime anomaly
tag moves, egress

Log in with the finding
live or dead?

Revoke now
GitHub own tokens, PyPI

Quarantine fence
AWS deny policy

Notify the issuer
partner programme

Rotate, audit use,
then remove the class via
OIDC and bound tokens

Dispose - the fork

Verify

Detect

Make it findable

Distinct token formats
ghp_..., pypi-...

Push protection
pre-receive block

Post-receive scan
code, issues, gists

Runtime anomaly
tag moves, egress

Log in with the finding
live or dead?

Revoke now
GitHub own tokens, PyPI

Quarantine fence
AWS deny policy

Notify the issuer
partner programme

Rotate, audit use,
then remove the class via
OIDC and bound tokens

Every box is attributable: formats and scanning from GitHub's and PyPI's documentation and code, the three disposal policies from GitHub, PyPI and AWS. The fork is the decision an architect owns.
Diagram source

The registry side is real code

PyPI's receiver verifies GitHub's HMAC signature on a disclosure, checks the token cryptographically, then calls delete_macaroon and only afterwards emails the owner. Two disclosure origins are wired in: GitHub and Google's Deps.dev.

Source: warehouse config.py, utils.py

The cloud side is a policy document

AWS's automated response attaches a deny policy to the exposed key's principal. The 2024 generation (V3) denies 98 actions: IAM escalation, instance launches, Lambda creation, and, notably, cloudtrail:LookupEvents, so the attacker cannot watch the investigation. Everything else keeps working.

Source: AWSCompromisedKeyQuarantineV3, tracked by MAMIP

The platform side is asymmetric

GitHub revokes its own leaked tokens automatically on public exposure. For partners' tokens the same scanner only forwards the finding, and "the service provider validates the string and then decides whether they should revoke the secret". Same detector, two different authorities.

Source: token revocation docs, partner programme docs

03

The decisions that matter

The recurring fork: what may act automatically when a credential is found in public, and what it may do. Three platforms answered it three ways, and the difference is not philosophy, it is blast radius.

When a live credential is found in public, does the machine revoke it?

Revoke, no human in the loop
  • GitHub, for its own tokens: pushed to a public repo, "the token will be automatically revoked" [docs]
  • PyPI: delete_macaroon first, email second [source]
  • Why it wins here: the worst case of a false kill is a broken publish or login, which retries
Fence or forward instead
  • AWS quarantines: a deny list of 98 actions, key stays live [policy]
  • GitHub, for partner tokens: the provider "decides whether they should revoke" [docs]
  • Why: a leaked AWS key may be load-bearing in someone's production; killing it converts a security event into an outage the platform caused
Flips when
  • The credential's legitimate function is availability-critical. The softer the automated response you observe, the bigger the revocation blast radius the operator fears
  • Design transfer: for each credential class you issue, decide at design time who may kill it automatically. If the honest answer is "nobody, revocation would cause an outage", you have found a dependency that needs to be short-lived instead

Should a scanner verify findings by logging in with them?

Verify
  • TruffleHog: verification as the core feature, "critical to know if there's an active present danger" [README]
  • GitHub validity checks: periodically "sending the secret to the issuer and testing it" [docs]
  • Betterleaks, from the gitleaks lineage, ships "Secrets Validation" natively [README]
Detect only
  • gitleaks stayed detection-only for its whole life; the maintainer built a 43-commit "Verify feature" branch in 2024 and closed it unmerged [PR #1515], then declared the tool feature-complete [README]
  • The closure reason is not recorded on the PR; what is recorded is that the same author shipped verification in the successor tool
Flips when
  • Finding volume exceeds triage capacity: verification is what turns ten thousand matches into the forty that are live
  • Flips back when sending found secrets to third-party APIs is itself a policy problem; GitHub's docs are explicit about choosing "the least intrusive endpoints" for exactly this reason

Does pinning third-party CI code by commit SHA protect you?

What March 2025 showed
  • Tag pinning failed totally: every tj-actions tag was force-moved to the malicious commit [#2463]
  • SHA pinning failed transitively: five reviewdog actions were compromised "regardless of version or pinning method" because they resolve an inner action at run time [GHSA]
And the quiet third failure
  • Repositories that pinned by SHA were observed running the backdoored commit anyway, "after updates by Renovate" [#2463]: the update bot dutifully bumped the pin to the attacker's hash
Flips when
  • Pinning works only when the whole chain is pinned and the pin-updater is itself gated by review. A SHA pin plus auto-merge equals a tag
  • OpenSSF Scorecard measures both halves: pinned dependencies and workflow token scope, the latter rated "Risk: High" [checks.md]

Figure 3 · A decision tree for each credential you issue

no

yes

no

yes

yes

no

Must this credential
outlive one job?

Issue it per job:
OIDC or TokenRequest.
A leak dies on its own

Would auto-revocation
break production serving?

Wire automatic revocation
to detection, PyPI-style

Can you enumerate every
holder and every use?

Quarantine first, AWS-style,
then rotate on a rehearsed path

You cannot respond yet.
Build the inventory before
the next control

no

yes

no

yes

yes

no

Must this credential
outlive one job?

Issue it per job:
OIDC or TokenRequest.
A leak dies on its own

Would auto-revocation
break production serving?

Wire automatic revocation
to detection, PyPI-style

Can you enumerate every
holder and every use?

Quarantine first, AWS-style,
then rotate on a rehearsed path

You cannot respond yet.
Build the inventory before
the next control

Derived from the three platform policies and the two KEPs; terminal nodes are actions. The bottom-left exit is the one most organisations are actually standing in.
Diagram source
DecisionChosenRejectedBecauseEvidence
Platform response to own leaked tokenAuto-revoke on public exposureOwner notification firstWorst case of a false kill is a retryable failureGitHub docs
Registry response to disclosed tokenDelete, then emailGrace periodPublishing can wait; exploitation cannotwarehouse utils.py
Cloud response to exposed access keyDeny-list quarantine, key stays liveAutomatic key disableThe key may be load-bearing; the fence blocks escalation and forensics-watching insteadquarantine V3
Nx durable fix after token theftTrusted publishing, no npm tokensNew tokens plus tighter storageA credential that does not exist cannot be re-stolenNx advisory
Kubernetes token modelBound, audience-scoped, expiringKeep legacy secret-based tokensLegacy tokens were "valid for as long as the service account exists"; removal took KEP-2799 three feature gates and until v1.30KEP-2799
reviewdog org access model, afterWrite access withdrawn from most members118-member auto-invited write teamThe auto-inviter was the attack surfacereviewdog #2079
History scrubbing after a leakRevoke first; scrubbing optionalRewrite git history as the fix"time-intensive and often unnecessary if you've already revoked"GitHub docs
04

What broke in production

Five failure classes cover the four incidents. None of them is "the scanner missed the secret". All of them are coordination failures around a secret that was already known to exist.

Figure 4 · The tj-actions failure path: memory to public log, past the masker

"Public build log""Runner""Victim workflow""Action repository""Attacker""Public build log""Runner""Victim workflow""Action repository""Attacker"masker matches literal registeredstrings, soencoded values pass throughuse stolen bot PAT,force-move every version tagresolve changed-files@v45malicious commit 0e58ed8job runs, secrets inRunner Worker memorymemdump.pyscrapesisSecret:true valuesprint secrets,double base64 encodedscrape public logs, decode
"Public build log""Runner""Victim workflow""Action repository""Attacker""Public build log""Runner""Victim workflow""Action repository""Attacker"masker matches literal registeredstrings, soencoded values pass throughuse stolen bot PAT,force-move every version tagresolve changed-files@v45malicious commit 0e58ed8job runs, secrets inRunner Worker memorymemdump.pyscrapesisSecret:true valuesprint secrets,double base64 encodedscrape public logs, decode
Reconstructed from GHSA-mrrh-fwg8-r2c3 and the live thread in #2463. The masking step fails because runner masking is literal string matching and the payload double-base64-encodes everything first.
Diagram source
Postmortem

The masked log was an exfiltration channel

AssumptionSecrets printed in CI logs are redacted, so logs are safe to make public.
What happenedThe tj-actions and reviewdog payloads dumped runner memory and printed every secret double-base64-encoded. Masking matches registered literal strings, so encoded output passed untouched; a commenter confirmed "double base64 --decode that output and boom, there is a github_token".
Blast radius23,000+ repositories used the action; every public repo's workflow log became a self-serve credential dump for about a day.
FixTags restored, PAT revoked; the structural gap remains: the runner's own maintainer described the mask-registration race in 2020 and the issue is still open, 75 comments later.
Design ruleTreat CI logs as an untrusted egress channel, not as redacted output. Masking is a courtesy to honest code; only egress control and short-lived credentials constrain hostile code.
Postmortem

The credential you forgot survived the response

AssumptionFixing the compromised pipeline ends the incident.
What happenedUltralytics shipped a workflow security fix on December 5, 2024. On December 7 the attacker published 8.3.45 and 8.3.46 straight to PyPI: versions that exist on the registry and nowhere in the repository, which is only possible with a registry credential the first response left alive.
Blast radiusTwo more poisoned releases of a package with mass adoption, days after the project believed itself clean.
FixPublishing split into isolated jobs (Dec 8 commit, "split PyPI publish jobs for security"); caches disabled and pruned.
Design ruleIncident response starts from the credential inventory, not from the incident's artifacts. Rotation completeness beats rotation speed; one surviving token reopens everything.
Postmortem

Attacker-controlled text met a credentialed shell

AssumptionA workflow that only validates PR titles is harmless, because it holds no secrets itself.
What happenedNx's validator ran echo "Validating PR title: ${{ github.event.pull_request.title }}" under pull_request_target. The injected title used the elevated token to trigger the publish workflow, which sent the npm token to an attacker webhook. Ultralytics fell to the same class months earlier; its own fix deleted the code reading PR titles into shell variables.
Blast radiusEight malicious package versions; every installer's filesystem scanned; harvested tokens posted as repos on victims' accounts.
FixInjection removed, CodeQL enabled, external-contributor workflows gated; the durable piece is trusted publishing, which removes the stealable token.
Design ruleIn CI, injection is a credential leak, not a code-quality nit. Any workflow that touches attacker-controlled strings must run with a token that can do nothing worth stealing.
Postmortem

Trust rode on a mutable reference

AssumptionA version tag names a version.
What happenedOne stolen PAT force-moved every tj-actions tag, back to v1.0.0, onto one malicious commit; consumers changed nothing and became vulnerable retroactively. reviewdog's inner action meant even SHA-pinned outer actions resolved attacker code, and Renovate re-pinned careful repos onto the malicious hash because the hash was where the tag now pointed.
Blast radiusThe full dependent population of both action families, in a combined window of about a day and two hours respectively.
FixTags restored from clean state; reviewdog cut write access for most of its 118-member auto-invited team.
Design ruleAnything resolved at run time is inside your credential perimeter. Pin the whole chain, gate the pin-updater, and alert on tag moves in dependencies you execute with secrets present.
Postmortem

The response automation had the wrong reflexes

AssumptionAutomation in the response path is neutral: at worst it does nothing.
What happenedUltralytics' triage bot answered the compromise report in 35 seconds with "pip install -U ultralytics", the exact command that installed the malware; the human correction took 94 minutes. tj-actions' bot answered "this repo got hacked" with "don't forget to star this project". And GitHub's cleanup of Nx exfiltration repos deleted victims' only copy of what was stolen: the advisory tells victims to ask support for the contents.
Blast radiusUnmeasurable directly; each reflex widened exposure or destroyed forensics during the highest-value hour.
FixNone recorded in the corpus. The bots' behaviour was unchanged by the incidents they misled.
Design ruleEvery automated responder needs an incident mode, and every automated cleanup needs an evidence-preservation step. If your bot cannot tell a breach report from a bug report, it is part of the breach.
Source

The structural fix runs on a decade clock

AssumptionOnce a credential design is known to be unsafe, it gets replaced.
What happenedKubernetes wrote down in August 2019 that its service account tokens were unbounded in audience and time. Bound tokens went stable in v1.22 (2021); actually removing the legacy tokens needed a second KEP, three feature gates and reached stable in v1.30, in 2024. Attackers, meanwhile, operated on the hour scale shown above.
Blast radiusEvery cluster that ran between the diagnosis and the cleanup carried tokens "valid for as long as the service account exists".
FixKEP-1205 plus KEP-2799, five years end to end for one ecosystem's one credential class.
Design ruleBudget structural credential migrations in years, and cover the gap with the fast controls (detection, revocation, quarantine) because the slow one will not arrive in time for this incident or the next.
05

Numbers you can plan against

Windows, populations and policy sizes from the primary record. Every row links its source; nothing here is an industry-survey figure.

Figure 5 · The Nx clock: what each hour of August 26–27, 2025 cost

6.32 pm · first malicious version published

8.30 pm · users open issues on GitHub

9.58 pm · team notices via Slack

10.44 pm · npm removes versions, freezes publishes

11.57 pm · all publishing tokens revoked

1.53 am · advisory published

11.57 am · trusted publishing, npm tokens abolished

6.32 pm · first malicious version published

8.30 pm · users open issues on GitHub

9.58 pm · team notices via Slack

10.44 pm · npm removes versions, freezes publishes

11.57 pm · all publishing tokens revoked

1.53 am · advisory published

11.57 am · trusted publishing, npm tokens abolished

Times in EDT from the Nx advisory. Detection came from users posting GitHub issues, not from any pipeline; the token-free fix landed twelve hours after full revocation.
Diagram source
MetricValueAtContextAs ofSource
Dependent repositories exposed23,000+tj-actionsOne action, all tags rewritten2025-03GHSA
Exposure window~1 daytj-actionsMarch 14–15, 2025, per advisory2025-03GHSA
Exposure window1h 49mreviewdog18:42–20:31 UTC, March 11, 20252025-03GHSA
Publish to full token revocation5h 25mNx / npm6:32 pm to 11:57 pm EDT2025-08advisory
Report to confirmed registry removal~3h 15mUltralytics / PyPIIssue 06:34 UTC; removal confirmed in-thread 09:50 UTC2024-12#18027
Days from first fix to last remediation commit3UltralyticsDec 5 fix; Dec 8 "split PyPI publish jobs for security"2024-12commit log
Quarantine policy deny-list growth28 → 89 → 98AWSV1 (tracked 2020) to V2 (2021) to V3 (2024, updated 2026-03)2026-03MAMIP
Auto-invited members holding write118reviewdogThe org team the attacker's PAT belonged to2025-03#2079
Secret types a scanner verifies800+TruffleHogDetectors that can "log in to confirm"2026-09README
Age of the mask-registration race issue6+ yrsactions/runnerOpened May 2020, open, 75 comments2026-09#475
Ecosystem time to retire one token class~5 yrsKubernetesKEP-1205 (2019-08) to KEP-2799 stable in v1.30 (2024)2024kep.yaml
Read these carefully

All figures above are measured or stated by the primary record; none are vendor marketing. What is missing is as important: this corpus contains no fetchable number for how fast a leaked cloud key is first abused (the widely cited minutes-scale honeypot studies live on hosts this session could not reach), no total for secrets leaked per year (the annual scanning-vendor reports were unreachable), and no measured false-positive rates for any scanner. If those numbers drive a decision for you, fetch and date them yourself before relying on them.

06

The evidence wall

Every source behind this page, graded. One host: this corpus is the GitHub record, by construction; the ledger shipped beside this page says exactly what that excludes.

Postmortem GitHub Advisory DB2025-03-15

CVE-2025-30066: tj-actions/changed-files supply chain attack

The authoritative record: 23,000+ dependent repositories, all tags retroactively moved to one malicious commit, secrets scraped from Runner Worker memory into public logs, patched in v46.0.1.

Carry forwardA tag rewrite makes you vulnerable retroactively; your dependency review never saw a diff.
github.com/advisories/GHSA-mrrh-fwg8-r2c3
Postmortem tj-actions community2025-03-14

Issue #2463: the incident, live

The detection report ("this tag was just updated 3 hours back"), the payload dissection, the double-base64 confirmation, and the observation that Renovate was updating SHA-pinned repos onto the malicious hash while the thread ran.

Carry forwardThe incident thread is the only place the auto-updater betrayal is recorded; advisories smooth it away.
github.com/tj-actions/changed-files/issues/2463
Postmortem tj-actions community2025-03-14

Issue #2464: the bot answers a breach report

"Pretty sure this repo got hacked" was answered in 38 seconds by the project's bot: "don't forget to star this project if you haven't already to help us reach a wider audience."

Carry forwardResponse automation without an incident mode becomes part of the incident.
github.com/tj-actions/changed-files/issues/2464
Postmortem GitHub Advisory DB / reviewdog2025-03-19

CVE-2025-30154: reviewdog/action-setup compromise

A one-hour-49-minute window on March 11, 2025; five downstream actions compromised "regardless of version or pinning method" because they resolve the inner action at run time.

Carry forwardYour pin is only as strong as the least-pinned reference in the transitive chain.
github.com/advisories/GHSA-qmg3-hpqr-gqvc
Postmortem reviewdog (haya14busa)2025-03-18/23

Issue #2079: the maintainer's own investigation

Auto-invites had given 118 contributors write access; the stolen PAT "originated from an attack on another repository"; remediation removed write from most members. Includes the maintainer asking GitHub for the API logs to find the tag-mover.

Carry forwardAccess-growth automation is a credential issuer; audit it like one.
github.com/reviewdog/reviewdog/issues/2079
Postmortem Nx (nrwl)2025-08-27

GHSA-cxm3-wv7p-598c: the s1ngularity advisory

A minute-by-minute timeline from PR-title bash injection under pull_request_target, through npm token exfiltration to a webhook, to malware that posted victims' credentials as repos on their own accounts. Ends with npm tokens abolished in favour of trusted publishing.

Carry forwardThe strongest fix in the whole corpus: make the stealable credential stop existing.
github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c
Postmortem Ultralytics community2024-12-05

Issue #18027: wheel does not match repository

A user diffed the published wheel against the tag and found a miner. The triage bot recommended upgrading to the compromised latest; the human correction and registry removal followed within about three hours.

Carry forwardRegistry-vs-repo diffing is a detection control nobody owns; the person who ran it was a user.
github.com/ultralytics/ultralytics/issues/18027
Source Ultralytics2024-12-05/08

The remediation, read from git

Dec 5: stored PAT replaced with the ephemeral job token, PR-title shell interpolation deleted. Dec 7: caches disabled and pruned. Dec 8: publish pipeline split "for security". The staging shows what the first response missed, and the registry-only versions 8.3.45/46 show what it cost.

Carry forwardRead remediation commits in order; the gaps between them are the lesson.
ultralytics commit 68c63a7e5
Source Ultralyticschecked 2026-09-20

The tag record, with holes

v8.3.43 and v8.3.44 exist; v8.3.41/42 were deleted after the compromise; v8.3.45/46 never existed in git at all. Inferred, and stated as inference: those two versions were published directly to the registry with a stolen token.

Carry forwardA version on the registry with no matching tag is a leaked-credential detector in itself.
github.com/ultralytics/ultralytics/tags
Source GitHub (actions/runner)2020-05, open

Issue #475: how masking actually works

The runner maintainer, 2020: "The mask needs to be added before the output is registered. … The output is streaming, so too late if value is already printed." Literal registration plus streaming output is exactly the gap double-base64 walked through five years later.

Carry forwardMasking is best-effort string replacement; nothing about it is a security boundary.
github.com/actions/runner/issues/475
Source PyPI (pypa/warehouse)current

The disclosure receiver, in production code

HMAC-verified disclosures from GitHub and Deps.dev; verify_signature_only, then delete_macaroon, then the email. The ordering is the policy: revocation does not wait for a human.

Carry forwardIf you issue tokens, this ~300-line file is the reference implementation of the receiving end.
pypa/warehouse integrations/secrets/utils.py
Source AWS, tracked by z0ph/MAMIP2020–2026

AWSCompromisedKeyQuarantine, three generations

The managed policy AWS attaches to exposed keys, diffed over time by a tracker repo: 28 denied actions in V1 (tracked 2020), 89 in V2 (2021), 98 in V3 (first seen 2024-08-21, still updated 2026-03). A deny list, not a kill: the fence model in public.

Carry forwardThe deny list doubles as AWS's published model of what attackers do with a stolen key.
z0ph/MAMIP policies/AWSCompromisedKeyQuarantineV3
Source Truffle Security2026-09

TruffleHog: verification as the product

"Over 800 secret types", each mapped to an identity, and for each "it can also log in to confirm if that secret is live or not. This step is critical to know if there's an active present danger."

Carry forwardAn unverified finding is a chore; a verified-live finding is an incident. Triage on liveness.
github.com/trufflesecurity/trufflehog
Source Gitleaks2024-09, closed

PR #1515: the verify feature that was not merged

The gitleaks maintainer built verification himself, 43 commits, opt-in, status enums, then closed the PR unmerged (as with its successor #1519) and later froze the project. The README today: gitleaks is "a tool for detecting secrets". The reason is not recorded; the outcome is.

Carry forwardA closed-unmerged PR from the maintainer is the strongest "we considered it" record there is.
github.com/gitleaks/gitleaks/pull/1515
Source Betterleaks2026

The successor ships what the ancestor declined

"Maintained by the folks who made Gitleaks, including the original author", with "Secrets Validation" as a headline feature: validate liveness with HTTP requests from inside the rule definition. The 2024 argument resolved itself in the 2026 tool.

Carry forwardWhere a feature was rejected, check the author's next project before treating the rejection as doctrine.
github.com/betterleaks/betterleaks
Source Thinkstcurrent

Canarytokens: credentials as tripwires

The self-hostable server behind canarytokens.org: mint deliberately plantable credentials whose only legitimate use is never, so any use is a high-confidence alarm. The inverse of everything else on this wall: a leak you want.

Carry forwardA planted key in your CI secrets answers "would we notice" empirically, before an attacker does.
github.com/thinkst/canarytokens
Decision record Kubernetes SIG Auth2019-08-06

KEP-1205: Bound Service Account Tokens

The problem statement that still reads as the whole field's thesis: tokens that are not audience bound let "any recipient of a JWT masquerade as the presenter to anyone else", and tokens that are not time bound stay valid "for as long as the service account exists".

Carry forwardAudience and expiry are the two attenuations that turn a leak from a compromise into an inconvenience.
kubernetes/enhancements KEP-1205
Decision record Kubernetes SIG Auth2021-06-25

KEP-2799: Reduction of Secret-based Service Account Tokens

The removal plan for the legacy tokens: no-auto-generation, then tracking of remaining use, then cleanup, three feature gates landing stable in v1.30. The tracking gate exists because you cannot delete what you cannot prove unused, which is the enumeration problem again, inside the fix.

Carry forwardPlan the migration off a credential class as: stop issuing, then measure remaining use, then delete. In that order, with telemetry.
kubernetes/enhancements KEP-2799
Decision record OpenSSFcurrent

Scorecard: Token-Permissions, risk High

The ecosystem's measurable proxy for this failure class: does each workflow token follow least privilege, because "attackers may use a compromised token with write access to … push malicious code into the project". Both March 2025 attacks ran through exactly that.

Carry forwardWorkflow token scope is auditable at scale today; it is the cheapest of these controls to check.
ossf/scorecard docs/checks.md
Platform docs GitHubcurrent

Token revocation on public exposure

"If a valid OAuth token, GitHub App token, or personal access token is pushed to a public repository or public gist, the token will be automatically revoked." Also: unused tokens die after a year.

Carry forwardThe platform kills its own tokens without asking; assume the same posture for tokens you issue.
github/docs token-expiration-and-revocation.md
Platform docs GitHubcurrent

The partner programme: notify, do not kill

For other issuers' secrets, GitHub forwards the finding and "the service provider validates the string and then decides whether they should revoke the secret, issue a new secret, or contact you directly. Their action will depend on the associated risks to you or them."

Carry forwardRevocation authority stays with whoever bears the outage; detection and authority are separable.
github/docs secret-scanning-for-partners.md
Platform docs GitHubcurrent

Remediation guidance: rotate, do not scrub

"Rotate the affected credential immediately … removing secrets from your Git history … is time-intensive and often unnecessary if you've already revoked the credential." The docs also enumerate the scan surface: issues, PR comments, discussions, wikis, gists.

Carry forwardThe secret is compromised at first exposure forever; only revocation changes its value.
github/docs secret-scanning.md
Platform docs GitHubcurrent

Push protection: move detection before the leak

"Rather than alerting you to credential leaks after the fact, push protection blocks pushes that contain secrets before they reach your repository", at the command line, the UI, the API and file upload.

Carry forwardPre-receive blocking is the only control that keeps the secret out of history entirely; everything after it is damage control.
github/docs push-protection.md
Platform docs GitHubcurrent

Validity checks: the platform logs in with your leak

Opt-in checks that "periodically check the validity of a detected credential by sending the secret to the issuer and testing it against APIs provided by that service", with deliberately least-intrusive endpoints.

Carry forwardLiveness triage has become a platform feature; wire your alert routing to the active state, not to detection.
github/docs validity-checks.md
Platform docs GitHubcurrent

OIDC for deployments: the credential that is not there

The Actions OIDC concept doc: the cloud "issues a short-lived access token that is only valid for a single job, and then automatically expires", and the first listed benefit is "No cloud secrets". This is the per-job exit at the top of Figure 3.

Carry forwardFor CI-to-cloud, the leak-proof credential is generally available today; storing cloud keys in CI is now a choice.
github/docs openid-connect.md
Platform docs AWScurrent

configure-aws-credentials: the vendor's default is keyless

The official action's quick start is titled "OIDC, recommended", and its security recommendations open with "Use temporary credentials when possible. OIDC is recommended because it provides temporary credentials and it's easy to set up."

Carry forwardWhen the cloud vendor's own quick start avoids stored keys, a stored key in CI needs a written justification.
aws-actions/configure-aws-credentials
07

Build a miniature, then productionise it

Seven rungs. The first three fit in a weekend on a sandbox account; the line from toy to real is crossed at rung four, where revocation meets production.

Trip your own wire

Mint a canary credential (a Canarytokens AWS key or one of your own dead tokens), plant it in a private test repo, a CI variable and a wiki page, then use it once.

Done when: the alert reaches a human, and you can say from which of the three plantings.  Teaches: which of your leak surfaces are actually watched.

Scan your own history, then triage by liveness

Run TruffleHog or a gitleaks-lineage scanner across your organisation's repositories, including issues and wikis where supported. Split findings into live and dead.

Done when: you have two counts, and every live finding has an owner.  Teaches: why the verify-vs-detect decision in section 3 dominates triage cost.

Build the inventory the incidents lacked

For one system, enumerate every long-lived machine credential: issuer, holder, storage location, scopes, revocation API, and who may call it. The Ultralytics second wave is the test case: would your list have contained the registry token?

Done when: a named person can produce the list in under an hour, unaided.  Teaches: enumeration is the actual hard problem; everything else is tooling.

Wire a disclosure receiver

Stand up a small service in the warehouse mould: accept a signed report of a leaked token of your own format, verify the signature, revoke, then notify. Feed it from your scanner's webhook.

Done when: a planted leak of your own token format is dead within minutes with no human action, and the owner has an email explaining why.  Teaches: revocation-before-notification as a code path, not a runbook step.

Rehearse the kill in production

Pick one production credential and revoke it on purpose during business hours, with the owning team watching. Measure what breaks and for how long. This is the AWS-fence lesson inverted: if you cannot afford to run this rehearsal, you cannot afford the real response either.

Done when: mean time to re-issue is known, and the one credential whose revocation would cause an outage is on a remediation list.  Teaches: the blast radius of revocation, which section 3 showed drives every platform's policy.

Abolish one credential class

Move one CI-to-cloud or CI-to-registry path from a stored secret to OIDC or trusted publishing, the Nx endgame. Then delete the old secret and watch what complains.

Done when: the old credential is revoked and the pipeline is green for a week.  Teaches: the KEP-2799 sequence in miniature: stop issuing, measure use, delete.

Watch your mutable references

Alert on tag moves and unexpected pin changes in the third-party actions and images your credentialed jobs execute, and gate your dependency-update bot's pin bumps behind review for that set.

Done when: a test tag-move on a fork raises an alert before the next scheduled job runs.  Teaches: why tj-actions' consumers had no move to make; the ones who noticed were the ones watching references, not code.

08

Keep hunting

What actually found this material. Half of it is ordinary search; the other half is treating GitHub itself as the archive, which works from any network that can reach it.

Incident primaries on GitHub

  • site:github.com advisories "supply chain" secrets leaked CI logs
  • tj-actions OR reviewdog OR ultralytics compromised issue postmortem
  • repo:<org>/<repo> in:title compromised OR hacked OR "rotate"
  • https://raw.githubusercontent.com/github/advisory-database/main/advisories/github-reviewed/<YYYY>/<MM>/<GHSA-id>/<GHSA-id>.json

Git archaeology on a remediation

  • git clone --filter=blob:none --no-checkout https://github.com/<org>/<repo>
  • git log --since=<incident> --until=+7days -- .github/workflows/publish.yml
  • git log -1 v<bad-version> # a missing tag means a registry-only release
  • git show <fix-commit> -- .github/workflows/ # what the first response changed

The rejected-design layer

  • is:pr is:closed is:unmerged sort:comments-desc verify
  • repo:kubernetes/enhancements path:keps/sig-auth token
  • repo:actions/runner is:issue mask secret sort:comments-desc

Policy and platform behaviour as data

  • repo:z0ph/MAMIP AWSCompromisedKeyQuarantine # AWS managed-policy diffs, dated
  • repo:github/docs path:content/code-security secret-scanning
  • repo:pypa/warehouse path:warehouse/integrations/secrets
09

References

  1. GitHub Advisory Database, CVE-2025-30066: tj-actions/changed-files embedded malicious code GitHub, published 2025-03-15, modified 2025-10-22. Checked 2026-09-20.
  2. tj-actions/changed-files issue #2463: Multiple tags in this action are compromised GitHub, 2025-03-14. Checked 2026-09-20.
  3. tj-actions/changed-files issue #2464: Pretty sure this repo got hacked GitHub, 2025-03-14. Checked 2026-09-20.
  4. GitHub Advisory Database, CVE-2025-30154: multiple reviewdog actions compromised GitHub, published 2025-03-19. Checked 2026-09-20.
  5. reviewdog issue #2079: Supply chain attack on reviewdog GitHub Actions (maintainer advisory and investigation updates) GitHub, 2025-03-18 to 2025-03-23. Checked 2026-09-20.
  6. Nx security advisory GHSA-cxm3-wv7p-598c: malicious versions of nx published (s1ngularity) GitHub, published 2025-08-27, modified 2026-07-28. Checked 2026-09-20.
  7. ultralytics issue #18027: Discrepancy between what's in GitHub and what's been published to PyPI for v8.3.41 GitHub, 2024-12-05. Checked 2026-09-20.
  8. ultralytics commit 68c63a7e5: 8.3.43 PyPI publishing security fix GitHub, 2024-12-05. Checked 2026-09-20.
  9. ultralytics commit e0f8eda36: 8.3.48 split PyPI publish jobs for security GitHub, 2024-12-08. Checked 2026-09-20.
  10. ultralytics repository tag listing GitHub, live record. Checked 2026-09-20.
  11. actions/runner issue #475: usage of add-mask still echoes the value to the log GitHub, 2020-05-11, open. Checked 2026-09-20.
  12. pypa/warehouse, warehouse/integrations/secrets/utils.py PyPI source code, main branch. Checked 2026-09-20.
  13. pypa/warehouse, warehouse/integrations/secrets/config.py PyPI source code, main branch. Checked 2026-09-20.
  14. z0ph/MAMIP, policies/AWSCompromisedKeyQuarantineV3 (and V1, V2, with git history) Monitor AWS Managed IAM Policies, tracked 2020–2026. Checked 2026-09-20.
  15. Kubernetes KEP-1205: Bound Service Account Tokens kubernetes/enhancements, created 2019-08-06, stable v1.22. Checked 2026-09-20.
  16. Kubernetes KEP-2799: Reduction of Secret-based Service Account Tokens kubernetes/enhancements, created 2021-06-25, stable v1.30. Checked 2026-09-20.
  17. trufflesecurity/trufflehog README GitHub. Checked 2026-09-20.
  18. gitleaks/gitleaks README (feature-complete notice) GitHub. Checked 2026-09-20.
  19. gitleaks pull request #1515: Verify feature, closed unmerged GitHub, opened 2024-09-21. Checked 2026-09-20.
  20. betterleaks/betterleaks README GitHub. Checked 2026-09-20.
  21. thinkst/canarytokens README GitHub. Checked 2026-09-20.
  22. OpenSSF Scorecard check documentation (Token-Permissions, Pinned-Dependencies) ossf/scorecard. Checked 2026-09-20.
  23. GitHub Docs source: Token expiration and revocation github/docs, main branch. Checked 2026-09-20.
  24. GitHub Docs source: Secret scanning (with push-protection.md, secret-scanning-for-partners.md, validity-checks.md, what-is-scanned.md beside it) github/docs, main branch. Checked 2026-09-20.
  25. GitHub Docs source: About security hardening with OpenID Connect github/docs, main branch. Checked 2026-09-20.
  26. aws-actions/configure-aws-credentials README GitHub. Checked 2026-09-20.