Secure Your CI/CD Supply Chain: 12 Steps, 90 Min [2026]

On March 19, 2026, a threat group tracked as TeamPCP force-pushed malicious code over 76 of 77 version tags in the aquasecurity/trivy-action GitHub Action and all seven tags in setup-trivy, then slipped a trojanized Trivy binary (v0.69.4) into official release channels for roughly three hours. The incident, tracked as CVE-2026-33634 with a CVSS v4.0 score of 9.4, went on to compromise cloud infrastructure at the European Commission and spread through more than 140 downstream package artifacts, according to SecurityWeek’s reporting on the breach. The attack didn’t exploit a bug in application code at all. It exploited the trust every pipeline places in a version tag.

That’s the uncomfortable truth about software supply chain security in 2026: the CI/CD pipeline itself, not just the code it builds, is now the target. This tutorial walks through hardening a GitHub Actions pipeline end to end, using the exact techniques that would have blocked or contained the Trivy compromise: commit SHA pinning, OIDC-based authentication, least-privilege tokens, SLSA provenance, and Sigstore/cosign artifact signing. By the end you’ll have a complete, working hardened pipeline you can adapt to your own repository.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why Your CI/CD Pipeline Is the New Attack Surface

Software supply chain security used to mean checking your dependencies for known CVEs and calling it done. That model is outdated. Modern pipelines pull in dozens of third-party GitHub Actions, each one able to read your repository secrets, write to your registries, and execute arbitrary code on every push. When Microsoft’s security team published its guidance on detecting the Trivy compromise, the core lesson was that attackers didn’t need to breach any individual company. They only needed write access to one popular Action’s release tags, and thousands of downstream pipelines inherited the compromise automatically on their next run.

GitHub itself has been explicit about the mechanics. As GitHub’s own documentation puts it, “tags and branches are mutable references — a malicious actor who gains write access to an action’s repository can silently move a tag (e.g., @v4) to a compromised commit, executing arbitrary code in your workflow (a supply chain attack).” That single sentence explains why a workflow line as innocuous as uses: some-org/some-action@v4 is a liability, not a convenience.

The Trivy incident is not an isolated case. It follows a pattern seen in the 2026 North Korea-linked npm hijack and other supply chain attacks reshaping cyber risk management, where attackers target the distribution mechanism rather than the target organization directly. GitHub’s security team has said plainly that “supply chain attacks chain together several weaknesses, and there is no single security capability that can stop them” — which is exactly why this tutorial layers multiple independent controls rather than relying on one fix.

Software supply chain security searches have climbed steadily through 2026 as more teams realize their CI/CD systems, not just their production servers, need the same hardening discipline as internet-facing infrastructure. If you already run a GitHub Actions CI/CD pipeline, everything in this guide layers on top of what you have. If you generate an SBOM or run vulnerability scans as part of your build, those tools become far more trustworthy once the pipeline producing them is hardened first.

Prerequisites: Tools, Versions, and Access You Need

Before starting, confirm you have the following. Version numbers matter here more than usual, since supply chain tooling moves fast and older releases sometimes carry the very vulnerabilities you’re trying to defend against.

  • A GitHub repository with Actions enabled and at least Maintain-level access to change branch protection rules
  • GitHub CLI (gh) version 2.68 or later, authenticated with gh auth login
  • Git 2.43 or later installed locally
  • Cosign 2.4 or later (Sigstore’s signing tool) — install via brew install cosign or the official binary release
  • Syft or Trivy for SBOM generation (either works; use whichever your team has already standardized on)
  • Docker or Podman for building and testing container images locally
  • A cloud provider account (AWS, Azure, or GCP) if you plan to replace static secrets with OIDC federation — this tutorial uses AWS as the example
  • Admin rights on the repository or organization to configure OIDC trust policies and required reviewers
  • Roughly 90 minutes for a single repository; add time per additional repository if you’re rolling this out fleet-wide

None of the tools above require a paid license. Cosign, Sigstore, SLSA tooling, and OpenSSF Scorecard are all open source, which matters because supply chain security shouldn’t be gated behind an enterprise contract — the attack surface exists whether you’re a solo maintainer or a Fortune 500 platform team.

Step 1: Audit Every Workflow File in Your Repository

