You are currently viewing How to Secure GitHub Actions Against Supply Chain Attacks

How to Secure GitHub Actions Against Supply Chain Attacks


GitHub Actions makes it easy to automate testing, builds and deployments. Add a workflow file to your repository, push some code, and GitHub can take care of the rest.

It’s convenient, but there’s something worth remembering: a workflow isn’t just a configuration file. It runs commands, downloads dependencies and may have access to repository tokens, package registries or production deployment credentials.

That’s what makes GitHub Actions an attractive target for supply chain attacks. An attacker doesn’t always need to change your application code. Sometimes compromising a dependency used by your build pipeline is enough.

The good news is that you don’t need an elaborate security platform to fix several of the most common problems. In this article, we’ll look at practical GitHub Actions security measures, including pinning dependencies, limiting permissions, handling pull requests safely and protecting deployment credentials.

How Supply Chain Attacks Affect GitHub Actions

A software supply chain attack targets something your application depends on rather than attacking the application directly.

In a GitHub Actions workflow, those dependencies can include third-party actions, npm packages, Composer packages, shell scripts, container images and tools downloaded during a build.

For example, suppose your project uses a third-party action to prepare a deployment. If an attacker manages to replace the code behind the version your workflow references, the next build could execute malicious code.

Depending on the workflow’s permissions, that code might be able to read sensitive files, access credentials, modify a release or interfere with published software.

These aren’t just hypothetical concerns. GitHub’s 2026 security updates discuss attacks involving compromised Actions dependencies, credential theft and abuse of CI/CD systems. See GitHub’s supply chain security update for a breakdown of the attack patterns and the defenses being introduced.

1. Pin Actions to a Full Commit SHA

This is one of the first things I’d check in an existing workflow.

GitHub Actions supports references such as:

uses: actions/checkout@v4
JSON

It’s convenient because you can follow a major version without editing the workflow every time an update is released.

The problem is that a version tag is a reference, not a guarantee that the underlying code will never change. If the reference is moved, your workflow may run different code on a future build.

GitHub recommends pinning actions to a complete commit SHA when you need an immutable reference. The format looks like this:

# Replace the placeholder with a reviewed 40-character commit SHA.
uses: actions/checkout@<FULL_COMMIT_SHA>
JSON

Don’t copy a random SHA from a blog post, including this example. Open the action’s official repository, choose a release you trust, verify its commit and use the corresponding full SHA.

This also creates a maintenance responsibility. A pinned action won’t automatically receive fixes when its maintainers publish a new release. You’ll need to review updates and move to newer verified commits.

Dependabot can help manage these changes by opening pull requests for GitHub Actions updates. But those pull requests still deserve review, especially when they modify the code your workflows execute.

GitHub explains SHA pinning and action verification in its Secure use reference.

2. Give Each Workflow Only the Permissions It Needs

Every GitHub Actions job can receive a GITHUB_TOKEN. The token is useful because it allows workflows to interact with the repository and GitHub APIs without creating a separate personal access token.

But a job that only runs tests shouldn’t need permission to publish releases, modify repository files or change pull requests.

For a straightforward test workflow, explicitly request read-only access:

name: Basic Checks

on:
  pull_request:

permissions:
  contents: read

jobs:
  checks:
    runs-on: ubuntu-latest

    steps:
      - name: Verify workflow
        run: echo "Running checks with read-only repository access"
JSON

This is a minimal working example. It doesn’t check out your project or run a test suite yet; it simply demonstrates how to restrict token permissions.

Once you add a checkout action and the commands required by your project, keep those permissions unless a specific step genuinely needs more access.

You can also set permissions at the job level. This is useful when one workflow contains separate testing and publishing jobs.

jobs:
  tests:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - run: echo "Run tests here"

  release:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - run: echo "Release steps go here"
JSON

The second job has broader access because publishing a release may require it. That’s intentional, but in a real workflow it should also be restricted to trusted events and protected release processes.

In short, don’t give the entire pipeline write access just because one step might eventually need it.

3. Be Very Careful With pull_request_target

This is one of the GitHub Actions settings developers need to understand before copying a workflow from another repository.

The normal pull_request event is designed for work such as testing proposed changes. Forked pull requests generally receive restricted access to repository credentials.

The pull_request_target event is different. It runs in the context of the base repository, which can make it suitable for certain trusted repository-management tasks but dangerous when combined with untrusted pull request code.

