myctrl.tools

GitHub Actions Hardening Guide

Focused guidance for hardening GitHub Actions. Start with baseline policy, then review third-party actions, untrusted pull request handling, secrets flow, runner exposure, and cloud authentication. The emphasis is safe defaults, working examples, and verification steps that teams can use in live repositories.

Focus

Workflow hardening, runner policy, secret handling, and deployment authentication for GitHub Actions environments.

Source basis

This guide draws primarily from GitHub documentation and the Wiz GitHub Actions guide, with supporting OWASP context where it clarifies the risk model.

Scope

Framework mappings and supplementary references are collected in the appendix so the main page can stay centered on implementation and review.

Fix These First

Start here when you are reviewing an existing setup or establishing an initial baseline for a new organization or repository.

critical vendor vendor

Baseline Guardrails

Treat Actions as a privileged execution platform, not just CI plumbing. Set secure org and repo defaults first so every new workflow starts from a safer baseline.

Recommended actions
  • Set the default `GITHUB_TOKEN` permission to read-only at the organization and repository layers.
  • Restrict Actions usage to GitHub-owned actions, verified creators, and a small explicit allowlist.
  • Protect `.github/workflows/`, `.github/actions/`, and `.github/dependabot.yml` with CODEOWNERS plus required reviews.
  • Require branch protection on the default branch, including stale review dismissal and CODEOWNER approval.
Verification
CLI repo

Check the repository default workflow token policy.

Requires repo admin or a token with repository administration rights.

bash
gh api repos/{owner}/{repo}/actions/permissions/workflow \
  --jq '{default_workflow_permissions, can_approve_pull_request_reviews}'
CLI repo

Inspect the repository allowlist for third-party actions and reusable workflows.

Only meaningful when the repository policy is set to selected actions rather than all actions.

bash
gh api repos/{owner}/{repo}/actions/permissions/selected-actions
CLI repo

Review the default branch protection gate for workflow changes and releases.

Use the real default branch name in place of `{branch}`. Pair this with CODEOWNERS on `.github/workflows/`.

bash
gh api repos/{owner}/{repo}/branches/{branch}/protection \
  --jq '{required_status_checks, required_pull_request_reviews, enforce_admins, allow_force_pushes, allow_deletions}'
CLI org

Read the organization-wide default workflow token policy.

Requires organization admin access or an appropriately scoped token.

bash
gh api orgs/{org}/actions/permissions/workflow \
  --jq '{default_workflow_permissions, can_approve_pull_request_reviews}'
Examples

Examples are shown inline so you can review working patterns, compare contrasting cases, and adapt them directly.

GOOD: Locked-Down CI Baseline

GitHub Docs - security hardening and least privilege

yaml
name: ci
on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  pull-requests: read

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

Use explicit top-level permissions instead of inheriting broad defaults. Start with read-only and grant additional scopes only to the single workflow that needs them.

GOOD: CODEOWNERS for Workflow Changes

GitHub Docs - CODEOWNERS for monitoring workflow changes

text
/.github/workflows/ @org/security-team @org/platform-team
/.github/actions/ @org/platform-team
/.github/dependabot.yml @org/security-team

Pair CODEOWNERS with branch protection that requires CODEOWNER approval. Without the branch protection gate, CODEOWNERS alone is advisory.

Selected tools

These tools support workflow review, policy validation, and runtime monitoring where they materially improve GitHub Actions security review.

OpenSSF Allstar

GitHub App for continuously enforcing org- and repo-level policy baselines such as branch protection and restricted Actions usage.

Open

Best for organizations that want policy drift detection rather than one-off review commands.

high vendor community vendor

Third-Party Actions and Reusable Workflows

Every external action or reusable workflow is part of your build supply chain. Pin what you run, minimize what you allow, and avoid passing broad secret sets downstream.

Recommended actions
  • Pin third-party actions to a full commit SHA instead of a mutable tag or branch.
  • Keep your allowlist small and tied to publishers you are actually willing to trust.
  • For reusable workflows, pass only the specific secrets the callee needs.
  • Treat reusable workflows as code dependencies that need review, versioning, and ownership.
Verification
CLI repo

Inspect the repository allowlist for third-party actions and reusable workflows.

