GitHub Actions Security: Permissions and SHA Pinning

2026-02-12

A GitHub Actions workflow can read source, receive tokens, publish artifacts, and modify a repository. Its security boundary includes workflow permissions, third-party actions, event triggers, runners, and environments. Two high-impact improvements are granting the GITHUB_TOKEN only required permissions and pinning external actions to immutable commit SHAs.

Use the GitHub Actions Explainer to inspect workflow structure, but review sensitive workflows and values only in approved environments.

Start with read-only permissions

Set an explicit workflow-level baseline:

name: CI

on:
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - run: npm ci
      - run: npm test

The SHA above is an example of the pinning form; verify the current release and commit in the action's official repository before use. A tag such as @v4 is convenient but movable. A full commit SHA identifies immutable Git content. Keep the human-readable version in a comment so dependency automation and reviewers can understand the pin.

Permissions not granted are set to none when an explicit permissions map is used. If jobs need different access, narrow it per job:

permissions:
  contents: read

jobs:
  build:
    permissions:
      contents: read
    # build steps

  publish:
    needs: build
    permissions:
      contents: read
      packages: write
    # publish steps

Do not grant write-all to fix an unexplained authorization failure. Identify the API operation and grant its documented permission only to the job that performs it.

Pin every external action

Pin actions from GitHub, the marketplace, and private repositories:

- uses: owner/example-action@0123456789abcdef0123456789abcdef01234567 # v2.3.1

The placeholder SHA demonstrates syntax and must not be used. Resolve the tag in the trusted upstream repository, review ownership and release notes, then copy the 40-character commit SHA. Automated dependency update tools can open reviewed pull requests when newer releases appear.

Docker images and downloaded scripts are separate dependencies. Pin images by digest when practical, verify checksums or signatures for downloaded binaries, and avoid:

- run: curl -sSL https://example.test/install.sh | sh

A pinned action can still download mutable content at runtime, so inspect what the action executes.

Protect tokens and environments

The built-in token is short-lived, but a compromised step can use its active permissions. Pass secrets only to steps that need them. Prefer OpenID Connect for cloud access with short-lived credentials:

permissions:
  contents: read
  id-token: write

id-token: write permits requesting an OIDC token; it does not itself grant cloud access. Configure the cloud trust policy narrowly for repository, branch or environment, workflow, and audience. Inventory configuration with the Environment Toolkit without exposing secret values.

Use protected environments for deployments, with required reviewers and scoped environment secrets. Do not print contexts wholesale because they may contain sensitive metadata. Masking is a backstop, not permission to echo secrets.

Treat pull requests as untrusted

Code from a fork can modify build scripts and dependencies. Avoid exposing secrets or write-capable credentials to untrusted pull-request code. Be especially cautious with pull_request_target: it runs in the base repository context and can become dangerous if it checks out and executes attacker-controlled code.

Separate untrusted build jobs from privileged publishing jobs. Transfer only validated artifacts, apply environment approval, and ensure the privileged job does not execute scripts bundled inside an untrusted artifact.

Self-hosted runners require additional isolation. Untrusted code may persist, inspect the network, or steal later credentials. Use ephemeral runners and strong network boundaries; do not place public-fork jobs on durable internal runners.

Review checklist and mistakes

For each workflow:

  1. List triggers and determine which inputs are attacker-controlled.
  2. Set workflow and job permissions explicitly.
  3. Pin external actions to reviewed full SHAs.
  4. Check transitive downloads, scripts, and container images.
  5. Scope secrets and OIDC trust to the narrowest job and environment.
  6. Prevent untrusted code from reaching privileged runners or deployment steps.
  7. Enable dependency updates and review pin changes like code.

Common mistakes include assuming an official-looking action is maintained by GitHub, pinning only high-risk actions, granting write permissions globally, and interpolating issue titles or branch names directly into shell scripts. Pass untrusted values through environment variables and quote them safely.

SHA pinning controls which action revision runs; least privilege controls the damage it can do. Use both, backed by trigger review and isolated deployment design.