Start by finding every workflow file and every third-party Action reference across your repository. Most teams are surprised by how many they’ve accumulated. Run this from your repository root to list every uses: line in every workflow:

find .github/workflows -name "*.yml" -o -name "*.yaml" | xargs grep -n "uses:" | sort -u

Output looks something like this:

.github/workflows/build.yml:12:      - uses: actions/checkout@v4
.github/workflows/build.yml:15:      - uses: aquasecurity/[email protected]
.github/workflows/deploy.yml:9:       - uses: aws-actions/configure-aws-credentials@v4
.github/workflows/release.yml:22:     - uses: docker/build-push-action@v6

For every line, note whether it’s pinned to a tag (like @v4), a branch, or a commit SHA. Anything using a tag or branch is a mutable reference and a candidate for hijacking, exactly the mechanism used in the Trivy compromise where 76 of 77 tags on trivy-action were force-pushed to point at malicious commits.

Step 2: Inventory Third-Party Actions and Rate Their Trust

Not every third-party Action carries the same risk. GitHub-owned Actions (actions/checkout, actions/upload-artifact) have a large maintenance team and strong release hygiene. A single-maintainer Action published two years ago with no recent commits is a different risk profile entirely. For each Action in your inventory, check:

  • Publisher identity — is it verified as a GitHub Verified Creator?
  • Commit and release frequency — actively maintained, or dormant?
  • Number of dependents — widely used Actions get more scrutiny from the community, but are also higher-value targets
  • Whether the Action requires write access to secrets or just reads repository content
  • Whether a security advisory has ever been filed against it (check the Security tab on its GitHub repo)

This inventory step is what would have flagged the Trivy dependency early: trivy-action and setup-trivy are exactly the kind of high-adoption, security-tooling Actions that deserve the strictest pinning and monitoring, precisely because a compromise there cascades into every pipeline that uses them for scanning.

Step 3: Pin GitHub Actions to a Full-Length Commit SHA

This is the single highest-leverage change in this entire tutorial. GitHub’s own CI/CD best-practices documentation states it directly: “always pin to a full-length commit SHA for maximum security and immutability.” A tag can be moved. A commit SHA cannot — it’s a cryptographic hash of the exact file tree at that commit, so pointing to it guarantees you get the code you reviewed and nothing else.

To find the SHA for a given tag, use the GitHub CLI:

gh api repos/aquasecurity/trivy-action/git/refs/tags/0.28.0 --jq '.object.sha'
# output: 6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5

Then update your workflow to reference the SHA, keeping the version number as a trailing comment so humans can still read intent:

# Before — mutable tag, vulnerable to tag hijacking
- uses: aquasecurity/[email protected]

# After — immutable commit SHA, with version noted for readability
- uses: aquasecurity/trivy-action@6c175e9c4083a92bbca2f9724c8a5e33bc2d97a5 # v0.28.0

Do this for every third-party Action across every workflow file. It’s tedious by hand across dozens of files, so use Dependabot to automate the ongoing maintenance: Dependabot supports SHA-pinned Actions and will open pull requests to bump the SHA (with the version comment) whenever a new release ships, so you get the security benefit without losing update visibility.

Before moving on, it helps to see the three reference types side by side, since the difference between them is the entire reason the Trivy compromise worked.

Reference TypeExampleMutable?Attack ScenarioRecommendation
Branch@mainYes, changes on every commitAny merge to the branch runs immediately in your pipeline, reviewed or notNever use for third-party Actions
Tag@v4Yes, can be force-pushed to a new commitExactly the Trivy incident — 76 of 77 tags redirected to malicious commitsAvoid for anything security-sensitive
Commit SHA@6c175e9c...No, cryptographically fixedNone — the SHA always resolves to the exact reviewed codeRequired for every third-party Action

This is why security teams increasingly treat tag-based Action references as a finding during code review, the same way they’d flag a hardcoded password. A tag looks pinned. It isn’t.

Step 4: Replace Long-Lived Secrets With OIDC

Static cloud credentials stored as repository secrets are a second major liability. If any workflow or Action is compromised, those secrets are exposed for as long as they remain valid, which for most teams is indefinitely. OpenID Connect (OIDC) federation replaces static keys with short-lived tokens minted per workflow run, so there’s nothing long-lived for an attacker to steal in the first place.

Here’s a GitHub Actions job that assumes an AWS IAM role via OIDC instead of using stored access keys:

