Docker and builds

How Egress CI holds the job's containers, docker compose, docker build and image pulls to the same allowlist as the job.

On this page

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.uid with 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 container and 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:

.github/egress.yml
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.com

Example

.github/workflows/image.yml
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 tests

containers: track, lock-all or off

ValueUse it when
trackDefault. 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-allThe 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.
offDocker 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.sock itself instead of reading DOCKER_HOST: its containers are not attributed; with lock-all their traffic outside the policy is still refused.
  • Containers started with --network host are 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.

Last updated:

↑↓ to navigate↵ to openesc to close