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

+46 -4
View File
@@ -24,6 +24,7 @@ timeouts 模块)、门禁类(preflight fail-fast、容量门、磁盘
→ sandbox_policy 模块)。见 PR 描述的复用清单。
"""
import os
import re
@@ -65,7 +66,8 @@ def build_chv_config(params, env):
cpus = str(params.get("cpus") or env.get("CHV_CPU") or "2")
memory = params.get("memory") or env.get("CHV_MEM") or "2G"
state_dir = params.get("state_dir") or env.get("CHV_STATE_DIR") \
or "/tmp/chv-vms"
or os.path.join(os.path.expanduser("~"),
".local", "share", "codex-app-server", "chv-vms")
ssh_key = params.get("ssh_key") or env.get("CHV_SSH_KEY")
if params.get("ssh_user") is not None \
and params.get("ssh_user") == "":
@@ -117,15 +119,49 @@ def build_chv_config(params, env):
raise ValueError(
"CHV_DISK_MODE 只能是 copy 或 reuse:%s" % disk_mode)
disk_gb_raw = env.get("CHV_DISK_GB")
disk_gb = float(disk_gb_raw) if disk_gb_raw else None
# feat-chv-secfix(M3):配额默认开启。CHV_DISK_GB 未设时
# 不再是 None(配额门直接返回),而是 50 GB 总量预算;
# 显式设 0 或负数同样非法——配额门是 fail-closed,不是
# “设个小值绕过”。check_disk_quota 仍接受 None(直接
# 调用者的第二道门),但执行器路径永远带默认值。
try:
disk_gb = float(disk_gb_raw) if disk_gb_raw else 50.0
except ValueError:
raise ValueError("CHV_DISK_GB 必须是数字")
if disk_gb <= 0:
raise ValueError("CHV_DISK_GB 必须大于 0")
egress_proxy = env.get("CHV_EGRESS_PROXY")
allow_direct_egress = env.get("CHV_ALLOW_DIRECT_EGRESS") == "1"
host_key_mode = params.get("host_key_mode") \
or env.get("CHV_HOST_KEY_MODE") or "keyscan"
or env.get("CHV_HOST_KEY_MODE") or "provisioned"
if host_key_mode not in ("provisioned", "keyscan"):
raise ValueError(
"CHV_HOST_KEY_MODE 只能是 provisioned 或 keyscan:%s"
% host_key_mode)
# feat-chv-secfix(M6):reuse 实验放行须同时声明单租户
# 项目(部署声明 + 审计,代码无法从环境变量验证数据库
# 里是否真的只有一个租户——这一点在文档与 preflight 的
# kind 里明写,不假装已校验)。
single_tenant_project = env.get("CHV_SINGLE_TENANT_PROJECT")
# feat-chv-secfix(M3 快照上限 / L2 快照目录白名单):
# 快照数、总量、保留期均有默认值;file:// 快照只允许在
# state_dir 或 CHV_SNAPSHOT_DIR 内,远端协议须显式放行
# (见 hardening.validate_snapshot_url)。
try:
snapshot_max_count = int(env.get("CHV_SNAPSHOT_MAX_COUNT")
or 10)
snapshot_max_total_gb = float(
env.get("CHV_SNAPSHOT_MAX_TOTAL_GB") or 20.0)
snapshot_retention_days = float(
env.get("CHV_SNAPSHOT_RETENTION_DAYS") or 30.0)
except ValueError:
raise ValueError("CHV_SNAPSHOT_* 必须是数字")
if snapshot_max_count < 0 or snapshot_max_total_gb < 0 \
or snapshot_retention_days < 0:
raise ValueError("CHV_SNAPSHOT_* 不能为负数")
snapshot_dir = env.get("CHV_SNAPSHOT_DIR")
allow_remote_snapshot = env.get("CHV_ALLOW_REMOTE_SNAPSHOT") \
== "1"
task_timeout_raw = env.get("CHV_TASK_TIMEOUT")
task_timeout = float(task_timeout_raw) \
if task_timeout_raw else None
@@ -166,4 +202,10 @@ def build_chv_config(params, env):
host_key_mode=host_key_mode,
task_timeout=task_timeout, snapshot_url=snapshot_url,
warm_pool_size=warm_pool_size, api_timeout=api_timeout,
ready_timeout=ready_timeout, ready_interval=ready_interval)
ready_timeout=ready_timeout, ready_interval=ready_interval,
single_tenant_project=single_tenant_project,
snapshot_max_count=snapshot_max_count,
snapshot_max_total_gb=snapshot_max_total_gb,
snapshot_retention_days=snapshot_retention_days,
snapshot_dir=snapshot_dir,
allow_remote_snapshot=allow_remote_snapshot)