Only meaningful when the repository policy is set to selected actions rather than all actions.

bash
gh api repos/{owner}/{repo}/actions/permissions/selected-actions
CLI local

Run a GitHub Actions-focused security audit across the repository.

Per zizmor quickstart, pointing it at the repository root collects workflows from `.github/workflows/` automatically.

bash
zizmor .
Examples

Examples are shown inline so you can review working patterns, compare contrasting cases, and adapt them directly.

Action Version Pinning

GitHub Docs - pinning actions to a full-length commit SHA

BAD
yaml
# BAD: Mutable action reference
- uses: actions/checkout@v4
GOOD
yaml
# GOOD: Immutable commit SHA
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608

GitHub's own hardening guide calls a full-length commit SHA the immutable option. Keep the readable tag in a code comment or changelog, not as the executable reference.

Reusable Workflow Secret Scope

Wiz - reusable workflow secret handling

BAD
yaml
# BAD: Passes every caller secret to the reusable workflow
jobs:
  deploy:
    uses: org/shared-workflows/.github/workflows/deploy.yml@v1
    secrets: inherit
GOOD
yaml
# GOOD: Pass only what the reusable workflow actually needs
jobs:
  deploy:
    uses: org/shared-workflows/.github/workflows/deploy.yml@v1
    secrets:
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
      REGISTRY_URL: ${{ secrets.REGISTRY_URL }}

Wiz explicitly calls out `secrets: inherit` as an overexposure pattern. Keep the callee interface narrow and review it like any other dependency surface.

Selected tools

These tools support workflow review, policy validation, and runtime monitoring where they materially improve GitHub Actions security review.

zizmor

Security-focused static analysis for GitHub Actions with dedicated audits for dangerous triggers, secret misuse, and permission problems.

Open
bash
zizmor .

The official quickstart documents running it directly against a repository root or specific workflow files.

OpenSSF Allstar

GitHub App for continuously enforcing org- and repo-level policy baselines such as branch protection and restricted Actions usage.

Open

Best for organizations that want policy drift detection rather than one-off review commands.

critical vendor community

Untrusted PRs and Poisoned Pipeline Execution

Most publicized GitHub Actions compromises come from running attacker-controlled code in a privileged workflow context. Separate untrusted CI from privileged automation and avoid dangerous trigger combinations.

Recommended actions
  • Default to `pull_request` for external contributions and keep that workflow read-only.
  • Avoid `pull_request_target` unless the job genuinely needs privileged context.
  • Never check out or execute fork-controlled code from a privileged trigger.
  • Treat `workflow_run` artifacts and outputs as untrusted inputs unless you can prove otherwise.
Verification
Query local

Search workflow files for common high-risk patterns in one pass.

Fast first-pass grep for the exact trigger and secret-handling patterns discussed on this page.

bash
rg -n "pull_request_target|workflow_run|secrets:\s*inherit|toJson\(secrets\)|GITHUB_ENV|GITHUB_PATH" .github/workflows/
CLI local

Run a GitHub Actions-focused security audit across the repository.

Per zizmor quickstart, pointing it at the repository root collects workflows from `.github/workflows/` automatically.

bash
zizmor .
CLI local

Run syntax, expression, and workflow wiring checks locally.

Installs cleanly via Homebrew on macOS (`brew install actionlint`). Install `shellcheck` and `pyflakes` too if you want its script integrations.

bash
actionlint
Examples

Examples are shown inline so you can review working patterns, compare contrasting cases, and adapt them directly.

Pull Request Trigger Selection

Wiz and GitHub Docs - PPE and privileged trigger misuse

BAD
yaml
# BAD: Privileged trigger plus checkout of attacker-controlled code
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 install && npm test
GOOD
yaml
# GOOD: Use the sandboxed pull_request trigger for untrusted PR code
on: pull_request
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
      - run: npm ci
      - run: npm test

GitHub explicitly warns that `pull_request_target` and `workflow_run` become dangerous when they process untrusted code or artifacts. Keep untrusted build/test work in a low-privilege workflow.

Selected tools

These tools support workflow review, policy validation, and runtime monitoring where they materially improve GitHub Actions security review.

zizmor

