GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - buildcage/docker: GitHub Action to build Docker images with outbound network access restricted to an allowlist

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace
BranchesTags
Open more actions menu

Latest commit

 

History

2,800 Commits
2,800 Commits

Folders and files

NameName
Last commit message
Last commit date
.github
.github
 
 
.vite-hooks
.vite-hooks
 
 
assets
assets
 
 
dist
dist
 
 
docker
docker
 
 
docs
docs
 
 
licenses
licenses
 
 
report
report
 
 
src
src
 
 
test
test
 
 
.dockerignore
.dockerignore
 
 
.gitattributes
.gitattributes
 
 
.gitignore
.gitignore
 
 
.glf.jsonc
.glf.jsonc
 
 
.trivyignore
.trivyignore
 
 
LICENSE
LICENSE
 
 
MAINTAINERS.md
MAINTAINERS.md
 
 
Makefile
Makefile
 
 
README.md
README.md
 
 
SECURITY.md
SECURITY.md
 
 
THIRD_PARTY_LICENSES
THIRD_PARTY_LICENSES
 
 
THIRD_PARTY_LICENSES_NPM
THIRD_PARTY_LICENSES_NPM
 
 
action.yml
action.yml
 
 
compose.yaml
compose.yaml
 
 
osv-scanner.toml
osv-scanner.toml
 
 
package.json
package.json
 
 
pnpm-lock.yaml
pnpm-lock.yaml
 
 
pnpm-workspace.yaml
pnpm-workspace.yaml
 
 
renovate.json
renovate.json
 
 
rolldown.config.js
rolldown.config.js
 
 
rolldown.scripts.config.js
rolldown.scripts.config.js
 
 
tsconfig.json
tsconfig.json
 
 
tsconfig.qjs.json
tsconfig.qjs.json
 
 
vite.config.ts
vite.config.ts
 
 

Repository files navigation

buildcage/docker

Buildcage

GitHub Marketplace version build test license

A docker build runs your RUN steps, and every dependency they fetch, with unrestricted network access. Buildcage puts that behind an allowlist: each RUN step can reach only the destinations you name, and anything else is blocked and reported.

  • Your Dockerfile doesn't change, BuildKit isn't patched, and the CA and variables Buildcage adds to a step are taken back out before its layer is committed, apart from the exceptions in CA trust and compatibility.
  • Run once in audit mode and the report writes the allowlist for you, ready to review and paste back into the workflow.
  • A rule can name an HTTP method and a URL, inside TLS as well, so a build can fetch a package from a registry without being able to publish one to it.
  • It all runs inside your GitHub Actions job, with no agent to install and no hosted service in the build's traffic path.

See buildcage.github.io for what it does and why. To isolate a workflow run: step rather than a Docker build, use buildcage/isolated-run.

Contents

  • Requirements
  • Usage
  • Engines
  • Inputs
  • Report action
  • How it works
  • CA trust and compatibility
  • Scope
  • Limitations
  • FAQ
  • GitHub's native egress firewall
  • Documentation

Requirements

The builder is a container on the runner itself, so this action needs a Linux runner with a working Docker installation:

  • GitHub-hosted
    • ubuntu-latest, the versioned ubuntu-* images, and their -arm variants
    • Lightweight images such as ubuntu-slim are not supported: they ship a Docker client with no daemon
  • Self-hosted
    • Docker Engine 25.0 or later, with Compose v2.20.2 or later
    • A host using cgroup v2
    • For the inspect engine, a Docker data root that can hold an overlayfs upper directory: not itself on overlayfs (Docker-in-Docker needs a volume for /var/lib/docker) and not on XFS formatted with ftype=0

A runner that falls short fails while the builder starts, before any RUN step runs.

Usage

Buildcage starts a BuildKit builder in your job. Point Docker Buildx at it as a remote driver and build as usual. Run once in audit mode to collect what the build reaches, then switch to restrict. The examples below use the default inspect engine; Engines covers the choice of engine.

