Skip to content

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:

app → mitmproxy:8080 → privoxy:8888 → internet
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:

# HashiCorp releases
releases.hashicorp.com

Reload iptables rules:

sudo /usr/local/bin/firewall.sh apply

Step 2: proxy.yaml (HTTP/HTTPS layer)

Add the same domain to .devcontainer/proxy.yaml under allowed:

allowed:
  - releases.hashicorp.com

Recompile the privoxy filter without restarting the proxy. This regenerates /tmp/privoxy-user.action from proxy.yaml:

PROXY_YAML=.devcontainer/proxy.yaml generate-proxy-filter

Step 3: verify

curl -v https://releases.hashicorp.com

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.