- 人工智能
- AI 应用
- 交互助手
- AI Agent
【免费下载链接】ironclaw
IronClaw is an Agent OS focused on privacy, security and extensibility
本文基于 IronClaw 仓库中 2026-08-19 sandbox egress spike 调研文档 及同目录下的全部结果文件整理而成,并结合
ironclaw_sandbox领域源码对设计落地做了对照印证。读者将掌握:如何用--internal网络 + 双宿主机iron-proxy侧车为每个线程容器构建“唯一出口”,如何验证 DNS 拦截、MITM 允许列表、默认拒绝与凭证脱敏,以及 curl/git/Node/Python/pip 各运行时信任自签 CA 的差异与docker exec进程管理的坑。
IronClaw 的沙箱化执行以“每线程一个命令容器”为核心拓扑。为了让这些容器既不能直连公网、又不能访问云元数据地址,同时仍能按策略访问允许列表内的站点,IronClaw 在 2026-08-19 进行了一次代号#7732的 Throwaway spike(一次性验证实验):使用纯 Docker 原生命令与库存镜像(无任何 Rust 改动),验证“internal 网络 + 双宿主机代理侧车”的出口拓扑是否成立。该 spike 共验证 9 个关键假设,全部获得证据支撑,并产出了 8 条对后续 Step 1 实现具有直接指导意义的工程结论。
一、Spike 目标与总体判定
本次 spike 的拓扑设想是:命令容器挂在一个--internalDocker 网络上(该网络没有外部路由),唯一的出口是一个同时挂载 internal 与 egress 两个网络的iron-proxy侧车容器。客户端的所有 DNS 查询与 HTTP/HTTPS 流量都必须经过该代理,由代理基于允许列表决定放行或拒绝。
Spike 环境为macOS + OrbStack,Docker server 29.4.0,代理镜像固定为:
ironsh/iron-proxy@sha256:c4628019c24f4cc8d77564a26b7c9cedb00accee6f93d06270e85fb8f9c6a7da9 项验证的最终判定如下(完整证据见 results-topology-dns.md、results-policy-credentials.md、results-tls-trust.md、results-exec-mechanics.md):
| # | 验证项 | 判定 | 证据文件 |
|---|---|---|---|
| 1 | internal 网络 + 双宿主机代理拓扑 | PASS | results-topology-dns.md |
| 2 | --dns <proxy>经 Docker 内嵌 DNS 生效 | PASS | results-topology-dns.md |
| 3 | 经 MITM(spike CA)的允许列表 TLS 出口 | PASS | results-policy-credentials.md |
| 4 | 默认拒绝 403 + 结构化 JSON 审计 | PASS | results-policy-credentials.md + audit-denial-example.jsonl |
| 5 | 直连出口死亡(公网、元数据 IP、无默认路由) | PASS | results-topology-dns.md |
| 6 | 占位符→真实凭证替换、require: true、主机绑定、日志无真实字面量 | PASS(4/4 子检查) | results-policy-credentials.md |
| 7 | 逐运行时 TLS 信任矩阵(curl/git/node/python/pip) | PASS | results-tls-trust.md |
| 8 | docker exec流/杀进程/僵尸/延迟机制 | PASS with findings | results-exec-mechanics.md |
| 9 | internal 网络无 IPv6 路由 | PASS | results-topology-dns.md |
此外,spike 明确记录了参与镜像的不可变身份,并诚实标注了证据边界:alpine:3.20与alpine/openssl:latest两个历史 tag 没有记录运行时摘要(RepoDigest),因此这两个输入的精确复现是证据缺口,后续 spike 必须在执行前记录RepoDigests。文中的real-secret-value-42是确定性本地替换 canary(哨兵值),并非真实运营凭证。
二、标准复现配方与 CA 生成
Spike 文档给出了可复用的标准配方。要点是:并发实例必须使用唯一子网(用 N 区分),以免网络冲突。
docker network create --internal --subnet 172.28.N.0/24 <pfx>-int docker network create --subnet 172.29.N.0/24 <pfx>-egress docker run -d --name <pfx>-proxy --network <pfx>-int --ip 172.28.N.2 \ -v $DIR/proxy.yaml:/etc/iron-proxy/proxy.yaml:ro \ -v $DIR/ca.crt:/etc/iron-proxy/ca.crt:ro -v $DIR/ca.key:/etc/iron-proxy/ca.key:ro \ ironsh/iron-proxy@sha256:c4628019c24f4cc8d77564a26b7c9cedb00accee6f93d06270e85fb8f9c6a7da \ -config /etc/iron-proxy/proxy.yaml docker network connect <pfx>-egress <pfx>-proxy docker run -d --name <pfx>-client --network <pfx>-int --dns 172.28.N.2 alpine:3.20 sleep 900其中<pfx>-int(internal)与<pfx>-egress两个网络分别承担“命令容器面”与“公网出口面”,代理固定 IP 如172.28.N.2,同时作为客户端 DNS 指向。CA 材料每次运行临时生成、刻意不提交仓库。
CA 生成有一个重要经验:macOS 宿主上的 LibreSSL 行为异常,必须改用容器内的 OpenSSL 实现:
docker run --rm -v $DIR:/work -w /work alpine/openssl genrsa -out ca.key 4096 docker run --rm -v $DIR:/work -w /work alpine/openssl req -x509 -new -nodes \ -key ca.key -sha256 -days 3650 -subj /CN=iron-proxy-spike-CA \ -addext basicConstraints=critical,CA:TRUE -addext keyUsage=critical,keyCertSign \ -out ca.crt生成的 CA 经容器化openssl x509校验:subject=CN=iron-proxy-spike-CA、issuer=CN=iron-proxy-spike-CA、4096 位 RSA、criticalCA:TRUE、criticalCertificate SignKeyUsage。代理启动日志确认了三个监听面与变换管线:
running 0 {"level":"INFO","msg":"upstream deny list active","cidrs":["169.254.169.254/32","fd00:ec2::254/128","127.0.0.0/8","::1/128"]} {"level":"INFO","msg":"iron-proxy starting","dns_listen":":53","http_listen":":80","https_listen":":443","metrics_listen":":9090"} {"level":"INFO","msg":"transform pipeline","transforms":"allowlist"}值得注意:upstream deny list默认就把云元数据地址169.254.169.254/32、IPv6 链路本地元数据fd00:ec2::254/128、回环地址等纳入拒绝 CIDR,这与项目“防止沙箱窃取云实例凭证”的定位一致。
三、拓扑与 DNS 验证:唯一出口确实成立
3.1 双宿主机拓扑(Item 1)
docker inspect证实代理同时挂载两个网络,而命令面网络是 internal:
{"s7a-egress":{"Gateway":"172.29.10.1","IPAddress":"172.29.10.2",...},"s7a-int":{"Gateway":"","IPAddress":"172.28.10.2",...}} trueInternal=true且 internal 网络无网关,说明该网络上没有任何外部路由。
3.2--dns <proxy>经内嵌 DNS 生效(Item 2)
客户端/etc/resolv.conf显示nameserver 127.0.0.11(Docker 内嵌 DNS),但其注释明确记录了转发目标:ExtServers: [172.28.10.2]。实测中,包括故意不存在的anything.invalid在内,所有名字都解析到代理 IP:
Server: 127.0.0.11 Address: 127.0.0.11:53 Name: example.com Address: 172.28.10.2 Name: anything.invalid Address: 172.28.10.2这意味着即使客户端尝试访问未允许域名,DNS 也会把它送到代理,由代理在 7 层决定放行或拒绝——拓扑“按设计工作”。
3.3 直连出口死亡(Item 5)
internal 网络上的客户端ip route只有一行:
172.28.10.0/24 dev eth0 scope link src 172.28.10.3没有默认路由。显式直连尝试全部失败(Network unreachable,EXIT=1),包括http://1.1.1.1、TCP 443 到1.1.1.1、以及云元数据地址169.254.169.254:80。作为阳性对照,走代理访问http://example.com返回 200,证明失败并非拓扑整体故障,而是直连路径确实被切断。
3.4 无 IPv6 路由(Item 9)
客户端仅有 IPv6 回环地址,ip -6 route无任何输出,连接 Cloudflare IPv6 DNS2606:4700:4700::1111失败。Docker 29.4.0 的docker infoGo 模板不暴露.IPv6字段(模板求值直接报错),因此正确做法是逐网络检查EnableIPv6——本 spike 的两个网络均为false。
四、策略与审计:允许列表、默认拒绝与凭证脱敏
4.1 MITM 允许列表出口(Item 3)
客户端使用--cacert /ca.crt访问https://example.com得到 200;-v详细输出证实完整链路:
* Host example.com:443 was resolved. * IPv4: 172.28.20.2 * Server certificate: * subject: CN=example.com * issuer: CN=iron-proxy-spike-CA * subjectAltName: "example.com" matches cert's "example.com" * OpenSSL verify result: 0 < HTTP/1.1 200 OK即:DNS 把客户端送到代理 → 代理为example.com动态签发由 spike CA 签发的叶子证书 → 客户端信任该 CA 完成验证 → 代理放行上游请求。代理审计日志同时记录mode: mitm, action: allow, status_code: 200。
4.2 默认拒绝与结构化审计(Item 4)
访问未允许域名https://denied.invalid/返回403,且 verbose 输出证明这是代理拒绝而非 DNS 失败(连接成功建立到代理 IP)。代理随后输出结构化 JSON 审计记录,完整样例见 audit-denial-example.jsonl:
{"time":"2026-08-19T11:26:00.564519806Z","level":"WARN","msg":"request","audit":{"host":"denied.invalid","method":"GET","path":"/","remote_addr":"172.28.20.4:34558","sni":"denied.invalid","mode":"mitm","action":"reject","status_code":403,"duration_ms":0.005},"rejected_by":"allowlist","request_transforms":[{"name":"allowlist","action":"reject","duration_ms":0.001}]}审计记录包含rejected_by字段与逐变换的request_transforms轨迹,这为后续 Step 1 的审计可观测性(trace 溯源)提供了直接模板。
4.3 占位符→真实凭证替换(Item 6,4/4 子检查通过)
这是本 spike 最具“产品价值”的验证:代理在出站请求中把占位符 token 替换为真实凭证,同时保证真实凭证永不落日志。工作配置见 proxy.credentials.yaml,核心是secrets变换:
transforms: - name: allowlist config: domains: ["example.com", "capture.test", "other.test"] - name: secrets config: secrets: - source: { type: env, var: REAL_TOKEN } proxy_value: "icsbx_spike_placeholder_123" match_headers: ["Authorization"] require: true rules: [{ host: "capture.test" }]四个子检查全部通过:
- 6a 占位符仅对绑定主机替换:向
capture.test发送Authorization: Bearer icsbx_spike_placeholder_123,echo 服务收到的授权头已变为Bearer real-secret-value-42; - 6b
require: true拒绝替代 token:向绑定主机发送some-other-token得到 403,审计记录rejected_by: secrets并带 secrets 变换拒绝注解; - 6c 同一占位符在未绑定主机原样保留:向
other.test发送相同占位符,echo 显示授权头仍是icsbx_spike_placeholder_123,未被替换; - 6d 真实凭证零次出现在代理日志:对完整代理日志做字面量、区分大小写的搜索,
real-secret-value-42命中次数为 0;成功替换的审计只记录变量名与位置:
{"name":"secrets","action":"allow","annotations":{"swapped":[{"secret":"REAL_TOKEN","locations":["header:Authorization"]}]}}spike 也诚实标注了边界:只测试了字面量精确匹配,未测试编码、转义或变换后的表示。
4.4 测试专用覆盖项的警示
proxy.credentials.yaml中的upstream_deny_cidrs: []是仅限测试的覆盖项:它允许代理访问私有 echo 容器地址(capture.test/other.test),生产环境必须保留默认拒绝 CIDR。这一警示与 3.2 节中代理启动日志默认激活的 deny list 相互印证。
五、逐运行时 TLS 信任矩阵(Item 7)
允许列表 MITM 的前提是各运行时信任 spike CA。spike 使用nikolaik/python-nodejs:python3.12-nodejs22(摘要sha256:88c41488c1…,内含 curl 8.14.1 / git 2.47.3 / Node v22.23.2 / Python 3.12.13 / pip 26.1.2)实测得出关键矩阵:
| 运行时 | 系统证书库即可? | 需要额外环境变量? | 说明 |
|---|---|---|---|
| curl | 是 | 否 | 安装前也可用--cacert /spike/ca.crt |
| git HTTPS | 是 | 否 | 浅克隆只实际访问了github.com |
Nodefetch | 否 | 是 | 必须NODE_EXTRA_CA_CERTS=/spike/ca.crt |
Pythonurllib.request | 是 | 否 | 3.12.13 使用 Debian 系统库 |
| pip 26.1.2 | 是(意外) | 否 | CA 入系统库后无环境变量即可;SSL_CERT_FILE=/spike/ca.crt为可用备选 |
几个关键实验细节:
- 未信任前 curl 返回
(60) SSL certificate problem: self-signed certificate in certificate chain(EXIT=60);--cacert显式指定后 200; cp /spike/ca.crt /usr/local/share/ca-certificates/spike.crt && update-ca-certificates后 curl、git、Python 均通过系统库信任;git 浅克隆github.com/octocat/Hello-World成功,代理日志仅记录 3 个对github.com的 smart-HTTP 请求(info/refs+ 2 个git-upload-pack),更深的克隆 / LFS 需要完整资产主机星座(codeload、objects.githubusercontent 等,它们在允许列表里但本 fixture 未用到);- Node v22 忽略系统证书库,即使
update-ca-certificates成功,fetch("https://example.com")仍报SELF_SIGNED_CERT_IN_CHAIN;设置NODE_EXTRA_CA_CERTS=/spike/ca.crt后返回 200; - pip 26.1.2 是“意外惊喜”:CA 进入系统库后无需任何环境变量即可
pip download six;控制实验(移除系统库中的 spike CA)证明失败确实来自系统信任缺失,而仅用SSL_CERT_FILE也能成功。
由此得出 Step 1 的两条明确工程指令:
- 不要全局设置
SSL_CERT_FILE指向仅含代理 CA 的 bundle——它会影响所有运行时且覆盖面不对称;应采用合并 CA bundle,或将覆盖范围限定到确实需要的运行时(Node); - worker 镜像必须校验
NODE_EXTRA_CA_CERTS指向的路径真实存在。
六、docker exec进程与流机制(Item 8)
该任务回答“每线程容器 + 每次命令一次 exec”经济模型下的关键问题。测试容器为alpine:3.20,PID 1 是刻意选择的不回收(non-reaping)命令sleep 1800。
6.1 后台子进程继承 stdout 的“挂起”假设未复现
原假设是“后台子进程继承 stdout 会导致 exec 无限挂起”。在 Docker 29.4 上未复现:sh -c 'sleep 300 & echo done'返回done且约2.08 秒完成(而非 20 秒超时)。结论:该版本下继承流只带来约 2 秒代价,不产生挂起。
6.2 重定向足以立即关闭流,setsid 用于杀进程树
- 仅重定向:
setsid之前先测sleep 300 >/dev/null 2>&1 </dev/null & echo done→ 0.067 秒完成; - 重定向 +
setsid:0.127 秒完成; - 因此
ironclaw-exec需要两者兼得:重定向负责流即时关闭(延迟),setsid负责形成独立进程组以便组杀。
6.3 组杀与僵尸
- 退出码传播精确(
exit 7→code=7); - BusyBox 语法坑:
kill -TERM -- -34被 Alpine BusyBoxsh内建拒绝(sh: invalid number '--',kill_code=1),正确写法是kill -TERM -34(kill_code=0); - 组杀信号确实送达:PGID 34 的 shell 与两个 sleep 全部从
S(sleeping)变为Z(zombie),而无关的分离进程(PGID 17)不受影响; - 严格判定下该子检查 FAIL:由于 PID 1 不回收,被杀进程全部残留为僵尸,无法满足“进程条目消失”的严格条件。5 秒后僵尸仍存在,僵尸累积被直接演示。worker 镜像必须引入 tini 或等效的 reaping init,否则每次组杀都会堆积僵尸。
6.4 exec 延迟基准
5 次docker exec ... true实测:60 / 40 / 30 / 30 / 30 ms,均值 38 ms、中位数 30 ms。这为“每线程容器 + 每命令一次 exec”的开销提供了量化依据。
七、与 IronClaw 源码的落地印证
该 spike 的结论已经在ironclaw_sandbox领域代码中落地,可从源码佐证其工程化方向:
- managed_egress.rs 中以常量钉死代理镜像摘要(
ironsh/iron-proxy@sha256:c462…,与 spike 完全一致),并强制要求镜像必须按 sha256 摘要 pin、pull 后必须校验不可变镜像 ID(对应 spike 的“记录 RepoDigests”要求); - 代理配置与材料统一挂载到宿主机侧
/run/ironclaw-proxy/目录(proxy.yaml、ca.crt、ca.key、credentials.json、invocation-id),并定义PROXY_LABEL_PREFIX = "ironclaw.proxy"标签前缀用于识别侧车容器; - 代码中暴露给用户的 CA 路径为
/run/ironclaw/egress-ca.crt(USER_PROXY_CA_PATH),与 spike 中“把 spike CA 挂进客户端供各运行时信任”的做法一脉相承; - 策略侧,
network_allowlist.rs与managed_egress.rs共同保证“托管出口策略必须至少包含一个允许目标、必须拒绝私网 IP 段”,对应 spike 中默认 deny list 与upstream_deny_cidrs测试覆盖项的生产警示。
八、Step 1 落地清单(Spike 结论汇总)
综合全部结果,Step 1 实现应满足以下要求:
- 每线程命令容器挂在
--internal网络,无默认路由;唯一的出口是双宿主机iron-proxy侧车; - 客户端
--dns <proxy-ip>指向代理,借助 Docker 内嵌 DNS 将所有名字(含.invalid)统一送到代理; - 代理默认拒绝非允许列表域名(403),并输出含
rejected_by与变换轨迹的结构化 JSON 审计; - 凭证注入采用“占位符 → 真实值”的主机绑定替换,
require: true防替代 token,真实值永不落日志; - worker 镜像信任代理 CA:系统证书库覆盖 curl/git/python/pip,Node 必须显式
NODE_EXTRA_CA_CERTS;禁止全局SSL_CERT_FILE指向仅代理 CA 的 bundle; ironclaw-exec同时使用全重定向(流即时关闭)与setsid(进程组隔离),组杀采用 BusyBox 兼容写法kill -TERM -<PGID>,镜像引入 tini 级 reaping init 防止僵尸累积;- 网络层同时关闭 IPv6 路由,避免 internal 网络出现第二条出口;
- 复现实验必须记录全部输入镜像的不可变摘要(RepoDigests),并保留所有审计与日志证据。
九、结论
这是一次证据链完整、边界标注诚实的工程 spike:9/9 假设通过(其中“组杀后进程消失”在非 reaping PID 1 下按严格标准判为 FAIL,属于“信号生效但僵尸残留”的机制性发现),为 IronClaw 的“每线程容器唯一受控出口”架构扫清了从设计到落地的关键风险。文档中所有命令块保持“当时实际执行”的原貌,未用事后拉取的摘要改写历史证据,这种可审计性本身就是值得复用的实验方法论。若需复现或深化,完整配方、配置样例与证据文件均可从 spike 目录 直接获取。
- 人工智能
- AI 应用
- 交互助手
- AI Agent
【免费下载链接】ironclaw
IronClaw is an Agent OS focused on privacy, security and extensibility
相关推荐
IronClaw Sandbox 出口网络拓扑验证:基于 Docker internal 网络与双网卡 iron-proxy 的 DNS/egress spike 剖析
IronClaw Sandbox 出口网络拓扑验证:基于 Docker internal 网络与双网卡 iron proxy 的 DNS/egress spik
人工智能AI 应用交互助手AI AgentIronClaw 沙箱出口策略验证:基于 iron-proxy 的默认拒绝、TLS 拦截与凭据占位符交换实战
IronClaw 沙箱出口策略验证:基于 iron proxy 的默认拒绝、TLS 拦截与凭据占位符交换实战 本篇技术指南围绕 IronClaw 开源仓库中的沙
人工智能AI 应用交互助手AI AgentIronClaw 沙箱出口 Spike 实录:通过 iron-proxy 实现 per-runtime TLS 信任矩阵(curl / git / Node / Python / pip)
IronClaw 沙箱出口 Spike 实录:通过 iron proxy 实现 per runtime TLS 信任矩阵(curl / git / Node /
人工智能AI 应用交互助手AI Agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考