1. Find out what the build reaches

- name: Start Buildcage in audit mode
  uses: buildcage/docker@1a8142a6fe143c76982405411cf41652cb247d54 # v4.4.3
  with:
    proxy-mode: audit # Log every destination, enforce no rules

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
  with:
    driver: remote
    endpoint: docker-container://buildcage

- name: Build
  uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
  with:
    context: .

- name: Show Buildcage report
  if: always()
  uses: buildcage/docker/report@1a8142a6fe143c76982405411cf41652cb247d54 # v4.4.3

The report action writes every destination the build contacted to the Job Summary:

Outbound Traffic Report (audit mode)

Its Switch to restrict mode section holds the allowlist, already written out from what the build actually did. A request whose method, host or path contains rule syntax, such as *, is listed below the allowlist instead of being added to it, since a rule copied from it would permit more than was sent.

2. Enforce the allowlist

Paste that allowlist into the setup step and switch the mode:

- name: Start Buildcage in restrict mode
  uses: buildcage/docker@1a8142a6fe143c76982405411cf41652cb247d54 # v4.4.3
  with:
    proxy-mode: restrict
    allowed-url-rules: |
      GET http://deb.debian.org/**
      GET https://registry.npmjs.org/**
      POST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk

Each rule names the methods it permits, so this one lets npm fetch packages without letting it publish any: a POST to the same host is blocked, as is every host not listed.

Outbound Traffic Report (restrict mode)

A blocked connection fails the job at the report step, so a build that starts reaching somewhere new doesn't pass unnoticed.

Example workflows

Each pair runs a build in audit, then in restrict with the rules the audit reported, plus one request those rules block so the report shows a block:

  • inspect on an apt and npm build: audit · restrict
  • universal on a Maven build: audit · restrict

Engines

proxy-engine sets how much of each connection Buildcage reads, and so what a rule can match. inspect is the default; universal has to be set explicitly.

inspect terminates TLS and re-signs it with a CA generated for each builder container. Rules match on method and URL, so GET|HEAD https://registry.npmjs.org/** allows a fetch while blocking a publish on the same host, and the report names every request with its URL. The build has to trust that CA: Buildcage adds it to the system store, to the CA-trust variables, to a JVM already in the base image, and to Chromium's NSS database, so most toolchains need nothing extra (see CA trust and compatibility).

universal reads only the SNI. Rules match on host and port only, such as registry.npmjs.org:443. The report shows host and port, and the build's own TLS is left untouched.

Start with inspect. When a tool pins a certificate or ships a trust store Buildcage cannot inject into, its host cannot be re-signed: switch the whole build to universal, or pass that host alone through with allowed-tls-rules.

Every engine intercepts at the network level, so a tool that ignores HTTP_PROXY is covered too. Only traffic from RUN steps is intercepted. What buildkitd fetches for itself is not: see Limitations.

Inputs

Every input is optional. The sections below cover the rule inputs you write by hand. The full list, with defaults and the engines each input applies to, is in Reference, and the grammar those rules are written in is in Rule syntax.

The builder is named buildcage unless builder-name says otherwise, and the Buildx endpoint has to match whatever it is named. The setup action's builder-name output holds the name it used.

The inputs can also live in a YAML file next to the Dockerfile, named by config-file. See Config file.

A # starts a comment at the start of a line or after whitespace, so a group of rules can carry a heading or a rule can carry its reason. It works the same in every rule input.

Operation modes

proxy-mode: audit logs every destination the build reaches and enforces none of the rules. It still blocks what no rule could allow, such as a malformed request or a name that resolves to an internal address like cloud metadata or the runner itself, and under inspect an origin whose certificate fails verification. restrict, the default, allows only what the rules match and blocks and logs everything else. Start with audit when you first adopt Buildcage or when a dependency changes, and keep restrict for everyday builds.

