Skip to content
Français
CI/CD & Automatisation medium

Hardening GitHub Actions runners with Harden-Runner

40 min de lecture

Read this page in French

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 build

At 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:443

From 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:443

The 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.

TrapRealityThe right reflex
Placing the action after checkoutEarlier traffic escapes the watchMake it the first step of the job
Pinning it by the @v2 tagA mobile third-party action can be hijackedPin by SHA (rule GHA-01)
Believing the blocking is infallibleAn egress bypass through DNS-over-HTTPS existed on the free tierKeep the agent up to date, combine with other controls
Relying on disable-sudo aloneCVE-2025-32955 allowed it to be bypassedVersion 2.12.0 or later plus disable-sudo-and-containers
Expecting it everywhereThe agent targets Ubuntu runnersPlan 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: audit to discover the endpoints actually used.
  • Then switch to block with allowed-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 replaces disable-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

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn