Everyone is talking about supply-chain attacks again, especially after the public campaigns that abused self-hosted and ephemeral runners in early 2025. The common thread: attackers weaponised pull requests to run malicious workflows, exfiltrate long-lived credentials, and ship tampered artifacts to registries. Here’s how I hardened my GitHub Actions estate without grinding the release train to a halt.
What the current wave looks like
The noisy incidents from the past quarter followed a familiar pattern:
- Forks or compromised contributors opened PRs that tweaked workflow YAML to run untrusted scripts.
- Default
GITHUB_TOKENscopes allowed package publishes, environment promotions, or even repo administration. - Self-hosted runners cached container layers or tools that kept payloads resident for the next build.
- Build results were not verified before deployment, so tampered artifacts moved downstream unnoticed.
If that sounds uncomfortably close to your setup, start with the guardrails below.
Shrink the blast radius of workflow tokens
The easiest win is forcing least privilege on every workflow. I standardised a shared workflow template that includes scoped permissions and ephemeral cloud credentials:
permissions:
contents: read
id-token: write
packages: none
jobs:
build:
runs-on: ubuntu-latest
environment: build
steps:
- uses: actions/checkout@v4
- name: Authenticate to AWS with OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-build
role-session-name: gha-${{ github.run_id }}
Cutting the default write-all scope eliminated a huge chunk of risk. When a job needs a new API, I add the permission intentionally and record the justification in code review.
Treat workflow YAML as production code
Pull requests that touch .github/workflows now trigger an approval gate from the DevSecOps group. I also lint with actionlint to block obvious footguns, and I run Codium’s AI-based policy check to catch suspicious shell expansions. This small delay is cheaper than rolling back a compromised package release.
Keep runners clean and observable
For self-hosted runners I:
- Rebuild the runner image daily and wipe workspace caches between jobs using
--oncemode. - Enable
auditdand feed OIDC-issued telemetry into my SIEM so I can trace which repository triggered which command. - Put runners on a dedicated subnet with egress-only access to artifact stores and GitHub.
Managed runners get fewer knobs, but I still add runtime constraints with job-level timeout-minutes and concurrency limits to stop unreviewed pull requests from melting the budget.
Verify artifacts before promoting them
Signing and SBOMs went from “nice to have” to required. Each repository I manage now:
- Generates an SPDX SBOM with
syftduring the build. - Signs container images and release archives with Sigstore Cosign.
- Verifies signatures and SBOM hashes in the promotion pipeline before deploying to staging or production.
This means a hostile or tampered build cannot silently land in production. The promotion job will fail unless the signature and bill of materials match what the build job produced.
Quick wins to copy
- Set repo defaults: disable
GITHUB_TOKENwrite permissions, require approval for first-time contributors, and lock workflow edits to trusted teams. - Adopt reusable workflows: manage your security guardrails from one place instead of copying them across dozens of repos.
- Instrument everything: push runner logs, OIDC access events, and deployment metadata to a central view so you can chase anomalies in minutes, not hours.
- Drill incident response: rehearse revoking tokens, re-issuing secrets, and rebuilding artifacts so the team remembers the muscle memory under pressure.
Supply-chain attacks will keep evolving, but these practices give you a comfortable buffer: tighter credentials, cleaner runners, and verifiable artifacts. Most importantly, they’re achievable in a sprint or two without putting the brakes on feature delivery. If you haven’t reviewed your GitHub Actions estate this quarter, consider this your nudge.