本文记录一下我最近从 LUKS 迁移到 LUKS + LVM 的过程。整理是最好的复习!

背景

Device mapper 是 Linux 里将块设备映射成虚拟块设备的框架。

dm-crypt 是用 DM 进行透明加密的组件。例如:将 /dev/sda2 映射成 /dev/mapper/cryptsda2,则往 /dev/mapper/cryptsda2 这个块设备写入的数据会被加密后实际写入下层的 /dev/sda2 块设备里。

LUKS 是以 dm-crypt 为基础,增加了密钥管理功能的加密实现。

我的笔记本电脑是这样的分区结构:

NAME             MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
nvme0n1          259:0    0 476.9G  0 disk
├─nvme0n1p1      259:1    0   512M  0 part  /boot
├─nvme0n1p2      259:2    0 450.0G  0 crypt /
└─nvme0n1p3      259:3    0  26.4G  0 crypt [SWAP]

一个引导分区(p1),一个主分区(p2),一个交换分区(p3)。其中主分区是用 LUKS 加密的,需要我每次开机时输入密码进行解锁;交换分区是以 plain dm-crypt 加密的,其密钥来自 /dev/urandom 提供的随机数据。交换分区以随机密钥进行加密是一种常见做法,密钥只存在于 RAM 里,关机之后交换分区就完全无法解密了,防止 RAM 里的敏感数据留在交换分区里被读取——尤其是在异常关机的时候。

休眠、但是醒不来

这样的加密交换分区有一个问题:无法休眠,确切地讲是能休眠,但是永远醒不来——因为断电之后密钥已经被丢弃了,所以重新开机的时候系统无法读取交换分区里的数据,所以无法恢复到之前的状态。

由于我的笔记本电脑几乎一直是插着电用,所以十几年来很少用到休眠的功能(更别提以前 Linux 休眠醒来之后网卡、扬声器等容易出 bug);偶尔要带出门的话,短时间我就睡眠,长时间我就直接关机。直到最近,由于一连串巧合,我的笔记本电脑没有插电,电池耗尽,系统自动尝试进入休眠状态——成功了,然后就再也醒不来了。

更糟糕的是,systemd 在系统启动时会等待交换分区出现,但因为永远等不到,所以会浪费两分钟等待直到超时。由于「从休眠中醒来」这个任务没有完成,所以下次重启电脑的时候,systemd 还会再等两分钟超时,周而复始。

我实在是不知道如何清除掉这个 flag 让它不要再等。最终我想了个解决方案:systemd 是根据 UUID 去找交换分区的,那我新建一个喂给它不就行了?于是我从日志里找到 systemd 苦等的 UUID,再用 mkswap -U that_uuid 建立一个 swap,再重启一次,果然它就不再等了。事成之后要记得 wipefs 擦掉分区签名,否则以后就一直是明文交换分区了。

结果这样的事情又发生了几次,每次我都要重启几次来修复。痛定思痛,我决定一劳永逸地解决这个问题,不然每次(不小心)触发休眠就会很麻烦。

思路

我调研了多种方法,在虚拟机里尝试了一番,发现最简单的方法是用 LUKS + LVM

LVM 也是基于 DM 的组件,主要用于在块设备上映射出多个逻辑卷(LV),这些 LV 类似于分区,但是不受分区表的限制,可以灵活地调整,甚至可以提供快照等功能。由于我的某些执念,我一直不愿意将两个 DM 套娃使用,因此我笔记本电脑上只有 LUKS,而 NAS 上只有 LVM。实际在虚拟机里测试了一下,发现 DM 叠叠乐也没什么大不了的,因此也就接受了。至于是将 LVM 叠在 LUKS 上,还是将 LUKS 叠在 LVM 上——当然是前者,因为后者和我现在的处境没什么太大的区别,并不能解决加密交换分区的问题。

所以我的分区调整计划是(对照前文的分区结构):

  1. 将 450 GiB 的 p2 和 26.4 GiB 的 p3 两个分区删除,建立一个新的 476.4 GiB 的 p2 取而代之;
  2. 将新的 p2 作为 LUKS 容器,但不在里面直接创建文件系统,而是再叠一层 LVM,在里面创建两个 LV,一个作为主分区(root),一个作为交换分区(swap)。

根据我在虚拟机里的试验,这样调整过后,开机时系统会先解开 LUKS,然后按需求(全新启动还是从休眠中恢复)读取 root 和/或 swap。

实施

