Skip to content

ci: pin GitHub Actions to commit SHAs - #854

Open
xbmc4lyfe wants to merge 5 commits into
nzbgetcom:developfrom
xbmc4lyfe:ci/pin-actions-sha
Open

xbmc4lyfe wants to merge 5 commits into
nzbgetcom:developfrom
xbmc4lyfe:ci/pin-actions-sha

Conversation

@xbmc4lyfe

Copy link
Copy Markdown
Contributor

Fixes #853

What

Replaces every mutable version tag in .github/workflows/ with the full 40-character commit SHA that tag currently resolves to, keeping the original version as a trailing comment so the pins stay legible and tooling can still bump them:

# before
uses: actions/checkout@v6
# after
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

Same versions, immutable refs — zero behavior change. Local reusable-workflow calls (uses: ./.github/workflows/…) reference in-repo files and are unchanged.

Also adds .github/dependabot.yml with a github-actions ecosystem entry (weekly) so the pinned SHAs continue to receive automated update PRs — Dependabot understands the # vX comment convention and keeps it in sync when bumping.

Why

Tags are movable. If any referenced action repository is compromised (maintainer account takeover or a force-moved release tag — the tj-actions/changed-files incident class), the next workflow run executes attacker-controlled code with whatever credentials the job holds. For this repo that means the release path in build.yml (secrets: inherit: code-signing key, SignPath credentials, macOS notarization identities, Docker Hub and Snapcraft tokens) and the self-hosted release runner. Pinning to commit SHAs removes the movable-tag vector. See #853 for the full threat model.

Pinned actions (audit table)

Every SHA was resolved via the GitHub API (annotated tags dereferenced to their underlying commit) and independently cross-checked against git ls-remote peeled refs — both methods agreed on all 14:

Action Tag Pinned commit SHA
signpath/github-action-submit-signing-request v2 b9d91eadd323de506c0c81cf0c7fe7438f3360fd
ncipollo/release-action v1 339a81892b84b4eeb0f6e744e4574d79d0d9b8dd
dev-drprasad/delete-tag-and-release v1.1 82600feb9527126eca69833f07bafe53279bd9b4
geekyeggo/delete-artifact v6 176a747ab7e287e3ff4787bf8a148716375ca118
peter-evans/dockerhub-description v4 432a30c9e07499fd01da9f8a49f0faf9e0ca5b77
miklinux/ghcr-cleanup-action v1 5c95ee8945fd81f837669635a20ea2f2b751c477
cross-platform-actions/action v0.27.0 fe0167d8082ac584754ef3ffb567fded22642c7d
cross-platform-actions/action v0.28.0 97419d18f6470332677e345e9df97cdc71244ead
docker/build-push-action v7 53b7df96c91f9c12dcc8a07bcb9ccacbed38856a
docker/setup-buildx-action v4 bb05f3f5519dd87d3ba754cc423b652a5edd6d2c
docker/login-action v4 af1e73f918a031802d376d3c8bbc3fe56130a9b0
actions/checkout v6 df4cb1c069e1874edd31b4311f1884172cec0e10
actions/upload-artifact v7 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
actions/download-artifact v8 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c

The two cross-platform-actions/action versions used in tests.yml are intentionally kept distinct and pinned to their own SHAs.

