<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Çalgan Aygün</title>
    <description>The latest articles on DEV Community by Çalgan Aygün (@calganaygun).</description>
    <link>https://dev.to/calganaygun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F389991%2F259e54a7-492e-4848-b98e-950b56893521.jpg</url>
      <title>DEV Community: Çalgan Aygün</title>
      <link>https://dev.to/calganaygun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/calganaygun"/>
    <language>en</language>
    <item>
      <title>Trust No Pipeline: Why Attackers Stopped Hacking Your Cluster and Started Riding Your CI/CD (And How to Stop Them on Google Cloud)</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Sat, 07 Nov 2026 10:00:52 +0000</pubDate>
      <link>https://dev.to/gde/trust-no-pipeline-why-attackers-stopped-hacking-your-cluster-and-started-riding-your-cicd-and-2m74</link>
      <guid>https://dev.to/gde/trust-no-pipeline-why-attackers-stopped-hacking-your-cluster-and-started-riding-your-cicd-and-2m74</guid>
      <description>&lt;p&gt;&lt;strong&gt;Sourced from:&lt;/strong&gt; my talk &lt;em&gt;"Trust No Pipeline: Securing Software Delivery &amp;amp; Workload Identity in the Cloud"&lt;/em&gt; (&lt;a href="https://calgan.dev/krk-iam" rel="noopener noreferrer"&gt;slides&lt;/a&gt;), the companion &lt;a href="https://github.com/calganaygun/trust-no-pipeline-gcp-demo" rel="noopener noreferrer"&gt;live demo repo&lt;/a&gt;, and the public 2025–2026 incident write-ups linked at the bottom.&lt;/p&gt;




&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;And attackers looked at all of that and said: "Cool. We will not touch it."&lt;/p&gt;

&lt;p&gt;Why would they? Why fight the fortress when you can hijack the delivery truck?&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;your&lt;/em&gt; truck. It carries &lt;code&gt;roles/owner&lt;/code&gt;. It runs &lt;code&gt;preinstall&lt;/code&gt; hooks from packages you have never read. It pulls GitHub Actions by mutable &lt;code&gt;@v1&lt;/code&gt; tags. And somewhere in its glovebox there is probably a Service Account JSON key that expires in the year 9999.&lt;/p&gt;

&lt;p&gt;That is the whole plot twist of modern supply chain attacks. Nobody is breaking in. They are riding in.&lt;/p&gt;

&lt;p&gt;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: &lt;strong&gt;we trust our pipelines way more than we should.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So let us stop doing that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bait and Switch That Hit 23,000 Repos Overnight
&lt;/h2&gt;

&lt;p&gt;Let me start with the one that made everyone pay attention.&lt;/p&gt;

&lt;p&gt;March 2025. &lt;strong&gt;CVE-2025-30066&lt;/strong&gt;. &lt;code&gt;tj-actions/changed-files&lt;/code&gt;, a GitHub Action used by more than 23,000 repositories.&lt;/p&gt;

&lt;p&gt;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 &lt;strong&gt;every single version tag&lt;/strong&gt; of the action -- &lt;code&gt;@v1&lt;/code&gt; all the way through &lt;code&gt;@v45&lt;/code&gt; -- to a malicious commit.&lt;/p&gt;

&lt;p&gt;That commit ran a little Python script called &lt;code&gt;memdump.py&lt;/code&gt;. It read &lt;code&gt;/proc/$pid/mem&lt;/code&gt; of the &lt;code&gt;Runner.Worker&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;If your workflow said &lt;code&gt;uses: tj-actions/changed-files@v45&lt;/code&gt;, you were not running the code you reviewed. You were running whatever the tag pointed to &lt;em&gt;that morning&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How they actually got there: six steps from SpotBugs to Coinbase
&lt;/h3&gt;

&lt;p&gt;What makes this one fascinating is that it was not a single hack. It was a cascade:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SpotBugs PR trap.&lt;/strong&gt; A maintainer PAT was added to &lt;code&gt;spotbugs/sonar-findbugs&lt;/code&gt;, then stolen through a malicious &lt;code&gt;pull_request_target&lt;/code&gt; PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rogue invite and leak.&lt;/strong&gt; On March 11, that PAT invited a rogue user (&lt;code&gt;jurkaofavak&lt;/code&gt;) to &lt;code&gt;spotbugs/spotbugs&lt;/code&gt;. Two minutes later a workflow leaked the Reviewdog maintainer's PAT, AES + RSA encrypted, so nobody would notice it in the logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reviewdog/action-setup@v1&lt;/code&gt; poisoned.&lt;/strong&gt; The leaked PAT overwrote the mutable &lt;code&gt;v1&lt;/code&gt; tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pivot to &lt;code&gt;tj-actions&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;tj-actions/eslint-changed-files&lt;/code&gt; used &lt;code&gt;reviewdog/action-setup@v1&lt;/code&gt; as a transitive dependency, which exposed the &lt;code&gt;tj-actions&lt;/code&gt; privileges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The actual target: &lt;code&gt;coinbase/agentkit&lt;/code&gt;.&lt;/strong&gt; Five days after Coinbase added &lt;code&gt;tj-actions/changed-files&lt;/code&gt;, the attacker forked the relevant repos to stage the attack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All &lt;code&gt;@v1&lt;/code&gt;–&lt;code&gt;@v45&lt;/code&gt; repointed.&lt;/strong&gt; And 23,000+ repositories came along for the ride.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then the Worms Showed Up
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;tj-actions&lt;/code&gt; was a targeted heist, 2025 and 2026 gave us something worse: &lt;strong&gt;self-propagating supply chain worms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shai-Hulud 2.0&lt;/strong&gt; (Sept–Nov 2025) ran inside &lt;code&gt;preinstall&lt;/code&gt; hooks, fired up an embedded TruffleHog scanner to harvest cloud credentials (&lt;code&gt;GOOGLE_CREDENTIALS&lt;/code&gt;, AWS keys, Vault tokens), then automatically republished trojanized versions of &lt;em&gt;every package the victim owned&lt;/em&gt;. As a bonus, it registered persistent rogue self-hosted GitHub Actions runners named &lt;code&gt;SHA1HULUD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mini Shai-Hulud&lt;/strong&gt; (May 2026) was the streamlined sequel. Triggered via &lt;code&gt;pull_request_target&lt;/code&gt; ("pwn requests"), it poisoned &lt;code&gt;pnpm&lt;/code&gt; caches and dumped &lt;strong&gt;OIDC tokens directly from CI runner memory&lt;/strong&gt;, hitting ecosystems including TanStack, Mistral AI, and UiPath.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChainDrop&lt;/strong&gt; (August 2026) compromised core JavaScript maintainer accounts -- most notably &lt;code&gt;keyv&lt;/code&gt; (think &lt;code&gt;flat-cache&lt;/code&gt;, &lt;code&gt;cacheable-request&lt;/code&gt;) -- and spread to &lt;strong&gt;400+ npm packages and 1,300+ versions&lt;/strong&gt;. It used a &lt;code&gt;preinstall&lt;/code&gt; hook (&lt;code&gt;setup.mjs&lt;/code&gt;) to drop an obfuscated Bun payload on Linux, macOS, and Windows, and routed its command-and-control through &lt;strong&gt;Ethereum smart contracts&lt;/strong&gt; so corporate DNS blocklists had nothing to block. It then committed malicious updates under the dummy author &lt;code&gt;claude&lt;/code&gt; with the very innocent message &lt;code&gt;chore: update config&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And the most uncomfortable lesson from ChainDrop? &lt;strong&gt;SLSA Level 2 did not save anyone.&lt;/strong&gt; The maintainer's credentials were compromised &lt;em&gt;before&lt;/em&gt; the build, so the provenance looked perfectly valid. The build system faithfully attested that it built the malware.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four (Okay, Five) Pillars
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 0: A Stolen Token Should Not Equal a Valid Commit
&lt;/h2&gt;

&lt;p&gt;Here is something that does not get talked about enough. Anyone can create a commit with someone else's name and email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;--author&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Trusted Maintainer &amp;lt;maintainer@example.com&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: update config"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git will happily accept it. GitHub will happily display it. That is exactly how ChainDrop's &lt;code&gt;claude&lt;/code&gt; commits blended in.&lt;/p&gt;

&lt;p&gt;What we actually care about is &lt;strong&gt;authenticity&lt;/strong&gt; (who really made this commit?) and &lt;strong&gt;integrity&lt;/strong&gt; (has it been tampered with since?). Commit signing gives you both. If a signed commit is modified, the signature breaks immediately.&lt;/p&gt;

&lt;p&gt;But here is where it gets good. If you enforce that only signatures from maintainers' &lt;strong&gt;physical YubiKey / FIDO keys&lt;/strong&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Lock Git to the maintainer's YubiKey GPG key&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.signingkey 0x9F8B1234
git config &lt;span class="nt"&gt;--global&lt;/span&gt; commit.gpgsign &lt;span class="nb"&gt;true
&lt;/span&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; tag.gpgSign &lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# 2. Every commit now requires a physical touch on the key&lt;/span&gt;
git commit &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: release v1.0.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then enforce it on both ends: turn on &lt;strong&gt;Settings → Rules → Require signed commits&lt;/strong&gt; in GitHub, and verify against a known keyring in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify Maintainer Hardware GPG&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;gpg --import .github/maintainer-yubikeys.asc&lt;/span&gt;
    &lt;span class="s"&gt;git verify-commit HEAD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stolen PAT without the YubiKey? Rejected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 1: Kill Static Secrets (Vampire Keys vs. Melting Ice Cubes)
&lt;/h2&gt;

&lt;p&gt;Why did Shai-Hulud and ChainDrop do so much damage? Because teams still export GCP Service Account JSON keys.&lt;/p&gt;

&lt;p&gt;A JSON key is a vampire. It lives until &lt;strong&gt;December 31, 9999&lt;/strong&gt; 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 &lt;code&gt;preinstall&lt;/code&gt; hook with a TruffleHog binary to come along and grab it.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;On Google Cloud, you can kill the vampires for your entire project or organization with one Organization Policy. Five lines of YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# disable-sa-keys.yaml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;projects/${PROJECT_ID}/policies/iam.disableServiceAccountKeyCreation&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;enforce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Audit what is already out there&lt;/span&gt;
gcloud iam service-accounts keys list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ci-deployer@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iam.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--managed-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"table(name.basename(), validAfterTime, validBeforeTime, keyAlgorithm)"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Block key creation going forward&lt;/span&gt;
gcloud org-policies set-policy disable-sa-keys.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While you are there, also look at &lt;code&gt;iam.disableServiceAccountKeyUpload&lt;/code&gt;, and check &lt;strong&gt;IAM &amp;amp; Admin → Service Accounts → Keys&lt;/strong&gt; for anything with that lovely &lt;code&gt;9999&lt;/code&gt; expiry date. The Recommender insights (&lt;code&gt;google.iam.serviceAccount.Insight&lt;/code&gt;) will tell you which keys and accounts have not been used in ages.&lt;/p&gt;

&lt;h3&gt;
  
  
  The demo: trying to mint a backdoor key
&lt;/h3&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts keys create /tmp/backdoor-key.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--iam-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ci-prod-deployer@&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the Org Policy enforced, GCP says no. No key, no persistence, no year-9999 souvenir.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 2: Workload Identity Federation (a.k.a. Passport Control)
&lt;/h2&gt;

&lt;p&gt;"Okay, no JSON keys. Then how does my GitHub Actions workflow talk to Google Cloud?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload Identity Federation (WIF).&lt;/strong&gt; And the easiest way I have found to explain it is airport passport control:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The CI runner&lt;/strong&gt; gets a signed OIDC JWT from GitHub for that specific job. That is the passport.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Cloud STS&lt;/strong&gt; is customs. It checks the signature and the claims inside the passport against your rules.&lt;/li&gt;
&lt;li&gt;If everything checks out, STS hands back a &lt;strong&gt;visitor badge&lt;/strong&gt; -- a short-lived token valid for roughly 15–60 minutes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is nothing stored in GitHub. Nothing to steal from a secrets store. Nothing that lives until 9999.&lt;/p&gt;

&lt;p&gt;So everybody migrated to WIF and lived happily ever after, right?&lt;/p&gt;

&lt;h3&gt;
  
  
  The #1 OIDC mistake: who is on the VIP list?
&lt;/h3&gt;

