Skip to content

GitHub Actions Security: Permissions, Secrets, OIDC and Supply Chain

7 min readGitHub Actions & CI/CD · Actions Security

CI/CD is the highest-value target in most engineering organisations. It holds credentials for every environment, it runs on every change, it can push code and publish artefacts, and it is frequently configured once and never reviewed again.

This cluster is about making that infrastructure defensible.

Start with GitHub Actions OIDC

Naming the objectives makes the defences make sense.

Credentials. Cloud keys, registry tokens, signing keys. A pipeline with a long-lived AWS access key in a repository secret is holding a credential that works from anywhere, forever, for whoever obtains it.

Code execution with your identity. A workflow runs with GITHUB_TOKEN. If an attacker can influence what it executes, they act as your repository — pushing commits, publishing releases, approving things.

The artifact. Compromising the build is more valuable than compromising the source, because the build output is what people install and it is what nobody reads.

Persistence. A self-hosted runner that survives between jobs is a machine to live on.

Each cluster lesson closes one of those routes.

Defence in depth for a pipeline

Five layers: eliminate long-lived credentials with OIDC, minimise token permissions, control what code runs by pinning actions, isolate the execution environment, and prove artifact provenance with attestations.

No stored credentialsOIDC instead of access keysMinimal permissionsGITHUB_TOKEN scoped per jobControlled codePinned actions, reviewed workflowsIsolated executionEphemeral runners, no untrusted codeProvable outputAttestations and verification

They are ordered by return on effort. The first two are configuration changes measured in lines. The last is a genuine engineering commitment. Most organisations get disproportionate value from doing the first two properly and never reaching the fifth.

If you take one thing from this cluster:

Replace stored cloud credentials with OIDC.

A long-lived cloud key in a repository secret is a credential that exists, can be exfiltrated, works from anywhere, and does not expire. OIDC replaces it with a short-lived token issued per run, scoped by claims your cloud provider verifies, valid for minutes.

permissions:
contents: read
id-token: write

Four lessons cover this: the concepts, then AWS, Azure and Google Cloud implementations, plus a migration guide for pipelines that already have keys to remove.

You should be able toCovered in
Write and read workflows fluentlyFundamentals
Explain what GITHUB_TOKEN isContexts and Expressions
Use environments and approvalsEnvironments
Choose a GitHub credential typeGitHub API Authentication
Configure repository policyRepository Rulesets

Cloud lessons assume enough access to create an identity provider and a role in the relevant provider. Every example uses placeholders; no real account identifiers, keys or tokens appear anywhere in this pillar.

  • Explain the OIDC trust architecture — issuer, subject, audience, claims and trust policy — rather than treating it as “logging in without a password”.
  • Federate GitHub with AWS, Azure and Google Cloud, restricted to specific repositories and refs.
  • Migrate an existing pipeline off long-lived credentials without an outage.
  • Set permissions to the minimum a job needs, and explain what each key grants.
  • Pin third-party actions by commit SHA and keep them updated deliberately.
  • Assess a third-party action before using it.
  • Recognise script injection from untrusted context data and write workflows that avoid it.
  • State precisely why pull_request_target is dangerous and when it is legitimate.
  • Operate self-hosted runners without giving strangers a foothold in your network.
  • Produce and verify build provenance attestations.
  1. Lesson 1: 01. GitHub Actions OIDCUnderstand OIDC in GitHub Actions as a trust architecture — what the token is, what claims it carries, who validates it, and why id-token write grants nothing by itself.Intermediate → Advanced5 min read
  2. Lesson 2: 02. AWS OIDCConfigure AWS to trust GitHub Actions — the OIDC identity provider, IAM role trust policy, sub claim conditions, session scoping and diagnosing AssumeRole failures.Advanced4 min read
  3. Lesson 3: 03. Azure OIDCConfigure Azure to trust GitHub Actions — app registration, federated identity credentials, subject identifiers, RBAC scoping and diagnosing AADSTS failures.Advanced4 min read
  4. Lesson 4: 04. Google Cloud OIDCConfigure Workload Identity Federation for GitHub Actions — pools, providers, attribute mappings and conditions, service account impersonation and direct federation.Advanced3 min read
  5. Lesson 5: 05. Remove Long-Lived CredentialsMigrate from stored cloud keys to OIDC without an outage — inventory, parallel running, cutover, verification that the old path is dead, and revocation.Intermediate → Advanced5 min read
  6. Lesson 6: 06. Least-Privilege PermissionsScope the GITHUB_TOKEN properly — the permissions key at workflow and job level, default settings, every available scope, fork behaviour and auditing what you grant.Intermediate4 min read
  7. Lesson 7: 07. Pinning ActionsReference actions safely — why tags are mutable, pinning to a full commit SHA, keeping pins current with Dependabot, allowlists, and the limits of pinning.Intermediate4 min read
  8. Lesson 8: 08. Secure Self-Hosted RunnersHarden your own runners — the fork threat model, ephemeral runners, network segmentation, runner groups, least-privileged service accounts and monitoring.Advanced6 min read
  9. Lesson 9: 09. Workflow SecurityPrevent script injection and untrusted-code execution — why expressions substitute before the shell runs, safe input handling, pull_request_target, and the workflow_run pattern.Advanced5 min read
  10. Lesson 10: 10. Software Supply-Chain SecurityProve what your pipeline built. Generate build provenance attestations and SBOMs in GitHub Actions, sign artifacts, and verify them before anything deploys.Advanced4 min read