Verification

  • Diff purity: reversing the transformation on all 96 added lines (@sha # tag@tag) reproduces the 96 removed lines byte-for-byte — the diff contains nothing but tag→SHA rewrites.
  • actionlint: output is byte-identical between develop and this branch (80 pre-existing findings, all shellcheck style notes in run: scripts plus the custom self-hosted runner label; zero new).

Known limitation / possible follow-ups

  • SHA-pinning covers actions referenced directly by this repo. It does not cover transitive actions that a pinned action invokes internally by tag — that remains the upstream action's responsibility.
  • An optional future hardening step would be egress monitoring on the runners (e.g. StepSecurity Harden-Runner), particularly valuable for the self-hosted release runner. Intentionally not included here to keep this PR a pure no-behavior-change pinning pass.

Replace mutable version tags with full commit SHAs for all external
actions across the workflows, keeping the original version as a
trailing comment. Same versions, immutable refs - no behavior change.
Local reusable-workflow calls (uses: ./.github/workflows/...) are
unchanged.

Also add a Dependabot config for the github-actions ecosystem so the
pinned SHAs continue to receive update PRs.

Fixes nzbgetcom#853
@dnzbk
dnzbk requested review from dnzbk and phnzb July 12, 2026 14:45
dnzbk and others added 4 commits July 12, 2026 17:46
- docker/Dockerfile: alpine 3.22.2 -> 3.22.5 (latest 3.22.x patch),
  pinned to the multi-arch OCI index digest so the base image is
  immutable across all build platforms
- .github/dependabot.yml: add docker ecosystem so the pinned digest
  gets update PRs alongside the github-actions pins
- refresh two pins whose tags moved since the PR was opened:
  actions/checkout v6.0.3 -> v6.1.0, docker/login-action v4.4.0 -> v4.6.0
  (both verified against the upstream release tags)
@xbmc4lyfe

Copy link
Copy Markdown
Contributor Author

Rebased the branch on current develop (clean merge, no conflicts) and extended the same hardening to the Docker image. Summary of this update:

  • Merged develop — no workflow changes had landed upstream, so all pins carried over intact; actionlint output is byte-identical to develop (80 pre-existing findings, none introduced).
  • Refreshed two pins whose tags moved since the PR was opened: actions/checkout v6.0.3 → v6.1.0 and docker/login-action v4.4.0 → v4.6.0. Both new SHAs were verified against the upstream release tags before adopting them.
  • Digest-pinned the Docker base image: alpine:3.22.2alpine:3.22.5@sha256:14358309... (latest patch on the 3.22 line; 3.22.3+ contain security fixes). The digest is the multi-arch OCI index digest, so all platforms built by docker/build-push-action resolve from the same immutable manifest.
  • Added the docker ecosystem to dependabot so the image digest gets update PRs the same way the action pins do.

Why this is a security PR

uses: some/action@v4 and FROM alpine:3.22.2 look pinned, but both are mutable references — a git tag and a registry tag can be repointed at any time by whoever controls (or compromises) the upstream repo/registry account. Every CI run re-resolves them, so the code executing on the runners can change without any change to this repository.

Concrete ways that gets exploited without SHA pinning:

  1. Retagged action → secret exfiltration. If an action's repo or a maintainer account is compromised, the attacker repoints the version tags at a malicious commit. The next scheduled/PR build executes it on the runner with access to whatever secrets the job has. This is not hypothetical — the March 2025 tj-actions/changed-files incident repointed v-tags at a commit that dumped CI secrets across thousands of repos, and reviewdog/action-setup was hit the same way.
  2. This repo's secrets are high-value. The workflows hold Docker Hub credentials, a GHCR token, and SignPath code-signing secrets. An attacker with those doesn't just read secrets — they can push a backdoored nzbgetcom/nzbget image or get malicious Windows binaries code-signed, turning a CI compromise into a supply-chain attack on every downstream user.
  3. Retagged base image → poisoned releases. The Docker tag has the same weakness on the registry side: alpine:3.22.2 re-resolves on every build, so a repointed tag would silently bake a poisoned rootfs into the published multi-arch images. The @sha256: digest makes the base content-addressed and immutable.

A 40-char commit SHA / manifest digest is immutable, so updates only happen through explicit, reviewable dependabot PRs instead of silently at run time. Known limitation (unchanged from the PR description): pinned actions can still call unpinned transitive actions internally; pinning the first hop is still the standard mitigation (OpenSSF Scorecard checks for exactly this).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI: pin GitHub Actions to commit SHAs to harden the release pipeline against supply-chain attacks

2 participants