AI Agent删库跑路怎么防?拆解DeepSeek Harness沙箱安全围栏
文章目录
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
前言
做AI Agent开发的兄弟,谁没脑补过Agent删库跑路的名场面?
你让它改个配置文件,它反手给你执行rm -rf /;你让它清理日志,它把系统盘一起扬了。能跑命令、能写文件的Agent,说好听点是生产力工具,说难听点就是没装保险丝的电锯——用着爽,出事就是重伤。
今天就聊DeepSeek Harness的沙箱系统,看看人家是怎么给Agent套上安全围栏的。
1 先给沙箱定个性:只管文件系统效果
很多人以为沙箱是万能盾,网络、进程、啥都能管。错了,dsh的沙箱定义特别明确:就管文件系统那点事。
网络访问限不限制?进程能不能看见宿主?这些都不在沙箱的业务范围内。想管网络?去进程层面做,沙箱只管“文件能不能读、能不能写”这件事。
1.1 三种模式,对应三种信任等级
直接上代码,沙箱模式就三种:
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
给大家翻译成人话:
read-only:纯观光模式。只能看文件,啥也改不了,临时文件都只能往/dev/null里写。适合代码审查、只读分析这种场景,就像你去博物馆,只能看不能摸,摸了就报警。
workspace-write:正常办公模式。工作目录随便你写,系统目录想都别想,/tmp也给你用。这是最常用的模式,就像公司给你分配了工位,你在自己工位上贴海报吃外卖都行,去总裁办公室乱翻试试?
danger-full-access:全权托管模式。啥都能写,根目录都给你敞开。注意了,这个模式根本不走沙箱服务,直接原始执行,相当于你把家门钥匙、银行卡密码全交出去了,不是极度信任的环境千万别开。
补个冷知识:read-only模式下,POSIX的runner还会给shell留个/dev/null当“垃圾桶”,Windows的ACL runner就比较抠,连个可写的地方都不给。
2 强制执行完整性:别拿partial当full用
沙箱后端会报告自己的执行完整性,就俩值:full和partial。
type SandboxEnforcement = 'full' | 'partial'
full就是承诺的所有文件效果都管住了,实打实的安全边界。partial就是因为各种原因,只能管住一部分,有缺口。
2.1 哪些场景会出现partial?
给大家列一下坑:
| 平台 | partial原因 | 影响范围 |
|---|---|---|
| Linux | 旧Landlock ABI版本 | 部分文件操作拦不住 |
| Windows | ACL runner的Everyone边界 | 环境ACL有缺口 |
| Windows | 硬链接边界 | 能通过硬链接绕过去 |
这里必须夸一句dsh的原则:fail loud,no silent degradation。
啥意思?宁可直接拒绝执行,也不偷偷降级。我做不到就是做不到,告诉你这是partial,你自己掂量着用。不像有些产品,宣传的时候说安全拉满,实际防护半残,还不告诉你,等出事了才发现裤衩都漏风。
划重点:需要绝对安全边界的场景,别用partial,直接拒绝,别凑合。
3 逐调用策略:不是全局配置,是每次调用单独算
很多人以为沙箱策略是全局配一次就完事了,dsh偏不,每次能力调用都会生成一份独立的执行策略。
interface SandboxExecutionPolicy {
mode: SandboxMode // 文件效果模式
workspaceRoot: string // workspace-write 可写的绝对根目录
sessionId?: SessionId // 调用会话的 opaque 身份标识
}
3.1 策略解析的优先级链
策略怎么来的?有个优先级链:
1. 显式批准的 mode 覆盖(用户审批后重试时携带)
↓ 如果没有
2. 会话策略
↓ 如果没有
3. 部署配置回退(没有 agent 时)
最有意思的是workspaceRoot的来源:会话的当前工作目录,而且要做两步规范化,不能光看字面路径。
为啥要两步?因为有符号链接的坑啊。比如路径是/home/user/link/…,词法上看是/home/user,实际link可能指向完全别的地方,光改字符串就被骗了。
这种设计的好处是什么?两个并发会话,可以向同一个沙箱服务要不同的边界,互不干扰,不用改服务状态。就像你和你室友合租,各自房间各自锁,不用出门还要给大门换锁,方便得很。
4 三个平台后端实现,原生隔离各显神通
dsh-sandbox-local给三个平台都做了原生隔离后端,不是一套方案到处套。
4.1 Linux:bwrap + Landlock 双保险
Linux这边是组合拳:bwrap做命名空间隔离,给你整个独立的文件系统视图;Landlock做内核级的访问控制,在内核层面把写入限制焊死。
看bwrap的参数就懂了:
// bwrap 命令行参数
export function bwrapProfileArgs(policy: SandboxPolicy): string[] {
const args = [
'--ro-bind', '/', '/', // 根文件系统只读挂载
'--dev', '/dev', // 挂载 /dev
'--proc', '/proc', // 挂载 /proc
'--die-with-parent' // 父进程退出时杀子进程
]
if (policy.mode === 'workspace-write') {
args.push('--tmpfs', '/tmp') // /tmp 用 tmpfs
args.push('--bind', policy.workspaceRoot, policy.workspaceRoot) // 工作区可写挂载
}
return args
}
根目录直接只读挂载,工作区单独挂成可写,父进程挂了子进程立刻陪葬,防止变成孤儿进程搞事情。
4.2 macOS:Seatbelt 内核沙箱
macOS用的是系统自带的Seatbelt,用SBPL语言写策略。逻辑很简单:默认允许所有操作,先拒绝所有写入,再把允许写的路径一个个加上。
export function seatbeltProfileArgs(policy: SandboxPolicy): string[] {
const forms = [
'(version 1)',
'(allow default)',
'(deny file-write*)',
`(allow file-write* (literal "${sbplString('/dev/null')}"))`
]
const roots = writableRoots(policy)
if (roots.length > 0) {
forms.push(`(allow file-write* ${roots.map(root => `(subpath "${root}")`).join(' ')})`)
}
return ['-p', forms.join(' ')]
}
相当于先把所有门都锁死,再给你配几把钥匙开对应的门,简单粗暴但有效。
4.3 Windows:ACL 受限令牌
Windows这边是给子进程一个受限的安全令牌,靠ACL控制权限。read-only模式下连个显式可写目录都不给,而且因为环境ACL有缺口,只能报告partial。
懂的都懂,Windows的权限系统向来都是“看起来很严,实际总能找到缝”,老传统了。
4.4 三个后端横向对比
| 维度 | Linux bwrap+Landlock | macOS Seatbelt | Windows ACL |
|---|---|---|---|
| 隔离机制 | 命名空间 + 内核LSM | 内核沙箱策略 | 受限令牌 + ACL |
| read-only可写区 | /dev/null | /dev/null | 无 |
| workspace-write可写区 | /tmp + workspace | /tmp + workspace | workspace |
| 完整性 | full(新内核)/ partial(旧Landlock) | full | partial |
| 进程隔离 | 是(命名空间) | 部分 | 部分 |
多说一句:容器、microVM、远程执行这些,都是同级的执行环境方案,不是沙箱的后端。它们是直接替换整个执行环境的,和本地沙箱是平行选项。
5 沙箱在工具链里的位置:不是内嵌,是包装
很多人以为沙箱是在工具内部调用的,其实不是。它是在tools/execute的事件链里,作为wrapper注入进去的。
以bash-sandbox为例,执行流程是这样的:
模型请求执行 bash 命令
|
v tools/pre-execute (waterfall)
|-- permission-presets: 静态规则匹配
|-- user-approval: 需要时人工审批
|
v tools/execute (waterfall)
|-- bash-sandbox 包装器:
| 1. ctx.sandboxPolicy.resolve(session) 解析策略
| 2. mode = danger-full-access -> 直接 spawn
| 3. mode = read-only 或 workspace-write -> ctx.sandbox.run()
| 4. 沙箱 Provider 包装 argv:
| Linux: bwrap --ro-bind / / ... -- bash -c "command"
| macOS: sandbox-exec -p "(deny file-write*)..." bash -c "command"
| Windows: 创建受限令牌进程
| 5. 执行命令,收集输出
| 6. 记录 mode + enforcement 到 processFacts
|
v tools/post-execute (waterfall)
|-- 结果处理
说白了,就是命令执行前必经沙箱这一关,给你包装一层再跑。就像你寄快递,快递员不是直接把东西扔车上,是先给你套个快递盒,再贴上面单,出问题也知道是哪个包裹的。
而且bash-sandbox和bash-local是两个平行的Provider,实现同一个接口,消费方根本感知不到区别。想用哪个配哪个,代码不用改。
6 权限审批和沙箱:分工明确,不抢活
dsh的权限系统和沙箱是协作关系,不是一回事。
| 层 | 包 | 职责 | 介入时机 | 机制 |
|---|---|---|---|---|
| 权限预设 | dsh-permission-presets | 静态规则匹配 | tools/pre-execute | allow/deny/ask |
| 用户审批 | dsh-user-approval | 人工确认 | tools/pre-execute | ask后追问 |
| 沙箱 | dsh-sandbox-local | 文件效果隔离 | tools/execute | argv包装 |
| 单调守卫 | dsh-tools(内置) | 不可推翻否决 | tools/execute | guard() |
| 环境清理 | dsh-bash-local | env脱敏 | 执行前 | ENV_OVERRIDES |
一句话总结:权限系统问“能不能做”,沙箱管“做了能影响啥”。一个管决策放行,一个管后果边界,分层不重叠。
举个例子:默认workspace-write被权限系统拒了,用户审批后同意用danger-full-access重试,这时候沙箱就直接放行,不做隔离了——用户都拍板了,沙箱就不拦着了。
7 防御性模式里的两个安全细节,很容易被忽略
dsh的防御性模式里有两条规则,看着不起眼,实际踩过坑的都知道有多重要。
7.1 绝不把敏感环境变量暴露给子进程
工具执行的时候,会先清理环境变量,所有带KEY、SECRET、TOKEN、PASSWORD的变量全给你扬了。
const ENV_OVERRIDES = {
NO_COLOR: '1',
TERM: 'dumb',
PAGER: 'cat',
GIT_PAGER: 'cat',
}
别觉得这是小事。之前就有公司的Agent执行脚本,把环境变量里的云服务密钥打日志里泄露了,直接损失六位数。把密钥放环境变量里,再传给不可信的子进程,纯属把银行卡密码写在便利贴上贴显示器上。
7.2 删除文件:链接和目录分开处理
删除文件的时候,先判断是不是符号链接。是链接就删链接本身,不是再递归删目录。
// 防止跟随符号链接删除意外目标
if (lstatSync(path).isSymbolicLink()) {
unlinkSync(path) // 链接用 unlink
} else {
rmSync(path, { recursive: true }) // 真实目录才用递归删除
}
这个坑踩过的人估计后背都发凉。要是不判断,一个指向根目录的符号链接,你递归删除,直接把整个系统盘给清了,删库跑路都没这么快。
8 沙箱是Seam:想换后端?消费方代码一行不用改
沙箱本质上是个接口,不是具体实现。你想自己写个Docker后端?完全可以,实现ctx.sandbox.run这个接口就行。
给大家看个Docker后端的示例:
import { SandboxService, type SandboxPolicy, type SandboxResult } from '@deepseek-ai/dsh-sandbox'
class DockerSandbox extends SandboxService {
async run(argv: string[], options: ExecOptions, policy: SandboxPolicy): Promise {
if (policy.mode === 'danger-full-access') {
return spawnDirectly(argv, options)
}
const dockerArgs = [
'docker', 'run', '--rm',
'--read-only', // 只读根文件系统
]
if (policy.mode === 'workspace-write') {
dockerArgs.push('-v', ${policy.workspaceRoot}:/workspace:rw)
dockerArgs.push('--workdir', '/workspace')
}
// Docker 额外可限制网络(超出沙箱词汇表但 Docker 可做)
dockerArgs.push('--network', 'none')
dockerArgs.push('dsh-sandbox:latest')
dockerArgs.push(...argv)
const result = await spawnDirectly(dockerArgs, options)
return {
...result,
enforcement: 'full' as const // Docker 容器隔离是完整的
}
}
}
写完之后,配置里换一下Provider名就行,上层的bash-sandbox代码一行都不用动。
这就是Seam的意思:接口统一,实现可替换。Linux用Landlock,macOS用Seatbelt,你想用Docker用Docker,消费方完全感知不到变化。就像你家插座,插什么牌子的充电器都能充电,不用为了换个充电器改家里的电路。
9 凭据隔离:配置只带引用,不带真实值
安全层还有个重要的部分:凭据服务。
// 凭据引用是 POSIX 风格环境变量名
type CredentialRef = Branded<'CredentialRef'>
// 解析结果
interface ResolvedCredential {
value: string // 非空机密值
source: string // 提供方定义的来源层 id
}
啥意思?配置文件里只写变量名,比如DEEPSEEK_API_KEY,不写真实的密钥值。值归凭据Provider管,每次操作的时候实时解析。
好处太多了:
首先,配置文件泄露了也不怕,里面都是引用,没有真实值。其次,密钥轮换不用重启服务,下一次模型请求自动用新值。
对比一下老系统:改个密钥要改配置,改完要重启服务,重启要等半天,业务都停摆了。dsh这设计,轮换密钥悄无声息就完成了。
10 最后放张全景表,部署的时候照着查
| 层 | 包 | 职责 | 机制 |
|---|---|---|---|
| 权限预设 | dsh-permission-presets | 静态规则匹配 | tools/pre-execute |
| 用户审批 | dsh-user-approval | 人工确认 | tools/pre-execute |
| 沙箱 | dsh-sandbox / dsh-sandbox-local | 文件效果隔离 | tools/execute |
| 单调守卫 | dsh-tools(内置) | 不可推翻否决 | tools/execute |
| 环境清理 | dsh-bash-local | env脱敏 | 执行前 |
| 凭据隔离 | dsh-credentials-local | 机密不进配置 | 凭据服务 |
| spill保护 | dsh-spill-policy | 工具输出溢出处理 | tools/code-dispatch-log |
其实dsh的安全层说复杂也不复杂,核心就是每一层只管一件事,组合起来形成完整边界。权限管放行,沙箱管边界,凭据管机密,各司其职,不瞎掺和。
毕竟做AI Agent安全,最怕的就是“大而全”的设计,啥都想管,最后啥都管不好。不如像dsh这样,分层明确,每个模块做好自己的事,反而更靠谱。
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
更多推荐


所有评论(0)