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
- Set
permissions: contents: readby default and grant more per job only when needed. - Use OIDC federation (
id-token: write) to get short-lived cloud credentials; don't store long-lived access keys as secrets. - Pin third-party actions to full commit SHAs, and keep them updated with Dependabot.
- Never interpolate untrusted input (
${{ github.event.* }}) directly intorun:scripts. Pass it throughenv:. - Treat
pull_request_targetandworkflow_runwith extreme care; never run untrusted PR code with secrets. - Protect deployments with environments (required reviewers, branch restrictions), and don't use self-hosted runners for public repositories.
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:
- AWS: IAM role trust policy on
token.actions.githubusercontent.com, conditioned onsub(for examplerepo:acme/api:ref:refs/heads/mainor:environment:production) andaud. - GCP: Workload Identity Federation pool and provider with attribute conditions.
- Azure: federated credentials on an app registration or managed identity.
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:
- Pin to a full-length commit SHA with a comment noting the version; Dependabot and Renovate update SHA pins.
- Prefer actions from GitHub, verified creators, or ones you've reviewed. Fork critical ones into your org.
- Use organization policies to allow only approved actions.
- Enable immutable releases and artifact attestations where available.
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
pull_requestfrom forks: read-only token, no secrets. It's safe to run untrusted code here.pull_request_target: runs in the base repository with a write token and secrets. If it checks out and builds the PR's head (ref: ${{ github.event.pull_request.head.sha }}), a fork can steal secrets. This is known as a "pwn request". Use it only for metadata tasks such as labeling, and never execute PR code in it.workflow_run: runs privileged after an untrusted workflow finishes. Treat artifacts it downloads from that run as untrusted input.
Secrets
- Store secrets at the narrowest scope: environment secrets for deployments, repository secrets otherwise, and organization secrets restricted to specific repositories.
- GitHub masks registered secret values in logs, but not transformed versions (base64-encoded or split). Don't echo secrets or derived values.
- Secrets aren't available to fork PRs or Dependabot-triggered runs by default, by design.
- Rotate any secret that may have been exposed, and prefer OIDC so there's nothing to rotate. See secrets management.
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
- GitHub Actions — The platform overview
- GitHub Actions Workflow Syntax — Permissions, environments, and triggers
- Reusable Workflows — Centralizing hardened pipelines
- Secrets Management — Storing and rotating credentials
- AWS IAM — Role trust policies for OIDC
- Container Security — Signing and scanning build outputs