sandbox-exec Policy 编写指南:基本结构与规则

适用场景:sandbox-exec -f xxx.sb

一、Policy 基本结构

SBPL 基于 Scheme 语法(Lisp 变体),采用 S-Expression(S 表达式)。

1. 最小结构

一个最基础的策略文件通常包含版本声明和默认行为:

(version 1)

(allow default)                            ; 默认允许所有操作
(deny file-read* (subpath "/Users/lihu/.ssh")) ; 显式拒绝读取 .ssh 目录

2. 语法组成

单条规则的基本格式如下:

(action permission (condition))

图解示例

(deny file-read* (subpath "/Users/lihu/.ssh"))
                    
 动作    权限类型    匹配条件
  • 动作 (Action): allow (允许) 或 deny (拒绝)
  • 权限 (Permission): 如 file-read*, network*, process-exec
  • 条件 (Condition): 如路径匹配、网络地址匹配等

二、核心文件权限分类

文件权限控制是沙盒策略中最常用的部分,主要分为以下三类:

1. file* (完全控制)

file*

定义:拥有对文件的所有权限。 等价于file-read* + file-write* + 元数据修改 + 打开 + stat + rename + unlink

👉 特点:读写删改全封死。

适用场景

  • 私钥 (~/.ssh)
  • Keychain 数据
  • Secrets / 凭证文件
  • 核心敏感资产

示例

(deny file* (subpath "~/.ssh"))

2. file-read* (读取相关)

file-read*

覆盖操作

操作 category 具体 syscall/command 是否包含
读取内容 read, cat, grep
元数据 ls, stat, lstat
打开文件 open(O_RDONLY)
内存映射 mmap (read)

不包含:没有任何写入、删除或创建文件的权限。

👉 特点:只能防泄密,不能防破坏。

3. file-write* (写入相关)

file-write*

覆盖操作

操作 category 具体 syscall/command 是否包含
创建/修改 touch, > 重定向, truncate
删除/移动 rm, mv, unlink
目录操作 mkdir, rmdir
属性修改 chmod

👉 特点:控制破坏能力(防篡改)。


三、细分文件权限

除了带 * 的聚合权限,SBPL 还提供了更细粒度的控制:

  • file-read-data / file-read-metadata: file-read* 的子集。
    • file-read-data: 读取文件内容。
    • file-read-metadata: 读取文件属性 (stat, ls)。
  • file-write-data: 仅允许修改文件内容,不允许删除或重命名。
  • file-write-create: 仅允许新建文件,不允许覆盖现有文件。
  • file-rename: 控制 mv 操作。
  • file-unlink: 控制 rm 操作。
  • file-metadata: 控制属性修改(chmod, chown, utime)。

⚠️ 实战建议:90% 的场景下,直接使用 file*, file-read*, file-write* 即可满足需求,无需过度细分。


四、路径匹配规则

在定义文件路径时,常用的匹配器如下:

1. subpath (子路径匹配)

(subpath "/Users/lihu/.ssh")

含义:匹配该目录路径及其下属的所有子目录和文件。 ✅ 推荐:最常用的匹配方式。

2. literal (精确匹配)

(literal "/Users/lihu/.ssh/id_ed25519")

含义:仅匹配指定路径及其对应的单个文件,不包含子内容(除非是文件本身)。

3. regex (正则匹配)

(regex "^/Users/lihu/.*\\.key$")

含义:使用正则表达式匹配路径。 ⚠️ 注意:开销较大且容易出错,慎用。

4. home-relative-path (家目录相对路径)

(home-relative-path ".ssh")

含义:等价于 ~/.ssh。 ✅ 优点:可移植性好,脚本在不同用户下通用。


五、Allow / Deny 优先级规则

这是编写 Seatbelt 策略最容易混淆的地方,由于其基于 Pattern Matching(模式匹配),遵循 “Specific overrides General” (具体覆盖宽泛) 的原则,通常表现为 Allow/Deny 互相 “打洞” (Hole punching)。

规则 1:Deny 优先于同级或更宽泛的 Allow

当一个操作同时被 Allow 和 Deny 覆盖时,如果范围相同或 Deny 更具体的针对该区域,通常 Deny 生效。

;; 1. 宽泛允许
(allow file-read* (subpath "/Users/lihu"))

;; 2. 局部拒绝 (在允许范围内挖一个洞)
(deny  file-read* (subpath "/Users/lihu/.ssh"))

结果

  • /Users/lihu/file.txt ✅ 可读 (匹配 Allow)
  • /Users/lihu/.ssh/id ❌ 拒绝 (匹配更具体的 Deny)

👉 结论:Deny 可以覆盖(Override)宽泛的 Allow。

规则 2:更具体的路径优先 (例外/打洞)

反之亦然,可以在一个大的 Deny 范围内,Allow 一个小子目录。

;; 1. 宽泛拒绝
(deny file* (subpath "/Users/lihu"))

;; 2. 局部允许 (在拒绝范围内挖一个洞)
(allow file-read* (subpath "/Users/lihu/test"))

结果

  • /Users/lihu/private ❌ 拒绝
  • /Users/lihu/test/log ✅ 可读

👉 结论:更具体的 Allow 可以覆盖宽泛的 Deny。

规则 3:全局默认与局部调整

这是最推荐的策略编写模式:先定义基准(Default),再定义例外。

(allow default)  ;; 基准:默认全开 (黑名单模式)

;; ... 添加具体的 deny 规则 ...
(deny file* (subpath "/Users/lihu/.ssh"))

六、default 的真实含义

(allow default)

黑名单模式 (Blacklist Mode)

  • 含义:未明确禁止的操作,默认全部允许。
  • 适用:Smoke Test(冒烟测试)、调试初期、兼容性要求高的场景。

(deny default)

白名单模式 (Whitelist Mode)

  • 含义:未明确允许的操作,默认全部禁止 (Abort)。
  • 适用:Hardened(加固)系统、生产环境、最终发布的策略。

七、典型安全模板

模板 1:特定敏感目录保护

适用于一般程序,防止读取 SSH 密钥。

(deny file* (home-relative-path ".ssh"))

模板 2:工作区模式

仅允许程序在一个特定目录下折腾。

(allow file-read*  (subpath "/opt/project_build"))
(allow file-write* (subpath "/opt/project_build"))

模板 3:Homebrew 只读保护

防止脚本篡改系统工具。

(allow file-read*  (subpath "/opt/homebrew"))
(deny  file-write* (subpath "/opt/homebrew"))

模板 4:终极严格模式 (示意)

(version 1)
(deny default) ; 白名单模式,拒绝所有

;; 允许系统基本库读取 (否则程序起不来)
(allow file-read* (subpath "/usr/lib"))
(allow file-read* (subpath "/System/Library"))

;; 业务逻辑
(allow file-read*  (subpath "/opt/openclaw"))
(allow file-write* (subpath "/opt/openclaw"))
(allow network-outbound) ; 允许联网