feat(chv): 安全审计修复 High/Medium/Low(堆叠于 #239)

审计报告 ~/workspace/chv-security-audit/REPORT.md(No-Go,
Critical 0 / High 7 / Medium 8 / Low 5)代码层修复,mock
测试覆盖,真机动态验证一律未做、只列清单(见
docs/chv-production-readiness.md §11)。

High:
- H1 egress 代码门:代理地址校验、直连显式允许+审计、
  每 VM 独立 tap/mac/ip 校验、net 字段白名单;宿主防火墙
  列为部署清单,代码门+防火墙缺一不可。
- H2 网关 key 移出 argv:provisioned seed 盘客内文件
  /etc/chv/guest-env(0600)投递,传输层密钥进 argv 直接
  ValueError;keyscan 模式密钥投递 fail-closed。
- H3 任务盘删除:stop/recycle/cleanup 删 disk.raw(保留期
  0)+ 残留盘 GC,删失败审计+脏标记。
- H4 默认值改 provisioned:keyscan 只作显式 opt-in。
- H5 seed.iso 生成即 0600;宿主私钥 seed 盘建好后即删;
  崩溃残留扫描 cleanup_stale_secrets。
- H6 state_dir 默认出 /tmp(~/.local/share/.../chv-vms,
  preflight 保 0700);api-socket 组/其它可写即 fail-fast。
- H7 tap 隔离校验:net.json 登记+stop 释放,共享即拒绝审计。
Medium:M1 shlex 远程转义;M2 清理失败即拒起(脏标记);
M3 配额默认 50GB + 快照数/总量/保留期上限;M4 SHA256 必填
+ BUILDINFO versions 实测记录(事前 lockfile 列清单);
M5 host key/ preflight/失败审计补齐;M6 reuse 须声明单租户
项目;M7 容量计数只认连通 socket;M8 sshd 加固进 build.sh
断言。Low:L1 链接不跟随;L2 路径白名单;L3 失败审计;
L4 双变量警告+字段白名单;L5 预热盘/任务盘对应关系定义。
旧测试:实验室默认值与生产默认值区分标注;行为已变的旧
测试按新语义更新(容量死 socket、快照白名单、磁盘路径
白名单、网关 argv、reuse 租户声明)。
测试:新 test_chv_secfix 39 项;旧 117 项 + 2 项新增断言 =
158 项;worker 相关 129 项;前端 vitest 9 项;cloud 全量
1588 项(skipped 4)。
This commit is contained in:
host committed 2026-10-31 03:02:10 +00:00
1 parent 003ea357f9
commit baf26beb78
16 files changed
+2106 -132

No files matched your search

+187 -9
View File
@@ -41,7 +41,10 @@
密钥目录;客机代码在镜像内 `/app`,不在宿主共享挂载上。
- ✅🧪 密钥不进镜像/代码/prompt:SSH argv 只有 key 文件路径;
模型 key 由客机 cloud-init 预置的环境变量名注入;测试断言
命令中不含哨兵密钥值。
命令中不含哨兵密钥值。feat-chv-secfix 起网关密钥不再走
SSH env 前缀(旧路径值进 argv 已废除),改走 provisioned
seed 盘客内文件(/etc/chv/guest-env,0600),keyscan 模式
下密钥投递 fail-closed;审计 detail 里绝无密钥值。
- ✅🧪 SSH host key 按 VM 钉死(优化轨,#216 的 no + /dev/null
已废除):每 VM 在 state_dir/<vm>/known_hosts 有独立文件,
首次启动经 ssh-keyscan 在解析出的 SSH 目标上捕获 key 写入该文件,
@@ -55,8 +58,9 @@
cloud-init seed(随 VM 关闭/回收而删),客机镜像硬性
要求见 chv-guest/README.md §5(cloud-init NoCloud、
首启先应用 seed 再起 sshd、不得无条件重生成 host key)。
ssh-keyscan 首次捕获只保留为弱回落(CHV_HOST_KEY_MODE
=keyscan),生产部署必须用 provisioned 模式;TOFU 首次
ssh-keyscan 首次捕获只保留为弱回落(须显式设
CHV_HOST_KEY_MODE=keyscan),生产默认值已是 provisioned
(feat-chv-secfix,H4,此前默认值 keyscan 是反的);TOFU 首次
真实性仍待预置 key 方案在真机上验证,安全审计时须单独立项确认。
- 🔒 需安全审计:VM 逃逸面(virtio 设备、tap 桥接、SSH 配置)、
客机 root 提权面、租户间东西向隔离的宿主网络配置,须由
@@ -97,6 +101,9 @@
shutdown + 杀进程 + 删 socket)、recycle_vm(保留
task_checkpoints/task_attempts,丢失 guest /tmp 与未拷出
的 worktree 临时物,绝不声称恢复临时文件)。
feat-chv-secfix 起 stop 即删任务 disk.raw(保留期 0,
需取证须在 shutdown 前经 SSH 拷出),残留盘由
gc_stale_disks 扫描清理,删失败记审计 + 脏标记。
- ✅🧪 execute 只读已运行 VM(优化轨):execute() 先查 VM
是否在运行(登记活进程或 api-socket 存在),未运行报
ChvFailure(kind=vm_not_running),不为一次 status 起 VM,
@@ -131,8 +138,12 @@
## 5. 资源与成本
- ✅🧪 每 VM 配额:CPU/内存进 VmConfig(CHV_CPU/CHV_MEM),
磁盘配额门(CHV_DISK_GB,超限拒绝),宿主容量门
(CHV_MAX_VMS,超限明确拒绝排队,不降级)。
磁盘配额门(CHV_DISK_GB,默认 50 GB 总量预算、超限拒绝,
feat-chv-secfix 起默认开启,此前未设即不查),快照上限
(每 VM 数量默认 10、总量默认 20 GB、保留期默认 30 天,
超量最旧优先剪枝),宿主容量门
(CHV_MAX_VMS,超限明确拒绝排队,不降级;残留 socket
须真连通才计数,死 socket 不再占满容量)。
- ✅ 超时与最大轮次:SSH 单次超时(CHV_TASK_TIMEOUT)+
worker task_timeout 外层护栏;goal 最大轮次 100(与 Local
同款),执行器内部不重试(重试是 worker max_retries 语义)。
@@ -152,9 +163,15 @@
- ✅🧪 受控 egress:默认 fail-closed(CHV_EGRESS_PROXY 未设且
未显式允许直连时 preflight 直接失败);客机命令加 env
代理前缀,出口只走白名单代理(Tinyproxy 思路复用)。
feat-chv-secfix 措辞纠正:env 前缀只是代码门,不是网络层
隔离——客内 root 可 unset/裸 socket 绕过;真正的隔离是
“代码门 + 宿主防火墙缺一不可”(见 §11 部署清单),
代码不再把 fail-closed 写成已隔离。
- ⏳🔒 需宿主网络配置 + 安全审计:tap 桥接是否真的只路由到
白名单代理、VM 能否绕过代理直连外网,是宿主网络配置问题,
执行器代码不能自证,必须在 KVM 宿主上实测并审计。
代码已做:每 VM 独立 tap/mac/ip 校验(共享即拒绝并审计)、
net 条目字段白名单、egress 代理地址校验。
- ✅ 审批门:任务 approval 经 `_approval_cli_args` 进客内 host.py
顶层参数,VM 内 agent 不能自改;delete/deploy/spend 类动作
仍走 server 的 approval/merge_gate,不由 VM 内 agent 自批
@@ -169,7 +186,12 @@
- ✅🧪 VM 事件审计:create/boot/ready/pause/resume/snapshot/
restore/shutdown/destroy/failed/recycled/warm_pool_acquire/
release 均经 audit_callback 发出;worker 接线后转 AuthStore
审计(_audit 本身 best-effort)。
审计(_audit 本身 best-effort)。feat-chv-secfix 起补齐:
host_key_generated/host_key_captured/host_key_mismatch、
preflight_failed、wake 前置失败(容量/配额/net 冲突)、
vm_disk_deleted/vm_disk_delete_failed/vm_disk_gc、
host_secret_residue_cleaned、net_config_dual、快照剪枝;
审计回调失败记宿主 error 日志,不再双重静默。
- ✅🧪 失败分类:ChvFailure 带 kind(kvm_missing/binary_missing/
kernel_missing/image_missing/ssh_key_missing/cpu_flag_missing/
egress_not_configured/api_timeout/guest_not_ready/vm_create_failed/
@@ -194,7 +216,9 @@
- ✅🧪 多 VM 并行:`_vms` 登记 + 锁保护,wake 按任务名登记,
不同任务互不覆盖;容量门在 wake 前检查。
- ✅🧪 残留 VM 清理:cleanup_stale_vms 处理已死/已关/socket
残留三种情况,只清 chv- 前缀 socket。
残留三种情况,只清 chv- 前缀 socket;feat-chv-secfix 起
清理前先跑残留盘 GC 与残留密钥扫描(不依赖 socket 是否
存在,覆盖 worker 崩溃后 socket 已删、文件仍在的情形)。
- ✅🧪 容量不足明确拒绝:kind=host_capacity_exceeded,调用方
排队/拒绝,绝不降级 Docker/Local(测试断言 launcher 未调用)。
- 📈 需压测:多 VM 并行下的宿主容量、VM 启动并发上限、残留
@@ -236,8 +260,9 @@
- ✅🧪 P2 已做(范围已恢复为全做,上一条收窄令作废):
test_chv_gaps.py 的 grep 文本断言改成真实 Worker._run_attempt
行为断言;stop_vm 删除失败记 vm_key_cleanup_failed 审计
(同名复用旧 key 的彻底根治“清理失败即拒起”列为遗留,不
假装已根治);配置校验时机统一到 preflight(CPU/内存非法
(同名复用旧 key 的彻底根治在本轨之前列为遗留——
feat-chv-secfix 起已根治:清理失败写脏标记,同名 VM
拒绝启动,见 §11 M2);配置校验时机统一到 preflight(CPU/内存非法
进 preflight,kind=config_invalid);build.sh 与 README 口径
对齐(章节顺序 §5 host key → §6 对接、BUILDINFO contents
纠正为名字列表无逐项版本、Node.js 不装写实);第二启动路径
@@ -247,3 +272,156 @@
小提取(preflight 的 reuse/CPU/内存校验块抽成
_preflight_config_checks,语义不变;config.py 字段解析与
其余长函数深耦合,本轨如实停、不硬拆)。
## 11. 安全审计修复轨 feat-chv-secfix(堆叠于 #239 之上)
> 审计报告:`~/workspace/chv-security-audit/REPORT.md`(只读静态
> 审计,verdict No-Go,Critical 0 / High 7 / Medium 8 / Low 5)。
> 本节逐项给结论:代码已做 = 本轨代码 + mock 测试已覆盖;
> 真机未验证 = 代码改了,但动态实效仍须 KVM 宿主验证,
> 绝不把静态通过写成动态已验。审计 verdict 本身仍是 No-Go,
> 直到 §11 末的真机清单全部通过——本轨只修代码层,不改 verdict。
### 11.1 High(全部代码已做、真机未验证)
- H1 egress 网络层强制:代码能做的全部做了——egress 代理
地址校验(http/https + 主机名)、直连仍须显式允许 + 审计、
每 VM 独立 tap/mac/ip 校验(共享即拒绝并审计)、net 条目
字段白名单。代码做不到的宿主防火墙规则见 §11.4 部署清单,
文档明写“代码门 + 宿主防火墙缺一不可”;env 前缀不再被
写成已隔离。
- H2 网关 key 移出 argv:密钥子集(SUB2API_API_KEY)在
provisioned 模式下随 seed 写进客内 /etc/chv/guest-env
(0600 root),远程命令 source 该文件;传输层对密钥名
进 argv 直接 ValueError(fail-closed)。比 argv 安全的理由:
值既不在宿主进程表(ps /proc cmdline)里,也不在宿主子
进程环境(/proc environ)里,只活在 seed.iso(0600,随
VM 销毁而删)与客机内——客内 agent 本来就有该 key,修
的是“宿主暴露面”,不是“客内可见性”。与 Docker 线的差异:
Docker 是“-e 只传名、值由 CLI 子进程环境提供”(值仍在
宿主进程环境里),CHV 是“值只进 seed 客内文件”(宿主
进程环境里也没有),比 Docker 线更严。keyscan 模式无
seed 可投递,有密钥即 fail-closed(secret_env_requires_
provisioned),不再将就跑。残留清理:seed 目录与 seed.iso
随 stop 而删,崩溃残留由 cleanup_stale_secrets 扫描。
- H3 任务盘删除:stop/recycle/cleanup 路径删 disk.raw
(保留期 0,需取证须在 shutdown 前经 SSH 拷出),删失败
记 vm_disk_delete_failed 审计 + 脏标记;gc_stale_disks
扫描非 live VM 的残留盘并审计。
- H4 默认值改 provisioned:CHV_HOST_KEY_MODE 默认
provisioned,keyscan 只作显式 opt-in;旧测试的
CHV_SSH_FORWARD=1 等实验室宣称已明确标注为实验室路径,
不再写成生产默认。
- H5 seed.iso 权限与私钥清理:seed.iso 生成即 0600;宿主
私钥在 seed 盘建好后即删(只留公钥 + known_hosts),删
失败 fail-fast(VM 尚未启动);私钥 chmod 失败不再静默;
启动/清理时 cleanup_stale_secrets 扫描残留私钥与 seed。
- H6 state_dir 出 /tmp:默认
~/.local/share/codex-app-server/chv-vms,preflight 建目录
并保证 0700(失败即 state_dir_insecure);api-socket 组/
其它可写即 fail-fast(socket_permissions_insecure;仅禁
写位,VMM 默认 0755 类权限不误杀——VMM 默认值代码控制
不了,真机权限仍须按清单核对)。
- H7 tap 隔离校验:NET_JSON 透传前校验 tap/mac/ip 跨 VM
不重复(wake 成功后登记 net.json,stop 时释放预约),
共用即拒绝并审计;东西向隔离的宿主防火墙部分见 §11.4,
不写进代码假装已防。
### 11.2 Medium(每项有修复或有理由的结论)
- M1 SSH 远程命令转义:远程 token 全部 shlex.quote 后仍以
列表返回(ssh 拼接后远程解析结果与本地 list 语义一致),
prompt 里的 ;/$()/引号不再改写执行语义。边界写清:客内
已是 root,本项防的是语义篡改与审计失真,不是客内提权。
- M2 清理失败同名复用旧 key:彻底根治,选“清理失败即拒起”
(脏标记 + provision/wake 双重拒绝),不再写“已审计即可”。
- M3 磁盘配额默认与快照上限:CHV_DISK_GB 默认 50 GB
(未设不再不查,0/负数拒绝);快照每 VM 数量默认 10、
总量默认 20 GB(超帽最旧优先剪枝)、保留期默认 30 天。
- M4 供应链版本钉死:已钉死——云镜像 SHA256 必填(干跑验
“已设”、全量验哈希)、Ubuntu 版本与 codex 版本均为必填
环境变量;BUILDINFO 新增 versions(构建时从客机读回的
dpkg + pip freeze 实际版本,可审计、可复现比对)。未钉死
的清单:apt/pip 事前精确版本 lockfile(须在构建宿主上
生成一次、再固化进脚本,本轨无构建宿主、未编造版本号);
Playwright Chromium 二进制版本同样只随 playwright pip
版本记录,未单独钉死。
- M5 审计补齐:host_key_generated/host_key_captured/
host_key_mismatch(stderr 含钉死拒绝标记时立即 fail-fast,
不轮询成普通超时)、preflight_failed、wake 前置失败
(容量/配额/net 冲突)均独立审计;审计回调失败记宿主
error 日志,不再双重静默。worker 层 _audit 本身仍
best-effort(未改,见 §7 既定语义)。
- M6 reuse 实验标志单租户校验:代码能校验的已校验——
实验放行须同时声明 CHV_SINGLE_TENANT_PROJECT,否则
preflight 拒绝(reuse_tenant_undeclared);不能校验的
部分明写:代码无法验证数据库里是否真的只有一个租户,
声明 + 审计就是本项的机制,不假装已校验。
- M7 容量计数残留 socket 误伤:只有真能连通的 socket 才
计数,空文件/死 socket 不计入 CHV_MAX_VMS;竞态窗口
写清(刚启动未 listen 的 VMM 会被判死,容量门是护栏
不是分布式锁)。
- M8 sshd 加固进构建:build.sh 写入
/etc/ssh/sshd_config.d/99-chv.conf(PasswordAuthentication no、
PermitRootLogin prohibit-password、ChallengeResponseAuthentication no、
KbdInteractiveAuthentication no、PubkeyAuthentication yes),
并经 sshd -t + grep 断言;README 声称与构建一致。
### 11.3 Low(顺手做)
- L1 磁盘拷贝符号链接:目标是链接时先删链接本身再拷贝,
不跟随写入目标文件(TOCTOU 窗口已在 disk.py 写清,
state_dir 0700 是前提)。
- L2 磁盘/快照路径白名单:task_disk/seed_disk 须在
state_dir 内(reuse 的基础镜像本身除外);file:// 快照
须在 state_dir 或 CHV_SNAPSHOT_DIR 内,远端协议须显式
CHV_ALLOW_REMOTE_SNAPSHOT=1。
- L3 失败审计:preflight/容量/配额/net 冲突失败均发
vm_failed 审计(见 M5)。
- L4 CHV_NET 与 CHV_NET_JSON 双变量:双设时发
net_config_dual 警告审计(不硬拒,CHV_NET 是手动直接
启动命令的透传值);NET_JSON 条目字段白名单
(tap/mac/ip/mask),未知字段直接拒绝。
- L5 WarmPool 预热盘与任务盘对应关系:定义已写进
pool.acquire 文档——预热盘是干净快照盘,不是任务盘;
将来接线时任务盘仍须按任务重新供给或校验快照来源与
任务的对应关系;接线本身仍未做,不假装已接。
### 11.4 宿主防火墙部署清单(代码做不到的部分,缺一不可)
- 每 VM 独立 tap + 独立 mac + 独立静态 ip(运维分配,
代码在 wake 时校验冲突,但分配本身是运维责任);
- tap 桥接默认 DROP,只放行到 egress 代理的流量
(iptables/nftables:默认 DROP + 仅代理地址/端口 ACCEPT,
不给 tap 直连外网的 NAT,不把 tap 桥接到宿主内网网段);
- 东西向隔离:VM 间默认禁通(每 VM 独立 tap + 桥接隔离,
或每 tap 独立网段 + 转发禁令),ARP spoof 场景下 SSH
钉死仍是第二道门,不是唯一门;
- 宿主进程隔离:worker 用户最小化、VMM api-socket 所在
state_dir 0700 且仅 worker 用户可访、宿主启用 hidepid
收紧 /proc 可见性、进程表采集不记录完整 cmdline;
- 部署后核对:state_dir 权限、socket 实际权限、
tap 规则实测(见 §11.5 否定测试)。
### 11.5 仍需真机验证的清单(本轨一律未做,不声称已验)
1. 否定测试:客内 unset 代理后 curl 外网必须不通;
VM A 扫 VM B / spoof B 的 ip 后 SSH 必须被钉死拒绝;
2. provisioned 首启时序:seed 先应用再起 sshd、不重生成
host key;sshd -T 实际输出与密码登录关闭;
3. 宿主 SSH argv 实测:真机 ps 下网关 key 不可见;
4. 长跑/崩溃注入:残留盘总量、快照增长、残留 socket 与
seed/私钥残留的实际表现、GC 覆盖;
5. 压测:配额与容量门在并发下的实际表现;
6. 渗透类:任何以“拿到宿主 shell / 读到别租户数据”为
目标的真实攻击尝试——本轨一律未做,静态通过不等于
已证明不可逃逸;
7. 审计 verdict:以上全部通过后,才能把 verdict 从 No-Go
改为“有条件启用(默认关闭 + 灰度单租户/受信项目)”,
不直接全量多租户。
### 11.6 本轨明确不做
- VM 控制按钮、预热池管理页、成本/配额新页面(前端零改动);
- 真机动态渗透、压测(无 KVM,做不了,只列清单);
- WarmPool dispatch 前查池接线(仍未接线,见 L5 定义)。