If you forget a domain the build needs, restrict blocks it and the report step fails with the destination named, which is why it is worth running audit first.

audit also lets through a connection made straight to an address. Under universal that includes cloud metadata (169.254.169.254) and the runner's own addresses; inspect still blocks those unless a rule names the address as its host.

Rules for the inspect engine

allowed-url-rules is the one to reach for. Each line is a method list, a space, and a URL pattern. * stays inside one domain label or path segment, ** crosses dots and slashes, and a rule with no path allows any path on that host:

allowed-url-rules: |
  # apt
  GET http://deb.debian.org/**

  # npm: fetch packages, and the audit endpoint it posts to
  GET https://registry.npmjs.org/**
  POST https://registry.npmjs.org/-/npm/v1/security/advisories/bulk

  # pip: one registry, two domains
  GET https://pypi.org/simple/**
  GET https://files.pythonhosted.org/packages/**

  # a private registry is an ordinary host
  GET|HEAD https://registry.internal.example.com:8443/**

allowed-tls-rules passes a TLS destination through undecrypted, judged on its SNI and port. It is for TLS that isn't HTTPS, and for hosts whose clients cannot accept the inspect CA, such as a tool that pins its certificate:

allowed-tls-rules: |
  db.example.com:5432
  repo.maven.apache.org:443

allowed-ip-rules covers connections made straight to an address, which never go through DNS. A rule may be an address, a CIDR block or a wildcard, on every engine:

allowed-ip-rules: |
  192.168.1.10:443
  10.0.0.0/8:443

Rules for the universal engine

universal never decrypts, so rules name a host and a port. allowed-https-rules and allowed-http-rules split by scheme, and allowed-ip-rules works as it does under inspect:

allowed-https-rules: |
  # npm and maven
  registry.npmjs.org:443
  repo.maven.apache.org:443
  *.internal.example.com:443  # every host one level under internal.example.com

allowed-http-rules: |
  deb.debian.org:80

allowed-ip-rules: |
  192.168.1.10:443

Destinations you expect to stay blocked

A noisy dependency, or a domain you are deliberately keeping off the allowlist to confirm it stays blocked, belongs in known-blocked-rules. Their rows move to the report's collapsed Known Blocked Hosts and do not fail the job; the destination stays unreachable:

known-blocked-rules: |
  telemetry.example.com

A name that was only looked up, never connected to, has no port. A bare telemetry.example.com (or telemetry.example.com:*) covers it; telemetry.example.com:443 does not, since no port was involved.

One rule per line, and on inspect a line can also be a URL rule (a method and a URL, the allowed-url-rules syntax) to acknowledge a single endpoint on a host whose other traffic is allowed, such as a telemetry POST to an API you otherwise use:

known-blocked-rules: |
  POST https://api.example.com/telemetry

Any other blocked request to that host still fails the step. See Blocked rules for the full syntax.

Report action

buildcage/docker/report reads the builder's communication log and writes the Job Summary. Add it with if: always() so a failing build still reports:

- name: Show Buildcage report
  if: always()
  uses: buildcage/docker/report@1a8142a6fe143c76982405411cf41652cb247d54 # v4.4.3

When anything needs attention, an alert under the report's title gives the counts, such as how many connections were blocked or failed. The report then lists, in this order:

  • what was blocked unexpectedly, and any connection that failed, each with its reason
  • the allowed hosts
  • what known-blocked-rules matched, collapsed under Known Blocked Hosts
  • in audit mode, the Switch to restrict mode allowlist
  • under inspect, Communication details: the URL of every request, allowed or blocked, with credential query parameters replaced (see Credentials in a URL)

How the alert and the tables are made up is in Report details.

In restrict mode the step fails when a blocked connection is found, and with it the workflow. Pass fail-on-blocked: false to report without failing, or list what you expect to stay blocked in the setup action's known-blocked-rules. In audit mode a blocked connection or an incomplete report does not fail the step. A proxy or resolver that stopped answering, or a log the report could not fetch, fails it in either mode. The action's inputs are in Reference.

