A GitHub Actions runner can, by default, reach any address on the internet. If a dependency or a third-party action is compromised, it exfiltrates your secrets without meeting the slightest obstacle. Harden-Runner is a security agent working like an EDR for your runners: it watches the network traffic, the file integrity and the processes, then lets you block whatever is not explicitly allowed. This guide shows you how to audit it, restrict its egress to an allowlist, disable sudo and spot a compromise. Prerequisites: a repository with workflows, and familiarity with pinning actions by SHA. Examples tested with Harden-Runner v2.20.0.
What is Harden-Runner?
Harden-Runner is an open source action published by StepSecurity that installs a security agent on the runner, at the very start of the job. Where scanners such as zizmor or poutine analyse your workflow files before execution, Harden-Runner acts during the build. It watches three things in real time:
- the outbound network traffic (egress), each connection tied to the step that emitted it;
- the integrity of the files modified during the job;
- the processes started on the runner.
That runtime watching is what made it possible to detect real supply chain
attacks, such as the compromise of the tj-actions/changed-files action in
2025, where a runner was exfiltrating secrets to an unexpected address.
The problem: a runner that can exfiltrate anything
Take an ordinary continuous integration workflow. It checks out the code,
installs dependencies, runs tests. Nothing suspicious on the face of it, but
nothing limits the network: if one of the installed dependencies carries a
malicious payload, it can ship the GITHUB_TOKEN or your secrets to the
attacker's server, and the build will succeed as if nothing happened.
The first reflex: audit mode
You never block blindly. The right approach starts with audit mode, which
observes without cutting anything and lists the endpoints your workflow
really contacts. Harden-Runner must be the very first step of the job,
before the checkout, so it intercepts all the traffic. Pin it by SHA, like any
third-party action:
name: Hardened CI
on: push: branches: [main]
permissions: {}
jobs: build: name: Build and test runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read steps: - name: Harden the runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit
- name: Check out the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Build run: make buildAt the end of the run, Harden-Runner publishes a summary of the connections in the job logs and on the StepSecurity dashboard. You see every destination reached, tied to the step that emitted it: the starting point for building your allowlist.
Reading the insights
The audit result can be read in two places. In the job tab, Harden-Runner
adds a recap listing the domains contacted. On the dashboard at
app.stepsecurity.io, a richer view correlates connections, files and
processes, and offers hardening recommendations directly, such as disabling sudo
when the job does not use it.
The purpose of that reading is simple: separate the legitimate endpoints (the package registry, the GitHub API, your internal mirror) from the connections you cannot explain. The former become the allowlist, the latter deserve an investigation.
Blocking egress with an allowlist
Once the list of legitimate destinations is established, switch egress-policy
to block and declare them in allowed-endpoints, in host:port form,
one per line. Any connection to a destination outside that list is then cut,
at the DNS and network layers.
- name: Harden the runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: block allowed-endpoints: > github.com:443 api.github.com:443 objects.githubusercontent.com:443From then on, the malicious payload from the earlier scenario hits a wall: its connection attempt towards the attacker's server is blocked, and the event shows in red in the insights. Egress filtering turns a silent exfiltration into a visible alert.
Hardening the runner further
Egress filtering pairs with two useful settings. GitHub-hosted runners grant
passwordless sudo: a compromised build tool can therefore install whatever
it likes. If your job does not need it, cut that access. The historical
disable-sudo input is being deprecated in favour of
disable-sudo-and-containers, which also removes container access:
- name: Harden the runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: block disable-sudo-and-containers: true allowed-endpoints: > github.com:443 api.github.com:443The disable-telemetry input stops the agent sending its data to the
StepSecurity API, which helps when your policy forbids any outbound telemetry.
For organisations, a centralised policy (the policy, api-key and
use-policy-store inputs) lets you define the allowlist once and apply it to
every repository, rather than repeating it in each workflow.
Traps and limits
Harden-Runner is an excellent guardrail, provided you know its bounds.
| Trap | Reality | The right reflex |
|---|---|---|
Placing the action after checkout | Earlier traffic escapes the watch | Make it the first step of the job |
Pinning it by the @v2 tag | A mobile third-party action can be hijacked | Pin by SHA (rule GHA-01) |
| Believing the blocking is infallible | An egress bypass through DNS-over-HTTPS existed on the free tier | Keep the agent up to date, combine with other controls |
Relying on disable-sudo alone | CVE-2025-32955 allowed it to be bypassed | Version 2.12.0 or later plus disable-sudo-and-containers |
| Expecting it everywhere | The agent targets Ubuntu runners | Plan another approach on macOS and Windows |
Above all, remember that Harden-Runner belongs to a defence in depth. It replaces neither SHA pinning, nor minimal permissions, nor static workflow analysis: it adds the runtime layer that was missing, the one that sees what happens during the build.
Community or Enterprise?
Harden-Runner comes in two tiers. The Community version, free and sufficient for the essentials, covers exactly what this guide shows: auditing and blocking egress, an endpoint allowlist, disabling sudo, and the connection recap inside the job. That is the baseline to deploy across all your repositories.
The Enterprise tier adds centralised management (the policy store, to define a single policy applied across the organisation), finer process monitoring, historical correlation across runs, and alerts on anomalous behaviour. It targets teams that must prove and steer the compliance of many pipelines. Start with the Community version: it already blocks the most common class of attack.
Key points
- Harden-Runner watches a GitHub Actions runner at run time: egress traffic, file integrity, processes.
- Always start in
egress-policy: auditto discover the endpoints actually used. - Then switch to
blockwithallowed-endpoints(host:port) to cut any exfiltration. - Make it the first step of the job and pin it by SHA, otherwise the protection is incomplete.
- Cut sudo with
disable-sudo-and-containers(which replacesdisable-sudo) when the job does not need it. - It is a layer of defence in depth, not a substitute for pinning, minimal permissions and static analysis.
Next steps
- Threat modelling a workflow: the seven questions that say whether egress filtering is your priority or a detail.
- OIDC: authentication without secrets: reducing what an exfiltration is worth, by replacing static credentials with tokens that live minutes.
- Security checklist: the systematic pass that verifies runtime hardening is in place on every workflow.