DEV Community

Olivier Buitelaar
Olivier Buitelaar

Posted on

GitHub Actions Security Checklist: 12 Things to Audit Before You Ship

GitHub Actions Security Checklist: 12 Things to Audit Before You Ship

GitHub Actions is powerful — and that power cuts both ways. A misconfigured workflow can leak secrets, allow unauthorized code execution, or let attackers pivot into your production environment.

Here's the checklist I run through before shipping any workflow.


1. Pin third-party actions to a full commit SHA

# ❌ Dangerous — tag can be moved
- uses: actions/checkout@v4

# ✅ Safe — immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
Enter fullscreen mode Exit fullscreen mode

Tags are mutable. An attacker who compromises an upstream action repo can push a new commit to the v4 tag. Pinning to a SHA means you get exactly what you audited.

Tool that catches this: workflow-guardian flags unpinned actions automatically.


2. Never use pull_request_target without extreme caution

pull_request_target runs with write permissions and access to secrets — even for PRs from forks. Combined with actions/checkout on the PR head, you have a critical vulnerability:

# ❌ Pwn request pattern — DO NOT DO THIS
on: pull_request_target
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm install && npm test  # attacker controls this code
Enter fullscreen mode Exit fullscreen mode

If you need pull_request_target, never check out or execute code from the PR branch.


3. Limit GITHUB_TOKEN permissions

Default token permissions vary by org settings. Be explicit:

permissions:
  contents: read        # only what you need
  pull-requests: write  # if you post PR comments
Enter fullscreen mode Exit fullscreen mode

Or lock everything down at the workflow level and grant up per-job:

permissions: read-all  # workflow default
jobs:
  deploy:
    permissions:
      contents: write  # only this job gets write
Enter fullscreen mode Exit fullscreen mode

4. Don't echo untrusted input into $GITHUB_ENV or $GITHUB_OUTPUT

This is an environment variable injection vector:

# ❌ Dangerous if PR title contains shell metacharacters
- run: echo "BRANCH=${{ github.event.pull_request.title }}" >> $GITHUB_ENV

# ✅ Use an intermediate env var
- run: echo "BRANCH=$TITLE" >> $GITHUB_ENV
  env:
    TITLE: ${{ github.event.pull_request.title }}
Enter fullscreen mode Exit fullscreen mode

5. Validate workflow syntax before merge

Broken workflows fail silently or at the worst moment. Lint them:

- name: Lint workflows
  uses: ollieb89/workflow-guardian@v1
Enter fullscreen mode Exit fullscreen mode

This catches syntax errors, deprecated features, and security anti-patterns before they hit main.


6. Don't store secrets in workflow files

Obvious, but it happens:

# ❌ Never do this
env:
  API_KEY: sk-prod-abc123xyz

# ✅ Use GitHub Secrets
env:
  API_KEY: ${{ secrets.API_KEY }}
Enter fullscreen mode Exit fullscreen mode

Audit your repo history too — git log -S 'sk-' --all can surface old leaks.


7. Restrict who can trigger workflow_dispatch

Anyone with write access can trigger manual workflows by default. If your dispatch workflow deploys to production, add an environment with required reviewers.


8. Use environments for production deployments

jobs:
  deploy:
    environment: production  # requires approval from configured reviewers
Enter fullscreen mode Exit fullscreen mode

Environments give you deployment protection rules, required reviewers, and scoped secrets.


9. Check for script injection in run steps

Any expression interpolated directly into a run block is a script injection risk:

# ❌ Vulnerable to script injection
- run: echo "PR author is ${{ github.event.pull_request.user.login }}"

# ✅ Use env vars
- run: echo "PR author is $AUTHOR"
  env:
    AUTHOR: ${{ github.event.pull_request.user.login }}
Enter fullscreen mode Exit fullscreen mode

10. Don't use self-hosted runners for public repos

Anyone can fork a public repo and submit a PR that runs on your self-hosted runner. Unless you have strict protections in place, use GitHub-hosted runners for public repos.


11. Rotate secrets regularly and audit access

  • Check Settings > Secrets for stale/unused entries
  • Rotate any secret that may have been exposed in logs
  • Grep your workflow run logs for accidental secret prints: *** masking doesn't catch every format

12. Enable Dependabot for action version updates

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
Enter fullscreen mode Exit fullscreen mode

This keeps your pinned actions updated with security patches automatically.


Automate as Much as Possible

This checklist is a lot to remember. Most of it can be caught automatically:

  • workflow-guardian — lints your workflow files on every PR, catching security issues and syntax errors before they merge
  • actionlint — static analysis for workflow syntax
  • Dependabot — keeps action versions current

The goal isn't to memorize all of this — it's to build the guardrails so you don't have to.


Found this useful? I build open-source tools for GitHub Actions security. Check out workflow-guardian — it automates most of this checklist.

Top comments (0)