Traffic artifact

upload-traffic-artifact: true uploads the whole timeline as a traffic.json, one row per request and per name lookup, with the method, URL, status, size and the address it resolved to. It is uploaded even when the build fails. Under universal it omits the method, URL, status and resolved address that only inspect sees. The fields are listed in Reference.

How it works

How Buildcage restricts what a build can reach

BuildKit itself is unpatched. Buildcage starts it with a network configuration that puts every RUN step on its own network, and wraps its runtime so the build CA is mounted into a step for as long as that step runs. Name lookups and traffic from there reach the resolver and the proxy in the builder container, at the network level, so a tool that ignores the proxy environment variables is covered as well. The figure is the inspect engine; universal follows the same path without terminating TLS, and so needs no CA.

Buildx needs driver: remote because the builder is a second BuildKit. Multi-stage builds, caching and the image that comes out are unaffected, and nothing Buildcage does changes the build definition BuildKit runs (its LLB) or a cache key.

Security Details has the architecture of each engine, with a diagram of what runs where and what it decides.

CA trust and compatibility

proxy-engine: inspect terminates TLS and re-signs it with a CA generated for each builder container, whose private key never leaves that container (details), so the build has to trust that CA. As each RUN step starts, Buildcage adds it where the common toolchains look:

  • The system CA store, including the distribution's anchor directory, so a step that installs ca-certificates partway through keeps trusting it.
  • The variables Node, Deno, OpenSSL, Python requests and pip read: NODE_EXTRA_CA_CERTS, DENO_CERT, SSL_CERT_FILE, REQUESTS_CA_BUNDLE and PIP_CERT, and CURL_CA_BUNDLE in a step with no system CA store. If the base image or the Dockerfile already sets one of them, the CA is appended to the file it names instead of the variable being pointed elsewhere. GIT_SSL_CAINFO, npm_config_cafile, AWS_CA_BUNDLE, CARGO_HTTP_CAINFO and BUNDLE_SSL_CA_CERT are handled only that way: appended to when already set, and otherwise left unset.
  • The keystore of a JVM already in the base image, at $JAVA_HOME and behind the first java on PATH, so mvn, gradle and java reach the proxy without proxy-engine: universal.
  • The NSS database in $HOME that Chromium reads, including the chrome-headless-shell that Puppeteer, Playwright and Remotion download.

All of it is taken back out before the layer is committed, so neither the CA nor the variables are left in the image. Two kinds of copy are the exception: one inside something Buildcage cannot read, such as a compressed archive, and one it found but could not remove, which fail-on-ca-residue: false lets through with a warning. How each one is done is in CA trust variables, and what this cannot cover is in Limitations, below.

Scope

Buildcage controls where your build can connect, not what code it runs. A malicious package delivered through an allowed domain still runs. Treat it as one layer among your other measures: if something gets past them, it still cannot reach a destination outside the allowlist.

An allowlist also cannot stop anything leaving through a service you had to allow anyway. It stops traffic to destinations that are not on the list, such as infrastructure an attacker set up; see Threat model.

An allowlist generated from an audit run already blocks every host the audit did not record, apart from what the allowed-tls-rules and allowed-ip-rules it carries over from the audit run's own inputs admit. Whether to go further depends on what the build has access to: Hardening is what to look at when it holds credentials, personal data, or source you do not publish. For the full threat model, see Security Details.

Limitations

