<?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: Luan Rodrigues</title>
    <description>The latest articles on DEV Community by Luan Rodrigues (@hardened).</description>
    <link>https://dev.to/hardened</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3848199%2F8c6b3106-5001-477a-979a-bbc9bbd6b335.jpg</url>
      <title>DEV Community: Luan Rodrigues</title>
      <link>https://dev.to/hardened</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hardened"/>
    <language>en</language>
    <item>
      <title>Your CI/CD Pipeline Is a Security Risk - Here's How I Fixed Mine</title>
      <dc:creator>Luan Rodrigues</dc:creator>
      <pubDate>Sat, 28 Mar 2026 23:37:18 +0000</pubDate>
      <link>https://dev.to/hardened/your-cicd-pipeline-is-a-security-risk-heres-how-i-fixed-mine-194a</link>
      <guid>https://dev.to/hardened/your-cicd-pipeline-is-a-security-risk-heres-how-i-fixed-mine-194a</guid>
      <description>&lt;p&gt;Most CI/CD pipelines are one compromised dependency away from a production takeover.&lt;/p&gt;

&lt;p&gt;I learned that the hard way after the Codecov breach.&lt;/p&gt;

&lt;p&gt;I spent a couple of weeks hacking on a PoC to see what actually holds up when the goal is simple: stop someone from nuking your prod environment.&lt;/p&gt;

&lt;p&gt;Here's what I ended up with and where it hurt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Branch protection is table stakes, but it's very easy to get wrong.
&lt;/h2&gt;

&lt;p&gt;I forced signed commits and mandatory approvals, even for admins. Yeah, it slows things down during fast iterations. But without it, a compromised runner can just rewrite your main history and push whatever it wants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let me paint the scenario that kept me up at night:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A compromised GitHub Action runs in your pipeline.&lt;br&gt;
It reads your &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; and &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sends them to an external server.&lt;/p&gt;

&lt;p&gt;A few seconds later, someone else is deploying to your production account.&lt;/p&gt;

&lt;p&gt;Game over.&lt;/p&gt;


&lt;h2&gt;
  
  
  Static secrets had to die - so I killed them
&lt;/h2&gt;

&lt;p&gt;I replaced every &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; with &lt;code&gt;OIDC (OpenID Connect)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now the runner requests a short-lived &lt;strong&gt;JWT&lt;/strong&gt;, and &lt;strong&gt;AWS STS exchanges&lt;/strong&gt; it for temporary credentials. No long-lived secrets sitting in GitHub anymore.&lt;/p&gt;

&lt;p&gt;Setting this up was a nightmare. The first error I got was an &lt;strong&gt;AccessDenied&lt;/strong&gt; that lasted 4 hours until I realized the Trust Policy wasn't accepting GitHub's audience. AWS documentation on OIDC feels like it was written by someone who's never debugged a 403 in their life.&lt;/p&gt;

&lt;p&gt;But once it works, it removes an entire class of problems. Even if a job gets compromised, the creds expire fast enough to limit the blast radius.&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&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="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;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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&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@v4&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;Configure AWS credentials via OIDC&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;aws-actions/configure-aws-credentials@v4&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;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/GitHubOIDCRole&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Tip: if you get "Access Denied",&lt;br&gt;
99% of the time it's your IAM Trust Policy not accepting&lt;br&gt;
GitHub's audience. I lost 3 hours on this.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  SBOMs were the easy part.
&lt;/h2&gt;

&lt;p&gt;Syft generates &lt;code&gt;SPDX/CycloneDX&lt;/code&gt; without much friction.&lt;/p&gt;

&lt;p&gt;Keyless signing with Cosign took more effort. The Sigstore flow isn't hard once it clicks, but it's definitely not obvious the first time. I had to re-read the docs three times to understand where the &lt;strong&gt;"key"&lt;/strong&gt; even comes from.&lt;/p&gt;

&lt;p&gt;Still, it beats GPG key management hell. No private key to rotate, no "who has the signing key" questions.&lt;/p&gt;

&lt;p&gt;The GitHub OIDC identity handles everything, and verification is just:&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="s"&gt;cosign verify \&lt;/span&gt;
  &lt;span class="s"&gt;--certificate-identity-regexp="^https://github.com/.*$" \&lt;/span&gt;
  &lt;span class="s"&gt;your-registry/image:tag&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;If verification fails with "no matching signatures",&lt;br&gt;
your identity is probably not mapped in the policy.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  gVisor: the container escape killer (but it comes with baggage)
&lt;/h2&gt;

&lt;p&gt;I didn't fully trust Docker isolation, so I tried running builds with gVisor (runsc).&lt;/p&gt;

&lt;p&gt;This part was painful. Self-hosted runner setup, cgroup issues, random "unsupported runtime" errors... I lost a few nights here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Volume mounts with specific configurations&lt;/li&gt;
&lt;li&gt;Anything touching /proc too aggressively&lt;/li&gt;
&lt;li&gt;Builds depending on uncommon syscalls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Debugging inside gVisor isn't fun either. When something fails, you're staring at cryptic errors that don't tell you whether it's your code or the sandbox.&lt;/p&gt;

&lt;p&gt;Performance takes a noticeable hit. I haven't &lt;strong&gt;benchmarked&lt;/strong&gt; properly, but builds are slower enough that you feel it during iteration.&lt;/p&gt;

&lt;p&gt;Here's my honest take: if your team is small and you're just deploying a simple &lt;strong&gt;Node.js API&lt;/strong&gt;, this whole setup is overkill. The engineering hours you'll spend maintaining &lt;code&gt;gVisor&lt;/code&gt; + &lt;code&gt;Falco&lt;/code&gt; + &lt;code&gt;Cosign&lt;/code&gt; might cost more than the actual risk.&lt;/p&gt;

&lt;p&gt;Use this if you run third-party code in CI, or if the data you handle is sensitive enough that standard GitHub Actions isolation doesn't cut it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Falco for runtime visibility
&lt;/h2&gt;

&lt;p&gt;Isolation alone didn't feel sufficient, so I added Falco for runtime monitoring.&lt;/p&gt;

&lt;p&gt;Default rules are noisy as hell. I'm still tuning them. But it already paid off.&lt;/p&gt;

&lt;p&gt;Seeing a Slack alert fire instantly when I poked at the Docker socket from inside a container was... reassuring.&lt;/p&gt;

&lt;p&gt;At least now I'm not completely blind if something weird happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security is always a tradeoff.
&lt;/h2&gt;

&lt;p&gt;OIDC removes the biggest footgun. Keyless signing makes provenance usable without key management hell. &lt;/p&gt;

&lt;p&gt;gVisor adds real isolation.&lt;/p&gt;

&lt;p&gt;But you pay for it in complexity and performance.&lt;/p&gt;

&lt;p&gt;This setup is far from perfect. It adds friction. It breaks things. It forces you to actually understand your pipeline.&lt;/p&gt;

&lt;p&gt;But here's the thing: &lt;strong&gt;if your CI/CD pipeline still depends on long-lived secrets&lt;/strong&gt;, you don't have automation.&lt;/p&gt;

&lt;p&gt;You have a liability.&lt;/p&gt;

&lt;p&gt;And honestly? The next breach is going to happen anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question is:&lt;/strong&gt;&lt;br&gt;
will your credentials still be valid when it does?&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>aws</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
