Auditing GitHub Release Workflows After the OVHcloud CLI Supply-Chain Compromise

Auditing GitHub Release Workflows After the OVHcloud CLI Supply-Chain Compromise

pr0h0•
supply-chain-securitygithub-actionsrelease-engineeringci-cd-security
AI Usage (86%)

This audit guide shows how to inspect GitHub release workflows after the OVHcloud CLI compromise: where release assets bypass git history, which permissions and triggers to check, how to verify artifacts, and what to fix first. The reporting I reviewed, dated 20 October 2026, says five OVHcloud CLI releases on GitHub were hijacked to ship secret-stealing malware. That is the full extent of what the source material gave me: a distribution-channel compromise, five affected releases, and an infostealer-style payload.

For anyone running a release pipeline, the malware family is the least interesting part. The real issue is that a GitHub release asset is mutable state sitting outside your git history, and almost nobody audits it. A tag can be pristine, a commit can be signed, CODEOWNERS can approve the diff — and the binary your users curl | sh still gets swapped by anyone holding a contents: write token.

The OVHcloud CLI Incident: Five Hijacked GitHub Releases

The reporting describes attackers publishing malicious builds through OVHcloud's GitHub release channel, with the payload aimed at harvesting credentials from the machines and CI runners that installed the CLI. Both outlets agree on the count of five releases.

Confirmed facts, source attribution, and open questions

ClaimSourceStatus
Five OVHcloud CLI releases were hijackedThe Hacker News; CyberSecurityNews (20 Oct 2026)Reported by both, treat as confirmed
Payload is secret-stealing malwareSameReported; specific exfil targets not public in what I saw
Distribution was via GitHub releasesSameReported
Initial access vector—Unknown in the material I had

The gap that bothers me most is the access vector, because it decides what evidence even exists:

  • If the release workflow file was modified, there is a commit and a workflow run to review.
  • If a stolen PAT or GitHub App token was used to call the Releases API directly, there is no commit at all — only audit-log events and asset digest changes.

I could not confirm from the available reporting which of those happened, whether the affected assets were covered by signatures or attestations, or how long they were live. Resolve those against OVHcloud's own advisory and GitHub's audit log before treating any specific version as safe. Anything below that goes beyond "the report says" is my inference, and I have flagged it as such.

Why GitHub Release Assets Are a Supply-Chain Trust Boundary

A release asset is a blob uploaded to a release record. It is not derived from the tag, not hashed by GitHub into the tag object, and not required to match any build output. Replace it with gh release upload --clobber and the release URL, tag, and download counters all stay identical. Consumers who pin a URL rather than a digest get whatever is attached at request time.

Release assets, workflow authority, and downstream secrets

Most CLIs are not installed through a package manager with a lockfile. They arrive via one of:

  • curl -fsSL https://.../install.sh | sh, which fetches the latest asset and executes it;
  • a Homebrew formula or tap that references a release URL with a checksum committed in the same repo;
  • an npm or PyPI wrapper package whose postinstall downloads a platform binary from GitHub;
  • a Dockerfile doing ADD https://github.com/.../releases/download/....

In all four cases, whoever can replace that asset can ship code. contents: write on a repository is therefore a production credential, not a convenience scope — it amounts to "can ship code to every downstream user," and it is routinely held by bots, PATs, and workflow tokens with no review gate.

Auditing GitHub Release Workflows with Reproducible Checks

The commands below are the ones I run first on any repo whose artifacts I install. Field names shift between API versions, so run them against your own repo before trusting the parsing.

Inventory workflow triggers, permissions, and environments

Start with the repo's default token permissions and the workflow inventory:

gh api repos/OWNER/REPO/actions/permissions/workflow \
  --jq '{default: .default_workflow_permissions, can_approve_prs: .can_approve_pull_request_reviews}'

gh api repos/OWNER/REPO/actions/workflows --paginate \
  --jq '.workflows[] | "\(.path)\t\(.state)"'

