101% more reported CVEs per day in 2026 than last year.

CI/CD security

Stopping exfiltration from GitHub Actions with an egress policy

A CI job holds secrets and runs code it downloaded a minute ago: actions, packages and their install scripts. If any of that code is malicious, the easiest way to steal the secrets is to send them to a server. An egress policy decides which servers a job may reach, and blocks the rest.

Updated

How secrets leave a CI job

A job has access to the repository token, the secrets passed to it and any cloud credentials it assumes. Code that runs in the job can read them. It can then get them out in a few ways:

  • Over the network. A request to a server the attacker controls. This is the channel an egress policy closes.
  • Through logs. Printing secrets, often encoded, into the job log. In March 2025, CISA reported that the compromised tj-actions/changed-files action exposed secrets this way.
  • Through outputs. Artifacts, caches or commits pushed with the job's token.

The code does not have to be a malicious package. An AI agent in the workflow can be turned by prompt injection hidden in an issue or pull request, and then use the same channels.

What an egress policy does

Most jobs need to reach a short list of destinations: package registries, container registries, source hosts and a few services. An egress policy allows that list and blocks everything else. With a default deny policy, a request to an unknown host fails, and the secrets stay in the job.

What happens when malware in a build tries to send your secrets out Left, a runner that allows all outbound traffic: the build reaches its package registry and publishes its output, then a malicious install hook sends the build's secrets to an attacker's server, and they arrive. Right, CRACI with a default-deny egress policy: the registry and publish connections are allowed, the connection to the attacker's server is blocked at the policy, and an alert is sent. The policy allows only what the build needs, so it works even against an attack nobody has seen before. Any outbound allowed build job holds your tokens and keys malicious install hook runs no egress policy package registry publish output attacker server allowed allowed secrets sent to the attacker CRACI default-deny egress policy build job on CRACI holds your tokens and keys malicious install hook runs egress policy: only what the build needs package registry publish output attacker server allowed allowed blocked blocked, and you get an alert
The policy allows only what the build needs, instead of looking for known-bad hosts, so it stops a new attack the first time it runs.

It does not close every door:

  • It does not stop secrets being printed to logs. Keep logs private and pin actions to commit SHAs.
  • A destination you allow can still carry data out, so keep the allowlist narrow.
  • It does not tell you which packages were malicious. That is the job of package screening and an SBOM.

Three ways to enforce one

  • An agent on the runner. StepSecurity Harden-Runner is added as the first step of a job. It maps each connection to the step that made it, builds a baseline in audit mode, and blocks egress to anything off an allowlist.
  • A runner with a policy built in. CRACI enforces the policy as the runner itself. Other runner providers offer network controls too, such as egress filtering on Depot's Linux runners and block or advisory egress policies on Namespace.
  • Your own network. Self-hosted runners behind a firewall or proxy you configure and maintain.

Rolling out without breaking builds

  1. Observe. Record every connection a job makes over a few runs.
  2. Allow. Build the allowlist from what the job actually needs.
  3. Block. Switch to default deny and watch for violations.
  4. Review. Treat every new violation as either a missing source or an incident.

How CRACI does it

On CRACI, the egress policy is part of the runner. It is validated before the job starts, unknown fields are rejected, and it fails closed. Built-in presets cover common package sources, and custom sources and connections add the rest. The job trace lists every connection, and each blocked one as a violation with its reason. Because the proxy is package-aware, the same record becomes the job's SBOM. The API returns SBOMs, network traces and provenance.

CRACI has no dedicated audit mode yet; it is on the roadmap. Without a network policy, a job's traffic is allowed and recorded in the trace, so you can see what a policy needs before you enforce it. CRACI runs GitHub Actions jobs on Linux. See CI/CD security tools compared and CRACI vs StepSecurity.

Egress policies in CI: frequently asked questions

What is an egress policy in CI?

A rule set for the outbound network connections a CI job may make. The job can reach the package registries and services it needs, and every other destination is blocked.

Would an egress policy have stopped the tj-actions compromise?

Not on its own. In that case CISA reported that secrets were exposed through workflow logs, not sent to an outside server. An egress policy closes the network channel; pinning actions to commit SHAs and keeping logs private address the log channel.

Does blocking egress break builds?

It can, if the policy misses a source the build needs. Start by recording what the job connects to, build the allowlist from that, then block. On CRACI, built-in presets cover common package sources, and a blocked connection shows in the job trace with the reason.

Can I use an egress policy on GitHub-hosted runners?

Yes, with an agent added to the job, such as StepSecurity Harden-Runner. Or move the job to a runner that enforces the policy itself, such as CRACI.

Put one job behind an egress policy

Run one workflow on CRACI and see every connection it made, which ones a default deny policy would block, and the SBOM of what it fetched.

Book a demo