GitHub Actions Security Best Practices

GitHub Actions workflows run code on real machines with access to your repository, secrets, and potentially production systems. A poorly configured workflow can expose credentials, execute malicious code, or allow unauthorized access to critical resources. This topic covers the security practices every team should apply to their workflows.

The Core Security Risks

Risk 1: Script injection
  User input from pull request titles, comments, or
  branch names gets inserted into a run: command.

Risk 2: Compromised third-party actions
  An action you depend on gets updated with malicious code.

Risk 3: Overly permissive tokens
  The workflow's GITHUB_TOKEN can read or write
  more than it needs to.

Risk 4: Secret exposure
  Secrets accidentally printed in logs or passed through
  insecure channels.

Risk 5: Untrusted code execution
  A pull request from a fork runs workflows with access
  to your repository's secrets.

Principle of Least Privilege for Tokens

Every workflow run receives a GITHUB_TOKEN automatically. By default, this token has broad permissions. Restrict it to only what the workflow actually needs.

Set default permissions at the workflow level:

permissions:
  contents: read      ← Read-only access to repository contents

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

Grant elevated permissions only on the specific job that needs them:

permissions:
  contents: read    ← Workflow default: read only

jobs:
  release:
    permissions:
      contents: write     ← This job needs to create releases
      packages: write     ← This job needs to push packages
    runs-on: ubuntu-latest
    steps:
      - run: gh release create v1.0

Common permission scopes

contents      → Read/write repository files and releases
packages      → Read/write GitHub Packages
pull-requests → Read/write pull request data
issues        → Read/write issues
id-token      → Needed for OIDC authentication
deployments   → Read/write deployments

Pinning Actions to Commit SHAs

Using a version tag like @v4 trusts whoever controls that tag. If the action's repository is compromised and the tag is updated, malicious code runs in your workflow.

Vulnerable — uses mutable tag:
  uses: actions/checkout@v4

Secure — pinned to an exact commit:
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

Get the SHA by visiting the action's repository on GitHub, clicking the version tag, and copying the full commit hash from the URL. Tools like Dependabot and pin-github-action CLI automate this process and keep pins updated.

Preventing Script Injection

Script injection happens when user-controlled data gets executed as a shell command. The most common source is using GitHub context values directly in run steps:

Dangerous — the PR title could contain shell commands:
  - run: echo "PR title: ${{ github.event.pull_request.title }}"

Safe — pass the value as an environment variable:
  - env:
      PR_TITLE: ${{ github.event.pull_request.title }}
    run: echo "PR title: $PR_TITLE"

When you pass context values through environment variables, the shell treats them as plain string data, not executable commands. This blocks injection attacks completely.

Handling Pull Requests from Forks Safely

Pull requests from external contributors run on forked repositories. GitHub restricts fork PRs from accessing secrets by default — but teams sometimes accidentally expose them.

Safe behavior (default):
  Fork PR triggers pull_request event
  → Workflow runs WITHOUT access to secrets
  → Safe for running tests

Risky behavior to avoid:
  Using pull_request_target trigger with untrusted code
  → Workflow DOES have access to secrets
  → Attacker can extract your secrets via malicious PR

Use pull_request_target only with extreme caution and never check out untrusted code in jobs that run under that trigger.

Restricting Workflow Permissions with CODEOWNERS

Use GitHub's CODEOWNERS file to require review approval before workflow files can be changed:

# .github/CODEOWNERS
.github/workflows/   @security-team @devops-team

Any pull request that modifies workflow files requires explicit approval from security team members. This prevents unauthorized actors from adding malicious steps to your pipelines.

Auditing Workflow Permissions

GitHub's organization settings let you control which repositories can use GitHub Actions and which actions are permitted:

  • Allow all actions — no restriction (not recommended for organizations)
  • Allow GitHub-created actions only — only official actions from GitHub
  • Allow select actions — specify an allowlist of permitted actions

For production teams, maintain an allowlist and review additions to it as part of your security process.

Environment Protection Rules

Apply protection rules to production environments to add a human approval gate before deployment:

  • Required reviewers — specific people must approve
  • Wait timer — enforces a delay between trigger and execution
  • Branch filters — only allow deployments from specific branches
jobs:
  deploy-production:
    environment: production     ← Triggers approval requirement
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

Security Checklist for Every Workflow

  • Set permissions: contents: read as the default at the top of every workflow
  • Grant elevated permissions only on specific jobs that need them
  • Pin all third-party actions to commit SHAs
  • Never use ${{ github.event... }} directly in run steps — use environment variables
  • Add CODEOWNERS for the .github/workflows/ directory
  • Require reviewers on production environments
  • Rotate secrets regularly
  • Use OIDC instead of long-lived access keys for cloud authentication
  • Review all third-party action source code before adding it to workflows

Leave a Comment

Your email address will not be published. Required fields are marked *