SecureBoot + dm-verity 打造经签名的救援系统 发布于 2023/06/01主页

介绍

在配置 LUKS + TPM + SecureBoot 后(参考资料:来自鱼塔塔的 Arch Linux on Btrfs RAID with LUKS),服务器的物理安全性提升了一个等级。唯一的问题是,当系统出现问题时,由于 initramfs 内的环境极其有限(busybox),很难进行修复。

与大部分人在系统出问题后会选择进入 Archiso 不同,由于我的 SecureBoot 是 Enforce 状态,我的机器并不能引导至 Archiso。由于我是服务器平台及我的安全配置,关闭 SecureBoot 非常复杂和漫长:我需要等待数分钟的自检进入平台设置禁用 SecureBoot,然后再禁用配置锁,然后再次重启等待数分钟的自检进入 Archiso;修复后,我需要重启进入平台设置启用 SecureBoot 和配置锁,然后再次重启使用密码重设配置锁。为了进入一次 Archiso,我需要等待4次漫长的自检过程,输入四次密码。这个过程极其繁琐,并且增大了凭据泄漏的风险(配置锁为文本密码)。另外就是这个过程似乎还会导致我的 TPM 拒绝解锁,即使 PCR 已通过认证(错误显示无法验证 TPM 完整性,似乎是配置锁这种比较重要的东西变动后可以提前一步防止 TPM 解封),必须用户输入密码解密。由于我最近弃用了文本密码,因此我无法进入系统;其次,每次我这么操作的时候本质都是在降级或关闭保护,这增加了维护风险和维护难度;另外就是每次进入 Archiso 都需要手动配置网络、软件等,耗时耗力。

综合以上问题,我选择新装一个系统,同样是 ArchLinux。这个系统平时并不使用,rootfs 通过 dm-verity 验证完整性。因此,我可以在里面提前配置好我所需要的工具,并且配置自己的鉴权。由于启动映像经过签名,只需重启即可进入救援系统,无需其他设置。

本文涉及到的一些技术与程序:SecureBoot, dm-verity, brd, btrfs seeding, systemd, mkinitcpio

配置

我的系统本身是两块盘 Btrfs RAID1,EFI 文件放在插在 Internal USB 口上的 U 盘里。由于把整个系统打包进 .efi 文件似乎不是很可行的样子,所以我需要单独分区做系统盘;dm-verity 也需要空间存放 checksum,因此把 U 盘重新分区为3个分区。第一个分区为 EFI 分区,第二个分区为 Btrfs 的救援系统的 rootfs,第三个分区为 dm-verity 分区。

在 U 盘上安装好系统后(在 Arch 中为另一张盘安装 Arch?请参考 wiki 条目:Install Arch Linux from existing Linux),在系统里配置自己想要的东西即可,对于我来说,我安装了wireguard-tools tcpdump openssh等包方便调试。另外也推荐在/etc/systemd下为机器配置基本的环境,例如网络、时间同步等。完成后,记得使用systemctl enable启用这些服务。

dm-verity的配置过程略过,可参考 ArchWiki 上的 dm-verity 条目,不过这个条目针对的是使用dracut作为生成 initcpio 工具的用户。如果你就是dracut用户,理论上按照 wiki 配置即可。不幸的是mkinitcpio并没有直接提供用于dm-verity的 hook,这会导致 initramfs 内缺少相关的 target 与依赖,所以systemd-verifysetup@.service并不会自动启动,因此 rootfs 无法被挂载。我搜遍了全网也没有找到,因此只能自己糊一个了。

/etc/initcpio/install/sd-verity
#!/bin/bash build() { add_binary "/usr/lib/systemd/systemd-veritysetup" add_binary "/usr/lib/systemd/system-generators/systemd-veritysetup-generator" add_module dm-verity map add_systemd_unit 'veritysetup.target' \ 'remote-veritysetup.target' } help() { cat <<HELPEOF This hook allows for an dm-verity root device with systemd initramfs. HELPEOF } # vim: set ft=sh ts=4 sw=4 et:

这是一个很简陋的 hook,由于它并不包含服务所需的依赖,你还需要引入sd-encrypt。hooks 最后看起来像是这样:

HOOKS=(base systemd sd-encrypt sd-verity filesystems keyboard)

如果你只是想要一个使用了mkinitcpio + systemd + veritysetup的根系统,那么故事到这里就结束了;事实上,这样做重启也是可以进入系统的。不过,由于根目录并不可写(dm-verity特性),这不方便我在救援系统内操作。

