Stop trusting mutable references: how Eclipse Foundation projects should harden GitHub Actions after the Trivy compromise

On March 19, 2026, an attacker used compromised credentials to publish a malicious Trivy v0.69.4 release, force-push 76 of 77 version tags in aquasecurity/trivy-action, and replace all 7 tags in aquasecurity/setup-trivy with malicious commits. On March 22, Aqua reported malicious Docker Hub images for versions 0.69.5 and 0.69.6. The malicious payload ran before the legitimate scanning logic and then let the workflow proceed normally. Every affected workflow looked fine. None of them were.

This was not a scanner problem or a malware problem. It was a trust-resolution problem. Existing, trusted references were silently redirected. Workflows kept resolving “the same” tag to what was now an attacker’s code. If your pipeline consumes dependencies by mutable reference — a tag, a branch, a floating Docker image — you are trusting that no one with write access to that reference will ever be compromised. The Trivy incident shows what happens when that bet loses.

For projects hosted at the Eclipse Foundation, and for any open source project on GitHub, the recommendation from the Eclipse Foundation Security Team is straightforward: consume dependencies immutably, minimize the authority of every secret that reaches a runner, and add runtime visibility so you can tell when something goes wrong.

This post covers how to use GitHub Actions safely as a consumer of third-party actions. A follow-up post will address how to reduce the chance that your own project becomes the next upstream incident.

What we have already done

The Eclipse Foundation Security Team has scanned all Eclipse Foundation projects for signs of compromise related to the Trivy incident and contacted the projects identified as potentially exposed. We are working with those projects, together with the Release Engineering team, to rotate any potentially compromised secrets and strengthen protections against similar incidents in the future. What follows in this post is the broader guidance we are now recommending for all Eclipse Foundation projects — and for any open source project on GitHub — to adopt as baseline practice.

If you just need the checklist, skip to the baseline summary at the end.

Pin to commit SHAs, not tags

GitHub’s own guidance is unambiguous: pinning to a full-length commit SHA is the only way to consume an action as an immutable release. Tags are mutable. They can be moved if an attacker gains control of the action’s repository. That is exactly what the Trivy compromise exploited.

This:

- uses: actions/checkout@v5
- uses: aquasecurity/[email protected]

should become this:

- uses: actions/checkout@<full-commit-sha> # v5.0.0
- uses: aquasecurity/trivy-action@<full-commit-sha> # 0.28.0

Don’t do this by hand. pinact exists for exactly this purpose: it rewrites action references to commit SHAs and preserves the version as a comment. For Eclipse Foundation organizations already managed through Otterdog, the pin_workflow blueprint can pin actions and reusable workflows at organization scale.

A practical rollout:

# Initial pinning
pinact run

# Later, to update pinned versions
pinact run -u

Pinning is baseline hygiene, not an optional improvement.

Watch out for “unpinnable” actions

SHA pinning is necessary but not always sufficient. Some actions, even when pinned to a commit, pull mutable external dependencies at runtime — a Docker image tagged latest, a Dockerfile that runs pip install without locked versions, or a composite action that references other unpinned actions. The commit is frozen, but the code it executes is not.

BoostSecurity’s research found that 32% of the top-starred actions on GitHub Marketplace are “unpinnable” in this way. That means nearly a third of the most popular actions can still introduce new, unreviewed code into your pipeline even after you pin them.

poutine is a build-pipeline security scanner that detects exactly this class of problem. Its unpinnable_action rule flags actions whose pinning does not actually guarantee immutability. Run it against your organization:

poutine analyze_org my-org --token "$GH_TOKEN"

For actions flagged as unpinnable, you have three options: replace them with a pinnable alternative, vendor the action into your own organization so you control the full dependency chain, or replace the action with an inline run: step that does the same thing without the trust dependency.

Reduce your third-party action surface

Every third-party action is an additional trust boundary. Many workflows use five or six actions where two would suffice, or where a simple run: step would replace the action entirely. Before pinning everything, audit what you actually need.

