With containers: track (the default) and root reachable through sudo or Docker, the job's Docker goes through a socket the sidecar serves. The action sets DOCKER_HOST to it, so docker, docker compose, Testcontainers and any other client that reads DOCKER_HOST use it without changes.
What the job's socket does
- Labels. Containers, networks and volumes created through it get the label
org.openwaf.egress-ci.uidwith the job user's id. - A network for the job. The sidecar creates a bridge network for the job. A container that asks for Docker's default bridge gets that network instead; a container that names its own network (Compose does) keeps it.
- Covered from the first packet. Each network created through the socket enters the lock by subnet as soon as it exists, before any container joins it. A container's DNS goes to the sidecar's forwarder and its TCP to the transparent proxy, exactly like the job's own; the rest is refused in enforce. Traffic between the job's containers on those networks is not restricted.
- Attributed. The summary's Via column says
containerand names the container. - Daemon unchanged. Every call is forwarded to the daemon the runner gave the job (its
DOCKER_HOST, or/var/run/docker.sock).
docker build
The action creates a job builder (BuildKit in a container, docker-container driver) through the job's socket and sets BUILDX_BUILDER to it. docker build and docker buildx build use it and load the result into the daemon, so docker run of the built image works as before.
Because the builder is one of the job's containers, every RUN step is held to the policy: a RUN curl to a name outside it is refused in enforce. The daemon's own builder runs build steps in sandboxes no job can be told apart in, which is why the action does not use it.
The builder's BuildKit state (cache of base images and layers) is kept for the next job of the same runner user, pruned to 4 GB. On GitHub-hosted runners each job gets a new machine, so it starts empty every time.
The builder's image, moby/buildkit, comes from Docker Hub on the first build: Docker Hub must be allowed (below).
Image pulls
A pull through the job's socket (docker pull, docker run of a missing image, Compose) is checked by registry before the daemon fetches anything. ghcr.io/acme/app:1 is checked against ghcr.io; a Docker Hub image (alpine:3, docker.io/library/alpine) against registry-1.docker.io. A registry outside allow is refused in enforce with 403 and the reason registry_not_allowed. The summary lists every pull with its decision.
docker pull and docker run fetch layers through the daemon, whose own connections are not proxied: for them only the registry check applies. The job builder pulls base images itself, from inside the lock, so its downloads must be allowed too. For Docker Hub, allow:
allow:
- auth.docker.io # tokens
- registry-1.docker.io # registry (every docker.io pull is checked against this name)
- production.cloudflare.docker.com # layers
- production.cloudfront.docker.comExample
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v5
- uses: openwaf-dev/egress-action@v0.3.2
with:
containers: lock-all
- run: docker build -t app:ci .
- run: docker compose up --abort-on-container-exit --exit-code-from testscontainers: track, lock-all or off
| Value | Use it when |
|---|---|
track | Default. The job's own containers follow its policy. Containers of other jobs on the same host are only observed (listed under «flows from containers that are not this job's»). |
lock-all | The runner runs one job at a time: GitHub-hosted runners, ephemeral self-hosted runners. In enforce it also refuses every other container's traffic outside the policy, which covers Docker used without DOCKER_HOST. Never on a host that runs several jobs at once: it would refuse the other jobs' containers. |
off | Docker is left untouched: no socket, no builder, no container coverage. |
Docker tracking needs root (the lock), the daemon's socket and the docker0 bridge. Without them the action runs with Docker untouched and the summary says Docker off; in enforce, a socket that fails to start fails the step.
What is not covered
- A tool that opens
/var/run/docker.sockitself instead of readingDOCKER_HOST: its containers are not attributed; withlock-alltheir traffic outside the policy is still refused. - Containers started with
--network hostare outside the lock unless they run as the job's user. - The daemon's own connections (pushes, and the downloads of a pull once its registry is allowed) are not proxied.
- Destinations in Docker's address pool (
172.16.0.0/12) are left to the runner's rules.