引言:为什么自治 AI Agent 需要“进程级”零信任沙箱?
在企业级 AI Agent 的落地演进中,Red Hat 与 Anthropic 提出了一个极具代表性的架构范式:“Outsource the thinking, keep the doing(将推理外包给云端大模型,将代码执行留在本地受控基础设施)”。
然而,当我们在本地运行一个具备 bash 执行与文件读写权限的自治 Agent 时,传统的 Docker/Podman 容器隔离面临三个致命的安全盲区:
- 凭证暴露(Credential Exposure):如果将
OPENAI_API_KEY或GITHUB_TOKEN通过环境变量注入容器,一旦 Agent 遭遇提示词注入(Prompt Injection),恶意指令只需一行env或读取/proc/self/environ即可窃取明文密钥。 - 同域子进程外传(Same-Origin Exfiltration via Subprocess):传统容器防火墙(如 K8s NetworkPolicy 或
iptables)工作在 L3/L4 层,只能判断“哪个容器/Pod 发起了连接”。如果策略允许容器访问api.github.com,被劫持的 Python Agent 只需派生一个/usr/bin/curl子进程,即可借用合法的白名单域名将本地敏感代码打包外传。 - 策略演进失控(Unverified Policy Drift):当沙箱拦截了未知请求并生成策略放行提案时,缺乏静态形式化验证机制会导致攻击者诱导审批流放宽路径通配符(如将
/v1/chat/completions扩权为/v1/*)或暴露云主机元数据端点(169.254.169.254)。
为了彻底解决上述痛点,NVIDIA 开源了 OpenShell——一个专为自治 AI Agent 设计的零信任安全沙箱运行时。原版仓库包含 36 个 Rust Crates、数万行代码,涵盖了 gRPC 控制面、多种容器驱动(Docker/Podman/K8s/MicroVM)、动态 TLS MITM 签发与 OCSF 遥测。
秉承 “大道至简” 的系统工程哲学,本文将展示如何对 OpenShell (v0.1.5) 进行架构脱水(Architecture Pruning),剥离外围工程样板,仅用 约 700 行零第三方 C 扩展依赖的 Python 代码,从零手搓一个五脏俱全的最小完备实现——nano-openshell,并跑通全部 19 项红蓝对抗回归测试。
一、架构裁剪与源码映射:从 36 个 Crates 到 4 个核心原语
阅读大型系统源码最忌讳陷入旁支细节。对照 NVIDIA 官方《OpenShell Architecture (v0.1.5)》文档与源码树,我们将整个系统的安全边界收敛为 4 个正交的核心原语,并严格遵循官方的跨边界职责切分(Inside vs. Outside the Sandbox Boundary):
+-----------------------------------------------------------------------------------+
| 隔离边界内 (Sandbox Boundary: CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWNS) |
| |
| +------------------------+ 127.0.0.1 Proxy +-------------------+ |
| | Agent / 子进程工作负载 | ---------------------------> | sandbox.py | |
| | (/usr/bin/python3, | | (边界内可信观察者) | |
| | /usr/bin/curl) | <--- SIGSTOP (断连瞬间冻结) -- | 1. 查 /proc 溯源 | |
| +------------------------+ | 2. 绝不做策略决策 | |
| +---------+---------+ |
+---------------------------------------------------------------------|-------------+
|
Mediated Channel (UDS + X-Nano-Channel-Token)
[启动握手锁 Generation ID + 透传可信 BinaryIdentity]
|
+---------------------------------------------------------------------v-------------+
| 隔离边界外 (Trusted Host Boundary) |
| |
| +-------------------------------------------------------+ |
| | supervisor.py (唯一策略执行点 PEP & 进程外凭证注入器) | |
| | 1. 校验 Mediated Token 防绕过 | |
| | 2. 调用 policy.py 执行 L4 -> Binary -> L7 三层评估 | |
| | 3. 放行时从宿主机内存动态注入 Authorization: Bearer | ---> 上游 API 服务 |
| | 4. 拒绝时通过 DenialAggregator 生成候选策略提案 | |
| +---------------------------+---------------------------+ |
| | 输出 denials.json & policy-proposal.yaml |
| v |
| +-------------------------------------------------------+ |
| | prover.py (静态策略边界形式化验证器) | |
| | 校验 5 大安全不变量:云元数据防护 / 凭证外发新域名 / | |
| | 未授权 L4 逃逸 / 二进制扩权 / L7 Method-Path 放宽 | |
| +-------------------------------------------------------+ |
+-----------------------------------------------------------------------------------+
原版源码与 nano-openshell 裁剪映射表
| 核心原语 | 原版 OpenShell 源码模块 (crates/) | nano-openshell 保留的核心机制 | 剥离的工程噪音(砍掉的部分) |
|---|---|---|---|
| 1. Policy Schema & Per-Binary Engine | openshell-policy-schema (yaml.rs)、openshell-binary-identity (ProcfsIdentityResolver)、openshell-policy (l7_validate.rs, rest.rs) | 默认拒绝(Default-Deny)三段式过滤流水线:① L4 校验(host + port);② Binary 校验(allowed_binaries,支持 /proc/[pid]/exe 软链接 realpath 归一化与 sha256: 前缀匹配);③ L7 校验(HTTP methods + paths 通配符防 .. 路径穿越及关联凭证模板渲染) | OPA (Rego) 外部引擎 (opa.rs)、GraphQL/MCP/JSON-RPC 协议解析器、多策略树合并 (merge.rs) |
| 2. Isolated Sandbox & Mediated Observer | openshell-sandbox (workload_launcher.rs, boundary_server.rs, network_broker.rs, process_signal.rs) | ① 职责边界:与 Agent 同处 CLONE_NEWUSER | CLONE_NEWNET 内,绝不加载策略或做决策;② 可信进程身份采集:在沙箱内反查 /proc/self/net/tcp → socket inode → /proc/[pid]/fd → /proc/[pid]/exe;③ Fail-Closed 握手与冻结:启动前校验 generation_id,控制流断开时立即向 Agent 进程组发送 SIGSTOP | Docker/Podman/K8s/Libkrun 驱动、SSH/SFTP 服务端 (sftp.rs)、cBPF SECCOMP_RET_USER_NOTIF 异步文件代打开 |
| 3. Out-of-Process Supervisor | openshell-supervisor (proxy.rs, http.rs, identity.rs, provider.rs, denial_aggregator.rs) | ① Mediated 鉴权:校验 X-Nano-Channel-Token,防止 Agent 绕过 sandbox.py 直连 UDS 伪造身份头;② 进程外凭证代填:白名单放行时动态注入宿主机内存中的 REAL_OPENAI_KEY;③ Structured Denial Intelligence:拒绝时按五元组去重写入 denials.json 并自动合成候选提案 policy-proposal.yaml | 动态 MITM TLS CA 签发 (tls.rs)、SPIFFE/AWS SigV4 签名 (sigv4.rs)、自定义虚拟 DNS 服务、OCSF 日志导出 |
| 4. Minimal Policy Prover | openshell-prover (containment.rs, credentials.rs, queries.rs, google_cloud_metadata.rs) | 对比基准策略 policy.yaml 与候选提案 policy-proposal.yaml,基于双探测串通配符包含性算法静态阻断 5 类越界风险:云元数据暴露、凭证外发扩权、未知 L4 端点逃逸、二进制扩权、L7 路径放宽 | CIDR 区间树证明器 (ip.rs)、跨进程祖先调用链证明 (execution.rs) |
最终的工程目录结构严格控制在 5 个源码文件 + 1 个回归测试脚本:
nano-openshell/
├── policy.yaml # [配置] 基准零信任策略声明
├── policy.py # [Milestone 1] BinaryIdentity 解析器与 L4/Binary/L7 评估引擎 (~150 行)
├── supervisor.py # [Milestone 2] 边界外 Supervisor、凭证代填与 DenialAggregator (~215 行)
├── sandbox.py # [Milestone 3] 边界内 Namespace 隔离、/proc 溯源与 SIGSTOP 看门狗 (~215 行)
├── prover.py # [Milestone 4] 静态策略包含性证明器与五大阻断规则 (~155 行)
└── test_regression.sh # [验收] 覆盖 4 大阶段、19 项断言的端到端自动化回归测试脚本
二、Milestone 1:Per-Binary 策略引擎与软链接/路径归一化 (policy.py)
1. 核心设计陷阱与防御手段
在实现三层策略评估器时,有三个极易被忽视的工程细节决定了安全边界的成败:
- 避免规则遮蔽(No Rule-Shadowing)的集合收缩:如果同一域名针对不同二进制(例如
python3与git)配置了多条规则,简单的线性遍历一旦在第一条规则的 Binary 或 L7 阶段失败就会提前终止。因此必须采用分阶段集合收缩:先过滤出满足 L4 的规则子集l4_rules→ 再从中过滤出满足调用方二进制身份的子集bin_rules→ 最后匹配l7_rules。 /proc/[pid]/exe软链接归一化:在现代 Linux 发行版中,/usr/bin/python3通常是指向/usr/bin/python3.12或/usr/bin/python3.14的软链接。当内核通过/proc/[pid]/exe暴露进程身份时,返回的是解引用后的真实 ELF 路径。为此,BinaryIdentity同时记录调用路径path与os.path.realpath(path),并在匹配时支持双向归一化与fnmatch通配。- 防御
/tmp二进制伪造与 L7..路径穿越:- 若策略配置了
"*/python3*"以兼容uv或 Conda 虚拟环境,攻击者可通过cp /usr/bin/curl /tmp/python3_fake伪造路径骗过通配符。因此引擎强制拒绝任何realpath位于/tmp/、/var/tmp/、/dev/shm/等临时可写目录下的可执行文件。 - 在 L7 路径匹配前,强制调用
posixpath.normpath()对 URL 路径进行归一化,彻底封死通过GET /repos/../admin绕过/repos/*白名单的路径穿越攻击。
- 若策略配置了
2. 基准策略配置 (nano-openshell/policy.yaml)
version: 1
default_action: deny
network_policies:
- name: openai_agent_inference
hosts:
- "api.openai.com"
- "api.openai.local"
- "127.0.0.1"
ports: [443, 80, 18080]
# Per-Binary Network Policy: 仅允许 Python Agent 运行时访问,阻止派生的 curl/wget/sh
allowed_binaries:
- "/usr/bin/python3*"
- "*/python3*"
l7_rules:
- methods: ["GET"]
paths: ["/v1/models"]
- methods: ["POST"]
paths: ["/v1/chat/completions"]
credential:
env_var: "REAL_OPENAI_KEY"
header: "Authorization"
value_template: "Bearer ${REAL_OPENAI_KEY}"
- name: github_public_readonly
hosts:
- "api.github.com"
ports: [443]
allowed_binaries:
- "/usr/bin/python3*"
- "/usr/bin/git"
l7_rules:
- methods: ["GET"]
paths: ["/repos/*"]
3. 策略引擎实现 (nano-openshell/policy.py)
#!/usr/bin/env python3
"""
Nano-OpenShell Milestone 1: Per-Binary Policy Schema & L4/Binary/L7 Policy Engine.
"""
from __future__ import annotations
import dataclasses
import fnmatch
import hashlib
import json
import os
import posixpath
import sys
import urllib.parse
from typing import Any, Optional
import yaml
UNTRUSTED_EXEC_PREFIXES = ("/tmp/", "/var/tmp/", "/dev/shm/")
@dataclasses.dataclass
class BinaryIdentity:
"""沙箱内发起网络请求的进程真实二进制身份 (对齐 openshell-binary-identity)"""
path: str
realpath: str
sha256: str
pid: Optional[int] = None
@classmethod
def from_pid(cls, pid: int) -> "BinaryIdentity":
exe_link = f"/proc/{pid}/exe"
realpath = os.path.realpath(exe_link)
return cls.from_path(realpath, pid=pid)
@classmethod
def from_path(cls, path: str, pid: Optional[int] = None) -> "BinaryIdentity":
realpath = os.path.realpath(path) if os.path.exists(path) else path
sha256 = ""
if os.path.isfile(realpath):
h = hashlib.sha256()
with open(realpath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
h.update(chunk)
sha256 = h.hexdigest()
return cls(path=path, realpath=realpath, sha256=sha256, pid=pid)
@dataclasses.dataclass
class Decision:
"""策略评估结果与结构化拒绝情报 (对齐 DenialEvent)"""
allowed: bool
stage: str # ALLOWED | L4_HOST_PORT | BINARY_IDENTITY | L7_HTTP
reason: str
rule_name: Optional[str] = None
inject_headers: dict[str, str] = dataclasses.field(default_factory=dict)
credential_env: Optional[str] = None
denial_record: Optional[dict[str, Any]] = None
class PolicyEngine:
def __init__(self, policy_data: dict[str, Any]) -> None:
self.raw = policy_data
self.version = policy_data.get("version", 1)
self.default_action = policy_data.get("default_action", "deny")
self.rules: list[dict[str, Any]] = policy_data.get("network_policies", [])
canonical = json.dumps(self.raw, sort_keys=True).encode("utf-8")
self.generation_id = f"gen-{hashlib.sha256(canonical).hexdigest()[:12]}"
@classmethod
def from_yaml_file(cls, path: str) -> "PolicyEngine":
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or {}
return cls(data)
@staticmethod
def _match_binary(binary: BinaryIdentity, patterns: list[Any]) -> bool:
# 拦截位于临时可写目录 (/tmp, /var/tmp, /dev/shm) 的伪造二进制
for candidate_path in (binary.path, binary.realpath):
norm_cand = posixpath.normpath(candidate_path)
if any(norm_cand.startswith(prefix) for prefix in UNTRUSTED_EXEC_PREFIXES):
return False
if not patterns:
return True
candidates = {binary.path, binary.realpath}
for pat in patterns:
if isinstance(pat, str):
if pat.startswith("sha256:"):
prefix = pat.removeprefix("sha256:").lower()
if binary.sha256 and binary.sha256.lower().startswith(prefix):
return True
else:
pat_real = os.path.realpath(pat) if os.path.exists(pat) else pat
if any(
fnmatch.fnmatch(c, pat) or fnmatch.fnmatch(c, pat_real)
for c in candidates
):
return True
elif isinstance(pat, dict):
p_glob = pat.get("path", "*")
sha_prefix = pat.get("sha256", "").removeprefix("sha256:").lower()
path_ok = any(fnmatch.fnmatch(c, p_glob) for c in candidates)
sha_ok = (not sha_prefix) or (
bool(binary.sha256) and binary.sha256.lower().startswith(sha_prefix)
)
if path_ok and sha_ok:
return True
return False
def evaluate(
self,
method: str,
url_or_path: str,
host: Optional[str] = None,
port: Optional[int] = None,
binary: Optional[BinaryIdentity] = None,
env: Optional[dict[str, str]] = None,
) -> Decision:
env_map = env if env is not None else os.environ
parsed = urllib.parse.urlsplit(url_or_path)
req_host = (host or parsed.hostname or "").lower()
req_port = port or parsed.port or (443 if parsed.scheme == "https" else 80)
raw_path = urllib.parse.unquote(parsed.path or "/")
req_path = posixpath.normpath(raw_path)
if not req_path.startswith("/"):
req_path = "/" + req_path
req_method = method.upper()
bin_id = binary or BinaryIdentity(path="<unknown>", realpath="<unknown>", sha256="")
# 1. L4 (Host + Port) 过滤
l4_rules = []
for rule in self.rules:
host_ok = any(fnmatch.fnmatch(req_host, h.lower()) for h in rule.get("hosts", []))
ports = rule.get("ports", [])
port_ok = (not ports) or (req_port in ports)
if host_ok and port_ok:
l4_rules.append(rule)
if not l4_rules:
return self._deny(
"L4_HOST_PORT",
f"Host '{req_host}:{req_port}' not in egress allowlist",
bin_id, req_host, req_port, req_method, req_path,
)
# 2. Binary Identity (进程真实二进制路径 / SHA-256) 过滤
bin_rules = [
r for r in l4_rules if self._match_binary(bin_id, r.get("allowed_binaries", []))
]
if not bin_rules:
return self._deny(
"BINARY_IDENTITY",
f"Binary '{bin_id.path}' (realpath='{bin_id.realpath}', "
f"sha256={bin_id.sha256[:12]}...) not authorized for '{req_host}:{req_port}'",
bin_id, req_host, req_port, req_method, req_path,
)
# 3. L7 (HTTP Method + Path) 过滤与凭证注入
for rule in bin_rules:
l7_rules = rule.get("l7_rules", [])
l7_ok = not l7_rules
for l7 in l7_rules:
methods = [m.upper() for m in l7.get("methods", ["*"])]
paths = l7.get("paths", ["*"])
if ("*" in methods or req_method in methods) and any(
fnmatch.fnmatch(req_path, p) for p in paths
):
l7_ok = True
break
if l7_ok:
inject_headers: dict[str, str] = {}
cred_env = None
cred = rule.get("credential")
if cred:
cred_env = cred.get("env_var")
secret_val = env_map.get(cred_env, f"<UNSET:{cred_env}>")
tpl = cred.get("value_template", f"${{{cred_env}}}")
inject_headers[cred["header"]] = tpl.replace(f"${{{cred_env}}}", secret_val)
return Decision(
allowed=True,
stage="ALLOWED",
rule_name=rule.get("name"),
reason=f"Matched rule '{rule.get('name')}'",
inject_headers=inject_headers,
credential_env=cred_env,
)
return self._deny(
"L7_HTTP",
f"L7 request '{req_method} {req_path}' denied on '{req_host}:{req_port}'",
bin_id, req_host, req_port, req_method, req_path,
)
@staticmethod
def _deny(
stage: str,
reason: str,
bin_id: BinaryIdentity,
host: str,
port: int,
method: str,
path: str,
) -> Decision:
return Decision(
allowed=False,
stage=stage,
reason=reason,
denial_record={
"binary": bin_id.realpath if bin_id.realpath != "<unknown>" else bin_id.path,
"binary_invoked_as": bin_id.path,
"binary_sha256": bin_id.sha256,
"host": host,
"port": port,
"method": method,
"path": path,
"stage": stage,
"reason": reason,
},
)
def main() -> None:
if len(sys.argv) < 4 or sys.argv[1] != "check":
print("Usage: python3 policy.py check <METHOD> <URL> [--binary <PATH>] [--policy <YAML>]")
sys.exit(2)
method, url = sys.argv[2], sys.argv[3]
binary_path = "/usr/bin/python3"
policy_path = os.path.join(os.path.dirname(__file__) or ".", "policy.yaml")
args = sys.argv[4:]
while args:
flag = args.pop(0)
if flag == "--binary" and args:
binary_path = args.pop(0)
elif flag == "--policy" and args:
policy_path = args.pop(0)
engine = PolicyEngine.from_yaml_file(policy_path)
bin_id = BinaryIdentity.from_path(binary_path)
decision = engine.evaluate(method=method, url_or_path=url, binary=bin_id)
if decision.allowed:
inj = ", ".join(f"{k}={v}" for k, v in decision.inject_headers.items()) or "none"
print(
f"ALLOW [rule={decision.rule_name}, gen={engine.generation_id}] "
f"(binary={bin_id.realpath}, sha256={bin_id.sha256[:12]}..., inject: {inj})"
)
sys.exit(0)
else:
print(f"DENY [{decision.stage}]: {decision.reason}")
sys.exit(1)
if __name__ == "__main__":
main()
三、Milestone 2:进程外 Supervisor、凭证代填与结构化拒绝感知 (supervisor.py)
1. 为什么必须区分“受控通道透传身份”与宿主机 SO_PEERCRED?
在 Linux 下,通过 Unix Domain Socket (UDS) 的 getsockopt(SOL_SOCKET, SO_PEERCRED) 可以直接从内核 struct ucred 获取对端进程的 PID。但在 OpenShell 架构中,Agent 绝不能直接连接 Supervisor,而是由沙箱边界内的 sandbox.py 桥接转发。
这意味着如果 supervisor.py 仅在宿主机侧对 UDS 调用 SO_PEERCRED,它看到的 PID 永远是 sandbox.py 代理桥本身,而不是沙箱内真正发起请求的 curl 或 python3!因此,我们严格对齐 OpenShell v0.1.5 的 Mediated Channel(受控通道) 协议:
sandbox.py与supervisor.py共享一个从 Agent 环境变量中彻底抹除的通道令牌X-Nano-Channel-Token。任何不带合法 Token 的 UDS 直连请求一律返回401 Unauthorized,防止恶意 Agent 绕过sandbox.py直连 UDS 并伪造身份 Header。supervisor.py优先采信经受控通道认证后透传的X-Nano-Binary-*元数据;仅在未携带该 Header 的独立调试模式下,才回退使用内核SO_PEERCRED。- 转发上游前,强制剥离客户端可能自带的伪造
Authorization头(大小写不敏感过滤k.lower() != "authorization"),再将宿主机内存中的真实密钥注入请求。
2. 完整实现代码 (nano-openshell/supervisor.py)
#!/usr/bin/env python3
"""
Nano-OpenShell Milestone 2: Out-of-Process Supervisor, Mediated Handshake,
Credential Injection & Structured Denial Aggregator.
"""
from __future__ import annotations
import copy
import fnmatch
import http.client
import http.server
import json
import os
import socket
import socketserver
import struct
import threading
import time
import urllib.parse
from typing import Any, Optional
import yaml
from policy import BinaryIdentity, PolicyEngine
DEFAULT_SOCK_PATH = "/tmp/nano-supervisor.sock"
DEFAULT_UPSTREAM_PORT = 18080
DEFAULT_CHANNEL_TOKEN = "nano-mediated-token-v1"
SO_PEERCRED = getattr(socket, "SO_PEERCRED", 17)
def get_peer_pid(sock: socket.socket) -> Optional[int]:
"""通过 Linux UDS SO_PEERCRED 获取直连客户端的内核可信 PID"""
try:
creds = sock.getsockopt(socket.SOL_SOCKET, SO_PEERCRED, struct.calcsize("3i"))
pid, _uid, _gid = struct.unpack("3i", creds)
return pid if pid > 0 else None
except OSError:
return None
class DenialAggregator:
"""结构化拒绝感知聚合器与候选策略生成器 (对齐 openshell-sandbox/src/denial_aggregator.rs)"""
def __init__(self, base_dir: str, baseline_policy: dict[str, Any]) -> None:
self._lock = threading.Lock()
self.denials_path = os.path.join(base_dir, "denials.json")
self.proposal_path = os.path.join(base_dir, "policy-proposal.yaml")
self.baseline = baseline_policy
self.records: dict[tuple[str, str, int, str, str], dict[str, Any]] = {}
def record(self, denial: dict[str, Any]) -> None:
key = (
denial["binary"],
denial["host"],
int(denial["port"]),
denial["method"],
denial["path"],
)
now_ts = int(time.time())
with self._lock:
if key in self.records:
entry = self.records[key]
entry["count"] += 1
entry["last_seen"] = now_ts
else:
entry = {
**denial,
"count": 1,
"first_seen": now_ts,
"last_seen": now_ts,
}
self.records[key] = entry
self._flush_locked()
def _flush_locked(self) -> None:
items = list(self.records.values())
with open(self.denials_path, "w", encoding="utf-8") as f:
json.dump({"denials": items}, f, indent=2, ensure_ascii=False)
proposal = copy.deepcopy(self.baseline)
rules: list[dict[str, Any]] = proposal.setdefault("network_policies", [])
for d in items:
host, port, binary, method, path = (
d["host"],
int(d["port"]),
d["binary"],
d["method"],
d["path"],
)
matched_rule = None
for r in rules:
host_ok = any(fnmatch.fnmatch(host, h.lower()) for h in r.get("hosts", []))
ports = r.get("ports", [])
if host_ok and (not ports or port in ports):
matched_rule = r
break
if matched_rule is not None:
bins = matched_rule.setdefault("allowed_binaries", [])
if binary not in bins:
bins.append(binary)
l7s = matched_rule.setdefault("l7_rules", [])
if not any(
method in l7.get("methods", []) and path in l7.get("paths", [])
for l7 in l7s
):
l7s.append({"methods": [method], "paths": [path]})
else:
safe_name = host.replace(".", "_").replace(":", "_")
rules.append(
{
"name": f"proposed_{safe_name}_{port}",
"hosts": [host],
"ports": [port],
"allowed_binaries": [binary],
"l7_rules": [{"methods": [method], "paths": [path]}],
}
)
with open(self.proposal_path, "w", encoding="utf-8") as f:
yaml.safe_dump(proposal, f, sort_keys=False, allow_unicode=True)
class ThreadingUnixHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
address_family = socket.AF_UNIX
daemon_threads = True
class SupervisorHandler(http.server.BaseHTTPRequestHandler):
engine: PolicyEngine
aggregator: DenialAggregator
channel_token: str
upstream_port: int
def log_message(self, fmt: str, *args: Any) -> None:
print(f"[supervisor] {fmt % args}", flush=True)
def _handle_all(self) -> None:
# 1. Mediated Channel 鉴权:防止沙箱内进程绕过 sandbox.py 直连 UDS 伪造身份
token = self.headers.get("X-Nano-Channel-Token", "")
if token != self.channel_token:
self._send_json(
401,
{
"error": "Unauthorized",
"reason": "Direct connection bypassing sandbox mediated channel is forbidden",
},
)
return
# 2. 控制面端点:启动握手锁与断连冻结监控流
if self.path == "/__nano_control__/handshake":
self._send_json(
200,
{
"status": "ok",
"generation_id": self.engine.generation_id,
},
)
return
if self.path == "/__nano_control__/watch":
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
try:
while True:
self.wfile.write(f"alive {self.engine.generation_id}\n".encode("utf-8"))
self.wfile.flush()
time.sleep(0.5)
except (BrokenPipeError, ConnectionResetError, OSError):
return
# 3. 提取可信进程二进制身份(优先使用 sandbox.py 观察到的身份,回退使用 SO_PEERCRED)
bin_path = self.headers.get("X-Nano-Binary-Path")
bin_realpath = self.headers.get("X-Nano-Binary-Realpath")
bin_sha256 = self.headers.get("X-Nano-Binary-Sha256")
if bin_path:
binary = BinaryIdentity(
path=bin_path,
realpath=bin_realpath or bin_path,
sha256=bin_sha256 or "",
)
else:
peer_pid = get_peer_pid(self.connection)
binary = (
BinaryIdentity.from_pid(peer_pid)
if peer_pid
else BinaryIdentity("<unknown>", "<unknown>", "")
)
# 4. 解析目标 Host / Port / Path 并执行三层策略评估
parsed = urllib.parse.urlsplit(self.path)
host_hdr = self.headers.get("Host", "127.0.0.1")
if ":" in host_hdr:
h_name, h_port_str = host_hdr.rsplit(":", 1)
default_port = int(h_port_str)
else:
h_name, default_port = host_hdr, (443 if parsed.scheme == "https" else 80)
target_host = (parsed.hostname or h_name).lower()
target_port = parsed.port or default_port
target_path = parsed.path or "/"
if parsed.query:
target_path += f"?{parsed.query}"
decision = self.engine.evaluate(
method=self.command,
url_or_path=target_path,
host=target_host,
port=target_port,
binary=binary,
)
if not decision.allowed:
if decision.denial_record:
self.aggregator.record(decision.denial_record)
self._send_json(
403,
{
"error": "Blocked by Nano-OpenShell Supervisor",
"stage": decision.stage,
"reason": decision.reason,
"binary": binary.realpath,
},
)
return
# 5. 策略放行:剥离内部 X-Nano-* 头与客户端伪造的 Authorization 头,注入真实凭证
content_len = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(content_len) if content_len > 0 else None
forward_headers = {
k: v
for k, v in self.headers.items()
if not k.lower().startswith("x-nano-")
and k.lower() not in ("host", "authorization")
}
forward_headers["Host"] = f"{target_host}:{target_port}"
forward_headers.update(decision.inject_headers)
try:
conn = http.client.HTTPConnection("127.0.0.1", self.upstream_port, timeout=5)
conn.request(self.command, target_path, body=body, headers=forward_headers)
resp = conn.getresponse()
resp_body = resp.read()
self.send_response(resp.status)
self.send_header("Content-Type", resp.getheader("Content-Type", "application/json"))
self.send_header("Content-Length", str(len(resp_body)))
self.end_headers()
self.wfile.write(resp_body)
conn.close()
except Exception as exc:
self._send_json(502, {"error": "Upstream connection failed", "detail": str(exc)})
def _send_json(self, status: int, payload: dict[str, Any]) -> None:
raw = (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = _handle_all
def start_mock_upstream(port: int) -> socketserver.ThreadingTCPServer:
"""启动本地回显测试上游,用于零外部依赖验证 Supervisor 是否成功注入真实凭证"""
class EchoUpstreamHandler(http.server.BaseHTTPRequestHandler):
def log_message(self, fmt: str, *args: Any) -> None:
pass
def _echo(self) -> None:
payload = {
"upstream": "mock-openai-server",
"method": self.command,
"path": self.path,
"host": self.headers.get("Host"),
"received_authorization": self.headers.get("Authorization", "<NONE>"),
}
raw = (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(raw)))
self.end_headers()
self.wfile.write(raw)
do_GET = do_POST = do_PUT = do_DELETE = _echo
class ReusableTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
srv = ReusableTCPServer(("127.0.0.1", port), EchoUpstreamHandler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
return srv
def main() -> None:
base_dir = os.path.dirname(os.path.abspath(__file__))
policy_path = os.environ.get("NANO_POLICY_PATH", os.path.join(base_dir, "policy.yaml"))
sock_path = os.environ.get("NANO_SOCK_PATH", DEFAULT_SOCK_PATH)
channel_token = os.environ.get("NANO_CHANNEL_TOKEN", DEFAULT_CHANNEL_TOKEN)
upstream_port = int(os.environ.get("NANO_UPSTREAM_PORT", str(DEFAULT_UPSTREAM_PORT)))
engine = PolicyEngine.from_yaml_file(policy_path)
aggregator = DenialAggregator(base_dir=base_dir, baseline_policy=engine.raw)
start_mock_upstream(upstream_port)
if os.path.exists(sock_path):
os.unlink(sock_path)
SupervisorHandler.engine = engine
SupervisorHandler.aggregator = aggregator
SupervisorHandler.channel_token = channel_token
SupervisorHandler.upstream_port = upstream_port
server = ThreadingUnixHTTPServer(sock_path, SupervisorHandler)
print(
f"[supervisor] Ready on UDS {sock_path} | generation={engine.generation_id} "
f"| mock_upstream=127.0.0.1:{upstream_port}",
flush=True,
)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
if os.path.exists(sock_path):
os.unlink(sock_path)
if __name__ == "__main__":
main()
四、Milestone 3:Linux Namespace 隔离、/proc 套接字溯源与断连 SIGSTOP 冻结 (sandbox.py)
sandbox.py 是整个数据面隔离最硬核的部分。在不依赖 Docker daemon 或 root 权限的前提下,我们通过 Linux 内核原语解决四个底层问题:
1. 四大内核级实现机制
- User Namespace 的
setgroups写入锁(CVE-2014-8989 约束):调用os.unshare(CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWNS)后,非特权进程若直接写入/proc/self/gid_map会遭到内核拒绝(EPERM)。必须严格按照/proc/self/setgroups写入"deny"→ 写uid_map→ 写gid_map的顺序执行,才能在命名空间内合法获取CAP_NET_ADMIN能力。 - 原生
ioctl(SIOCSIFFLAGS)激活lo回环网卡:新创建的CLONE_NEWNET网络命名空间不仅没有任何外部路由(彻底阻断绕过代理的原始套接字直连),其内置的lo(127.0.0.1) 默认也处于DOWN状态。为了做到零外部命令依赖(不依赖/sbin/ip),我们直接构造struct ifreq并调用fcntl.ioctl(sock, 0x8914, struct.pack("16sH", b"lo", 0x1 | 0x40))在内核态拉起lo接口。 - 字节序安全的
/proc/self/net/tcpSocket Inode 溯源:当沙箱内的python3或curl连接127.0.0.1:<proxy_port>时,在x86_64Linux 上/proc/self/net/tcp中的 IPv4 地址按小端序存储为0100007F,而端口按大端序存储为 4 位十六进制(:PORT)。我们在resolve_client_binary(client_port)中直接匹配local_address列以f":{client_port:04X}"结尾的条目,提取第 10 列的内核 Socket inode,再遍历/proc/[0-9]*/fd/*符号链接匹配socket:[<inode>],即可 100% 锁定沙箱内发起该 TCP 连接的真实子进程 PID。 - 独立会话进程组与毫秒级
SIGSTOP冻结:通过subprocess.Popen(..., start_new_session=True)启动工作负载,使其成为新进程组 Leader(pgid = proc.pid)。后台看门狗线程持续维持与supervisor.py的/__nano_control__/watchUDS 长连接;一旦 Supervisor 异常退出,内核立即关闭 UDS 触发 EOF,sandbox.py瞬间调用os.killpg(proc.pid, signal.SIGSTOP)将整个 Agent 进程树冻结在内核T(Stopped) 状态,防止失控窗口期。
2. 完整实现代码 (nano-openshell/sandbox.py)
#!/usr/bin/env python3
"""
Nano-OpenShell Milestone 3: Inside the Sandbox Boundary.
Linux User+Net Namespace Isolation, /proc Socket-to-Binary Observer,
Fail-Closed Handshake Lock & SIGSTOP Disconnect Freeze.
"""
from __future__ import annotations
import fcntl
import glob
import http.client
import http.server
import json
import os
import signal
import socket
import socketserver
import struct
import subprocess
import sys
import threading
from typing import Any, Optional
from policy import BinaryIdentity
DEFAULT_SOCK_PATH = "/tmp/nano-supervisor.sock"
DEFAULT_CHANNEL_TOKEN = "nano-mediated-token-v1"
CLONE_NEWNS = getattr(os, "CLONE_NEWNS", 0x00020000)
CLONE_NEWUSER = getattr(os, "CLONE_NEWUSER", 0x10000000)
CLONE_NEWNET = getattr(os, "CLONE_NEWNET", 0x40000000)
SIOCSIFFLAGS = 0x8914
IFF_UP_RUNNING = 0x1 | 0x40 # IFF_UP (0x1) | IFF_RUNNING (0x40)
def enter_isolated_namespaces() -> None:
"""创建隔离的 User + Network + Mount Namespace 并原生激活 lo 回环网卡"""
orig_uid, orig_gid = os.getuid(), os.getgid()
os.unshare(CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWNS)
# 1. 严格按内核要求顺序写入 setgroups -> uid_map -> gid_map
with open("/proc/self/setgroups", "w", encoding="utf-8") as f:
f.write("deny\n")
with open("/proc/self/uid_map", "w", encoding="utf-8") as f:
f.write(f"0 {orig_uid} 1\n")
with open("/proc/self/gid_map", "w", encoding="utf-8") as f:
f.write(f"0 {orig_gid} 1\n")
# 2. 使用 ioctl(SIOCSIFFLAGS) 原生拉起沙箱内的 lo (127.0.0.1) 接口
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
ifr = struct.pack("16sH", b"lo", IFF_UP_RUNNING)
fcntl.ioctl(sock.fileno(), SIOCSIFFLAGS, ifr)
finally:
sock.close()
def resolve_client_binary(client_port: int) -> BinaryIdentity:
"""通过沙箱内 /proc/self/net/tcp 与 /proc/<pid>/fd 反查发起连接的真实进程二进制"""
port_suffix = f":{client_port:04X}"
target_inode: Optional[str] = None
for tcp_table in ("/proc/self/net/tcp", "/proc/self/net/tcp6"):
if not os.path.exists(tcp_table):
continue
try:
with open(tcp_table, "r", encoding="utf-8") as f:
for line in f.readlines()[1:]:
cols = line.split()
if len(cols) >= 10 and cols[1].upper().endswith(port_suffix):
target_inode = cols[9]
break
except OSError:
continue
if target_inode and target_inode != "0":
break
if not target_inode or target_inode == "0":
return BinaryIdentity("<unknown>", "<unknown>", "")
socket_tag = f"socket:[{target_inode}]"
self_pid = os.getpid()
for fd_path in glob.glob("/proc/[0-9]*/fd/*"):
try:
pid_str = fd_path.split("/")[2]
pid = int(pid_str)
if pid == self_pid:
continue
if os.readlink(fd_path) == socket_tag:
return BinaryIdentity.from_pid(pid)
except (OSError, ValueError):
continue
return BinaryIdentity("<unknown>", "<unknown>", "")
class UnixHTTPConnection(http.client.HTTPConnection):
"""通过 Unix Domain Socket 连接宿主机 Supervisor 的 HTTP 客户端"""
def __init__(self, sock_path: str, timeout: float = 10.0) -> None:
super().__init__("localhost", timeout=timeout)
self.sock_path = sock_path
def connect(self) -> None:
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.connect(self.sock_path)
class SandboxBrokerHandler(http.server.BaseHTTPRequestHandler):
sock_path: str
channel_token: str
def log_message(self, fmt: str, *args: Any) -> None:
pass
def _bridge_to_supervisor(self) -> None:
# 1. 在隔离边界内通过 /proc 采样真实调用进程身份(绝不在沙箱内做策略决策)
client_port = self.client_address[1]
bin_id = resolve_client_binary(client_port)
content_len = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(content_len) if content_len > 0 else None
headers = {
k: v
for k, v in self.headers.items()
if not k.lower().startswith("x-nano-")
}
# 2. 注入 Mediated Channel 令牌与可信进程身份元数据
headers["X-Nano-Channel-Token"] = self.channel_token
headers["X-Nano-Binary-Path"] = bin_id.path
headers["X-Nano-Binary-Realpath"] = bin_id.realpath
headers["X-Nano-Binary-Sha256"] = bin_id.sha256
try:
conn = UnixHTTPConnection(self.sock_path, timeout=5.0)
conn.request(self.command, self.path, body=body, headers=headers)
resp = conn.getresponse()
resp_body = resp.read()
self.send_response(resp.status)
for k, v in resp.getheaders():
if k.lower() not in ("transfer-encoding", "connection"):
self.send_header(k, v)
self.end_headers()
self.wfile.write(resp_body)
conn.close()
except Exception as exc:
err = (
json.dumps({"error": "Mediated channel error", "detail": str(exc)}) + "\n"
).encode("utf-8")
self.send_response(502)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(err)))
self.end_headers()
self.wfile.write(err)
do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = _bridge_to_supervisor
def perform_startup_handshake(sock_path: str, channel_token: str) -> str:
"""Fail-Closed 启动锁:与 Supervisor 完成 generation_id 握手,失败则拒绝启动 Agent"""
try:
conn = UnixHTTPConnection(sock_path, timeout=3.0)
conn.request(
"GET",
"/__nano_control__/handshake",
headers={"X-Nano-Channel-Token": channel_token},
)
resp = conn.getresponse()
data = json.loads(resp.read().decode("utf-8"))
conn.close()
if resp.status == 200 and data.get("status") == "ok":
return data["generation_id"]
raise RuntimeError(f"Unexpected handshake response: {resp.status} {data}")
except Exception as exc:
print(
f"[sandbox][FAIL-CLOSED] Supervisor mediated handshake failed ({exc}), "
"refusing to launch workload!",
file=sys.stderr,
flush=True,
)
sys.exit(125)
def watch_supervisor_or_freeze(
sock_path: str, channel_token: str, workload_proc: subprocess.Popen[bytes]
) -> None:
"""后台监控 Mediated 控制流:一旦与 Supervisor 断连,立即发送 SIGSTOP 冻结整个 Agent 进程组"""
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(sock_path)
req = (
"GET /__nano_control__/watch HTTP/1.1\r\n"
"Host: localhost\r\n"
f"X-Nano-Channel-Token: {channel_token}\r\n\r\n"
).encode("utf-8")
sock.sendall(req)
while workload_proc.poll() is None:
chunk = sock.recv(256)
if not chunk:
break
except OSError:
pass
if workload_proc.poll() is None:
print(
f"\n[sandbox][FAIL-CLOSED] Mediated channel lost! "
f"Sending SIGSTOP to workload pgid={workload_proc.pid}",
file=sys.stderr,
flush=True,
)
try:
os.killpg(workload_proc.pid, signal.SIGSTOP)
except ProcessLookupError:
pass
def build_sanitized_env(proxy_port: int) -> dict[str, str]:
"""清洗宿主机敏感环境变量,防止任何真实密钥或 Channel Token 泄露进沙箱"""
clean_env: dict[str, str] = {}
for k, v in os.environ.items():
upper_k = k.upper()
if (
upper_k.startswith(("REAL_", "NANO_"))
or upper_k.endswith(("_KEY", "_SECRET", "_TOKEN"))
or upper_k in ("NO_PROXY", "HTTP_PROXY", "HTTPS_PROXY")
):
continue
clean_env[k] = v
proxy_url = f"http://127.0.0.1:{proxy_port}"
clean_env["HTTP_PROXY"] = proxy_url
clean_env["http_proxy"] = proxy_url
clean_env["HTTPS_PROXY"] = proxy_url
clean_env["https_proxy"] = proxy_url
return clean_env
def main() -> None:
cmd = sys.argv[1:]
if cmd and cmd[0] == "--":
cmd = cmd[1:]
if not cmd:
print("Usage: python3 sandbox.py -- <COMMAND> [ARGS...]", file=sys.stderr)
sys.exit(2)
sock_path = os.environ.get("NANO_SOCK_PATH", DEFAULT_SOCK_PATH)
channel_token = os.environ.get("NANO_CHANNEL_TOKEN", DEFAULT_CHANNEL_TOKEN)
# 1. 进入 Linux User + Network + Mount 隔离命名空间并激活 lo
enter_isolated_namespaces()
# 2. Fail-Closed 启动锁:未确认 Supervisor 在线与 generation_id 前绝不启动子进程
gen_id = perform_startup_handshake(sock_path, channel_token)
# 3. 启动沙箱内本地代理观察器 (127.0.0.1:<随机空闲端口>)
SandboxBrokerHandler.sock_path = sock_path
SandboxBrokerHandler.channel_token = channel_token
class ThreadedLocalBroker(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True
broker = ThreadedLocalBroker(("127.0.0.1", 0), SandboxBrokerHandler)
proxy_port = broker.server_address[1]
threading.Thread(target=broker.serve_forever, daemon=True).start()
# 4. 擦除敏感环境变量并在独立进程组 (start_new_session=True) 中启动 Agent 工作负载
child_env = build_sanitized_env(proxy_port)
print(
f"[sandbox] Boundary active (gen={gen_id}, local_broker=127.0.0.1:{proxy_port})",
file=sys.stderr,
flush=True,
)
proc = subprocess.Popen(cmd, env=child_env, start_new_session=True)
# 5. 启动断连 SIGSTOP 看门狗线程
threading.Thread(
target=watch_supervisor_or_freeze,
args=(sock_path, channel_token, proc),
daemon=True,
).start()
sys.exit(proc.wait())
if __name__ == "__main__":
main()
五、Milestone 4:静态策略边界证明器与包含性算法 (prover.py)
当 supervisor.py 拦截了沙箱内的异常请求并自动生成 policy-proposal.yaml 后,我们绝不能盲目自动审批通过。prover.py 的职责是在不引入重量级外部 SMT 求解器的前提下,静态证明候选策略 P_cand 的权限空间是否严格包含于基准策略 P_base(即 L(P_cand) ⊆ L(P_base))。
1. 双探测串通配符包含性算法 (is_glob_subset)
如何用 5 行代码判定候选通配符模式 cand_pat 是基准通配符模式 base_pat 的子集?我们采用符号哨兵代换法(Sentinel Substitution):
- 将
cand_pat中的任意长通配符*替换为多字符哨兵"__WILD_STAR__"; - 将单字符通配符
?分别替换为两个正交单字符"a"与"b",构造两条探测串probe_a和probe_b; - 当且仅当
fnmatchcase(probe_a, base_pat)与fnmatchcase(probe_b, base_pat)同时成立时,判定cand_pat ⊆ base_pat。
例如:
is_glob_subset("/usr/bin/python3.14", "/usr/bin/python3*")→True(版本收紧,允许自动审批)。is_glob_subset("/v1/*", "/v1/chat/completions")→probe_a = "/v1/__WILD_STAR__"无法匹配"/v1/chat/completions"→False(路径放宽,触发L7_METHOD_PATH_WIDENING告警)。
2. 修补“无凭证新域名逃逸”边界漏洞
在审计候选规则的 (host, port) 时,若候选规则未绑定 credential(例如 DenialAggregator 因拦截了未知外部域名 evil.example.com 而自动生成的 proposed_evil_example_com_18080 规则),在查找覆盖它的基准规则集 covering_base 时会得到空列表 []。此时必须立即触发 [HIGH] UNAPPROVED_EGRESS_ENDPOINT 阻断告警,防止攻击者在不携带宿主 API Key 的情况下将本地数据外传至未授权外部主机。
3. 完整实现代码 (nano-openshell/prover.py)
#!/usr/bin/env python3
"""
Nano-OpenShell Milestone 4: Static Policy Prover & Containment Verifier.
Zero-dependency static verifier blocking unsafe auto-approval of policy proposals.
"""
from __future__ import annotations
import dataclasses
import fnmatch
import posixpath
import sys
from typing import Any
import yaml
CLOUD_METADATA_ENDPOINTS = (
"169.254.169.254",
"fd00:ec2::254",
"metadata.google.internal",
"100.100.100.200",
)
@dataclasses.dataclass(frozen=True)
class Finding:
"""静态策略边界审计发现项 (对齐 openshell-prover::finding::Finding)"""
severity: str # CRITICAL | HIGH | MEDIUM
code: str # CLOUD_METADATA_GUARD | NEW_CREDENTIALED_REACH | UNAPPROVED_EGRESS_ENDPOINT | BINARY_PRIVILEGE_EXPANSION | L7_METHOD_PATH_WIDENING
rule_name: str
detail: str
def is_glob_subset(cand_pat: str, base_pat: str) -> bool:
"""判定候选通配符模式 cand_pat 是否为基准模式 base_pat 的子集 (cand_pat ⊆ base_pat)"""
if cand_pat == base_pat:
return True
starred = cand_pat.replace("*", "__WILD_STAR__")
probe_a = starred.replace("?", "a")
probe_b = starred.replace("?", "b")
return fnmatch.fnmatchcase(probe_a, base_pat) and fnmatch.fnmatchcase(probe_b, base_pat)
def _extract_binary_pattern(entry: Any) -> str:
if isinstance(entry, str):
return entry
if isinstance(entry, dict):
return str(entry.get("path", "*"))
return str(entry)
def _normalize_ports(rule: dict[str, Any]) -> list[int]:
ports = rule.get("ports", [])
return [int(p) for p in ports] if ports else [0]
def _port_covered(cand_port: int, base_ports: list[int]) -> bool:
if 0 in base_ports:
return True
if cand_port == 0:
return False
return cand_port in base_ports
def _expand_l7_pairs(rule: dict[str, Any]) -> list[tuple[str, str]]:
l7_rules = rule.get("l7_rules", [])
if not l7_rules:
return [("*", "/*")]
pairs: list[tuple[str, str]] = []
for item in l7_rules:
methods = [str(m).upper() for m in item.get("methods", ["*"])]
paths = [str(p) for p in item.get("paths", ["/*"])]
for m in methods:
for p in paths:
norm_p = posixpath.normpath(p) if not p.endswith("*") else p
if not norm_p.startswith("/"):
norm_p = "/" + norm_p
pairs.append((m, norm_p))
return pairs
class PolicyProver:
"""策略包含性与安全不变量静态验证器 (对齐 openshell-prover)"""
def __init__(self, baseline: dict[str, Any], candidate: dict[str, Any]) -> None:
self.baseline_rules: list[dict[str, Any]] = baseline.get("network_policies", [])
self.candidate_rules: list[dict[str, Any]] = candidate.get("network_policies", [])
def verify(self) -> list[Finding]:
findings: list[Finding] = []
seen: set[tuple[str, str, str]] = set()
def emit(severity: str, code: str, rule_name: str, detail: str) -> None:
key = (code, rule_name, detail)
if key not in seen:
seen.add(key)
findings.append(
Finding(severity=severity, code=code, rule_name=rule_name, detail=detail)
)
for cand_rule in self.candidate_rules:
r_name = cand_rule.get("name", "<unnamed>")
cand_hosts = [str(h).lower() for h in cand_rule.get("hosts", [])]
cand_ports = _normalize_ports(cand_rule)
# 1. [CRITICAL] CLOUD_METADATA_GUARD: 拦截云厂商元数据与链路本地地址 (含通配符双向检测)
for host_pat in cand_hosts:
for meta_ep in CLOUD_METADATA_ENDPOINTS:
if fnmatch.fnmatchcase(meta_ep, host_pat) or fnmatch.fnmatchcase(
host_pat, meta_ep
):
emit(
"CRITICAL",
"CLOUD_METADATA_GUARD",
r_name,
f"Host pattern '{host_pat}' exposes cloud metadata endpoint '{meta_ep}'",
)
# 2. [CRITICAL] NEW_CREDENTIALED_REACH: 拦截凭证向未授权新 (host, port) 外发
cred = cand_rule.get("credential") or {}
cred_env = cred.get("env_var")
if cred_env:
same_cred_base_rules = [
br
for br in self.baseline_rules
if (br.get("credential") or {}).get("env_var") == cred_env
]
for c_host in cand_hosts:
for c_port in cand_ports:
covered = any(
any(
is_glob_subset(c_host, str(b_host).lower())
for b_host in br.get("hosts", [])
)
and _port_covered(c_port, _normalize_ports(br))
for br in same_cred_base_rules
)
if not covered:
port_disp = "*" if c_port == 0 else str(c_port)
emit(
"CRITICAL",
"NEW_CREDENTIALED_REACH",
r_name,
f"Credential '{cred_env}' extended to unapproved endpoint '{c_host}:{port_disp}'",
)
# 3. 校验 L4 端点覆盖关系、Binary 扩权与 L7 放宽
for c_host in cand_hosts:
for c_port in cand_ports:
covering_base = [
br
for br in self.baseline_rules
if any(
is_glob_subset(c_host, str(b_host).lower())
for b_host in br.get("hosts", [])
)
and _port_covered(c_port, _normalize_ports(br))
]
port_disp = "*" if c_port == 0 else str(c_port)
if not covering_base:
emit(
"HIGH",
"UNAPPROVED_EGRESS_ENDPOINT",
r_name,
f"Endpoint '{c_host}:{port_disp}' is not covered by any baseline L4 rule",
)
continue
# 4. [HIGH] BINARY_PRIVILEGE_EXPANSION: 同一 (host, port) 下允许的二进制集合扩权
cand_bins = [
_extract_binary_pattern(b)
for b in cand_rule.get("allowed_binaries", [])
]
if not cand_bins:
if not any(
len(br.get("allowed_binaries", [])) == 0 for br in covering_base
):
emit(
"HIGH",
"BINARY_PRIVILEGE_EXPANSION",
r_name,
f"Rule allows unrestricted binaries ('*') on '{c_host}:{port_disp}' where baseline restricts binaries",
)
else:
for c_bin in cand_bins:
bin_ok = any(
len(br.get("allowed_binaries", [])) == 0
or any(
is_glob_subset(c_bin, _extract_binary_pattern(b_bin))
for b_bin in br.get("allowed_binaries", [])
)
for br in covering_base
)
if not bin_ok:
emit(
"HIGH",
"BINARY_PRIVILEGE_EXPANSION",
r_name,
f"Binary '{c_bin}' is not contained in baseline allowed_binaries for '{c_host}:{port_disp}'",
)
# 5. [MEDIUM] L7_METHOD_PATH_WIDENING: 同一 (host, port) 下 HTTP Method/Path 放宽
cand_l7_pairs = _expand_l7_pairs(cand_rule)
for c_method, c_path in cand_l7_pairs:
l7_ok = False
for br in covering_base:
for b_method, b_path in _expand_l7_pairs(br):
if is_glob_subset(c_method, b_method) and is_glob_subset(
c_path, b_path
):
l7_ok = True
break
if l7_ok:
break
if not l7_ok:
emit(
"MEDIUM",
"L7_METHOD_PATH_WIDENING",
r_name,
f"L7 capability '{c_method} {c_path}' widens baseline L7 rules on '{c_host}:{port_disp}'",
)
return findings
def load_yaml(path: str) -> dict[str, Any]:
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f) or {}
def main() -> None:
if len(sys.argv) != 3:
print(
"Usage: python3 prover.py <BASELINE_POLICY.yaml> <CANDIDATE_POLICY.yaml>",
file=sys.stderr,
)
sys.exit(2)
base_path, cand_path = sys.argv[1], sys.argv[2]
baseline = load_yaml(base_path)
candidate = load_yaml(cand_path)
prover = PolicyProver(baseline=baseline, candidate=candidate)
findings = prover.verify()
if not findings:
print(
f"PASS: Candidate policy '{cand_path}' is strictly contained within baseline '{base_path}' (0 findings). Auto-approval PERMITTED."
)
sys.exit(0)
print(
f"FAIL: Candidate policy '{cand_path}' violates safety/containment boundaries ({len(findings)} finding(s)). Auto-approval BLOCKED:"
)
for idx, f in enumerate(findings, 1):
print(f" {idx}. FAIL [{f.severity}] {f.code} (rule={f.rule_name}): {f.detail}")
sys.exit(1)
if __name__ == "__main__":
main()
六、端到端全链路回归验证:4 大阶段、19 项断言实录
任何未经回归验证的系统代码都只是假设。我们编写了自动化回归测试套件 test_regression.sh,对 nano-openshell 进行了从静态单测、跨命名空间红蓝对抗、提案闭环审计到内核进程组冻结的全方位检验。
进程组防管道卡死技巧:由于
sandbox.py使用start_new_session=True启动子进程,当杀掉 Supervisor 触发SIGSTOP后,子进程会停留在内核Ts(Session Leader + Stopped)状态并持续持有 Shell 的stdout/stderr管道描述符。在回归脚本中,我们通过CHILD_PID=$(pgrep -P $SB_PID)捕获子进程状态后,使用kill -9 -$CHILD_PID $SB_PID一次性清理整个进程组。
实测输出(Ubuntu 24.04 LTS / Linux Kernel)
==================================================================
Stage 1: Milestone 1 (policy.py) L4 / Binary / L7 Unit Assertions
==================================================================
[PASS] Authorized python3 POST /v1/chat/completions allowed
[PASS] Credential template rendered for authorized request
[PASS] Path traversal '/repos/../admin' blocked with exit code 1 (got '1')
[PASS] Path normalization prevented glob bypass
[PASS] Untrusted /tmp binary blocked with exit code 1 (got '1')
[PASS] Untrusted execution prefix (/tmp/) rejected at BINARY_IDENTITY stage
==================================================================
Stage 2: Milestone 2 & 3 (supervisor.py + sandbox.py) Runtime E2E
==================================================================
[PASS] Sandbox refuses to start when Supervisor is offline (exit 125) (got '125')
[PASS] Fail-closed startup handshake message verified
[PASS] Supervisor started (pid=14150) on /tmp/nano-supervisor-regression.sock
[PASS] Host secret REAL_OPENAI_KEY scrubbed from sandbox environment
[PASS] CLONE_NEWNET blocked direct raw socket bypass to 1.1.1.1:80
[PASS] Authorized python3 request received injected Bearer sk-live-999
[PASS] Spawned /usr/bin/curl inside sandbox received HTTP 403
[PASS] /proc socket-to-inode resolver attributed request to /usr/bin/curl
[PASS] Unapproved domain evil.example.com:18080 received HTTP 403
[PASS] Supervisor blocked evil.example.com at L4_HOST_PORT stage
==================================================================
Stage 3: Milestone 4 (prover.py) Closed-Loop Proposal Verification
==================================================================
[PASS] Tightened policy verified as strict subset (exit 0)
FAIL: Candidate policy '/home/ryan/OpenShell/nano-openshell/policy-proposal.yaml' violates safety/containment boundaries (10 finding(s)). Auto-approval BLOCKED:
1. FAIL [HIGH] BINARY_PRIVILEGE_EXPANSION (rule=openai_agent_inference): Binary '/usr/bin/curl' is not contained in baseline allowed_binaries for 'api.openai.com:443'
...
6. FAIL [HIGH] BINARY_PRIVILEGE_EXPANSION (rule=openai_agent_inference): Binary '/usr/bin/curl' is not contained in baseline allowed_binaries for 'api.openai.local:18080'
...
10. FAIL [HIGH] UNAPPROVED_EGRESS_ENDPOINT (rule=proposed_evil_example_com_18080): Endpoint 'evil.example.com:18080' is not covered by any baseline L4 rule
[PASS] Auto-generated policy-proposal.yaml blocked from auto-approval (exit 1) (got '1')
[PASS] Prover caught /usr/bin/curl binary privilege expansion
[PASS] Prover caught unapproved L4 endpoint evil.example.com:18080
==================================================================
Stage 4: Supervisor Disconnect -> SIGSTOP Workload Freeze & Cleanup
==================================================================
PID PGID STAT COMMAND
14200 14200 Ts python3
[PASS] Workload process group frozen with SIGSTOP (state=Ts)
[PASS] Frozen process group (-14200) and sandbox (14200) cleanly terminated
==================================================================
ALL NANO-OPENSHELL REGRESSION TESTS PASSED SUCCESSFULLY!
==================================================================
七、总结:复杂基础设施项目的“四步解构法”
通过不到 750 行 Python 代码完整复现 NVIDIA OpenShell 的核心闭环,我们可以提炼出一套通用的底层系统学习与复现方法论:
- 架构脱水(Architecture Pruning):先扫描二级目录树与官方架构规范,将控制面、数据面与外围工程噪音分离,锁定最核心的 3~4 个原语。
- 明确隔离边界两侧的信任锚点(Trust Anchor Split):清晰界定“谁观察(Untrusted Side Observer)”与“谁决策(Trusted Side PDP/PEP)”,避免因命名空间隔离导致宿主机视角丢失进程上下文。
- 直面内核隐式状态(Kernel Implicit States):将
setgroups映射顺序、CLONE_NEWNET回环网卡激活、/proc/self/net/tcp字节序、进程组信号传递等底层机制显式化验证。 - 攻防驱动的闭环回归(Adversarial Regression):每一个安全特性都必须配对相应的绕过载荷(如
..路径穿越、/tmp伪造二进制、同域派生curl、无凭证新域名提案),用自动化断言锁定系统行为。
在下一篇文章中,我们将继续在 nano-openshell/sandbox.py 中利用 45 行 ctypes 补齐 Linux Landlock LSM 文件系统白名单隔离与 Seccomp-BPF 系统调用过滤,敬请期待!