Security-focused static analysis for GitHub Actions with dedicated audits for dangerous triggers, secret misuse, and permission problems.

Open
bash
zizmor .

The official quickstart documents running it directly against a repository root or specific workflow files.

actionlint

Fast static checker for workflow syntax, expressions, runner labels, reusable workflow wiring, and script injection footguns.

Open
bash
actionlint

Use it to catch broken workflow syntax and wiring before you focus on deeper security review.

critical vendor community

Secrets and Environment Handling

Credential blast radius in Actions is usually self-inflicted. Keep secret scope narrow, avoid unsafe env propagation, and mask any value generated at runtime before it reaches a log.

Recommended actions
  • Do not dump the entire `secrets` context into a job or step.
  • Avoid `secrets: inherit` unless the callee truly needs every caller secret.
  • Do not write untrusted input to `GITHUB_ENV` or `GITHUB_PATH`.
  • Use step-scoped `env` values and call `::add-mask::` for derived secrets before logging anything that touches them.
Verification
Query local

Search workflow files for common high-risk patterns in one pass.

Fast first-pass grep for the exact trigger and secret-handling patterns discussed on this page.

bash
rg -n "pull_request_target|workflow_run|secrets:\s*inherit|toJson\(secrets\)|GITHUB_ENV|GITHUB_PATH" .github/workflows/
CLI local

Run a GitHub Actions-focused security audit across the repository.

Per zizmor quickstart, pointing it at the repository root collects workflows from `.github/workflows/` automatically.

bash
zizmor .
Examples

Examples are shown inline so you can review working patterns, compare contrasting cases, and adapt them directly.

Workflow Secret Passing

Wiz - secret passing anti-patterns

BAD
yaml
# BAD: Dumps every repository secret into one environment variable
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - env:
          ALL_SECRETS: ${{ toJson(secrets) }}
        run: ./deploy.sh
GOOD
yaml
# GOOD: Pass only the secrets this step needs
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
          REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: ./deploy.sh

This is one of the clearest examples in the Wiz guide and one of the easiest fixes to make today.

Workflow Environment Variable Handling

Wiz - GITHUB_ENV and GITHUB_PATH handling

BAD
yaml
# BAD: Writes untrusted input into a shared workflow environment file
- name: Capture branch
  run: echo "BRANCH=${{ github.head_ref }}" >> "$GITHUB_ENV"
GOOD
yaml
# GOOD: Keep untrusted values step-scoped and validate before use
- name: Process branch
  env:
    BRANCH: ${{ github.head_ref }}
  run: |
    if [[ "$BRANCH" =~ ^[a-zA-Z0-9/_-]+$ ]]; then
      echo "Safe branch name: $BRANCH"
    fi

Step-scoped `env` avoids poisoning later steps. Validation matters because "step-scoped" is still not the same thing as "trusted."

GOOD: Mask Runtime-Derived Secrets

GitHub Docs - secret redaction behavior

yaml
- name: Mint short-lived token
  run: |
    TOKEN="$(./scripts/mint-token.sh)"
    echo "::add-mask::$TOKEN"
    echo "token_ready=true" >> "$GITHUB_OUTPUT"

GitHub redacts registered secrets and masked values, but only after the runner knows about them. Mask runtime-derived credentials before any debug or error output can leak them.

Selected tools

These tools support workflow review, policy validation, and runtime monitoring where they materially improve GitHub Actions security review.

zizmor

Security-focused static analysis for GitHub Actions with dedicated audits for dangerous triggers, secret misuse, and permission problems.

Open
bash
zizmor .

The official quickstart documents running it directly against a repository root or specific workflow files.

high vendor vendor community

Runner Strategy and Isolation

GitHub-hosted runners are the safer default because they are ephemeral and isolated. Self-hosted runners need strong repo scoping, trust separation, and lifecycle controls or they become a durable foothold.

Recommended actions
  • Use GitHub-hosted runners by default, especially for public repositories.
  • Do not expose self-hosted runners to public repository pull requests.
  • Limit self-hosted runner use to selected repositories and segregate them by trust level.
  • Prefer JIT or otherwise ephemeral runner infrastructure, and assume the underlying host is sensitive infrastructure.
Verification
Query local

Find workflow jobs that target self-hosted runners.