For example, imagine a workflow that runs with privileged credentials, checks out code from an external contributor’s pull request and executes its build scripts. If that contribution contains malicious code, the build can become an entry point into a more trusted environment.

For ordinary tests, prefer:

on:
  pull_request:
JSON

Do not switch to pull_request_target simply to make permissions or secrets easier to access.

If your repository has a legitimate use case for it, keep privileged work isolated from untrusted code and follow GitHub’s pull_request_target security guidance.

4. Don’t Insert Untrusted Input Directly Into Shell Scripts

A workflow can contain security problems even when all its actions come from trusted repositories.

Consider a step that reads the title of a pull request:

# Unsafe: untrusted text is inserted into the shell script.
- name: Print pull request title
  run: echo "${{ github.event.pull_request.title }}"
JSON

The pull request title is controlled by the person opening the pull request. GitHub evaluates expressions before passing the resulting script to the shell, which can create an injection risk if the value contains specially crafted characters.

A safer approach is to pass the value through an environment variable:

- name: Print pull request title
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: printf '%s\n' "$PR_TITLE"
JSON

This prevents the pull request title from becoming part of the generated shell script itself. You still need to validate and safely handle the value if another command later interprets it as a filename, argument or executable instruction.

The same caution applies to issue titles, branch names, commit messages and other values that contributors may control.

GitHub covers this attack pattern in its workflow security documentation.

5. Avoid Long-Lived Cloud Credentials Where Possible

Many deployment workflows begin with a cloud access key stored in GitHub Secrets.

It’s easy to understand why. The build needs access to a hosting platform, a container registry or some other service, and an access key seems like the quickest solution.

The downside is that a long-lived key can remain useful to an attacker even after the workflow has finished. If the key is stolen, the attacker may be able to use it elsewhere until you revoke or rotate it.

Where your cloud provider supports it, GitHub Actions can use OpenID Connect (OIDC) to request short-lived credentials instead of relying on a stored long-term secret.

An OIDC-enabled deployment job typically needs a permission such as:

permissions:
  contents: read
  id-token: write
JSON

That does not automatically give the job access to your cloud account. You also need to configure the cloud provider to trust the correct repository, workflow or deployment environment and issue only the privileges required.

Be careful with those trust conditions. If you configure them too broadly, a workflow you didn’t intend to authorize might be able to request credentials.

GitHub provides official OIDC deployment documentation with provider-specific setup guidance.

6. Protect Deployment Jobs With GitHub Environments

Even with a carefully configured token, you should consider which workflows are allowed to deploy to production.

GitHub Environments let repositories apply deployment-related controls, including branch restrictions and, where available for your repository and plan, approval requirements.

A deployment job might declare:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Deployment placeholder
        run: echo "Deploy only after environment rules allow it"
JSON

The name production doesn’t protect anything by itself. You still need to configure the environment’s protection rules in the repository settings.

For a small project, separating testing from production deployment is already a useful improvement. For larger projects, environment-specific permissions and approvals make it easier to control sensitive changes.

7. Review Dependencies Before They Enter Your Build

Action pinning is important, but it only covers one part of the software supply chain.

Suppose your workflow runs:

npm install
JavaScript

That command downloads and installs JavaScript dependencies, and package lifecycle scripts may execute during installation.

For a project with a committed npm lockfile, a more predictable CI installation normally uses:

npm ci
JavaScript

It installs according to the lockfile rather than updating it as part of a normal install. That improves reproducibility, although it doesn’t make every dependency safe.

The same basic issue applies to PHP and other ecosystems: locking package versions helps you control what gets installed, but you still need to consider the security of those packages and any scripts they execute.

GitHub’s dependency review tools can help identify vulnerable dependencies introduced by a pull request. Depending on the repository type and enabled GitHub security features, you may also be able to require dependency review checks before changes are merged.

See GitHub’s dependency review documentation for eligibility and setup.

8. Review Who Can Change Workflow Files

Your workflow files live inside the repository, usually under:

.github/workflows/
JavaScript

Anyone who can successfully change a sensitive workflow may be able to change what gets executed during future builds or deployments.

That’s why workflow changes deserve the same attention as application code, and sometimes more.

One useful option is a CODEOWNERS file that requests review from the people responsible for your CI/CD configuration.

# .github/CODEOWNERS
/.github/workflows/ @your-org/security-team
JavaScript

