Skip to content
T threat.cafe

· Nadia Ferreira-Strand (editorial persona) · Enforcement architecture · on threat.cafe

Egress allowlisting for coding agents and CI runners: the control that would have stopped the axios callback

Restrict outbound network access for coding agents and CI runners to a short list of known destinations. Most malware that reaches developers through packages, skills or MCP servers needs a second connection: to fetch its payload or to send stolen data out. In the 31 March 2026 axios compromise, StepSecurity's runner monitor flagged exactly that connection during a routine CI run, and the company says its egress allowlist would have blocked the payload fetch before the remote access trojan arrived (StepSecurity). Run the allowlist in audit mode first, then block. The cost is maintaining the destination list.

Outcome, time and access

Outcome: CI jobs and agent shell sessions can reach only the hosts they need. Anything else is logged, then blocked.

Time: about an hour to start audit mode on one repository's CI and one agent profile. Two to four weeks of observation before blocking.

Access needed:

  • Admin on the GitHub repositories whose workflows you will change.
  • Rights to deploy Claude Code managed settings, or each developer's agreement to edit their user settings.
  • A named owner for the destination list. Without one, do not start.

Use this runbook if, and do not use it if

Use it if your CI runners or agents run with open internet access, which is the default for both.

Use it if you already block known-bad packages and want a layer for what the list has not caught yet.

Do not use it as your only control. The vendor evidence here comes from a company that sells egress control. Treat it as one layer.

Do not block on day one. Every build has destinations nobody wrote down.

The evidence, in order

StepSecurity's timeline, in UTC:

During that window, Harden-Runner flagged an outbound connection to the attacker's server on port 8000 during a routine CI run in the Backstage repository. StepSecurity says the connection "was automatically marked as anomalous because it had never appeared in any prior workflow run." The Backstage team confirmed that workflow is intentionally sandboxed and was not affected.

That last fact is the second case the vendor claim needs. One workflow with restricted egress met the malicious install and did not become an incident.

On the agent side, Claude Code's sandbox gives shell commands "no direct route out," per its sandboxing documentation. Connections pass through a local proxy that checks each host against "your allowed domains, which start empty." InfoQ's November 2025 report on the launch described the same design: network isolation so the agent "can only connect to approved servers."

Pre-flight checks

  1. Confirm runner type. Per the Harden-Runner README, GitHub-hosted Linux runners get full support. GitHub-hosted Windows and macOS runners get audit mode only. Self-hosted runners require StepSecurity's Enterprise tier. The free tier covers public repositories on GitHub-hosted runners.
  2. Confirm the agent sandbox can start. On macOS it uses Seatbelt. On Linux and WSL2 it needs bubblewrap and socat. Run /sandbox in Claude Code; its panel lists missing dependencies.
  3. Name the list owner and where change requests go.

Step-by-step execution

CI runners

  1. Add Harden-Runner as the first step of each job, pinned to a commit SHA. This is the README's own example:
steps:
  - name: Harden Runner
    uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0
    with:
      egress-policy: audit
  1. Run the workflows normally for two to four weeks, covering at least one release build.
  2. Export the observed destinations. Remove any you cannot explain, and ask the job owner about each one.
  3. Switch the job to block mode with the reviewed list. Check StepSecurity's documentation for the current input names before you do.
  4. Verify: add a throwaway step that calls curl https://example.com. The step should fail. Remove it after.

Agent sandboxes

  1. Turn on the sandbox with an explicit allowlist and no unsandboxed retries. A starter for a Node project:
{
  "sandbox": {
    "enabled": true,
    "allowUnsandboxedCommands": false,
    "network": {
      "allowedDomains": ["registry.npmjs.org", "github.com", "api.github.com"]
    }
  }
}
  1. Leave strictAllowlist off for the first two weeks. Unlisted hosts then prompt instead of failing, which shows you what is missing.
  2. Collect the hosts people approved. Add the legitimate ones to the list.
  3. Turn on "strictAllowlist": true under network. For a fleet, put the list in managed settings with allowManagedDomainsOnly, so only the managed entries count.
  4. Verify: ask the agent to run curl --noproxy '*' https://example.com. The docs say it should fail with Could not resolve host. Type nothing yourself: commands entered at the ! prompt usually run outside the sandbox.

The unified policy starter

One list, two enforcement points. Keep it in a repository the owner reviews.

EGRESS POLICY v1                          owner: ________  reviewed: ______

Tier A  Package registries      registry.npmjs.org, pypi.org,
                                files.pythonhosted.org, (your mirror)
Tier B  Source control          github.com, api.github.com (see warning)
Tier C  Your own services       artifact store, internal APIs
Tier D  Model and agent APIs    the provider endpoints your agent needs
                                (often outside the shell sandbox anyway)

CI runners:      A + B + C           audit 2-4 weeks -> block
Agent sandboxes: A + B (+ C if needed)  prompt 2 weeks -> strictAllowlist
Change process:  ticket -> owner review -> add with reason + date
Review cadence:  monthly; remove hosts unused for 90 days

These hostnames are an illustrative starting point. Your audit output decides the real list.

How much maintenance to expect

No public dataset in the sources here measures how many destinations a typical CI job or agent session needs, so measure your own before committing. The audit-mode weeks give you the number.

Illustrative example: if a repository's jobs show 12 distinct destinations after four weeks, and 2 of them change in a quarter, the owner handles about one change request a month for that repository. A repository showing 60 destinations, many of them one-off downloads in test fixtures, is telling you to vendor those files or route them through your artifact store first.

Record three numbers per repository at the end of audit mode: distinct destinations, destinations you could not explain, and change requests in the first month of blocking. Those three numbers are the honest cost of the control.

Three failure modes and exact fixes

Failure 1: the build breaks after blocking

A post-install script or test fixture calls a host that only appears in release builds or on a schedule. Fix: extend audit mode to cover a release cycle. Keep the allowlist change process fast; a slow process pushes teams to disable the control.

Failure 2: an allowed domain becomes the leak

The sandbox documentation warns that broad domains such as github.com "can create paths for data exfiltration." The proxy decides from the hostname without inspecting TLS, so domain fronting can reach hosts outside the list. Fix: prefer narrow hostnames. If your threat model needs more, the docs describe a custom proxy that terminates and inspects TLS.

Failure 3: traffic that never touches the sandbox

The sandbox wraps shell commands only. The docs state that local MCP servers, hooks and helper commands "run with your full access," and that built-in tools such as WebFetch follow permission rules instead. Fix: for full coverage, run the agent itself inside a container, VM or the @anthropic-ai/sandbox-runtime with a network allowlist. Tools that ignore proxy settings, such as plain ssh and most database drivers, cannot connect from inside the sandbox at all. Plan for that before blocking.

Rollback and post-change health check

Rollback trigger: a blocked destination stops a release and the owner cannot approve a fix within your release window.

  1. CI: set the affected job back to egress-policy: audit. Do not remove the step; you lose the record.
  2. Agents: turn off strictAllowlist in the scope you set it. Leave the sandbox on.
  3. Add the missing host through the normal change process, then re-enable blocking.

Health check, weekly for the first month: count blocked connections per job. A sudden new destination in a job that has not changed is worth a look, as Backstage's run showed. If an unexplained destination traces back to a dependency, treat the run as compromised and rotate every secret the job could reach, as StepSecurity advises.

The allowlist contains what a known-bad check has not caught yet; the threat.cafe board records the axios incident with its indicators and the finder credited.

Sources

#egress-control #ci-security #sandbox #axios #harden-runner

All posts