View on GitHub

field-cage

A lightweight eBPF agent that monitors and restricts outbound network connections on GitHub Actions runners.

Usage

Policy file

mode: block          # optional: audit or block; defaults to audit when omitted
                     # (the --mode flag overrides this in either case)
allow_all_dns: false # optional; see "DNS handling" below (default false)

allowlist:
  - github.com
  - api.github.com
  - codeload.github.com
  - objects.githubusercontent.com
  - "*.githubusercontent.com" # wildcard: matches any subdomain, not the bare domain itself
  - 1.2.3.4             # single IPv4 address
  - 2001:db8::1         # single IPv6 address
  - 10.0.0.0/8          # IPv4 CIDR subnet (private range)
  - 203.0.113.0/24      # any /N prefix length is supported
  - 2001:db8::/32       # IPv6 CIDR subnet

Note: Wildcards must be of the form *.example.com and anchor at least a second-level domain — *.com / *.jp (a bare TLD) are rejected when the policy is loaded, as are any other shapes (e.g. api.*.com, *). A wildcard matches proper subdomains only: *.example.com does not also allow example.com — list that separately if it must be reachable too. Wildcard entries can’t be pre-resolved at startup (there’s no single FQDN to look up); unlike an exact-match domain, they have no startup-seeding fallback and are enforced only once a matching domain’s DNS response is observed on the wire — see “Block mode enforcement” below. This is a simple label-count check, not a public-suffix list, so *.co.jp or similar multi-label TLD-like suffixes are accepted even though they’re registrable-suffix-like.

Caution with wildcards on multi-tenant zones: a wildcard scoped to a domain where third parties can create their own subdomains (e.g. a generic *.s3.amazonaws.com, *.github.io, or *.pages.dev) lets anyone who controls a name under that zone get their IP added to the live allowlist — defeating the point of an allowlist. Scope wildcards to zones you or a trusted vendor fully control.

Strict keys: Unknown keys are rejected when the policy is loaded, so a misspelled key (e.g. mdoe:) fails fast instead of silently falling back to defaults.

CIDR: A CIDR entry seeds the eBPF LPM trie directly, so all addresses in the subnet are permitted without per-IP DNS resolution.

DNS handling: By default, port 53 is permitted only to the resolvers configured in /etc/resolv.conf plus loopback — this prevents a port-53 listener on an arbitrary host from being used as a general outbound tunnel. Set allow_all_dns: true to permit port 53 to any destination (the pre-0.x behavior). Even under the default, DNS tunneling through a legitimate resolver (data encoded in subdomains, resolved recursively) is not blocked; see Limitations below.

Standalone binary

Install

curl -fsSL https://takihito.github.io/field-cage/install.sh | sh

Installs to ~/.local/bin by default (no sudo required for installation). To change the install directory:

curl -fsSL https://takihito.github.io/field-cage/install.sh | sudo env FIELD_CAGE_INSTALL_DIR=/usr/local/bin sh

Requires Linux (amd64 or arm64) — field-cage depends on eBPF and has no macOS or Windows build. For pre-built binaries or manual downloads, see the Releases page.

Run

# Audit mode — log all connections, no policy file required
sudo field-cage

# Audit mode with a policy file
sudo field-cage --config policy.yml

# Block mode — default-deny; only allowlisted destinations are permitted.
# A policy file is required (block mode without one would deny all traffic).
sudo field-cage --config policy.yml --mode block

# Print version
field-cage --version

sudo resets PATH on some systems, so if sudo field-cage reports “command not found”, use the full path (e.g. sudo ~/.local/bin/field-cage) or run sudo with -E/--preserve-env=PATH.

Block mode enforcement model

Block mode is default-deny: the cgroup/connect4 and cgroup/connect6 programs reject every outbound IPv4/IPv6 connection unless its destination IP is on the allowlist. UDP is covered by the same policy on both paths a datagram socket can take: cgroup/connect4/connect6 handle a socket that calls connect(), and cgroup/sendmsg4/sendmsg6 handle one that calls sendto()/sendmsg() directly without ever connecting — a separate kernel attach point that connect4/connect6 alone cannot see, so both must be enforced or unconnected UDP would silently bypass the allowlist. IPv4-mapped IPv6 destinations (::ffff:a.b.c.d, the path dual-stack runtimes such as Node.js and Java take to IPv4 hosts) are checked against the IPv4 allowlist, so one IPv4 entry covers both socket families. The allowlist is built by:

  1. Startup seeding — explicit IP and CIDR entries are added directly, and each allowlisted domain is resolved (A and AAAA) and its addresses added.
  2. Live DNS observation — when a DNS response for an allowlisted domain is seen on the wire, its A/AAAA-record IPs are added to the allowlist before the application connects. Only responses originating from a configured resolver (the nameserver entries in /etc/resolv.conf) or from loopback are trusted for this; responses from any other source are cached for logging but never extend the kernel allowlist, so a forged response with a spoofed source port 53 cannot poison it.

Loopback (127.0.0.0/8 and ::1) is always permitted so that local services keep working. DNS (destination port 53) is permitted only to the system’s resolvers (plus loopback) so that name resolution works without turning port 53 into a general outbound tunnel to arbitrary hosts; set allow_all_dns: true in the policy to restore unconditional port-53 access. Resolvers are discovered from /etc/resolv.conf; when it lists only a loopback stub (systemd-resolved’s 127.0.0.53), the stub’s upstream servers from /run/systemd/resolve/resolv.conf are permitted as well — enforcement applies to the stub daemon’s own outbound queries too, so its upstreams must be reachable. If no resolvers can be determined at startup, only loopback DNS is permitted (fail-closed). Denied port-53 connections are reported as DENY, not SKIP(dns). A policy file is required in block mode; without one the agent refuses to start rather than deny all traffic.

Limitations