Freestyle Integration
This guide walks through running iron-proxy inside Freestyle VMs. You will create a VM snapshot with iron-proxy pre-installed, then use that snapshot to launch ephemeral VMs with egress control already configured.
Freestyle VMs do not include the iptables owner match module (xt_owner), so egress control is limited to DNS-based interception only. See Egress Control Limitations for details.
How It Works
The setup has two phases:
- Snapshot creation: A one-time script builds a Freestyle VM that installs iron-proxy, generates a CA certificate, trusts it system-wide, and configures DNS to resolve through the proxy. Freestyle snapshots the result.
- VM launch: New VMs boot from the snapshot with iron-proxy already running. DNS queries resolve through iron-proxy, which checks them against your allowlist and terminates TLS using per-domain leaf certificates.
Because iron-proxy owns DNS inside the VM, all domain-based traffic flows through the proxy automatically. There is no need for per-process configuration or custom environment variables.
Prerequisites
- A Freestyle account with API access
- Node.js 18+
- A
FREESTYLE_API_KEYenvironment variable set in your shell
Setup
Install the Freestyle SDK
npm install freestyle-sandboxesCreate the Snapshot Script
Save this as create-snapshot.mjs. It builds a Debian VM, installs iron-proxy, generates a trusted CA, and snapshots the result.
import { freestyle, VmSpec, VmBaseImage } from "freestyle-sandboxes";
const IRON_PROXY_VERSION = process.env.IRON_PROXY_VERSION || "latest";
// iron-proxy configuration
const ironProxyConfig = `
dns:
listen: ":53"
proxy_ip: "127.0.0.1"
upstream_resolver: "8.8.8.8:53"
proxy:
http_listen: ":80"
https_listen: ":443"
tls:
ca_cert: "/etc/iron-proxy/ca.crt"
ca_key: "/etc/iron-proxy/ca.key"
transforms:
- name: allowlist
config:
warn: false
domains: []
log:
level: "info"
`.trimStart();
// Oneshot script: download iron-proxy, generate CA, trust CA, stop systemd-resolved
const installScript = `#!/bin/bash
set -euo pipefail
# Resolve version
VERSION="${IRON_PROXY_VERSION}"
if [ "$VERSION" = "latest" ]; then
VERSION=$(curl -fsSL https://api.github.com/repos/ironsh/iron-proxy/releases/latest | jq -r '.tag_name | ltrimstr("v")')
fi
echo "Installing iron-proxy v$VERSION"
# Download and install binary
curl -fsSL -o /tmp/iron-proxy.tgz \\
"https://github.com/ironsh/iron-proxy/releases/download/v\${VERSION}/iron-proxy_\${VERSION}_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
# Generate CA for TLS interception
mkdir -p /etc/iron-proxy
openssl genrsa -out /etc/iron-proxy/ca.key 2048 2>/dev/null
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 2>/dev/null
# Trust the CA system-wide
cp /etc/iron-proxy/ca.crt /usr/local/share/ca-certificates/iron-proxy-ca.crt
update-ca-certificates
# Stop systemd-resolved to free port 53
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
# Route DNS through the proxy
echo "nameserver 127.0.0.1" > /etc/resolv.conf
`;
async function main() {
const baseImage = new VmBaseImage("FROM debian:trixie-slim").runCommands(
"apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y curl ca-certificates openssl jq"
);
const spec = new VmSpec()
.baseImage(baseImage)
.additionalFiles({
"/etc/iron-proxy/config.yaml": { content: ironProxyConfig },
"/usr/local/bin/install-iron-proxy.sh": { content: installScript },
})
.systemdService({
name: "install-iron-proxy",
mode: "oneshot",
exec: ["bash /usr/local/bin/install-iron-proxy.sh"],
wantedBy: ["multi-user.target"],
remainAfterExit: true,
timeoutSec: 120,
})
.systemdService({
name: "iron-proxy",
mode: "service",
exec: ["/usr/local/bin/iron-proxy -config /etc/iron-proxy/config.yaml"],
after: ["install-iron-proxy.service"],
requires: ["install-iron-proxy.service"],
restartPolicy: {
policy: "on-failure",
restartSec: 5,
},
});
console.log("Creating iron-proxy VM and snapshotting...");
const { vm, snapshotId } = await freestyle.vms.create({
snapshot: spec,
persistence: { type: "ephemeral" },
});
// Verify everything is running
const status = await vm.exec("systemctl status iron-proxy --no-pager");
console.log("\niron-proxy service status:");
console.log(status.stdout);
const resolv = await vm.exec("cat /etc/resolv.conf");
console.log("DNS configuration:");
console.log(resolv.stdout);
await vm.stop();
console.log(`\nSnapshot ID: ${snapshotId}`);
console.log(
"Done. Use this snapshotId to create ephemeral VMs with iron-proxy pre-configured."
);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});A few things to note about this script:
dns.proxy_ipis set to127.0.0.1because iron-proxy runs inside the same VM as your workloads.dns.upstream_resolveruses8.8.8.8:53. Change this if your network requires a different upstream resolver.warn: falsemeans non-allowlisted requests are blocked immediately. Set totruewhile building your allowlist.domains: []starts with an empty allowlist. Add the domains your workloads need before creating the snapshot.- The install script disables
systemd-resolvedand points/etc/resolv.confat127.0.0.1so all DNS goes through iron-proxy. - The CA is trusted system-wide via
update-ca-certificates.
Bootstrap the Snapshot
Run the script to create your snapshot. This requires a FREESTYLE_API_KEY in your environment:
export FREESTYLE_API_KEY="your-api-key"
node create-snapshot.mjsThe first run will take longer than usual because Freestyle needs to build the base image from scratch. You should expect to see output like this:
Creating iron-proxy VM and snapshotting...
VM creation is taking longer than expected. This usually happens when there's a cache miss on your vm's base snapshot. Subsequent vm creations with this configuration will likely be much faster.
iron-proxy service status:
● iron-proxy.service - iron-proxy
Loaded: loaded (/etc/systemd/system/iron-proxy.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-04-08 19:50:52 UTC; 4s ago
Invocation: 54e29589b0bd4aaa827f7bd0ef8370d2
Main PID: 1984 (iron-proxy)
Tasks: 10 (limit: 9551)
Memory: 3M (peak: 3.6M)
CPU: 51ms
CGroup: /system.slice/iron-proxy.service
└─1984 /usr/local/bin/iron-proxy -config /etc/iron-proxy/config.yaml
...
DNS configuration:
nameserver 127.0.0.1
Snapshot ID: sc-gogdcl41ilq3jabxytjn
Done. Use this snapshotId to create ephemeral VMs with iron-proxy pre-configured.Save the snapshot ID. You will use it to launch new VMs.
Launch VMs From the Snapshot
Use the snapshot ID to create ephemeral VMs with iron-proxy already running:
import { freestyle } from "freestyle-sandboxes";
const { vm } = await freestyle.vms.create({
snapshotId: "sc-abc123...",
persistence: { type: "ephemeral" },
});
// Traffic is already routed through iron-proxy
const result = await vm.exec("curl -s https://httpbin.org/get");
console.log(result.stdout);
await vm.stop();Verify
Check that iron-proxy is intercepting traffic inside a running VM:
# Inside the VM (via vm.exec or SSH)
systemctl status iron-proxyYou should see the service active and running. Make a test request to confirm:
curl -sv https://httpbin.org/get 2>&1 | grep "issuer"If iron-proxy is working, the TLS certificate issuer will be iron-proxy CA rather than the real upstream issuer.
Customizing the Allowlist
Edit the domains array in the ironProxyConfig string before creating the snapshot:
transforms:
- name: allowlist
config:
warn: false
domains:
- "registry.npmjs.org"
- "pypi.org"
- "api.github.com"To update an existing deployment, re-run create-snapshot.mjs to produce a new snapshot ID, then update your application to use the new ID.
For the full set of configuration options, see the configuration reference.
Egress Control Limitations
Normally, you would use iptables to block all outbound traffic except from iron-proxy. This requires the xt_owner kernel module, which provides the --uid-owner match: it lets you write rules that allow traffic from a specific user (e.g., root, which iron-proxy runs as) while rejecting everything else. Freestyle VMs do not include xt_owner, so there is no straightforward way to distinguish traffic from iron-proxy vs. traffic from other processes. Any rule that blocks outbound connections would also block the proxy from reaching upstream servers.
Without iptables enforcement, iron-proxy’s egress control on Freestyle relies entirely on DNS interception: /etc/resolv.conf points at iron-proxy, which resolves domains to its own address and terminates TLS. If a process hardcodes an IP address or uses its own DNS resolver, that traffic will bypass the proxy.
In practice, this covers most workloads. Few applications connect to raw IP addresses. However, if you need to enforce strict egress control against adversarial code, you should use a platform that provides kernel-level egress control, or use Freestyle’s built-in network permissions.
Freestyle’s Built-In Egress Control
If you use Freestyle’s serverless runs , you can use Freestyle’s built-in network permissions instead of iron-proxy. Serverless runs support allow and deny rules that restrict which domains the run can access at the platform level. When an allow rule is specified, only whitelisted domains are accessible and all other requests are blocked. This is a simpler alternative if you do not need iron-proxy’s TLS interception, logging, or transform features.
Trusting the CA
The snapshot script trusts the CA system-wide and sets common runtime environment variables. Most tools will work without additional configuration. If you run into TLS errors, see the CA certificate reference for per-runtime details.
Troubleshooting
iron-proxy service is not running
Check the service logs:
journalctl -u iron-proxy --no-pager -n 50If the install oneshot failed, check that too:
journalctl -u install-iron-proxy --no-pager -n 50Common causes: network issues during the iron-proxy binary download, or a version string that does not match a GitHub release.
DNS resolution fails
Verify that /etc/resolv.conf points at 127.0.0.1:
cat /etc/resolv.confIf it was overwritten (e.g., by DHCP), the snapshot’s systemd-resolved disable may not have taken effect. Re-run the snapshot creation.
TLS certificate errors
If you see certificate signed by unknown authority, the CA is not trusted by the runtime making the request. Check that update-ca-certificates ran successfully during snapshot creation, and ensure the appropriate environment variable is set for your runtime. See CA Certificates for details.