Review every hit and make sure it is isolated by trust level, repository scope, and runner lifecycle.

bash
rg -n "self-hosted" .github/workflows/
CLI org

Read the organization self-hosted runner policy.

Confirms whether self-hosted runners are disabled, allowed for all repositories, or limited to selected repositories.

bash
gh api orgs/{org}/actions/permissions/self-hosted-runners
CLI org

List repositories that are allowed to use organization self-hosted runners.

Useful when your runner policy is set to selected repositories and you want to verify the actual allowlist.

bash
gh api orgs/{org}/actions/permissions/self-hosted-runners/repositories \
  --jq '.repositories[].full_name'
Examples

Examples are shown inline so you can review working patterns, compare contrasting cases, and adapt them directly.

Runner Exposure Model

GitHub Docs and Wiz - self-hosted runner risk

BAD
yaml
# BAD: Public pull requests can execute on infrastructure you manage
on: pull_request
jobs:
  build:
    runs-on: [self-hosted]
GOOD
yaml
# GOOD: Keep untrusted PR workloads on GitHub-hosted runners
on: pull_request
jobs:
  build:
    runs-on: ubuntu-latest

GitHub states that self-hosted runners should almost never be used for public repositories. Even private repositories need careful trust-boundary analysis.

GOOD: Restrict Self-Hosted Runner Use to Selected Repositories

GitHub REST API - self-hosted runner policy

json
{
  "enabled_repositories": "selected"
}

This is the policy body for the organization self-hosted runner setting. Follow it by managing the repository allowlist through the matching REST endpoints.

Selected tools

These tools support workflow review, policy validation, and runtime monitoring where they materially improve GitHub Actions security review.

StepSecurity Harden-Runner

Runner-side monitoring and egress control for GitHub Actions jobs, useful when you need more visibility into workflow runtime behavior.

Open

Use after you understand your legitimate outbound dependencies. It is most helpful for sensitive workflows and self-hosted or privileged jobs.

OpenSSF Allstar

GitHub App for continuously enforcing org- and repo-level policy baselines such as branch protection and restricted Actions usage.

Open

Best for organizations that want policy drift detection rather than one-off review commands.

medium vendor vendor

Cloud Auth and Provenance

Long-lived cloud secrets in GitHub Actions are a liability. Prefer OpenID Connect for deployment auth, and add artifact attestation or provenance where your release flow needs stronger supply-chain integrity signals.

Recommended actions
  • Use OpenID Connect instead of static cloud credentials wherever your provider supports it.
  • Grant `id-token: write` only to workflows that actually mint OIDC tokens.
  • Keep deployment workflows separate from untrusted PR execution paths.
  • Add build provenance or artifact attestations to release workflows that publish software externally.
Verification
Query local

Search workflow files for common high-risk patterns in one pass.

Fast first-pass grep for the exact trigger and secret-handling patterns discussed on this page.

bash
rg -n "pull_request_target|workflow_run|secrets:\s*inherit|toJson\(secrets\)|GITHUB_ENV|GITHUB_PATH" .github/workflows/
Examples

Examples are shown inline so you can review working patterns, compare contrasting cases, and adapt them directly.

Cloud Authentication Strategy

GitHub Docs - OpenID Connect

BAD
yaml
# BAD: Long-lived cloud credentials in repository secrets
env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
GOOD
yaml
# GOOD: Short-lived cloud auth with GitHub OIDC
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: us-east-1

The permission and workflow structure are correct; pin the action to a commit SHA when you operationalize it in a production repository.

GOOD: Build Provenance for Releases

GitHub Docs and NIST-aligned provenance guidance

yaml
name: release
on:
  push:
    tags:
      - 'v*'