在折腾这些分区之前得先备份数据。我的主分区是 450 GiB,但只有 235 GiB 数据在里面。我有块 500 GB 的 SSD 移动硬盘可以暂存数据,这块移动硬盘里最大的一个分区是 377 GiB,倒是够存;但这分区没有加密,而且是 exFAT 文件系统,因此不适合把笔记本电脑里的数据直接复制进去。

那我把整个 LUKS 容器给 dd 进去?这就又太大了,而且由于加密数据块的信息熵极高,所以再怎么压缩也是塞不下的。要不在移动硬盘里创建一个 LUKS 容器然后把文件系统整个倒进去?但是我的笔记本电脑用的是 Ext4 文件系统,似乎并没有像 xfs_copy 那样只复制有用数据块的工具。我灵机一动,想到可以用 BorgBackup 直接直接读取 LUKS 解密后的明文块设备,由于 Borg repo 是加密的,所以可以存到不加密的移动硬盘里。

BorgBackup 甚至专门有个文档解释了这种用法:

  1. 首先用 zerofree 把 Ext4 文件系统里的没用的数据块归零——这工具比 dd if=/dev/zero 更环保、高效
  2. borg create --read-special repo::archive /dev/mapper/luks-xxxx 备份 Ext4 所在的块设备,那些被归零的数据块几乎不会占用存储空间
  3. borg extract --stdout repo::archive | dd of=/dev/mapper/xxxx 恢复 Ext4 到新的块设备上

由于以下操作都是需要对系统分区进行操作,所以我是启动进 archiso 里操作的。在退出主系统之前,先把 /etc/fstab/etc/crypttab 里即将没用的条目给注释掉,防止调整完后进系统时 systemd 又在那儿苦等。

第 2 步完成之后的分区调整命令(根据记忆默写;忘记屏摄了):

# 擦除 p2 上的 LUKS 签名防止被 cryptsetup 误读
wipefs -a /dev/nvme0n1p2
# 删除 p2 p3 并建立新的 p2
sgdisk -d 2 -d 3 -n 2 /dev/nvme0n1
# 在 p2 上创建 LUKS 容器
cryptsetup luksFormat /dev/nvme0n1p2
# 加载 LUKS 容器
cryptsetup open /dev/nvme0n1p2 lukslvm
# 初始化 LVM 并创建两个 LV
pvcreate /dev/mapper/lukslvm
vgcreate arch /dev/mapper/lukslvm
lvcreate -n root -l 450G lukslvm
lvcreate -n swap -L 100%FREE lukslvm

此时应该已经有 /dev/mapper/arch-root/dev/mapper/arch-swap 两个 LV 了。它们的下层设备是 /dev/mapper/lukslvm 这个 LUKS 容器,而 LUKS 容器的下层设备是 /dev/nvme0n1p2 这个 SSD 的分区。这就是 DM 叠叠乐!

此时执行第 3 步,将之前备份的 Ext4 写入到 /dev/mapper/arch-root 上。写入成功后 file -sL /dev/mapper/arch-rootblkid 应该能观察到和原先一样的 UUID。

上述操作中,第 2 步耗时 53 分钟,450 GiB 的文件系统(已用 235 GiB)最终被去重和压缩成 162 GiB 的 Borg repo;第 3 步将压缩后的文件系统写回 450 GiB 的块设备里,耗时 143 分钟。

恢复完成之后,需要调整一下 boot loader 传递给内核的参数:

options rd.luks.uuid=5834fee0-d5f5-4985-aef1-c55d50bd069c rd.luks.options=discard root=UUID=e15bf41d-3922-49a8-abc2-7640f66e318c rw

其中 rd.luks.uuid= 是 LUKS 容器(上文的 /dev/mapper/lukslvm)的 UUID,因为重建了容器,所以这个值需要更新;而 root= 是主分区的 UUID,因为我整个 Ext4 导出到导入,一个字节都没有动过,所以这个值是不变的。

除了更新内核参数,还要确保 initrd 里有 LVM 相关的组件。调整完成之后,退出 archiso 重启,成功进系统!

现在分区布局变成了这样:

NAME                                          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
nvme0n1                                       259:0    0 476.9G  0 disk
├─nvme0n1p1                                   259:1    0   512M  0 part  /boot
└─nvme0n1p2                                   259:2    0 476.4G  0 part
  └─luks-5834fee0-d5f5-4985-aef1-c55d50bd069c 254:0    0 476.4G  0 crypt
    ├─arch-root                               254:1    0   450G  0 lvm   /
    └─arch-swap                               254:2    0  26.4G  0 lvm   [SWAP]

试试休眠……成功了!试试唤醒……也成功了!

再也不用小心翼翼害怕休眠了!


欢迎留下评论。评论前,请先阅读《隐私声明》。