egress.yml
The policy is a YAML file in the repository, .github/egress.yml unless the policy input names another one.
version: 1
mode: enforce
presets: [github-actions]
allow:
- proxy.golang.org
- "*.npmjs.org"
- registry.example.com:5000
- ci-cache.example.com:*
inspect:
- api.staging.example.com| Key | Default | Meaning |
|---|---|---|
version | 1 | Format version. Only 1 exists. |
mode | audit | audit: everything passes and what enforce would refuse is reported. enforce: what is outside allow is refused. The mode input overrides it. |
presets | none | Named lists maintained with the action. The only one is github-actions. |
allow | none | Destinations the job and its containers may reach. |
inspect | none | Hosts whose TLS the proxy terminates to scan request bodies for secrets. See Inspection and DLP. |
Unknown keys are an error: a typo such as alow: fails the action's step instead of silently allowing nothing. The action checks the file before it starts anything.
Allow rules
| Rule | Matches |
|---|---|
proxy.golang.org | That name, ports 80 and 443 |
*.npmjs.org | Any name below npmjs.org (registry.npmjs.org, a.b.npmjs.org), not npmjs.org itself; ports 80 and 443 |
registry.example.com:5000 | That name, port 5000 only |
registry.example.com:80,5000 | That name, ports 80 and 5000 |
ci-cache.example.com:* | That name, any port |
203.0.113.10 | That IPv4 address, ports 80 and 443 |
Names are case-insensitive and a trailing dot is ignored. A wildcard is only valid as the first label (*.example.com).
DNS answers for an allowed name are given to the job; the lock lets non-TCP flows (UDP such as QUIC) through only to the addresses those answers contained. Image pulls are checked against the registry's name on any port.
The github-actions preset
github-actions is the list GitHub publishes for Actions runners in the domains.actions field of api.github.com/meta (external link): the runner's service, checkout, the API, the Actions cache, artifacts and logs, with each Azure storage account by its exact name (never *.blob.core.windows.net, where anyone can create an account). The action adds raw.githubusercontent.com, where setup-* actions read their version manifests.
The action downloads the list at start with the token input and keeps it for 6 hours in the runner user's ~/.cache/egress-ci/. If the download fails, it uses a copy built into the action. The job summary says which one was used.
Inputs
| Input | Default | Meaning |
|---|---|---|
policy | .github/egress.yml | Path of the policy file. |
mode | empty | audit or enforce; overrides mode in the file. Useful to try one file in audit on pull requests and enforce it on main. |
lock | auto | The nftables lock for connections that ignore the proxy. auto: on when the job reaches root through sudo or Docker, off otherwise. on: like auto, but the step fails when root is not reachable. off: only HTTPS_PROXY is used. In enforce, a lock that cannot be installed fails the step. |
containers | track | track: the job's Docker goes through its own socket, and its containers and builds are held to the policy. lock-all: also refuse, in enforce, every other container's traffic outside the policy (runners that run one job at a time). off: Docker is left as it is. See Docker and builds. |
no-proxy | empty | Extra comma-separated NO_PROXY entries, for hosts proxy-aware tools should reach directly. With the lock on, those connections still go through the transparent proxy and the policy. |
token | ${{ github.token }} | Token used to read api.github.com/meta. It is passed to curl on standard input, never on the command line. |
send-events | off | on: send the job's destinations, blocks and detections to your organization's Egress events. See Send events to OpenWAF. |
openwaf-api | https://dash.openwaf.dev | OpenWAF API used by send-events. Must be https://. |
| Output | Meaning |
|---|---|
proxy | The proxy URL, http://127.0.0.1:PORT. |
What the action sets for the job
| Variable | Value |
|---|---|
HTTPS_PROXY, HTTP_PROXY, https_proxy, http_proxy | The proxy URL. |
NO_PROXY, no_proxy | localhost,127.0.0.1,::1,172.16.0.0/12,.local,.localhost,.test, your no-proxy, and .blob.core.windows.net when the lock is on (the Actions cache then goes through the transparent proxy, still limited to GitHub's exact storage accounts). |
EGRESS_CI_STATE | The sidecar's state directory, $RUNNER_TEMP/egress-ci (the event log is events.jsonl there). |
DOCKER_HOST, BUILDX_BUILDER | With Docker tracking: the job's Docker socket and the job builder. |
NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, GIT_SSL_CAINFO | With inspect: the run's CA, alone or added to the system bundle. |
The sidecar listens on 127.0.0.1 (and on the Docker bridge address for containers) on three ports below the ephemeral range, fixed per runner user: the proxy, the DNS forwarder (next port) and the transparent proxy (the one after).
Decisions and reasons
Every connection, DNS query and image pull gets a decision: allow, audit (outside the policy, let through because the mode is audit) or block. A refused one has a reason:
| Reason | Why |
|---|---|
not_allowed | The name, or the port for that name, is not in allow. |
sni_mismatch | A CONNECT to an allowed name carried a TLS SNI for a name outside the policy. |
name_ip_mismatch | A connection named an allowed host but went to an address that name does not resolve to (domain fronting). |
dlp | The request carried a secret (see Inspection and DLP). |
registry_not_allowed | An image pull from a registry outside the policy. |
private_destination | Loopback, link-local (such as the cloud metadata address) or multicast, refused in both modes. |
In enforce the proxy answers 403 with the header X-OpenWAF-Egress: blocked (or dlp), DNS answers NXDOMAIN, the Docker socket answers 403, and other connections are reset.
The report
The post step appends the report to the job summary and prints report.json in a log group named egress-ci report.json. It holds names, ports, counts and detection ids, never request bodies.
| Field of report.json | Meaning |
|---|---|
meta | Version, mode, policy file and its SHA-256, lock and Docker state, where the GitHub list came from, start and stop times, the job's containers. |
destinations | One entry per host: ports, via, containers, connections, bytes_up, bytes_down, allowed, blocked, audited, dns_refused, reasons, detections. |
blocked, would_block, dns_refused, detections, dns_queries | Totals. |
suggested_allow | Names outside the policy, ready for allow. |
image_pulls | Pulls through the job's socket, with their decision. |
other_containers | Container flows on the runner that are not this job's. |
unattributed_addresses | Non-TCP flows to addresses no DNS answer named. |