&lt;p&gt;Not quite. This is where Nx &lt;code&gt;s1ngularity&lt;/code&gt; (August 2025) and Mini Shai-Hulud come back in.&lt;/p&gt;

&lt;p&gt;A lot of teams moved to WIF and configured their provider to check exactly one thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assertion.repository == 'acme/app'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some checked only &lt;code&gt;repository_owner&lt;/code&gt;, which is even worse.&lt;/p&gt;

&lt;p&gt;The problem? A &lt;code&gt;pull_request_target&lt;/code&gt; workflow, an unmerged feature branch, or a job on a rogue self-hosted runner &lt;strong&gt;still carries &lt;code&gt;"repository": "acme/app"&lt;/code&gt; in its OIDC token.&lt;/strong&gt; So your customs officer checks the passport, sees the right country, and waves through an untrusted PR to mint a production GCP token.&lt;/p&gt;

&lt;p&gt;You moved away from static keys and then handed out VIP badges to anyone who can open a pull request.&lt;/p&gt;

&lt;p&gt;The fix is a strict &lt;strong&gt;CEL attribute condition&lt;/strong&gt; 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 &lt;code&gt;SHA1HULUD&lt;/code&gt; -- the runner environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam workload-identity-pools create &lt;span class="s2"&gt;"github-prod-pool"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"GitHub Actions Prod Pool"&lt;/span&gt;

gcloud iam workload-identity-pools providers create-oidc &lt;span class="s2"&gt;"github-oidc-provider"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--workload-identity-pool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"github-prod-pool"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"GitHub Actions Hardened OIDC"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--issuer-uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute-mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref,attribute.workflow_ref=assertion.job_workflow_ref"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute-condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"assertion.repository_owner_id == '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_ORG_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' &amp;amp;&amp;amp; assertion.repository == '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REPO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' &amp;amp;&amp;amp; assertion.ref == 'refs/heads/main' &amp;amp;&amp;amp; assertion.event_name == 'push' &amp;amp;&amp;amp; assertion.runner_environment == 'github-hosted'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can tighten it further with &lt;code&gt;assertion.job_workflow_ref&lt;/code&gt; to pin the exact workflow file allowed to deploy.&lt;/p&gt;

&lt;p&gt;And you do not even need an intermediate service account anymore. Grant the role &lt;strong&gt;directly to the federated principal&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud artifacts repositories add-iam-policy-binding &lt;span class="s2"&gt;"prod-containers"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/artifactregistry.writer"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"principalSet://iam.googleapis.com/projects/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NUMBER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/locations/global/workloadIdentityPools/github-prod-pool/attribute.repository/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REPO&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pattern I like a lot on top of this: &lt;strong&gt;split identities by environment.&lt;/strong&gt; A read-only &lt;code&gt;terraform plan&lt;/code&gt; identity that PRs are allowed to use, and a separate &lt;code&gt;apply&lt;/code&gt; / deploy identity restricted to the &lt;code&gt;production&lt;/code&gt; GitHub Environment with required human reviewers.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hardened workflow
&lt;/h3&gt;