Untrusted input. A pull request title, branch name or issue body is text an attacker chooses. Interpolated directly into a run: block, it becomes a command:

# Do not do this
- run: echo "Reviewing ${{ github.event.pull_request.title }}"

The value is substituted before the shell runs, so shell metacharacters in the title are shell syntax. The fix is to pass it through the environment, where it is data rather than script:

- env:
TITLE: ${{ github.event.pull_request.title }}
run: echo "Reviewing $TITLE"

One line different, and the entire class of vulnerability is gone. Workflow Security covers this and the other injection surfaces.

Third-party actions. uses: some-org/some-action@v1 runs somebody else’s code inside your job, with your token and your secrets in reach. A tag is a mutable pointer the author can move; a commit SHA cannot be changed under you. Pinning Actions covers the trade-off, and it is a genuine trade-off — pinning means you no longer receive security fixes automatically.

Being clear about scope. These are real, and they belong elsewhere:

Vulnerable dependencies in your application. Dependency scanning and update automation is a security-pillar subject; this cluster covers the dependencies of your pipeline.

Secrets committed to the repository. Secret scanning and history rewriting — related, and not specific to Actions.

Compromise of a maintainer’s account. Covered by Account Setup and Signed Commits.

Repository governance. Who may merge and who may change protected branches is the Pull Requests cluster of Pillar 3.

The boundary is roughly: this cluster covers what runs in your pipeline and what it can reach. Everything upstream — who can propose the code, whose account is trusted — is governance and account security.

Every lesson in this pillar raises the relevant security consideration where it belongs. The CI lessons cover why pull requests from forks get no secrets. The Docker lesson covers why you must not push images built from untrusted pull requests. The deployment lessons use OIDC throughout rather than presenting it as an upgrade.

This cluster is the systematic treatment, not the only place security appears — and reading it will change how the rest of the pillar reads.

For an existing setup, this is the order that finds the most in the least time.

  1. List every secret. gh secret list at repository, environment and organisation scope. For each, ask whether OIDC could remove it and whether anything still uses it.
  2. Check every workflow’s permissions block. Missing means the repository default; find out what that is.
  3. Find every third-party action. grep -rh 'uses:' .github/workflows/ | grep -v 'uses: actions/' — each is a supplier.
  4. Check version references. Tags are mutable; SHAs are not.
  5. Look for pull_request_target and workflow_run. Both run privileged. Confirm neither executes pull request code.
  6. Grep for interpolated event data in run: blocks — ${{ github.event. inside a shell command is the injection signature.
  7. Check who can change workflows. .github/workflows/ in CODEOWNERS, and whether the default branch requires review.
  8. Review self-hosted runners, if any: what network they sit on, whether they are ephemeral, and whether fork pull requests can reach them.

Steps 1 and 6 typically find the most. Step 8 finds the worst.

A pipeline that has had this treatment:

  • Holds no long-lived cloud credentials — OIDC everywhere, with trust policies restricted to specific repositories and refs.
  • Declares permissions on every workflow, narrowed further per job where one needs more.
  • Pins third-party actions by commit SHA, with Dependabot keeping them current.
  • Passes every event-derived value through env: rather than interpolating it.
  • Runs untrusted pull requests only on ephemeral, isolated runners — or only on GitHub-hosted ones.
  • Gates production behind an environment with protection rules.
  • Produces attestations for anything it publishes, and verifies them before deploying.

That list is achievable incrementally. The first two items are configuration changes measured in lines and deliver most of the value; the last is a genuine engineering commitment that many organisations correctly decide not to make.

This is the final cluster of Pillar 4. What it opens onto is a security pillar proper: repository security, secret scanning, dependency management, signing and DevSecOps practice more broadly.

For now, the practical endpoint is a pipeline that holds no long-lived credentials, runs with minimal permissions, executes only code you have pinned, and can prove what it produced.

This cluster is more prescriptive than the rest of the pillar. Where other lessons present trade-offs, several of these state a rule — do not store cloud keys, do not execute pull request code with secrets, do not run untrusted code on a privileged runner.

That is deliberate. The trade-offs on the other side of those rules are almost always convenience, and the failure modes are almost always severe and slow to detect.

Begin: GitHub Actions OIDC

The GitHub Actions Security Checklist turns this cluster into something you can audit against, with the specific attack each item prevents. Lab: secure an unsafe workflow is the same material as an exercise.