GitHub Actions Security

CI/CD pipelines are high-value targets. A workflow typically has write access to the repository, publishes packages, and holds credentials that can deploy to production. Compromising it means compromising everything downstream. Real-world supply-chain incidents have hit GitHub Actions directly: popular third-party actions have been hijacked to leak secrets from thousands of repositories, and workflows have been exploited through crafted pull request titles and branch names.

The good news: a small set of practices removes most of the risk. Minimal token permissions, OIDC instead of stored cloud keys, pinned dependencies, careful handling of untrusted input, and isolation for anything touching pull requests from forks.

TL;DR

Quick Example

A hardened deployment workflow:

The IAM role's trust policy only accepts tokens whose subject is repo:acme/orders-api:environment:production, so only this workflow in this environment can assume it.

Core Concepts

GITHUB_TOKEN Permissions

Every job gets an automatically generated GITHUB_TOKEN scoped to the repository. Its permissions come from the repository or organization default (set it to read-only) and the permissions: key:

Declaring any permission sets all unlisted ones to none. Tight permissions limit the damage when a step or action is compromised.

OIDC Federation

Instead of storing an AWS key or GCP service account JSON as a secret, the job requests a signed OIDC token from GitHub describing exactly who it is: repository, branch, environment, workflow. The cloud provider trusts GitHub's issuer and exchanges the token for short-lived credentials, but only if claims match the trust policy:

No long-lived secret exists to leak or rotate. The same mechanism powers keyless package publishing (npm provenance, PyPI trusted publishing) and artifact signing.

Third-Party Actions Are Dependencies

uses: some/action@v3 runs someone else's code with your token and secrets. Tags are mutable: the owner, or an attacker who compromises the owner, can move v3 to malicious code. Mitigations:

Script Injection

Expressions are substituted into scripts before the shell runs. Attacker-controlled values, such as PR titles and bodies, issue comments, branch names, and commit messages, can break out of quotes and run commands:

The same applies to actions/github-script inputs. Pass untrusted values as environment variables or action inputs, never inline in code.

Dangerous Triggers

Secrets

Self-Hosted Runners

Self-hosted runners execute workflow code on your infrastructure, and persistent runners can be poisoned by one job for the next. Never attach them to public repositories, where any fork PR could run code on them. Use ephemeral runners (one job per VM or container, for example with Actions Runner Controller on Kubernetes), isolate them in dedicated network segments, and give them minimal cloud permissions.

Best Practices

Scan Workflows Automatically

Tools like zizmor, actionlint, and CodeQL's Actions queries flag injection risks, excessive permissions, unpinned actions, and dangerous triggers. Run them in CI and code review.

Gate Production With Environments

Require reviewers, restrict deployments to protected branches, and keep production credentials (or OIDC role trust) bound to the environment. A compromised PR workflow then can't deploy.

Restrict Who Can Change Workflows

Use CODEOWNERS on .github/workflows/ so pipeline changes need platform or security review, and require approval before running workflows from first-time contributors.

Protect Build Outputs

Sign images and packages and generate provenance (SLSA attestations via actions/attest-build-provenance), so consumers can verify artifacts came from your pipeline. See container security.

Common Mistakes

Long-Lived Cloud Keys in Secrets

AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY stored as repository secrets are powerful, rarely rotated, and exfiltrated by any compromised step. Replace them with OIDC roles scoped to repository and environment.

Default Write-All Token Permissions

Older repositories may still grant the GITHUB_TOKEN write access to everything by default. Switch the organization default to read-only and declare permissions explicitly in each workflow.

Caching or Uploading Secrets

Writing credentials into files that end up in caches or artifacts (a .npmrc with a token, a kubeconfig) can expose them to anyone with read access to the repository's Actions data. Keep credentials in memory or environment variables, and use persist-credentials: false on checkout.

FAQ

Why pin actions to SHAs instead of tags?

A tag is a movable pointer. If an action's repository is compromised, the attacker can retag v4 to malicious code, and every workflow using @v4 runs it on the next build. A full commit SHA is immutable. Pinning plus automated update PRs gives both safety and freshness.

Is pull_request_target ever safe?

Yes, when it never executes code from the pull request: labeling PRs, posting comments, triaging based on metadata. Problems start when it checks out the PR's head commit and runs its build or tests with the base repository's secrets and write token.

What does id-token: write actually grant?

Only the ability to request an OIDC token from GitHub for this job. The token itself grants nothing until a cloud provider's trust policy accepts it. Security therefore depends on writing tight trust conditions (specific repository, branch, or environment), not broad ones like repo:acme/*.

Are GitHub-hosted runners safe to use?

Yes. Each job gets a fresh, isolated VM that's destroyed afterwards, so jobs can't contaminate each other. The main risks lie in what your workflow does: its permissions, the actions it runs, and how it handles untrusted input.

Related Topics

References