&lt;p&gt;On the GitHub side, this is what the deploy workflow looks like. Notice three things: least-privilege &lt;code&gt;permissions&lt;/code&gt;, &lt;code&gt;id-token: write&lt;/code&gt; only on the job that actually needs it, and every action &lt;strong&gt;pinned to a full 40-character commit SHA&lt;/strong&gt; (this alone would have neutralized &lt;code&gt;tj-actions&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Deploy to GKE&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Global default: least privilege&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# ONLY on the job that exchanges OIDC tokens&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683&lt;/span&gt; &lt;span class="c1"&gt;# v4.2.2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci --ignore-scripts&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f&lt;/span&gt; &lt;span class="c1"&gt;# v2.1.7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;workload_identity_provider&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects/123456789/locations/global/workloadIdentityPools/github-prod-pool/providers/github-oidc-provider"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;--ignore-scripts&lt;/code&gt; is not decoration, by the way. More on that in a second.&lt;/p&gt;

&lt;h3&gt;
  
  
  The demo: forging a PR token at the STS border
&lt;/h3&gt;

&lt;p&gt;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:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://token.actions.githubusercontent.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme-corp/payments-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ref"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refs/pull/99/merge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pull_request_target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"runner_environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"self-hosted"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It then sends it straight to &lt;code&gt;https://sts.googleapis.com/v1/token&lt;/code&gt; asking for a &lt;code&gt;cloud-platform&lt;/code&gt; access token. STS rejects the exchange. Customs did its job.&lt;/p&gt;

&lt;p&gt;The repo also has a real GitHub workflow for this (&lt;code&gt;.github/workflows/attack-sim-oidc-hijack.yml&lt;/code&gt;) that triggers on &lt;code&gt;pull_request&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  The demo: the worm that never gets to run
&lt;/h3&gt;

&lt;p&gt;Attack vector 1 is the ChainDrop / Shai-Hulud pattern in miniature. The demo package is about as small as it gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chaindrop-shai-hulud-simulator"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"preinstall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node setup.mjs"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;setup.mjs&lt;/code&gt; does what the real worms did first -- it scans &lt;code&gt;process.env&lt;/code&gt; for anything that smells like a credential (safely, values are redacted in the simulation):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sensitiveVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="sr"&gt;/GOOGLE|GCP|AWS|TOKEN|SECRET|KEY|GITHUB|KUBE/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run a plain &lt;code&gt;npm install&lt;/code&gt; and the hook fires before a single dependency is even fetched. Run &lt;code&gt;npm install --ignore-scripts&lt;/code&gt; (or drop &lt;code&gt;ignore-scripts=true&lt;/code&gt; into your &lt;code&gt;.npmrc&lt;/code&gt;) and it simply never executes. One flag. That is the difference between "CI runner leaked its environment" and "nothing happened".&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 3: Stop Sharing the Master Key on GKE
&lt;/h2&gt;

&lt;p&gt;So the pipeline is locked down. What about &lt;em&gt;inside&lt;/em&gt; the cluster?&lt;/p&gt;

&lt;p&gt;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, &lt;code&gt;...-compute@developer.gserviceaccount.com&lt;/code&gt;, sometimes with &lt;code&gt;roles/editor&lt;/code&gt; and the full &lt;code&gt;cloud-platform&lt;/code&gt; scope.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workload Identity Federation for GKE&lt;/strong&gt; 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 &lt;em&gt;that&lt;/em&gt; identity only.&lt;/p&gt;

&lt;p&gt;And just like on the CI side, you no longer need an intermediate Google service account. Bind IAM &lt;strong&gt;directly to the Kubernetes ServiceAccount&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace payments-prod
kubectl create serviceaccount payments-api-ksa &lt;span class="nt"&gt;--namespace&lt;/span&gt; payments-prod

gcloud secrets add-iam-policy-binding &lt;span class="s2"&gt;"stripe-webhook-secret"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/secretmanager.secretAccessor"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"principal://iam.googleapis.com/projects/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NUMBER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/locations/global/workloadIdentityPools/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.svc.id.goog/subject/ns/payments-prod/sa/payments-api-ksa"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that &lt;code&gt;--member&lt;/code&gt; string once slowly. It says: &lt;em&gt;exactly&lt;/em&gt; this ServiceAccount, in &lt;em&gt;exactly&lt;/em&gt; this namespace, in &lt;em&gt;exactly&lt;/em&gt; this cluster's identity pool, can read &lt;em&gt;exactly&lt;/em&gt; this one secret. That is what least privilege looks like when you stop pretending.&lt;/p&gt;

&lt;p&gt;The workload itself is hardened too. From the demo repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-vault-api&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-prod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;serviceAccountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-api-ksa&lt;/span&gt;
      &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;runAsUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;65532&lt;/span&gt;
        &lt;span class="na"&gt;seccompProfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RuntimeDefault&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
          &lt;span class="c1"&gt;# Immutable digest, verified by Binary Authorization&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-central1-docker.pkg.dev/.../payments-api@sha256:e882491c...&lt;/span&gt;
          &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="na"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
            &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALL"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The namespace enforces the &lt;code&gt;restricted&lt;/code&gt; Pod Security Standard, and the ServiceAccount sets &lt;code&gt;automountServiceAccountToken: false&lt;/code&gt;. Non-root, read-only root filesystem, no capabilities, image by digest.&lt;/p&gt;

&lt;p&gt;The demo app (&lt;code&gt;app/main.py&lt;/code&gt;) is a tiny Python service that asks the GKE Metadata Server who it is and how long its token lives, and reports whether &lt;code&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/code&gt; exists in its environment. On a properly configured cluster, it tells you its scoped principal, a short TTL, and &lt;code&gt;"static_json_keys_present": false&lt;/code&gt;. Which is exactly the answer you want.&lt;/p&gt;

&lt;p&gt;To verify on your own cluster, check &lt;strong&gt;Kubernetes Engine → Clusters → Security&lt;/strong&gt; for &lt;code&gt;Workload Identity: Enabled (&amp;lt;PROJECT_ID&amp;gt;.svc.id.goog)&lt;/code&gt;, and make sure your node pools have the GKE Metadata Server enabled and are not running on the default Compute SA.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pillar 4: No Provenance? No Entry to GKE
&lt;/h2&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;Three things, stacked:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Immutable tags in Artifact Registry
&lt;/h3&gt;

&lt;p&gt;Remember &lt;code&gt;tj-actions&lt;/code&gt;? Mutable tags were the entire attack. Container tags have exactly the same problem: &lt;code&gt;:v1.0.0&lt;/code&gt; today might not be &lt;code&gt;:v1.0.0&lt;/code&gt; tomorrow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud artifacts repositories create prod-containers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--immutable-tags&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Production container registry with immutable tags"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once a tag is pushed, it is pinned. Attack vector 4 in the demo tries to repoint an existing &lt;code&gt;v1.0.0&lt;/code&gt; release to a poisoned image, and Artifact Registry refuses. And in your manifests, deploy by digest (&lt;code&gt;@sha256:...&lt;/code&gt;) anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SLSA Level 3 provenance from Cloud Build
&lt;/h3&gt;

&lt;p&gt;ChainDrop proved SLSA L2 is not enough when the attacker is upstream of the build. You want builds running on &lt;strong&gt;isolated, hermetic builders&lt;/strong&gt; that you control -- not on a maintainer's laptop. Regional Cloud Build generates verifiable build provenance for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud builds submit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-docker.pkg.dev/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/prod-containers/payments-api:v1.0.0"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

gcloud artifacts docker images describe &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-docker.pkg.dev/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/prod-containers/payments-api:v1.0.0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--show-provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the console, open the image digest in Artifact Registry and look at the &lt;strong&gt;Build Provenance&lt;/strong&gt; tab: builder identity, source commit, the whole chain. The &lt;strong&gt;Vulnerabilities&lt;/strong&gt; tab gives you CVE scanning for free while you are there.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Binary Authorization as the GKE admission gate
&lt;/h3&gt;

&lt;p&gt;Provenance is useless if nothing checks it. Binary Authorization is the bouncer at the cluster door:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;defaultAdmissionRule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;evaluationMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRE_ATTESTATION&lt;/span&gt;
  &lt;span class="na"&gt;enforcementMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENFORCED_BLOCK_AND_AUDIT_LOG&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now try to sneak in a rogue Pod. The demo has one ready, appropriately named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaindrop-rogue-miner&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-prod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;serviceAccountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;payments-api-ksa&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rogue-container&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/library/alpine:latest&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;echo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'Attempting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;unauthorized&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;workload&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;execution...'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;3600"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/rogue-pod-attack.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No stamp, no entry. It does not matter whose token was used to push it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cheat Sheet: Threat → Control
&lt;/h2&gt;

&lt;p&gt;Here is the whole talk in one table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat / Incident&lt;/th&gt;
&lt;th&gt;Root Vulnerability&lt;/th&gt;
&lt;th&gt;CI/CD Fix&lt;/th&gt;
&lt;th&gt;Google Cloud / GKE Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;tj-actions&lt;/code&gt; / &lt;code&gt;reviewdog&lt;/code&gt;&lt;/strong&gt; (CVE-2025-30066)&lt;/td&gt;
&lt;td&gt;Mutable action tags + runner memory secret dump&lt;/td&gt;
&lt;td&gt;Pin actions to 40-char commit SHAs&lt;/td&gt;
&lt;td&gt;Kill JSON keys (&lt;code&gt;iam.disableServiceAccountKeyCreation&lt;/code&gt;) + short-lived WIF tokens + VPC-SC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Nx &lt;code&gt;s1ngularity&lt;/code&gt;&lt;/strong&gt; &amp;amp; loose OIDC trust&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pull_request_target&lt;/code&gt; + WIF checking only &lt;code&gt;repository&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Never check out untrusted PR code in privileged workflows&lt;/td&gt;
&lt;td&gt;Strict WIF &lt;strong&gt;CEL condition&lt;/strong&gt; (&lt;code&gt;repository_owner_id&lt;/code&gt;, &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;job_workflow_ref&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Shai-Hulud 2.0&lt;/strong&gt; &amp;amp; &lt;strong&gt;Mini Shai-Hulud&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Rogue &lt;code&gt;SHA1HULUD&lt;/code&gt; runners + &lt;code&gt;pnpm&lt;/code&gt; cache / PR OIDC theft&lt;/td&gt;
&lt;td&gt;Audit runners; &lt;code&gt;permissions: contents: read&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Require &lt;code&gt;runner_environment == 'github-hosted'&lt;/code&gt; + &lt;code&gt;ref == 'refs/heads/main'&lt;/code&gt; in CEL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;ChainDrop&lt;/strong&gt; &amp;amp; &lt;strong&gt;GhostAction&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Compromised maintainers + &lt;code&gt;preinstall&lt;/code&gt; droppers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--ignore-scripts&lt;/code&gt;; OIDC Trusted Publishing for npm&lt;/td&gt;
&lt;td&gt;Immutable tags + Cloud Build SLSA L3 + Binary Authorization + VPC-SC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GKE Pod compromise / SSRF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Default Compute SA exposed via metadata server&lt;/td&gt;
&lt;td&gt;Drop capabilities; network policies&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Workload Identity Federation for GKE&lt;/strong&gt; with direct per-KSA IAM bindings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  A Reality Check (Because Nothing Is Perfect)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strict CEL conditions will break things.&lt;/strong&gt; The first time someone tries a legitimate &lt;code&gt;workflow_dispatch&lt;/code&gt; 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".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SHA pinning is a maintenance tax.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--ignore-scripts&lt;/code&gt; is not free.&lt;/strong&gt; 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".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware keys are a people problem.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short-lived tokens are short, not zero.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Binary Authorization needs a break-glass plan.&lt;/strong&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Everything above is in the demo repo, and you can run it from Cloud Shell with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/calganaygun/trust-no-pipeline-gcp-demo.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;trust-no-pipeline-gcp-demo &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
./scripts/01-setup-gcp-guardrails.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bootstrap script detects your active project and sets up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The required APIs (&lt;code&gt;iam&lt;/code&gt;, &lt;code&gt;sts&lt;/code&gt;, &lt;code&gt;artifactregistry&lt;/code&gt;, &lt;code&gt;cloudbuild&lt;/code&gt;, &lt;code&gt;binaryauthorization&lt;/code&gt;, &lt;code&gt;containerscanning&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A CI service account (&lt;code&gt;ci-prod-deployer&lt;/code&gt;) and the &lt;code&gt;iam.disableServiceAccountKeyCreation&lt;/code&gt; policy&lt;/li&gt;
&lt;li&gt;A Workload Identity Pool (&lt;code&gt;github-prod-pool&lt;/code&gt;) with a CEL-locked OIDC provider requiring &lt;code&gt;repository_owner_id&lt;/code&gt;, &lt;code&gt;refs/heads/main&lt;/code&gt;, and &lt;code&gt;github-hosted&lt;/code&gt; runners&lt;/li&gt;
&lt;li&gt;An Artifact Registry repo (&lt;code&gt;prod-containers&lt;/code&gt;) with immutable tags, bound directly to the federated principal&lt;/li&gt;
&lt;li&gt;A sample &lt;code&gt;payments-api:v1.0.0&lt;/code&gt; build through regional Cloud Build, with provenance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then launch the attack simulator and watch all five attacks fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/02-run-attack-vectors.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Attack&lt;/th&gt;
&lt;th&gt;Blocked by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;ChainDrop / Shai-Hulud &lt;code&gt;preinstall&lt;/code&gt; worm&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install --ignore-scripts&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Minting a backdoor Service Account JSON key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;iam.disableServiceAccountKeyCreation&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;PR / rogue runner OIDC token exchange&lt;/td&gt;
&lt;td&gt;WIF CEL attribute condition at GCP STS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tj-actions&lt;/code&gt;-style tag overwrite of &lt;code&gt;:v1.0.0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Artifact Registry immutable tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Unattested rogue Pod on GKE&lt;/td&gt;
&lt;td&gt;Binary Authorization admission&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want to click through the slides too, they are a single &lt;code&gt;index.html&lt;/code&gt; -- run &lt;code&gt;python3 -m http.server 8080&lt;/code&gt; in Cloud Shell and use &lt;strong&gt;Web Preview&lt;/strong&gt;, or just grab them at &lt;a href="https://calgan.dev/krk-iam" rel="noopener noreferrer"&gt;calgan.dev/krk-iam&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;If you take nothing else from this post, take the four stickers from the last slide back to your desk on Monday morning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sign and pin.&lt;/strong&gt; Hardware-backed GPG commits, and 40-character SHAs for every action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burn your JSON keys.&lt;/strong&gt; &lt;code&gt;iam.disableServiceAccountKeyCreation&lt;/code&gt;, today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the branch and the runner.&lt;/strong&gt; WIF CEL: &lt;code&gt;main&lt;/code&gt; + &lt;code&gt;github-hosted&lt;/code&gt;, not just the repo name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stamp your artifacts.&lt;/strong&gt; SLSA L3 provenance in, Binary Authorization at the door.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Trust no pipeline. Not even your own.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;All the sourcing for this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://calgan.dev/krk-iam" rel="noopener noreferrer"&gt;Trust No Pipeline -- slides&lt;/a&gt; -- the interactive deck from the talk&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/calganaygun/trust-no-pipeline-gcp-demo" rel="noopener noreferrer"&gt;trust-no-pipeline-gcp-demo&lt;/a&gt; -- the live attack/defense demo repo used throughout this post&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-github-action-cve-2025-30066" rel="noopener noreferrer"&gt;CISA alert on &lt;code&gt;tj-actions/changed-files&lt;/code&gt; (CVE-2025-30066)&lt;/a&gt; -- the official advisory&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2025-30066" rel="noopener noreferrer"&gt;NVD: CVE-2025-30066&lt;/a&gt; -- the CVE record&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/nrwl/nx/security/advisories/GHSA-cxm3-wv7p-598c" rel="noopener noreferrer"&gt;Nx &lt;code&gt;s1ngularity&lt;/code&gt; GitHub security advisory&lt;/a&gt; -- the &lt;code&gt;pull_request_target&lt;/code&gt; compromise of &lt;code&gt;nx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack" rel="noopener noreferrer"&gt;Wiz Research: Shai-Hulud supply chain worm&lt;/a&gt; -- breakdown of the npm worm waves&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://panther.com/blog/shai-hulud-npm-supply-chain-attack" rel="noopener noreferrer"&gt;Panther Labs: Shai-Hulud analysis&lt;/a&gt; -- detection-focused write-up&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.aikido.dev/blog/red-hat-npm-packages-compromised-credential-stealing-worm" rel="noopener noreferrer"&gt;Aikido: credential-stealing npm worm&lt;/a&gt; -- package-level analysis&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://hackstacksecure.com/blogs/npm-worm-supply-chain-defense-2026/" rel="noopener noreferrer"&gt;2026 npm worm defense analysis&lt;/a&gt; -- Mini Shai-Hulud and defenses&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.elastic.co/security-labs/threat-command/shai-hulud-chaindrop-npm-supply-chain" rel="noopener noreferrer"&gt;Elastic Security Labs: Shai-Hulud &amp;amp; ChainDrop&lt;/a&gt; -- ChainDrop deep dive&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://unit42.paloaltonetworks.com/chaindrop-npm-worm-analysis/" rel="noopener noreferrer"&gt;Palo Alto Unit 42: ChainDrop npm worm&lt;/a&gt; -- Bun dropper and Ethereum C2 analysis&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.stepsecurity.io/blog/chaindrop-npm-worm" rel="noopener noreferrer"&gt;StepSecurity: ChainDrop incident breakdown&lt;/a&gt; -- CI/CD-centric timeline&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://blog.gitguardian.com/ghostaction-campaign-3-000-secrets-stolen/" rel="noopener noreferrer"&gt;GitGuardian: GhostAction, 3,000+ secrets stolen&lt;/a&gt; -- rogue workflow secret exfiltration&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines" rel="noopener noreferrer"&gt;Google Cloud: Workload Identity Federation with deployment pipelines&lt;/a&gt; -- the official WIF + GitHub Actions guide&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity" rel="noopener noreferrer"&gt;Google Cloud: Workload Identity Federation for GKE&lt;/a&gt; -- per-KSA identities on GKE&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/binary-authorization/docs" rel="noopener noreferrer"&gt;Google Cloud: Binary Authorization&lt;/a&gt; -- admission-time attestation enforcement&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Good thing to re-focus on, regarding todays "consciousness-bullshit" discussions :)</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Tue, 06 Oct 2026 08:58:05 +0000</pubDate>
      <link>https://dev.to/calganaygun/good-thing-to-re-focus-on-regarding-todays-consciousness-bullshit-discussions--274h</link>
      <guid>https://dev.to/calganaygun/good-thing-to-re-focus-on-regarding-todays-consciousness-bullshit-discussions--274h</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m" class="crayons-story__hidden-navigation-link"&gt;Resisting the Eye of the Machine: A Reflection on AI and Data Ownership&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/calganaygun" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F389991%2F259e54a7-492e-4848-b98e-950b56893521.jpg" alt="calganaygun profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/calganaygun" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Çalgan Aygün
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Çalgan Aygün
                
                
              
              &lt;div id="story-author-preview-content-3279190" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/calganaygun" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F389991%2F259e54a7-492e-4848-b98e-950b56893521.jpg" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Çalgan Aygün&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 24&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m" id="article-link-3279190"&gt;
          Resisting the Eye of the Machine: A Reflection on AI and Data Ownership
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/data"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;data&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/privacy"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;privacy&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>ethics</category>
    </item>
    <item>
      <title>Good thing to re-focus on, regarding todays "consciousness-bullshit" discussions :)</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Tue, 06 Oct 2026 08:58:02 +0000</pubDate>
      <link>https://dev.to/calganaygun/good-thing-to-re-focus-on-regarding-todays-consciousness-bullshit-discussions--2c0m</link>
      <guid>https://dev.to/calganaygun/good-thing-to-re-focus-on-regarding-todays-consciousness-bullshit-discussions--2c0m</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m" class="crayons-story__hidden-navigation-link"&gt;Resisting the Eye of the Machine: A Reflection on AI and Data Ownership&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/calganaygun" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F389991%2F259e54a7-492e-4848-b98e-950b56893521.jpg" alt="calganaygun profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/calganaygun" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Çalgan Aygün
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Çalgan Aygün
                
                
              
              &lt;div id="story-author-preview-content-3279190" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/calganaygun" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F389991%2F259e54a7-492e-4848-b98e-950b56893521.jpg" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Çalgan Aygün&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Feb 24&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m" id="article-link-3279190"&gt;
          Resisting the Eye of the Machine: A Reflection on AI and Data Ownership
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/discuss"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;discuss&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/data"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;data&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/privacy"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;privacy&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>ai</category>
      <category>ethics</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Building your own Git remote in under an hour</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Tue, 18 Aug 2026 19:59:27 +0000</pubDate>
      <link>https://dev.to/calganaygun/building-your-own-git-remote-in-under-an-hour-1gjp</link>
      <guid>https://dev.to/calganaygun/building-your-own-git-remote-in-under-an-hour-1gjp</guid>
      <description>&lt;p&gt;git·vodka's idea had been sitting in the back of my mind for a while.&lt;/p&gt;

&lt;p&gt;not as some big product idea. I just wanted a tiny git remote I could run for myself, friends, and family. something a little nicer than throwing bare repos on a server, but without turning it into another giant developer platform.&lt;/p&gt;

&lt;p&gt;today, with everyone suddenly talking about GitHub alternatives again after the recent downtime, I checked git.vodka and saw the domain was available.&lt;/p&gt;

&lt;p&gt;so I bought it.&lt;/p&gt;

&lt;p&gt;from there it was mostly impulse. pointed the domain at Cloudflare, opened Codex, and started building.&lt;/p&gt;

&lt;p&gt;the whole thing runs on Cloudflare: workers for the app, R2 for git objects and blobs, and D1 for the database. it also contains its own source code, which felt like a fun requirement for something hosting git repos.&lt;/p&gt;

&lt;p&gt;there wasn't really a plan beyond "let's see if this works."&lt;/p&gt;

&lt;p&gt;it did.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv82m2k3sgpp5b0rrxkj6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv82m2k3sgpp5b0rrxkj6.png" alt="Snapshot of git.vodka" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;from buying the domain to having a working version online, the whole thing took less than an hour. Codex wrote most of the boring parts while I kept changing my mind about how I wanted it to work.&lt;/p&gt;

&lt;p&gt;it's currently running entirely on Cloudflare's free tier, which is kind of ridiculous in the best way.&lt;/p&gt;

&lt;p&gt;anyway, that's git·vodka.&lt;/p&gt;

&lt;p&gt;you can poke around at &lt;a href="https://git.vodka/~c/git-vodka" rel="noopener noreferrer"&gt;git.vodka&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;and yes, the main advantage of owning your own git hosting service is that nobody can stop you from taking the username &lt;code&gt;c&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>git</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:31:58 +0000</pubDate>
      <link>https://dev.to/calganaygun/-47ei</link>
      <guid>https://dev.to/calganaygun/-47ei</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/calganaygun/i-hacked-every-dumb-screen-in-my-house-into-one-dashboard-3e8f" class="crayons-story__hidden-navigation-link"&gt;I hacked every dumb screen in my house into one dashboard&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/calganaygun" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F389991%2F259e54a7-492e-4848-b98e-950b56893521.jpg" alt="calganaygun profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/calganaygun" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Çalgan Aygün
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Çalgan Aygün
                
              
              &lt;div id="story-author-preview-content-4283081" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/calganaygun" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F389991%2F259e54a7-492e-4848-b98e-950b56893521.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Çalgan Aygün&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/calganaygun/i-hacked-every-dumb-screen-in-my-house-into-one-dashboard-3e8f" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 31&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/calganaygun/i-hacked-every-dumb-screen-in-my-house-into-one-dashboard-3e8f" id="article-link-4283081"&gt;
          I hacked every dumb screen in my house into one dashboard
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/gadget"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;gadget&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/automation"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;automation&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/calganaygun/i-hacked-every-dumb-screen-in-my-house-into-one-dashboard-3e8f" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/calganaygun/i-hacked-every-dumb-screen-in-my-house-into-one-dashboard-3e8f#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Agent Sandboxes: Giving AI Agents Their Own Little Linux Box (And Why You Should Care)</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Fri, 07 Aug 2026 09:38:40 +0000</pubDate>
      <link>https://dev.to/gde/agent-sandboxes-giving-ai-agents-their-own-little-linux-box-and-why-you-should-care-jl4</link>
      <guid>https://dev.to/gde/agent-sandboxes-giving-ai-agents-their-own-little-linux-box-and-why-you-should-care-jl4</guid>
      <description>&lt;p&gt;&lt;strong&gt;Sourced from:&lt;/strong&gt; &lt;a href="https://docs.cloud.google.com/kubernetes-engine/docs/concepts/machine-learning/agent-sandbox" rel="noopener noreferrer"&gt;GKE Agent Sandbox docs&lt;/a&gt;, &lt;a href="https://github.com/kubernetes-sigs/agent-sandbox" rel="noopener noreferrer"&gt;kubernetes-sigs/agent-sandbox&lt;/a&gt;  &lt;/p&gt;




&lt;p&gt;So here is the thing. We are asking AI agents to do increasingly wild stuff. Write code. Browse the web. Run shell commands. Spin up browsers. Click around. Deploy websites. Use a computer like a human would. And for the most part, they are doing it.&lt;/p&gt;

&lt;p&gt;But there is a lurking problem that keeps platform engineers up at night.&lt;/p&gt;

&lt;p&gt;Where the hell is this code running?&lt;/p&gt;

&lt;p&gt;When you ask an agent to "run this script" or "install this package" or "check if that endpoint responds", you are effectively handing over a loaded gun to a very enthusiastic intern who has read every programming book but has absolutely zero survival instincts. That code could be malicious. Or buggy. Or it could just go nuclear on your host system because the agent decided to &lt;code&gt;rm -rf /&lt;/code&gt; thinking it was cleaning up temp files.&lt;/p&gt;

&lt;p&gt;That is where &lt;strong&gt;Agent Sandboxes&lt;/strong&gt; come in.&lt;/p&gt;

&lt;p&gt;Think of them as giving each agent its own airtight, disposable Linux container. A little VM crib. A playpen with rubber walls where they can run around and break things without ever touching your real infrastructure.&lt;/p&gt;

&lt;p&gt;And here is the kicker: this is not some vaporware future concept. Google Cloud ships Agent Sandbox as a managed GKE feature, and the open-source project behind the Kubernetes APIs lives under &lt;code&gt;kubernetes-sigs/agent-sandbox&lt;/code&gt;. You can run the open-source controller on your own Kubernetes cluster or use the managed GKE integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What GKE Agent Sandbox Actually Is
&lt;/h2&gt;

&lt;p&gt;Let me quote the docs directly so we are on the same page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"GKE Agent Sandbox helps you manage isolated, stateful, and single-replica workloads on GKE. It is optimized for use cases like AI agent runtimes, where untrusted, LLM-generated code must be executed in a secure and performant environment."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What that means in plain English is:&lt;/p&gt;

&lt;p&gt;You get a Kubernetes-native way to spin up a single, stateful container that behaves like a lightweight VM. It has a stable hostname, persistent storage that survives restarts, kernel-level isolation, and network policies that default to "deny all". Your agent can connect to it, do its thing, disconnect, come back later, and find everything exactly as it left it. Or it can get garbage collected after a TTL and disappear without a trace.&lt;/p&gt;

&lt;p&gt;With a &lt;code&gt;SandboxWarmPool&lt;/code&gt;, already-started sandboxes can be assigned in milliseconds instead of waiting for a cold Pod to schedule, pull an image, and start. Cold-start latency still depends on your cluster, scheduler, image, and runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture: CRDs All the Way Down
&lt;/h2&gt;

&lt;p&gt;The whole thing is built on Kubernetes custom resource definitions. There are four main ones that matter:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Sandbox (the core)
&lt;/h3&gt;

&lt;p&gt;This is the primitive. A single, stateful Pod with a stable hostname, optional persistent storage, and full lifecycle management. You create this, and the controller handles the rest -- Pod creation, network identity, volume provisioning, scheduled deletion, pausing, and resuming.&lt;/p&gt;

&lt;p&gt;It is the answer to the question: "I want a Linux box that stays alive for my agent. How do I get one?"&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SandboxTemplate (the blueprint)
&lt;/h3&gt;

&lt;p&gt;Tired of writing the same YAML over and over? Templates let you codify runtime configuration once -- image, resource limits, runtime class (gVisor, Kata, standard), network policies, environment variables -- and reuse them everywhere. Platform teams define the templates. Developers just pick one and go.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SandboxClaim (the request)
&lt;/h3&gt;

&lt;p&gt;This is the user-facing abstraction. In the current &lt;code&gt;v1beta1&lt;/code&gt; API, a developer or agent SDK creates a &lt;code&gt;SandboxClaim&lt;/code&gt; that references a &lt;code&gt;SandboxWarmPool&lt;/code&gt;. The warm pool references the &lt;code&gt;SandboxTemplate&lt;/code&gt;. The controller adopts an available pre-warmed sandbox from that pool, or creates capacity according to the pool configuration, and hands back a ready-to-use environment.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;Claim Model&lt;/strong&gt; in action. You separate the &lt;em&gt;what&lt;/em&gt; (I need a sandbox) from the &lt;em&gt;how&lt;/em&gt; (here is where it runs on the cluster). And honestly? This is the pattern Kubernetes has needed for stateful workloads for a long time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. SandboxWarmPool (the performance hack)
&lt;/h3&gt;

&lt;p&gt;Here is where it gets fast. A &lt;code&gt;SandboxWarmPool&lt;/code&gt; maintains a set of pre-warmed Pod instances in a ready state. When a claim comes in, the controller instantly assigns a Pod from the pool instead of waiting for image pulls and container startup.&lt;/p&gt;

&lt;p&gt;Warm pools reduce provisioning latency by keeping Sandbox instances pre-created and ready to be claimed. On GKE, Pod snapshots are a separate feature that can save and restore sandbox state for pause/resume and faster recovery; they are not required for a warm pool to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security: Default Deny Is the Only Way
&lt;/h2&gt;

&lt;p&gt;Here is something that does not get talked about enough. When you give an agent a Linux box, you are trusting it with network access. A lot of agent sandbox solutions just throw a container at you and say "good luck".&lt;/p&gt;

&lt;p&gt;GKE Agent Sandbox implements a &lt;strong&gt;Default Deny&lt;/strong&gt; network security posture. Out of the box, code inside a sandbox cannot access your internal networks, your GKE control plane, or anything it should not. You explicitly define allowed egress and ingress rules in your &lt;code&gt;SandboxTemplate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why does this matter? Because the most realistic threat from LLM-generated code is not "the AI becomes self-aware and takes over the world". It is "the AI wrote a Python script with a typo that accidentally hit the production database". Default deny prevents the accident. Fine-grained rules let you open specific doors when you need to (e.g., pip needs to reach PyPI, your agent needs to call an API).&lt;/p&gt;




&lt;h2&gt;
  
  
  Isolation Runtimes: Pick Your Poison
&lt;/h2&gt;

&lt;p&gt;The Sandbox CRD is runtime-agnostic. You can pair it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standard containers&lt;/strong&gt; -- fast, familiar, but minimal isolation between workloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;gVisor&lt;/strong&gt; -- Google's user-space kernel. Gives you a second security boundary between your container and the host kernel. This is the recommended default for untrusted code execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kata Containers&lt;/strong&gt; -- hardware-level VM isolation. Each sandbox gets its own lightweight VM with its own kernel. The heaviest isolation, but also the strongest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The beauty of the API is that switching between these is a field change in your &lt;code&gt;SandboxTemplate&lt;/code&gt;. You do not redesign your architecture. You just change &lt;code&gt;runtimeClassName&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Programmatic Access SDKs
&lt;/h2&gt;

&lt;p&gt;You do not need to write Kubernetes YAML to use this. There are first-class client libraries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python SDK&lt;/strong&gt; -- high-level client for LangChain, Vertex AI Agentic SDK, or any Python agent framework. Create, query, manage, and destroy sandboxes from your agent code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go SDK&lt;/strong&gt; -- for platform engineers building controllers and services on top of Agent Sandbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both SDKs abstract away the claim lifecycle. With the current Python SDK, your agent creates a sandbox with &lt;code&gt;client.create_sandbox(warmpool="python-sandbox-pool", namespace="default")&lt;/code&gt;. Behind the scenes, the SDK creates a &lt;code&gt;SandboxClaim&lt;/code&gt;, the controller assigns a Sandbox from the warm pool, and the client returns a handle for command execution and file operations.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Comparison Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;GKE Agent Sandbox (managed)&lt;/th&gt;
&lt;th&gt;kubernetes-sigs/agent-sandbox (open source)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed GKE add-on&lt;/td&gt;
&lt;td&gt;Self-installed Kubernetes controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CRDs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sandbox, SandboxTemplate, SandboxClaim, SandboxWarmPool&lt;/td&gt;
&lt;td&gt;Sandbox, SandboxTemplate, SandboxClaim, SandboxWarmPool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Provisioning speed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Warm pools reduce claim latency to millisecond-scale assignment&lt;/td&gt;
&lt;td&gt;Varies by cluster; warm pools avoid cold-start work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Isolation runtimes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;gVisor is required for managed Agent Sandbox workloads&lt;/td&gt;
&lt;td&gt;Runtime-agnostic through Kubernetes &lt;code&gt;runtimeClassName&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Default deny, fine-grained egress/ingress rules&lt;/td&gt;
&lt;td&gt;Configurable via standard K8s NetworkPolicies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snapshots&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GKE Pod snapshots integrate with pause/resume&lt;/td&gt;
&lt;td&gt;Platform/runtime dependent; not required for warm pools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SDKs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python + Go&lt;/td&gt;
&lt;td&gt;Python + Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lifecycle mgmt&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed by Google (upgrades, patches)&lt;/td&gt;
&lt;td&gt;Self-managed via controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Limitations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires GKE 1.35.2-gke.1269000+ for full feature support&lt;/td&gt;
&lt;td&gt;Compatibility is release-specific; pin and test a release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Licensing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Cloud SLA&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google Cloud native teams who want "it just works"&lt;/td&gt;
&lt;td&gt;Multi-cloud, self-managed, air-gapped environments&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;Agent Sandbox solves a real problem that has been getting more urgent by the month. As LLMs get better at generating code, the volume of untrusted code being executed in agent loops is going to explode. Every one of those executions is an opportunity for something to go wrong.&lt;/p&gt;

&lt;p&gt;The old approach was: "just run it in a Docker container, it will be fine." A container can absolutely have a hostname and persistent volumes, but a one-off Docker workflow does not give you the Kubernetes-native &lt;code&gt;Sandbox&lt;/code&gt; lifecycle, claim/warm-pool allocation model, controller-managed identity, or stronger isolation runtime by itself. Those are the abstractions Agent Sandbox is adding.&lt;/p&gt;

&lt;p&gt;Agent Sandbox gives you those lifecycle and allocation primitives on top of Kubernetes, with SDKs in Python and Go. Isolation strength still depends on the runtime you configure; the managed GKE feature enforces gVisor for sandbox workloads.&lt;/p&gt;

&lt;p&gt;The open-source project under &lt;code&gt;kubernetes-sigs/agent-sandbox&lt;/code&gt; is the bedrock. Google's managed GKE add-on is the "press this button and it works" version. Both are worth knowing about.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Reality Check (Because Nothing Is Perfect)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Latency is low but not zero.&lt;/strong&gt; Even with warm pools and Pod snapshots, you are looking at hundreds of milliseconds to a second for sandbox assignment. For most agent use cases that is fine. For real-time loops that need sub-100ms responses, you will feel it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State management is your problem.&lt;/strong&gt; A sandbox is stateful by design. But what happens when an agent session crashes mid-execution? Do you keep the sandbox around? For how long? The TTL feature helps, but designing your garbage collection strategy is on you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network policy complexity is real.&lt;/strong&gt; Default deny is great for security. But the first time your agent cannot pip install because egress to PyPI is blocked, you will spend time debugging. And the first time a client's script needs to reach an internal API, you will be writing policy rules. It is manageable. But it is not zero-effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Costs add up in warm pools.&lt;/strong&gt; Each pre-warmed Pod is a running container burning CPU and memory while it waits. If you keep a pool of 20 sandboxes hot, you are paying for 20 idle containers. The pause/resume via Pod snapshots helps offset this, but it is something to budget for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring is a blank canvas.&lt;/strong&gt; Your existing Kubernetes monitoring? Probably set up for Deployments and StatefulSets. Ephemeral sandboxes that live for seconds to minutes are a different monitoring challenge. Log aggregation, metrics collection, cost attribution -- you are building some of this yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you want to reproduce the open-source setup on a fresh Kubernetes cluster, the sequence below is complete: install the controller &lt;strong&gt;and extensions&lt;/strong&gt;, create an executable Python runtime template, create a warm pool, give an in-cluster client the required RBAC, and run the SDK from a normal Python Pod.&lt;/p&gt;

&lt;p&gt;The commands below intentionally use &lt;code&gt;kubectl apply -f - &amp;lt;&amp;lt;'EOF'&lt;/code&gt; so you do not need to write YAML files locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Install Agent Sandbox with the extension CRDs
&lt;/h3&gt;

&lt;p&gt;As of this writing, the current upstream release is &lt;code&gt;v0.5.3&lt;/code&gt;. Pinning the version makes the example reproducible instead of silently changing when a new release lands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"v0.5.3"&lt;/span&gt;

kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/kubernetes-sigs/agent-sandbox/releases/download/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/sandbox-with-extensions.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the controller components to come up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; agent-sandbox-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that both the core and extension APIs are installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl api-resources | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see resources including &lt;code&gt;sandboxes&lt;/code&gt;, &lt;code&gt;sandboxclaims&lt;/code&gt;, &lt;code&gt;sandboxtemplates&lt;/code&gt;, and &lt;code&gt;sandboxwarmpools&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create a Python runtime template and warm pool
&lt;/h3&gt;

&lt;p&gt;A plain &lt;code&gt;python:3.12-slim&lt;/code&gt; container with &lt;code&gt;sleep infinity&lt;/code&gt; is &lt;strong&gt;not enough&lt;/strong&gt; for &lt;code&gt;sandbox.commands.run()&lt;/code&gt;. The SDK sends HTTP requests to the runtime's &lt;code&gt;/execute&lt;/code&gt; endpoint on port &lt;code&gt;8888&lt;/code&gt;, so the sandbox image must implement that API.&lt;/p&gt;

&lt;p&gt;The upstream Python runtime image does exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxTemplate
metadata:
  name: python-sandbox-template
  namespace: default
spec:
  podTemplate:
    spec:
      containers:
      - name: python-sandbox
        image: registry.k8s.io/agent-sandbox/python-runtime-sandbox:v0.1.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8888
---
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxWarmPool
metadata:
  name: python-sandbox-pool
  namespace: default
spec:
  replicas: 1
  sandboxTemplateRef:
    name: python-sandbox-template
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify that the template, pool, Sandbox, and Pod exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get sandboxtemplates,sandboxwarmpools,sandboxes,pods &lt;span class="nt"&gt;-n&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until the warm-pool sandbox Pod is &lt;code&gt;Running&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create a dedicated ServiceAccount and RBAC for the SDK client
&lt;/h3&gt;

&lt;p&gt;Do not run the SDK with the namespace's &lt;code&gt;default&lt;/code&gt; ServiceAccount in a real deployment. Give the client its own ServiceAccount with the permissions it needs to create/delete claims and resolve the underlying Sandbox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
apiVersion: v1
kind: ServiceAccount
metadata:
  name: sandbox-client
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sandbox-client
  namespace: default
rules:
- apiGroups:
  - agents.x-k8s.io
  resources:
  - sandboxes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - extensions.agents.x-k8s.io
  resources:
  - sandboxclaims
  verbs:
  - get
  - list
  - watch
  - create
  - delete
- apiGroups:
  - extensions.agents.x-k8s.io
  resources:
  - sandboxwarmpools
  - sandboxtemplates
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ""
  resources:
  - pods
  - services
  verbs:
  - get
  - list
  - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sandbox-client
  namespace: default
subjects:
- kind: ServiceAccount
  name: sandbox-client
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: sandbox-client
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the critical permissions using the fully qualified resource name syntax supported by &lt;code&gt;kubectl auth can-i&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl auth can-i get sandboxes.agents.x-k8s.io &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:serviceaccount:default:sandbox-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; default

kubectl auth can-i create sandboxclaims.extensions.agents.x-k8s.io &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:serviceaccount:default:sandbox-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; default

kubectl auth can-i delete sandboxclaims.extensions.agents.x-k8s.io &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:serviceaccount:default:sandbox-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three should return &lt;code&gt;yes&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Start a Python client Pod
&lt;/h3&gt;

&lt;p&gt;The SDK can run locally using &lt;code&gt;kubectl port-forward&lt;/code&gt;, but that mode requires &lt;code&gt;kubectl&lt;/code&gt; in the environment running the Python process. For this reproducible example, run the client &lt;strong&gt;inside the cluster&lt;/strong&gt; and use &lt;code&gt;SandboxInClusterConnectionConfig&lt;/code&gt;, which connects directly to the sandbox runtime and does not spawn &lt;code&gt;kubectl&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run python-pod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;python:3.12-slim &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Never &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--overrides&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{"spec":{"serviceAccountName":"sandbox-client"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;infinity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready pod/python-pod &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the Python SDK in that Pod, pinned to the controller release used above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; default python-pod &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  pip &lt;span class="nb"&gt;install &lt;/span&gt;k8s-agent-sandbox&lt;span class="o"&gt;==&lt;/span&gt;0.5.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Create a sandbox and execute Python inside it
&lt;/h3&gt;

&lt;p&gt;Run the client directly through stdin; no Python file is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; default python-pod &lt;span class="nt"&gt;--&lt;/span&gt; python - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
from k8s_agent_sandbox import SandboxClient
from k8s_agent_sandbox.models import SandboxInClusterConnectionConfig

client = SandboxClient(
    connection_config=SandboxInClusterConnectionConfig()
)

sandbox = client.create_sandbox(
    warmpool="python-sandbox-pool",
    namespace="default",
)

try:
    result = sandbox.commands.run(
        'python3 -c &lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="sh"&gt;print("hello from my sandbox")&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="sh"&gt;'
    )
    print(result.stdout)
finally:
    sandbox.terminate()
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello from my sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point the complete path is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python-pod
    |
    | SandboxInClusterConnectionConfig
    v
SandboxClaim -&amp;gt; SandboxWarmPool -&amp;gt; Sandbox -&amp;gt; runtime Pod:8888
                                      |
                                      +-&amp;gt; POST /execute
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Troubleshooting the three failures you are most likely to hit
&lt;/h3&gt;

&lt;p&gt;If you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SandboxWarmPoolNotFoundError: SandboxWarmPool "python-sandbox-pool" not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;check that the warm pool exists in the same namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get sandboxwarmpools &lt;span class="nt"&gt;-n&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get a &lt;code&gt;403 Forbidden&lt;/code&gt; mentioning &lt;code&gt;sandboxclaims&lt;/code&gt; or &lt;code&gt;sandboxes&lt;/code&gt;, check the ServiceAccount RBAC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl auth can-i get sandboxes.agents.x-k8s.io &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;system:serviceaccount:default:sandbox-client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTPConnection(... port=8888): Failed to establish a new connection: Connection refused
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;check the image used by the sandbox Pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;NAME:.metadata.name,IMAGE:.spec.containers[&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;.image,IP:.status.podIP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sandbox must run a runtime server that implements &lt;code&gt;/execute&lt;/code&gt; on port &lt;code&gt;8888&lt;/code&gt;; a normal Python image that only sleeps will not work with &lt;code&gt;sandbox.commands.run()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Clean up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete pod python-pod &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="nt"&gt;--ignore-not-found&lt;/span&gt;
kubectl delete sandboxwarmpool python-sandbox-pool &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="nt"&gt;--ignore-not-found&lt;/span&gt;
kubectl delete sandboxtemplate python-sandbox-template &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="nt"&gt;--ignore-not-found&lt;/span&gt;
kubectl delete rolebinding sandbox-client &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="nt"&gt;--ignore-not-found&lt;/span&gt;
kubectl delete role sandbox-client &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="nt"&gt;--ignore-not-found&lt;/span&gt;
kubectl delete serviceaccount sandbox-client &lt;span class="nt"&gt;-n&lt;/span&gt; default &lt;span class="nt"&gt;--ignore-not-found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  On GKE instead
&lt;/h3&gt;

&lt;p&gt;For the managed GKE feature, Agent Sandbox requires GKE &lt;code&gt;1.35.2-gke.1269000&lt;/code&gt; or later. On an existing Standard cluster, Google also requires a gVisor-enabled node pool before enabling Agent Sandbox. The current command uses the beta cluster surface and includes the cluster location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud beta container clusters update &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLUSTER_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--enable-agent-sandbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GKE additionally enforces sandbox workload requirements such as &lt;code&gt;runtimeClassName: gvisor&lt;/code&gt;, disabling automatic ServiceAccount-token mounting, running as non-root, dropping Linux capabilities, setting CPU/memory limits, and scheduling onto the gVisor sandbox node pool. Use the managed GKE deployment manifest from the official GKE guide rather than the minimal self-managed template above.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;GKE Agent Sandbox -- and the open-source &lt;code&gt;kubernetes-sigs/agent-sandbox&lt;/code&gt; project it is built on -- is what happens when someone finally builds the right abstraction for agent runtimes. It is Kubernetes-native, which means it fits into existing infrastructure without fighting it. It is fast enough for interactive use. It is secure by default. And it has SDKs that let agents manage their own environments programmatically.&lt;/p&gt;

&lt;p&gt;The era of "just run it in Docker and pray" is ending. The era of "give the agent its own Linux box, properly isolated, properly managed, properly fast" is here.&lt;/p&gt;

&lt;p&gt;And honestly? It is about damn time.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;All the sourcing for this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.cloud.google.com/kubernetes-engine/docs/concepts/machine-learning/agent-sandbox" rel="noopener noreferrer"&gt;GKE Agent Sandbox documentation&lt;/a&gt; -- the primary source on the managed GKE add-on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kubernetes-sigs/agent-sandbox" rel="noopener noreferrer"&gt;kubernetes-sigs/agent-sandbox&lt;/a&gt; (~3k stars) -- the open-source CNCF SIG project under SIG Apps&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://agent-sandbox.sigs.k8s.io/docs/" rel="noopener noreferrer"&gt;Agent Sandbox documentation site&lt;/a&gt; -- full docs, getting started guides, Python/Go SDK references&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pypi.org/project/k8s-agent-sandbox/" rel="noopener noreferrer"&gt;Agent Sandbox Python SDK&lt;/a&gt; -- programmatic sandbox management from Python&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pkg.go.dev/sigs.k8s.io/agent-sandbox" rel="noopener noreferrer"&gt;Agent Sandbox Go SDK&lt;/a&gt; -- same, but for Go&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>I hacked every dumb screen in my house into one dashboard</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Fri, 31 Jul 2026 19:46:50 +0000</pubDate>
      <link>https://dev.to/calganaygun/i-hacked-every-dumb-screen-in-my-house-into-one-dashboard-3e8f</link>
      <guid>https://dev.to/calganaygun/i-hacked-every-dumb-screen-in-my-house-into-one-dashboard-3e8f</guid>
      <description>&lt;p&gt;This Kindle was supposed to be obsolete.&lt;/p&gt;

&lt;p&gt;The tiny LCD beside it was designed to do little more than rotate a slide show.&lt;/p&gt;

&lt;p&gt;Now both show live transit departures, weather, tasks, and anything else I can pull from an API—and I control them from the same self-hosted dashboard.&lt;/p&gt;

&lt;p&gt;No custom firmware on the LCD. No soldering. No cloud subscription.&lt;/p&gt;




&lt;p&gt;It started with a cheap Chinese display:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fez00sezyv81wu09hhltd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fez00sezyv81wu09hhltd.png" alt="GeekMagic Ultra screen showing transport data" width="799" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;GeekMagic Ultra&lt;/strong&gt;, or "SmallTV" as people call it, a 240×240 IPS panel with a stock firmware that just rotates through a gallery slideshow or weather UI. Static images only, no dynamic data unles you use weather screen, no API for live content. But it has a &lt;code&gt;/doUpload&lt;/code&gt; endpoint and a &lt;code&gt;/set&lt;/code&gt; call. Punch in its IP, overwrite the same JPEG every time, and suddenly that dumb gallery display is showing live transit departures and Todoist tasks.&lt;/p&gt;

&lt;p&gt;No firmware mods. No soldering. Just one script.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;a href="https://github.com/calganaygun/geekmagic-ultra-scripts" rel="noopener noreferrer"&gt;geekmagic-ultra-scripts&lt;/a&gt;&lt;/strong&gt; to prove it works. Transit boards, task lists, weather — all running on stock hardware that cost pocket change. That project was fun. I thought it was the end of the story.&lt;/p&gt;




&lt;p&gt;Then I found a &lt;strong&gt;used, EOL'd Kindle 4 NT&lt;/strong&gt; for 100 PLN (like 25 USD) on the second-hand market. Amazon abondaned it, but I haven't :) It runs on Linux. It has an e-ink display that uses almost no power. A command called &lt;code&gt;eips&lt;/code&gt; can render images. It can toggle Wi-Fi, report battery, last weeks on a charge, and wake from deep sleep via its RTC.&lt;/p&gt;

&lt;p&gt;One jailbreak way to an SSH session and I knew: this thing is a perfect pull-based display. I highly inspired from melihkarakelle's project called &lt;a href="https://github.com/melihkarakelle/kindle4-weather-display" rel="noopener noreferrer"&gt;kindle4-weather-display&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78nk6paelultbh8lx6e5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78nk6paelultbh8lx6e5.png" alt="Kindle Weather/Todosit UI" width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now I had two completely different hacked screens: one push (GeekMagic), one pull (Kindle), and no unified way to manage them. Every device needed its own script, its own config, its own workflow.&lt;/p&gt;

&lt;p&gt;So I sat down and built the thing that ties them all together.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;hackreen&lt;/strong&gt; is a self-hosted control plane and design studio for every hacked display in your house. One web UI, one declarative screen language, deploy to any device.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;Boots in two minutes (&lt;code&gt;docker compose up -d&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GeekMagic → push mode.&lt;/strong&gt; Configure the endpoint. Hackreen renders a 240×240 JPEG, overwrites the same gallery file, calls &lt;code&gt;/set&lt;/code&gt;, and the display updates instantly. Zero hardware hacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kindle 4 → pull mode.&lt;/strong&gt; Copy-paste one &lt;code&gt;wget | sh&lt;/code&gt; command into the Kindle. It installs a client that wakes Wi-Fi every 30 minutes, pulls a fresh image, renders it with &lt;code&gt;eips&lt;/code&gt;, reports battery, then goes back to RTC deep sleep for days of battery life. SSH stays reachable. There's even a debug mode: hold the center 5-way key during power-on and you get a shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hackreen Screen Language (HSL).&lt;/strong&gt; Declarative JSON format. Define data sources (clock, weather, Todoist, transit APIs), compose layers with text, images, icons, shapes. Every live source ships with &lt;strong&gt;sample data&lt;/strong&gt; so the preview works offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design Studio.&lt;/strong&gt; Visual editor with instant preview. Connect to live APIs, iterate in real time, push when it looks right — without touching a config file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets Vault.&lt;/strong&gt; AES-256-GCM encrypted. Tokens stay safe at rest, never exposed by the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2,000+ Lucide icons.&lt;/strong&gt; Built-in. &lt;code&gt;icon:lucide:cloud-rain&lt;/code&gt;, done.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Kindles pull. The GeekMagic gets pushed. You design once, deploy everywhere.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38ylun2mgd38pj4gswz8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38ylun2mgd38pj4gswz8.png" alt="Hackreen Snapshot" width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why self-hosted?
&lt;/h2&gt;

&lt;p&gt;This is a home lab tool. You run it on your LAN. Access through a VPN. No third party, no subscription, no "we changed our API, sorry." You own your data and your screens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lineage
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/calganaygun/geekmagic-ultra-scripts" rel="noopener noreferrer"&gt;geekmagic-ultra-scripts&lt;/a&gt;&lt;/strong&gt; — the first proof of concept. Turned a stock gallery slideshow into a live information display by repeatedly overwriting one file. Transit, Todoist, weather templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kindle 4 NT discovery&lt;/strong&gt; — second-hand e-ink device that could pull and display images with almost zero power consumption. Proved the pull model worked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/calganaygun/hackreen" rel="noopener noreferrer"&gt;Hackreen&lt;/a&gt;&lt;/strong&gt; — the orchestrator. Brought push and pull under one roof with a proper design pipeline, HSL language, and a unified API.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Every hacked screen in this house goes into Hackreen. The SmallTV in the kitchen. The Kindle on the desk. Whatever cheap display I find next — it's getting a pull URL or a push button. The question isn't "can I show this data on that screen?" anymore. The answer is always yes, it's just another screen in the dashboard.&lt;/p&gt;




&lt;p&gt;Check it out: &lt;strong&gt;&lt;a href="https://github.com/calganaygun/hackreen" rel="noopener noreferrer"&gt;github.com/calganaygun/hackreen&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;This project is highly inspired by Melih Karakelle's tweets and &lt;a href="https://github.com/melihkarakelle" rel="noopener noreferrer"&gt;projects&lt;/a&gt;, so special thanks to him.&lt;/p&gt;

</description>
      <category>gadget</category>
      <category>programming</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>Resisting the Eye of the Machine: A Reflection on AI and Data Ownership</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Tue, 24 Feb 2026 21:08:49 +0000</pubDate>
      <link>https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m</link>
      <guid>https://dev.to/calganaygun/resisting-the-eye-of-the-machine-a-reflection-on-ai-and-data-ownership-510m</guid>
      <description>&lt;p&gt;AI both creates and consumes. For someone like me, who’s invested deeply in both the benefits and risks of these evolving systems, the duality is impossible to ignore. On one hand, AI supplements creativity, fast-tracks productivity, and offers insights unprecedented in human history. On the other, AI is a voracious consumer that treats every public thought, image, and pixel as a potential resource for improvement—its improvement, not ours.&lt;/p&gt;

&lt;p&gt;What happens to ideas when they’re not just shared, but consumed, repurposed, and disjointed from their original intention? This is not a fight against inevitable progress but an invitation to consider where the boundaries should lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hunger of the Machine
&lt;/h2&gt;

&lt;p&gt;Today’s AI systems are participants in a digital ecosystem. To feed their model-building appetite, they consume everything: tweets, screenshots, vague status updates, unfinished sketches, throwaway jokes—anything to refine prediction and replication capabilities. These systems don’t ask for permission; most of the time, they don’t even acknowledge the humans who originally created the content.&lt;/p&gt;

&lt;p&gt;As someone interested in sparking friction within this process, I’ve explored ways to actively undermine AI’s consumption. One approach stems from visual obfuscation principles to disrupt AI readers at the OCR level, reducing their ability to reconstruct coherent text segments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic Segmentation and My Implementation
&lt;/h3&gt;

&lt;p&gt;The core idea relies on &lt;strong&gt;radical dynamic segmentation&lt;/strong&gt;. Imagine text that never fully "settles"—it pulses, shifts, even disassembles itself momentarily. While the human eye has an incredible ability to "fill in the blanks" and interpret movement, machines struggle with this flux.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgyd4yhqsq3cpemuzgis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgyd4yhqsq3cpemuzgis.png" alt="Initial concept from h43z." width="598" height="767"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/h43z/status/1949241540465045853" rel="noopener noreferrer"&gt;h43z's initial concept&lt;/a&gt; focused on kinetic text—characters in constant motion to disrupt machine vision. This was the spark that got me thinking, but I took a different route: breaking characters into partial segments that flash independently. The chunking method was something I figured out through experimentation.&lt;/p&gt;

&lt;p&gt;The implementation is JavaScript-based, using Canvas to analyze each character's pixels and fragment them in various ways—radial slices, concentric rings, diagonal strips, random distributions. Each chunk flashes at randomized intervals. The result: text that humans can read but machines can't easily parse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3zm3sgzv4hz9n3zg0yd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd3zm3sgzv4hz9n3zg0yd.png" alt="One of my tested concepts, A screenshot shows 'Hello dev.to'" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I tested against OCR libraries like Tesseract and even LLM-adjacent pipelines. The simpler systems failed completely. Modern Live/Video APIs struggled but occasionally recovered—revealing both the method's potential and its limits.&lt;/p&gt;

&lt;p&gt;Why it works: It exploits the gap between human and machine perception. We thrive on approximation and motion. Machines expect clean, static data. Every fragmented flash introduces chaos that challenges their assumptions about how information should look.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations and Lessons
&lt;/h3&gt;

&lt;p&gt;Technology can’t be a self-contained solution to problems of technological overreach. One critical limitation is that dynamic segmentation, while effective on smaller scales, is computationally expensive to produce and impractical for systems requiring long text readability.&lt;/p&gt;

&lt;p&gt;Moreover, it raises a practical question: How many people would opt to obfuscate their content actively? To scale such efforts and normalize resistance, the tools need to be seamless, almost invisible to the writer. A browser extension that dynamically applies segmentation while retaining human readability—a future goal—might bridge this gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Philosophical Trade-Offs
&lt;/h2&gt;

&lt;p&gt;This technical exploration also emphasizes deeper philosophical questions. By participating in this cat-and-mouse game against AI, do we solve the problem or play into its competitive escalation? As much as the act of resistance feels gratifying, it’s worth asking: What happens when the effort to defend against machines becomes indistinguishable from innovations driving them forward?&lt;/p&gt;

&lt;p&gt;AI reveals how much has been surrendered—privacy, ownership, sometimes even the joy of creating for humans rather than systems. But agency remains deeply human. Perhaps that’s the key takeaway: less about domination and more about choosing what remains ours.&lt;/p&gt;

&lt;h2&gt;
  
  
  In Closing: Toward Balancing Agency
&lt;/h2&gt;

&lt;p&gt;Dynamic segmentation obviously is not a silver bullet. It won’t end AI overconsumption, but it does add another tool to the resistance. My experiments aim to create what friction I can—disrupting the expectation that AI must own everything it sees. At the heart of this is the hope that small, deliberate acts can start larger conversations about human ownership in an AI-driven culture.&lt;/p&gt;

&lt;p&gt;As experiments continue, the tension between control and participation stays alive. To stabilize both technology and thought, we’ll need boundaries that don’t eliminate creativity but celebrate and protect its imperfection.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Acknowledgments:&lt;/strong&gt; To h43z, whose kinetic text concept was the initial inspiration that sparked my thinking on this problem. While their approach focused on character movement, it pushed me to explore the chunking and partial segmentation methods documented here.&lt;/p&gt;




&lt;p&gt;Additional References:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My implementation of dynamic segmentation tests: &lt;a href="http://e.43z.one/ega5yzz" rel="noopener noreferrer"&gt;Live Demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The foundational thought by h43z: &lt;a href="https://x.com/h43z/status/1949241540465045853" rel="noopener noreferrer"&gt;Original Tweet&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>data</category>
      <category>discuss</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Redefining Event-Driven Architecture on Google Cloud</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Mon, 16 Feb 2026 19:10:10 +0000</pubDate>
      <link>https://dev.to/gde/redefining-event-driven-architecture-on-google-cloud-5abj</link>
      <guid>https://dev.to/gde/redefining-event-driven-architecture-on-google-cloud-5abj</guid>
      <description>&lt;p&gt;Building event-driven systems with Cloud Run, Pub/Sub, and Eventarc is a powerful pattern, but the gap between "Hello World" tutorials and production stability is wide. These are the hard-won lessons from the trenches of Google Cloud serverless architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Choosing Your Transport: Eventarc vs. Pub/Sub
&lt;/h2&gt;

&lt;p&gt;While Eventarc often uses Pub/Sub under the hood, the choice of which to interface with directly depends on your source and required control.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Eventarc&lt;/th&gt;
&lt;th&gt;Direct Pub/Sub&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GCP-native events (GCS, Firestore, Audit Logs)&lt;/td&gt;
&lt;td&gt;Custom inter-service messaging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed wrapper, high convenience&lt;/td&gt;
&lt;td&gt;Manual configuration, high control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Filtering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in attribute filtering&lt;/td&gt;
&lt;td&gt;Fine-grained policies &amp;amp; custom attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IAM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;eventarc.eventReceiver&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;pubsub.subscriber&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Use Eventarc for "plumbing" Google Cloud events. Use Direct Pub/Sub when you need a custom event bus or specific retry/ordering logic.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. The Infrastructure Reality Check
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Idempotency Requirement
&lt;/h3&gt;

&lt;p&gt;Pub/Sub guarantees &lt;strong&gt;at-least-once&lt;/strong&gt; delivery by default. While exactly-once delivery exists as an opt-in feature, your consumers must assume duplicates will happen. If your handler isn't idempotent, you risk double-charging users or corrupting state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Non-Negotiable Flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract a unique &lt;strong&gt;Idempotency Key&lt;/strong&gt; (UUID/Hash) from the event.&lt;/li&gt;
&lt;li&gt;Check a fast-access cache (Memorystore or Firestore) for the key.&lt;/li&gt;
&lt;li&gt;If present, &lt;strong&gt;discard&lt;/strong&gt; the message as a duplicate.&lt;/li&gt;
&lt;li&gt;If absent, process the event and &lt;strong&gt;store&lt;/strong&gt; the key with a TTL.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Managing Cold Starts
&lt;/h3&gt;

&lt;p&gt;Cloud Run’s "scale to zero" is a budget-saver but a latency-killer. A container startup typically adds &lt;strong&gt;2–8 seconds&lt;/strong&gt; to the first request.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Set &lt;code&gt;min-instances&lt;/code&gt; for latency-critical paths (this incurs cost).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Alternative:&lt;/strong&gt; Use &lt;strong&gt;Cloud Run Jobs&lt;/strong&gt; for asynchronous batch processing where start-time overhead is less relevant.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Eventarc Propagation Delays
&lt;/h3&gt;

&lt;p&gt;When deploying Eventarc triggers via Terraform or CLI, filtering rules can take &lt;strong&gt;60–120 seconds&lt;/strong&gt; to propagate across the Google network.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Production Fix:&lt;/strong&gt; Build a "warm-up" delay into your CI/CD pipeline. Do not trigger integration tests immediately after a successful deployment, or you will see intermittent, false-positive failures.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Calculating Realistic Costs
&lt;/h2&gt;

&lt;p&gt;Tutorials often suggest serverless is "pennies," but at scale, the math changes. Pub/Sub is billed at &lt;strong&gt;$40 per TiB&lt;/strong&gt;, with a &lt;strong&gt;1 KB minimum&lt;/strong&gt; per message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: 10M Events/Day (300M/Month)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data Volume:&lt;/strong&gt; 300M messages × 1 KB (min size) ≈ &lt;strong&gt;293 GiB (0.286 TiB)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingest &amp;amp; Delivery:&lt;/strong&gt; (0.286 TiB × $40) + (0.286 TiB × $40) = &lt;strong&gt;$23/month&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute (Cloud Run):&lt;/strong&gt; 300M invocations (assuming 1s duration, 512MB RAM) ≈ &lt;strong&gt;$150–$200/month&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total:&lt;/strong&gt; ~$175–$225/month. Significant, but highly predictable if you monitor message size and invocation duration.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Advanced Production Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Regional Co-location
&lt;/h3&gt;

&lt;p&gt;Eventarc triggers for multi-region services (like Firestore &lt;code&gt;nam5&lt;/code&gt;) are often pinned to specific regions (e.g., &lt;code&gt;us-central1&lt;/code&gt;). If your Cloud Run service resides elsewhere, you will incur &lt;strong&gt;cross-region egress charges&lt;/strong&gt; and increased latency. Always verify the region mapping table in the GCP documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Payload Sanitization at the Edge
&lt;/h3&gt;

&lt;p&gt;Using &lt;strong&gt;Eventarc Advanced&lt;/strong&gt;, you can use Common Expression Language (CEL) to transform or redact payloads before they reach the consumer. This is vital for PII compliance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Example: Redacting an email address in-flight
message.setField("data.email", re.extract(message.data.email, "(^.).*@(.*)", "\\1***@\\2"))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Observability and Dead-Letter Topics (DLTs)
&lt;/h3&gt;

&lt;p&gt;In a distributed system, an event can "disappear" if a filter drops it or a consumer fails silently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DLTs:&lt;/strong&gt; Every subscription must have a Dead-Letter Topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting:&lt;/strong&gt; Monitor the &lt;code&gt;subscription/dead_letter_message_count&lt;/code&gt; metric. A rising count is your first sign of a logic bug or schema mismatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracing:&lt;/strong&gt; Use OpenTelemetry to inject &lt;strong&gt;Trace IDs&lt;/strong&gt; into event attributes, allowing you to follow a single request from the producer through the bus to the consumer logs.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. When to Avoid Event-Driven
&lt;/h2&gt;

&lt;p&gt;Don't "cargo-cult" this architecture if it doesn't fit your needs. Skip it if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You require &lt;strong&gt;strong consistency&lt;/strong&gt; and immediate transactions.&lt;/li&gt;
&lt;li&gt;Your end-to-end latency requirements are &lt;strong&gt;&amp;lt; 100ms&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The overhead of debugging distributed traces outweighs the scaling benefits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these cases, stick to &lt;strong&gt;Synchronous gRPC/HTTP&lt;/strong&gt; or &lt;strong&gt;Cloud Workflows&lt;/strong&gt; for structured orchestration.&lt;/p&gt;




&lt;h3&gt;
  
  
  Final Takeaway
&lt;/h3&gt;

&lt;p&gt;The combination of Cloud Run and Eventarc is one of the most robust patterns in 2026. By respecting the boundaries of the platform—accounting for propagation delays, ensuring idempotency, and co-locating regions—you can build a system that scales effortlessly from zero to millions of events.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>googlecloud</category>
      <category>serverless</category>
    </item>
    <item>
      <title>A CAPTCHA Bypass Technique: Audio Files</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Fri, 30 Jan 2026 12:38:29 +0000</pubDate>
      <link>https://dev.to/calganaygun/a-captcha-bypass-technique-audio-files-315k</link>
      <guid>https://dev.to/calganaygun/a-captcha-bypass-technique-audio-files-315k</guid>
      <description>&lt;p&gt;This is a draft write-up from 2019, documenting a CAPTCHA bypass technique I discovered back then. All code and images shown are examples for educational purposes.&lt;/p&gt;




&lt;p&gt;One day back in 2019, I got tired of repeatedly logging into my school's student system just to enroll in a full class. Then a lightbulb went off in my head: I could automate these attempts and refocus on my actual work.&lt;/p&gt;

&lt;p&gt;When it comes to automating website processes, I love using &lt;a href="https://en.wikipedia.org/wiki/Userscript" rel="noopener noreferrer"&gt;user scripts&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Automation Plan
&lt;/h2&gt;

&lt;p&gt;Our school system expired user sessions after a while, even if you were actively working. So first, I needed to automate the login process. Once successfully logged in, navigating and enrolling in a class would be straightforward (just some click event magic).&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Login UI
&lt;/h2&gt;

&lt;p&gt;The school system had an internally managed CAPTCHA service. It displayed two numbers and asked for their sum. The images were slightly corrupted—but not enough to prevent OCR. However, I didn't want to rely on any API or image processing system to solve this CAPTCHA.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63iiiylbkwj5rfrmgr4y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F63iiiylbkwj5rfrmgr4y.png" alt="Example captcha image" width="442" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Inspecting the Audio Service for CAPTCHA
&lt;/h2&gt;

&lt;p&gt;After deciding not to use any image-related service, I started inspecting the network traffic of the login system. I noticed that the answer to the generated CAPTCHA was stored server-side in a session associated with me.&lt;/p&gt;

&lt;p&gt;When I clicked the audio playback button, I realized it was reading out the answer directly. Two different endpoints returned two different audio files, split into tens and ones digits.&lt;/p&gt;

&lt;p&gt;The audio system seemed like a perfect route for me. I had no intention of feeding these audio files into a speech-to-text service—I was looking for the fastest, hackiest solution possible.&lt;/p&gt;

&lt;p&gt;Since the same audio files should have the same file size, I decided to test this hypothesis first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvg4vgyuvgzaz0a1d7vd4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvg4vgyuvgzaz0a1d7vd4.png" alt="Example HTTP traffic" width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And bingo! By mapping each tens and ones digit to their file sizes beforehand, I could automatically determine the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Solution
&lt;/h2&gt;

&lt;p&gt;Here's an example of how the code worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example mapping of file sizes to digit values&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tensMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 0 tens&lt;/span&gt;
  &lt;span class="mi"&gt;1456&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// 1 ten&lt;/span&gt;
  &lt;span class="mi"&gt;1567&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// 2 tens&lt;/span&gt;
  &lt;span class="c1"&gt;// ... etc&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onesMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;987&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;// 0 ones&lt;/span&gt;
  &lt;span class="mi"&gt;1023&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 1 one&lt;/span&gt;
  &lt;span class="mi"&gt;1145&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// 2 ones&lt;/span&gt;
  &lt;span class="c1"&gt;// ... etc&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Example: Fetch audio files and determine answer by file size&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;solveCaptcha&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tensResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/captcha/audio/tens&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onesResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/captcha/audio/ones&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tensSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tensResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onesSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onesResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-length&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tensValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tensMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tensSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onesValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;onesMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;onesSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tensValue&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;onesValue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Use it in the login flow&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;solveCaptcha&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#captcha-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#login-button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I tested this approach, it worked perfectly!&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Lessons Learned
&lt;/h2&gt;

&lt;p&gt;This experience taught me a few things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Look for alternative attack vectors:&lt;/strong&gt; When the obvious solution (OCR) seems complex, there might be simpler paths (audio files, metadata, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security through obscurity fails:&lt;/strong&gt; Just because a CAPTCHA uses audio doesn't mean it's secure—especially if the files are deterministic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;File size is metadata:&lt;/strong&gt; Even without processing the content, file properties can leak information&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This technique worked because the audio files were pre-generated and static. A more secure implementation would generate unique audio files or add random noise to prevent size-based fingerprinting.&lt;/p&gt;

</description>
      <category>security</category>
      <category>captcha</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Uncomfortable Truth About AI-Assisted Development</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Wed, 28 Jan 2026 11:59:53 +0000</pubDate>
      <link>https://dev.to/calganaygun/the-uncomfortable-truth-about-ai-assisted-development-4ckp</link>
      <guid>https://dev.to/calganaygun/the-uncomfortable-truth-about-ai-assisted-development-4ckp</guid>
      <description>&lt;p&gt;&lt;em&gt;Or: How We Learned to Stop Worrying and Start Debugging at 2am&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I need to tell you about a report that made me uncomfortable. Not because it revealed anything shocking, but because it quantified what I've been watching happen in real-time across engineering teams for the past year.&lt;/p&gt;

&lt;p&gt;CodeRabbit analyzed 470 pull requests — 320 co-authored by AI, 150 written by humans. The headline number: &lt;strong&gt;AI-generated code contains 1.7x more issues than human-written code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But that's not the uncomfortable part. The uncomfortable part is that we already knew this. We just chose not to measure it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Velocity Trap
&lt;/h2&gt;

&lt;p&gt;Here's what's happening on the ground:&lt;/p&gt;

&lt;p&gt;Teams adopted AI coding assistants in 2024. By 2025, they're shipping 20% more PRs per developer. Product managers are thrilled. Engineers feel productive. The metrics look great.&lt;/p&gt;

&lt;p&gt;Then production incidents spike by 23.5%.&lt;/p&gt;

&lt;p&gt;At first, you blame other factors. Infrastructure changes. New team members. Bad luck. But when you dig into the post-mortems, a pattern emerges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A null pointer dereference that should have been caught by basic error handling&lt;/li&gt;
&lt;li&gt;Hardcoded credentials that somehow made it through review&lt;/li&gt;
&lt;li&gt;A database query executing 500 times in a loop instead of being batched&lt;/li&gt;
&lt;li&gt;Exception handling that swallows errors and returns success anyway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't exotic bugs. They're &lt;em&gt;boring&lt;/em&gt; bugs. The kind a junior developer makes in their first month. The kind that code review is supposed to catch.&lt;/p&gt;

&lt;p&gt;Except we're not reviewing like we used to. Because the code &lt;em&gt;looks fine&lt;/em&gt;. It compiles. It follows naming conventions. The structure makes sense. So we skim it, assume the AI got it right, and hit approve.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Numbers Actually Mean
&lt;/h2&gt;

&lt;p&gt;Let's break down what CodeRabbit found, because the devil is in the details:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logic errors: 2x higher&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Algorithm/business logic: 2.25x&lt;/li&gt;
&lt;li&gt;Concurrency control: 2.29x&lt;/li&gt;
&lt;li&gt;Null handling: 2.27x&lt;/li&gt;
&lt;li&gt;Exception handling: 1.97x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tells you something fundamental: &lt;strong&gt;AI doesn't understand execution flow&lt;/strong&gt;. It pattern matches syntax. It knows what error handling &lt;em&gt;looks like&lt;/em&gt;, but not when you actually need it. It generates code that passes the happy path and explodes on edge cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security vulnerabilities: 1.57x higher&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;XSS injection: 2.74x&lt;/li&gt;
&lt;li&gt;Insecure object references: 1.91x&lt;/li&gt;
&lt;li&gt;Password handling: 1.88x&lt;/li&gt;
&lt;li&gt;Insecure deserialization: 1.82x&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the scary one. Because security bugs don't just crash your app—they compromise your users. And AI is literally trained on public code repositories, including all the insecure code that's been written over the past two decades. It's regurgitating attack vectors from 2015 Stack Overflow answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance issues: 7.9x more I/O problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This one made me laugh, then cry. AI optimizes for "code that works" not "code that works &lt;em&gt;efficiently&lt;/em&gt;". Why batch database queries when you can just loop? Why cache when you can fetch? It's technically correct. It's also a production disaster waiting to happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code quality: 3.15x worse readability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The irony here is brutal. One of the selling points of AI assistants is that they write "clean code". Except what they actually write is verbose, repetitive code with inconsistent naming and unnecessary abstraction. It's &lt;em&gt;formatted&lt;/em&gt; nicely. That's not the same thing as &lt;em&gt;readable&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 90th Percentile Problem
&lt;/h2&gt;

&lt;p&gt;Here's the number that keeps me up at night: at the 90th percentile, AI PRs contain &lt;strong&gt;26 issues&lt;/strong&gt; versus 12 for humans.&lt;/p&gt;

&lt;p&gt;This means AI doesn't just create more bugs on average — it occasionally creates absolute disasters. Code that's so broken it shouldn't have made it past the IDE, let alone into production.&lt;/p&gt;

&lt;p&gt;And because we're shipping faster, we're hitting these edge cases more often. It's not a theoretical risk. It's a ticking time bomb in your codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Hard to Fix
&lt;/h2&gt;

&lt;p&gt;The obvious answer is "just review AI code more carefully". But that misses the psychological trap we've fallen into.&lt;/p&gt;

&lt;p&gt;AI-generated code &lt;em&gt;feels&lt;/em&gt; trustworthy because it's consistently formatted, confidently written, and superficially correct. It doesn't have the telltale signs of junior code like hesitant variable names, inconsistent style, obvious copy-paste errors.&lt;/p&gt;

&lt;p&gt;So your brain does a pattern match: "this looks like senior-level code" → "probably fine" → approve.&lt;/p&gt;

&lt;p&gt;Except it's not senior-level code. It's &lt;em&gt;senior-looking&lt;/em&gt; code generated by a system that has no mental model of what it's building.&lt;/p&gt;

&lt;p&gt;The traditional markers we use for code quality — structure, naming, formatting — have been decoupled from actual correctness. We need new heuristics, and we haven't built them yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;p&gt;I'm not going to tell you to stop using AI assistants. I use them daily. They're legitimately useful for scaffolding, refactoring, and handling boilerplate.&lt;/p&gt;

&lt;p&gt;But here's what I've learned:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Ground the model in your context&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't just paste code into ChatGPT and expect it to understand your domain. Give it your architecture docs. Your API contracts. Your error handling conventions. The more context you provide, the less it has to guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Treat AI output as untrusted by default&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Would you merge code from a contractor you've never worked with before without thorough review? No? Then don't do it for AI.&lt;/p&gt;

&lt;p&gt;Any code touching auth, payments, PII, or critical business logic gets manual review. No exceptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Automate what AI gets wrong&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI is terrible at formatting, naming consistency, and basic security checks. So automate those with linters, formatters, and SAST tools in your CI pipeline.&lt;/p&gt;

&lt;p&gt;Don't waste human review time catching things machines can catch. Use humans for semantic review; does this actually solve the problem correctly?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Use independent review tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is CodeRabbit's obvious pitch, but it's also correct: don't use the same AI that generated code to review it. That's like asking someone to grade their own homework.&lt;/p&gt;

&lt;p&gt;Independent static analysis, security scanners, and code review tools catch different classes of bugs than generative models do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Accept the maintenance tax&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI-assisted development is a trade-off: velocity now, maintenance cost later. If you're not willing to pay down the technical debt, don't take on the loan.&lt;/p&gt;

&lt;p&gt;Budget time for refactoring. Expect to fix edge cases in production. Plan for the eventual rewrite when the generated code becomes unmaintainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Question
&lt;/h2&gt;

&lt;p&gt;The uncomfortable truth isn't that AI generates bugs. It's that &lt;strong&gt;we're willing to accept more bugs in exchange for speed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's a legitimate trade-off in some contexts. Early-stage startups trying to find product-market fit? Ship fast, fix later. Mature companies with millions of users and regulatory compliance? Maybe slow down.&lt;/p&gt;

&lt;p&gt;But let's be honest about what we're doing. We're not "augmenting developer productivity" in a risk-free way. We're shifting the risk/reward curve.&lt;/p&gt;

&lt;p&gt;More features, faster iteration, shorter cycle times; at the cost of more incidents, more maintenance burden, and more time spent debugging.&lt;/p&gt;

&lt;p&gt;Is that worth it? Depends on your context. But you can't answer that question if you're not measuring the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens Next
&lt;/h2&gt;

&lt;p&gt;AI coding assistants aren't going away. They're getting better. Context windows are expanding. Models are learning from feedback. The tooling is improving.&lt;/p&gt;

&lt;p&gt;But the fundamental problem remains: &lt;strong&gt;LLMs are pattern-matching engines, not reasoning systems&lt;/strong&gt;. They don't understand your invariants. They don't trace execution paths. They don't think about failure modes.&lt;/p&gt;

&lt;p&gt;They surface-fit syntax without semantic understanding.&lt;/p&gt;

&lt;p&gt;Until that changes, AI-generated code will always carry a quality tax. The question is whether we're willing to pay it; and whether we're honest about the price.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI code creates 1.7x more issues than human code across logic, security, performance, and maintainability&lt;/li&gt;
&lt;li&gt;The worst AI PRs (90th percentile) contain 26 issues vs 12 for humans; catastrophic failure modes are real&lt;/li&gt;
&lt;li&gt;Teams are shipping 20% more PRs but seeing 23.5% more production incidents&lt;/li&gt;
&lt;li&gt;AI optimizes for "looks correct" not "is correct"; it passes shallow tests but fails on edge cases&lt;/li&gt;
&lt;li&gt;Effective mitigation requires grounding models in context, automating quality checks, and treating AI output as untrusted&lt;/li&gt;
&lt;li&gt;The velocity gain is real, but so is the maintenance cost; choose consciously&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Full CodeRabbit report: &lt;a href="https://www.coderabbit.ai/whitepapers/state-of-AI-vs-human-code-generation-report" rel="noopener noreferrer"&gt;https://www.coderabbit.ai/whitepapers/state-of-AI-vs-human-code-generation-report&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>devops</category>
      <category>ai</category>
      <category>development</category>
    </item>
    <item>
      <title>AI Productivity Tips 1 - SQL Companion Bot with Gemini 1.5 Pro</title>
      <dc:creator>Çalgan Aygün</dc:creator>
      <pubDate>Sun, 25 Aug 2024 10:27:36 +0000</pubDate>
      <link>https://dev.to/calganaygun/ai-productivity-tips-1-sql-companion-bot-with-gemini-15-pro-5g2c</link>
      <guid>https://dev.to/calganaygun/ai-productivity-tips-1-sql-companion-bot-with-gemini-15-pro-5g2c</guid>
      <description>&lt;p&gt;In my current role, I often need to analyze database records and evaluate user performance metrics to identify anomalies. However, manually writing these one-off queries across multiple tables isn't efficient, especially when dealing with complex joins. To streamline this process, I turned to AI.&lt;/p&gt;

&lt;p&gt;Here’s a step-by-step guide on how to boost your productivity using AI for SQL queries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start by navigating to &lt;a href="https://aistudio.google.com/" rel="noopener noreferrer"&gt;Google AI Studio&lt;/a&gt; and create a new chat prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm9f28cn6ml538h14fqas.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm9f28cn6ml538h14fqas.png" alt="Google AI Studio Snapshot" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Next, set up a SQL Helper system prompt. Provide your database schema in the system prompt, also specify the RDBMS you’re using so that the AI can generate queries with the correct syntax. I tested this with &lt;a href="https://db-book.com/university-lab-dir/sqljs.html" rel="noopener noreferrer"&gt;The DB Book&lt;/a&gt;'s schema.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s an example system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Act as a DB Admin/SQL Developer and generate the requested SQL queries based on the following DDL/Schema. Provide explanations and ensure that the queries are safe to run in a production environment. Add "Do not forget to review the SQL query before running." to all SQL responses.

RDBMS/SQL Syntax: &amp;lt;Your RDBMS&amp;gt;

My database DDL: &amp;lt;Your database and table DDLs&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Now, ask your query! For example, I asked: &lt;code&gt;I need to identify the students who are enrolled in a course taught by Teacher 1 and list all their other courses in a comma-separated format in a column.&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And just like that, the AI (thanks to Gemini 1.5 Pro) generated the query I needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7572ng29ty4qrzpk1l4j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7572ng29ty4qrzpk1l4j.png" alt="Chat Snapshot" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>sql</category>
      <category>gemini</category>
      <category>productivity</category>
      <category>googlecloud</category>
    </item>
  </channel>
</rss>
