DevSecOps

GitHub Actions Security Best Practices in 2022

A practical guide to hardening your GitHub Actions workflows against supply chain attacks, secret leaks, and privilege escalation.

Yukti Singhal
Security Engineer
6 min read

GitHub Actions has become the default CI/CD platform for millions of repositories. That popularity makes it a prime target. In 2021 and 2022 alone, researchers disclosed multiple attack vectors — from compromised third-party actions to exfiltrated secrets via pull request triggers. If your workflows are not hardened, your entire software supply chain is exposed.

This guide covers the security practices that matter most, based on real-world incidents and the threat model GitHub Actions actually presents.

The Threat Model for GitHub Actions

Before diving into fixes, understand what you are defending against:

  • Compromised third-party actions: An attacker takes over a popular action's repository and pushes malicious code. Your workflow pulls it automatically.
  • Pull request poisoning: A fork submits a PR that modifies workflow files or injects commands into steps that run on pull_request_target.
  • Secret exfiltration: Malicious code in a step reads environment variables or the GITHUB_TOKEN and sends them to an external endpoint.
  • Privilege escalation: A workflow runs with write permissions it does not need, and an attacker leverages that to push code, create releases, or modify branch protections.

Every practice below maps back to one or more of these threats.

Pin Actions to Full Commit SHAs

The single most impactful change you can make: stop referencing actions by tag.

# Bad — tag can be moved to point at malicious code
- uses: actions/checkout@v3

# Good — immutable reference
- uses: actions/checkout@93ea575cb5d8a053eeb0ac9frandomsha384757347

Tags are mutable. A compromised maintainer (or a compromised maintainer account) can force-push a tag to a new commit. A full SHA is immutable — if the commit changes, your workflow breaks loudly instead of silently running attacker code.

Use tools like pin-github-action or Dependabot's GitHub Actions update feature to keep SHA references current without losing the security benefit.

Restrict GITHUB_TOKEN Permissions

By default, the GITHUB_TOKEN in many repositories still carries broad write access. GitHub introduced granular permissions in late 2021, but adoption has been slow.

permissions:
  contents: read
  pull-requests: read

Set the top-level permissions key in every workflow file. Grant only what each job needs. If a job only runs tests, it needs contents: read and nothing else. If a job posts a comment on a PR, add pull-requests: write for that specific job.

The principle is simple: if a step gets compromised, the blast radius is limited to whatever the token can do.

Avoid pull_request_target Unless You Fully Understand It

The pull_request_target trigger runs in the context of the base branch with access to secrets. It was designed for labeling and triage bots. It was not designed for building and testing PR code.

If you use pull_request_target and then check out the PR's head ref:

# DANGEROUS
on: pull_request_target
jobs:
  build:
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm install && npm test

You are running untrusted code with access to your repository's secrets. An attacker forks your repo, modifies package.json to exfiltrate GITHUB_TOKEN, and submits a PR.

If you must use pull_request_target, never check out the PR code in the same job that has access to secrets. Use a two-job pattern where the first job checks out and builds in a sandboxed environment, and the second job (which has secrets) only consumes artifacts.

Use Environment Protections for Deployments

GitHub Environments let you gate deployments behind required reviewers, wait timers, and branch restrictions. Use them.

jobs:
  deploy-production:
    environment:
      name: production
      url: https://app.example.com
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: ./deploy.sh

With the production environment configured to require approval from a security team member, no workflow — compromised or otherwise — can deploy without human sign-off.

Audit Third-Party Actions Before Use

Do not install actions the way you would npm packages. Before adding a third-party action:

  1. Read the source code. Most actions are a few hundred lines.
  2. Check the maintainer's reputation and commit history.
  3. Look for unnecessary permissions or network calls.
  4. Prefer actions from verified creators or the actions/ organization.
  5. If the action does something simple, consider writing an inline script instead.

A curl command in a run step is more auditable than a third-party action that abstracts the same thing behind layers of JavaScript.

Manage Secrets Properly

  • Never hardcode secrets in workflow files. Use GitHub's encrypted secrets feature.
  • Scope secrets to environments when possible, so only production deployment jobs can access production credentials.
  • Rotate secrets on a schedule, especially long-lived tokens.
  • Use OIDC federation instead of static credentials for cloud providers. GitHub Actions supports OIDC with AWS, GCP, and Azure — no more storing cloud access keys as secrets.
permissions:
  id-token: write
  contents: read

steps:
  - name: Configure AWS Credentials
    uses: aws-actions/configure-aws-credentials@v1
    with:
      role-to-assume: arn:aws:iam::123456789:role/github-actions
      aws-region: us-east-1

OIDC tokens are short-lived and scoped. There is nothing to steal.

Use CodeQL and Dependency Scanning in CI

GitHub provides CodeQL for free on public repositories. Run it on every PR:

- name: Initialize CodeQL
  uses: github/codeql-action/init@v2
  with:
    languages: javascript

- name: Perform CodeQL Analysis
  uses: github/codeql-action/analyze@v2

Combine this with dependabot alerts and npm audit (or your language equivalent) to catch known vulnerabilities in dependencies before they reach production.

Limit Self-Hosted Runner Exposure

If you use self-hosted runners, understand the risk: any workflow that runs on that runner has access to the host machine. A compromised workflow can install persistence mechanisms, read files from other jobs, or pivot to your internal network.

  • Never use self-hosted runners on public repositories.
  • Isolate runners using ephemeral containers or VMs that are destroyed after each job.
  • Use runner groups to restrict which repositories can target which runners.
  • Monitor runner machines for unexpected processes and network connections.

Enable Workflow Audit Logging

GitHub's audit log tracks workflow runs, secret access, and permission changes. Export these logs to your SIEM. Look for:

  • Workflow runs triggered by users outside your organization.
  • Changes to workflow files in protected branches.
  • Unusual patterns in secret access (e.g., a workflow that suddenly starts accessing secrets it never used before).

How Safeguard.sh Helps

Safeguard.sh integrates directly with your GitHub repositories to provide continuous security visibility across your CI/CD pipelines. It automatically inventories your workflow files, flags insecure patterns like unpinned actions and overly broad token permissions, and monitors for drift over time. When a new vulnerability is disclosed in a third-party action you depend on, Safeguard.sh alerts you before an attacker can exploit it. Combined with SBOM generation and policy-as-code enforcement, it gives your security team a single pane of glass over the entire software delivery lifecycle — without slowing down your developers.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.