Skip to content

Latest commit

 

History

History
283 lines (199 loc) · 8.61 KB

File metadata and controls

283 lines (199 loc) · 8.61 KB

Proxy Add-on (Transparent URL Allowlisting)

The proxy is an optional add-on. When enabled, the devtools container routes all HTTP/HTTPS traffic through a URL-allowlisting proxy. It is experimental, but it has worked well for the current developer flows using Claude, Codex, and pi. Contributions are welcome, especially from folks with Rust and HTTP/2 expertise.

How It Works

  1. acl-proxy runs on the host, listening on ports 8881 (HTTP) and 8889 (HTTPS)
  2. iptables rules inside the container redirect outbound traffic to the proxy
  3. The proxy checks each request against a URL allowlist
  4. Allowed requests are forwarded; denied requests get blocked

HTTPS Interception (MITM)

To inspect HTTPS traffic, the proxy performs TLS termination:

  1. On first run, acl-proxy generates a CA (Certificate Authority) and saves it to certs/ca-cert.pem
  2. When a client connects to https://github.com, the proxy:
    • Extracts the hostname from TLS SNI (Server Name Indication)
    • Generates a certificate for github.com signed by its CA (cached for reuse)
    • Completes the TLS handshake with the client using this certificate
  3. The proxy then opens its own TLS connection to the real github.com
  4. Traffic flows: Client ↔ Proxy (decrypted) ↔ Upstream

This is why clients must trust the proxy's CA—without it, they'd see certificate errors for every HTTPS site.

Why Use This?

  • Security: Containers can only access approved URLs
  • Audit: Requests can be logged
  • Control: Prevent data exfiltration, limit API access

Prerequisites

  • iptables - required on host for proxy redirection rules

    # RHEL/Fedora/Rocky
    sudo dnf install iptables
    
    # Debian/Ubuntu
    sudo apt install iptables
  • openssl-devel - required to build acl-proxy

    # RHEL/Fedora/Rocky
    sudo dnf install openssl-devel
    
    # Debian/Ubuntu
    sudo apt install libssl-dev

Setup

1. Install acl-proxy

git clone https://github.com/kcosr/acl-proxy.git
cd acl-proxy
cargo install --path .

This installs acl-proxy to ~/.cargo/bin/.

2. Trust the CA Certificate

acl-proxy performs HTTPS interception (MITM). Clients must trust its CA.

Since this setup bind-mounts certificate directories (/etc/pki/ca-trust, /etc/pki/tls) read-only from the host, installing the CA on the host makes it automatically available inside containers.

# Start proxy to generate CA (first time only, run from devtools/)
acl-proxy --config proxy/acl-proxy.toml

# Install CA system-wide (RHEL/Fedora/Rocky)
# certs/ is created in the working directory where acl-proxy runs
sudo cp certs/ca-cert.pem /etc/pki/ca-trust/source/anchors/acl-proxy-ca.pem
sudo update-ca-trust

# Install CA system-wide (Debian/Ubuntu)
sudo cp certs/ca-cert.pem /usr/local/share/ca-certificates/acl-proxy-ca.crt
sudo update-ca-certificates

Note: If you use a self-contained container (not bind-mounting /etc), you'll need to copy the CA cert into the image and run update-ca-trust during the build.

3. Configure URL Allowlist

Edit proxy/acl-proxy.toml:

[policy]
default = "deny"

[[policy.rules]]
action = "allow"
pattern = "https://api.github.com/**"
description = "GitHub API"

[[policy.rules]]
action = "allow"
pattern = "https://registry.npmjs.org/**"
description = "NPM registry"

4. Start the Proxy

acl-proxy --config proxy/acl-proxy.toml

5. Launch Container with Proxy

PROXY=1 ./container/run.sh

iptables Rules

When PROXY=1, run.sh uses nsenter to set up these rules in the container's network namespace:

# NAT table - redirect HTTP/HTTPS
iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN          # skip loopback
iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to 169.254.1.2:8881
iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to 169.254.1.2:8889

