Don't become the next Trivy: how to make your releases, tags, and automation resistant to compromise

This is Part 2 of our response to the Trivy supply-chain compromise. Part 1 covered how to consume GitHub Actions safely. This post covers the other side: how to publish safely, so your project doesn’t become the upstream incident that impacts everyone downstream.

The attacker who compromised trivy-action force-pushed 76 of 77 version tags. The one tag that survived? 0.35.0protected by GitHub’s immutable releases feature. That single data point should change how every maintainer thinks about release hygiene. If every tag had been immutable, the attacker would have had compromised credentials and nothing useful to do with them.

The recommendations below apply to any project that publishes GitHub Actions, CLI tools, container images, libraries, or any artifact consumed in downstream automation. For Eclipse Foundation projects, most of these controls should be applied through Otterdog-managed configuration so the policy is reviewable, repeatable, and resistant to drift.

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

Enable immutable releases

This is the single most important thing a maintainer can do. GitHub’s immutable releases prevent release assets and the associated Git tag from being changed after publication. Tags cannot be moved or deleted. Assets cannot be modified or removed. The feature also automatically generates a signed release attestation — a cryptographically verifiable record linking the release to its repository and commit.

The Trivy incident provides the most compelling proof point: v0.35.0 was the only tag the attacker could not replace. It was immutable. The other 76 were not.

Enable this feature at the repository or organization level. Once enabled, all new releases are immutable. There is one operational adjustment: create releases as drafts first, attach all assets, then publish. Immutability applies after publication, so you need everything in place before you flip the switch.

A published release should be the end of mutation, not the beginning of it. If you publish anything consumed in automation — actions, binaries, container images, packages — enable immutable releases unless you have a concrete operational reason not to.

Protect version tags with rulesets

Immutable releases are the strongest control, but they only protect tags associated with published releases. Projects should also protect version tags directly using GitHub rulesets, which can target tags and restrict creations, updates, deletions, and force pushes.

The policy objective for release tags is usually simple: no tag updates, no tag deletions, no force pushes. Optionally, restrict tag creation to only your designated release automation identity.

For Eclipse Foundation projects, this is exactly the sort of policy that should be rolled out through Otterdog-managed organization or repository rulesets rather than configured by hand in the UI. Otterdog supports rulesets whose target can be tag, with controls including allows_creations, allows_deletions, allows_updates, and allows_force_pushes.

Don’t wait for the perfect immutable-releases rollout to start protecting tags. Rulesets can be deployed today as an immediate hardening step.

Stop using personal access tokens for release automation

The Trivy incident began with compromised credentials. GitHub’s documentation is clear: personal access tokens are meant for accessing GitHub on behalf of yourself, not for organizational automation. For organization access and long-lived integrations, use a GitHub App. GitHub App installation tokens are scoped to the installation and expire after one hour; a compromised classic PAT, by contrast, can provide broad, persistent access.

The priority order:

  1. GITHUB_TOKEN inside workflows — when it provides sufficient permissions, this is the simplest and most constrained option.
  2. GitHub App — for release automation, bot activity, and any cross-repository operation. Scoped, short-lived, auditable.
  3. Fine-grained PATs — only where a GitHub App is not feasible. Scope them as narrowly as possible and set an expiration.
  4. Classic PATs — avoid entirely unless the platform still requires one for a specific endpoint.

This is not just better hygiene. It is blast-radius reduction. User-bound long-lived credentials are exactly the sort of foothold that turns an automation mistake into an organization-wide incident. The Trivy timeline makes that clear: compromised credentials from an earlier incident were not fully revoked, and the attacker used the residual access to execute the tag-rewriting attack weeks later.

Publish provenance attestations by default

GitHub artifact attestations provide signed provenance linking artifacts to the source repository, workflow, commit SHA, and triggering event. They use the Sigstore bundle format and can include an associated software bill of materials (SBOM). GitHub’s documentation is careful to note that attestations don’t prove an artifact is “safe” — they prove where and how it was built, which is what allows consumers to define and enforce verification policy.

For maintainers, this means release pipelines should generate provenance by default:

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

steps:
  - name: Build
    run: make build

  - name: Generate attestation
    uses: actions/attest@v4
    with:
      subject-path: 'dist/my-binary'

Consumers can then verify with the GitHub CLI:

gh attestation verify dist/my-binary --owner my-org

When combined with immutable releases (which automatically generate release attestations), this creates a layered verification model. Incident response becomes more precise: consumers can ask not just “did I download a file named X,” but “does the file I ran match the expected build provenance from the expected repository and workflow?”

For projects targeting higher supply-chain maturity, using a reusable workflow for the build and attestation step provides isolation between the calling workflow and the build process, which meets SLSA v1.2 Build Level 3.

Harden your release workflow itself

Provenance attestations prove where an artifact was built, but they are only as trustworthy as the workflow that generates them. If your release workflow uses unpinned third-party actions, grants overly broad permissions, or runs on a self-hosted runner accessible from pull requests, the attestation just faithfully records a compromised build.

Apply every recommendation from Part 1 to your own release workflows with extra rigor:

  • Pin all actions to commit SHAs.
  • Set GITHUB_TOKEN to the minimum permissions required.
  • Use OIDC for any cloud or registry access.
  • Prefer GitHub-hosted runners.
  • Lint the release workflow with zizmor and poutine.

Your release workflow is your highest-value CI/CD target. Treat its security accordingly.

