In early 2025, the tj-actions/changed-files GitHub action was compromised, exposing more than 15,000 repositories to a supply chain attack. The attack method was simple: the attacker replaced the v1 tag to point to a malicious commit. Every workflow using uses: tj-actions/changed-files@v1 suddenly started executing attacker-controlled code.
The root cause wasn’t some sophisticated zero-day exploit. It was a design choice most of us made without thinking – relying on mutable version tags instead of immutable commit SHAs.
Understanding software supply chain security in the context of CI/CD pipelines has become critical for any organisation running production workloads through GitHub Actions. If your workflows touch production credentials, deploy to cloud infrastructure, or publish packages, you’ve got the same vulnerability. The good news is hardening your workflows doesn’t require a massive security overhaul. You can migrate to commit SHA pinning in 1-2 weeks, configure OIDC authentication in 2-4 weeks, and layer on runtime monitoring without disrupting your existing pipelines.
Let’s walk through exactly how to do it.
Why Did GitHub Actions Become a Prime Target for Supply Chain Attacks?
GitHub Actions processes over 100 million workflows weekly across millions of repositories. That scale makes it a high-value target for supply chain attacks.
The default workflow configuration uses mutable tags like actions/checkout@v3. These are Git references that can be deleted and re-pointed to different commits. The tj-actions compromise proved this trust is misplaced.
Until February 2023, workflows ran with write permissions by default. A compromised action could push commits, create releases, or modify repository content. Even with more restrictive defaults now in place, many workflows still grant broad permissions.
CI/CD pipelines access production infrastructure, cloud provider APIs, and package registries. They’re sitting on AWS access keys, GitHub personal access tokens, and deployment credentials. Compromise a single popular GitHub Action and you get access to thousands of credential stores.
The attack timeline tells the story. SolarWinds in 2020 compromised the build system. Codecov in 2021 exfiltrated credentials via CI integration. The tj-actions attack in 2025 exploited mutable tags.
Developers implicitly trust actions from the actions/* namespace because GitHub publishes them. That trust often extends to third-party namespaces without scrutiny. Analysis shows 60% of popular actions use mutable dependencies, creating transitive risk even when you’ve hardened your workflows.
How Do I Migrate from Mutable Tags to Commit SHA Pinning in GitHub Actions?
Commit SHA pinning replaces mutable tags with immutable 40-character commit hashes. Instead of actions/checkout@v3, you use actions/checkout@8e5e7e5a8f1e2a3b4c5d6e7f8a9b0c1d2e3f4a5b. The commit SHA is a cryptographic hash that can’t be changed or re-pointed.
Start by identifying all third-party actions in your workflows. Run grep -r "uses:" .github/workflows/ from your repository root.
For each action, navigate to its GitHub repository and locate the version tag you’re using. Click through to see which commit the tag points to. Copy the full 40-character commit SHA.
Now replace it. Change uses: tj-actions/changed-files@v40 to uses: tj-actions/changed-files@2d756ea93da014e7b7df225d13f5e6e43e5c2ee7 # v40.0.2. Notice the comment at the end. This maintains human readability while giving you immutability.
Test the workflow in a pull request branch before merging to main.
Don’t migrate everything at once. Start with workflows that have production deployment access – your highest risk targets (1-3 days). Then move to workflows that access secrets (4-7 days). Finally, handle read-only workflows (8-14 days).
Doesn’t commit SHA pinning break automatic security updates? No. Dependabot supports GitHub Actions updates.
Create .github/dependabot.yml in your repository:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Dependabot will scan your workflows, identify commit SHA pins, check for newer versions, and open PRs with updated SHAs. Review and merge these weekly.
What Branch Protection and Tag Protection Rules Prevent GitHub Actions Attacks?
Branch protection rules enforce code review and status checks before merge. Someone can’t just push a change to .github/workflows/deploy.yml and compromise your production pipeline.
Configure branch protection on your main branch: “Require pull request reviews” (minimum one approval), “Require status checks to pass before merge”, and “Require signed commits”. Find these under Repository Settings → Branches → Branch protection rules.
Configure your CI workflow jobs as required checks. This forces security scans – secret scanning, dependency review, vulnerability checks – to pass before any code merges.
Tag protection rules prevent unauthorised tag deletion or re-pointing, which is exactly the attack vector used in the tj-actions compromise.
Set up tag protection: Repository Settings → Tags → Add protection rule. Use the pattern v* to match all version tags. Enable “Prevent tag deletion” and “Require signed tags”.
Create .github/CODEOWNERS and add .github/workflows/ @yourorg/security-team. This ensures any workflow changes require review from your security team.
For organisations with dozens of repositories, applying protection rules per-repository doesn’t scale. Use organisation rulesets instead. Go to Organisation Settings → Rulesets → New ruleset → Target “All repositories”.
Limit “Allow force pushes” and “Allow deletions” to break-glass administrator accounts with MFA enforcement.
How Do I Configure OIDC to Eliminate Long-Lived Credentials in GitHub Actions?
OIDC (OpenID Connect) replaces long-lived AWS access keys with short-lived federated tokens issued per workflow run. Instead of storing AWS_ACCESS_KEY_ID in GitHub Secrets where it remains valid for 90+ days, you get temporary credentials that expire in 15-60 minutes.
Here’s how it works: GitHub Actions requests a JWT token, AWS STS validates that token against your trust policy, then issues temporary credentials scoped to the specific workflow run.
Setting this up on AWS involves three steps. First, create an OIDC identity provider in IAM. The provider URL is https://token.actions.githubusercontent.com and the audience is sts.amazonaws.com.
Second, create an IAM role with a trust policy that allows your specific GitHub repository. The trust policy condition: token.actions.githubusercontent.com:sub equals repo:yourorg/yourrepo:ref:refs/heads/main. Lock it down to specific repositories and branches.
Third, attach a least-privilege permissions policy. If your workflow deploys to S3, grant s3:PutObject for the specific deployment bucket. Don’t attach s3:* or AdministratorAccess. The most common mistake is attaching excessive IAM permissions because it’s easier.
On the workflow side, add permissions: { id-token: write, contents: read } to your job. Forgetting this causes the “OIDC token not found” error.
Then use aws-actions/configure-aws-credentials@e3dd6d6512e493a47ee3ea56a9890a770ddb8787 # v4 with the role-to-assume parameter pointing to your IAM role ARN.
Phase the migration: identify workflows using long-lived secrets (1-3 days), create OIDC providers and IAM roles (4-7 days), test in staging (8-14 days), roll out to production (15-21 days). Only after confirming OIDC works should you remove long-lived secrets – give yourself a 30-day rollback window.
Validate by verifying AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are absent from repository secrets. Check CloudTrail logs for AssumeRoleWithWebIdentity API calls.
How Does Runtime Monitoring with Harden-Runner Detect GitHub Actions Attacks?
Harden-Runner from StepSecurity monitors GitHub Actions workflow execution at runtime, detecting anomalous behaviour that static analysis can’t catch. You can hash pin every action, configure perfect branch protection, and migrate to OIDC – but if a compromised action executes malicious code, none of those controls stop it. Runtime monitoring fills that gap – and contributes to SLSA build integrity requirements.
Add step-security/harden-runner@v2 as the first step in your job. Configure egress-policy: audit for monitoring mode or egress-policy: block for enforcement mode. Start with audit mode to establish a baseline.
Network monitoring tracks all outbound network calls. If your deployment workflow normally calls api.github.com, registry.npmjs.org, and s3.amazonaws.com, that’s your baseline. When a workflow suddenly calls attacker-controlled-domain.com, Harden-Runner alerts you. This is exactly how the tj-actions attack would have been detected.
File integrity monitoring detects modifications outside expected paths. If a workflow starts writing to /home/runner/.ssh/ for persistence, that triggers an alert.
Process monitoring identifies unexpected process execution. If a workflow that normally runs npm and AWS CLI suddenly spawns a cryptocurrency miner or reverse shell, that’s flagged immediately.
The correlation engine compares runtime behaviour against baselines from previous runs. Deviations – new network destinations, unusual file access patterns, unexpected processes – become potential compromise indicators.
Harden-Runner generates provenance attestation recording observed behaviour, contributing to SLSA Level 2 requirements.
Performance impact is minimal. Sub-second overhead per step, less than 2% measured across thousands of runs.
Deploy in audit mode for 2-4 weeks to establish baselines. Then enable block mode for highest-risk workflows – anything touching production infrastructure or handling secrets. After validating enforcement mode isn’t breaking legitimate workflows, expand to all workflows.
How Do I Audit Third-Party GitHub Actions Across My Organisation?
You need to know what you’re running. Inventory all third-party actions, assess maintainer health, review permissions, and identify high-risk dependencies.
Use the GitHub API to enumerate repositories and parse uses: directives across your organisation. The CLI approach is gh api /orgs/{org}/repos | jq '.[].workflows_url' but you’ll want to script this to fetch and parse workflow files.
Categorise by risk. Actions from the actions/* namespace are low risk – GitHub publishes them. Verified publishers are medium risk. Individual maintainers are high risk and require more scrutiny.
Assess maintainer health using OpenSSF Scorecard. This tool evaluates repositories across multiple security dimensions and produces a score from 0-10. For more detailed guidance on evaluating security tools and frameworks, see our supply chain security tool selection framework.
Set minimum acceptable scores. For workflows touching production or accessing secrets, require 7.0 or higher. For non-sensitive workflows, 5.0 is reasonable. Red flags: scores below 3.0, failed “Dangerous-Workflow” checks, or failed “Token-Permissions” checks.
actions/checkout scores 9.8, actions/setup-node scores 9.5. Many third-party actions fall in the 3.0-6.0 range.
Review the permissions each action requests. Flag excessive permission requests for manual review.
Check update frequency. Actions with the last commit more than 12 months ago might be abandoned. Consider replacing them with actively maintained alternatives.
Generate a compliance report showing what percentage of your actions use tags versus commit SHAs. Target 100% SHA adoption.
Set up a remediation workflow: any action scoring below 3.0 requires replacement or manual security review within 30 days. Either find a better-maintained alternative or perform your own security audit.
How Do I Maintain GitHub Actions Security Over Time?
Hardening your workflows isn’t a one-time exercise. New actions get added weekly. Existing actions release updates. The threat landscape evolves. You need continuous maintenance.
Configure Dependabot to track GitHub Actions and auto-generate pull requests when new commits are available. Weekly schedule is reasonable.
If you’re on GitHub Enterprise, enable GitHub Advanced Security dependency alerts for actions with known vulnerabilities.
Run quarterly manual audits. Re-execute your third-party action inventory and OpenSSF Scorecard assessment. Track metrics – is your average Scorecard score improving?
Enforce CODEOWNERS review for .github/workflows/* changes. This ensures your security team has visibility into new action adoption.
Conduct developer training on secure workflow authorship semi-annually. Cover commit SHA pinning, minimal permissions, and proper secret handling.
Define an incident response runbook for suspected workflow compromise: disable GitHub Actions organisation-wide, rotate all secrets and credentials, audit CloudTrail and workflow run logs, perform forensic analysis, re-enable Actions once you’ve remediated the compromise. Having this written down before you need it saves time during an incident.
Track metrics: SHA pinning adoption percentage (target 100%), average OpenSSF Scorecard across all actions (target above 7.0), number of long-lived secrets in use (target 50% reduction through OIDC migration).
Enable secret scanning push protection to prevent developers from accidentally committing credentials. Use dependency review to block vulnerable action versions.
Subscribe to threat intelligence sources: GitHub Security Advisories (GHSA), StepSecurity threat reports, and Hacker News for emerging attack techniques. When new attack vectors are disclosed, assess whether your workflows are vulnerable and remediate proactively.
Review and update your organisation security policy annually. Security isn’t static – your policies shouldn’t be either. For a comprehensive overview of supply chain security strategies beyond GitHub Actions hardening, see our comprehensive guide to software supply chain security.
FAQ
What is the difference between commit SHA pinning and semantic versioning tags?
Semantic versioning tags like @v3 or @v3.5.2 are Git references that point to commits. They can be deleted and re-pointed to different code – that’s what enabled the tj-actions compromise.
Commit SHAs like @8e5e7e5a8f1e2a3b4c5d6e7f8a9b0c1d2e3f4a5b are cryptographic hashes. They’re immutable. You cannot modify them retroactively.
When you pin to a SHA, you guarantee that the exact same code executes every time. Best practice is to use the commit SHA with a version comment – @8e5e7e5a # v3.5.2 – so you get both security and readability.
Does commit SHA pinning break automatic security updates?
No. Dependabot supports GitHub Actions and creates pull requests when new commits are available for your pinned actions.
Configure .github/dependabot.yml with package-ecosystem: "github-actions" and Dependabot will scan your workflows weekly, identify newer versions, and open PRs with updated commit SHAs. It preserves version comments automatically.
Can I use OIDC with cloud providers other than AWS?
Yes. Major cloud providers support OIDC federation.
Azure uses Entra ID workload identity. You create a federated credential in your App Registration and configure the subject claim filter to match your repository.
Google Cloud offers Workload Identity Federation. Create a Workload Identity Pool and configure a provider with https://token.actions.githubusercontent.com as the issuer.
The workflow pattern is consistent: request a JWT from GitHub, exchange it with your cloud provider, receive temporary credentials scoped to your workflow run.
How much does GitHub Advanced Security cost for a 100-person engineering team?
Pricing is $49 per active committer per month for GitHub Enterprise Cloud. For 100 committers, that’s $4,900 monthly.
You get secret scanning push protection, dependency review, code scanning with CodeQL, and the security overview dashboard.
An active committer is any developer who committed to a private repository in the last 90 days. Public repositories get these features free.
For smaller organisations, you can combine free features like Dependabot alerts and public repository scanning with third-party tools like StepSecurity or Snyk to get comparable coverage at lower cost.
What happens if Harden-Runner blocks a legitimate workflow step?
When Harden-Runner is in block mode and detects a policy violation, your workflow fails. You’ll need to investigate and adjust your policy.
The recommended approach is running egress-policy: audit for 2-4 weeks to establish a baseline before enabling block mode. This identifies legitimate behaviour that needs to be whitelisted.
When you do get a false positive, review the StepSecurity dashboard to see what network call was blocked. If it’s legitimate, add it to your allowed endpoints in the workflow configuration.
If you need an escape hatch during production deployment, temporarily set egress-policy: audit for that specific workflow while you troubleshoot.
How do I handle private GitHub Actions in my organisation?
Private actions stored in internal repositories require different authentication than public Marketplace actions.
For actions in the same repository, use GITHUB_TOKEN with contents: read permission. For cross-repository access, use GitHub App installation tokens instead of personal access tokens.
Private actions bypass Marketplace security review entirely. Apply the same hardening standards you use for your workflows – commit SHA pinning, CODEOWNERS review, automated testing.
What OpenSSF Scorecard score should I require for third-party actions?
OpenSSF Scorecard ranges from 0 to 10, evaluating security practices like branch protection, dependency updates, and vulnerability disclosure.
For workflows touching production infrastructure or accessing secrets, require a minimum score of 7.0. For non-sensitive workflows, 5.0 is acceptable.
Red flags that should trigger immediate review: scores below 3.0, failed “Dangerous-Workflow” checks, or failed “Token-Permissions” checks.
Can I enforce organisation-wide GitHub Actions security policies?
Yes, if you have GitHub Enterprise.
Organisation rulesets let you apply branch protection, tag protection, and workflow permissions across all repositories. Go to Organisation Settings → Rulesets → New ruleset → Target “All repositories” and configure your required checks, signed commits, and permission defaults.
You can set organisation-level defaults for workflow permissions – permissions: read-all by default, requiring explicit write grants per workflow.
Free and Team plans only support repository-level policies. Organisation-level enforcement requires Enterprise.
How long does it take to migrate 50 workflows from tags to commit SHA pinning?
Timeline depends on workflow complexity. Simple workflows take 1-2 hours each. Complex workflows with many dependencies might take 4-6 hours.
For 50 workflows, use a phased approach: inventory and prioritise (1 day), migrate the 10 highest-risk workflows (2-3 days), handle the remaining 40 workflows (5-7 days), and allocate 2-3 days for testing and validation.
Total duration is 10-14 days for an experienced team, or 15-21 days if you’re learning as you go.
You can script the SHA replacement to generate PRs automatically, which reduces manual effort to under an hour per workflow.
What are the most common mistakes when implementing OIDC for GitHub Actions?
First mistake: overly permissive IAM trust policies that allow any repository in your organisation instead of locking down to specific repositories and branches.
Second mistake: attaching excessive IAM permissions like AdministratorAccess instead of least-privilege policies scoped to specific resources.
Third mistake: forgetting to add permissions: { id-token: write } to the workflow job, which causes “OIDC token not found” errors.
Fourth mistake: deleting long-lived secrets before validating OIDC works, creating a production outage if you need to roll back.
Prevention strategy: test OIDC in staging workflows first, validate CloudTrail shows AssumeRoleWithWebIdentity calls, and maintain secrets for a 30-day rollback window after migration.
How do I convince my team to adopt commit SHA pinning despite the readability loss?
The objection is valid – commit SHAs are unreadable. Developers can’t tell at a glance which version is pinned.
The solution is appending version comments: uses: actions/checkout@8e5e7e5a # v3.5.2. You get immutability with human readability.
Dependabot preserves these comments automatically when creating update PRs, so there’s no ongoing maintenance burden.
For the risk framing, show them the tj-actions incident timeline. Walk through how tag mutation works and why SHA pinning eliminates that entire attack vector.
Start with production deployment workflows – the highest risk, highest value targets. Once the team sees there’s no operational impact, expand to other workflows.
Does runtime monitoring with Harden-Runner work for self-hosted runners?
Yes, Harden-Runner supports self-hosted runners on Linux – Ubuntu, Debian, and RHEL.
Installation uses the same workflow configuration (step-security/harden-runner@v2). The monitoring agent installs on your runner during step execution.
Performance overhead is slightly higher on self-hosted runners – 1-3% CPU – compared to GitHub-hosted runners due to the instrumentation.
Network monitoring is particularly valuable for self-hosted runners with access to internal networks. You can detect lateral movement attempts that wouldn’t be possible from GitHub-hosted runners.
Windows and macOS self-hosted runners aren’t supported as of 2025. Linux only.