Your GitHub Actions Pipeline Is Part of Your Supply Chain — Why Full Commit SHA Pinning Matters
You review your application code.
You scan your dependencies.
You protect your main branch.
You require pull-request approvals.
Then your GitHub workflow contains this:
- uses: some-company/some-action@v3
It looks harmless.
It may even be an Action used by thousands of repositories.
But there is an important security question:
What code will actually execute when GitHub resolves
@v3tomorrow?
That question sits at the heart of GitHub Actions supply-chain security.
A third-party GitHub Action is not simply a configuration reference. It is code that you are allowing to execute inside your CI/CD environment.
And CI/CD environments frequently sit very close to the most sensitive parts of an organization:
Source Code
│
▼
GitHub Repository
│
▼
GitHub Actions
│
├── Build
├── Test
├── Security Scan
├── Package
└── Deploy
│
▼
AWS / Azure / GCP
Kubernetes
Registries
Production
If an attacker compromises something your pipeline trusts, they may not need to attack your application directly.
They attack the software factory that builds the application.
That is a software supply-chain attack.
First: What Is a Software Supply-Chain Attack?
Imagine that your company builds a product.
You control your own source code, but your product depends on many things you did not create yourself:
Your Application
│
├── npm packages
├── Python packages
├── Container images
├── Build tools
├── Compilers
├── GitHub Actions
├── CI/CD plugins
└── Third-party services
These components form part of your software supply chain.
Instead of attacking your application directly, an attacker may compromise something that your development or deployment process already trusts.
The basic model looks like this:
Normal situation
Developer
│
▼
Trusted Dependency
│
▼
CI/CD Pipeline
│
▼
Application
A supply-chain attacker tries to insert themselves upstream:
ATTACKER
│
▼
Developer ──► Compromised Dependency
│
▼
CI/CD Pipeline
│
▼
Application
The dangerous part is trust inheritance.
Your organization may never have trusted the attacker.
But it trusted the dependency.
The attacker compromises the dependency.
Your system then executes the attacker's code using the trust you originally gave to the dependency.
Where Do GitHub Actions Fit Into This?
GitHub Actions is GitHub's automation platform for CI/CD and other repository workflows. A workflow can build applications, run tests, perform security scans, publish containers, create releases, or deploy workloads.
A simplified workflow might look like this:
name: Build
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
The important line is:
uses:
A run: step executes a command you defined.
A uses: step executes an Action.
An Action is a reusable unit of automation. GitHub supports JavaScript Actions, Docker container Actions, and composite Actions.
For example:
- uses: vendor/security-scanner@v3
means roughly:
Find the repository:
vendor/security-scanner
Find whatever:
v3
currently points to
Download that Action
Execute it inside this workflow
That last sentence is the important one:
Execute it.
What Is a Third-Party GitHub Action?
A third-party Action is an Action maintained outside the codebase or organization you directly control.
Developers use them because they solve common problems without requiring every team to write the same automation again.
For example, an Action might perform:
Repository
│
▼
Third-Party Action
│
├── Scan container
├── Upload artifact
├── Run linter
├── Generate release
├── Authenticate to cloud
└── Deploy application
This is extremely useful.
It is also a trust decision.
When your workflow executes:
- uses: vendor/action@v3
you are effectively saying:
"I trust code maintained in this external repository enough to execute it within my CI/CD job."
That is significantly different from installing a harmless editor extension on a developer laptop.
A GitHub Actions job may interact with source code, build artifacts, repository tokens, credentials explicitly supplied to the job, cloud authentication, package registries, container registries, deployment infrastructure, or other workflow components.
GitHub specifically warns that compromise of a single Action within a workflow can be significant because jobs and Actions can interact with the workflow environment, and a compromised Action may be able to abuse the GITHUB_TOKEN or accessible secrets.
There is an important nuance here.
A third-party Action does not magically receive every repository secret.
Secrets generally have to be made available to the workflow. However, Actions can access the GITHUB_TOKEN through the github.token context even when you do not explicitly pass that token to the Action. This is why GitHub recommends restricting GITHUB_TOKEN permissions to the minimum required.
Now We Reach the Supply-Chain Problem
Consider this workflow:
steps:
- uses: actions/checkout@v4
- uses: trusted-company/security-action@v3
Today:
v3
│
▼
Commit A
│
▼
Safe code
Your pipeline runs normally.
But a Git tag is a reference.
It can potentially be changed.
Imagine an attacker gains sufficient control over the upstream Action repository.
The attacker changes the tag:
BEFORE
v3
│
▼
Commit A
│
▼
Safe Action
Then:
AFTER COMPROMISE
v3
│
▼
Commit X
│
▼
Malicious Action
Your workflow file has not changed.
It still says:
uses: trusted-company/security-action@v3
No developer changed your repository.
No pull request modified your workflow.
Your code review process may see nothing.
But the next workflow run could resolve v3 to different code.
That is the security problem SHA pinning addresses.
What Could a Malicious Action Actually Do?
Think of a compromised Action as malicious code executing inside your CI/CD job.
Its potential impact depends on the permissions and credentials available to that job.
Consider a deployment workflow:
GitHub Runner
│
├── Repository source code
├── GITHUB_TOKEN
├── Build artifacts
├── Registry access
├── Cloud authentication
└── Deployment permissions
Now place malicious code inside the workflow:
GitHub Runner
│
├── Repository source code
├── GITHUB_TOKEN
├── Build artifacts
├── Cloud authentication
│
└────► MALICIOUS ACTION
│
▼
ATTACKER
Depending on the job's privileges, the consequences could include credential theft, source-code access, artifact manipulation, unauthorized repository modification, poisoned releases, cloud compromise, container registry compromise, or production deployment compromise.
This is why CI/CD supply-chain attacks can become significantly more serious than compromise of a normal development dependency.
The attacker may be executing code at exactly the point where your organization converts:
SOURCE CODE
│
▼
TRUSTED SOFTWARE
│
▼
PRODUCTION
A Real Example: tj-actions/changed-files
This is not merely theoretical.
In March 2025, the widely used:
tj-actions/changed-files
GitHub Action was compromised.
GitHub's reviewed advisory for CVE-2025-30066 reports that attackers modified multiple version tags so that they referenced a malicious commit. The compromised Action executed malicious code intended to obtain secrets from GitHub Actions runner memory and expose them through workflow logs.
The GitHub advisory reports that more than 23,000 repositories used the affected Action.
The attack pattern looked conceptually like this:
Repository Workflow
│
▼
tj-actions/changed-files@v44
│
▼
v44 TAG
│
│ attacker changes reference
▼
Malicious Commit
│
▼
GitHub Runner executes it
│
▼
Sensitive information exposed
Notice something important.
The victim repositories did not necessarily need to change their workflow.
Their workflow might continue saying:
uses: tj-actions/changed-files@v44
The meaning of v44 changed upstream.
This is exactly the security distinction between:
VERSION REFERENCE
and:
IMMUTABLE CODE IDENTITY
Tags Are Names. Commit SHAs Identify Exact Commits.
This is the easiest way to understand SHA pinning.
Imagine a Git tag as a signpost:
┌──────────────┐
│ v3 │
└──────┬───────┘
│
▼
Commit A
Someone with sufficient repository permissions may be able to move the signpost:
┌──────────────┐
│ v3 │
└──────┬───────┘
│
▼
Commit X
The name remained:
v3
but the destination changed.
A commit SHA works differently.
Every Git commit has an identifier.
For example:
uses: actions/javascript-action@a824008085750b8e136effc585c3cd6082bd575f
GitHub describes commit SHAs as immutable and recommends using the full SHA rather than an abbreviated SHA when consuming third-party Actions.
So instead of asking:
Give me whatever v3 means today
you ask:
Give me exactly this commit
That is SHA pinning.
Vulnerable Pattern
Consider:
- uses: vendor/security-action@main
This follows the branch.
If new code appears on main, your workflow can execute the new code.
Now:
- uses: vendor/security-action@v3
This looks safer.
Operationally, it is more controlled than main.
But v3 remains a tag.
Now:
- uses: vendor/security-action@v3.2.1
This looks extremely specific.
But it is still a tag.
A specific-looking version number does not automatically make the reference immutable.
Finally:
- uses: vendor/security-action@<FULL_COMMIT_SHA>
Now the workflow is tied to one exact commit.
GitHub states that pinning an Action to a full-length commit SHA is currently the only way to use an Action as an immutable release.
Why Partial or Abbreviated SHAs Should Be Blocked
At this point, a reasonable question is:
"If a commit SHA identifies a commit, why do we need the full SHA? Why not use the shorter SHA GitHub often displays?"
For normal Git usage, you may see abbreviated commit IDs such as:
a824008
or:
a82400808575
These are shortened prefixes of the complete commit identifier.
The full commit SHA looks like this:
a824008085750b8e136effc585c3cd6082bd575f
The important distinction is:
PARTIAL SHA
│
▼
Prefix of an object identifier
versus:
FULL SHA
│
▼
Complete object identifier
GitHub's own Actions documentation is explicit: when pinning an Action by commit, use the full SHA and not an abbreviated value.
There are several reasons.
1. A Short SHA Is Only a Prefix
A shortened SHA does not contain the complete commit identifier.
For example:
Full SHA
a824008085750b8e136effc585c3cd6082bd575f
Partial SHA
a824008
The partial value means, conceptually:
Find the object whose SHA starts with:
a824008...
That may be convenient for humans.
It is not the strongest way to define the identity of executable CI/CD code.
2. Short SHAs Can Become Ambiguous
A short SHA may be unique in a repository today.
As more Git objects are created, another object may eventually share the same prefix.
Conceptually:
Today
a824008... ──► Commit A
Later:
a824008... ──► Commit A
a824008... ──► Commit B
The prefix is no longer sufficient to uniquely identify one object.
Git tooling can require a longer abbreviation when this happens.
That behavior is acceptable for a developer reading Git history.
It is not what we want for a security control protecting executable CI/CD dependencies.
3. Fewer Characters Mean a Smaller Prefix Search Space
A full GitHub commit SHA provides the complete object identifier.
An abbreviated SHA exposes only part of that identifier.
For example, a seven-character hexadecimal prefix represents only 28 bits of prefix space:
7 hexadecimal characters
│
▼
7 × 4 bits
│
▼
28 bits
A malicious party attempting to create another Git object with a chosen short prefix has a dramatically smaller search problem than producing a collision against the complete SHA.
This does not mean that every partial SHA is immediately exploitable.
It means that intentionally reducing the identifier is the wrong security trade-off when the full immutable identifier is already available.
GitHub's security guidance specifically states that pinning to the full commit SHA helps mitigate repository-compromise attacks because an attacker would need to produce a valid Git object with the required full SHA collision.
4. A Partial SHA Does Not Satisfy GitHub's Full-SHA Security Control
GitHub provides a native policy named:
Require actions to be pinned to a full-length commit SHA
When this policy is enabled, Actions must use a full-length commit SHA.
So these references should not be accepted as the security standard:
# Branch — mutable
uses: vendor/action@main
# Tag — potentially movable
uses: vendor/action@v3
# Specific-looking tag — still a tag
uses: vendor/action@v3.2.1
# Abbreviated SHA — incomplete identifier
uses: vendor/action@a824008
# Longer but still abbreviated SHA
uses: vendor/action@a82400808575
The expected pattern is:
uses: vendor/action@a824008085750b8e136effc585c3cd6082bd575f
For readability:
uses: vendor/action@a824008085750b8e136effc585c3cd6082bd575f # v3.2.1
The version comment is for humans.
The full commit SHA is the security control.
How to Block Partial SHAs in GitHub
The best enforcement point is GitHub itself.
Do not depend only on documentation such as:
"Developers should use full SHAs."
Enforce it.
For an organization:
GitHub
│
▼
Organization
│
▼
Settings
│
▼
Actions
│
▼
General
│
▼
Policies
│
▼
Enable:
Require actions to be pinned to a full-length commit SHA
After this policy is enabled, Actions referenced by tags, branches, or abbreviated SHAs do not satisfy the policy.
For GitHub Enterprise Cloud, the equivalent enterprise-level control is available under:
Enterprise
│
▼
Policies
│
▼
Actions
│
▼
Require actions to be pinned to a full-length commit SHA
This is preferable when you want the requirement applied consistently across organizations.
The resulting control becomes:
Workflow references Action
│
▼
Is the Action pinned to a
FULL-LENGTH COMMIT SHA?
│
┌───┴───┐
│ │
YES NO
│ │
▼ ▼
Continue Policy prevents
use of the Action
That means the organization no longer needs to rely on developers remembering whether:
a824008
is "long enough."
GitHub performs the policy decision.
Why Full SHA Pinning Stops Tag-Retargeting Attacks
Consider this workflow:
uses: vendor/action@v3
Originally:
v3 ─────► SAFE COMMIT
The attacker compromises the upstream repository:
v3 ─────► MALICIOUS COMMIT
Your next workflow execution follows v3.
Now compare that with:
uses: vendor/action@8b7c...full-commit-sha...
The attacker moves v3:
┌──► MALICIOUS COMMIT
│
v3 ──────────────────┘
YOUR WORKFLOW
FULL SHA ───────────────► SAFE COMMIT
Your workflow does not care where v3 now points.
It asks GitHub for the exact previously selected commit.
The attacker's changed tag therefore does not automatically change the code your pipeline executes.
That is the protection.
How to Implement Full Commit SHA Pinning
Suppose your workflow currently contains:
- name: Run security scanner
uses: vendor/security-action@v3
Do not simply find some random commit SHA and paste it into the workflow.
The objective is:
Trusted Release
│
▼
Verify Upstream Repository
│
▼
Review Release / Commit
│
▼
Obtain Full Commit SHA
│
▼
Pin Workflow
Verify that the SHA came from the Action's actual repository rather than from a fork.
Then change:
uses: vendor/security-action@v3
to:
uses: vendor/security-action@FULL_40_CHARACTER_COMMIT_SHA
A useful operational convention is keeping the human-readable version in a comment:
uses: vendor/security-action@FULL_40_CHARACTER_COMMIT_SHA # v3.2.1
The security control is:
FULL_40_CHARACTER_COMMIT_SHA
The comment is simply there so humans can understand which upstream release the SHA represents.
Before and After
Before
name: Security Scan
on:
push:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: vendor/security-action@v3
Trust model:
Workflow
│
▼
v3
│
▼
Whatever commit v3 currently references
After SHA Pinning
name: Security Scan
on:
push:
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@FULL_VERIFIED_SHA # v4
- uses: vendor/security-action@FULL_VERIFIED_SHA # v3.x.x
Trust model:
Workflow
│
▼
Exact reviewed commit
Notice another security improvement:
permissions:
contents: read
Pinning protects the identity of the Action code you selected.
Least privilege limits the damage if something still goes wrong.
You want both.
SHA Pinning Is Not a Magic Security Shield
This is one of the most important parts of the discussion.
A statement such as:
"We SHA-pin all GitHub Actions, therefore GitHub Actions supply-chain attacks cannot affect us."
would be incorrect.
SHA pinning solves an important problem:
Prevent an upstream mutable reference
from silently changing the code you execute.
It does not prove that the pinned code itself is safe.
Imagine you approve:
Commit X
but Commit X was already malicious.
Pinning it means:
You have now immutably pinned malicious code.
The security process therefore needs two stages:
VERIFY THE CODE
+
PIN THE CODE
There are additional limitations.
An Action may itself download scripts, packages, container images, binaries, or other content while running.
Conceptually:
Pinned Action SHA
│
▼
Trusted Action Code
│
▼
Downloads something from Internet
│
▼
Mutable external dependency
Pinning the Action does not automatically make everything that Action later retrieves immutable.
This is why Action review remains important.
Why Least Privilege Matters Just as Much
Assume a malicious Action somehow executes.
Compare these environments.
Environment A
GITHUB_TOKEN
│
├── contents: write
├── packages: write
├── pull-requests: write
└── deployments: write
Potential blast radius:
LARGE
Now:
Environment B
GITHUB_TOKEN
│
└── contents: read
Potential blast radius:
SIGNIFICANTLY SMALLER
For example:
permissions:
contents: read
Then grant additional permissions only to jobs that genuinely require them.
jobs:
release:
permissions:
contents: write
packages: write
The security principle is simple:
Assume eventually something may fail.
Then design the permissions so that
failure does not automatically become catastrophe.
Protecting Cloud Credentials
Another common CI/CD pattern is storing long-lived AWS, Azure, or GCP credentials as GitHub secrets.
A better architecture, where supported, is workload identity using OpenID Connect (OIDC).
Instead of:
GitHub Secrets
│
▼
Long-lived AWS Access Key
use:
GitHub Workflow
│
▼
OIDC Identity
│
▼
Cloud IAM Trust Policy
│
▼
Short-lived Credentials
This does not eliminate Action risk.
But it can reduce the usefulness and lifetime of stolen credentials.
Organizations Can Enforce SHA Pinning
Relying entirely on developers remembering this rule is not ideal.
Security controls are stronger when policy enforcement replaces documentation.
GitHub provides organization-level policies that can require Actions to be referenced using full-length commit SHAs.
Conceptually:
Developer workflow
│
▼
GitHub Organization Policy
│
├── Full SHA?
│ │
│ ├── YES ──► Allowed
│ │
│ └── NO ───► Blocked
│
▼
Workflow execution
This turns:
"Please remember to pin Actions"
into:
"Unpinned Actions cannot execute."
For an enterprise environment, that is a much stronger control.
Restrict Which Actions Are Allowed — And Enforce It in GitHub
SHA pinning answers:
Which exact commit of this Action are we running?
But supply-chain governance requires another question:
Should this Action be permitted in the organization at all?
A strong control model therefore has multiple gates:
Developer adds Action
│
▼
Is the Action source approved?
│
┌───┴───┐
│ │
YES NO
│ │
▼ ▼
Full SHA? BLOCK
│
┌──┴───┐
│ │
YES NO
│ │
▼ ▼
Run BLOCK
with
restricted
permissions
GitHub provides native controls for the first two gates.
GitHub Control 1: Restrict Which Actions May Run
At the organization level, navigate to:
GitHub
│
▼
Organization
│
▼
Settings
│
▼
Actions
│
▼
General
│
▼
Policies
Instead of allowing every public Action, select the option that allows Actions from your organization plus selected non-organization Actions and reusable workflows.
GitHub then provides controls to allow:
Actions created by GitHub
and/or
Marketplace Actions by verified creators
and/or
Specific Actions / repositories
For stronger supply-chain governance, explicitly allow only the third-party Actions your engineering and security teams have approved.
For example, your approved list could conceptually contain:
actions/checkout@<FULL_APPROVED_SHA>
vendor/security-action@<FULL_APPROVED_SHA>
vendor/build-action@<FULL_APPROVED_SHA>
GitHub supports the same OWNER/REPOSITORY@TAG-OR-SHA syntax used inside workflows when defining specific allowed Actions.
You can therefore choose between two governance models.
Model A — Approve the Action Repository
Approved:
vendor/security-action
Then combine that allowlist with the global full-SHA requirement.
This means developers may use approved commits from that Action repository, but every workflow reference must still be a full SHA.
Operationally:
Approved Action repository
+
Full-SHA enforcement
+
Pull-request review
This is usually easier to operate.
Model B — Approve the Exact Action SHA
For higher assurance, allow only:
vendor/security-action@FULL_APPROVED_SHA
Now both the Action and the exact commit are centrally approved.
The trade-off is operational overhead:
New Action release
│
▼
Security review
│
▼
New SHA approved
│
▼
Organization allowlist updated
│
▼
Workflow PR merged
This is stricter, but every Action upgrade becomes a governed change.
GitHub Control 2: Require Full-Length Commit SHA Pinning
On the same organization Actions policy page, enable:
Require actions to be pinned to a full-length commit SHA
GitHub states that when this control is enabled, all Actions must use a full-length commit SHA.
This includes Actions:
Authored by GitHub
+
Inside your organization
+
Third-party Actions
A significant exception is reusable workflows: GitHub's documentation states that reusable workflows may still be referenced by tag under this particular setting.
The security flow is therefore:
Workflow
│
▼
Action allowed by organization policy?
│
┌─┴──────────────┐
│ │
YES NO
│ │
▼ ▼
Full-length BLOCK
commit SHA?
│
┌┴───────────────┐
│ │
YES NO
│ │
▼ ▼
Action may BLOCK
execute
This is the control that turns:
"We recommend SHA pinning"
into:
"GitHub will not allow an Action that does not meet
the organization's full-SHA policy."
GitHub Control 3: Restrict the Default GITHUB_TOKEN
The first two controls govern what code may execute.
You should separately control what that code may do.
In the same organization area:
Organization
│
▼
Settings
│
▼
Actions
│
▼
General
│
▼
Workflow permissions
Choose the restricted default where GITHUB_TOKEN receives read access to repository contents and packages instead of broad read/write permissions.
Then workflows should explicitly request only the additional permissions genuinely required.
For example:
permissions:
contents: read
A release job that genuinely needs more could define its permissions separately:
jobs:
release:
permissions:
contents: write
packages: write
This is a separate security layer.
The Action allowlist and full-SHA policy answer:
WHAT MAY EXECUTE?
GITHUB_TOKEN permissions answer:
WHAT CAN IT DO IF IT EXECUTES?
You need both.
The GitHub-Enforced Supply-Chain Flow
Putting the controls together:
Developer adds third-party Action
│
▼
ORGANIZATION ACTION POLICY
│
▼
Is Action approved?
│ │
YES NO
│ │
▼ ▼
REQUIRE FULL SHA BLOCK
│
▼
Is reference full-length?
│ │
YES NO
│ │
▼ ▼
Workflow BLOCK
starts
│
▼
Restricted default GITHUB_TOKEN
│
▼
Explicit per-job permissions
│
▼
EXECUTE
This is much stronger than:
Found Action on Marketplace
│
▼
uses: vendor/action@v3
│
▼
Run with broad token permissions
A Marketplace presence or verified-creator status is a useful trust signal.
It is not the same as:
Approved dependency
+
Immutable Action reference
+
Least-privilege execution
One Important Edge Case: Local Actions
GitHub's Action access policies do not restrict local Actions referenced from the runner filesystem using paths such as:
uses: ./.github/actions/my-action
That code is controlled through your own repository instead.
So protect it through:
Branch protection / rulesets
+
CODEOWNERS
+
Required pull-request reviews
+
Workflow and Action code review
This is another reason .github/ should be treated as security-sensitive production code.
Protect the Workflow Files Themselves
Now imagine you secure every Action:
uses: vendor/action@FULL_SHA
but any developer can change:
.github/workflows/*
without specialist review.
An attacker who compromises a developer account might simply replace:
uses: vendor/action@SAFE_SHA
with:
uses: attacker/action@MALICIOUS_SHA
Technically it is SHA-pinned.
It is also malicious.
For example:
.github/workflows/ @security-team @platform-team
Now your trust chain becomes:
Workflow change
│
▼
CODEOWNER Review
│
▼
Approved Action
│
▼
Reviewed Commit
│
▼
Full SHA
│
▼
Least Privilege
│
▼
Execution
That is much closer to a production-grade supply-chain security model.
But How Do We Update SHA-Pinned Actions?
This is the operational trade-off.
With:
uses: vendor/action@v3
you may automatically consume whatever the maintainer decides v3 should reference.
With:
uses: vendor/action@FULL_SHA
you intentionally stop automatic movement.
That is the security benefit.
But it also means updates must be managed deliberately.
The correct operational pattern is:
New Action Release
│
▼
Update PR
│
▼
Review changes
│
▼
Verify upstream SHA
│
▼
Security / CODEOWNER review
│
▼
Merge
You are turning dependency updates from:
implicit trust
into:
explicit change management
For security-sensitive CI/CD systems, that is usually the desired behavior.
Think of GitHub Actions as Production Dependencies
One common mistake is treating .github/workflows/*.yml as harmless automation configuration.
It is better to think of it as part of your production codebase.
Consider:
application.py
package.json
Dockerfile
Terraform
Helm chart
.github/workflows/deploy.yml
From a security perspective, that final file may be one of the most sensitive files in the repository.
Why?
Because it defines:
WHAT EXECUTES
+
WITH WHICH CREDENTIALS
+
AGAINST WHICH ENVIRONMENT
That is a security boundary.
A Practical GitHub Actions Security Baseline
For production repositories, a reasonable baseline is:
- [ ] Pin every external Action to a verified full-length commit SHA.
- [ ] Block tags, branches, and abbreviated/partial SHAs for third-party Actions.
- [ ] Enable GitHub's Require actions to be pinned to a full-length commit SHA policy.
- [ ] Restrict Actions to an approved organization/enterprise allowlist.
- [ ] Keep the corresponding release version as a comment for readability.
- [ ] Verify the SHA comes from the original Action repository, not a fork.
- [ ] Review Action source code and changes before approving updates.
- [ ] Configure
GITHUB_TOKENwith minimum required permissions. - [ ] Restrict which Actions and reusable workflows the organization allows.
- [ ] Protect
.github/workflows/through branch protection andCODEOWNERS. - [ ] Prefer short-lived OIDC cloud authentication over long-lived static credentials where possible.
- [ ] Do not assume SHA pinning protects runtime dependencies downloaded by the Action.
- [ ] Treat an Action version update as a software supply-chain change, not routine YAML maintenance.
The Security Model in One Picture
Without sufficient controls:
INTERNET
│
▼
THIRD-PARTY ACTION
│
mutable @v3 tag
│
▼
GitHub Runner
│
┌──────────┼───────────┐
│ │ │
▼ ▼ ▼
Source Tokens Cloud
Code Access
│
▼
RISK
With stronger controls:
APPROVED ACTION
│
▼
VERIFIED RELEASE
│
▼
REVIEWED COMMIT
│
▼
FULL COMMIT SHA
│
▼
GitHub Runner
│
LEAST PRIVILEGE
│
┌────────┴────────┐
▼ ▼
Limited Token OIDC Identity
│
▼
Short-lived Access
The goal is not to pretend compromise is impossible.
The goal is to reduce both:
LIKELIHOOD
and:
BLAST RADIUS
Final Takeaway
The most important conceptual shift is this:
A third-party GitHub Action is third-party code running inside your CI/CD trust boundary.
Once you understand that, SHA pinning stops looking like a minor Git hygiene recommendation.
It becomes a supply-chain security control.
This:
uses: vendor/action@v3
means:
Trust whatever code this movable reference resolves to.
This:
uses: vendor/action@FULL_VERIFIED_COMMIT_SHA
means:
Execute exactly the commit we reviewed and approved.
That difference matters.
The 2025 tj-actions/changed-files incident demonstrated precisely why. Attackers were able to redirect version tags to malicious code, and downstream repositories trusted those references.
But mature GitHub Actions security should not stop there.
The stronger model is:
APPROVED THIRD-PARTY ACTION
+
SOURCE REVIEW
+
FULL SHA PINNING
+
LEAST-PRIVILEGE GITHUB_TOKEN
+
OIDC / SHORT-LIVED CREDENTIALS
+
CODEOWNERS
+
ORGANIZATION ACTION POLICY
+
CONTROLLED UPDATE PROCESS
Because the real security question is not:
"Do we trust this GitHub Action?"
It is:
"Exactly what code are we allowing to execute inside our software delivery pipeline, what can that code access, and what happens if that trust is wrong?"
That is the question a secure software supply chain needs to answer.
The important point is that these controls are implemented across GitHub Organization settings, repository controls, workflow YAML, and cloud IAM. There is no single switch that enables all of them.
1. Restrict GitHub Actions to Approved Sources
Start at:
GitHub Organization
↓
Settings
↓
Actions
↓
General
↓
Actions permissions
Instead of allowing every Action, configure the organization to allow your own Actions plus only selected external Actions and reusable workflows.
For example, your approved Action list might include:
actions/checkout@*
anchore/scan-action@*
aws-actions/configure-aws-credentials@*
Do not treat inclusion in the GitHub Marketplace as automatic security approval. Security or Platform Engineering should review the Action's repository, maintainer, action.yml, scripts, dependencies, runtime downloads, permissions, and release history before placing it on the approved list.
2. Enforce Full Commit SHA Pinning
On the same Organization → Settings → Actions → General page, enable:
☑ Require actions to be pinned to a full-length commit SHA
Do not allow:
uses: actions/checkout@main
or:
uses: actions/checkout@v4
or a partial SHA:
uses: actions/checkout@11bd719
Use the complete commit identifier:
uses: actions/checkout@<FULL_40_CHARACTER_COMMIT_SHA> # v4
The version comment is for human readability. The full SHA is the security reference.
Approved Action?
│
▼
Full commit SHA?
│
┌───┴───┐
│ │
YES NO
│ │
▼ ▼
ALLOW BLOCK
3. Make GITHUB_TOKEN Read-Only by Default
Configure:
Organization
↓
Settings
↓
Actions
↓
General
↓
Workflow permissions
Select the restricted option that gives GITHUB_TOKEN read access to repository contents and packages instead of broad read/write access.
Then make permissions explicit inside workflows:
permissions:
contents: read
The principle is:
ACTION ALLOWLIST + SHA PINNING
↓
Controls WHAT CODE can execute
GITHUB_TOKEN PERMISSIONS
↓
Controls WHAT THAT CODE can do
4. Replace Long-Lived Cloud Credentials with OIDC
For AWS, Azure, GCP, and other supported providers, avoid storing permanent cloud access keys in GitHub secrets when workload identity federation is available.
GitHub Actions
│
│ OIDC identity
▼
Cloud IAM
│
▼
Short-lived credentials
The workflow normally requires:
permissions:
contents: read
id-token: write
For example:
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@<FULL_SHA>
- uses: aws-actions/configure-aws-credentials@<FULL_SHA>
with:
role-to-assume: arn:aws:iam::123456789012:role/github-deploy
aws-region: ap-southeast-1
The authentication Action itself should also be SHA-pinned.
5. Protect .github/workflows/ with CODEOWNERS
Create:
.github/CODEOWNERS
and assign ownership of security-sensitive workflow files:
.github/workflows/** @your-org/security-team @your-org/platform-team
.github/actions/** @your-org/security-team @your-org/platform-team
.github/dependabot.yml @your-org/security-team @your-org/platform-team
Then enforce it using a branch ruleset or branch protection:
Repository / Organization
↓
Settings
↓
Rules
↓
Rulesets
↓
Require pull request before merging
↓
Require review from Code Owners
That prevents a developer—or a compromised developer account—from silently replacing:
uses: approved/action@SAFE_FULL_SHA
with:
uses: attacker/action@MALICIOUS_FULL_SHA
SHA pinning does not help if an attacker can simply edit the workflow itself.
6. Manage Action Updates Through Pull Requests
SHA pinning intentionally stops an Action from moving automatically to new code.
New Action Release
│
▼
Dependabot / Update PR
│
▼
Review upstream changes
│
▼
CODEOWNER approval
│
▼
CI / Security checks
│
▼
Merge new full SHA
Configure Dependabot for GitHub Actions:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
For production repositories, these PRs should normally receive Security or Platform review rather than being blindly auto-merged.
Security principle:
If third-party code is allowed to execute inside your CI/CD pipeline, it has entered your software supply chain.Know exactly what you trust.
Know exactly which version you trust.
And give it only the permissions it actually needs.
Top comments (0)