Files
codex-app-server/cloud/chv/ssh_transport.py
T
host 9771ed9e8c feat: CHV 审核修复(P0+P1,堆叠于 #230)
P0:seed 磁盘 CIDATA ISO 挂进 VmConfig 第二块只读盘;
restore provisioned 下复用钉死 known_hosts、缺失 fail-fast
绝不 keyscan;preflight 拦截 CHV_DISK_MODE=reuse(显式
实验二次确认 + 审计放行);SSH 目标选客机 IP 方案(显式
host > net 静态 ip > 显式转发宣称回环,三者皆无 fail-fast)。
P1:网关 extra_env 经 SSH env 前缀注入、worker 守卫收窄到
Local/CHV;取消/清理/看门狗接 CHV 通用路径;WarmPool
acquire 内 resume;容量门持久化计数;磁盘配额总量+余量
与已存在磁盘校验重拷;API 按 Content-Length 验完整性;
快照 file:// 验落盘再 shutdown;execute 死端口归类
vm_not_running。
P2 全部排除未做(用户范围收窄令,留待以后一轨)。
测试:原 84 + 新增 29 = 113 项,mock,无真 VM。
2026-10-31 01:54:13 +00:00

108 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Cloud Hypervisor 客机 SSH 传输(叶子模块,不依赖
cloud.sandbox,避免循环导入)。
安全模型(本轨的核心变化,替代 #216 的
StrictHostKeyChecking=no + UserKnownHostsFile=/dev/null):
- 每 VM 一个独立 known_hosts 文件:
state_dir/<vm 名>/known_hosts(与每任务磁盘同目录,
任务销毁时不自动删——known_hosts 留在宿主 state_dir 里,
供事后审计该 VM 当时钉死的 key;VM 名本身已含任务 id,
不会串到别的 VM 上)。
- 首次信任(TOFU,边界必须写清,不假装是强验证):
首次启动时客机 host key 未知,执行器用 ssh-keyscan 经
宿主转发端口(127.0.0.1:<port>,只走本机回环)抓一次
当时呈现的 key,写入该 VM 的 known_hosts 文件;此后
同一 VM 的每次 SSH 都用 StrictHostKeyChecking=yes +
该文件校验,VM 被替换 / 中间人换 key 时 SSH 直接拒绝,
不再静默接受。首次捕获本身不验证“这台客机就是我们要
的那台”——它只防首次之后的替换;更强的方案是镜像构建
/cloud-init 时把客机 host key 预置进 known_hosts(未来
工作,见 docs/chv-production-readiness.md 的待办),本轨
在 mock 测试里只能验证“捕获→写文件→后续命令带钉死
选项”的管线,绝不声称 mock 验证了首次密钥的真实性。
- 密钥红线不变:argv 里只有 key 文件路径,绝无 key 值;
known_hosts 文件只含 host key 公钥行,不含任何私钥。
"""
import os
def known_hosts_path(state_dir, vm_name):
"""该 VM 的 known_hosts 文件路径:state_dir/<vm>/known_hosts。"""
return os.path.join(state_dir, vm_name, "known_hosts")
def build_ssh_command(ssh_port, ssh_key, ssh_user, egress_proxy,
known_hosts_file, inner_cmd, ssh_host="127.0.0.1",
extra_env=None):
"""构造 SSH 命令:ssh -p <端口> -i <key 文件> + 钉死选项
+ user@<ssh_host> + 客内命令。
feat-chv-fix(P0-4):ssh_host 不再在传输层写死回环,
由执行器解析传入(显式 CHV_SSH_HOST / net 客机 IP /
显式转发宣称下的回环);本函数缺省回环仅为旧调用点
兼容,执行器路径绝不依赖该缺省。
extra_env(feat-chv-fix,P1-1 网关注入):经同一 env
前缀进客机环境(与 egress 代理前缀同款机制),不进
镜像/代码/prompt;值本身只出现在宿主侧 ssh argv(与
worker 同一信任边界),执行器绝不把完整命令记入日志。
egress_proxy 已设时客内命令前加 env 前缀(代理地址本身
不是密钥;客机出口只走该白名单代理)。"""
inner = list(inner_cmd)
env_assignments = []
if extra_env:
for key in sorted(extra_env):
env_assignments.append("%s=%s" % (key, extra_env[key]))
if egress_proxy:
env_assignments += [
"HTTPS_PROXY=%s" % egress_proxy,
"HTTP_PROXY=%s" % egress_proxy,
"NO_PROXY=localhost,127.0.0.1"]
if env_assignments:
inner = ["env"] + env_assignments + inner
return ["ssh", "-p", str(ssh_port),
"-i", ssh_key,
"-o", "StrictHostKeyChecking=yes",
"-o", "UserKnownHostsFile=%s" % known_hosts_file,
"-o", "BatchMode=yes",
"%s@%s" % (ssh_user, ssh_host)] + inner
def parse_keyscan_output(text, host="127.0.0.1"):
"""从 ssh-keyscan 的 stdout 里挑出目标主机的 host key
行:形如 "[<host>]:<port> ssh-ed25519 AAAA..."(-p 时
的标准写法)或 "<host> ssh-rsa AAAA..."。其它主机行、
注释行、空行一律丢掉,绝不把陌生主机的 key 写进文件。
host 缺省回环仅为旧调用点兼容;客机 IP 方案下应传
解析出的客机 IP。"""
kept = []
for line in (text or "").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) < 3:
continue
line_host, keytype = parts[0], parts[1]
if line_host != host \
and not line_host.startswith("[%s]:" % host):
continue
if not keytype.startswith("ssh-"):
continue
kept.append(line)
return kept
def write_known_hosts(path, key_lines):
"""原子写 known_hosts:先写同目录临时文件再 rename,
文件权限 0600(只含公钥行,仍按敏感文件对待)。"""
directory = os.path.dirname(path)
if directory:
os.makedirs(directory, exist_ok=True)
tmp_path = path + ".tmp"
with open(tmp_path, "w") as fh:
fh.write("\n".join(key_lines) + "\n")
os.chmod(tmp_path, 0o600)
os.replace(tmp_path, path)