1. 为什么LLM代码执行必须要有沙箱
1.1 从一次真实的翻车现场说起
去年下半年我参与了一个内部工具链项目,核心逻辑是让大语言模型根据用户的自然语言描述自动生成数据处理脚本,然后直接在服务器上跑出结果。听起来很美好对吧?用户说一句“帮我把上个月的销售数据按区域汇总一下”,模型吐出几行Python,后端拿到代码直接exec(),结果返回给前端。开发阶段一切顺利,直到有一次测试同学输入了一句“帮我清理一下临时文件”,模型生成的代码里赫然出现了shutil.rmtree('/tmp')这样的调用——虽然/tmp本身不是关键目录,但那一刻我后背是真的发凉。如果模型当时生成的是os.remove配合通配符,或者更隐蔽地读取环境变量里的密钥再外发,整个系统就彻底裸奔了。
这件事让我意识到一个根本问题:LLM生成的代码和人类写的代码,在安全假设上完全是两回事。人类开发者写代码时有意图、有上下文、有代码审查,而LLM生成的代码是基于概率分布“拼”出来的,它可能在99%的情况下表现正常,但剩下1%的异常输出你根本无法预测。更麻烦的是,攻击者可以通过精心构造的提示词,诱导模型生成恶意代码——这就是所谓的提示注入攻击,在OWASP 2026年的AI智能体安全风险清单里,它排在非常靠前的位置。
所以当我看到“AI智能体安全沙箱agentguard”这个项目标题时,第一反应是:终于有人把这件事当成一个独立的基础设施来做了。它要解决的核心问题很明确——让LLM生成的代码在一个受控的、隔离的、可审计的环境里执行,即使代码本身是恶意的,也无法对宿主系统造成实质性伤害。这篇文章我会从设计思路、核心机制、实操落地、踩坑经验几个维度,把这个沙箱的完整面貌拆开来讲清楚。
1.2 沙箱要防的到底是什么
很多人一提到“沙箱”就想到Docker,觉得把代码扔进容器里跑就万事大吉了。这个认知不能说错,但太粗糙。LLM代码执行场景下的威胁模型比普通容器逃逸要复杂得多,我把它拆成四个层面:
第一层是文件系统破坏。模型可能生成删除文件、覆盖配置、篡改日志的代码。这类威胁最直接,也最容易防——只要把文件系统挂载成只读,或者限制可写目录就行。
第二层是敏感信息泄露。模型生成的代码可能读取环境变量、配置文件、SSH密钥,然后通过HTTP请求把数据发出去。这类威胁的隐蔽性很强,因为代码表面上可能只是在“读取配置”,但实际目的是外传。
第三层是资源耗尽。一个死循环、一个无限递归、一个内存泄漏,就能把整个服务器的CPU和内存吃干。LLM生成的代码尤其容易出现这类问题,因为它对“边界条件”的理解往往不够精确。
第四层是横向移动。如果沙箱和宿主网络没有隔离,恶意代码可能扫描内网、攻击其他服务、甚至尝试逃逸到宿主机。这是最危险的一层,也是很多简易沙箱方案最容易忽略的。
agentguard的设计思路就是围绕这四个层面,逐层设防。它不是简单地套一个Docker容器,而是在容器之上叠加了系统调用过滤、网络策略、资源配额、代码静态分析四道防线。下面我逐一拆解。
1.3 为什么不用现成的方案
你可能会问:市面上已经有Firecracker、gVisor、nsjail这些成熟的沙箱方案,为什么还要单独做一个agentguard?这个问题我在项目初期也纠结过,后来想明白了:通用沙箱和LLM代码执行沙箱,优化目标不一样。
通用沙箱追求的是“隔离强度最大化”,为此可以牺牲启动速度、可以接受复杂的配置。但LLM代码执行场景有一个特殊需求:每次执行都是短生命周期的,可能几秒钟就结束,而且并发量可能很大。如果每次执行都要启动一个完整的虚拟机,延迟和资源开销都受不了。agentguard的定位是在“足够安全”和“足够轻量”之间找平衡点,它更接近一个进程级沙箱+系统调用白名单的方案,而不是重量级的虚拟化隔离。
另一个原因是可观测性。通用沙箱通常只关心“能不能跑”,但LLM代码执行场景需要知道“跑了什么、访问了什么、有没有异常行为”。agentguard内置了执行审计日志,每一次代码执行都会记录系统调用序列、网络请求、文件访问路径,这些数据对于排查问题和优化提示词都非常有价值。
2. agentguard的核心架构拆解
2.1 整体分层设计
agentguard的架构可以分成四层,从外到内依次是:
接入层负责接收执行请求,做初步的代码静态扫描和参数校验。这一层会检查代码里有没有明显的危险模式,比如subprocess调用、os.system、eval嵌套等。注意,静态扫描不是万能的,它只能拦住“笨”的攻击,真正复杂的恶意代码需要靠后面的运行时防护。
调度层负责资源分配和生命周期管理。每个执行请求会被分配一个独立的沙箱实例,执行完成后立即销毁。调度层还负责并发控制,防止同时执行太多代码把系统压垮。
隔离层是核心,基于Linux的namespace和cgroup实现。每个沙箱实例有独立的PID namespace、mount namespace、network namespace,以及CPU和内存配额。这一层还集成了seccomp过滤器,限制可用的系统调用。
审计层负责记录执行过程中的所有关键事件。包括代码内容、执行时长、退出码、系统调用统计、网络连接记录等。这些日志会异步写入独立的存储,不会阻塞执行流程。
这四层的关系可以用一个简单的流程来描述:请求进来→静态扫描→分配沙箱→执行代码→收集审计日志→销毁沙箱→返回结果。整个流程的设计目标是单次执行延迟控制在200毫秒以内(不含代码本身的执行时间),这个指标在实际测试中基本能达到。
2.2 系统调用白名单的设计逻辑
seccomp是agentguard最核心的安全机制,也是设计起来最纠结的部分。原理很简单:默认禁止所有系统调用,只放行白名单里的调用。但难点在于,白名单要放行哪些调用,才能既保证Python解释器能正常启动,又不给恶意代码留下可乘之机。
我实测下来,一个最小化的Python执行环境需要放行的系统调用大概有60到80个,包括read、write、openat、close、mmap、brk、futex、clone、execve等基础调用。但这里有几个关键决策:
execve要不要放行?如果放行,恶意代码可以通过execve启动其他程序,比如/bin/sh。如果不放行,Python解释器本身就没法启动。agentguard的做法是放行execve,但通过mount namespace限制可执行文件的路径,只允许访问沙箱内部的Python解释器,宿主系统的/bin目录根本不可见。
socket要不要放行?这取决于业务需求。如果LLM生成的代码需要访问外部API,就必须放行网络相关的系统调用。agentguard的做法是默认禁止所有网络调用,但可以通过配置开启“受限网络模式”——只允许连接到白名单里的域名和端口,其他连接一律拒绝。
ptrace要不要放行?绝对禁止。ptrace可以让一个进程调试另一个进程,是容器逃逸的经典手段。agentguard直接把它加入黑名单,任何尝试调用ptrace的代码都会被立即终止。
下面这张表是我整理的几个关键系统调用的处理策略,可以作为你自己配置时的参考:
| 系统调用 | 处理策略 | 理由 |
|---|---|---|
| execve | 放行但限制路径 | Python解释器启动必需,通过mount namespace限制可执行文件范围 |
| socket | 默认禁止,可配置放行 | 防止数据外传,业务需要时开启白名单模式 |
| ptrace | 禁止 | 容器逃逸风险极高 |
| mount | 禁止 | 防止挂载宿主文件系统 |
| reboot | 禁止 | 防止重启系统 |
| kexec_load | 禁止 | 防止加载自定义内核 |
| open_by_handle_at | 禁止 | 可绕过文件权限检查 |
| perf_event_open | 禁止 | 可能泄露侧信道信息 |
注意:seccomp白名单的配置需要根据实际的Python版本和依赖库进行调整。不同版本的CPython在启动时调用的系统调用集合会有差异,建议先用
strace记录一次完整的启动过程,再基于记录结果生成白名单。
2.3 网络隔离的实现细节
网络隔离是agentguard里我花时间最多的部分,因为这里有一个根本矛盾:LLM生成的代码经常需要访问外部资源(比如调用API、下载数据),但网络访问又是数据外传的主要通道。
agentguard的解决方案是双模式网络策略:
模式一:完全隔离。沙箱实例运行在独立的network namespace里,只有一个loopback接口,没有任何外部网络连接。这种模式适合纯计算任务,比如数据转换、格式解析、数学计算等。
模式二:白名单代理。沙箱内部的所有网络请求都必须经过一个本地代理,代理根据预设的白名单决定是否放行。白名单的粒度可以细到“域名+端口+HTTP方法”的组合。比如允许GET https://api.example.com/data,但禁止POST到任何地址。
这个代理的实现有一个关键细节:DNS解析必须在代理层完成,不能让沙箱内部的代码自己解析DNS。否则恶意代码可以通过DNS查询把数据编码在外传请求里。agentguard的做法是沙箱内部只允许连接到一个固定的本地代理地址,所有域名解析和连接建立都由代理完成。
还有一个容易被忽略的点:IPv6。很多沙箱方案只关注IPv4的隔离,忘了IPv6。如果宿主系统开启了IPv6,沙箱内部的代码可能通过IPv6地址绕过IPv4的防火墙规则。agentguard在network namespace创建时会同时禁用IPv6,确保没有遗漏。
2.4 资源配额的计算与设置
资源配额看起来简单,实际上很容易设错。设得太松,恶意代码可以把系统拖垮;设得太紧,正常的代码跑不起来。我根据实际经验总结了一套计算方法:
CPU配额:LLM生成的代码通常是IO密集型或计算密集型的短任务。建议单次执行的CPU时间上限设为10秒,墙钟时间上限设为30秒。这两个值的区别在于:CPU时间只计算实际占用CPU的时间,墙钟时间包括等待IO的时间。如果一个任务CPU时间超过10秒,基本可以判定是死循环或计算量过大。
内存配额:Python解释器本身启动就需要大约20到30MB内存,加上常用的库(比如pandas、numpy)可能要到100MB以上。建议基础配额设为256MB,如果业务需要处理大数据,可以适当上调到512MB或1GB。超过配额时,cgroup会触发OOM killer,沙箱实例会被立即终止。
磁盘配额:沙箱内部的临时文件系统建议限制在64MB以内。大多数LLM生成的代码不需要大量磁盘写入,如果需要处理大文件,应该通过挂载只读卷的方式提供输入数据,而不是让代码自己写文件。
进程数配额:防止fork炸弹。建议限制单个沙箱实例最多创建16个进程。Python解释器本身会创建几个线程,留一些余量是必要的。
这些数值不是拍脑袋定的,而是通过压测得出的。我建议你在自己的环境里也做一轮压测:用典型的业务代码跑100次,记录CPU时间、内存峰值的分布,然后取P99值再上浮50%作为配额上限。
3. 从零搭建一个agentguard实例
3.1 环境准备与依赖安装
agentguard的核心依赖是Linux内核的namespace和cgroup功能,所以它只能在Linux环境下运行。我实测过Ubuntu 22.04和Debian 12,都能正常工作。内核版本建议5.15以上,因为一些新的seccomp特性需要较新的内核支持。
安装步骤不复杂,但有几个坑需要注意:
# 更新系统包 sudo apt update && sudo apt upgrade -y # 安装基础依赖 sudo apt install -y python3.11 python3.11-venv python3-pip \ libseccomp-dev libcap-dev gcc make # 确认cgroup v2已启用 mount | grep cgroup2 # 应该看到类似 cgroup2 on /sys/fs/cgroup type cgroup2 的输出 # 确认user namespace可用 cat /proc/sys/kernel/unprivileged_userns_clone # 输出应该是1如果unprivileged_userns_clone是0,需要修改内核参数。但注意,开启user namespace会带来一定的安全风险,建议只在受控环境中使用。agentguard在生产环境中通常以root权限运行,然后通过降权的方式执行沙箱代码,这样就不需要依赖user namespace。
Python环境建议用虚拟环境隔离,避免污染系统Python:
python3.11 -m venv /opt/agentguard/venv source /opt/agentguard/venv/bin/activate pip install agentguard-core3.2 核心配置文件的编写
agentguard的配置文件是一个YAML文件,定义了沙箱的各项策略。下面是我在实际项目中使用的配置模板,你可以直接参考:
sandbox: # 执行超时设置 timeout: cpu_seconds: 10 wall_seconds: 30 # 资源配额 resources: memory_mb: 256 disk_mb: 64 max_processes: 16 max_open_files: 128 # 文件系统策略 filesystem: root_readonly: true writable_paths: - /tmp/sandbox readable_paths: - /usr/lib/python3.11 - /opt/agentguard/runtime hidden_paths: - /etc/shadow - /root - /home # 网络策略 network: mode: whitelist # 可选: isolated, whitelist allowed_endpoints: - domain: api.example.com port: 443 methods: [GET, POST] - domain: data.example.org port: 443 methods: [GET] dns_resolution: proxy_only # 系统调用策略 seccomp: default_action: deny allowed_syscalls: - read - write - openat - close - mmap - munmap - brk - futex - clone - execve - exit_group # ... 其他白名单调用 blocked_syscalls: - ptrace - mount - reboot - kexec_load - open_by_handle_at - perf_event_open这个配置的核心逻辑是默认拒绝,显式放行。所有没有明确允许的操作都会被阻止。这种设计虽然配置起来麻烦一些,但安全性最高。
提示:
hidden_paths里的路径在沙箱内部会被挂载为空目录,代码尝试访问时会得到“文件不存在”的错误,而不是“权限不足”。这样做的好处是不会泄露宿主系统的目录结构信息。
3.3 执行流程的代码实现
agentguard的核心执行函数大概长这样(简化版):
import subprocess import json import time from pathlib import Path def execute_in_sandbox(code: str, config: dict) -> dict: """ 在沙箱中执行LLM生成的代码 返回: {success, output, error, execution_time, audit_log} """ # 第一步:静态扫描 scan_result = static_scan(code) if scan_result.has_critical_risk: return { "success": False, "error": f"代码静态扫描未通过: {scan_result.reason}", "execution_time": 0 } # 第二步:准备沙箱环境 sandbox_id = generate_sandbox_id() sandbox_dir = Path(f"/tmp/sandbox/{sandbox_id}") sandbox_dir.mkdir(parents=True, exist_ok=True) # 将代码写入沙箱目录 code_file = sandbox_dir / "user_code.py" code_file.write_text(code) # 第三步:构建执行命令 cmd = [ "agentguard-runtime", "--config", json.dumps(config), "--sandbox-id", sandbox_id, "--code-file", str(code_file) ] # 第四步:执行并计时 start_time = time.monotonic() try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=config["sandbox"]["timeout"]["wall_seconds"] ) execution_time = time.monotonic() - start_time return { "success": result.returncode == 0, "output": result.stdout, "error": result.stderr, "execution_time": execution_time, "audit_log": load_audit_log(sandbox_id) } except subprocess.TimeoutExpired: return { "success": False, "error": "执行超时", "execution_time": config["sandbox"]["timeout"]["wall_seconds"] } finally: # 第五步:清理沙箱 cleanup_sandbox(sandbox_id)这段代码里有几个关键点值得展开说:
静态扫描的位置。静态扫描放在沙箱执行之前,目的是快速拦截明显的恶意代码,避免浪费沙箱资源。但静态扫描不能替代运行时防护,因为攻击者可以通过字符串拼接、编码转换等方式绕过静态检测。
沙箱ID的生成。每个执行请求都有唯一的沙箱ID,这个ID用于关联审计日志和临时文件。建议用UUID加时间戳的组合,确保不会冲突。
超时处理。subprocess.run的timeout参数只能控制墙钟时间,不能控制CPU时间。CPU时间的限制需要在沙箱内部通过cgroup实现。两层超时机制配合使用,才能既防止死循环,又防止IO阻塞。
清理逻辑。沙箱执行完成后必须立即清理,包括临时文件、cgroup、namespace等资源。如果清理不彻底,残留的cgroup可能导致资源泄漏,残留的临时文件可能被后续的沙箱实例访问到。
3.4 审计日志的采集与分析
审计日志是agentguard区别于普通沙箱的重要特性。每次执行都会产生一份结构化的日志,包含以下字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| sandbox_id | string | 沙箱实例唯一标识 |
| code_hash | string | 执行代码的SHA256哈希 |
| start_time | timestamp | 开始执行的时间 |
| end_time | timestamp | 结束执行的时间 |
| exit_code | int | 退出码 |
| cpu_time_ms | int | CPU时间消耗 |
| memory_peak_mb | int | 内存峰值 |
| syscall_count | map | 各系统调用的调用次数 |
| network_requests | list | 网络请求记录 |
| file_accesses | list | 文件访问记录 |
| blocked_operations | list | 被阻止的操作 |
这份日志的价值在于事后追溯和模式发现。比如你发现某个提示词模板经常生成触发blocked_operations的代码,就可以针对性地优化提示词。又比如你发现某类任务的CPU时间普遍偏高,就可以调整资源配额。
我实际使用中还有一个技巧:把审计日志和代码内容关联起来做聚类分析。把功能相似的代码聚在一起,看看它们的系统调用模式是否一致。如果某个聚类里出现了异常的系统调用模式,往往意味着这类代码存在潜在风险。
4. 实操中踩过的坑与解决方案
4.1 Python解释器启动失败的排查
这是最常见的问题,表现是沙箱里的代码还没开始执行就报错退出。原因通常是seccomp白名单缺少了某个必要的系统调用。排查方法如下:
# 在宿主机上用strace记录Python启动的完整系统调用序列 strace -f -e trace=all python3.11 -c "print('hello')" 2>&1 | \ grep -oP '^\d+\s+\K\w+' | sort -u > /tmp/syscalls.txt # 对比当前白名单,找出缺失的调用 comm -23 /tmp/syscalls.txt <(sort allowed_syscalls.txt)这个命令会输出白名单里缺少的系统调用。把它们加进去,重新测试。注意,不同Python版本、不同依赖库的启动调用集合会有差异,建议在目标环境中实际跑一遍再确定白名单。
还有一个隐蔽的坑:futex系统调用的参数。Python的线程同步依赖futex,但futex有不同的操作码。如果seccomp规则只允许了部分操作码,线程同步可能会失败。agentguard的默认配置放行了所有futex操作码,因为精确控制操作码的收益不大,反而容易导致兼容性问题。
4.2 内存配额设置不当导致的误杀
内存配额设得太低,正常的代码会被OOM killer干掉。我遇到过好几次“代码在本地跑得好好的,进沙箱就挂”的情况,排查后发现都是内存不够。
这里有一个容易忽略的点:Python的内存分配不是线性的。解释器启动时会预分配一批内存,然后根据代码执行情况动态增长。如果配额刚好卡在启动内存和峰值内存之间,就会出现“有时能跑有时不能跑”的随机失败。
我的建议是:先用一个宽松的配额跑一批典型代码,记录内存峰值的分布,然后取P99值上浮50%作为最终配额。比如P99峰值是180MB,那配额就设270MB。这样既能拦住真正吃内存的恶意代码,又不会误杀正常任务。
另外,memory_peak_mb这个审计字段很有用。定期review这个字段的分布,如果发现某些任务的峰值持续接近配额上限,说明配额可能需要调整,或者这类任务需要单独处理。
4.3 网络白名单的粒度控制
网络白名单的粒度是一个需要权衡的问题。粒度太粗,比如只限制域名不限制路径,恶意代码可以通过合法域名的不同路径外传数据。粒度太细,比如限制到具体的URL路径和查询参数,配置起来非常繁琐,而且业务代码稍微改一下就可能被拦住。
我的经验是分三档处理:
高信任级别:只限制域名和端口,不限制路径和方法。适合内部可信API,比如公司自己的微服务。
中信任级别:限制域名、端口和HTTP方法。适合外部合作伙伴的API,允许GET和POST,但禁止PUT和DELETE。
低信任级别:限制到具体的URL路径前缀。适合公开的第三方API,比如只允许访问/api/v1/data开头的路径。
这个分级策略在实际使用中比较平衡,既不会太繁琐,又能提供足够的安全保障。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 代码执行立即退出 | seccomp白名单缺少必要调用 | 用strace对比系统调用 | 补充缺失的系统调用 |
| 执行超时但CPU时间很短 | IO阻塞或网络等待 | 查看审计日志的wall_time和cpu_time差值 | 检查网络策略,确认是否有连接被挂起 |
| 内存不足被终止 | 配额设置过低 | 查看memory_peak_mb | 上调内存配额或优化代码 |
| 网络请求全部失败 | 白名单配置错误 | 检查allowed_endpoints | 确认域名、端口、方法是否匹配 |
| 文件写入失败 | 可写路径未配置 | 查看file_accesses日志 | 在writable_paths中添加目标路径 |
| 沙箱启动缓慢 | 镜像或运行时加载慢 | 测量沙箱初始化时间 | 预热运行时,或使用更轻量的基础镜像 |
| 并发执行时相互影响 | 资源隔离不彻底 | 检查cgroup配置 | 确认每个沙箱有独立的cgroup |
注意:这张表里的“解决方案”都是治标的方法。如果某个问题反复出现,建议从架构层面重新审视配置策略,而不是每次都打补丁。
4.5 性能优化的几个实用技巧
沙箱的性能开销主要来自三个方面:进程创建、文件系统挂载、seccomp规则加载。我实测下来,一个空沙箱的启动时间大约在50到80毫秒之间,其中seccomp规则加载占了将近一半。
优化seccomp加载速度的方法:把seccomp规则编译成BPF字节码,缓存起来复用。每次创建沙箱时直接加载字节码,而不是重新解析规则文本。这个优化可以把seccomp加载时间从30毫秒降到5毫秒以内。
文件系统挂载的优化:使用overlayfs而不是bind mount。overlayfs的挂载速度更快,而且支持写时复制,多个沙箱实例可以共享同一个只读基础层,只有写入的文件才会占用额外空间。
进程创建的优化:预热一批沙箱实例。维护一个空闲沙箱池,请求到来时直接从池里取一个已经初始化好的实例,执行完成后重置而不是销毁。这个方案可以把端到端延迟从80毫秒降到20毫秒左右,代价是需要额外的内存来维持空闲池。
不过预热池有一个风险:状态残留。如果上一个执行的代码在沙箱里留下了文件或环境变量,下一个执行的代码可能会读到。agentguard的做法是在重置时彻底清理/tmp和/home目录,并重置所有环境变量。这个清理过程本身也有开销,需要权衡。
5. 安全边界的持续验证
5.1 红队测试的常规套路
沙箱做完之后,怎么验证它真的安全?我的做法是自己先当一次攻击者,用各种手段尝试突破沙箱边界。以下是我实际测试过的攻击向量,你可以参考:
文件系统逃逸:尝试通过/proc/self/root、/proc/1/root等路径访问宿主文件系统。agentguard的mount namespace会把这些路径重定向到沙箱内部,访问不到宿主文件。
进程逃逸:尝试通过kill、ptrace等操作影响宿主进程。PID namespace隔离了进程视图,沙箱内部看不到宿主进程,也无法发送信号。
网络逃逸:尝试通过原始套接字、IPv6、DNS隧道等方式绕过网络隔离。agentguard禁用了原始套接字和IPv6,DNS解析强制走代理。
资源耗尽:尝试通过fork炸弹、内存泄漏、磁盘填充等方式耗尽资源。cgroup配额会在达到上限时终止沙箱实例。
seccomp绕过:尝试通过syscall指令直接调用被禁止的系统调用。seccomp的BPF过滤器在内核层面拦截,无法通过用户态手段绕过。
每次测试后,我都会把攻击向量和结果记录到一张表里,作为安全基线的参考。如果某次测试发现了新的绕过方法,就针对性地修补,然后重新测试。
5.2 审计日志的异常检测
审计日志不仅能用于事后追溯,还能用于实时异常检测。我设置了几条简单的规则:
规则一:如果单次执行的blocked_operations数量超过10,标记为可疑。正常代码很少触发被阻止的操作,频繁触发说明代码可能在尝试突破沙箱。
规则二:如果network_requests里出现了白名单之外的域名,立即告警。这说明代理层可能被绕过了,或者白名单配置有误。
规则三:如果cpu_time_ms和wall_time_ms的比值低于0.1,说明代码大部分时间在等待IO或网络。结合network_requests分析,如果等待时间很长但网络请求很少,可能是连接被挂起,需要检查网络策略。
规则四:如果同一来源的代码在短时间内多次触发blocked_operations,说明攻击者可能在尝试不同的绕过方法。这种情况下应该临时提高该来源的审查级别。
这些规则不复杂,但实际运行中确实能发现一些有意思的模式。比如有一次我们发现某个提示词模板生成的代码频繁触发open_by_handle_at被阻止,排查后发现是模型在尝试用这个系统调用读取文件,虽然它本身没有恶意,但这种行为模式值得关注。
5.3 沙箱策略的迭代节奏
沙箱策略不是一次配置好就一劳永逸的。随着业务变化和攻击手法演进,策略需要持续迭代。我的建议是每月做一次策略review,重点看三个指标:
误杀率:正常代码被沙箱阻止的比例。如果这个比例超过1%,说明策略太严格,需要放宽。
漏杀率:恶意代码成功执行的比例。这个指标很难直接测量,但可以通过红队测试和审计日志的异常检测来间接评估。
性能开销:沙箱执行相比裸机执行的额外延迟。如果这个开销超过业务可接受的范围,需要优化沙箱实现。
迭代的时候要注意灰度发布。新的策略先在小范围流量上验证,确认没有大规模误杀后再全量推送。直接全量推送新策略的风险很大,一旦配置有误可能导致所有代码执行失败。
6. 一些个人体会
这个项目做下来,我最大的感受是:LLM代码执行的安全问题,本质上不是技术问题,而是信任边界的问题。传统软件里,代码是人写的,我们对代码的信任建立在代码审查和测试的基础上。但LLM生成的代码,我们无法用同样的方式建立信任——因为它的输出空间太大,无法穷举测试。
agentguard的思路是把信任边界从“代码级别”下沉到“系统调用级别”。不管代码写的是什么,最终都要通过系统调用来与操作系统交互。只要控制住系统调用这一层,就能在不知道代码具体内容的情况下,保证它不会造成实质性伤害。这个思路我觉得是通用的,不仅适用于LLM代码执行,也适用于任何“不可信代码执行”的场景。
另一个体会是可观测性的重要性。很多沙箱方案只关注“拦住”,不关注“记录”。但实际运维中,知道“什么被拦住了、为什么被拦住”比单纯拦住更有价值。审计日志不仅能用于安全分析,还能用于优化提示词、调整资源配额、发现业务异常。我甚至用审计日志发现过一个业务逻辑的bug——某类任务的代码总是尝试访问一个不存在的文件,排查后发现是提示词里给的文件路径写错了。
最后说一个实操中的小技巧:在沙箱里预置一个“安全模板”。对于常见的任务类型(比如数据读取、格式转换、数学计算),预先写好安全的代码模板,LLM只需要填充参数部分。这样既能降低LLM生成恶意代码的概率,又能提高执行效率。当然,这个方案的前提是任务类型足够标准化,对于开放式任务还是得依赖沙箱本身的安全机制。