What isn't covered

  • buildkitd's own traffic is outside the allowlist. FROM, ADD <url>, git contexts and the frontend image a # syntax= directive names are fetched by buildkitd itself rather than by a RUN step, and no rule applies to them. See What buildkitd fetches itself.
  • Traffic an allowed-tls-rules entry passes is not decrypted. The SNI and the port are checked, and the proxy resolves that name itself, so the connection reaches the host the rule named, but nothing inside the TLS session is seen.
  • Traffic an allowed-ip-rules entry passes is not inspected at all, and doesn't require TLS either: once an ip:port pair is allowed, any TCP-based protocol can use that path. Prefer a domain rule wherever the destination has a stable name.
  • universal never sees the method or the path. They travel inside TLS, so neither is enforced and neither reaches the report or the traffic artifact. A request fronted behind an allowed SNI is invisible to it as well, while inspect matches on the real Host and blocks it. See Domain fronting.
  • An allowlisted name that resolves to cloud metadata, to loopback, or to an address the runner itself holds is blocked, so a compromised name cannot turn the proxy into a route back into the runner. A mirror or registry running on the runner is therefore not reachable by name: allow it with allowed-ip-rules, which skips that guard except for the proxy's own port. See A name may not resolve inward.
  • Under inspect, the generated allowlist writes URL rules only from the requests the proxy read. allowed-tls-rules and allowed-ip-rules are copied over unchanged from the audit run's own inputs, since the traffic they pass through is never decrypted.
  • Under universal, the generated allowlist writes a host rule for the SNI or Host of each connection made through a name, and an IP rule for each address reached directly.
  • Buildcage produces no SLSA provenance. The traffic artifact is an observation record with no content digest.

Protocols

  • UDP is dropped, so QUIC and HTTP/3 either fall back to TCP or fail. The one exception is port 53 on the gateway, which is the proxy's own address and where the resolver listens. ICMP is dropped too.
  • IPv6 is not used anywhere. The rule syntax refuses an IPv6 address, forwarded IPv6 is dropped, and the proxy reaches allowed names over IPv4 only, so an allowed name with AAAA records and no A record never resolves and no rule can clear it.

Service discovery

The resolver has no upstream, so it returns nothing for a discovery record: SRV, TXT, TLSA and URI queries come back empty, and the build connects to the name a rule allowed rather than to one a nameserver picked for it. A client that treats SRV as an optional discovery layer falls back to the host name itself, so the host a rule names is the host the build reaches.

What this breaks is a client with no fallback, where the record is the only way it can find the service at all. A mongodb+srv:// connection string is the one to expect: use mongodb:// with the shard hostnames written out and allowlist those instead. Active Directory and Kerberos discovery have the same shape.

Under inspect, a lookup for a _service._proto.<host> name is reported as discovery when the rules allow that host, and is not counted as blocked. A service name under any other host is reported as blocked; see Blocked service names.

