
Auditing GitHub Release Workflows After the OVHcloud CLI Supply-Chain Compromise
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
| Claim | Source | Status |
|---|---|---|
| Five OVHcloud CLI releases were hijacked | The Hacker News; CyberSecurityNews (20 Oct 2026) | Reported by both, treat as confirmed |
| Payload is secret-stealing malware | Same | Reported; specific exfil targets not public in what I saw |
| Distribution was via GitHub releases | Same | Reported |
| 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
postinstalldownloads 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:
| Pattern | Why it is a problem |
|---|---|
on: workflow_run releasing artifacts | Runs with the base repo's secrets and token, on code from a fork |
pull_request_target + checkout of PR head | Classic token/secrets theft path |
Top-level permissions: write-all or contents: write | Every 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 job | No 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:
updated_atlater thanpublished_at. An asset re-uploaded after publication is the fingerprint of an in-place swap.- A
digestthat differs from the value you recorded for the same asset name. If your API version returnsdigest, 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
| Control | Proves | Does not prove |
|---|---|---|
| SHA256 in the release notes | Nothing about authenticity | The file matches; the notes are editable by the same token |
| SHA256 pinned in a separate repo | The file matches what you recorded | Your record was correct |
| Sigstore signature | Artifact signed by a specific workflow identity | That the workflow was not modified |
| Build provenance attestation | Which repo, workflow, and commit produced it | That 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: writeplus the provider's trust policy pinned torepo:OWNER/REPO:environment:release) rather than storing a static API key. environment: releasewith 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
| Priority | Action | Effort |
|---|---|---|
| 1 | Move contents: write into a single environment-gated publish job | Low |
| 2 | Remove long-lived cloud keys; use OIDC with repo+environment-bound trust | Medium |
| 3 | Set org default workflow permissions to read | Low |
| 4 | Pin all uses: to commit SHAs, enable Dependabot | Medium |
| 5 | Publish and verify attestations; document the verification command | Medium |
| 6 | Enable immutable releases; alert on updated_at > published_at | Low |
| 7 | Audit repos still accepting workflow_run release triggers | Medium |
| 8 | Store asset digests out-of-band on a schedule you control | High |
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
- The Hacker News: Hackers Hijack Five OVHcloud CLI Releases to Distribute Secret-Stealing Malware — news report, 20 Oct 2026; resolve the redirect to the publisher page.
- CyberSecurityNews: Hackers Hijack OVHcloud GitHub Releases to Distribute Secret-Stealing Malware — second report of the same event.
- GitHub Docs: Security hardening for GitHub Actions — pinning,
GITHUB_TOKENscope, script injection. - GitHub Docs: Artifact attestations — what provenance records and how to verify it.
- GitHub Docs: OpenID Connect — short-lived cloud credentials from workflows.
- GitHub REST API: Release assets — asset upload, replace, and delete semantics.
- NVD: CVE-2025-30066 —
tj-actions/changed-filestag-repointing compromise, referenced above. - Sigstore: Verifying signatures with cosign — identity and issuer pinning for keyless verification.
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.


