Pin your GitHub Actions

15 Mar 2025 in Tech

Way back in 2019, Julien Renaux published Use GitHub Actions at your own risk. While the title is a little sensational, it correctly pointed out that any maintainer can update a branch or tag to point at new code without you knowing. This means that if any action is compromised, you'll start leaking secrets without knowing it.

Today, tj-actions/changed-files, a widely used GitHub Action was compromised and started leaking secrets.

Hopefully this was the wakeup call the industry needed to start paying attention to supply chain security.

Solving the problem

Security is always a trade-off. You can solve the supply chain problem by specifying a full length commit SHA, but fetching that SHA for every action is a painful process. Then, whenever you want to upgrade your action you have to do it all again.

Thankfully, there are tools and automations available to solve this problem. You can be secure and feel minimal pain thanks to these projects:

  1. pin-github-action - This is one of my projects. It takes a directory of workflows and uses the GitHub API to convert tag and branch references to a full length commit SHA.
  2. github-actions-ensure-sha-pinned-actions - A community action that will fail the build if it detects any unpinned actions being used in the current repository.
  3. Dependabot / Renovate - Dependency management systems that submits PRs to upgrade GitHub Actions. They both natively understand the # <ref> comments in workflow files.

Pin to a SHA

The first thing to do is update all of your existing workflows to use the long commit SHA using pin-github-action.

You can run it with npx or docker. If you're not sure which to use, use docker through the following alias:

bash
alias pin-github-action="docker run --rm -v $(pwd):/workflows -e GITHUB_TOKEN mheap/pin-github-action"

If you're working with a large number of workflows, or any private actions you'll need to set the GITHUB_TOKEN environment variable to prevent rate limiting or provide valid access credentials to a repository:

bash
export GITHUB_TOKEN="ghp_YOUR_TOKEN_HERE"

Finally, change in to your repository and run pin-github-action:

bash
cd my-repo
pin-github-action .github/workflows

If you run git diff .github/workflows you'll see that all of your actions have been updated to the long commit SHA:

diff
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- - uses: MyOrg/some-action@v1
+ - uses: MyOrg/some-action@25ed13d0628a1601b4b44048e63cc4328ed03633 # v1

I recommend pinning all actions to a SHA, but this may not be feasible for some companies that use internal actions. If you want to trust internal actions, you can pass the --allow flag to pin-github-action:

bash
pin-github-action --allow "MyOrg/*" .github/workflows

This will ignore any actions with the prefix MyOrg:

diff
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: MyOrg/some-action@v1

Prevent regressions

Pinning all actions to a specific SHA solves the problem today, but it doesn't guarantee that a new action won't be added in the future without using a SHA. To prevent that happening, github-actions-ensure-sha-pinned-actions can be used to fail the build when any unpinned actions are detected.

The README for the action contains examples, but if you're using pin-github-action you can automatically add a new workflow using the --enforce flag. The --enforce flag writes a workflow containing github-actions-ensure-sha-pinned-actions to the path provided, including adding any actions passed in --allow to the allowlist input for the action.

The following command will create a workflow at .github/workflows/security.yaml that ensures all actions are using the long commit SHA, unless they are actions from MyOrg:

bash
pin-github-action --allow "MyOrg/*" --enforce .github/workflows/security.yaml .github/workflows

The created workflow looks like this:

yaml
on: push
name: Security
jobs:
ensure-pinned-actions:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Ensure SHA pinned actions
uses: zgosalvez/github-actions-ensure-sha-pinned-actions@25ed13d0628a1601b4b44048e63cc4328ed03633 # v3
with:
allowlist: |
MyOrg/

Automating updates

Finally, we need to keep our dependencies up to date. You have two options here:

  1. Run pin-github-action again
  2. Use Dependabot or Renovate

I recommend using Dependabot or Renovate.

Running pin-github-action on a repository with pinned SHAs and will extract the target version from the comment and update the SHA like so:

diff
steps:
- - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

This is great for updating actions in bulk, but it doesn't improve your security posture that much. You're still blindly upgrading to the latest version.

I recommend using Dependabot or Renovate to update your actions. Both tools create Pull Requests with updated SHAs and provide a diff for review. Each pull request contains a link to the compare view on GitHub that you can use to audit the changes since your last update and ensure that the action has not been compromised.

Stay safe, pin your dependencies

The compromise of tj-actions/changed-files serves as a crucial reminder of the security risks in GitHub Actions. While pinning actions to commit SHAs adds an extra step, it significantly reduces the risk of supply chain attacks.

By leveraging tools like pin-github-action, enforcing SHA pinning with github-actions-ensure-sha-pinned-actions, and automating updates with Dependabot or Renovate, teams can secure their workflows without unnecessary overhead.

Supply chain security is an ongoing effort, but with these practices in place, you can proactively protect your repositories and secrets from potential threats.