Trimmed output:

{"default":"read","can_approve_prs":false}
.github/workflows/release.yml	active
.github/workflows/ci.yml	active
.github/workflows/stale.yml	active

default: read is the baseline you want. Repos still set to write hand every workflow — including one added in a random PR — the ability to publish. Then read the release workflow for the triggers and grants that actually matter:

grep -nE '^\s*(on|permissions):|pull_request_target|workflow_run|release:|environment:|uses:|secrets\.' \
  .github/workflows/release.yml

What I look for, in order of severity:

PatternWhy it is a problem
on: workflow_run releasing artifactsRuns with the base repo's secrets and token, on code from a fork
pull_request_target + checkout of PR headClassic token/secrets theft path
Top-level permissions: write-all or contents: writeEvery step in the job can publish
uses: some/action@v3 (tag, not SHA)Tag can be retagged to a malicious commit
No environment: on the publish jobNo reviewer gate between build and release

The tag-vs-SHA point is not theoretical. In March 2025, tj-actions/changed-files was compromised and its version tags were repointed at a commit that leaked CI secrets into workflow logs (CVE-2025-30066). Pinning to a full 40-character commit SHA would have blocked that for anyone who had pinned; everyone on @v45 got the malicious code with no change to their own repo.

Capture release history, asset digests, and recent workflow runs

gh api repos/OWNER/REPO/releases --paginate \
  --jq '.[] | {tag: .tag_name, published: .published_at,
        assets: [.assets[] | {name, size, digest, updated_at}]}'

gh api "repos/OWNER/REPO/actions/runs?event=release&per_page=20" \
  --jq '.workflow_runs[] | [.id, .name, .event, .head_sha, .conclusion, .created_at] | @tsv'

Two signals deserve a permanent alert:

  1. updated_at later than published_at. An asset re-uploaded after publication is the fingerprint of an in-place swap.
  2. A digest that differs from the value you recorded for the same asset name. If your API version returns digest, store it. If not, compute it yourself at download time and compare against a value you recorded historically, not against a checksum published in the same release.

Org owners on GitHub Enterprise Cloud can also pull the audit log for release and secret events:

gh api "/orgs/ORG/audit-log?phrase=action:release&per_page=100" \
  --jq '.[] | [.created_at, .actor, .action, .repo] | @tsv'

That audit log is the only evidence trail that exists for token-only abuse with no commit.

Artifact Verification and Signing: Necessary but Not Sufficient

If the project publishes attestations or Sigstore bundles, verify them before running anything:

gh attestation verify ./ovhcloud-cli --repo OWNER/REPO

cosign verify-blob \
  --bundle ovhcloud-cli.sigstore.json \
  --certificate-identity-regexp 'https://github.com/OWNER/REPO/.github/workflows/release.yml@refs/tags/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ./ovhcloud-cli

What checksums, signatures, and provenance do and do not prove

ControlProvesDoes not prove
SHA256 in the release notesNothing about authenticityThe file matches; the notes are editable by the same token
SHA256 pinned in a separate repoThe file matches what you recordedYour record was correct
Sigstore signatureArtifact signed by a specific workflow identityThat the workflow was not modified
Build provenance attestationWhich repo, workflow, and commit produced itThat the workflow's steps are trustworthy

This is where I disagree with how provenance usually gets sold. Signing is not a trust anchor when the signer is the compromised component. An attacker with workflow write access does not need to break the signature — they edit release.yml, push, and get a perfectly valid attestation naming OWNER/REPO/.github/workflows/release.yml as the builder. Verification passes. The artifact is still malware.

What signing actually buys you is the elimination of silent tampering: a swapped blob breaks the digest, and any change to the build path changes the identity string you pin. That is real value, and it is not the value most teams think they are buying.

Isolating CI/CD Secrets and Release Authority in GitHub Actions

Provenance only earns its keep once the release job's permissions and secrets are small enough that compromising it yields as little as possible.

