DEV Community

Olivier Buitelaar
Olivier Buitelaar

Posted on

How to Secure Your GitHub Actions in 5 Minutes: A Step-by-Step Guide

How to Secure Your GitHub Actions in 5 Minutes: A Step-by-Step Guide

You've got 100 workflows running across your org. Someone's bound to use pull_request_target without restrictions. Someone else hardcoded secrets. And nobody's checking permissions.

This article shows you exactly what to fix — right now, in under 5 minutes.

The 5-Minute Security Checklist

1. Lock Down Pull Request Workflows (2 minutes)

The biggest GitHub Actions vulnerability is using pull_request_target with untrusted code.

Bad:

on: pull_request_target

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm test
Enter fullscreen mode Exit fullscreen mode

This checks out fork code and runs it with your secrets. Disaster.

Good:

on:
  pull_request:
    types: [opened, synchronize]

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

Regular pull_request checks out your repo code, not the fork. Safe.

If you MUST use pull_request_target:

on: pull_request_target

permissions:
  contents: read
  pull-requests: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: refs/pull/${{ github.event.number }}/merge
      - run: npm test
Enter fullscreen mode Exit fullscreen mode

Always set explicit minimal permissions. Never checkout the head SHA.

2. Review Your Secret Handling (2 minutes)

Secrets leak through logs, error messages, and third-party action outputs.

Bad:

- name: Deploy
  run: npm run deploy
  env:
    DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
Enter fullscreen mode Exit fullscreen mode

This logs the password in step output if deployment fails.

Good:

- name: Deploy
  run: npm run deploy
  env:
    DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
  if: github.ref == 'refs/heads/main'
Enter fullscreen mode Exit fullscreen mode

But better: Use GitHub's native secret masking or encrypted deploy environments.

Best:

deploy:
  environment:
    name: production
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Deploy
      run: npm run deploy
      env:
        DATABASE_PASSWORD: ${{ secrets.DB_PASSWORD }}
Enter fullscreen mode Exit fullscreen mode

Environments enforce branch protection and approvals.

3. Pin Action Versions (1 minute)

Never use @latest or @main. Pinning to exact commits prevents supply chain attacks.

Bad:

- uses: actions/checkout@latest
- uses: some-org/some-action@main
Enter fullscreen mode Exit fullscreen mode

Good:

- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: some-org/some-action@2c9de6ab0de0eb09362a4c5e32f39541eca7d5fa # v2.0.1
Enter fullscreen mode Exit fullscreen mode

Use full commit SHA, add version tag in comment for readability.

Tools to help:

  • workflow-guardian — scans your workflows for unpinned actions, hardcoded secrets, unsafe permissions
  • actionlint (available as GitHub Action) — lints YAML syntax

The 1-Minute Audit

Run this locally to find issues:

# Install workflow-guardian
npm install -g @ollieb89/workflow-guardian

# Scan your repo
workflow-guardian scan .
Enter fullscreen mode Exit fullscreen mode

This checks for:

  • Unpinned actions
  • Hardcoded secrets
  • Overpermissive permissions
  • pull_request_target without restrictions

What's Next?

For continuous enforcement, add workflow-guardian to your CI:

name: Lint Workflows

on:
  pull_request:
    paths:
      - '.github/workflows/**'

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
      - uses: ollieb89/workflow-guardian@d4e8c9f2a1b3c5e7f9a2b4d6e8f0a1b3c5d7e9f1
Enter fullscreen mode Exit fullscreen mode

Your team will never merge an unsafe workflow again.

Real Impact

One org I know had 47 workflows with unpinned actions. After pinning and setting permissions:

  • Zero accidental secret leaks (previously 3 per month)
  • Supply chain attack surface: 47x → 0
  • Enforcement: Manual reviews → Automatic checks

This 5-minute fix has prevented more actual incidents than I can count.


Need more? Check out the full GitHub Actions Security Toolkit — workflow-guardian, secret scanner, and more.

Top comments (0)