ironsh/iron-proxy

GitHub: ironsh/iron-proxy

面向不可信工作负载的出口防火墙,通过默认拒绝策略和边界级机密注入,防止敏感数据外泄并实现完整的出站流量审计。

Stars: 211 | Forks: 6

# iron-proxy [![Docker Pulls](https://img.shields.io/docker/pulls/ironsh/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)以及任何您运行不完全信任代码的环境而构建。
Blocked exfiltration + secret rewriting in action:

## 安装 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截图, 中间人代理, 依赖混淆防护, 凭证注入, 出口防火墙, 反向外壳防护, 安全防御评估, 容器安全, 底层编程, 提示词注入防护, 数据投毒防御, 数据防泄露, 日志审计, 沙箱, 流量审计, 网络安全, 自动笔记, 请求拦截, 隐私保护, 零信任, 默认拒绝策略