A good example is peter-evans/create-pull-request, one of the most widely used third-party actions. It requires write access to your repository and runs a substantial amount of code to do something the GitHub CLI already does natively:

# Instead of this (third-party action with broad repo access):
- uses: peter-evans/create-pull-request@<sha>
  with:
    title: "Automated update"
    branch: "auto-update"

# Do this (zero third-party trust required):
- run: |
    git checkout -b auto-update
    git add .
    git commit -m "Automated update"
    gh pr create --title "Automated update" --body "Created by automation" --base main
  env:
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The gh CLI is pre-installed on GitHub-hosted runners. No action to pin, no action to audit, no action that can be compromised upstream. Apply this thinking to every third-party action in your workflows: if you can replace it with a few lines of shell, do it. Treat third-party actions like third-party libraries — each one should justify its presence.

Enforce pinning in policy, not documentation

A written rule that nobody enforces is not a control. GitHub supports requiring actions to be pinned to a full-length commit SHA at both repository and organization scope. When enabled, workflows that reference unpinned actions fail. GitHub also supports explicit allow and block patterns for actions, including !-prefixed deny entries.

For Eclipse Foundation projects, don’t configure this by hand in the UI. Otterdog already manages GitHub organizations through infrastructure-as-code and pull-request-driven changes. Define the policy once, review it through PRs, apply it declaratively.

One nuance: GitHub’s SHA-pinning enforcement applies to actions, but reusable workflows can still be referenced by tag. Review reusable workflow references with the same suspicion you apply to action references, and pin them where your tooling supports it.

Keep pins updated with Dependabot, but add an adoption delay

The usual objection to pinning is maintenance overhead. That objection stopped being strong years ago. GitHub has first-class Dependabot support for the github-actions ecosystem, and it now supports a cooldown option that delays version updates by a configurable number of days. Importantly, cooldown applies only to version updates, not security updates.

A 7-day cooldown is a sensible default. It avoids automatic adoption of a newly published tag or release during the most chaotic part of an upstream incident.

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    cooldown:
      default-days: 7

This is a time buffer, not a guarantee. Combined with SHA pinning, it gives maintainers time to review what they are about to trust instead of consuming whatever changed upstream that day.

Add runtime visibility with Harden-Runner

The Trivy payload ran before the legitimate scanner and then let the workflow complete normally. Prevention is not enough — you need runtime visibility into what the runner actually does: network calls, file access, process execution.

StepSecurity’s Harden-Runner provides this. It monitors actual runner behavior rather than reasoning from YAML alone. StepSecurity reported that Harden-Runner detected anomalous outbound connections from compromised trivy-action runs during the incident.

Start in audit mode:

steps:
  - uses: step-security/harden-runner@<full-commit-sha> # v2.x.y
    with:
      egress-policy: audit

Audit mode gives you telemetry and a recommended block policy based on observed outbound destinations. Once that baseline is stable, move selected jobs to block mode so unexpected outbound calls are denied, not just logged. Even audit mode is valuable for public open source projects because it dramatically improves incident triage.

Minimize token permissions and use OIDC for cloud access

A compromised action runs inside your trust boundary. If it can read broad secrets or use a powerful token, the incident stops being “about CI” and becomes about your releases, your repository, and your cloud accounts.

GitHub recommends setting the default GITHUB_TOKEN permission to read-only and elevating only for specific jobs. For cloud access, use OpenID Connect so workflows authenticate with short-lived, scoped tokens instead of stored secrets.

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read

  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write

The model is simple: assume one job may become hostile. Make sure that hostile job receives as little authority as possible. When there is no long-lived cloud credential to steal, runner compromise still hurts, but it hurts far less.

Segment high-value secrets into isolated workflows

Job-level permissions help, but a single workflow file with multiple jobs still shares some context. For your most sensitive secrets — signing keys, production deploy credentials, registry tokens — isolate them into separate, minimal workflows triggered via workflow_call or workflow_dispatch.

This way, a compromised upstream action in a build job cannot reach the signing job at all. The blast radius shrinks from “everything in the workflow” to “everything in one job in one workflow.”

