Pin your GitHub Actions
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:
- 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.
- 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.
- 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-repopin-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: pushname: Securityjobs:ensure-pinned-actions:runs-on: ubuntu-lateststeps:- name: Checkout codeuses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4- name: Ensure SHA pinned actionsuses: zgosalvez/github-actions-ensure-sha-pinned-actions@25ed13d0628a1601b4b44048e63cc4328ed03633 # v3with:allowlist: |MyOrg/
Automating updates
Finally, we need to keep our dependencies up to date. You have two options here:
- Run
pin-github-action
again - 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.