Use mitmproxy on the external exit node from Route Sandboxes Through Your IP Proxy when you need to see, modify, or replay HTTP and HTTPS traffic from a sandbox.
This guide uses mitmproxy’s transparent mode. The sandbox does not need proxy environment variables: its default route points through the exit node, and nftables redirects TCP ports 80 and 443 into mitmproxy before forwarding everything else normally. UDP 443 is rejected so HTTP/3 clients fall back to inspectable HTTPS over TCP.
Only intercept traffic you own or are authorized to inspect. HTTPS inspection gives the exit node access to plaintext credentials, cookies, and bodies.
Only need to add an API key or header to outbound HTTPS? Use Freestyle’s TLS rules API instead. It keeps secret values out of the sandbox and avoids operating an exit node, private CA, or proxy service. Use mitmproxy when you need to inspect, modify, or replay complete requests and responses.
Install The SDK
pnpm add freestyle@latestbun add freestyle@latestnpm install freestyle@latestyarn add freestyle@latest export FREESTYLE_API_KEY="your-api-key"
Create The Network And Tunnel
Create one VPC and attach an exit tunnel. The attachment addresses become the next hop used by every sandbox in the VPC.
import { Freestyle } from "freestyle";
import { readFile, writeFile } from "node:fs/promises";
const freestyle = new Freestyle();
const { vpcId } = await freestyle.vpc.create({
slug: "mitmnet",
cidr: "10.100.0.0/22",
});
const tunnel = await freestyle.tunnels.create({
slug: "mitm-exit",
vpcs: [{ vpcId, exit: true }],
});
const [attachment] = tunnel.attachments;
console.log(attachment.ipv4, attachment.ipv6); // 10.100.0.1 fd20:…::1
await writeFile("mitm-exit.conf", tunnel.clientConfig, { mode: 0o600 });
Take both attachment addresses. If you pin only ipv4 or ipv6 when creating the tunnel, clients have no routed exit for the omitted family.
Build The Exit Node
Use a dedicated Ubuntu machine with a static public address. The cloud-provider setup is identical to the IP proxy guide: enable source/destination forwarding where your provider requires it, copy mitm-exit.conf to the machine, then SSH in.
Do not expose TCP 8080 or 8081 in the machine’s public firewall. Sandboxes reach the proxy over the Freestyle tunnel, and you reach the web UI through SSH.
Install WireGuard and bring up the tunnel:
sudo apt-get update
sudo apt-get install -y wireguard-tools nftables curl jq ca-certificates openssl
sudo install -d -m 700 /etc/wireguard
sudo install -m 600 /tmp/mitm-exit.conf /etc/wireguard/freestyle.conf
# The Freestyle gateway cannot dial your machine, so keep the client-initiated
# session alive even when no sandbox is currently sending traffic.
echo "PersistentKeepalive = 25" | sudo tee -a /etc/wireguard/freestyle.conf
sudo systemctl enable --now wg-quick@freestyle
sudo wg show freestyle
sudo ip address show freestyle should list the same attachment addresses returned by the SDK.
Install mitmproxy
Install the latest official standalone build. Native distribution packages often trail mitmproxy releases, and the standalone archive includes mitmproxy, mitmdump, and mitmweb together.
MITMPROXY_VERSION="$(
curl -fsSL https://api.github.com/repos/mitmproxy/mitmproxy/releases/latest |
jq -r '.tag_name | ltrimstr("v")'
)"
case "$(uname -m)" in
x86_64|amd64) MITMPROXY_ARCH=x86_64 ;;
aarch64|arm64) MITMPROXY_ARCH=aarch64 ;;
*) echo "unsupported architecture: $(uname -m)" >&2; exit 1 ;;
esac
curl -fsSL -o /tmp/mitmproxy.tar.gz \
"https://downloads.mitmproxy.org/${MITMPROXY_VERSION}/mitmproxy-${MITMPROXY_VERSION}-linux-${MITMPROXY_ARCH}.tar.gz"
mkdir -p /tmp/mitmproxy-bin
tar -xzf /tmp/mitmproxy.tar.gz -C /tmp/mitmproxy-bin
sudo install -o root -g root -m 0755 \
/tmp/mitmproxy-bin/mitmproxy \
/tmp/mitmproxy-bin/mitmdump \
/tmp/mitmproxy-bin/mitmweb \
/usr/local/bin/
mitmweb --version
Run mitmweb as a dedicated user. Its proxy listener is :8080; its web UI stays on loopback at 127.0.0.1:8081. The first start generates a unique CA under /var/lib/mitmproxy.
id -u mitmproxy >/dev/null 2>&1 || \
sudo useradd --system --home-dir /var/lib/mitmproxy --shell /usr/sbin/nologin mitmproxy
sudo install -d -o mitmproxy -g mitmproxy -m 700 /var/lib/mitmproxy
MITMWEB_PASSWORD="$(openssl rand -hex 24)"
printf 'MITMWEB_PASSWORD=%s\n' "$MITMWEB_PASSWORD" | sudo tee /etc/mitmproxy.env >/dev/null
sudo chmod 600 /etc/mitmproxy.env
sudo tee /etc/systemd/system/mitmproxy.service >/dev/null <<'EOF'
[Unit]
Description=mitmproxy transparent proxy
After=network-online.target wg-quick@freestyle.service
Wants=network-online.target
Requires=wg-quick@freestyle.service
[Service]
Type=simple
User=mitmproxy
Group=mitmproxy
EnvironmentFile=/etc/mitmproxy.env
ExecStart=/usr/local/bin/mitmweb --mode transparent --listen-port 8080 --set confdir=/var/lib/mitmproxy --web-host 127.0.0.1 --web-port 8081 --set web_open_browser=false --set web_password=${MITMWEB_PASSWORD}
Restart=always
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/mitmproxy
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mitmproxy
for attempt in $(seq 1 30); do
sudo test -s /var/lib/mitmproxy/mitmproxy-ca-cert.pem && break
sleep 1
done
sudo systemctl is-active mitmproxy
sudo test -s /var/lib/mitmproxy/mitmproxy-ca-cert.pem
The distributable file is mitmproxy-ca-cert.pem. Keep mitmproxy-ca.pem on the exit node: it also contains the CA private key, and anyone who gets it can forge certificates trusted by your sandboxes.
Redirect Web Traffic Into mitmproxy
Enable forwarding, disable ICMP redirects, and redirect only traffic arriving on the Freestyle tunnel. Restricting the redirect by input interface prevents mitmproxy’s own upstream connections from looping back into mitmproxy.
Replace eth0 with the exit node’s public interface. This configuration replaces the nftables ruleset, so use it on the dedicated exit node described above rather than on a machine with unrelated firewall rules.
sudo tee /etc/sysctl.d/99-mitm-exit.conf >/dev/null <<'EOF'
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
net.ipv4.conf.all.send_redirects=0
EOF
sudo sysctl -p /etc/sysctl.d/99-mitm-exit.conf
sudo tee /etc/nftables.conf >/dev/null <<'EOF'
flush ruleset
table inet mitm_exit {
chain clamp_mss {
type filter hook prerouting priority mangle; policy accept;
# Transparent connections terminate on this machine, so a forward-chain
# clamp never sees their return path. Advertise a tunnel-safe MSS before
# the redirect instead. 1,200 also fits inside IPv6's 1,280-byte minimum.
iifname "freestyle" tcp flags syn tcp option maxseg size set 1200
}
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "freestyle" tcp dport { 80, 443 } redirect to :8080
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "eth0" masquerade
}
chain forward {
type filter hook forward priority filter; policy accept;
# Stop HTTP/3 from bypassing the TCP interception. Clients fall back to
# HTTPS over TCP, which the prerouting chain sends through mitmproxy.
iifname "freestyle" udp dport 443 reject
}
}
EOF
sudo systemctl enable nftables
sudo systemctl restart nftables
sudo nft list ruleset
TCP traffic on 80 and 443 now terminates at mitmproxy. Other protocols and nonstandard ports still use the exit node as a normal routed gateway.
Add A Sandbox
One standing firewall rule lets every member of the VPC reach the tunnel. The sandbox itself receives no public-internet grant; its two default routes are the only way out.
await freestyle.firewall.rules.create({
action: "allow",
source: { vpcId },
destination: { tunnelId: tunnel.tunnelId },
});
const viaMitmExit = [
{ cidr: "0.0.0.0/0", via: attachment.ipv4 },
{ cidr: "::/0", via: attachment.ipv6 },
];
const { vm } = await freestyle.vms.create({
slug: "inspected-worker",
vpcs: [{ vpcId, routes: viaMitmExit }],
firewall: { rules: [] },
});
There are no HTTP_PROXY or HTTPS_PROXY variables to set. A program that ignores proxy settings still follows the sandbox’s default route and is intercepted when it uses HTTP or HTTPS over the standard TCP ports.
Trust The mitmproxy CA
Copy the public CA certificate from the exit node to the machine running your SDK script. Use the SSH target for your provider:
ssh ubuntu@203.0.113.10 \
'sudo install -m 0644 /var/lib/mitmproxy/mitmproxy-ca-cert.pem /tmp/mitmproxy-ca-cert.pem'
scp ubuntu@203.0.113.10:/tmp/mitmproxy-ca-cert.pem ./mitmproxy-ca-cert.pem
Install that certificate in the sandbox’s system trust store:
const mitmproxyCa = await readFile("mitmproxy-ca-cert.pem", "utf8");
await vm.fs.writeTextFile(
"/usr/local/share/ca-certificates/mitmproxy.crt",
mitmproxyCa,
);
await vm.exec("update-ca-certificates");
Repeat this step for every independently built sandbox image, or build the CA into a private base snapshot. Anyone who can boot a snapshot containing the CA certificate trusts this exit node, so do not publish that snapshot as a general-purpose image.
Some runtimes keep a separate trust store. Java needs the certificate imported with keytool, and certificate-pinned applications may reject interception even after the operating-system store trusts the CA.
Verify And Open mitmweb
Make a request with every conventional proxy variable removed. A successful response proves that transparent interception does not depend on process configuration:
const check = await vm.exec({
timeoutMs: 60_000,
command: `env \
-u HTTP_PROXY -u HTTPS_PROXY -u NO_PROXY \
-u http_proxy -u https_proxy -u no_proxy \
curl -4 -fsS -o /dev/null -w '%{http_code}\\n' https://example.com`,
});
console.log(check.stdout); // 200
Forward the loopback-only UI to your workstation:
ssh -N -L 8081:127.0.0.1:8081 ubuntu@203.0.113.10
Read the generated password on the exit node with sudo sed -n 's/^MITMWEB_PASSWORD=//p' /etc/mitmproxy.env, then open http://127.0.0.1:8081. The request to example.com appears as a decoded flow. sudo journalctl -u mitmproxy -f shows service and connection errors.
The public address is still the exit node’s address. Verify it from the sandbox:
const address = await vm.exec(
"curl -4 -fsS https://api.ipify.org",
);
console.log(address.stdout); // 203.0.113.10
Use An Explicit Proxy Instead
Transparent mode is useful when you cannot trust applications to honor proxy settings. If you control the workload, mitmproxy recommends its simpler regular mode.
Change the service to --mode regular and bind the proxy listener to the tunnel attachment’s IPv4 address rather than every interface:
ExecStart=/usr/local/bin/mitmweb --mode regular --listen-host 10.100.0.1 --listen-port 8080 --set confdir=/var/lib/mitmproxy --web-host 127.0.0.1 --web-port 8081 --set web_open_browser=false --set web_password=${MITMWEB_PASSWORD}
Then allow only the proxy port through the tunnel and create the sandbox without default routes:
await freestyle.firewall.rules.create({
action: "allow",
source: { vpcId },
destination: {
tunnelId: tunnel.tunnelId,
port: 8080,
protocol: "tcp",
},
});
const { vm: explicitProxyVm } = await freestyle.vms.create({
vpcs: [{ vpcId }],
firewall: { rules: [] },
});
const proxyUrl = `http://${attachment.ipv4}:8080`;
const response = await explicitProxyVm.exec({
env: {
HTTP_PROXY: proxyUrl,
HTTPS_PROXY: proxyUrl,
http_proxy: proxyUrl,
https_proxy: proxyUrl,
},
command: "curl -fsS https://example.com",
});
Install the CA exactly as above. With no default route and only TCP 8080 allowed, proxy-aware programs work and programs that ignore the proxy cannot silently egress around it. For VM-wide shell, apt, Git, systemd, and Docker proxy defaults, reuse the client configuration in How to Run Iron Proxy on a Router VM with this proxy URL.
Production Notes
- The CA private key is a high-value credential. Keep
/var/lib/mitmproxy/mitmproxy-ca.pemprivate, rotate it deliberately, and remove the old public CA from client trust stores after rotation. - Flows contain secrets. mitmweb retains captured traffic in memory. Do not expose its UI publicly, and save flow files only when you have an explicit retention and access policy.
- Transparent interception here is deliberately scoped. TCP
80and443are inspected, UDP443is rejected, and other ports pass through normally. Narrow the forward policy if your workload must not use other egress protocols. - Certificate pinning still works. A pinned client may fail instead of accepting mitmproxy’s forged leaf certificate. Exclude that host from interception or change the client only when you are authorized to do so.
- A tunnel is part of the availability path. Deleting it removes firewall rules that name it, and pausing the exit node takes egress down for every sandbox routed through it.
- Update mitmproxy regularly. Its standalone distribution bundles Python, OpenSSL, and other dependencies rather than updating them independently.