A transparent, domain-allowlist egress firewall for CI runners and containers. Only the domains you list can be reached; every other host is sinkholed and the connection fails.
Same idea as StepSecurity harden-runner's block
mode, but self-contained, free, and it actually enforces the block — harden-runner's community
tier only monitors the allowlist on GitHub-hosted runners (a non-allowlisted host like
example.com still returns HTTP 200); egress-guard drops it.
CI jobs run a lot of third-party code — npm/pnpm install, build tools, LLM/agent CLIs. A single
compromised dependency can exfiltrate your secrets/tokens to an attacker's host. egress-guard
limits outbound traffic to an explicit allowlist so an unexpected call (e.g. to example.com)
simply fails.
- A local dnsmasq forwards only allowlisted domains to a real upstream resolver; every
other name is answered with
0.0.0.0(address=/#/0.0.0.0), so a connection to it goes nowhere. /etc/resolv.confpoints at dnsmasq.- iptables drops any DNS that tries to bypass dnsmasq (so a process can't hardcode
8.8.8.8).
Domain-based (not IP-based), so it survives CDN IP rotation — unlike a static iptables -d <ip>
allowlist.
- uses: artschekoff/egress-guard@v1
with:
allowed-domains: >
github.com api.github.com codeload.github.com
registry.npmjs.org npm.pkg.github.com api.osv.dev
# ...the rest of the job now runs under the egress allowlist.
# A call to any other host fails.Inputs: allowed-domains (required), mode (block default | audit), upstream-dns
(default 1.1.1.1), include-runner-baseline (default true). Runs on Linux runners; needs
sudo (default on GitHub-hosted ubuntu).
Runner baseline: on top of your allowed-domains, the runner's own infra
(github.com, githubusercontent.com, blob.core.windows.net, actions.githubusercontent.com)
is allowed by default so the job can still upload logs/results — otherwise a minimal allowlist
strangles the runner's own finalization and the job hangs. Set include-runner-baseline: false
if you want a strict allowlist you fully control.
Optional final step to surface / fail on blocked attempts:
- run: FAIL_ON_BLOCK=true bash "$GITHUB_ACTION_PATH/scripts/report-blocked.sh"docker build -t egress-guard .
docker run --rm --cap-add=NET_ADMIN \
-e ALLOWED_DOMAINS="github.com,registry.npmjs.org" \
egress-guard bash -c 'curl https://github.com; curl https://example.com'
# github.com -> 200 ; example.com -> connection failsOr ./run-demo.sh for the full build + proof.
| Mode | Behaviour |
|---|---|
block (default) |
Non-allowlisted names sinkhole to 0.0.0.0 — connections fail (enforced). |
audit |
Everything resolves; all lookups are logged. Nothing is blocked (visibility only). |
| harden-runner (community, GitHub-hosted) | egress-guard | |
|---|---|---|
| Blocks arbitrary non-allowlisted host | ❌ monitors only | ✅ enforced |
| Domain-based (CDN-safe) | ✅ | ✅ |
| Process / file / step-correlated EDR, baseline, anomaly detection | ✅ (it's an EDR) | ❌ (network only) |
| Cost of real enforcement | paid/enterprise | free |
| Transparent (every rule visible) | ❌ agent + backend | ✅ dnsmasq + iptables |
They are complementary: run harden-runner in audit for visibility/detection and
egress-guard in block for real enforcement.
- DNS-over-HTTPS (DoH): an attacker with code execution can still tunnel data through an allowed HTTPS host's DoH endpoint. Same limitation as harden-runner's community tier; fully closing it needs deep packet inspection.
- Needs root /
NET_ADMIN(for dnsmasq + iptables). Standard on GitHub-hosted ubuntu. - Linux only.
MIT — see LICENSE.