2026-06-11 · 7 min read
Hardening GitHub Actions: OIDC, Pinned Actions, and Least Privilege
A practical guide to securing your GitHub Actions pipelines, replace long-lived cloud keys with OIDC, pin actions to SHAs, scope permissions, and stop supply-chain attacks before they start.

Hardening GitHub Actions: OIDC, Pinned Actions, and Least Privilege
CI/CD pipelines are a juicy target: they hold cloud credentials, run third-party code, and deploy to production. Most GitHub Actions setups I review have the same handful of avoidable risks. Here's how to close them.
1. Replace long-lived cloud keys with OIDC
The biggest risk is a stored AWS_SECRET_ACCESS_KEY or GCP service-account key sitting in repo
secrets. If it leaks, an attacker has standing access to your cloud.
Fix: OpenID Connect (OIDC). GitHub issues a short-lived token per run; your cloud trusts it and hands back temporary credentials. No long-lived keys anywhere.
permissions:
id-token: write # required for OIDC
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-deploy
aws-region: us-east-1
GCP and Azure have the same pattern (Workload Identity Federation). Once you switch, delete the stored keys.
2. Pin actions to a full commit SHA
uses: some/action@v3 follows a moving tag. If that action is compromised, v3 can be repointed at
malicious code and it runs with your secrets. Pin to an immutable SHA:
# Instead of: uses: actions/checkout@v4
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Use Dependabot to bump the SHAs so you stay current without losing the protection.
3. Set least-privilege permissions
By default the GITHUB_TOKEN can be broad. Set permissions explicitly, start from nothing and add
what each job needs:
permissions:
contents: read # most jobs only need this
Add packages: write or id-token: write only on the specific jobs that require them.
4. Be careful with pull_request_target and untrusted input
pull_request_target runs with repo secrets in the context of a PR, a classic exfiltration vector.
Avoid checking out and running untrusted PR code in that context. Treat any PR-supplied value as
hostile; never interpolate it directly into a run: shell line (script injection).
5. Scan in the pipeline
Add the basics so problems fail the build, not production:
- gitleaks: catch committed secrets.
- Trivy / Grype: scan images and dependencies.
- tfsec / checkov: scan IaC.
(My devsecops-starter-kit wires these in.)
6. Protect the branches that deploy
- Require PR review and passing checks on
main. - Use environments with required reviewers for production deploys.
- Restrict who can edit workflows.
The short checklist
- OIDC instead of long-lived cloud keys
- Actions pinned to SHAs (+ Dependabot)
- Explicit least-privilege
permissions: - No untrusted input in
run:; careful withpull_request_target - Secret/image/IaC scanning in CI
- Branch protection + environment approvals
None of this is exotic, it's the difference between a pipeline that's a liability and one that's an asset. Do it once and it protects every deploy after.
Want your CI/CD pipelines hardened? That's part of my DevOps consulting, reach out.