Manage Egress¶
The container enforces egress control at two independent layers. For HTTP/HTTPS, both layers must permit a destination for outbound traffic to succeed. DNS and SSH are gated by the iptables layer only.
All application HTTP/HTTPS also transits the always-on mitmproxy on
127.0.0.1:8080 in front of privoxy on 127.0.0.1:8888:
graph TD
A["Outbound HTTP/HTTPS request"] --> M["mitmproxy 127.0.0.1:8080<br/>(TLS inspect)"]
M --> C{Domain allowed by<br/>privoxy 127.0.0.1:8888?}
C -->|No| E["Proxy BLOCK"]
C -->|Yes| B{IP in iptables<br/>allowlist?}
B -->|No| D["REJECT (icmp-port-unreachable)"]
B -->|Yes| F["ACCEPT → internet"]
G[".devcontainer/egress-allowlist.txt"] --> H["firewall.sh apply"]
H -->|resolve hostnames → IPs| I["iptables OUTPUT chain"]
I --> B
J[".devcontainer/proxy.yaml"] --> K["generate-proxy-filter"]
K --> L["/tmp/privoxy-user.action"]
L --> C
Layer 1 — iptables (firewall.sh): resolves allowlisted hostnames to IPs
at apply-time and sets the OUTPUT chain policy to DROP. Covers all TCP/UDP
traffic, including DNS and SSH. All blocked traffic is rejected (not
silently dropped — you get an immediate ICMP error).
Layer 2 — privoxy: domain-name allowlist for HTTP/HTTPS only.
Hot-reloadable without a firewall flush. privoxy does not read proxy.yaml
directly — it reads the generated /tmp/privoxy-user.action, which
generate-proxy-filter compiles from .devcontainer/proxy.yaml. DNS and SSH
do not pass through privoxy.
There is no default allowlist baked into the image. Each project defines its
own .devcontainer/egress-allowlist.txt, pointed at by the EGRESS_ALLOWLIST
environment variable in devcontainer.json remoteEnv.
Adding a new HTTPS host¶
Allowing a new HTTPS host requires updating both layers, in this order.
Step 1: egress-allowlist.txt (iptables layer)¶
Add the hostname (or CIDR block) to .devcontainer/egress-allowlist.txt.
Wildcards are not supported — list specific hostnames or CIDR blocks:
Reload iptables rules:
Step 2: proxy.yaml (HTTP/HTTPS layer)¶
Add the same domain to .devcontainer/proxy.yaml under allowed:
Recompile the privoxy filter without restarting the proxy. This regenerates
/tmp/privoxy-user.action from proxy.yaml:
Step 3: verify¶
Checking current rules¶
# iptables rules
sudo /usr/local/bin/firewall.sh status
# privoxy domain allowlist (source of truth)
cat .devcontainer/proxy.yaml
# compiled privoxy action file (what privoxy actually reads)
cat /tmp/privoxy-user.action
The dev-monitor dashboard at http://127.0.0.1:9090 shows the Proxy Feed
and Connection Failures panels in real time — useful for identifying exactly
which hostname is being blocked.
Reference template¶
examples/egress-allowlist.txt in this repo covers Alpine, npm, PyPI,
RubyGems, container registries, HashiCorp, OpenBao, GitHub, Gitea, GitLab,
Anthropic, 1Password, OCI, Proxmox, vSphere, mise, crates.io, and Ansible
Galaxy. All entries are commented out. Uncomment exactly what the project needs.
If this project uses the bao CLI — or Ansible's community.hashi_vault (via the
hvac library) — against a Vault/OpenBao server, add the server's
hostname to both layers (its openbao.example.com placeholder rows in
egress-allowlist.txt and proxy.yaml), replacing the placeholder with your
real host. The token and BAO_ADDR/BAO_CACERT wiring lives in
.devcontainer/env.sh — see Environment Variables.