第六章 · 实验 D:LUKS 全盘加密绑定 TPM 自动解锁
前面三个实验都是“玩票“性质的:extend 几个 PCR、封一个小文件、读一读日志。这一章动真格的——把虚拟机的全盘加密(LUKS)绑定到 TPM 上,实现开机自动解锁。这基本就是 Windows 上 BitLocker + TPM 的 Linux 对应实现,也是 TPM 在家用场景里最有存在感的一个用法。
本章目标
改造前:每次开机都要在 initramfs 里敲一遍 LUKS 密码。
改造后:开机全程不用敲密码,TPM 自己确认“启动环境没被动过“之后,自动把卷密钥解封给 LUKS,直接进系统。
如果有人在固件层动了手脚——改了 Secure Boot 配置、换了平台密钥——TPM 拒绝交出密钥,机器退回密码提示。
背景知识:这件事为什么成立
第三章我们验证过 PCR 哈希链,第四章玩过 Seal/Unseal。本章做的事,用第四章的话说就是:把 LUKS 的卷密钥 Seal 到 TPM 的 PCR 状态上。
- 卷密钥本身不存进 TPM,而是以加密 blob 的形式放在 LUKS 头的 token 区里;
- 开机时 initramfs 里的 systemd-cryptsetup 读 token,拿 blob 去找 TPM 解封;
- TPM 检查当前 PCR 值和 seal 时绑定的 policy 是否一致,一致才吐出密钥。
所以“自动解锁“不是“去掉密码“,而是“多了一把由 TPM 保管的钥匙“。原密码 slot 一直留着,任何时候 TPM 不给面子,都能用密码兜底。理解这一点很重要,下面整个实验的安全性都建立在它上面。
第一步:侦察——我的磁盘到底长什么样
动全盘加密之前,先搞清楚现状。别跳过这一步,改启动链的翻车里有一半是“以为自己知道分区布局“。
lsblk -f
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS
vda
├─vda1 vfat FAT32 1234-ABCD 1019M 0% /boot
└─vda2 crypto_LUKS 2 43bf0994-f67b-4b1c-b247-8b5d0ca269f5
└─root btrfs root-uuid-... /
读法:
vda1是 vfat 格式的 ESP(EFI 系统分区),挂在/boot,明文。UKI、引导器配置都在这,这不奇怪——它们本来就签着名、被度量着,不需要加密。vda2是 LUKS2 加密容器,UUID 是43bf0994-f67b-4b1c-b247-8b5d0ca269f5,这个 UUID 后面改 cmdline 要用,抄下来。root是 LUKS 解开之后露出来的 dm 设备,里面是 btrfs 根文件系统。
插曲:我一度以为 LUKS 根本没生效。
有一次开机输密码时手滑敲错了,结果系统照样进来了。我当时心里一惊:这 LUKS 是摆设吗?
查完才知道不是:busybox 体系的
encrypt钩子在密码错误时会重试,默认给 3 次机会。我敲错的那次只是第一次,第二次敲对了,自然就进来了——密码提示一闪而过,看起来像“没输也进了“。教训:判断加密有没有生效,别靠“开机要不要密码“这种表面现象,
lsblk -f里crypto_LUKS下面套着root这层结构才是证据。设备层级不会骗你。
第二阶段之一:enroll——给 TPM 开一把新钥匙
侦察完,动手的第一步出乎意料地简单:
sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 /dev/vda2
它会提示你输入现有的 LUKS 密码。注意这个密码的用途:不是验证身份那么简单,而是用它解开卷密钥,然后为 TPM 这把“新钥匙“在 LUKS 头里开一个新的 keyslot。
这个阶段是零风险的。 enroll 只是新增一个 keyslot,不碰原有密码 slot。就算后面全搞砸了,密码永远能进。可以放心折腾。
为什么是 PCR 7
--tpm2-pcrs=7 这个参数值得停下来讲清楚,它是整个安全模型的核心选择。
PCR 7 度量的是 Secure Boot(安全启动)状态,具体包括 Secure Boot 开关、以及 PK / KEK / db / dbx 这些密钥和证书库的内容。选它的理由:
- 稳定:Secure Boot 配置不会天天变。相比之下,PCR 4(bootloader、内核)、PCR 11(UKI 里的内核和 initrd)每次升级内核都会变,绑了它们意味着每次系统更新后都要重新 enroll。
- 敏感得恰到好处:一旦有人在固件层动手脚——关掉 Secure Boot、换掉平台密钥、往 db 里塞自己的证书——PCR 7 立刻变,TPM 拒绝解封,自动解锁当场失效。这正是我们想要检测的那类攻击。
思考:PCR 7 够吗?
不够,它有盲区——bootloader 和 initramfs 被整个换掉时 PCR 7 可以保持不变。这个盲区本章先不展开,第八章讲 Evil Maid 攻击时会把它彻底讲透。目前先记住:绑 PCR 7 防的是“固件配置被篡改“,不是“启动文件被替换“。
验证:token 真的写进去了
sudo cryptsetup luksDump /dev/vda2 | grep -A10 Tokens
Tokens:
0: systemd-tpm2
tpm2-hash-pcrs: 7
tpm2-pcr-bank: sha256
tpm2-primary-alg: ecc
tpm2-blob: AJEA...
tpm2-srk: gAS6AA....
tpm2-pin: false
Keyslot: 1
逐项解读:
systemd-tpm2:这是 LUKS2 的 token 机制,告诉解锁工具“这个 slot 的密钥材料在 TPM 里,去找 systemd 的 TPM2 解锁逻辑“。tpm2-hash-pcrs: 7和tpm2-pcr-bank: sha256:绑定 PCR 7,用 SHA-256 bank。tpm2-primary-alg: ecc、tpm2-srk:blob 是封在存储根密钥(Storage Root Key, SRK)派生的 primary key 之下的——第四章说过,SRK 是存储层的“房梁“,所有 seal 对象的父级。tpm2-blob:这就是第四章里seal.pub+seal.priv两个文件的打包形式——公钥部分 + 加密后的私钥部分,存成一段 base64。tpm2-pin: false:没加 PIN。加了的话开机还是要输东西,就违背本章目标了。Keyslot: 1:占用了 keyslot 1。slot 0 还是你的密码,没动。
到这里,“密钥侧“完成了。但故事只讲了一半——LUKS 头里有了 TPM 钥匙,不等于开机时有人会用它。负责解锁的是 initramfs,而我们的 initramfs 现在还不认识 TPM。
第二阶段之二:initramfs 侦察(本章的侦探时间)
按 Arch Wiki 的通用教程,下一步应该是“把 HOOKS 里的 encrypt 换成 sd-encrypt,然后 mkinitcpio -P“。我先没急着照抄,而是把这套启动栈摸了一遍——很庆幸摸了,因为 Omarchy 的启动栈和 wiki 上的标准画法差得相当远。
发现一:明面上的 HOOKS 有问题
grep ^HOOKS /etc/mkinitcpio.conf
HOOKS=(base systemd autodetect microcode modconf kms keyboard keymap consolefont block filesystems fsck)
systemd 体系的钩子列表,但既没有 encrypt 也没有 sd-encrypt。按常理,这样的 initramfs 根本解不开 LUKS——可系统明明每天都在解密启动。矛盾,说明这份配置不是真相。
发现二:真相在 drop-in 里
cat /etc/mkinitcpio.conf.d/omarchy_hooks.conf
HOOKS=(base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs resume)
mkinitcpio.conf.d/ 下的 drop-in 会整体覆盖主配置里的 HOOKS。真实生效的是这一套 busybox 体系:udev 而非 systemd,encrypt 而非 sd-encrypt。这就解释了为什么系统能正常解密——它压根没走 systemd 路线。
发现三:cmdline 是 busybox 风格
cat /proc/cmdline
root=/dev/mapper/root rw cryptdevice=PARTUUID=72afbdf0-...:root resume=/dev/mapper/root ...
cryptdevice=PARTUUID=...:root 是 busybox encrypt 钩子认的参数;systemd 体系认的是 rd.luks.uuid= / rd.luks.name=。后面改 cmdline 时这个对应关系不能搞错。
发现四:启动方式是 UKI + Limine
ls /boot/EFI/Linux/
omarchy_linux.efi
一个文件,内核 + initrd + cmdline 全打包在里面(Unified Kernel Image, UKI),Limine 引导器直接加载它。limine.conf 里的条目路径还带着 #哈希值——那是 Limine 对 UKI 做的完整性校验,UKI 内容变了哈希就得跟着更新,否则拒绝启动。
发现五:cmdline 有三个来源
这是我排查时最绕的一个点。最终拼进 cmdline 的参数来自三层:
- 基础层:
/etc/default/limine里的KERNEL_CMDLINE[default],主要参数(cryptdevice=、root=、resume=)都在这; - drop-in 追加层:
/etc/limine-entry-tool.d/*.conf,比如resume.conf往里面追加resume=参数; - 启动时叠加层:
limine.conf里的cmdline:行,由 Limine 在启动时通过 EFI 的 LoadOptions 追加。
前两层会被打进 UKI,第三层不进镜像、启动时才叠上。改 cmdline 要改第一层,改错了层会百思不得其解“为什么我改了没生效“。
发现六:重建工具不是 mkinitcpio preset
ls /etc/mkinitcpio.d/
(空的)
标准 Arch 的 preset 文件不存在。Omarchy 用的是 limine-mkinitcpio——它背后调 /usr/share/libalpm/scripts/limine-mkinitcpio-install,用 mkinitcpio --uki 构建 UKI,并顺手更新 limine.conf 里的条目路径和哈希。这意味着重建 UKI 必须用 limine-mkinitcpio,直接 mkinitcpio -P 会生成镜像但 limine.conf 的哈希不更新,结果就是启动被拒。
坑:别拿通用教程直接套定制化发行版。
现代发行版的启动栈高度定制:谁生成 UKI、谁更新引导配置、HOOKS 从哪一层生效,每家都不一样。改启动链之前,先花半小时摸清“谁生成什么“,比照着 wiki 抄错之后修砖划算得多。
第三阶段:改造
侦察完毕,方案清晰了:把 busybox 体系换成 systemd 体系(sd-encrypt 原生支持 TPM2 解锁),cmdline 换成 rd.luks.* 风格。
a. 备份
sudo cp /etc/default/limine /etc/default/limine.bak
sudo cp /boot/EFI/Linux/omarchy_linux.efi /boot/omarchy_linux.efi.bak
一份配置备份,一份能启动的 UKI 备份。这是后悔药,别省。
b. 改 cmdline 基础层
编辑 /etc/default/limine,把 KERNEL_CMDLINE[default] 里的
cryptdevice=PARTUUID=72afbdf0-...:root
替换为:
rd.luks.uuid=43bf0994-f67b-4b1c-b247-8b5d0ca269f5 rd.luks.name=43bf0994-f67b-4b1c-b247-8b5d0ca269f5=root
UUID 就是第一步 lsblk -f 里抄下来的 LUKS 容器 UUID。
rd.luks.name=...=root 这一句不能省:它指定解开后的 dm 设备还叫 root。cmdline 里的 root=/dev/mapper/root 和 resume=/dev/mapper/root 都指着这个名字,改了设备名它们就全断了——休眠恢复(resume)会第一个挂掉。
c. 覆盖 HOOKS
新建 /etc/mkinitcpio.conf.d/zz-tpm2.conf:
sudo tee /etc/mkinitcpio.conf.d/zz-tpm2.conf <<'EOF'
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole plymouth block sd-encrypt filesystems fsck sd-btrfs-overlayfs resume)
EOF
文件名前缀 zz- 是为了让它在字母序里排最后,压过 omarchy_hooks.conf 的覆盖——drop-in 链是后来者居上。
钩子映射关系:
| busybox 体系(原) | systemd 体系(新) | 说明 |
|---|---|---|
udev | systemd | initramfs 的事件处理核心 |
keymap consolefont | sd-vconsole | 控制台键盘和字体 |
encrypt | sd-encrypt | LUKS 解锁,原生支持 TPM2 token |
btrfs-overlayfs | sd-btrfs-overlayfs | Omarchy 的 overlay 根目录机制 |
最后一条值得注意:sd-btrfs-overlayfs 是 Omarchy 自带的 systemd 变体钩子。发行版作者显然预想到了有人会走这条路,连自家定制钩子都准备了双份——这给了我们很大信心:这条改造路径是官方预留的,不是野路子。
想还原也简单:删掉 zz-tpm2.conf,恢复 /etc/default/limine,重新构建即可。
d. 重建 UKI
sudo limine-mkinitcpio
真实构建输出的关键行:
==> Using drop-in configuration file: zz-tpm2.conf
==> Starting build: '6.x.x-arch'
-> Running build hook: [base]
-> Running build hook: [systemd]
...
-> Running build hook: [sd-encrypt]
-> Running build hook: [filesystems]
...
==> Creating unified kernel image: /boot/EFI/Linux/omarchy_linux.efi
==> Image generation successful
盯两件事:Using drop-in configuration file: zz-tpm2.conf 确认我们的覆盖生效了;hook 列表里出现 sd-encrypt 且没有 encrypt,确认钩子换对了。
e. 重启前验证:TPM2 组件真的进镜像了吗
这是个好习惯,强烈建议养成:重启之前,先从 UKI 里把 initrd 抠出来检查一遍。
sudo objcopy -O binary --only-section=.initrd \
/boot/EFI/Linux/omarchy_linux.efi /tmp/initrd.img
lsinitcpio /tmp/initrd.img | grep tss2
usr/bin/systemd-cryptsetup
usr/lib/cryptsetup/libcryptsetup-token-systemd-tpm2.so
usr/lib/libtss2-esys.so.0
usr/lib/libtss2-mu.so.0
usr/lib/libtss2-rc.so.0
usr/lib/libtss2-tcti-device.so.0
...
一共约 40 个相关文件。解读一下这份清单:
systemd-cryptsetup:systemd 体系里负责解 LUKS 的程序;libcryptsetup-token-systemd-tpm2.so:让 cryptsetup 认识systemd-tpm2token 的插件——就是我们 luksDump 里看到的那个 token 类型;libtss2-*:TPM2 软件栈(tpm2-tss)的库,和 TPM 通信全靠它们。
这些文件不是我们手动加进去的——sd-encrypt 钩子检测到系统里装了 tpm2-tss,就自动把它们打进 initramfs。钩子作者把这条路也铺好了。
坑:UKI 是 0600 root 权限,
objcopy要用 sudo。我第一遍没加 sudo,
objcopy静默失败,/tmp/initrd.img是个空文件,后面lsinitcpio的输出自然也是空的——我差点得出“TPM 组件没打进去“的错误结论。教训有两层:一,诊断命令失败时先看输入文件是否有效(
ls -l /tmp/initrd.img一眼就能看出来是 0 字节);二,别用2>/dev/null把错误吞掉,排查问题时错误信息是你唯一的朋友。
第四阶段:重启验证
深呼吸,重启。
如果一切正常,你会发现:密码提示没有了。屏幕闪过引导画面,直接进系统。
来点日志证据,确认不是“碰巧进去了“:
journalctl -b -u "systemd-cryptsetup@*"
systemd[1]: Starting Cryptography Setup for root...
systemd-cryptsetup[...]: Successfully activated the device.
systemd[1]: Finished Cryptography Setup for root.
systemd-cryptsetup 无交互地完成了解密。它读了 LUKS 头里的 token,拿 blob 找 TPM,TPM 检查 PCR 7 一致,交出密钥,收工。
这说明什么
从按下电源到进入系统,信任是这么传递的:
- 固件度量 Secure Boot 状态 → PCR 7;
- systemd-cryptsetup 请求 TPM 解封卷密钥;
- TPM 核对 PCR 7 == seal 时的值 → 一致,放钥匙;
- LUKS 用卷密钥解开根分区,系统启动。
第四章你亲手 seal/unseal 的那个小玩具,和这套生产机制是同一套原语。区别只是:这次 blob 放在 LUKS 头里,解封发生在开机最早的几十秒里。
翻车实录:heredoc 抢了 sudo 的 stdin
这段是我真实翻的车,原样保留,因为它正好演示了这类改造最危险的地方。
我第一次操作时,流程是这样的:先 sed 改了 /etc/default/limine(cmdline 换成 rd.luks.*),然后写 zz-tpm2.conf 用了一条类似这样的命令:
echo 'mypassword' | sudo -S tee /etc/mkinitcpio.conf.d/zz-tpm2.conf <<'EOF'
HOOKS=(...)
EOF
问题:sudo -S 从 stdin 读密码,而 heredoc 也占着 stdin——heredoc 抢了密码的位置,sudo 认证失败,tee 没执行,zz-tpm2.conf 根本没写出来。更要命的是管道把错误吞掉了大半,看起来“命令跑完了“。
紧接着我跑了 sudo limine-mkinitcpio。它忠实地用**旧钩子(busybox encrypt)+ 新 cmdline(rd.luks.*)**这个自相矛盾的组合打了包。如果我当时重启,必然起不来:busybox 的 encrypt 钩子在 cmdline 里找不到 cryptdevice=,会直接放弃解锁。
运气(或者说习惯)救了我:重启前我按 e 步做了 initramfs 验证,发现组件不对,回头 cat /etc/mkinitcpio.conf.d/zz-tpm2.conf——文件不存在,真相大白。重写文件、重建、再验证,第二次就正常了。
三条教训:
- 这类改造的危险在中间态:配置已改、镜像未重建(或重建错了)的窗口期里,机器是“必然起不来“的状态。这个窗口越短越好,操作尽量一口气做完。
- 每步执行后验证产物:写完配置
cat一下,重建完看构建日志的 hook 列表。命令“跑完了“和“生效了“是两回事。 - 兜底设计要在动手前就位:就算真起不来,
sd-encrypt解不开会自动退回密码提示(密码 slot 还在);虚拟机有快照;UKI 有.bak备份。三层兜底,任何一层都能救命。
安全模型讨论:自动解锁之后,谁在守门
改完重启,一切丝滑。但我很快注意到一个此前没想过的问题:系统直接进了桌面,连登录器都没经过。
查了 /etc/sddm.conf.d/autologin.conf——Omarchy 默认给 SDDM 配了自动登录。这在改造前是合理的:LUKS 密码就是开机唯一的一道门,进门之后再要一次密码纯属重复劳动。
但 TPM 自动解锁之后,情况变了。现在的开机流程是:按电源 → TPM 放钥匙 → 自动登录 → 直达桌面。全程零输入。
把防护能力摆出来看:
- 防拔盘离线读取:✓。硬盘被拆走,离开这台机器的 TPM 和正确的 PCR 状态,盘上就是一堆密文。
- 防整机抱走:✗。别人把整台机器拿走,按下电源键,直达你的桌面。
也就是说,当前配置保护的是“数据在盘里的机密性“,不保护“整机的现场安全“。这个模型和 BitLocker 的默认形态(TPM-only)是一样的,不是缺陷,是一个明确的取舍。
如果你想要“自动解锁 + 进系统要密码“——比如笔记本经常带出门——删掉 autologin 即可:
sudo rm /etc/sddm.conf.d/autologin.conf
这样 TPM 负责解开磁盘(防离线读取 + 防固件篡改),登录密码负责拦住当面开机的人。两道门各管各的,互不重复。真机部署时,这个取舍要根据自己的使用场景想清楚。
思考:能不能让 TPM 也要求输点东西?
可以。
systemd-cryptenroll支持--tpm2-with-pin=yes,解封时要求输一个 PIN。这是“TPM 持有 + 你记忆“的双因素,安全性更高,代价是开机又要输东西了——和本章“零输入“的目标背道而驰。安全性和便利性的天平,永远是你自己来调。
小结
本章干了什么:
lsblk -f摸清分区布局,确认 LUKS 真的在保护根分区(别靠表面现象判断);systemd-cryptenroll --tpm2-pcrs=7给 TPM 开了新 keyslot,原密码 slot 原封不动;- 侦探式排查了 Omarchy 的启动栈:drop-in 覆盖 HOOKS、UKI 打包、cmdline 三层来源、
limine-mkinitcpio重建工具; - 换 systemd 体系钩子 +
rd.luks.*cmdline,重建 UKI,重启前验证 initramfs 组件; - 开机零输入进系统,日志确认 TPM 无交互解锁;
- 复盘了一次 heredoc 抢 stdin 的翻车,和“中间态最危险“的教训;
- 厘清了自动解锁后的安全模型:防拔盘,不防抱机;以及 autologin 的取舍。
到这里,本书的实战篇就完整了:从手动 extend 一个 PCR,到生产级的全盘加密自动解锁,用的始终是同一套原语——度量、PCR、seal。
但还记得本章埋下的那个伏笔吗:我们绑的是 PCR 7,它度量固件和 Secure Boot 配置。如果攻击者不动固件,而是直接替换 /boot 里的 UKI——反正 ESP 是明文的——PCR 7 不会变,TPM 照样放钥匙。这个盲区有多真实、Secure Boot 在其中扮演什么角色,第八章“Evil Maid 攻击“见。