Egress CI makes a job's outbound traffic visible and limits it to a list. It raises the cost of exfiltration by a compromised dependency, build script or action, and shows the attempt in the summary. It is not a sandbox: read what it does not cover before relying on it.
Coverage
| Traffic | Covered | How |
|---|---|---|
Tools that honour HTTPS_PROXY/HTTP_PROXY | Yes | The proxy: name and SNI checked |
| Tools that ignore the proxy variables, the runner itself | With the lock | The transparent proxy: named by SNI or Host, name checked against the address |
| DNS | With the lock | The forwarder: logged, NXDOMAIN for names outside the policy in enforce |
| UDP, ICMP, IPv6 | With the lock | Only to addresses of allowed names (IPv4); the rest refused in enforce |
The job's containers and docker build steps | With Docker tracking | Same as the job, per container |
| Image pulls through the job's socket | With Docker tracking | Registry checked |
| Steps before the action | No |
Known limits
- Root can undo it. The job has the root the action uses: on GitHub-hosted runners every job has
sudo, and on self-hosted runners the docker group is equivalent to root. A step that sets out to remove the lock can. - Only the steps after the action. Put it right after
actions/checkout. - HTTPS content is not seen unless the host is under
inspect, and inspection needs the tool to trust the run's CA. A secret sent over HTTPS to an allowed host is not detected. - Allowed hosts can carry data. A package registry or an object store you allow can receive an upload. Allow exact names, and keep write credentials out of jobs that do not need them.
- Docker without
DOCKER_HOST. A tool that opens/var/run/docker.sockitself is not attributed; withlock-allits containers' traffic outside the policy is still refused. Containers with--network hostare outside the lock unless they run as the job's user. - The Docker daemon's own connections (pushes, layer downloads of a pull once the registry is allowed) are not proxied.
- Docker's address pool (
172.16.0.0/12) is left to the runner's own rules, and is inNO_PROXY. - IPv4 only for naming TCP connections; the job's IPv6 is refused in enforce.
- Without root (a self-hosted runner with neither passwordless
sudonor Docker), onlyHTTPS_PROXYis covered.lock: "on"makes that an error. - Linux only, x64 and arm64. No macOS or Windows runners.
- One job per runner user at a time on a host.
What it costs
The action starts the sidecar before the job's own steps and stops it in the post step; the log line OpenWAF Egress CI: proxy …, started in … ms and the summary give the times of each run. Connections through the proxies add one local hop. With Docker tracking, docker build runs in the job builder instead of the daemon's builder; on runners that start clean for each job its cache starts empty.
Security model
- The sidecar runs as root and the job's steps as the runner user; the lock matches that user's sockets and the job's Docker networks only.
- The binaries are in the action's
dist/, built reproducibly from its source (CI checks it); their SHA-256 are indist/SHA256SUMSand checked before every run. - Tokens are never put on a command line or in a log: the GitHub token reaches
curlon standard input, the OIDC tokens stay in the post step's memory. - The run's CA key never leaves the sidecar's memory.
- The summary and
report.jsonhold names, ports, counts and detection ids, never bodies or the secrets found.
Report vulnerabilities to security@openwaf.dev, as described in the repository's SECURITY.md (external link).