---
title: "How to Run Iron Proxy on a Router VM"
description: "Route a VM through a router VM that runs iron-proxy, then configure the client VM to use that proxy by default."
url: "/docs/guides/run-iron-proxy-on-a-router-vm"
---

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_KEY` into `Authorization`.


## 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.

```mermaid
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:

```ts
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.

```ts
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.

```ts
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:

```ts
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:

```ts
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.

```ts
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.

```ts
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.

```ts
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:

```bash
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:

```ts
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 `allowlist` and rules to the minimal set of domains and paths you need.
- Rotate proxy CA keys and API keys regularly.
- Monitor `iron-proxy` service logs with `journalctl -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`.