Pin Docker images by digest, not tag

The Trivy incident included malicious Docker Hub images, but the blog post’s guidance applies beyond GitHub Actions tags. Any workflow that pulls container images by tag (image:latest, image:v1) faces the same mutable-reference problem.

Pin container images by digest:

# Not this
container: myimage:v1.2.3

# This
container: myimage@sha256:abc123...

Where available, enable Docker Content Trust or Cosign verification. The principle is the same as for actions: if the reference can be silently redirected, it will be — eventually.

Continuously lint your workflows

Manual review of YAML security posture does not scale. zizmor is a purpose-built static analysis tool for GitHub Actions that catches many of the patterns discussed in this post: unpinned actions, template injection vulnerabilities, overly broad permissions, dangerous triggers with untrusted code, and more. It was born out of the Homebrew project’s need to audit its CI/CD security, and has since been adopted at scale by organizations like Grafana Labs — who deployed it across 2,000+ repositories after their own GitHub Actions incident. zizmor outputs SARIF, so findings integrate directly into GitHub’s Advanced Security tab, and it ships with a VS Code extension and LSP support for real-time feedback while editing workflows.

For organization-wide coverage, combine zizmor with poutine, which adds rules that go beyond workflow-level analysis — including unpinnable actions, known vulnerabilities in build components, and risky self-hosted runner configurations across entire organizations.

Prepare your incident response plan now

Once mutable references are abused, every secret accessible to affected runners must be treated as compromised. Aqua says exactly that for affected workflows, and GitHub recommends rotating all exposed secrets.

Every project should have a pre-written response plan for CI/CD compromise:

  1. Identify affected workflow runs and exposure windows.
  2. List every secret, token, cloud credential, SSH key, and registry credential available to those jobs.
  3. Revoke and reissue all of them.
  4. Inspect audit logs, release history, and action references.
  5. Review caches, artifacts, and published packages produced during the exposure window.
  6. Communicate clearly to downstream users: what was affected, for how long, and what they should rotate.

The hard part is not knowing these steps. The hard part is doing them fast. Write the playbook before you need it.

What the Eclipse Foundation Security Team recommends as a baseline

For Eclipse Foundation projects hosted on GitHub, the baseline is:

  1. Pin all third-party actions and reusable workflows to full commit SHAs. Automate with pinact or Otterdog’s pin_workflow blueprint.
  2. Scan for unpinnable actions. Use poutine to identify actions where pinning alone does not guarantee immutability, and replace or vendor them.
  3. Audit and minimize your action surface. Replace third-party actions with inline run: steps wherever practical.
  4. Enforce SHA pinning and action allow/block policy at repository or organization level via Otterdog.
  5. Enable Dependabot for github-actions with a 7-day cooldown for version updates.
  6. Add Harden-Runner in audit mode, then move selected jobs to block mode once a stable egress policy is established.
  7. Default GITHUB_TOKEN to read-only. Elevate only where needed. Prefer OIDC over long-lived cloud secrets. Segment high-value secrets into isolated workflows.
  8. Pin Docker images by digest, not tag. Enable signature verification where available.
  9. Lint workflows continuously with zizmor and poutine.
  10. Maintain a rehearsed CI/CD compromise playbook.

Closing thought

The Trivy incident should not be read as “don’t trust Trivy.” It should be read as: stop trusting mutable references and over-privileged automation. Many GitHub Actions environments are still built on moving tags, broad secrets, and zero runtime visibility. That is a survivable posture right up until the day it isn’t. The projects that fare best in the next incident — and there will be a next incident — will be the ones that have already made trust explicit, immutable, and narrow.

Need help?

If your Eclipse Foundation project needs help hardening its GitHub Actions workflows, assessing exposure from the Trivy incident, or implementing any of the controls described in this post, the Eclipse Foundation Security Team is here to support you. Reach out by email at [email protected] or start a discussion at eclipse-csi on GitHub.

A follow-up post will cover the maintainer side: how to make your own releases, tags, and automation resistant to compromise so your project doesn’t become the next Trivy.