Sourced from: my talk "Trust No Pipeline: Securing Software Delivery & Workload Identity in the Cloud" (slides), the companion live demo repo, and the public 2025–2026 incident write-ups linked at the bottom.
So here is the thing. We spent the last decade turning our production clusters into fortresses. Private GKE nodes. VPCs. Cloud Armor. IAM policies reviewed by three different teams. Firewalls on top of firewalls.
And attackers looked at all of that and said: "Cool. We will not touch it."
Why would they? Why fight the fortress when you can hijack the delivery truck?
Your CI/CD pipeline is that truck. It drives through the front gate every ten minutes. Nobody checks it at the gate, because it is your truck. It carries roles/owner. It runs preinstall hooks from packages you have never read. It pulls GitHub Actions by mutable @v1 tags. And somewhere in its glovebox there is probably a Service Account JSON key that expires in the year 9999.
That is the whole plot twist of modern supply chain attacks. Nobody is breaking in. They are riding in.
And here is the kicker: this is not a theoretical "what if" talk. Every pattern in this post comes from a real incident in 2025 or 2026. Tens of thousands of repositories. Hundreds of npm packages. Billions of monthly downloads. All of it exploiting the same thing: we trust our pipelines way more than we should.
So let us stop doing that.
The Bait and Switch That Hit 23,000 Repos Overnight
Let me start with the one that made everyone pay attention.
March 2025. CVE-2025-30066. tj-actions/changed-files, a GitHub Action used by more than 23,000 repositories.
The attackers did not hack Google Cloud. They did not hack AWS. They did not even hack the victims. They stole a bot token, and used it to repoint every single version tag of the action -- @v1 all the way through @v45 -- to a malicious commit.
That commit ran a little Python script called memdump.py. It read /proc/$pid/mem of the Runner.Worker process and dumped everything it found straight into the build logs. Cleartext. GCP Service Account JSON keys, AWS keys, npm tokens, GitHub tokens. In public logs, for a lot of open-source projects.
If your workflow said uses: tj-actions/changed-files@v45, you were not running the code you reviewed. You were running whatever the tag pointed to that morning.
How they actually got there: six steps from SpotBugs to Coinbase
What makes this one fascinating is that it was not a single hack. It was a cascade:
- SpotBugs PR trap. A maintainer PAT was added to
spotbugs/sonar-findbugs, then stolen through a maliciouspull_request_targetPR. - Rogue invite and leak. On March 11, that PAT invited a rogue user (
jurkaofavak) tospotbugs/spotbugs. Two minutes later a workflow leaked the Reviewdog maintainer's PAT, AES + RSA encrypted, so nobody would notice it in the logs. -
reviewdog/action-setup@v1poisoned. The leaked PAT overwrote the mutablev1tag. - Pivot to
tj-actions.tj-actions/eslint-changed-filesusedreviewdog/action-setup@v1as a transitive dependency, which exposed thetj-actionsprivileges. - The actual target:
coinbase/agentkit. Five days after Coinbase addedtj-actions/changed-files, the attacker forked the relevant repos to stage the attack. - All
@v1–@v45repointed. And 23,000+ repositories came along for the ride.
Read that list again. Every single hop is a pipeline trusting another pipeline. Nobody exploited a buffer overflow. Nobody needed a zero-day. Mutable tags and over-privileged tokens did all the work.
Then the Worms Showed Up
If tj-actions was a targeted heist, 2025 and 2026 gave us something worse: self-propagating supply chain worms.
Shai-Hulud 2.0 (Sept–Nov 2025) ran inside preinstall hooks, fired up an embedded TruffleHog scanner to harvest cloud credentials (GOOGLE_CREDENTIALS, AWS keys, Vault tokens), then automatically republished trojanized versions of every package the victim owned. As a bonus, it registered persistent rogue self-hosted GitHub Actions runners named SHA1HULUD.
Mini Shai-Hulud (May 2026) was the streamlined sequel. Triggered via pull_request_target ("pwn requests"), it poisoned pnpm caches and dumped OIDC tokens directly from CI runner memory, hitting ecosystems including TanStack, Mistral AI, and UiPath.
ChainDrop (August 2026) compromised core JavaScript maintainer accounts -- most notably keyv (think flat-cache, cacheable-request) -- and spread to 400+ npm packages and 1,300+ versions. It used a preinstall hook (setup.mjs) to drop an obfuscated Bun payload on Linux, macOS, and Windows, and routed its command-and-control through Ethereum smart contracts so corporate DNS blocklists had nothing to block. It then committed malicious updates under the dummy author claude with the very innocent message chore: update config.
And the most uncomfortable lesson from ChainDrop? SLSA Level 2 did not save anyone. The maintainer's credentials were compromised before the build, so the provenance looked perfectly valid. The build system faithfully attested that it built the malware.
What do all of these have in common? They eat keys. Static keys, long-lived tokens, OIDC tokens sitting in runner memory. Starve them of keys and they stop being worms and start being very sad scripts.
The Four (Okay, Five) Pillars
The rest of this post is the defense. I structured the talk around four pillars, plus a "pillar zero" that I think gets ignored way too often. For each one I will show the idea, the actual config, and the attack from the demo repo failing against it.
Pillar 0: A Stolen Token Should Not Equal a Valid Commit
Here is something that does not get talked about enough. Anyone can create a commit with someone else's name and email:
git commit --author="Trusted Maintainer <maintainer@example.com>" -m "chore: update config" Git will happily accept it. GitHub will happily display it. That is exactly how ChainDrop's claude commits blended in.
What we actually care about is authenticity (who really made this commit?) and integrity (has it been tampered with since?). Commit signing gives you both. If a signed commit is modified, the signature breaks immediately.
But here is where it gets good. If you enforce that only signatures from maintainers' physical YubiKey / FIDO keys are accepted, then a stolen GitHub PAT is no longer enough to land code. The attacker has your token. They do not have the piece of hardware on your keychain.
# 1. Lock Git to the maintainer's YubiKey GPG key git config --global user.signingkey 0x9F8B1234 git config --global commit.gpgsign true git config --global tag.gpgSign true # 2. Every commit now requires a physical touch on the key git commit -S -m "feat: release v1.0.0" Then enforce it on both ends: turn on Settings → Rules → Require signed commits in GitHub, and verify against a known keyring in CI:
- name: Verify Maintainer Hardware GPG run: | gpg --import .github/maintainer-yubikeys.asc git verify-commit HEAD Stolen PAT without the YubiKey? Rejected.
Pillar 1: Kill Static Secrets (Vampire Keys vs. Melting Ice Cubes)
Why did Shai-Hulud and ChainDrop do so much damage? Because teams still export GCP Service Account JSON keys.
A JSON key is a vampire. It lives until December 31, 9999 unless somebody drives a stake through its heart. It sits in a GitHub secret, gets loaded into the runner's environment, and waits for the first preinstall hook with a TruffleHog binary to come along and grab it.
An ephemeral token is an ice cube. It is issued on demand and melts in about an hour. Even if it ends up dumped in a public log, by the time someone finds it, it is a puddle.
On Google Cloud, you can kill the vampires for your entire project or organization with one Organization Policy. Five lines of YAML:
# disable-sa-keys.yaml name: projects/${PROJECT_ID}/policies/iam.disableServiceAccountKeyCreation spec: rules: - enforce: true # 1. Audit what is already out there gcloud iam service-accounts keys list \ --iam-account="ci-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \ --managed-by=user \ --format="table(name.basename(), validAfterTime, validBeforeTime, keyAlgorithm)" # 2. Block key creation going forward gcloud org-policies set-policy disable-sa-keys.yaml While you are there, also look at iam.disableServiceAccountKeyUpload, and check IAM & Admin → Service Accounts → Keys for anything with that lovely 9999 expiry date. The Recommender insights (google.iam.serviceAccount.Insight) will tell you which keys and accounts have not been used in ages.
The demo: trying to mint a backdoor key
Attack vector 2 in the demo plays the attacker who already compromised the CI identity and wants persistence. The obvious move is to mint a long-lived key:
gcloud iam service-accounts keys create /tmp/backdoor-key.json \ --iam-account=ci-prod-deployer@${PROJECT_ID}.iam.gserviceaccount.com With the Org Policy enforced, GCP says no. No key, no persistence, no year-9999 souvenir.
Pillar 2: Workload Identity Federation (a.k.a. Passport Control)
"Okay, no JSON keys. Then how does my GitHub Actions workflow talk to Google Cloud?"
Workload Identity Federation (WIF). And the easiest way I have found to explain it is airport passport control:
- The CI runner gets a signed OIDC JWT from GitHub for that specific job. That is the passport.
- Google Cloud STS is customs. It checks the signature and the claims inside the passport against your rules.
- If everything checks out, STS hands back a visitor badge -- a short-lived token valid for roughly 15–60 minutes.
There is nothing stored in GitHub. Nothing to steal from a secrets store. Nothing that lives until 9999.
So everybody migrated to WIF and lived happily ever after, right?
The #1 OIDC mistake: who is on the VIP list?
Not quite. This is where Nx s1ngularity (August 2025) and Mini Shai-Hulud come back in.
A lot of teams moved to WIF and configured their provider to check exactly one thing:
assertion.repository == 'acme/app' Some checked only repository_owner, which is even worse.
The problem? A pull_request_target workflow, an unmerged feature branch, or a job on a rogue self-hosted runner still carries "repository": "acme/app" in its OIDC token. So your customs officer checks the passport, sees the right country, and waves through an untrusted PR to mint a production GCP token.
You moved away from static keys and then handed out VIP badges to anyone who can open a pull request.
The fix is a strict CEL attribute condition on the provider. Check the immutable numeric owner ID (it survives org renames), the exact repo, the protected branch, the event type, and -- because of SHA1HULUD -- the runner environment:
gcloud iam workload-identity-pools create "github-prod-pool" \ --project="${PROJECT_ID}" \ --location="global" \ --display-name="GitHub Actions Prod Pool" gcloud iam workload-identity-pools providers create-oidc "github-oidc-provider" \ --project="${PROJECT_ID}" \ --location="global" \ --workload-identity-pool="github-prod-pool" \ --display-name="GitHub Actions Hardened OIDC" \ --issuer-uri="https://token.actions.githubusercontent.com" \ --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref,attribute.workflow_ref=assertion.job_workflow_ref" \ --attribute-condition="assertion.repository_owner_id == '${GITHUB_ORG_ID}' && assertion.repository == '${GITHUB_REPO}' && assertion.ref == 'refs/heads/main' && assertion.event_name == 'push' && assertion.runner_environment == 'github-hosted'" You can tighten it further with assertion.job_workflow_ref to pin the exact workflow file allowed to deploy.
And you do not even need an intermediate service account anymore. Grant the role directly to the federated principal:
gcloud artifacts repositories add-iam-policy-binding "prod-containers" \ --location="${REGION}" \ --role="roles/artifactregistry.writer" \ --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-prod-pool/attribute.repository/${GITHUB_REPO}" A pattern I like a lot on top of this: split identities by environment. A read-only terraform plan identity that PRs are allowed to use, and a separate apply / deploy identity restricted to the production GitHub Environment with required human reviewers.
The hardened workflow
On the GitHub side, this is what the deploy workflow looks like. Notice three things: least-privilege permissions, id-token: write only on the job that actually needs it, and every action pinned to a full 40-character commit SHA (this alone would have neutralized tj-actions):
name: Build and Deploy to GKE on: push: branches: [ "main" ] # Global default: least privilege permissions: contents: read jobs: deploy: runs-on: ubuntu-latest environment: production permissions: contents: read id-token: write # ONLY on the job that exchanges OIDC tokens steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: npm ci --ignore-scripts - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: "projects/123456789/locations/global/workloadIdentityPools/github-prod-pool/providers/github-oidc-provider" That --ignore-scripts is not decoration, by the way. More on that in a second.
The demo: forging a PR token at the STS border
Attack vector 3 builds an OIDC token that looks exactly like what Mini Shai-Hulud would bring to the border -- right repository, right owner ID, but:
{ "iss": "https://token.actions.githubusercontent.com", "repository": "acme-corp/payments-api", "ref": "refs/pull/99/merge", "event_name": "pull_request_target", "runner_environment": "self-hosted" } It then sends it straight to https://sts.googleapis.com/v1/token asking for a cloud-platform access token. STS rejects the exchange. Customs did its job.
The repo also has a real GitHub workflow for this (.github/workflows/attack-sim-oidc-hijack.yml) that triggers on pull_request and tries to authenticate against the hardened provider. Open a PR, watch it fail at the GCP boundary. It is honestly one of the most satisfying red X's you will ever see in GitHub Actions.
The demo: the worm that never gets to run
Attack vector 1 is the ChainDrop / Shai-Hulud pattern in miniature. The demo package is about as small as it gets:
{ "name": "chaindrop-shai-hulud-simulator", "private": true, "scripts": { "preinstall": "node setup.mjs" } } And setup.mjs does what the real worms did first -- it scans process.env for anything that smells like a credential (safely, values are redacted in the simulation):
const sensitiveVars = Object.keys(process.env).filter((k) => /GOOGLE|GCP|AWS|TOKEN|SECRET|KEY|GITHUB|KUBE/i.test(k) ); Run a plain npm install and the hook fires before a single dependency is even fetched. Run npm install --ignore-scripts (or drop ignore-scripts=true into your .npmrc) and it simply never executes. One flag. That is the difference between "CI runner leaked its environment" and "nothing happened".
Pillar 3: Stop Sharing the Master Key on GKE
So the pipeline is locked down. What about inside the cluster?
The legacy setup on GKE is that every Pod on a node inherits the node's service account. And very often that is the Compute Engine default service account, ...-compute@developer.gserviceaccount.com, sometimes with roles/editor and the full cloud-platform scope.
That is one giant master key on a lanyard, shared by every Pod on the node. One SSRF in one Pod, one call to the metadata server, and the attacker owns the whole project.
Workload Identity Federation for GKE flips this. One Pod, one scoped identity. Each Kubernetes ServiceAccount gets its own tiny, least-privilege IAM badge, and the GKE Metadata Server hands out short-lived tokens for that identity only.
And just like on the CI side, you no longer need an intermediate Google service account. Bind IAM directly to the Kubernetes ServiceAccount:
kubectl create namespace payments-prod kubectl create serviceaccount payments-api-ksa --namespace payments-prod gcloud secrets add-iam-policy-binding "stripe-webhook-secret" \ --role="roles/secretmanager.secretAccessor" \ --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/payments-prod/sa/payments-api-ksa" Read that --member string once slowly. It says: exactly this ServiceAccount, in exactly this namespace, in exactly this cluster's identity pool, can read exactly this one secret. That is what least privilege looks like when you stop pretending.
The workload itself is hardened too. From the demo repo:
apiVersion: apps/v1 kind: Deployment metadata: name: payments-vault-api namespace: payments-prod spec: template: spec: serviceAccountName: payments-api-ksa securityContext: runAsNonRoot: true runAsUser: 65532 seccompProfile: type: RuntimeDefault containers: - name: api # Immutable digest, verified by Binary Authorization image: us-central1-docker.pkg.dev/.../payments-api@sha256:e882491c... securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] The namespace enforces the restricted Pod Security Standard, and the ServiceAccount sets automountServiceAccountToken: false. Non-root, read-only root filesystem, no capabilities, image by digest.
The demo app (app/main.py) is a tiny Python service that asks the GKE Metadata Server who it is and how long its token lives, and reports whether GOOGLE_APPLICATION_CREDENTIALS exists in its environment. On a properly configured cluster, it tells you its scoped principal, a short TTL, and "static_json_keys_present": false. Which is exactly the answer you want.
To verify on your own cluster, check Kubernetes Engine → Clusters → Security for Workload Identity: Enabled (<PROJECT_ID>.svc.id.goog), and make sure your node pools have the GKE Metadata Server enabled and are not running on the default Compute SA.
Pillar 4: No Provenance? No Entry to GKE
Here is the last gap. Even if your pipeline is perfect, what stops someone from pushing an image from their laptop -- with a stolen maintainer token, just like ChainDrop -- and deploying it?
Three things, stacked:
1. Immutable tags in Artifact Registry
Remember tj-actions? Mutable tags were the entire attack. Container tags have exactly the same problem: :v1.0.0 today might not be :v1.0.0 tomorrow.
gcloud artifacts repositories create prod-containers \ --repository-format=docker \ --location="${REGION}" \ --immutable-tags \ --description="Production container registry with immutable tags" Once a tag is pushed, it is pinned. Attack vector 4 in the demo tries to repoint an existing v1.0.0 release to a poisoned image, and Artifact Registry refuses. And in your manifests, deploy by digest (@sha256:...) anyway.
2. SLSA Level 3 provenance from Cloud Build
ChainDrop proved SLSA L2 is not enough when the attacker is upstream of the build. You want builds running on isolated, hermetic builders that you control -- not on a maintainer's laptop. Regional Cloud Build generates verifiable build provenance for you:
gcloud builds submit \ --region="${REGION}" \ --tag="${REGION}-docker.pkg.dev/${PROJECT_ID}/prod-containers/payments-api:v1.0.0" . gcloud artifacts docker images describe \ "${REGION}-docker.pkg.dev/${PROJECT_ID}/prod-containers/payments-api:v1.0.0" \ --show-provenance In the console, open the image digest in Artifact Registry and look at the Build Provenance tab: builder identity, source commit, the whole chain. The Vulnerabilities tab gives you CVE scanning for free while you are there.
3. Binary Authorization as the GKE admission gate
Provenance is useless if nothing checks it. Binary Authorization is the bouncer at the cluster door:
defaultAdmissionRule: evaluationMode: REQUIRE_ATTESTATION enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG Now try to sneak in a rogue Pod. The demo has one ready, appropriately named:
apiVersion: v1 kind: Pod metadata: name: chaindrop-rogue-miner namespace: payments-prod spec: serviceAccountName: payments-api-ksa containers: - name: rogue-container image: docker.io/library/alpine:latest command: ["sh", "-c", "echo 'Attempting unauthorized workload execution...' && sleep 3600"] kubectl apply -f k8s/rogue-pod-attack.yaml Error from server (Forbidden): admission webhook "imagepolicywebhook.image-policy.k8s.io" denied the request: Image docker.io/library/alpine:latest denied by Binary Authorization default admission rule: No valid attestation found for attestor projects/your-gcp-project-id/attestors/prod-ci-attestor No stamp, no entry. It does not matter whose token was used to push it.
The Cheat Sheet: Threat → Control
Here is the whole talk in one table:
| Threat / Incident | Root Vulnerability | CI/CD Fix | Google Cloud / GKE Fix |
|---|---|---|---|
tj-actions / reviewdog (CVE-2025-30066) | Mutable action tags + runner memory secret dump | Pin actions to 40-char commit SHAs | Kill JSON keys (iam.disableServiceAccountKeyCreation) + short-lived WIF tokens + VPC-SC |
Nx s1ngularity & loose OIDC trust | pull_request_target + WIF checking only repository | Never check out untrusted PR code in privileged workflows | Strict WIF CEL condition (repository_owner_id, ref, job_workflow_ref) |
| Shai-Hulud 2.0 & Mini Shai-Hulud | Rogue SHA1HULUD runners + pnpm cache / PR OIDC theft | Audit runners; permissions: contents: read | Require runner_environment == 'github-hosted' + ref == 'refs/heads/main' in CEL |
| ChainDrop & GhostAction | Compromised maintainers + preinstall droppers | --ignore-scripts; OIDC Trusted Publishing for npm | Immutable tags + Cloud Build SLSA L3 + Binary Authorization + VPC-SC |
| GKE Pod compromise / SSRF | Default Compute SA exposed via metadata server | Drop capabilities; network policies | Workload Identity Federation for GKE with direct per-KSA IAM bindings |
A Reality Check (Because Nothing Is Perfect)
Strict CEL conditions will break things. The first time someone tries a legitimate workflow_dispatch from a release branch, it will fail. That is the point, but you will need to model your real deployment paths in the condition (or in separate providers) instead of opening it back up to "just the repository".
SHA pinning is a maintenance tax. Forty-character hashes do not update themselves. Use Dependabot or Renovate to bump pinned SHAs, otherwise you trade a supply chain risk for an "everything is two years old" risk.
--ignore-scripts is not free. Some packages genuinely need install scripts (native modules, binary downloads). You will need an allowlist or a separate, isolated step for those. It is still a much better default than "run arbitrary code from 1,500 transitive dependencies with my cloud credentials in the environment".
Hardware keys are a people problem. YubiKeys get lost, left at home, or forgotten in a laptop bag. Plan for backup keys and a revocation process before you enforce signed commits on the main branch.
Short-lived tokens are short, not zero. A one-hour token dumped from runner memory is still a one-hour window. This is exactly why VPC Service Controls matter: a stolen token should not work from an attacker's workstation IP in the first place.
Binary Authorization needs a break-glass plan. When prod is on fire at 3 AM and your attestor pipeline is down, you need a documented, audited bypass. Otherwise somebody will "temporarily" turn enforcement off and forget about it.
Getting Started
Everything above is in the demo repo, and you can run it from Cloud Shell with one command:
git clone https://github.com/calganaygun/trust-no-pipeline-gcp-demo.git && \ cd trust-no-pipeline-gcp-demo && \ ./scripts/01-setup-gcp-guardrails.sh The bootstrap script detects your active project and sets up:
- The required APIs (
iam,sts,artifactregistry,cloudbuild,binaryauthorization,containerscanning) - A CI service account (
ci-prod-deployer) and theiam.disableServiceAccountKeyCreationpolicy - A Workload Identity Pool (
github-prod-pool) with a CEL-locked OIDC provider requiringrepository_owner_id,refs/heads/main, andgithub-hostedrunners - An Artifact Registry repo (
prod-containers) with immutable tags, bound directly to the federated principal - A sample
payments-api:v1.0.0build through regional Cloud Build, with provenance
Then launch the attack simulator and watch all five attacks fail:
./scripts/02-run-attack-vectors.sh | # | Attack | Blocked by |
|---|---|---|
| 1 | ChainDrop / Shai-Hulud preinstall worm | npm install --ignore-scripts |
| 2 | Minting a backdoor Service Account JSON key | iam.disableServiceAccountKeyCreation |
| 3 | PR / rogue runner OIDC token exchange | WIF CEL attribute condition at GCP STS |
| 4 | tj-actions-style tag overwrite of :v1.0.0 | Artifact Registry immutable tags |
| 5 | Unattested rogue Pod on GKE | Binary Authorization admission |
If you want to click through the slides too, they are a single index.html -- run python3 -m http.server 8080 in Cloud Shell and use Web Preview, or just grab them at calgan.dev/krk-iam.
The Bottom Line
If you take nothing else from this post, take the four stickers from the last slide back to your desk on Monday morning:
- Sign and pin. Hardware-backed GPG commits, and 40-character SHAs for every action.
- Burn your JSON keys.
iam.disableServiceAccountKeyCreation, today. - Check the branch and the runner. WIF CEL:
main+github-hosted, not just the repo name. - Stamp your artifacts. SLSA L3 provenance in, Binary Authorization at the door.
Attackers have figured out that your pipeline is the softest, most privileged thing you own. It has production credentials, it runs untrusted code all day long, and nobody audits it like they audit prod.
The era of "it is our CI, it is fine" is over. The era of treating the delivery truck with the same suspicion as anything else coming through the gate is here.
Trust no pipeline. Not even your own.
References
All the sourcing for this post:
- Trust No Pipeline -- slides -- the interactive deck from the talk
- trust-no-pipeline-gcp-demo -- the live attack/defense demo repo used throughout this post
- CISA alert on
tj-actions/changed-files(CVE-2025-30066) -- the official advisory - NVD: CVE-2025-30066 -- the CVE record
- Nx
s1ngularityGitHub security advisory -- thepull_request_targetcompromise ofnx - Wiz Research: Shai-Hulud supply chain worm -- breakdown of the npm worm waves
- Panther Labs: Shai-Hulud analysis -- detection-focused write-up
- Aikido: credential-stealing npm worm -- package-level analysis
- 2026 npm worm defense analysis -- Mini Shai-Hulud and defenses
- Elastic Security Labs: Shai-Hulud & ChainDrop -- ChainDrop deep dive
- Palo Alto Unit 42: ChainDrop npm worm -- Bun dropper and Ethereum C2 analysis
- StepSecurity: ChainDrop incident breakdown -- CI/CD-centric timeline
- GitGuardian: GhostAction, 3,000+ secrets stolen -- rogue workflow secret exfiltration
- Google Cloud: Workload Identity Federation with deployment pipelines -- the official WIF + GitHub Actions guide
- Google Cloud: Workload Identity Federation for GKE -- per-KSA identities on GKE
- Google Cloud: Binary Authorization -- admission-time attestation enforcement