接下来需要使用一些比较手动挡拖拉机的方式,让根系统跑在内存里。在这里,我使用了 Btrfs 的 Seeding 功能, 他可以将其中一个不可写设备(例如光盘或者本文中的文件系统)作为播种设备,把状态保存在另一张可写盘上。由于我并不需要持久化的救援系统,因此我选择将状态保存在内存中,类似一般的 LiveCD。

使用这个功能需要根系统为 Btrfs 驱动。如果你没有或不想,也可以使用 overlayfs 等方式。Github 上的 mkinitcpio-overlayfs 可以实现。需要注意的是,这个 hook 与本文所提到的配置一起使用时是不开箱即用的。

为了让 initramfs 挂载根目录后增加写设备,我们需要增加一个 service,使其在sysroot.mount之后,initrd-switch-root.target之前运行。

/etc/systemd/system/setup-rootfs.service
[Unit] Description=Mount ramdisk for rootfs After=sysroot.mount Before=initrd-switch-root.target [Service] Type=oneshot ExecStart=/usr/local/bin/setup-rootfs RemainAfterExit=yes [Install] RequiredBy=initrd-switch-root.target
/usr/local/bin/setup-rootfs
#!/bin/sh modprobe -r brd modprobe brd rd_nr=1 rd_size=8388608 # 8G btrfs device add /dev/ram0 /sysroot # systemd 后续的服务会自动 remount 为 rw,无需自行 remount。

你还需要在mkinitcpio.conf中增加brd模块,将/usr/local/bin/setup-rootfs设置为可执行。

由于mkinitcpio中的systemdhook 提供的add_systemd_unit()只会搜索/usr/lib/systemd/system:/lib/systemd/system这两个目录,因此我们需要手动添加服务到 initramfs 内。首先使用systemctl enable setup-rootfs启用服务,然后将其放入mkinitcpio.conf即可,mkinitcpio会自动处理链接。

此时mkinitcpio.conf差不多为:

MODULES=(usb-storage uas brd) # brd 提供基于 RAM 的 Block Device;usb-storage 与 uas 提供 USB 存储设备支持。
BINARIES=()
FILES=(/usr/local/bin/setup-rootfs /etc/systemd/system/initrd-switch-root.target.requires/setup-rootfs.service)
HOOKS=(base btrfs systemd sd-encrypt sd-verity filesystems keyboard)

在系统内的配置工作设定完成后,将文件系统卸载并将其标记为播种设备即可,然后计算分区的 checksum 并将其填充进内核参数打包为 .efi 文件。重启后引导 .efi 文件后即可进入由 RAM 驱动的环境。这样一来,救援系统的根目录由dm-verity保护,而dm-verity中的 roothash 作为 cmdline 的一部分被打包进 .efi 由 SecureBoot 的证书签名。实现了从机器上电到进入系统的全链条保护。

结语

配置完成后,我只需要重启即可进入我熟悉的环境对我的系统进行更改,一切都是开箱即用的。由于这是一个完整的系统,我也可以在里面自由的配置 PAM 等鉴权,设置一些服务(例如引导后自动擦除 TPM 芯片和硬盘的 TPM 解锁槽位)保证数据安全。以后系统爆炸后不再需要繁琐的流程即可安全地进行修复。不过如果救援系统也卡 emergency 了怎么办?是不是需要一个救援系统的救援系统

附录

kernel cmdline 配置节选

roothash=[ROOTHASH] systemd.verity_root_data=UUID=[lsblk -o name,uuid] systemd.verity_root_hash=UUID=[lsblk -o name,uuid] systemd.verity_root_options=panic-on-corruption

若引导时出现问题,去除quiet参数并设置loglevel=7即可看到详细过程。

配置 initramfs 的 root 密码以进入 Emergency Shell

mkinitcpio.conf里配置FILES=(/etc/shadow)即可从现有的系统复制账户密码到 initramfs。现有系统的 root 需要有一个有效的密码。

配置无密码(空密码)登录 Shell

请谨慎使用,可能导致安全问题。

passwd --delete [username]

若不再希望账户可被空密码登录,但又不想设置一个密码,可以通过passwd -l [username]锁定账户

若想让系统拒绝空密码登录请求,移除 PAM 配置中pam_unix.sonullok参数即可。

固定救援系统内的 SSH 主机密钥

在密封救援系统前,在里面执行/usr/bin/ssh-keygen -A

你可能还想要固定 systemd machine-id:systemd-machine-id-setup

警告:dm-verity并不提供机密性。