引言
「相同的输入产生相同的输出」这句话里,真正的难点是什么算输入。如果构建脚本能读宿主机的 /etc/hosts、能继承 $HOME、能随手 curl 一个文件,那么「输入」就变成了整台机器加整个互联网——可复现无从谈起。Nix 的构建沙箱(build sandbox)就是用来把「输入」这个概念收窄到可枚举范围的:只保留声明的依赖、清空环境变量、默认断开网络、用一个专用的非特权用户执行构建。
很多人把沙箱当成「Linux 上的一个安全选项」,出问题就 sandbox = false 关掉。本文要说明的是:关掉沙箱等于放弃可复现性,而绝大多数沙箱失败都有正确的修法。全文拆开沙箱的模式与内部结构、环境变量清洗规则、网络隔离与固定输出例外、纯度检测手段,以及在容器与 CI 环境中沙箱失效的原因。
目录
- 1. 沙箱到底防的是什么
- 2. 三种沙箱模式
- 3. 沙箱内部做了什么
- 4. 环境变量是如何被清洗的
- 5. 网络隔离与固定输出例外
- 6. 检测构建是否纯净
- 7. 有意放宽沙箱的手段
- 8. 沙箱失败的典型原因
- 9. 在容器与 CI 里跑沙箱
- 10. 速查表与一句话记忆
1. 沙箱到底防的是什么
1.1 三类隐式输入
① 文件系统隐式输入:脚本读了未声明的 /etc/passwd、/usr/lib、家目录下的缓存
② 环境隐式输入:继承了宿主机的 LANG、TZ、HOME、PATH,行为随之漂移
③ 网络隐式输入:构建时下载依赖、拉取最新版本、访问私有 registry
这三类输入都不会出现在 .drv 里,因此哈希算不出来,缓存也命中不了。
1.2 后果不是「不安全」而是「不可复现」
症状 A:A 机器构建成功、B 机器失败(B 没装某个系统库)
症状 B:同一 commit 两次构建产物哈希不同(下载到了不同版本)
症状 C:本地能构建、CI 失败(CI 没有 ~/.cache 里的东西)
症状 D:二进制缓存命中率异常低(每次构建的输入其实都不同)
1.3 沙箱要建立的边界
文件系统:只有 /nix/store 里声明过的路径(只读)+ 一个私有可写工作目录
环境变量:由 derivation 属性显式生成,宿主环境一律不带入
网络:默认不存在(除非是声明了输出哈希的固定输出派生)
身份:用专用的非特权构建用户,而不是当前用户
记忆:沙箱防的是三类隐式输入——文件系统、环境变量、网络;它们的后果不是「不安全」而是「哈希算不出来、缓存命中不了、换机器就崩」,因此关沙箱等于放弃可复现性。
2. 三种沙箱模式
2.1 配置项与三种模式
# /etc/nix/nix.conf;NixOS 上写 nix.settings.sandbox = true;
# sandbox = true | relaxed | false
| 模式 | 隔离程度 | 何时用 |
|---|---|---|
true | 完整沙箱:独立挂载/网络/PID 命名空间 + chroot + 专用构建用户 | Linux 默认,生产与 CI 的标准配置 |
relaxed | 仍启用沙箱,但降级掉部分隔离能力 | 内核或容器不允许创建用户命名空间的环境 |
false | 完全关闭:可读宿主文件系统、可访问网络、继承当前用户身份 | 只用于排错对比,绝不用于生产 |
2.2 平台差异
Linux:沙箱依赖命名空间(user/mount/pid/ipc/net),能力完整
macOS:没有等价的内核原语,只能做有限限制,无法提供完整 chroot 隔离
结论:同一份 derivation 在 Linux 上更容易做到封闭,这也是 Linux 作为官方构建平台的原因
2.3 如何确认当前设置
nix show-config | grep -E '^sandbox'
nix --version
记忆:三种模式——
true完整隔离(Linux 默认)、relaxed降级但仍启用(受限内核/嵌套容器)、false只用于排错;macOS 缺少等价内核原语,隔离能力天然弱于 Linux。
3. 沙箱内部做了什么
3.1 一次沙箱构建的建立过程
① 创建新的命名空间:mount / pid / ipc / uts / net(以及 user,视配置)
② 挂载私有根文件系统:
/nix/store → 以只读方式绑定(整个 store 可见)
/tmp、/build → tmpfs,构建的工作目录 NIX_BUILD_TOP
/dev → 最小设备集(null、zero、random、urandom、tty 等)
/proc → 供脚本读取进程信息
/etc → 仅放入必要的静态文件(如 passwd、group、hosts)
③ 切换到专用的构建用户(build-users-group 中的 uid)
④ 清空环境变量,注入 derivation 派生的变量($NIX_BUILD_TOP、$TMPDIR、$out)
⑤ 执行 builder(通常是 bash)
3.2 一个容易被忽略的细节
沙箱把整个 /nix/store 以只读方式暴露,而不是只暴露声明的依赖。
后果:
- 构建脚本可以"恰好读到"某个未声明但已在 store 里的路径
- 这次构建会成功,但该引用会被扫描进 .drv 的引用集,成为运行时依赖
- 若依赖没有被正确声明,换一台没有该路径的机器就会失败
因此:沙箱防的是"宿主文件系统",不能替代"依赖声明正确性"。
3.3 构建用户与权限
# NixOS 上指定构建用户组,构建以组内随机 uid 执行
nix.settings.build-users-group = "nixbld";
id nixbld1 2>/dev/null || getent group nixbld
若构建用户不可用(例如容器里没有创建这些用户),沙箱会直接失败——这是第 8 节要处理的问题。
记忆:沙箱构建 = 新命名空间 + 私有挂载视图(store 只读绑定、tmpfs 工作目录、最小 /dev 与 /proc、精简 /etc)+ 专用构建用户 + 清洗后的环境变量;注意整个 store 都只读可见,因此沙箱不能替代依赖声明的正确性。
4. 环境变量是如何被清洗的
4.1 清洗规则
宿主环境变量:一律不带入(PATH、HOME、LANG、TZ、SSH_AUTH_SOCK 全部消失)
derivation 属性:每个字符串属性变成一个同名环境变量
例如 buildInputs、nativeBuildInputs 会被拼成 PATH
固定注入:NIX_BUILD_TOP、TMPDIR、TMP、TEMP、TEMPDIR、PWD、HOME=/homeless-shelter
NIX_STORE、NIX_BUILD_CORES、NIX_LOG_FD、out / outputHash 等
4.2 一个直观的例子
pkgs.stdenv.mkDerivation {
pname = "demo";
version = "1.0";
MY_FLAG = "enabled"; # → 构建脚本里 $MY_FLAG 可用
buildPhase = ''
echo "flag=$MY_FLAG" # 输出 flag=enabled
echo "home=$HOME" # 输出 home=/homeless-shelter
echo "lang=$LANG" # 输出 lang=(空,宿主 LANG 被丢弃)
'';
}
4.3 需要放行时的做法
# 明确列出允许穿透的环境变量(常用于固定输出下载的代理)
pkgs.fetchurl {
url = "https://example.com/archive.tar.gz";
hash = "sha256-...";
impureEnvVars = [ "http_proxy" "https_proxy" "no_proxy" ];
}
# 更好的做法:不放行,而是把值作为属性显式传入构建
4.4 结构化属性
__structuredAttrs = true 会把属性以 JSON 形式传给构建脚本,
而不是展开成环境变量,避免超长环境变量与转义问题。
记忆:环境变量清洗规则 = 宿主变量全丢、derivation 属性逐条变变量、Nix 固定注入少数几个(HOME 被设为 /homeless-shelter);确实需要穿透时用
impureEnvVars显式列白名单,而不是关沙箱。
5. 网络隔离与固定输出例外
5.1 默认无网络
普通 derivation:没有网络出口,任何 curl / wget / git fetch 都会失败
失败信息通常形如:
curl: (6) Could not resolve host: example.com
fatal: unable to access 'https://...': Could not resolve host
这是设计意图,不是缺陷:构建期联网意味着产物依赖外部世界的当前状态。
5.2 固定输出派生是唯一例外
# 声明了 outputHash 的派生(固定输出)允许联网,因为产物会被哈希校验
src = pkgs.fetchFromGitHub {
owner = "NixOS";
repo = "nixpkgs";
rev = "24.11";
hash = "sha256-AAAA..."; # 校验点:内容不符即失败
};
哈希是「锚」:即使中途内容变化,最终校验会拦住;但哈希只保证「内容就是这一份」,不保证来源可信——信任来自你写下这个哈希的那一刻。
5.3 把联网构建改造成离线构建
反模式:buildPhase 里直接 npm install / cargo fetch / pip download
改造步骤:
① 用固定输出派生把依赖预取到 store(如 npmDepsHash、cargoHash 对应的 fetch 派生)
② 构建阶段使用离线模式(npm ci --offline、cargo build --offline)
③ 校验:把网络彻底断掉后构建仍能成功
检查是否偷偷联网:
nix build .#mypkg -L 2>&1 | grep -iE 'could not resolve|network is unreachable'
记忆:普通派生默认无网络,联网是设计禁止;唯一例外是声明了
hash的固定输出派生(哈希校验内容,但信任仍来自你写下哈希的那一刻);把 npm/cargo/pip 类联网构建改造成「固定输出预取 + 离线构建」。
6. 检测构建是否纯净
6.1 对照法:开沙箱 vs 关沙箱
nix build .#mypkg --print-out-paths # 沙箱下构建
nix build .#mypkg --option sandbox false --print-out-paths # 仅用于对比
判读:两者输出路径相同 → 没有读到宿主环境(纯净)
输出路径不同 → 脚本依赖了未声明的东西,需要定位
6.2 重构建校验
# 强制重新构建并比较结果(可复现性的直接检验)
nix build .#mypkg --check -L
6.3 查看引用集,找出意外依赖
nix-store --query --references ./result # 该产物的直接引用(运行时依赖)
nix why-depends ./result /nix/store/<某路径> # 为什么产物依赖某个路径
nix log .#mypkg | grep -E '/etc/|/usr/|/home/|/var/' # 构建日志里的宿主路径痕迹
多出来的引用往往就是「隐式输入」留下的痕迹。
6.4 求值层面的纯净
# 纯求值模式:禁止 builtins.currentSystem、getEnv 等不纯内建
nix eval --option pure-eval true .#packages.x86_64-linux.mypkg.drvPath
记忆:四种检测手段——
--option sandbox false对照构建(输出路径不同即有隐式输入)、nix build --check重构建、nix-store --query --references与nix why-depends查引用、grep构建日志找宿主路径;求值层面用--option pure-eval true。
7. 有意放宽沙箱的手段
7.1 向沙箱内精确添加路径
# 某些构建确实需要访问特定宿主路径(如虚拟机测试需要 /dev/kvm)
{
requiredSystemFeatures = [ "kvm" ];
}
# 系统侧:nix.settings.extra-sandbox-paths = [ "/dev/kvm" ];
requiredSystemFeatures 让调度器只把该派生派给具备该能力的机器,extra-sandbox-paths 负责把路径放进沙箱——两者配合才能既保留隔离又满足需求。
7.2 旧式逃逸开关与不纯派生
{ __noChroot = true; } # 让该派生不做 chroot 隔离(历史遗留,尽量避免)
{ __impure = true; } # 显式声明"这个构建不保证可复现"
__impure 的代价:
- 产物路径不可复现,二进制缓存无法有效复用
- 依赖它的派生也无法稳定缓存
适用:确实无法封闭的构建(如需要访问本地设备的测试),且要隔离在依赖图边缘
7.3 决策顺序
遇到沙箱失败时的正确顺序:
① 先问:失败是否因为缺少声明?→ 补 buildInputs / nativeBuildInputs
② 再问:是否真的需要宿主路径?→ extra-sandbox-paths 精确添加
③ 最后才考虑:sandbox = relaxed 或 false(并记录原因与影响范围)
记忆:放宽沙箱的手段按优先级——
extra-sandbox-paths+requiredSystemFeatures精确放行 >__noChroot(历史遗留)>__impure(明确放弃可复现);决策顺序永远是先补声明、再精确放行、最后才降级模式。
8. 沙箱失败的典型原因
8.1 无法创建用户命名空间
症状:error: while setting up the build environment: setting up a private
mount namespace: Operation not permitted
原因:内核禁用了非特权用户命名空间,或所在容器限制了 clone/unshare
处理(按优先级):
① 开启内核支持:sysctl kernel.unprivileged_userns_clone=1
② 容器里放开:docker run --privileged,或 --security-opt seccomp=unconfined
③ 确实无法放开时,退到 sandbox = relaxed
8.2 构建用户缺失
症状:error: the build users group 'nixbld' has no members
原因:容器镜像里没有创建 nixbld 用户
处理:在镜像里创建组与用户;或直接使用官方 nixos/nix 镜像
8.3 需要读取宿主 /etc 里的文件
症状:脚本报 /etc/resolv.conf 或 /etc/passwd 缺失
正解:不要添加宿主 /etc,而是把需要的内容作为构建输入传进去
反例:extra-sandbox-paths = [ "/etc" ] ← 直接把宿主环境泄漏进构建
8.4 与代理相关的失败
症状:固定输出派生在企业代理后无法下载
处理:用 impureEnvVars 放行 http_proxy/https_proxy/no_proxy,
而不是关沙箱(固定输出本身允许联网,缺的只是代理环境变量)
8.5 在 macOS 上的期望偏差
症状:macOS 上"关沙箱"与"开沙箱"构建结果一致,误以为沙箱无效
说明:macOS 的隔离能力有限,沙箱主要限制部分操作而非提供完整 chroot
处理:把可复现性验证放到 Linux 构建机上做,macOS 只做开发
记忆:五类沙箱失败——命名空间被禁(放开或 relaxed)、nixbld 用户缺失(建用户或用官方镜像)、脚本想读宿主 /etc(应改为传入输入)、代理变量未放行(用 impureEnvVars)、macOS 隔离能力有限。
9. 在容器与 CI 里跑沙箱
9.1 容器里的三种姿态
| 姿态 | 配置 | 隔离程度 |
|---|---|---|
| 特权容器 | docker run --privileged | 沙箱可正常工作,隔离最强 |
| 放开 seccomp | --security-opt seccomp=unconfined | 多数场景可用 |
| 降级沙箱 | sandbox = relaxed 或 false | 可运行,但放弃封闭性 |
docker run --rm -it --security-opt seccomp=unconfined \
-v "$PWD:/src" -w /src nixos/nix:latest nix build .#mypkg
9.2 CI 上的推荐配置
env:
NIX_CONFIG: |
sandbox = true
experimental-features = nix-command flakes
trusted-users = root runner
要点:
- 保持 sandbox = true;若必须降级,在流水线里显式写注释说明原因
- trusted-users 决定谁能使用额外的沙箱路径与 impure 特性,不要给普通用户
- 与构建缓存配合:只有封闭构建才可能被安全共享,参见二进制缓存篇
9.3 沙箱与供应链可验证性
沙箱保证"构建只依赖声明的东西",这是可追溯的前提:
- 产物 → derivation → 输入哈希 这条链才有意义
- 若构建能读宿主文件或联网,链条断裂,产物来源无法证明
因此沙箱不是"安全加固的加分项",而是供应链可验证性的基础设施。
需要设备(如 nixosTest 的 /dev/kvm)时:
- 系统侧 extra-sandbox-paths = [ "/dev/kvm" ]
- 派生侧 requiredSystemFeatures = [ "kvm" ]
这样既保留沙箱,又精确放行了唯一需要的设备。
记忆:容器里三种姿态(privileged / seccomp=unconfined / 降级沙箱),CI 上尽量保持
sandbox = true并把降级原因写进注释;沙箱是供应链可验证性的前提而不是加分项,需要设备时用extra-sandbox-paths+requiredSystemFeatures精确放行。
10. 速查表与一句话记忆
| 主题 | 要点 |
|---|---|
| 默认值 | Linux 上 sandbox = true |
| 环境变量 | 宿主变量全丢,HOME=/homeless-shelter |
| 网络 | 默认无;仅固定输出派生可联网 |
| 纯净检测 | --option sandbox false 对照、nix build --check |
| 精确放行 | extra-sandbox-paths + requiredSystemFeatures |
| 最后手段 | sandbox = relaxed(记录原因) |
一句话记忆:沙箱把构建的「输入」收窄成可枚举集合——私有挂载视图(整个 store 只读可见、tmpfs 工作目录、最小 /dev 与 /proc)、由 derivation 属性生成的环境变量(宿主变量全丢、HOME 设为 /homeless-shelter)、默认断开的网络(唯一例外是声明了 hash 的固定输出派生);检测是否纯净用「--option sandbox false 对照构建看输出路径是否一致」加 nix build --check 与 nix why-depends;遇到失败按「先补依赖声明 → 再用 extra-sandbox-paths 精确放行 → 最后才降级模式」的顺序处理,因为沙箱不是安全加分项,而是可复现与供应链可验证性的前提。
延伸阅读
- 可复现与封闭构建
- derivation 与 store 内幕
- 构建调试与错误排查
- NixOS 安全加固实战
- Linux 容器隔离原理 — 命名空间与 cgroup 的底层机制
- 制品仓库与来源证明 — 供应链可追溯的工程实践
- Nix 手册:sandbox 配置
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。