On a self-hosted runner the action needs the same thing as on a GitHub-hosted one: root, to install the nftables lock. It gets it in one of two ways, tried in this order:
| How | What it needs | How the sidecar runs |
|---|---|---|
sudo | The job's user can run sudo without a password (sudo -n true succeeds) | As a root process on the host |
| Docker | The job's user can reach the Docker daemon (member of the docker group) | In a privileged container with the host network, from an empty local image (nothing is pulled), with the host's / mounted read-only |
With neither, the action still starts the proxy, but without the lock: only what goes through HTTPS_PROXY is reported and, in enforce, refused; tools that ignore the proxy variables and containers are not covered. Set lock: "on" to make the step fail instead. When root is available but the lock cannot be installed (no nft, for example), enforce fails the step.
Requirements
- Linux on x64 or arm64, with nftables (
nftin/usr/sbin,/sbinor/usr/bin) and a kernel that supports it, and iproute2 (ip). bash,curl,gzip,sha256sumandsudoordockeron the job'sPATH.- For Docker tracking: the daemon's socket and the
docker0bridge. - The runner's job user is not root (the lock matches that user's sockets; the action refuses to lock uid 0).
Example
jobs:
test:
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v5
- uses: openwaf-dev/egress-action@v0.3.2
with:
lock: "on"
containers: track
- run: make testShared hosts
Several runners can share one host if each runs as its own user:
- The lock is an nftables table per user (
egress_ci_<uid>) that matches only that user's sockets and the subnets of that job's Docker networks. Another runner user's jobs are not affected. - The sidecar's ports are fixed per user, below the ephemeral range; if they are taken, it moves on to the next free ones.
- Use
containers: track, neverlock-all:lock-allrefuses every container's traffic outside the current job's policy, including the other jobs'. - A runner that hands each job slot its own Docker socket in
DOCKER_HOSTkeeps working: the job's socket forwards to it.
Run each runner on a host as its own user: two runners of the same user on one host would share the lock table.
What root means here
The action needs root to lock the job, and the job has the same root: a step that runs sudo nft delete table … or uses the docker group to start a privileged container can remove the lock. On a self-hosted runner the docker group is equivalent to root. Egress CI makes exfiltration by a dependency or a tool visible and harder; it does not contain a step that sets out to escape it. See limits.
Ephemeral runners
On runners that start clean for each job (ephemeral or just-in-time runners, autoscaled VMs), lock-all is safe, and there is nothing to clean up between jobs. On persistent runners the post step (which runs even when a step fails) removes the lock, the sidecar and the job builder; it keeps the builder's BuildKit cache, up to 4 GB, for the next job of the same user.