permissions:
  id-token: write   # required for OIDC token minting
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.2.2
      - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1
          # no access-key-id or secret-access-key — token is short-lived and scoped per run

The IAM role’s trust policy restricts which repository and branch can assume it, so even a fully compromised workflow can’t mint a token for a role it isn’t explicitly trusted to use. Azure and GCP support the equivalent pattern through Workload Identity Federation — the setup differs slightly but the underlying principle (no static secret, short-lived scoped token, per-run) is identical across providers.

Step 5: Restrict GITHUB_TOKEN Permissions to the Minimum

Every workflow run gets an automatically generated GITHUB_TOKEN, and by default it often has far more scope than any given job actually needs — including write access to repository contents, packages, and pull requests. Set the default to read-only at the repository level, then grant elevated scope only where a specific job genuinely requires it.

# At the top of the workflow file — restrict by default
permissions:
  contents: read

jobs:
  build:
    permissions:
      contents: read      # this job only needs to read code
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.2.2

  publish-release:
    permissions:
      contents: write      # only this job needs write access, and only for this purpose
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.2.2

You can also enforce this org-wide under Settings → Actions → General, setting the default workflow permissions to read-only. This is a one-time change that pays off every single time a compromised or buggy Action runs in your pipeline going forward, since it caps the blast radius even if a step is malicious.

Step 6: Require Reviews for Workflow File Changes

A pipeline can be perfectly hardened today and quietly weakened tomorrow if anyone can merge a change to .github/workflows/ without review. Add a CODEOWNERS entry that requires security or platform team sign-off on any change to workflow files, and enable branch protection so that approval is mandatory, not optional.

# .github/CODEOWNERS
/.github/workflows/  @your-org/platform-security-team

Then enable the corresponding branch protection rule:

gh api repos/your-org/your-repo/branches/main/protection \
  --method PUT \
  -f required_pull_request_reviews[require_code_owner_reviews]=true \
  -f required_pull_request_reviews[required_approving_review_count]=1 \
  -f enforce_admins=true

This closes off one of the quieter attack paths: an attacker who compromises a contributor’s account (through phishing or a stolen token) but lacks CODEOWNERS approval rights still can’t silently modify what your pipeline trusts or executes.

Step 7: Harden the Runner With Egress Allowlisting

Even a fully pinned, least-privilege pipeline can still exfiltrate secrets if a compromised dependency makes an outbound network call to an attacker-controlled server during the build. Runner hardening tools like StepSecurity’s Harden-Runner intercept and restrict outbound network traffic during a workflow run, blocking connections to anything not explicitly allowlisted.

steps:
  - uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
    with:
      egress-policy: block
      allowed-endpoints: >
        github.com:443
        api.github.com:443
        registry.npmjs.org:443
        pypi.org:443

Run it in audit mode first (egress-policy: audit) for a week to see what your pipeline normally contacts, then switch to block once you’ve built a complete allowlist. This single control would have limited how far a compromised dependency’s credential-stealing payload could actually phone home during the exposure window, even if it made it into the pipeline undetected.

Step 8: Generate SLSA Provenance for Every Build

SLSA (Supply-chain Levels for Software Artifacts) provenance is a signed, verifiable statement describing exactly how an artifact was built: which source commit, which workflow, which builder. It turns “trust me, this binary is legitimate” into a cryptographically checkable claim. GitHub’s native attest-build-provenance Action generates SLSA-compliant provenance with almost no extra configuration:

permissions:
  id-token: write
  attestations: write
  contents: read

steps:
  - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.2.2
  - name: Build artifact
    run: make build
  - uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
    with:
      subject-path: 'dist/myapp-binary'

Once generated, provenance is stored alongside your artifact and can be independently verified by anyone downstream, including automated deployment gates, before the artifact is trusted enough to run in production.

Step 9: Sign Container Images and Artifacts With Cosign

Provenance tells you how something was built. A signature tells you it hasn’t been tampered with since. Cosign, part of the Sigstore project, supports keyless signing using your CI identity itself as the signing credential, so there’s no private key to generate, rotate, or accidentally leak.

permissions:
  id-token: write
  packages: write

steps:
  - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fbe5a2d5e56e5be # v3.7.0
  - name: Sign the container image (keyless)
    run: |
      cosign sign --yes ghcr.io/your-org/your-image@${{ steps.build.outputs.digest }}