Review action update PRs with suspicion, not trust

Part 1 recommended pinning actions to commit SHAs and using tools like Pinact or pinned-actions to automate the initial conversion. A fair counterpoint, raised in response to that post: if you pin without manually verifying the SHA, you might be locking in a commit from an already-compromised-but-not-yet-disclosed repository. That’s a real risk. Manually verifying every digest — checking that the commit date, tag date, and release date are consistent, and that the commit history looks plausible — is the safer approach.

Here is my take on the trade-off: for a first-time setup across a project with dozens of unpinned actions, the benefit of moving to pinned SHAs outweighs the risk of pinning one that is already silently compromised. Requiring manual verification of every digest will discourage many teams from pinning at all, which leaves them exposed to the exact class of attack that hit Trivy. Start by pinning. Then build the verification habit into your ongoing process.

Where that verification habit matters most is when updates arrive. Once your actions are pinned, Dependabot or Renovate will propose PRs to update them. These PRs deserve more scrutiny than a typical dependency bump. Specifically, watch for:

  • A digest change without a corresponding tag change. A Dependabot PR that updates the SHA but reports the same version tag is a red flag. It may mean the tag was moved — exactly the technique used in the Trivy attack. Investigate before merging: compare the old and new commits, check the repository’s release history, and verify that the new SHA matches a legitimate release.
  • A digest change to a commit that predates the tag. If the proposed SHA points to a commit older than the tag it claims to represent, something is wrong. Legitimate releases don’t move backward.
  • Unfamiliar or unexpected actions appearing in the update. If Dependabot proposes an update for an action you don’t recognize or didn’t intentionally add, treat it as a potential indicator of a compromised workflow or configuration change.

The goal is not to make every Dependabot merge a forensic exercise. Most updates will be routine. But a moved tag is the signature of the attack pattern we saw with trivy-action, and the update PR is exactly where that signal will surface. Train your reviewers to recognize it.

Plan for credential compromise before it happens

A painful lesson from the Trivy timeline: the March 19 attack was not the first breach. It followed an earlier incident where compromised credentials were not fully revoked. The attacker retained residual access through still-valid tokens and used them weeks later to execute the tag-rewriting attack.

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

  1. Identify affected workflow runs and exposure windows.
  2. Inventory every secret, token, cloud credential, SSH key, and registry credential available to those jobs.
  3. Revoke and reissue all of them. Not some. All. Incomplete rotation is how the Trivy incident escalated from a contained breach to a full supply-chain compromise.
  4. Audit release history, tag integrity, published packages, caches, and artifacts produced during the exposure window.
  5. Communicate clearly to downstream users: what was affected, for how long, and what they should rotate on their end.

The hard part is not knowing these steps. The hard part is doing them fast, under pressure, at 2 AM, without missing a credential. Write the playbook before you need it. Rehearse it. Make sure more than one person knows how to execute it.

Consider reproducible builds for high-value artifacts

Provenance attestations prove where something was built. Reproducible builds go further: they let anyone independently verify that the source code produces the expected binary, bit for bit. If a build pipeline is compromised but the build is reproducible, the discrepancy is detectable.

This is a harder lift than the other recommendations in this post, and it’s not feasible for every project. But for high-value artifacts — signing keys, security-critical binaries, widely consumed libraries — it is the strongest defense against a compromised build environment. The Reproducible Builds project maintains tooling and documentation for multiple ecosystems.

Even partial reproducibility (deterministic builds that consumers can verify, even if not all do) raises the cost of a supply-chain attack significantly. If your project has the resources, invest here.

What the Eclipse Foundation Security Team recommends as a baseline

For Eclipse Foundation projects that publish actions, binaries, packages, or releases consumed by others, the baseline is:

  1. Enable immutable releases. This is the single highest-impact control. If every trivy-action tag had been immutable, the attack would have failed.
  2. Protect version tags with rulesets. No updates, no deletions, no force pushes. Deploy through Otterdog.
  3. Move release automation away from personal access tokens. Prefer GITHUB_TOKEN, then GitHub Apps, then fine-grained PATs. Avoid classic PATs entirely.
  4. Publish provenance attestations by default using actions/attest. Include SBOMs where feasible.
  5. Harden your release workflows with the same rigor described in Part 1: pinned actions, minimal permissions, OIDC, GitHub-hosted runners, continuous linting.
  6. Treat action update PRs as a security review surface. A Dependabot PR that changes a digest without changing the tag is the signature of a moved-tag attack. Train reviewers to catch it.
  7. Maintain a rehearsed CI/CD compromise playbook. Include credential inventory, revocation procedures, and a downstream communication plan. Rehearse it.
  8. Apply all controls through Otterdog-managed configuration so policy is reviewable, repeatable, and resistant to settings drift.

Closing thought

The Trivy compromise succeeded because mutable references and long-lived credentials gave the attacker two things: something to redirect and something to redirect it with. Immutable releases remove the first. Proper credential hygiene removes the second. Neither is hard to implement. Both require the decision to stop treating release infrastructure as an afterthought.

The projects that will weather the next incident — and there will be a next one — are the ones building that infrastructure now: immutable tags, signed provenance, short-lived tokens, and a playbook that’s been rehearsed before the crisis hits. Start today. The attacker already has.

Need help?

If your Eclipse Foundation project needs help implementing immutable releases, hardening release automation, setting up provenance attestations, or building a CI/CD compromise playbook, 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.