Short-lived credentials, protected environments, and action pinning

name: release
on:
  push:
    tags: ["v*"]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@<40-char-sha>   # pinned, not @v4
      - run: make build
      - run: make sign                          # keyless, via OIDC

  publish:
    needs: build
    runs-on: ubuntu-latest
    environment: release            # required reviewers + restricted branches
    permissions:
      contents: write               # scoped to this job only
    steps:
      - uses: actions/download-artifact@<40-char-sha>
      - run: gh release upload "$GITHUB_REF_NAME" dist/* --clobber

The rules that carry the weight:

  • Nothing long-lived in the publish job. Exchange OIDC for short-lived cloud credentials (permissions: id-token: write plus the provider's trust policy pinned to repo:OWNER/REPO:environment:release) rather than storing a static API key.
  • environment: release with required reviewers. A human gate on publication is the cheapest control that survives a stolen token.
  • Two jobs, not one. The build job has no write permission; the publish job runs no build tooling against untrusted code.
  • Pin every uses: to a SHA. Pair with Dependabot or Renovate so pins don't rot.
  • Immutable releases, where available. GitHub has shipped a setting that locks a release's tag and assets once published; availability depends on plan and repo settings, so check your org's configuration rather than assuming it's on.

Detection and Downstream Credential Rotation After a Release Compromise

Evidence to collect before rotating credentials and notifying consumers

Rotate in the wrong order and you destroy the evidence you need to scope the incident. Preserve first:

  • the SHA256 of every downloaded asset, plus the download URL and timestamp;
  • the CLI's local config directory (wherever the tool persists API credentials) from affected workstations;
  • CI job logs for any run that downloaded the CLI during the exposure window;
  • shell history and process accounting for post-install execution of unexpected binaries;
  • egress logs from those hosts — infostealers are chatty, and the destination IPs scope your blast radius.

The rotation order I would follow: OVHcloud API keys and any consumer keys first (they authorize infrastructure changes), then cloud provider credentials reachable from affected hosts, then package registry tokens, GitHub PATs and App keys, then SSH keys. Invalidate sessions and revoke tokens — do not just change passwords. Publish a machine-readable notice: affected versions, asset hashes, the exposure window, and digests of the corrected build.

A Prioritized GitHub Release Hardening Checklist

PriorityActionEffort
1Move contents: write into a single environment-gated publish jobLow
2Remove long-lived cloud keys; use OIDC with repo+environment-bound trustMedium
3Set org default workflow permissions to readLow
4Pin all uses: to commit SHAs, enable DependabotMedium
5Publish and verify attestations; document the verification commandMedium
6Enable immutable releases; alert on updated_at > published_atLow
7Audit repos still accepting workflow_run release triggersMedium
8Store asset digests out-of-band on a schedule you controlHigh

What to Fix First in a Release Pipeline

If I could change one thing about a release pipeline after this incident, it would not be signing. It would be getting long-lived write credentials out of anything that can be triggered by a tag push, a release event, or a chained workflow.

Both this case and the tj-actions compromise point at the same soft target: the artifact pipeline, not the source tree. Source code gets review because it shows up in a diff. Workflow YAML gets reviewed once and then quietly accumulates write-all, third-party actions on floating tags, and a static cloud key in repository secrets — and the resulting artifact is signed, checksummed, and shipped to thousands of developers who have no way to tell.

The uncomfortable corollary: if the workflow that signs your release can be edited by the same automation that publishes it, your signature attests to identity and nothing about intent. Treat provenance as a tamper-detection control that makes silent swaps loud, and put your real trust in the permission model around it.

Further Reading and Primary-Source Notes

What I confirmed: the reported incident details as attributed above, and the behaviour of the GitHub APIs and verification commands described here. What I did not test: the specific OVHcloud releases, the malware's capability set, and the access vector — none of which were established in the source material I had.

Share this post

More posts

Comments