Skip to main content

Securing GitHub Actions with SHA Pinning

· 6 min read
Calvin Wilkinson
Creator and Maintainer of Velaptor (and other projects)

So you've set up your CI/CD pipelines with GitHub Actions, and everything is running smoothly. It feels great to push code and watch those green checkmarks light up. But there's a hidden, often overlooked security risk lurking in most of our workflow files: version tags.

Today we're going to dive into why pinning your GitHub Actions to a commit SHA is arguably one of the most effective and simplest security upgrades you can make to your pipelines. Let's get into it! 🚀

The Danger of Mutable Tags

When we first write a workflow, it's pretty standard to just grab an action and point to its major version tag. You've probably seen (and written) something like uses: actions/checkout@v3 a hundred times. It looks safe, right?

Here's the problem: version tags are mutable. They are literally just pointers that action maintainers can move around whenever they want. If an attacker manages to hijack a widely used action, they can inject malicious code—like a script designed to silently steal your API keys or deployment tokens—and re-tag that compromised commit as v3.

Because your workflows automatically pull whatever v3 points to during the next build, your runner will download and execute that malware right alongside your codebase. This is a classic supply chain attack, and because pipelines typically have access to highly sensitive environments, the blast radius can be massive. We all know about the massive increase of supply chain attacks in recent years, and GitHub Actions has been a prime target due to its popularity and the level of access it has in many organizations.

Why SHA Pinning is the Answer

This brings us to SHA pinning. Instead of relying on a human-readable tag like v3, we point our workflow to a specific, 40-character commit hash (a SHA).

Why does this matter? Immutability. A full-length commit SHA uniquely identifies an exact point in time in a repository's history. By relying on this hash, it becomes cryptographically impossible for the underlying code to be altered without generating an entirely new SHA. Pinning guarantees that the action executed by your runner is the exact, audited version you expect, effectively closing the door on those sneaky tag re-assignment attacks.

How to Pin Your Actions

So how do we actually do this? Honestly, the process is pretty straightforward.

  1. Locate the Action Repo: Head over to the third-party action's repository on GitHub (for example, actions/checkout).
  2. Find the Release: Click on the Releases or Tags page, and locate the version you normally use.
  3. Grab the Hash: Next to the release, you'll see a shortened commit hash link. Click that to view the actual commit, and copy the full 40-character alphanumeric SHA.
  4. Update your Workflow: Go into your workflow YAML and replace the version tag with the SHA.

Here's what a vulnerable setup looks like before we fix it:

steps:
- uses: actions/checkout@v3
- uses: nosborn/github-action-markdown-cli@v3.5.0

And here is the secured version after pinning:

steps:
# It is best practice to keep the version tag as a comment for readability!
- uses: actions/checkout@61b9e3751b92087fd0b06925ba6dd6314e06f089 # v3.0.0
- uses: nosborn/github-action-markdown-cli@508d6cefd8f0cc99eab5d2d4685b1d5f470042c1 # v3.5.0

Notice that I've left the original semantic version as a comment on the same line. A hash gives absolutely zero context about what version you're on, so dropping a comment is a lifesaver for future you (or your teammates) when reading the file.

Automating the Pain Away

Now, you might be thinking: "Tracking down these 40-character strings and manually updating them sounds like an absolute nightmare." And you'd be right. If you have dozens of repositories, the maintenance burden gets heavy very quickly.

To stop you from pulling your hair out, you should heavily rely on automation. Dependabot is fantastic for this. You can easily configure your .github/dependabot.yml to automatically submit pull requests whenever an action gets a new release. Dependabot is smart enough to translate the new version tag directly into the secure commit SHA for you.

version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
Renovate Support

Renovate supports this as well, and if you have a more complex setup or want to integrate this into a custom CI job, there are plenty of scripts and tools in the community that can help automate the process of fetching the latest SHAs for your pinned actions.

Set it up once, and never hunt for a commit hash by hand again.

GitHub's New Security Policies

As of August 2025, GitHub actually made it a lot easier for organizations to enforce these best practices globally. Administrators now have the ability to explicitly block specific malicious actions, and more importantly, they can mandate SHA pinning across the entire org.

If this policy is enabled, GitHub's checks will immediately fail any workflow that tries to use a mutable branch name or tag. It's a fantastic feature for security teams that need to sleep better at night knowing their supply chain is locked down.

For more information, check out the following:

Conclusion

At the end of the day, securing our pipelines is just as important as securing the code we write. Switching from mutable tags to immutable SHAs might feel like a minor change, but it removes a huge attack vector from your day-to-day workflow. Combined with automated dependency updates, you get top-tier security without sacrificing developer velocity.

If you have any questions, want to chat, or contribute to any of my open-source projects, feel free to reach out to me on our discord channel or X located at the top right.