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 OIDCWhat an attacker wants from your pipeline
Section titled “What an attacker wants from your pipeline”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.
The five defences
Section titled “The five defences”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.
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.
The single highest-value change
Section titled “The single highest-value change”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: writeFour lessons cover this: the concepts, then AWS, Azure and Google Cloud implementations, plus a migration guide for pipelines that already have keys to remove.
Prerequisites
Section titled “Prerequisites”| You should be able to | Covered in |
|---|---|
| Write and read workflows fluently | Fundamentals |
Explain what GITHUB_TOKEN is | Contexts and Expressions |
| Use environments and approvals | Environments |
| Choose a GitHub credential type | GitHub API Authentication |
| Configure repository policy | Repository 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.
Learning objectives
Section titled “Learning objectives”- 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
permissionsto 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_targetis dangerous and when it is legitimate. - Operate self-hosted runners without giving strangers a foothold in your network.
- Produce and verify build provenance attestations.
The learning path
Section titled “The learning path”- 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.
- 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.
- Lesson 3: 03. Azure OIDCConfigure Azure to trust GitHub Actions — app registration, federated identity credentials, subject identifiers, RBAC scoping and diagnosing AADSTS failures.
- Lesson 4: 04. Google Cloud OIDCConfigure Workload Identity Federation for GitHub Actions — pools, providers, attribute mappings and conditions, service account impersonation and direct federation.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Two things that are not optional
Section titled “Two things that are not optional”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.
Threats this cluster does not cover
Section titled “Threats this cluster does not cover”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.
Security is not only this cluster
Section titled “Security is not only this cluster”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.
A pipeline security review
Section titled “A pipeline security review”For an existing setup, this is the order that finds the most in the least time.
- List every secret.
gh secret listat repository, environment and organisation scope. For each, ask whether OIDC could remove it and whether anything still uses it. - Check every workflow’s
permissionsblock. Missing means the repository default; find out what that is. - Find every third-party action.
grep -rh 'uses:' .github/workflows/ | grep -v 'uses: actions/'— each is a supplier. - Check version references. Tags are mutable; SHAs are not.
- Look for
pull_request_targetandworkflow_run. Both run privileged. Confirm neither executes pull request code. - Grep for interpolated event data in
run:blocks —${{ github.event.inside a shell command is the injection signature. - Check who can change workflows.
.github/workflows/in CODEOWNERS, and whether the default branch requires review. - 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.
What good looks like
Section titled “What good looks like”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
permissionson 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.
Then what?
Section titled “Then what?”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.
A note on tone
Section titled “A note on tone”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 OIDCAudit your own workflows
Section titled “Audit your own workflows”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.