Replace that example team with an actual GitHub user or team that has the required repository access.

To make ownership meaningful, combine it with branch protection or repository rulesets that require the relevant reviews before changes can be merged.

Also review the list of actions and reusable workflows allowed to run in your organization. Not every repository needs access to every third-party action published on GitHub.

9. Treat Runners and Build Artifacts as Part of Your Security Boundary

A GitHub Actions runner is the machine that actually executes your workflow.

GitHub-hosted runners are convenient because they provide fresh managed environments for typical jobs. Self-hosted runners offer more control, but also place more responsibility for isolation, patching and cleanup on you.

Be especially careful about letting untrusted pull request code run on a persistent self-hosted runner. That code might be able to inspect local files, interfere with later jobs or access resources available on the host.

Build artifacts deserve attention too. A successful build tells you that commands completed, but it doesn’t necessarily prove where a downloaded binary came from or whether the pipeline was compromised.

GitHub supports artifact attestations, which can record verifiable information about the source and workflow used to produce an artifact.

Attestations are particularly useful when publishing software that other people will download or deploy. They don’t prove that the software contains no vulnerabilities; they help establish provenance.

You can read more in GitHub’s artifact attestation guide.

10. Check Whether Workflow Dependency Lockfiles Are Available

Pinning every action to a commit is a useful safeguard, but it’s still possible for a workflow to depend on other actions indirectly.

GitHub has been expanding its support for workflow dependency lockfiles to make those dependencies easier to track and verify.

A dependency lockfile can record verified commits for the actions a workflow depends on, helping make runs more reproducible and changes easier to review.

As of this update, GitHub documents this feature for GitHub Enterprise Cloud, so don’t assume it is available in every repository.

If your organization has access, review the workflow dependency lockfile guide and the related organization policies.

For a smaller repository without that feature, pinning reviewed actions, keeping dependencies updated and protecting workflow changes are still worthwhile steps.

What to Do If a GitHub Action You Use Is Compromised

Suppose a security advisory identifies a compromised third-party action that your repository has been using.

The first thing I’d do is check whether the affected version or commit actually ran in the repository. Review the workflow definitions, recent run history and any builds that used the dependency.

If the affected code ran in a job with access to credentials, treat those credentials as potentially exposed until you’ve investigated further. Revoke or rotate the relevant secrets and tokens using the systems that issued them.

Also inspect release artifacts, deployment activity and suspicious changes made around the affected runs.

Simply updating the action reference doesn’t undo damage that may already have occurred. The response needs to account for what the compromised workflow could access.

GitHub’s Advisory Database and official security announcements are useful places to check for confirmed affected versions and remediation instructions.

A Practical GitHub Actions Security Checklist

If you’re maintaining an existing project, you don’t need to rebuild the entire CI/CD pipeline at once. Start with the parts that can cause the most damage.

  1. Review every third-party action and replace mutable references with verified commit SHAs where appropriate.
  2. Give each workflow or job the minimum GITHUB_TOKEN permissions it requires.
  3. Check for dangerous uses of pull_request_target and other privileged workflow triggers.
  4. Make sure untrusted input is never inserted directly into executable shell scripts.
  5. Limit secrets and deployment permissions to trusted jobs and environments.
  6. Use OIDC instead of long-lived cloud keys where your provider supports it.
  7. Review package changes and keep action dependencies updated.
  8. Protect workflow files with code review and appropriate repository rules.
  9. Keep untrusted code away from sensitive or persistent runners.
  10. Have a plan for compromised dependencies and exposed credentials.

You can also enable GitHub code scanning with CodeQL where your repository is eligible. GitHub recommends it for finding risky GitHub Actions workflow patterns, including some injection and permissions problems.

Final Thoughts

GitHub Actions is useful precisely because it can execute code and automate tasks that would otherwise require manual access to your repositories and infrastructure.

That power is also why workflow security matters.

For most projects, the biggest improvements come from a few decisions: don’t blindly trust action tags, don’t grant write access to jobs that only need to read code, and don’t allow untrusted contributions to run inside privileged environments.

Then build on that foundation with dependency reviews, protected deployments, short-lived credentials and better visibility into what your builds actually execute.

A secure workflow isn’t one that never changes. It’s one where changes are reviewed, permissions are deliberate and a compromised dependency has as little room to cause damage as possible.

Leave a Reply