Cosign records the signature and a transparency log entry in Rekor, Sigstore’s public, append-only log, so the signing event itself is auditable and can’t be quietly rewritten after the fact. This is exactly the class of control that would let downstream consumers of a tool like Trivy detect that a given binary wasn’t actually signed by the legitimate release pipeline.

Step 10: Enforce Signature Verification Before Deployment

Signing is only half the control. It has to be paired with mandatory verification at deploy time, or an attacker who bypasses your build pipeline entirely (by pushing directly to a registry, for example) faces no additional friction. Add a verification gate as a required step before any deployment job runs:

steps:
  - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fbe5a2d5e56e5be # v3.7.0
  - name: Verify image signature before deploy
    run: |
      cosign verify \
        --certificate-identity="https://github.com/your-org/your-repo/.github/workflows/build.yml@refs/heads/main" \
        --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
        ghcr.io/your-org/your-image@${{ steps.build.outputs.digest }}

If the image wasn’t signed by that exact workflow identity, cosign verify exits non-zero and the deployment job fails before anything unverified reaches production. For Kubernetes environments, the same policy can be enforced cluster-wide with an admission controller like Kyverno or Sigstore’s own policy-controller, so the check happens even for deployments that bypass your CI entirely.

Step 11: Monitor Continuously With OpenSSF Scorecard and Zizmor

Hardening a pipeline once is not the same as keeping it hardened. New Actions get added, permissions creep back up, and dependencies update on their own schedule. Two open source tools automate ongoing checks:

  • OpenSSF Scorecard scores your repository against supply chain security best practices (branch protection, pinned dependencies, vulnerability disclosure, and more) and can run automatically on a schedule, publishing results as a badge
  • Zizmor is a static analysis tool built specifically for GitHub Actions workflows — it flags unpinned Actions, dangerous pull_request_target usage, script injection risks in run: steps, and overly broad permissions
# Run zizmor locally or in CI against all workflow files
pip install zizmor
zizmor .github/workflows/

Wire both into a scheduled workflow (weekly is reasonable) so drift gets caught automatically rather than during the postmortem after the next incident. Combine this with regular vulnerability scanning of your dependencies so both the code and the pipeline building it stay under continuous review.

Step 12: Write an Incident Response Runbook for a Compromised Action

Even a well-hardened pipeline needs a documented response plan, because the Trivy incident showed how fast these events move — the malicious binary window was open for roughly three hours and the compromised Docker images for about ten. A runbook should cover, at minimum:

  • How to identify which of your workflows reference the compromised Action, using the inventory from Step 1 and Step 2
  • How to immediately pin or roll back to the last known-safe SHA across all affected repositories
  • How to rotate every secret and OIDC role that any affected workflow could have touched, on the assumption it may be compromised
  • How to check Rekor’s transparency log and your own build logs for anomalous signing or artifact-publishing events during the exposure window
  • Who owns the decision to halt all pipeline runs organization-wide versus isolating the affected repositories only

Test this runbook against a tabletop scenario at least twice a year, treating a hypothetical compromised Action the same way you’d treat a ransomware tabletop exercise. Speed of response is what separated organizations that contained the Trivy incident quickly from the European Commission’s cloud infrastructure, which was breached because a compromised credential from the initial window went unrotated.

The Complete Hardened Pipeline: A Working Example

Putting every step together, here’s a complete GitHub Actions workflow that builds, signs, attests, and verifies a container image using every control covered above. Save this as .github/workflows/secure-build.yml:

name: Secure Build and Deploy

on:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  build-sign-attest:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      attestations: write
      packages: write
    steps:
      - name: Harden the runner
        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
        with:
          egress-policy: block
          allowed-endpoints: >
            github.com:443
            api.github.com:443
            ghcr.io:443
            *.docker.io:443

      - name: Checkout
        uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.2.2

      - name: Build container image
        id: build
        run: |
          docker build -t ghcr.io/your-org/your-image:${{ github.sha }} .
          docker push ghcr.io/your-org/your-image:${{ github.sha }}
          echo "digest=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/your-org/your-image:${{ github.sha }})" >> "$GITHUB_OUTPUT"

      - name: Install cosign
        uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fbe5a2d5e56e5be # v3.7.0

      - name: Sign image (keyless)
        run: cosign sign --yes "${{ steps.build.outputs.digest }}"

      - name: Generate SLSA provenance
        uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
        with:
          subject-name: ghcr.io/your-org/your-image
          subject-digest: ${{ steps.build.outputs.digest }}
          push-to-registry: true

  deploy:
    needs: build-sign-attest
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Install cosign
        uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fbe5a2d5e56e5be # v3.7.0

      - name: Verify signature before deploy
        run: |
          cosign verify \
            --certificate-identity="https://github.com/${{ github.repository }}/.github/workflows/secure-build.yml@refs/heads/main" \
            --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
            ghcr.io/your-org/your-image:${{ github.sha }}

      - name: Assume deployment role via OIDC
        uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1

      - name: Deploy
        run: ./scripts/deploy.sh ghcr.io/your-org/your-image:${{ github.sha }}

Every job runs with least-privilege permissions, every third-party Action is SHA-pinned, secrets never touch the pipeline as static values, and the deployment job refuses to run against an image it can’t cryptographically verify. This is the baseline every internet-facing pipeline should reach in 2026, not an aspirational target reserved for large security teams.

Supply Chain Security Tool Comparison

Each tool in this tutorial solves a distinct piece of the problem. Here’s how they stack up so you can decide what to adopt first if you’re rolling this out incrementally.

ToolSolvesLicenseSetup EffortBest First Step
SHA pinning + DependabotMutable tag hijackingFree / built-inLowYes — do this first
OIDC federationLong-lived secret exposureFree / built-inMediumYes — second priority
Cosign / SigstoreTamper detection on artifactsApache 2.0, freeMediumThird
SLSA provenance (attest-build-provenance)Build origin verificationFree / built-inLowPairs with cosign
StepSecurity Harden-RunnerRuntime exfiltration via network egressFree tier availableMediumFourth
OpenSSF ScorecardOngoing posture driftApache 2.0, freeLowFifth — automate and forget
ZizmorStatic workflow misconfigurationsMIT, freeLowRun alongside Scorecard

Notice that every tool here is free and open source. Supply chain security in 2026 isn’t gated by budget — it’s gated by whether teams take the time to wire these controls into pipelines that were built for speed, not scrutiny.

The Trivy Compromise, Component by Component

Understanding exactly how the March 2026 incident unfolded makes the case for each control in this tutorial concrete. Here’s the exposure timeline as documented by multiple security vendors tracking the incident.

ComponentAffected VersionExposure WindowSafe Version
Trivy binaryv0.69.4~3 hours (Mar 19)v0.69.2 – v0.69.3 or later clean releases
Trivy Docker Hub imagesv0.69.5, v0.69.6, latest tag~10 hours (Mar 22–24)v0.69.3 or earlier / later clean tags
trivy-action GitHub ActionTags 0.0.1 – 0.34.2 (76 of 77 tags force-pushed)~12 hours (Mar 19–20)v0.35.0, pinned via commit SHA
setup-trivy GitHub ActionAll 7 published tagsConcurrent with trivy-actionv0.2.6, pinned via commit SHA

Every single one of these compromises worked specifically because downstream pipelines referenced mutable tags rather than immutable SHAs. Teams that had already pinned their Trivy and Action references to a commit hash before March 19 continued running the safe, reviewed version throughout the entire incident window, completely unaffected by the tag force-push. That’s not a coincidence — it’s the entire point of Step 3 in this tutorial.

If you’re currently using Trivy for container scanning and want a refresher on safe, up-to-date setup alongside this hardening work, see our guide on scanning container images with Trivy — just make sure whatever version you reference is pinned per Step 3 above.

How Supply Chain Hardening Fits Your Broader Security Program

None of the controls in this tutorial replace the rest of your security program — they close a gap that traditional application security tooling was never designed to cover. A static application security testing tool reviews your code. A container scanner reviews your dependencies. Neither one checks whether the pipeline assembling those pieces together can be trusted, which is exactly the blind spot the Trivy compromise exploited.

If your team already maintains a formal vulnerability management program, the pipeline itself belongs in scope alongside your production servers and application code. Treat .github/workflows/ files as security-relevant configuration, not build plumbing, and bring them into the same patch and review cadence you’d apply to a public-facing service. The same logic applies to secrets: if you’re already following a process for how to stop leaked secrets from reaching your Git history with Gitleaks, extend that discipline to the OIDC trust policies and role definitions described in Step 4, since a misconfigured trust policy is functionally the same risk as a leaked static key.