permissions:
  contents: read
  id-token: write
  attestations: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
      - run: npm ci
      - run: npm run build
      - uses: actions/attest-build-provenance@v1
        with:
          subject-path: dist/**

Artifact attestations are most valuable on trusted release paths, not on untrusted PR workflows. Pin the provenance action before production rollout if you adopt this pattern.

Selected tools

These tools support workflow review, policy validation, and runtime monitoring where they materially improve GitHub Actions security review.

StepSecurity Harden-Runner

Runner-side monitoring and egress control for GitHub Actions jobs, useful when you need more visibility into workflow runtime behavior.

Open

Use after you understand your legitimate outbound dependencies. It is most helpful for sensitive workflows and self-hosted or privileged jobs.

Verification Toolkit

These checks are grouped by review task and labeled by scope so you can move from local inspection to repository and organization policy validation without guessing at prerequisites.

Repo baseline

3 commands
CLI repo

Check the repository default workflow token policy.

Requires repo admin or a token with repository administration rights.

bash
gh api repos/{owner}/{repo}/actions/permissions/workflow \
  --jq '{default_workflow_permissions, can_approve_pull_request_reviews}'
CLI repo

Inspect the repository allowlist for third-party actions and reusable workflows.

Only meaningful when the repository policy is set to selected actions rather than all actions.

bash
gh api repos/{owner}/{repo}/actions/permissions/selected-actions
CLI repo

Review the default branch protection gate for workflow changes and releases.

Use the real default branch name in place of `{branch}`. Pair this with CODEOWNERS on `.github/workflows/`.

bash
gh api repos/{owner}/{repo}/branches/{branch}/protection \
  --jq '{required_status_checks, required_pull_request_reviews, enforce_admins, allow_force_pushes, allow_deletions}'

Org policy

2 commands
CLI org

Read the organization-wide default workflow token policy.

Requires organization admin access or an appropriately scoped token.

bash
gh api orgs/{org}/actions/permissions/workflow \
  --jq '{default_workflow_permissions, can_approve_pull_request_reviews}'
CLI org

Read the organization-wide Actions allowlist policy.

Use this to confirm whether GitHub-owned actions, verified creators, and explicit patterns are allowed.

bash
gh api orgs/{org}/actions/permissions/selected-actions

Runner posture

2 commands
CLI org

Read the organization self-hosted runner policy.

Confirms whether self-hosted runners are disabled, allowed for all repositories, or limited to selected repositories.

bash
gh api orgs/{org}/actions/permissions/self-hosted-runners
CLI org

List repositories that are allowed to use organization self-hosted runners.

Useful when your runner policy is set to selected repositories and you want to verify the actual allowlist.

bash
gh api orgs/{org}/actions/permissions/self-hosted-runners/repositories \
  --jq '.repositories[].full_name'

Workflow inspection

2 commands
Query local

Search workflow files for common high-risk patterns in one pass.

Fast first-pass grep for the exact trigger and secret-handling patterns discussed on this page.

bash
rg -n "pull_request_target|workflow_run|secrets:\s*inherit|toJson\(secrets\)|GITHUB_ENV|GITHUB_PATH" .github/workflows/
Query local

Find workflow jobs that target self-hosted runners.

Review every hit and make sure it is isolated by trust level, repository scope, and runner lifecycle.

bash
rg -n "self-hosted" .github/workflows/

Static analysis

2 commands
CLI local

Run a GitHub Actions-focused security audit across the repository.

Per zizmor quickstart, pointing it at the repository root collects workflows from `.github/workflows/` automatically.

bash
zizmor .
CLI local

Run syntax, expression, and workflow wiring checks locally.

Installs cleanly via Homebrew on macOS (`brew install actionlint`). Install `shellcheck` and `pyflakes` too if you want its script integrations.

bash
actionlint

References and Framework Mappings

Use this appendix when you want the upstream source material or when you need framework-specific mapping context for evidence collection and assessment work.

References
vendor

GitHub Docs: Security hardening for GitHub Actions

Primary reference for hardening guidance on untrusted pull requests, token permissions, CODEOWNERS for workflow changes, OpenID Connect, and runner risk. Use this for the supported GitHub configuration model and current platform guidance.

vendor

GitHub REST API: Actions permissions

Official API reference for repository and organization Actions policy, including workflow token defaults, allowlisted actions, and self-hosted runner policy endpoints. Use this to verify the commands on this page.

owasp

OWASP Top 10 CI/CD Security Risks

Supporting reference for CI/CD risk categories and terminology. Use it as background context alongside the implementation guidance on this page.

Framework pages

Use the framework pages when you need control mappings, evidence framing, or program-specific assessment context that is outside the scope of this implementation guide.