# Filter table - default deny
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT              # DNS
iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT              # DNS
iptables -A OUTPUT -p tcp -d 169.254.1.2 --dport 8881 -j ACCEPT
iptables -A OUTPUT -p tcp -d 169.254.1.2 --dport 8889 -j ACCEPT
iptables -A OUTPUT -j DROP                                   # deny all else

The final DROP rule ensures all outbound traffic not explicitly allowed is blocked. Only DNS and proxy traffic can leave the container—everything else must go through the proxy to reach the network.

169.254.1.2 is host.containers.internal in podman.

GitHub Auth Injection

The proxy can inject real GitHub credentials without exposing tokens inside the container. This works by:

Note: Use Git over HTTPS (not SSH) so requests go through the acl-proxy and can be allowlisted/filtered. SSH traffic does not traverse the HTTP/HTTPS proxy.

  1. Dummy credentials in container - The overlay provides:

    • ~/.config/gh/hosts.yml - dummy token for gh CLI
    • ~/.gitconfig - inline credential helper that returns dummy creds for git

    Why inline helper? The standard credential.helper = store tries to write to ~/.git-credentials. Since overlay files are bind-mounted read-only, the write fails and breaks auth. The inline function just echoes credentials without writing.

  2. Proxy replaces the header - When gh or git sends an Authorization header, the proxy replaces it with the real token.

Ensure your overlay files are in place and updated (see README.md for overlay setup), including:

  • container/overlay/home/<username>/.config/gh/hosts.yml - set your GitHub username
  • container/overlay/home/<username>/.gitconfig - set your name/email

Auth Format Differences

GitHub requires different formats for different endpoints:

API (api.github.com) - token format:

[[policy.rules.header_actions]]
direction = "request"
action = "set"
name = "authorization"
value = "token ghp_YOUR_TOKEN"
when = "if_present"

Git over HTTPS (github.com) - Basic auth format:

# Generate the value:
echo -n "ghp_YOUR_TOKEN:x-oauth-basic" | base64
[[policy.rules.header_actions]]
direction = "request"
action = "set"
name = "authorization"
value = "Basic <base64_output>"
when = "if_present"

How when = "if_present" Works

  • Requests with an auth header → replaced with real credentials
  • Requests without an auth header → pass through unauthenticated

This means:

  • Tools that send auth (gh, git with credential helper) get real credentials
  • Unauthenticated requests (curl without -u) stay unauthenticated
  • The real token never exists inside the container

Managing Rules at Runtime

The proxy-iptables.sh script manages iptables rules for any running container:

./container/proxy-iptables.sh <command> <container-name>

Commands

Command Description
add Add proxy redirect rules
remove Remove all rules (disable proxy)
status Show current iptables rules

Examples

# Check current rules
./container/proxy-iptables.sh status devcontainer

# Disable proxy (flush rules)
./container/proxy-iptables.sh remove devcontainer

# Re-enable proxy
./container/proxy-iptables.sh add devcontainer

Manual nsenter (if needed)

You can also use nsenter directly:

PID=$(podman inspect --format '{{.State.Pid}}' devcontainer)

# View NAT rules
sudo nsenter -t $PID -n iptables -t nat -L OUTPUT -n

# View filter rules
sudo nsenter -t $PID -n iptables -L OUTPUT -n

# Flush all rules
sudo nsenter -t $PID -n iptables -t nat -F OUTPUT
sudo nsenter -t $PID -n iptables -F OUTPUT

Security Notes

  • nsenter requires sudo on the host to enter the container's network namespace
  • Container has no network capabilities - users inside cannot modify iptables
  • Rules are per-container - each container has its own network namespace

Troubleshooting

"Blocked by URL policy"

The URL is not in your allowlist. Check proxy/acl-proxy.toml.

SSL Certificate Errors

The CA certificate isn't trusted. Make sure you ran update-ca-trust.

Can't Connect to Anything

Check that acl-proxy is running on the host:

curl https://api.github.com/zen

Testing Auth Injection

From inside the container, test that dummy auth gets replaced with real credentials:

# Should work - gh sends auth header, proxy replaces with real token
gh repo list --limit 3

# Should work - curl with dummy auth header triggers replacement
curl -H "Authorization: token dummy" https://api.github.com/user

# Should fail (401) - no auth header sent, so none injected
curl https://api.github.com/user