Securing GitHub Actions: Secrets, OIDC and Permissions Done Right

Practical tips for hardening GitHub Actions workflows with OIDC, secret management, action pinning and least-privilege permissions.

Jean-Pierre Broeders

Freelance DevOps Engineer

March 15, 20266 min. read

Securing GitHub Actions: Secrets, OIDC and Permissions Done Right

Most teams get their CI/CD pipeline running within an afternoon. Tests pass, deployments go out automatically, everyone's happy. But ask how many of those pipelines are actually secured properly. The answer is usually: not great.

A GitHub Actions workflow has access to your code, your secrets, and often your cloud environment. That's an attack surface that gets overlooked way too often. Here are the things that make the difference between "it works" and "it works safely".

Permissions: Too Broad by Default

GitHub gives workflows contents: read and actions: read permissions by default. But the moment a GITHUB_TOKEN gets used without explicit scoping, that token ends up with more privileges than necessary.

The fix is simple but rarely applied: set permissions explicitly at the workflow level.

permissions:
  contents: read
  packages: write
  id-token: write

Put this at the top of the workflow, and every job gets exactly the rights it needs. Nothing more. A workflow that only runs tests doesn't need packages: write. Sounds obvious, but it's almost never configured.

Even better: set the default permissions to read-only at repository level via Settings → Actions → General. That forces every workflow to explicitly declare what it needs.

Secrets: Stop Dumping Everything in Repository Secrets

Repository secrets are convenient, but there's a catch: every workflow in that repo can access them. With larger teams running multiple workflows, things get messy fast.

Environment secrets offer more control. By creating an environment (e.g. production) and attaching secrets to it, access is limited to workflows that target that specific environment.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy to production
        env:
          API_KEY: ${{ secrets.PROD_API_KEY }}
        run: ./deploy.sh

The bonus: environments also support required reviewers. Nobody deploys to production without someone clicking "approve". Feels like overhead until it prevents the Friday afternoon panic deploy.

OIDC: Ditch Those Long-Lived Credentials

This might be the single most impactful improvement. Traditionally, cloud credentials (AWS access keys, Azure service principal secrets) get stored as repository secrets. Those keys either don't expire, or have an expiry date so far out that nobody bothers rotating them.

OpenID Connect (OIDC) fixes this. Instead of a static key, the workflow requests a short-lived token from the cloud provider. No more secrets sitting in GitHub for months.

For AWS, the setup looks like this:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
          aws-region: eu-west-1

      - name: Deploy
        run: aws s3 sync ./dist s3://my-bucket

No AWS_ACCESS_KEY_ID in secrets anymore. The token AWS issues is valid for 15 minutes. Even if someone intercepts it, the window for abuse is minimal.

Azure works similarly with azure/login and a federated credential on the app registration. Google Cloud uses google-github-actions/auth. The pattern is the same everywhere.

Pin Actions to a SHA

A popular best practice that almost nobody follows: pin third-party actions to a commit SHA instead of a version tag.

The difference:

# ❌ Risky: tag can be moved
- uses: actions/checkout@v4

# ✅ Safe: SHA is immutable
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

Why? A maintainer (or attacker with access) can move a tag to a different commit. With a SHA, that's impossible. GitHub's Dependabot can automatically update these SHAs via PRs, so the maintenance burden is manageable.

For internal actions within the same organisation, this matters less. But for anything third-party: pin to SHA.

MethodSecurityMaintenance
Tag (v4)Low — tag can shiftEasy
Branch (main)Very low — changes constantlyNone
SHAHigh — immutableDependabot helps

Fork PRs: An Underestimated Risk

Open source projects receive pull requests from forks. By default, workflows on fork PRs run with limited permissions (no access to secrets). But when pull_request_target is used instead of pull_request, that changes. The workflow then runs in the context of the base repo, with full access to secrets.

This is by design for specific use cases, but it gets misused regularly. The rule of thumb: only use pull_request_target when you know what you're doing, and never checkout the PR code with actions/checkout without explicitly restricting the ref.

Audit Logging

GitHub Enterprise provides audit logs for Actions. But even without Enterprise, there are options. The actions/toolkit has a core.setSecret() function that prevents values from appearing in logs. Use it for anything sensitive, even if it doesn't come from secrets.

- name: Mask dynamic secret
  run: echo "::add-mask::${DYNAMIC_VALUE}"

Small effort, but it prevents tokens or API responses from showing up in build logs that might be public.

Wrapping Up

Pipeline security isn't a one-time thing. It's a hygiene practice, same as writing tests or updating dependencies. The essentials:

  • Permissions at workflow level, default read-only at repo level
  • Environment secrets instead of repository secrets for sensitive environments
  • OIDC for cloud access, no more static keys
  • SHA pinning for third-party actions
  • Fork PR workflows configured correctly

None of these steps take more than an hour to set up. The gap between a pipeline that "just works" and one that works safely is surprisingly small.

Want to stay updated?

Subscribe to my newsletter or get in touch for freelance projects.

Get in Touch