ironsh/iron-proxy
GitHub: ironsh/iron-proxy
面向不可信工作负载的出口防火墙,通过默认拒绝策略和边界级机密注入,防止敏感数据外泄并实现完整的出站流量审计。
Stars: 211 | Forks: 6
# iron-proxy
[](https://hub.docker.com/r/ironsh/iron-proxy)
## 问题所在
CI 作业、AI 编码代理和沙箱容器可以发起任意的出站请求。受损的依赖项、提示词注入或恶意的构建步骤可能会窃取机密、回连主控端或打开反向 shell。大多数团队对其工作负载发出的内容毫无可见性,更不用说有任何方法来阻止它了。
## iron-proxy 的功能
iron-proxy 是一个内置 DNS 服务器的 MITM 出站代理,位于您的不可信工作负载和互联网之间。它在网络边界实施默认拒绝策略,因此工作负载只能访问您明确允许的域名。真实机密永远不会进入沙箱。工作负载使用代理令牌,iron-proxy 在出口处将其替换为真实凭证,这意味着受损的工作负载只能窃取在代理之外毫无价值的令牌。
单一二进制文件。单一 YAML 配置。
- **默认拒绝出站。** 每个出站请求都会被阻止,除非目的地与您的允许列表匹配。列出您的域名和 CIDR,其他所有内容都会收到 403。
- **边界级机密注入。** 工作负载发送代理令牌;iron-proxy 在请求离开之前将其替换为真实机密。如果沙箱受损,攻击者获得的令牌在代理之外毫无用处。
- **每次请求审计追踪。** 每个请求都以结构化 JSON 格式记录,包含完整的转换流水线结果:交换了哪些机密、匹配了哪些规则、阻止了什么以及原因。
- **流感知。** WebSocket 升级和 Server-Sent Events 原生代理。对于持有长期连接的代理工作负载,无需特殊配置。
专为 CI 流水线、GitHub Actions、AI 代理(Claude Code、Cursor、Codex)以及任何您运行不完全信任代码的环境而构建。
## 安装
Docker 镜像可在 [Docker Hub](https://hub.docker.com/r/ironsh/iron-proxy) 获取,Linux/macOS (amd64/arm64) 的预构建二进制文件可在 [GitHub Releases](https://github.com/ironsh/iron-proxy/releases) 获取。
或者从源代码构建:
```
go build -o iron-proxy ./cmd/iron-proxy
```
## 快速开始
```
cd examples/docker-compose
docker compose up
```
这将启动 iron-proxy 和一个通过代理发送五个请求的演示客户端。检查日志以查看被允许、被阻止和机密重写的请求:
```
docker compose logs proxy
```
每个请求都会生成一个结构化的 JSON 审计条目:
```
{
"host": "httpbin.org",
"method": "GET",
"path": "/headers",
"action": "allow",
"status_code": 200,
"duration_ms": 142,
"request_transforms": [
{ "name": "allowlist", "action": "continue" },
{
"name": "secrets",
"action": "continue",
"annotations": { "swapped": [{ "secret": "OPENAI_API_KEY", "locations": ["header:Authorization"] }] }
}
]
}
```
被拒绝的请求包含一个 `rejected_by` 字段并以 WARN 级别记录。有关完整架构,请参阅 [审计日志格式](#audit-log-format)。
## 生产环境使用
### 1. 生成 CA
iron-proxy 通过动态生成由您提供的 CA 签名的叶证书来终止 TLS。客户端容器必须信任此 CA。
```
mkdir -p certs
openssl genrsa -out certs/ca.key 4096
openssl req -x509 -new -nodes \
-key certs/ca.key \
-sha256 -days 3650 \
-subj "/CN=iron-proxy CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-addext "keyUsage=critical,keyCertSign" \
-out certs/ca.crt
```
### 2. 创建 Docker 网络
iron-proxy 需要一个固定 IP,以便容器可以将其 DNS 指向它:
```
docker network create --subnet=172.20.0.0/24 iron-proxy
```
### 3. 启动 iron-proxy
使用您的机密创建一个 env 文件(将其排除在版本控制之外):
```
echo "OPENAI_API_KEY=sk-real-key" > .env
```
```
docker run -d --name iron-proxy \
--network iron-proxy --ip 172.20.0.2 \
-v $(pwd)/proxy.yaml:/etc/iron-proxy/proxy.yaml:ro \
-v $(pwd)/certs/ca.crt:/etc/iron-proxy/ca.crt:ro \
-v $(pwd)/certs/ca.key:/etc/iron-proxy/ca.key:ro \
--env-file .env \
ironsh/iron-proxy:latest -config /etc/iron-proxy/proxy.yaml
```
### 4. 通过代理路由容器
最简单的方法是基于 DNS 的路由:将容器的 DNS 指向 iron-proxy,所有主机名查找都会解析为代理 IP,从而自动通过它路由流量:
```
docker run --rm \
--network iron-proxy \
--dns 172.20.0.2 \
-v $(pwd)/certs/ca.crt:/certs/ca.crt:ro \
curlimages/curl --cacert /certs/ca.crt https://httpbin.org/get
```
为了更强的执行力,叠加 nftables 规则以阻止非代理出口,或使用 TPROXY 进行内核级拦截。有关每种方法的详细信息,请参阅 [将流量路由到代理](#routing-traffic-to-the-proxy)。
## 为什么选择 iron-proxy?
| | iron-proxy | Squid | mitmproxy | Envoy |
| ------------------------ | ------------------------------ | --------------------------- | ------------------------- | ---------------------------------- |
| 默认拒绝出站 | 内置 | 需要复杂的 ACL 配置 | 需要自定义脚本 | 需要 RBAC/过滤器配置 |
| 机密注入 | 内置 | 否 | 否 | 否 |
| 结构化审计日志 | 内置,每次转换追踪 | 基本访问日志 | 基于插件 | 可配置的访问日志 |
| 设置复杂度 | 单一二进制文件 + YAML | 广泛的配置语言 | Python 脚本 | 复杂的 YAML 或控制平面 |
iron-proxy 是专为一项工作量身定制的:控制和审计来自不可信工作负载的出口。Squid 可以实现默认拒绝,但需要大量的 ACL 配置,并且没有机密注入的概念。mitmproxy 是一个很棒的调试工具,但并非为生产环境执行而设计。Envoy 是一个通用代理,可以配置为执行其中的部分功能,但它的复杂程度远超该问题所需。
## 工作原理
iron-proxy 运行一个 DNS 服务器和一个 HTTP/HTTPS 代理。将您的容器的 DNS 指向 iron-proxy,所有主机名查找都会解析为代理 IP,从而自动通过它路由流量。代理终止 TLS(使用您提供的 CA 动态生成叶证书),通过有序的转换流水线运行请求,将其转发到上游,并通过流水线传回响应。
```
Container → DNS lookup → iron-proxy IP → TLS termination → transforms → upstream
```
转换按顺序运行。内置转换:
| 转换 | 作用 |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| `allowlist` | 允许请求访问匹配的域名/CIDR;拒绝其他所有内容 (403)。 |
| `secrets` | 扫描标头、查询参数和可选的正文以查找代理令牌,并从环境变量中交换入真实机密。 |
## 配置
iron-proxy 接受一个标志:`-config path/to/config.yaml`。这是完整的形态(有关可复制粘贴的起点,请参阅 [`iron-proxy.example.yaml`](iron-proxy.example.yaml)):
```
dns:
listen: ":53"
proxy_ip: "10.16.0.1" # IP where iron-proxy is running (required)
passthrough: # Domains forwarded to OS resolver
- "*.internal.corp"
- "metadata.google.internal"
records: # Static DNS records (highest precedence)
- name: "internal.example.com"
type: A
value: "10.0.0.5"
proxy:
http_listen: ":80"
https_listen: ":443"
max_request_body_bytes: 1048576 # 1 MiB (default)
max_response_body_bytes: 0 # uncapped (default)
tls:
ca_cert: "/etc/iron-proxy/ca.crt" # Required
ca_key: "/etc/iron-proxy/ca.key" # Required
cert_cache_size: 1000 # LRU cache for generated leaf certs
leaf_cert_expiry_hours: 72
transforms:
- name: allowlist
config:
domains:
- "api.openai.com"
- "*.anthropic.com"
cidrs:
- "10.0.0.0/8"
- name: secrets
config:
source: env
secrets:
- var: OPENAI_API_KEY # Env var holding the real secret
proxy_value: "proxy-token-123" # Token the sandbox sends
match_headers: ["Authorization"]
match_body: false
hosts:
- name: "api.openai.com"
log:
level: "info" # debug, info, warn, error
```
### DNS
默认情况下,所有内容都解析为 `proxy_ip`,这就是通过代理路由流量的原因。例外情况:
- **`passthrough`:** 转发到 OS 解析器的 glob 模式(例如,`*.internal.corp`)。流向这些主机的流量完全绕过代理。
- **`records`:** 静态 A 或 CNAME 记录。最高优先级。
### Allowlist
默认拒绝。请求必须至少匹配一个域名 glob 或 CIDR 才能继续。不匹配的请求会收到 `403 Forbidden`。
域名模式使用 glob 匹配:`*.example.com` 匹配任何子域和 `example.com` 本身。
### Secrets
沙箱从不保存真实凭证。而是:
1. 在 iron-proxy 容器上将真实机密设置为环境变量。
2. 给沙箱一个代理令牌(例如,`proxy-openai-abc123`)。
3. 配置 `secrets` 转换以将代理令牌映射到环境变量。
iron-proxy 扫描出站请求,并在转发到上游之前将代理令牌替换为真实值。您可以控制它查找的位置:
- **`match_headers`:** 要扫描的标头名称列表。空列表 = 所有标头。
- **`match_body`:** 扫描请求正文(缓冲至 `max_request_body_bytes`)。
- **`hosts`:** 将交换限制为特定域名或 CIDR。
查询参数始终会被扫描。
### 正文限制
检查或转发请求/响应正文(机密正文匹配、gRPC 转换)的转换在缓冲的正文上运行。两个全局设置控制最大缓冲区大小:
- **`max_request_body_bytes`**(默认值:`1048576` / 1 MiB):限制为转换缓冲的请求正文量。超出此限制的数据从转换的角度会被截断,但仍会转发到上游。
- **`max_response_body_bytes`**(默认值:`0` / 不限制):限制为转换缓冲的响应正文量。设置为 `0` 以缓冲完整响应,这对于大多数工作负载(例如,npm 包、模型权重)来说是正确的默认值。
正文随着转换读取它们而增量缓冲,并在流水线阶段之间自动倒回。如果转换不读取正文,则不会发生缓冲,并且正文原封不动地流过。
### TLS
iron-proxy 动态生成由您提供的 CA 签名的叶证书。客户端容器必须信任此 CA(将其添加到系统信任存储或通过 `--cacert` 传递)。证书缓存在由 SNI 主机名键控的 LRU 缓存中。
## 将流量路由到代理
有三种方法,执行力依次增强。
### 基于 DNS(简单)
将容器的 DNS 指向 iron-proxy。所有查找都解析为代理 IP,因此 HTTP/HTTPS 流量自然地流经它。这就是 [Docker Compose 示例](#docker-compose-example) 使用的方法:
```
services:
client:
dns:
- 172.20.0.2 # iron-proxy IP
```
易于设置但易于绕过:工作负载可以硬编码 IP 或使用其自己的 DNS 解析器完全跳过代理。
### DNS + nftables 出口防火墙(强制执行)
在 DNS 路由之上叠加 nftables 防火墙。DNS 仍然将流量引导至代理,但 nftables 确保工作负载_无法_与其他任何内容通信,即使使用硬编码 IP 也是如此。
[`examples/nftables`](examples/nftables/) 目录包含一个有效的设置。客户端容器在运行任何应用程序流量之前在启动时加载防火墙规则:
**nftables.conf** 允许流量流向代理,丢弃其他所有内容:
```
table ip iron {
chain output {
type filter hook output priority 0; policy drop;
# allow loopback
oif lo accept
# allow traffic to the proxy itself (DNS + HTTP/HTTPS)
ip daddr 172.20.0.2 tcp dport { 80, 443 } accept
ip daddr 172.20.0.2 udp dport 53 accept
# allow established/related (return traffic)
ct state established,related accept
# log and drop everything else
log prefix "iron-proxy-drop: " drop
}
}
```
**docker-compose.yml:** 客户端镜像预先安装了 nftables。入口点加载规则,然后运行演示。加载规则需要 `CAP_NET_ADMIN`:
```
services:
proxy:
# ... same as DNS example ...
networks:
demo:
ipv4_address: 172.20.0.2
client:
build:
context: .
dockerfile: Dockerfile.client # alpine + curl + nftables
dns:
- 172.20.0.2
cap_add:
- NET_ADMIN
volumes:
- ./nftables.conf:/etc/nftables.conf:ro
- certs:/certs:ro
networks:
demo:
ipv4_address: 172.20.0.4
```
在生产设置中,您需要在入口点包装器中加载规则,然后在没有 `CAP_NET_ADMIN` 的情况下以非 root 用户 `exec` 您的实际进程。
### TPROXY(透明代理)
对于您根本无法控制工作负载 DNS 的环境,nftables TPROXY 可以在内核级别重定向流量,而无需工作负载的任何配合。这会在 PREROUTING 链中拦截数据包,并直接将其移交给 iron-proxy:
```
table ip iron {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
# redirect HTTP/HTTPS to iron-proxy via TPROXY
tcp dport 80 tproxy to 172.20.0.2:80 meta mark set 1 accept
tcp dport 443 tproxy to 172.20.0.2:443 meta mark set 1 accept
}
chain output {
type route hook output priority mangle; policy accept;
# mark locally-originated packets for policy routing
tcp dport { 80, 443 } meta mark set 1
}
}
```
这需要 `ip rule` 和 `ip route` 设置来将标记的数据包路由到本地套接字,并且 iron-proxy 必须使用 `IP_TRANSPARENT` 进行绑定。这设置起来更复杂,但提供了流量无法绕过代理的最强保证。TPROXY 在 DNS 之下运行,因此它会捕获硬编码 IP、自定义解析器以及工作负载可能尝试的任何其他内容。
## Docker Compose 示例
[`examples/docker-compose`](examples/docker-compose/) 目录包含一个有效的设置。关键部分:
**docker-compose.yml:** 代理和客户端位于共享桥接网络上。真实机密仅作为环境变量在代理容器上设置:
```
services:
proxy:
build:
context: ../..
dockerfile: examples/docker-compose/Dockerfile
environment:
- OPENAI_API_KEY=sk-real-openai-key-do-not-share
- INTERNAL_TOKEN=real-internal-secret-value
volumes:
- certs:/certs
networks:
demo:
ipv4_address: 172.20.0.2
client:
image: alpine:latest
dns:
- 172.20.0.2 # Point DNS at the proxy
volumes:
- certs:/certs:ro
networks:
demo:
ipv4_address: 172.20.0.4
```
**proxy.yaml** 允许列表包括 `httpbin.org` 和 `icanhazip.com`,交换两个机密:
```
transforms:
- name: allowlist
config:
domains:
- "httpbin.org"
- "icanhazip.com"
cidrs:
- "172.20.0.0/24"
- name: secrets
config:
source: env
secrets:
- var: OPENAI_API_KEY
proxy_value: "proxy-openai-abc123"
match_headers: ["Authorization"]
hosts:
- name: "httpbin.org"
- var: INTERNAL_TOKEN
proxy_value: "proxy-internal-tok"
match_headers: [] # scan all headers
hosts:
- name: "httpbin.org"
```
客户端脚本发送五个请求以演示每种行为:
```
# 1. 允许的请求
curl https://httpbin.org/get
# 2. 阻止的请求 (不在 allowlist 中)
curl https://example.com/
# 3. Secret swap: Authorization header 中的 proxy token 替换为真实密钥
curl -H "Authorization: Bearer proxy-openai-abc123" https://httpbin.org/headers
# 4. Secret swap: 自定义 header 中的 proxy token
curl -H "X-Internal: proxy-internal-tok" https://httpbin.org/headers
# 5. Secret swap: 查询参数中的 proxy token
curl "https://httpbin.org/get?token=proxy-openai-abc123&q=hello"
```
## 审计日志格式
每个代理请求都会生成一个结构化的 JSON 日志条目:
```
{
"host": "httpbin.org",
"method": "GET",
"path": "/headers",
"action": "allow",
"status_code": 200,
"duration_ms": 142,
"request_transforms": [
{
"name": "allowlist",
"action": "continue"
},
{
"name": "secrets",
"action": "continue",
"annotations": {
"swapped": [{ "secret": "OPENAI_API_KEY", "locations": ["header:Authorization"] }]
}
}
],
"response_transforms": []
}
```
被拒绝的请求包含一个 `rejected_by` 字段并以 WARN 级别记录。
## iron.sh
需要 Vault/KMS 机密后端、Kubernetes 运算符或集中式策略管理?[iron.sh](https://iron.sh) 在 iron-proxy 之上构建,为大规模运行的团队提供企业功能。
标签:AI 代码代理安全, CI/CD 安全, DLL注入, DNS 服务器, Docker, EVTX分析, GitHub Actions, IP 地址批量处理, JSONLines, Web截图, 中间人代理, 依赖混淆防护, 凭证注入, 出口防火墙, 反向外壳防护, 安全防御评估, 容器安全, 底层编程, 提示词注入防护, 数据投毒防御, 数据防泄露, 日志审计, 沙箱, 流量审计, 网络安全, 自动笔记, 请求拦截, 隐私保护, 零信任, 默认拒绝策略