Kubernetes-based deployment targets deserve particular attention here. If your deploy job assumes a role or reads a secret to reach a cluster, the same least-privilege principles from securing Kubernetes secrets should extend to how that pipeline authenticates in the first place — there’s little value hardening secrets at rest inside a cluster if the pipeline deploying to that cluster still authenticates with a static, broadly scoped credential. And once your patching process already follows a CISA KEV-driven patch workflow for known exploited vulnerabilities, add a parallel trigger: any CVE affecting a GitHub Action, build tool, or CI dependency you depend on should route through the same urgency tiering, since CVE-2026-33634 proved that CI tooling itself can land on that list with the same severity as an internet-facing appliance.

The organizations that weathered the Trivy incident with the least disruption were, almost without exception, the ones that had already folded pipeline security into their existing vulnerability and secrets management processes rather than treating it as a separate, optional initiative. Supply chain security isn’t a new discipline bolted onto DevOps — it’s an extension of security practices most teams already run, applied to a part of the stack that’s historically gotten far less scrutiny than it deserves.

Common Pitfalls When Hardening a Supply Chain

Teams rolling this out for the first time tend to trip over the same handful of mistakes. Watch for these specifically.

  • Pinning the Action but not its transitive dependencies. A SHA-pinned composite Action can still call unpinned Actions internally. Check what the Action itself references before trusting the pin fully.
  • Forgetting to update the version comment when bumping a SHA. Without Dependabot automation, teams often update the hash but leave a stale version comment, which makes future audits confusing and error-prone.
  • Granting id-token: write at the workflow level instead of the job level. This gives every job in the workflow OIDC minting rights, even ones that don’t need cloud access, unnecessarily widening the blast radius.
  • Treating OIDC role trust policies too loosely. A trust policy scoped to an entire organization rather than a specific repository and branch defeats much of the purpose — any repo in the org could then assume the deployment role.
  • Signing images but never enforcing verification. Cosign signing without a mandatory cosign verify gate before deployment is security theater — the signature exists but nothing actually checks it.
  • Running Harden-Runner in block mode without an audit period first. This breaks builds unpredictably when a legitimate dependency fetch gets blocked, and teams often disable the tool entirely in frustration rather than fixing the allowlist.

Most of these pitfalls share a root cause: partial implementation. Pinning without OIDC, or signing without verification, leaves the exact gap an attacker needs. Layer the controls as a set, not a menu.

Troubleshooting Common Supply Chain Security Errors

