Self-hosted runners

Run Egress CI on self-hosted Linux runners, with sudo or Docker, on single-job and shared hosts.

On this page

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:

HowWhat it needsHow the sidecar runs
sudoThe job's user can run sudo without a password (sudo -n true succeeds)As a root process on the host
DockerThe 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 (nft in /usr/sbin, /sbin or /usr/bin) and a kernel that supports it, and iproute2 (ip).
  • bash, curl, gzip, sha256sum and sudo or docker on the job's PATH.
  • For Docker tracking: the daemon's socket and the docker0 bridge.
  • The runner's job user is not root (the lock matches that user's sockets; the action refuses to lock uid 0).

Example

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

Shared 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, never lock-all: lock-all refuses 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_HOST keeps 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.

Last updated:

↑↓ to navigate↵ to openesc to close