Dev.to Security 🔐 Cybersecurity 👁 0 📖 18 min read

Trust No Pipeline: Why Attackers Stopped Hacking Your Cluster and Started Riding Your CI/CD (And How to Stop Them on Google Cloud)

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

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:

  1. SpotBugs PR trap. A maintainer PAT was added to spotbugs/sonar-findbugs, then stolen through a malicious pull_request_target PR.
  2. Rogue invite and leak. On March 11, that PAT invited a rogue user (jurkaofavak) to spotbugs/spotbugs. Two minutes later a workflow leaked the Reviewdog maintainer's PAT, AES + RSA encrypted, so nobody would notice it in the logs.
  3. reviewdog/action-setup@v1 poisoned. The leaked PAT overwrote the mutable v1 tag.
  4. Pivot to tj-actions. tj-actions/eslint-changed-files used reviewdog/action-setup@v1 as a transitive dependency, which exposed the tj-actions privileges.
  5. The actual target: coinbase/agentkit. Five days after Coinbase added tj-actions/changed-files, the attacker forked the relevant repos to stage the attack.
  6. All @v1–@v45 repointed. 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 <[email protected]>" -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:

  1. The CI runner gets a signed OIDC JWT from GitHub for that specific job. That is the passport.
  2. Google Cloud STS is customs. It checks the signature and the claims inside the passport against your rules.
  3. 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, [email protected], 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:

  1. The required APIs (iam, sts, artifactregistry, cloudbuild, binaryauthorization, containerscanning)
  2. A CI service account (ci-prod-deployer) and the iam.disableServiceAccountKeyCreation policy
  3. A Workload Identity Pool (github-prod-pool) with a CEL-locked OIDC provider requiring repository_owner_id, refs/heads/main, and github-hosted runners
  4. An Artifact Registry repo (prod-containers) with immutable tags, bound directly to the federated principal
  5. A sample payments-api:v1.0.0 build 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:

  1. Sign and pin. Hardware-backed GPG commits, and 40-character SHAs for every action.
  2. Burn your JSON keys. iam.disableServiceAccountKeyCreation, today.
  3. Check the branch and the runner. WIF CEL: main + github-hosted, not just the repo name.
  4. 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:

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.