# Self-hosted runners

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

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 (`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

```yaml
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](https://developers.openwaf.dev/egress/ci/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.