Expect to hit a few of these while rolling out the hardened pipeline. Here’s how to resolve them.

  • “Error: Resource not accessible by integration” after restricting GITHUB_TOKEN. A step needs a permission scope you removed. Add the specific scope (e.g., pull-requests: write) to that job only, not the whole workflow.
  • “Error: Could not assume role with OIDC” on AWS. The IAM role’s trust policy condition on token.actions.githubusercontent.com:sub almost always doesn’t match your actual repository and branch string exactly — check for typos and case sensitivity.
  • Cosign sign fails with “getting signer: getting key from Fulcio.” Keyless signing requires id-token: write permission on the job; confirm it’s present and that you’re running in an actual CI environment (not locally without a matching OIDC provider).
  • Harden-Runner blocks a legitimate package registry. Run in audit mode again, capture the blocked endpoint from the run summary, and add it explicitly to allowed-endpoints.
  • Dependabot doesn’t open PRs for SHA-pinned Actions. Confirm the version comment format matches exactly what Dependabot expects (@<sha> # <tag>), and that package-ecosystem: "github-actions" is present in dependabot.yml.
  • cosign verify fails with “no matching signatures” after a rebuild. The image digest changed on rebuild but you’re verifying against a stale digest reference; always pass through the digest output from the build step rather than a cached value.
  • SLSA attestation upload fails with a permissions error. The job needs both attestations: write and id-token: write simultaneously — one without the other fails silently in some GitHub Actions runner versions.
  • Branch protection blocks your own emergency hotfix. This is by design during an incident response scenario — use a documented break-glass procedure with a second approver rather than disabling protection outright, even under time pressure.

Advanced Tips for SLSA Level 3 and Keyless Signing

Once the baseline pipeline above is running cleanly, a few additional steps push your posture toward SLSA Level 3, the tier most enterprise procurement teams now ask about explicitly.

  • Use a hosted, reusable build workflow rather than inline build steps in every repository. SLSA Level 3 requires the build platform itself to generate provenance, which is easier to guarantee from a single centrally maintained reusable workflow than dozens of ad hoc ones.
  • Enforce provenance verification at admission, not just deploy time. Tools like Sigstore’s policy-controller or Kyverno can block any Kubernetes deployment lacking a valid, matching SLSA attestation, closing the gap for anyone who tries to deploy manually.
  • Restrict your OIDC issuer’s trust to specific workflow file paths, not just repository and branch. GitHub’s OIDC claims support matching on the exact workflow filename, so a compromised unrelated workflow in the same repo still can’t assume your deployment role.
  • Sigstore’s keyless signing never leaves a long-lived private key to rotate — audit the Rekor transparency log periodically instead, looking for signing events from identities you don’t recognize.
  • Apply the same hardening to your Action’s own release pipeline if you publish any Actions or packages yourself. The Trivy incident is a reminder that if you distribute software to others, you are someone else’s supply chain, and the same SHA-pinning and OIDC principles apply to your release process too.

Frequently Asked Questions

What is a software supply chain attack in CI/CD?
It’s an attack that targets the tools, dependencies, or Actions your build pipeline trusts, rather than your application code directly. The 2026 Trivy compromise is a clear example: attackers didn’t touch any company’s source code, they compromised the scanning tool and GitHub Actions that thousands of companies’ pipelines pulled in automatically.

Is pinning GitHub Actions to a SHA enough to stop supply chain attacks?
It’s the single most effective individual control, but not sufficient alone. GitHub’s own guidance notes that supply chain attacks chain together multiple weaknesses, so pinning should be paired with least-privilege permissions, OIDC, and signature verification for defense in depth.

What’s the difference between SLSA provenance and a Sigstore signature?
Provenance describes how an artifact was built (source, builder, workflow). A signature proves the artifact hasn’t been altered since it was signed. They’re complementary — provenance answers “was this built the right way,” and signing answers “is this the exact thing that was built.”

Do I need a paid tool to sign container images, or is cosign free?
Cosign and the broader Sigstore project are open source under the Apache 2.0 license, and keyless signing via Fulcio and Rekor is free to use. There’s no cost barrier to adopting this control.

How do I know if my organization was affected by the Trivy CVE-2026-33634 incident?
Check whether any workflow referenced the Trivy binary version v0.69.4, Docker Hub images tagged v0.69.5, v0.69.6, or latest between March 22–24, 2026, or trivy-action/setup-trivy by tag (rather than SHA) during the exposure windows documented by Aqua Security’s advisory. If any of these apply, rotate any secrets those pipelines could access.

What is OpenSSF Scorecard and how is it different from Dependabot?
Dependabot flags outdated or vulnerable dependencies and Actions. Scorecard evaluates your repository’s overall supply chain security posture — branch protection, pinning practices, vulnerability disclosure policy — giving you a broader health score rather than individual dependency alerts.

Can I use OIDC with providers other than AWS?
Yes. Azure supports Workload Identity Federation and GCP supports Workload Identity Federation for GitHub Actions, both following the same pattern of exchanging a GitHub-issued OIDC token for a short-lived cloud credential without any static secret.

How long does it take to harden an existing CI/CD pipeline?
Budget around 90 minutes for a single repository following this tutorial in full. Pinning Actions and restricting GITHUB_TOKEN permissions can be done in under 30 minutes; OIDC migration and signature enforcement typically take longer since they touch cloud IAM configuration outside GitHub itself.

Nadia Dubois

Nadia Dubois

AI & Innovation Editor

Nadia Dubois is the AI & Innovation Editor at Tech Insider, where she tracks the rapid evolution of artificial intelligence, from foundation models to real-world enterprise deployment. She previously covered AI and startups for La Tribune and contributed to MIT Technology Review's European coverage. Nadia specializes in generative AI, AI regulation, and the intersection of technology and European industrial policy. She holds a dual degree in Computational Linguistics and Journalism from Sciences Po Paris.

View all articles