Secure Boot 作为 UEFI 规范中防止恶意引导代码执行的核心机制,长期以来却是 NixOS 生态中缺失的一环。Lanzaboote 项目填补了这一空白 —— 它以 Rust 实现的 UEFI stub 为核心,结合 lzbt 签名工具与 NixOS module 系统集成,为 NixOS 提供了一条完整且工程化的可信启动链。本文深入解析其技术实现细节,包括 UKI 打包策略、签名验证流程、Measured Boot 支持,以及从部署配置到生产落地的关键参数。
传统 UKI 打包的困境与 NixOS 的特殊性
标准 UKI(Unified Kernel Image)将 Linux 内核、initrd 以及内核命令行参数统一打包为单个 EFI 可执行文件。这一设计简化了签名流程 —— 只需对最终的 UKI 二进制文件进行签名,UEFI 的 LoadImage 调用即可在加载时自动完成签名验证。systemd-boot 等引导管理器随后只需加载这个签名后的 UKI,控制权随后转交给其中嵌入的 kernel 和 initrd。
然而,NixOS 的 generational 模型使这一方案遭遇根本性瓶颈。NixOS 允许用户在不退化旧配置的前提下测试升级,每次 nixos-rebuild switch 都会生成一个独立的新 generation。这些 generation 共享大量相同的基础组件(同一个 kernel 版本、同一份 initrd),但按照传统 UKI 模式,每个 generation 都必须拥有一份包含完整 kernel 和 initrd 的独立 UKI 文件。以一个运行超过 20 个 generation 的活跃系统为例,ESP(EFI System Partition,典型容量 512MB)在几周内就会面临耗尽风险。
Lanzaboote 的核心创新正在于打破 kernel 和 initrd 必须内嵌于 UKI 的约束。它实现的 stub 仅携带路径引用而非实际二进制内容,从而在维持 UEFI 签名验证链完整性的前提下,大幅削减了每个 generation 的存储占用。
Lanzaboote UEFI Stub 的设计与实现
Lanzaboote stub 是一个符合 UKI 规范却不包含内嵌 kernel/initrd 的 UEFI 应用程序,位于 nix-community/lanzaboote 仓库中,采用 Rust 语言基于 rust-osdev/uefi-rs 开发。项目选择 Rust 的原因在于两点:标准库支持的 x86_64-unknown-uefi 目标架构已升至 Tier 2 级别,配套工具链成熟稳定;其次 uefi-rs 封装层有效降低了直接操作 UEFI Runtime Services 的复杂度。
从功能分解角度看,Lanzaboote stub 完成了三项核心工作。首先,它通过 UEFI LoadImage 接口加载外部 kernel 文件 —— 由于该调用本身执行签名验证,任何对 kernel 二进制文件的篡改都会导致加载失败。其次,stub 在 UKI 签名中嵌入了 initrd 的加密哈希值:当 stub 加载 initrd 时,会计算实际读取到的 initrd 哈希并与嵌入值比对,匹配失败则中止启动流程。这种设计在避免内嵌体积的同时,确保了 initrd 的完整性校验。最后,stub 将内核命令行参数和启动状态信息传递给 kernel,完成控制权的正式交接。
对比 systemd-stub(systemd 项目提供的标准 UKI stub)可以更清晰地理解这一权衡。systemd-stub 将 kernel 和 initrd 直接打包进 UKI ELF 镜像中,加载时直接在内存展开,无需额外文件系统访问。其优点是签名验证范围覆盖完整引导载荷,缺点是每个 generation 都必须复制一份完整的 kernel + initrd 对。Lanzaboote 的策略恰好相反:用路径引用替代内嵌内容,将签名验证的职责卸载给 UEFI LoadImage,而对 initrd 则使用嵌入式哈希进行补充验证。
需要特别指出的是,Lanzaboote stub 本身仍然需要经过签名。stub 的签名内容中包含了 initrd 哈希值,如果攻击者试图替换 initrd 文件,即使替换文件的签名验证仍然通过(因为 stub 本身未被篡改),stub 在加载 initrd 时也会检测到哈希不匹配而拒绝继续。这种分层防御设计在不内嵌 initrd 的约束下实现了等效的安全保证。
lzbt 工具链:签名、UKI 组装与 ESP 管理
lzbt(Lanzaboote Tool)是 Lanzaboote 项目提供的命令行工具,负责从 NixOS bootspec 描述文件开始,依次完成签名、UKI 组装和 ESP 部署的完整流水线。bootspec 是 NixOS 23.05 起默认启用的 RFC 规范,它以结构化格式描述了一个 NixOS 配置的启动所需组件 ——kernel 路径、initrd 路径、内核参数、启动文件列表等。
实际工作流程如下:lzbt 读取系统当前所有可启动的 NixOS generation 对应的 bootspec 文档,对每个 generation 的 kernel 和 initrd 应用签名操作(使用用户自己生成的密钥,或经由 PKCS#11 接口访问硬件安全模块),随后使用 Lanzaboote stub 组装成 UKI 文件,最终将 UKI 连同 stub 自身的签名文件一并写入 ESP 的对应目录。工具还能感知 generation 生命周期 —— 当某个 generation 被垃圾回收删除时,相关的签名和 UKI 文件也会被清理。
签名的执行环境是一个关键的安全考量。Lanzaboote 的设计理念是私钥不应出现在 NixOS 构建上下文中,因为构建环境本身不应被信任。因此,签名操作发生在已运行系统中,而非 nix-build 的沙盒内。这一决策虽然增加了部署流程的复杂性,但避免了私钥泄露风险,也符合最小特权原则。用户需要预先通过 sbctl 或手动步骤生成 PKI 证书链(Platform Key、Key Exchange Key、Signature Database Key),并将签名密钥安全存储。
Measured Boot 与 TPM 集成
除 Secure Boot 外,Lanzaboote 项目还支持 Measured Boot 路径。Measured Boot 利用 TPM 2.0 的 PCR(Platform Configuration Registers)对引导过程中每个阶段的度量值进行累加扩展,最终形成一条不可篡改的引导轨迹记录。这些 PCR 值可用于绑定磁盘加密密钥 —— 例如 LUKS2 卷的密钥可以仅在 TPM 报告的 PCR 值与预期匹配时才释放。
具体到 Lanzaboote 的实现,stub 在执行过程中会对 kernel、initrd 和内核参数等关键组件的哈希值进行扩展写入 TPM PCR。配合 Nitrokey 或 YubiKey 等 PKCS#11 设备存储加密密钥,用户可以构建一个即使攻击者获取了物理磁盘也无法读取数据的防御体系。这一方案独立于 Secure Boot 运行 —— 系统可以在仅启用 Measured Boot 而禁用 Secure Boot 的情况下工作,反之亦然,但两者结合能提供最深层的防护。
需要注意的是,Measured Boot 本身不阻止未经授权的代码执行,它的作用是提供可验证的启动完整性报告。因此 NixOS Wiki 明确建议:最佳实践是同时启用 Secure Boot(阻止未签名代码执行)+ Full Disk Encryption(防止物理磁盘读取)+ BIOS 密码保护(阻止从外部介质引导或修改引导顺序)。单独启用 Measured Boot 而不配合其他安全措施,其防护效果将大打折扣。
NixOS Module 系统集成与部署配置
Lanzaboote 通过 Nixpkgs 的 module 系统实现透明集成。用户无需手动调用 lzbt,只需在 configuration.nix 中启用相应选项,系统在 nixos-rebuild switch 时即可自动完成 Secure Boot 相关的所有配置变更。
核心配置项包括启用 lanzaboote 模块本身、配置 Secure Boot 签名密钥路径(指向之前生成的 DB 密钥)、设置 TPM 策略(若使用 Measured Boot),以及可选的 fwupd 集成 —— 当两者同时启用时,fwupd.service 的启动脚本会确保固件更新过程中使用的 fwupd 二进制文件也是签名版本,防止因固件更新流程绕过 Secure Boot 验证。
当前状态是,Lanzaboote 作为 nix-community 仓库的独立项目维护,集成方式是向用户 configuration.nix 添加 Flakes 引用或 overlay。项目团队正在积极推进上游至 Nixpkgs 的工作,目标是让 Secure Boot 支持成为 NixOS 安装后的开箱即用功能。NLnet 和 NGI Assure 基金为该项目提供了资金支持,确保了开发的持续性。
从实际部署角度,以下几点值得注意。首先是 PKI 初始化:用户需要使用 sbctl 或手动步骤在离线环境中生成密钥对,将公钥导出为 EFI 签名列表(.efi 格式)后注册到 UEFI 固件中。这一步是不可逆的固件操作,错误配置可能导致系统无法引导,务必在执行前备份原有 PKI 状态。其次是 ESP 容量规划:即使 Lanzaboote 大幅减少了每个 generation 的占用,ESP 仍然需要预留足够空间。建议至少 1GB ESP 分区以容纳多个并存的 generation。最后是内核升级注意事项:当内核版本升级时,已签名 UKI 仍然有效(因为签名的是 stub 而非 kernel),但 lzbt 会在 nixos-rebuild switch 时重新为新 kernel 生成 UKI,整个过程对用户透明。
与 mokutil/PreLoader 的路径对比
在 Lanzaboote 出现之前,社区曾尝试通过 mokutil 和 PreLoader 组合为 NixOS 添加 Secure Boot 支持。该方案的核心思路是使用一个小型已签名的 PreLoader 引导程序加载一个未签名但功能完整的 bootloader,由 PreLoader 负责验证 bootloader 签名并在签名失败时拒绝执行。这一方案虽然能在不重新打包内核的情况下工作,但其安全模型较弱 ——PreLoader 仅验证直接下一跳的组件,无法覆盖完整的启动链。
Lanzaboote 采用的 UKI 模型则不同:整个引导链从 UEFI → systemd-boot → UKI → kernel 全部处于签名验证的保护范围内。攻击者若试图在任何环节注入恶意代码,签名验证将立即失败。相比之下,PreLoader 方案中存在一个未被签名的中间层,存在被绕过的理论可能。因此,从安全架构的完整性角度,Lanzaboote 的 UKI 路径是更优选择。
工程化落地的关键参数清单
部署 Lanzaboote 前,建议确认以下系统条件:UEFI 固件支持 Secure Boot(bootctl status 输出中 Secure Boot 项非 unsupported);TPM 2.0 可用(若使用 Measured Boot);ESP 分区至少 1GB 且格式为 FAT32;NixOS 版本 ≥ 23.05(bootspec 默认启用)。密钥生成推荐使用 sbctl create-keys 在离线 live environment 中完成,并将私钥备份至安全位置(建议使用 YubiKey 等硬件密钥存储)。ESP 目录结构上,Lanzaboote 生成的 UKI 文件位于 /EFI/Linux/ 子目录,签名数据库位于 /EFI/keys/,fwupd 集成文件位于 /run/fwupd/。
若在配置过程中遇到签名验证失败导致系统无法引导,可以使用 mokutil --import 将签名密钥临时注册为 Machine Owner Key(MOK)以进入维护模式,或在 UEFI 固件设置中切换至 Setup Mode 以重新管理签名数据库。
小结
Lanzaboote 项目以 Rust 实现的高质量 UEFI stub 为核心,通过路径引用加 initrd 哈希验证的混合策略,在不内嵌 kernel/initrd 的前提下维持了完整的 Secure Boot 信任链。lzbt 工具链将签名、UKI 组装和 ESP 管理集成进 NixOS 的构建 - 部署流程,nix-community 仓库中的 module 系统配置使其对用户透明。尽管上游至 Nixpkgs 的工作仍在推进中,但其设计理念和实现质量已在实际部署中得到了充分验证。对于需要在 NixOS 上启用 UEFI Secure Boot 的生产环境,Lanzaboote 是目前最成熟且社区活跃的解决方案。
资料来源:Lanzaboote 官方文档与 GitHub 仓库(https://x86.lol/generic/2022/11/26/lanzaboote.html、https://github.com/nix-community/lanzaboote/)
内容声明:本文无广告投放、无付费植入。
如有事实性问题,欢迎发送勘误至 i@hotdrydog.com。