Use iron-proxy on a dedicated router VM when workloads in a VPC need controlled outbound access and header injection.
This pattern gives you:
- A client VM with Freestyle internet disabled.
- A router VM that provides NAT and runs iron-proxy.
- HTTPS interception with a local CA so the proxy can inspect and transform requests.
- VM-wide proxy defaults on the client, so standard tooling uses iron-proxy without per-program proxy code.
- Secret injection from router environment variables, such as
FREESTYLE_API_KEYintoAuthorization.
Architecture
- Client VM sends its default route to router
192.168.51.1. - Router VM performs NAT for egress and runs iron-proxy on
:3128. - Client VM installs the iron-proxy CA and proxy defaults once.
- iron-proxy applies domain rules and secret transforms before forwarding upstream.
flowchart LR
C[Client VM\n192.168.51.30\ninternet disabled\nproxy defaults installed] -->|default route + HTTPS proxy| R[Router VM\n192.168.51.1\nNAT + iron-proxy :3128]
R -->|transformed HTTPS| A[api.freestyle.sh]
1. Create A VPC
Start with the SDK, your API key, and the network constants used by both VMs:
import { freestyle } from "freestyle";
const freestyleApiKey = process.env.FREESTYLE_API_KEY;
if (!freestyleApiKey) {
throw new Error("FREESTYLE_API_KEY is required");
}
const latestIronProxyRelease = await fetch(
"https://api.github.com/repos/ironsh/iron-proxy/releases/latest",
).then((response) => response.json() as Promise<{ tag_name: string }>);
const ironProxyVersion = latestIronProxyRelease.tag_name.replace(/^v/, "");
const vpcCidr = "192.168.51.0/24";
const routerIp = "192.168.51.1";
const clientIp = "192.168.51.30";
const proxyPort = 3128;
const { vpcId } = await freestyle.vpc.create({
name: "ironproxy-example",
cidr: vpcCidr,
});
2. Create The Router VM
Create the router VM before configuring it. This VM keeps normal internet access so it can download the iron-proxy binary and forward client egress.
const { vm: routerVm } = await freestyle.vms.create({
nics: [{ vpc: vpcId, mode: "routed", ipv4: routerIp }],
});
3. Configure The Router VM
Create the iron-proxy YAML config locally, then write it onto the router VM.
const ironProxyConfig = `dns:
proxy_ip: "${routerIp}"
proxy:
tunnel_listen: ":${proxyPort}"
tls:
mode: "mitm"
ca_cert: "/etc/iron-proxy/ca.crt"
ca_key: "/etc/iron-proxy/ca.key"
transforms:
- name: allowlist
config:
domains:
- "api.freestyle.sh"
- name: secrets
config:
secrets:
- source:
type: env
var: FREESTYLE_API_KEY
inject:
header: "Authorization"
formatter: "Bearer {{ .Value }}"
rules:
- host: "api.freestyle.sh"
log:
level: "info"
`;
Install iron-proxy, generate the router CA, write the config and systemd service, then enable NAT:
await routerVm.exec({
timeoutMs: 120_000,
command: `set -eux
curl -fsSL -o /tmp/iron-proxy.tgz "https://github.com/ironsh/iron-proxy/releases/download/v${ironProxyVersion}/iron-proxy_${ironProxyVersion}_linux_amd64.tar.gz"
tar -xzf /tmp/iron-proxy.tgz -C /tmp
mv /tmp/iron-proxy /usr/local/bin/iron-proxy
chmod +x /usr/local/bin/iron-proxy
rm -f /tmp/iron-proxy.tgz
mkdir -p /etc/iron-proxy
openssl genrsa -out /etc/iron-proxy/ca.key 2048
openssl req -x509 -new -nodes \
-key /etc/iron-proxy/ca.key \
-sha256 -days 365 \
-subj "/CN=iron-proxy CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign" \
-out /etc/iron-proxy/ca.crt
cat >/etc/iron-proxy/config.yaml <<'EOF'
${ironProxyConfig}
EOF
cat >/etc/systemd/system/iron-proxy.service <<'EOF'
[Unit]
Description=iron-proxy
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/iron-proxy -config /etc/iron-proxy/config.yaml
Environment=FREESTYLE_API_KEY=${freestyleApiKey}
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sysctl -w net.ipv4.ip_forward=1
cat >/etc/sysctl.d/99-freestyle-router.conf <<'EOF'
net.ipv4.ip_forward=1
EOF
cat >/etc/nftables.conf <<'EOF'
flush ruleset
table ip freestyle_router {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr ${vpcCidr} oifname "eth0" masquerade
}
}
EOF
systemctl daemon-reload
systemctl enable --now nftables
systemctl enable --now iron-proxy
`,
});
Check that iron-proxy is running before creating clients that depend on it:
const routerStatus = await routerVm.exec("systemctl is-active iron-proxy");
console.log(routerStatus.stdout?.trim()); // active
4. Create The Client VM
Create a client VM with Freestyle’s normal internet route disabled. Its default route points at the router VM, so outbound traffic goes through the router.
const { vm: clientVm } = await freestyle.vms.create({
internet: "disabled",
nics: [
{
default: true,
vpc: vpcId,
mode: "routed",
ipv4: clientIp,
routes: [{ cidr: "0.0.0.0/0", via: routerIp }],
},
],
});
5. Trust The Proxy CA On The Client
Install the router CA into the client VM’s system trust store. After this, standard HTTPS clients can trust certificates minted by iron-proxy.
const caCertResult = await routerVm.exec("cat /etc/iron-proxy/ca.crt");
const caCert = caCertResult.stdout?.trim() ?? "";
await clientVm.exec({
command: [
"cat >/usr/local/share/ca-certificates/iron-proxy.crt <<'EOF'",
caCert,
"EOF",
"update-ca-certificates",
].join("\n"),
});
6. Configure VM-Wide Proxy Defaults
Set proxy defaults on the client VM so shells, package managers, and future systemd services use iron-proxy by default.
await clientVm.exec({
command: `set -eux
cat >/etc/profile.d/90-ironproxy.sh <<'EOF'
export HTTP_PROXY=http://${routerIp}:${proxyPort}
export HTTPS_PROXY=http://${routerIp}:${proxyPort}
export NO_PROXY=localhost,127.0.0.1,::1,${routerIp},${vpcCidr}
export http_proxy=$HTTP_PROXY
export https_proxy=$HTTPS_PROXY
export no_proxy=$NO_PROXY
EOF
cat >/etc/environment <<'EOF'
HTTP_PROXY=http://${routerIp}:${proxyPort}
HTTPS_PROXY=http://${routerIp}:${proxyPort}
NO_PROXY=localhost,127.0.0.1,::1,${routerIp},${vpcCidr}
http_proxy=http://${routerIp}:${proxyPort}
https_proxy=http://${routerIp}:${proxyPort}
no_proxy=localhost,127.0.0.1,::1,${routerIp},${vpcCidr}
EOF
mkdir -p /etc/systemd/system.conf.d
cat >/etc/systemd/system.conf.d/90-ironproxy.conf <<'EOF'
[Manager]
DefaultEnvironment=HTTP_PROXY=http://${routerIp}:${proxyPort} HTTPS_PROXY=http://${routerIp}:${proxyPort} NO_PROXY=localhost,127.0.0.1,::1,${routerIp},${vpcCidr}
EOF
cat >/etc/apt/apt.conf.d/95proxies <<'EOF'
Acquire::http::Proxy "http://${routerIp}:${proxyPort}";
Acquire::https::Proxy "http://${routerIp}:${proxyPort}";
EOF
git config --system http.proxy http://${routerIp}:${proxyPort} || true
git config --system https.proxy http://${routerIp}:${proxyPort} || true
systemctl daemon-reload
`,
});
If you run Docker in the client VM, add a systemd drop-in so image pulls and builds also use iron-proxy:
mkdir -p /etc/systemd/system/docker.service.d
cat >/etc/systemd/system/docker.service.d/proxy.conf <<'EOF'
[Service]
Environment="HTTP_PROXY=http://192.168.51.1:3128"
Environment="HTTPS_PROXY=http://192.168.51.1:3128"
Environment="NO_PROXY=localhost,127.0.0.1,::1,192.168.51.1,192.168.51.0/24"
EOF
systemctl daemon-reload
systemctl restart docker
Restart existing long-running services after this step so they pick up the new environment.
7. Validate VM-Wide Behavior
Run one request with proxy variables explicitly removed, then one request through a login shell that loads the VM-wide proxy defaults:
const direct = await clientVm.exec({
command: `env -u HTTP_PROXY -u HTTPS_PROXY -u NO_PROXY -u http_proxy -u https_proxy -u no_proxy \
curl -sS -o /tmp/direct.out -w '%{http_code}' https://api.freestyle.sh/auth/v1/whoami`,
});
const proxied = await clientVm.exec({
command: `bash -lc "curl -sS -o /tmp/proxied.out -w '%{http_code}' https://api.freestyle.sh/auth/v1/whoami"`,
});
console.log(direct.stdout?.trim()); // 401
console.log(proxied.stdout?.trim()); // 200
The direct request still reaches the internet through the router VM’s NAT, but it bypasses iron-proxy because the command explicitly removes proxy settings. Normal shells and proxy-aware tools use iron-proxy by default after the client setup.
Production Notes
- Keep API keys in a secret manager or boot-time credential flow. Avoid baking long-lived secrets into snapshots.
- Restrict
allowlistand rules to the minimal set of domains and paths you need. - Rotate proxy CA keys and API keys regularly.
- Monitor
iron-proxyservice logs withjournalctl -u iron-proxy. - Environment-based proxy defaults cover standard proxy-aware userland tools. To make bypass impossible for arbitrary binaries, add transparent interception rules on the router and test that mode separately.
For a full runnable example, see the SDK script in the repository at freestyle-sdk/examples/vpc-ironproxy.ts.