Hardening GitHub Actions: Secrets, OIDC, and Least Privilege

Philip Rehberger Oct 11, 2026 7 min read

Stop pasting long-lived AWS keys into Actions. OIDC, environment scoping, and pinned actions.

GitHub Actions is convenient. It also handles secrets, deploys to clouds, and runs whatever code your .github/workflows/ files describe. A compromised Actions setup means a compromised production environment, often in ways that are silent until the breach is public.

Hardening GitHub Actions does not require new tools — it requires using the features that already exist. This post is the configuration patterns that turn a workflow from "anything can happen" to "controlled, audited, least-privilege."

The Threat Model

What can go wrong in a GitHub Actions setup:

  • A malicious PR runs code on a runner with secrets attached
  • A third-party action you reference is compromised in a later version
  • A repository secret leaks through workflow logs
  • A workflow uses long-lived AWS credentials that get exfiltrated
  • A workflow can deploy to production from any branch

Each of these has a fix. None requires new infrastructure.

OIDC for Cloud Authentication

The biggest single improvement: replace long-lived cloud credentials with OIDC.

The traditional pattern stores AWS access keys as repository secrets. The keys live forever (until rotated). A workflow can use them; a compromised workflow can exfiltrate them.

The OIDC pattern: GitHub issues a short-lived token for each workflow run. AWS verifies the token via its OIDC provider and issues short-lived AWS credentials.

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
      - run: aws s3 cp ./build s3://mybucket/ --recursive

The AWS role's trust policy restricts who can assume it:

{
  "Effect": "Allow",
  "Principal": {
    "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
  },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub": "repo:yourorg/yourrepo:ref:refs/heads/main"
    }
  }
}

Only workflows from the main branch of yourorg/yourrepo can assume the role. A fork, a PR, or a different repo cannot.

Adopt OIDC for AWS, GCP, and Azure. The patterns are similar across providers. Long-lived keys in repository secrets are a 2020 pattern.

Permissions: Set Them Per Workflow

GitHub Actions has a permissions block at the workflow or job level. It controls what the workflow's GITHUB_TOKEN can do.

By default, the GITHUB_TOKEN often has broad read/write permissions. This is one of the most common Actions misconfigurations.

# Restrictive default
permissions: read-all

jobs:
  build:
    permissions:
      contents: read    # checkout
    runs-on: ubuntu-latest
    # ...

  release:
    permissions:
      contents: write    # tag and release
      packages: write    # publish to ghcr.io
    runs-on: ubuntu-latest
    # ...

Each job has only the permissions it needs. A compromised build step cannot push tags. A compromised release step cannot rewrite history.

Set the organization-wide default to read-all or read for the GITHUB_TOKEN. Override per workflow as needed. Most workflows do not need write permissions.

Pin Actions to SHAs

A common pattern:

- uses: actions/checkout@v4

The @v4 is a moving target. The maintainer can change what v4 points to. A compromised action update silently affects every workflow that references it.

The hardened pattern: pin to a specific commit SHA.

- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

The SHA cannot change without a different SHA. Dependabot can update these for you, with diffs you can review before merging.

For your own organization's actions: SHAs are still recommended, even though you trust the publisher. The SHA captures exactly what version was running at the time of the workflow.

The major exception is internal "reusable workflows" called from your own repo at ref main — the trust boundary is the same.

Restrict Where Workflows Can Run From

By default, any repository in your organization can use any workflow. Restrict this.

Organization settings → Actions → "Allow specified actions and reusable workflows" lets you allow only specific GitHub-owned actions, your own, and a small allow-list of verified creators.

This prevents a malicious or compromised third-party action from being added to a workflow without explicit approval.

Environment Protection Rules

For production deploys, GitHub Actions has environment protection rules. An environment can require:

  • Specific reviewers to approve before the workflow runs
  • A wait timer before deployment proceeds
  • Branch restrictions (only main can deploy to production)
  • Required environment variables (forces explicit configuration)