Under the inspect engine

  • A tool that pins a specific certificate, or ships a bundled trust store it never lets the system update, still needs proxy-engine: universal or an allowed-tls-rules passthrough: inspect re-signs the connection, and a pinned or bundled store will not accept the new certificate.

  • A client that insists on HTTP/2 through ALPN, as gRPC clients do (grpc-go since 1.67, for one), fails to connect: inspect answers no ALPN. Pass its host through with allowed-tls-rules, which judges the SNI and port only. A client that falls back to HTTP/1.1, such as curl, works.

  • Only two JDKs get the CA in their keystore: the one at $JAVA_HOME and the one behind the first java on PATH. Even those are left alone when the keystore is sealed with a password other than the JDK default; such a JDK needs proxy-engine: universal or an allowed-tls-rules passthrough. These JDKs do not trust the CA: one further along PATH, a Gradle toolchain under ~/.gradle/jdks, or one the step itself downloads (an sdk install, an unpacked tarball). Fetch such a JDK in an earlier RUN step, then use ENV to put its bin first on PATH or point JAVA_HOME at it:

    RUN tar xzf jdk-21.tar.gz -C /opt/java && /opt/java/jdk-21/bin/java -jar fetch.jar   # fails
    RUN tar xzf jdk-21.tar.gz -C /opt/java
    ENV PATH=/opt/java/jdk-21/bin:$PATH
    RUN java -jar fetch.jar                                                              # fine
  • Chromium trusts the CA only through the NSS database under the HOME the step starts with. Chromium started with another HOME (HOME=/tmp chromium) or as another user (su, gosu, sudo -u) does not trust it: switch users with USER and leave HOME alone within the step. A Chromium before M146 reads only ~/.pki/nssdb. Buildcage adds the CA by giving a copy of the database's pkcs11.txt an extra entry that points at a store holding only the CA. A database the step's user cannot write gets no such entry, and the step warns; use proxy-engine: universal for Chromium there. See Chromium.

  • A Firefox carrying Mozilla's own root list, such as Playwright's or the one Selenium drives, reads a per-profile database and does not trust the CA; use proxy-engine: universal or an allowed-tls-rules passthrough for it.

  • Every RUN step has the NSS database's directory as a mount point, created if the home has none, so rm -rf ~/.pki, git clone <url> . into the home and rm -rf ~/.[!.]* fail; use a WORKDIR outside the home. A step that changes the CA's own trust with certutil -M fails the build.

  • A RUN step that copies the system CA bundle into a binary or an uncompressed archive (go:embed, include_str!, tar cf) fails: the copy carries the build's CA, which cannot be cut out of a binary without corrupting it. Copy the bundle in an earlier RUN step instead:

    RUN cp /etc/ssl/certs/ca-certificates.crt ./certs/ && go build   # fails
    RUN cp /etc/ssl/certs/ca-certificates.crt ./certs/
    RUN go build                                                      # fine
  • A copy of the CA left in a step's layer is removed. One that cannot be, such as in a JKS keystore sealed with a password other than changeit, fails the build (fail-on-ca-residue: false makes it a warning). One that cannot be read, such as in a compressed archive, is not found and stays in the image. See CA residue.

  • audit terminates TLS as well. It drops the rules, not the interception, so a tool that cannot accept the CA fails in audit just as it would in restrict.

  • An image with no system CA store (scratch, distroless, or debian:*-slim before ca-certificates is installed) still gets every variable set, but pointed at a file trusting only the proxy's own CA. That is enough for ordinary HTTPS, since inspect re-signs all of it with that CA, but not for an allowed-tls-rules or allowed-ip-rules passthrough, which presents its own real certificate. The decision is made once, from the rootfs as the step begins, so installing ca-certificates partway through a step doesn't help a passthrough made later in the same step:

    RUN apt-get install -y ca-certificates && \
        curl https://internal.example.com/pkg.tgz -o pkg.tgz   # still fails: CURL_CA_BUNDLE was already
                                                               # fixed to the proxy-CA-only fallback
    
    RUN apt-get install -y ca-certificates
    RUN curl https://internal.example.com/pkg.tgz -o pkg.tgz   # this step starts with a store, so
                                                               # CURL_CA_BUNDLE points at it instead
  • A task runner that starts each command with a filtered environment drops the CA variables before the command sees them. vite-plus does this for a cached vp run task, which keeps NODE_OPTIONS but not NODE_EXTRA_CA_CERTS, so Node inside it fails with SELF_SIGNED_CERT_IN_CHAIN. In an image with a system CA store, which holds the CA already, have Node read that store. As an ARG the flag reaches every later RUN in the stage without staying in the image, unless an ENV NODE_OPTIONS overrides it:

    ARG NODE_OPTIONS=--use-system-ca   # Node 22.15+ (23.9+ on 23.x); older Node refuses to start
    RUN vp run build

    Otherwise pass the variable through on the task itself, as untrackedEnv rather than env, which would put its value in the cache key. A task with cache: false keeps the whole environment.

    // vite.config.ts
    build: { command: "vp build", untrackedEnv: ["NODE_EXTRA_CA_CERTS"] },
  • The directories holding a bundle or keystore the CA is added to are mounted over while the step runs. Removing or renaming one fails, while what is inside it behaves normally:

    RUN rm -rf /etc/ssl/certs        # fails: the directory is a mount point
    RUN rm -rf /etc/ssl/certs/*      # fine

    A JDK with a keystore of its own has its lib/security mounted, so take it off PATH and JAVA_HOME with ENV before a step removes it. See Mounted directories.

FAQ

A host only ever gets looked up, never connected to. How do I write a rule for it?

The report gives it a row with DNS as the rule kind and no port. If you want it to stay unreachable without failing the job, put the name in known-blocked-rules, which is the one input where a host rule may omit the port. If the build actually needs it, write an ordinary host or URL rule and the lookup is reported as allowed.

Why not use BuildKit's built-in --proxy-network?

BuildKit's exec network proxy injects HTTP_PROXY/HTTPS_PROXY into each RUN step and can record what it fetched as SLSA provenance, which Buildcage does not do. Two limits kept it from being the enforcement mechanism. Its source policy matches a request's host, port and URL path but has no notion of an HTTP method, so it cannot allow a fetch from a registry while blocking a publish to the same host, which inspect does by terminating TLS. And because it is an explicit proxy, what it enforces and records reaches only tools that honor the proxy variables; a tool that ignores them is not covered. Buildcage enforces at the network level instead, so it also covers those tools.

Can I trust the action itself?

You can check what it runs instead of taking it on trust. The source is public, and the action runs only what was built from it: each release is immutable, its image is built from the release commit and signed, and the action verifies that signature before it runs. Changing what Buildcage does means changing its public source first. See Image provenance verification.

GitHub's native egress firewall

GitHub is building an egress firewall directly into Actions runners (technical preview as of September 2026): opt a job into a firewall-enabled runner image and its traffic is inspected outside the runner VM, in log or enforce mode, from a single .github/egress-firewall.yaml in the repository. Because it sits outside the VM, a workflow that gains root inside the runner cannot switch it off. Firewall-enabled images are GitHub-hosted and Linux only.

One policy for the whole run is one allowlist for every step in it: the destinations actions/checkout, the caches and the setup actions need stay open to the build as well. Buildcage writes a separate allowlist for the build you don't trust, so a docker build gets the hosts that build needs and nothing else, and a rule there can name a method and a URL rather than only a host. The two can be used together: GitHub's firewall as an outer limit the job cannot turn off, and Buildcage as a narrower allowlist for the build.

Buildcage also runs on any Linux runner with Docker, self-hosted included, rather than on a firewall-enabled runner image.

Documentation

Doc What's in it
Reference Every input, the rule syntax in full, and the report's own output
Security Details Architecture and threat model for every engine, attack resistance
Development Guide Working on Buildcage itself: setup, local runs, tests and the rules

Contributing

Issues and pull requests are welcome at github.com/buildcage/docker.

Show Your Support

Knowing that this project is useful to others gives me the motivation to keep working on it. If you find Buildcage useful, please consider giving it a star on GitHub.

Disclaimer

This software is provided "as is", without warranty of any kind, express or implied. The authors and contributors are not liable for any damages, losses, or security incidents arising from the use of this software. Use at your own risk.

License

The Buildcage source code is licensed under the MIT License. See LICENSE file for details.

The Docker image includes third-party components under their own licenses (GPL, Apache 2.0, ISC, etc.). See THIRD_PARTY_LICENSES for the full list. The image carries that file, this repository's LICENSE and the license texts it points to under /opt/buildcage/.

The Actions bundle their npm dependencies (MIT, Apache 2.0, ISC) into the committed dist/ files. See THIRD_PARTY_LICENSES_NPM for their license texts.

About

GitHub Action to build Docker images with outbound network access restricted to an allowlist

buildcage.github.io

Resources

Readme
MIT license

Security policy

Security policy
Activity
Custom properties

Stars

11 stars

Watchers

0 watching

Forks

0 forks
Report repository

Releases

Sponsor this project

Packages

Used by

Contributors

Languages

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.