Repository navigation
Network Egress Filtering
An optional, recommended second line of defence. The service already refuses private and reserved addresses in code (see Security Overview). A network rule is the independent backstop for whatever the code might miss - a bug in the address check or in the local proxy, a future Chrome feature that bypasses the proxy switches, any other process in the container: private, link-local (cloud metadata), carrier-grade-NAT, multicast and other reserved addresses become unreachable from the container, whatever Chrome tries.
docker-compose.egress.example.yml does that with a small helper container, egress-guard, that installs firewall rules in the service's network namespace. It adds a layer; it does not replace the code-level checks.
This page is the working summary. The complete behaviour, the restart table and every known limitation are in the deployment guide - read its limitations before you rely on this.
-
Find out where
WP_RECEIVER_URLpoints. A public address (https://your-site.example/...) needs no exception. A private one (WordPress in another container, a LAN host,host.docker.internal) needs its IP address and port. Give a WordPress container a fixed address (ipv4_address:in its network, which needs anipamsubnet), or a recreated container gets a new address and delivery is refused. -
Copy the example next to your
docker-compose.yml, and replaceyour_wordpress_networkwith the network name your own compose file uses. -
Add the settings below to the
.envnext to yourdocker-compose.yml(see Settings):EGRESS_ALLOWwith your receiver'sADDRESS:PORTif it is private, andEGRESS_ALLOW_DNSonly if your resolver needs it. -
Move networking settings to the guard. The service now shares the guard's network namespace, so
ports,expose,dns,dns_search,extra_hosts,hostnameandsysctlsbelong on theegress-guardservice, not oncritical-css-service. Docker refusesports,expose,dns,extra_hostsandhostnameon a service that usesnetwork_mode: service:...(checked with Docker Engine 29.8.1 and Compose 5.5.1);dns_searchis ignored without an error, and anet.*sysctl on the service changes the shared namespace, so neither belongs there. -
Start both files together:
docker compose -f docker-compose.yml -f docker-compose.egress.example.yml up -d
From now on every docker compose command for this stack needs both -f flags. With only the base file, Compose does not know the guard: up -d recreates the service, with or without a new image, from the base file alone, without network_mode: service:egress-guard, and the filtering is silently gone (the only hint is a warning about an orphan container). restart egress-guard fails, logs leaves out the guard and down leaves it running.
To avoid typing both flags you can put COMPOSE_FILE=docker-compose.yml:docker-compose.egress.example.yml in the .env next to your compose file (the separator is ; on Windows). An explicit -f overrides it, so the commands above stay correct either way. After any up -d you can check that the service is still inside the guard's namespace:
docker inspect -f '{{.HostConfig.NetworkMode}}' critical-css-serviceIt must print container: followed by an ID; a network name such as <project>_<network> means the guard is bypassed.
You need Docker Compose 2.24 or newer.
Read from the .env next to your docker-compose.yml when the guard is created. Both are optional.
| Variable | Value |
|---|---|
EGRESS_ALLOW |
Private destinations the service may still open TCP connections to, as space-separated ADDRESS:PORT pairs, e.g. 172.20.0.10:80. IPv6 is written [fd00::10]:80. Leave it empty if WP_RECEIVER_URL is public. List the receiver's exact address and port, nothing wider. |
EGRESS_ALLOW_DNS |
Only if your DNS resolver is itself one of the refused addresses (a private or link-local one, or Azure's 168.63.129.16). Space-separated resolver addresses, port 53 only. |
docker exec critical-css-service node -e "fetch('http://169.254.169.254/',{signal:AbortSignal.timeout(4000)}).then(()=>console.log('NOT FILTERED'),e=>console.log(e.cause?.code==='ECONNREFUSED'?'OK: egress filtering is active':'NOT CONCLUSIVE: '+(e.cause?.code||e.name)))"-
OK- the connection was refused locally, at once. -
NOT FILTERED- the address answered; the rules are not working. -
NOT CONCLUSIVE(usually a timeout) - nothing refused it. Checkdocker compose -f docker-compose.yml -f docker-compose.egress.example.yml ps(the guard should behealthy) anddocker logs critical-css-egress-guard, which should end withready.
Then run a normal render (Getting Started) to confirm delivery to your receiver still works, and docker exec critical-css-service node -e "require('dns').lookup('example.com',console.log)" to confirm name resolution still works.
- The guard starts first and gates the service: it only starts once the rules are in the kernel, and the service's own start command also waits until a connection to the metadata address is refused. A guard that cannot install the rules never becomes healthy, so the service never starts.
- If the guard dies, the service loses its network. That fails closed.
- Changes to
EGRESS_ALLOWandEGRESS_ALLOW_DNS(and every other.envvalue) are read when a container is created.docker restartdoes not re-read the.env: apply a change withdocker compose -f docker-compose.yml -f docker-compose.egress.example.yml up -d. - Restarting only the service keeps the rules. Restarting only the guard with plain Docker leaves the service without a network until you restart it too; the same with both compose files,
docker compose -f docker-compose.yml -f docker-compose.egress.example.yml restart egress-guard, does both. - If name lookups start failing with
SERVFAILafter you switch it on, your DNS resolver is one of the refused addresses. Put it inEGRESS_ALLOW_DNS.
- Anything inside the container's own loopback. The service's port, Chrome's DevTools port and
127.0.0.0/8stay reachable from Chrome because loopback has to stay open; the local proxy keeps Chrome off it. - Whatever you put in
EGRESS_ALLOWandEGRESS_ALLOW_DNS. Page content can reach those addresses too. - The public internet. The service renders your public site and its assets, so those stay open by design.
- Not tested on a plain Linux host yet, and Compose only: plain
docker run, Swarm, Kubernetes and rootless Docker or Podman are untested (on Kubernetes use aNetworkPolicywith anipBlockexceptlist). Do not combine it withnetwork_mode: host.
WP Critical CSS
Set up
Understand
Operate
Contribute