jobs:
  deploy-production:
    environment: production    # protected
    runs-on: ubuntu-latest
    # ...

The protection rules prevent a "deploy main to production" workflow from running automatically on every push. A human approves; the deploy proceeds.

Secret Scanning

GitHub Actions allows secrets to be referenced via ${{ secrets.NAME }}. The secrets are masked in logs — usually. The masking can be bypassed by:

  • Echoing the secret with a transform (echo ${SECRET:1})
  • Logging the secret to a file that is uploaded as an artifact
  • Sending the secret to a remote service via curl

Treat secret access as a privileged operation. Only the steps that need the secret should have access to it. Use env: scoping per step rather than per-job when possible.

The permissions block does not protect against secret exfiltration once the secret is in scope. The mitigation is to minimize the surface area — fewer steps with access to fewer secrets.

Fork PR Workflows

The most dangerous pattern: workflows that run automatically on PRs from forks. A malicious PR can include arbitrary workflow changes.

GitHub's defaults are relatively safe:

  • Workflows from fork PRs run in a restricted context
  • Secrets are not available
  • The GITHUB_TOKEN is read-only

But teams often relax these for convenience. Common bad patterns:

  • pull_request_target instead of pull_request — runs in the context of the target branch with secrets. A fork PR can now execute code with full repository access.
  • Custom checks that download and run untrusted code from the PR.

The safe rule: never give untrusted PR code access to secrets. Use pull_request (sandboxed). For deploys that need credentials, use a separate workflow that runs only on main.

Audit Logs

GitHub Audit Log shows every workflow run, secret access, permission change. For organizations with security teams, exporting this to a SIEM is valuable.

Things to alert on:

  • Changes to organization secrets
  • Permission changes on production-deploying repos
  • New runners added (especially self-hosted)
  • Actions allowed list changes

Most of these are infrequent enough that an alert per event is manageable.

Self-Hosted Runners

Self-hosted runners introduce new risks:

  • A compromised job can persist data on the runner between jobs
  • The runner has network access from your network
  • Long-lived runners accumulate cached state

The mitigations were covered in a separate post on self-hosted runners. Briefly:

  • Use ephemeral runners (destroyed per job)
  • Never run untrusted code (forks, PR-triggered) on self-hosted runners
  • Network-isolate runners from sensitive systems they do not need to reach

A Minimal Hardening Checklist

For an organization that wants to harden Actions over a week:

  1. Set organization-wide default permissions: read-all for GITHUB_TOKEN.
  2. Allowlist actions at the organization level.
  3. Pin actions to SHAs in all workflows.
  4. Adopt OIDC for cloud authentication; rotate out long-lived keys.
  5. Add environment protection rules for production deploys.
  6. Audit secrets: any unused, any over-permissioned, any not needed.
  7. Review pull_request_target usage. Most should be pull_request.
  8. Stream audit log to SIEM if you have one.

A week of work prevents an entire category of incidents.

What This Does Not Cover

Hardening Actions reduces the supply chain attack surface. It does not address:

  • Compromised developers (a developer with commit access can still cause damage)
  • Bugs in dependencies (need software composition analysis)
  • Issues in the application code itself

Defense in depth: hardened Actions is one layer, not the only one.

The Practical Outcome

A hardened GitHub Actions setup is mostly invisible. Workflows work the same as before. Permissions are more restrictive but rarely missing. OIDC replaces secrets in the right places. The audit trail is searchable.

The investment is one engineer for one week. The benefit is preventing the class of "compromised CI led to compromised production" incidents that have hit several major companies in recent years.


Reviewing GitHub Actions security and not sure where the gaps are? We help teams audit Actions setups and roll out OIDC, pinning, and protection rules without breaking active workflows. scopeforged.com

Share this article

Related Articles

Need help with your project?

Let's discuss how we can help you build reliable software.