跳转到内容

dm-crypt/特殊应用

来自 Arch Linux 中文维基

保护未加密的boot分区

[编辑 | 编辑源代码]

即使在加密了根分区的情况下,磁盘上仍存在两处未加密的区域:/boot主引导记录(MBR)。通常情况下,引导加载程序需要读取/boot,BIOS需要读取主引导记录,若两者被加密,则无法解密dm-crypt容器,进而完成启动过程。不过,GRUB支持解密经过加密的/boot,详见dm-crypt/加密整个系统#加密的boot分区(GRUB)

本节介绍了可提高启动过程安全性的方法。

警告:虽然对/boot分区和主引导记录进行额外保护可避免许多针对启动过程的攻击方法。但系统仍可能面临其它威胁,例如BIOS/UEFI/固件被篡改,键盘记录器,冷启动攻击等。这些攻击方式不在本文的讨论范围中。有关全盘加密的系统信任问题,参见[1]
注意:对于UEFI启动的系统,在配置得当时(例如,使用统一内核映像),只有EFI 系统分区是未加密的。配合安全启动,可确保启动链的安全性。该方式的安全性与本节中介绍的其它方法相同,且配置简单。详见dm-crypt/加密整个系统#使用TPM2和安全启动在单一分区上配置

从可移动设备引导

[编辑 | 编辑源代码]

使用单独的引导设备启动系统非常直接,并能极大提高系统在面临特定类型攻击时的安全性。在加密了根分区的系统中,易受到攻击的部分为:

要成功启动系统,上述部分必须保持未加密状态。要避免上述未加密区域被篡改,建议将它们存储到可移动介质(例如,USB设备)中,并从可移动介质中引导,而不是直接从硬盘引导。只要保证相应的可移动介质的安全,就可确保相关区域不被篡改,提高解密系统时的安全性。

如下内容假定已完成系统配置,并使用单独boot分区,挂载到/boot。若未还未完成系统配置,参见Dm-crypt/系统配置#内核参数,注意在配置过程中,用可移动设备替换硬盘。

注意:必须确保系统支持从选择的可移动介质(USB设备、移动硬盘、SD卡等)启动。

准备可移动设备(/dev/sdx):

# gdisk /dev/sdx # 按需格式化。也可以使用cgdisk、fdisk、cfdisk、gparted等分区工具
# mkfs.ext2 /dev/sdx1 # BIOS启动的系统
# mkfs.fat -F 32 /dev/sdx1 # UEFI启动的系统
# mount /dev/sdx1 /mnt

将已有/boot分区内容复制到可移动设备中:

# cp -ai /boot/* /mnt/

将可移动设备挂载到/boot,并更新fstab

# umount /boot
# umount /mnt
# mount /dev/sdx1 /boot
# genfstab -p -U / > /etc/fstab

更新GRUB配置。grub-mkconfig应当能自动检测新分区的UUID。但若配置了自定义启动选项,可能需要手动更新相应条目。

# grub-mkconfig -o /boot/grub/grub.cfg
# grub-install /dev/sdx # 安装到可移动设备,而不是硬盘中。用于BIOS启动的系统
# grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub # 用于UEFI启动的系统

重启以测试新的配置。注意在BIOS/UEFI中选择正确的启动设备。若系统启动失败,仍可以从硬盘启动以修复存在的问题。

chkboot

[编辑 | 编辑源代码]
警告:chkboot可发现并提示/boot遭到篡改(tamper-evident),但不能防止篡改(tamper-proof)。chkboot脚本运行前,输入的密码可能已经被篡改过的引导加载程序、 内核、initrd读取。因此,当chkboot完整性检查失败时,数据安全性已经无法保证。

根据ct-magazine中一篇文章(Issue 3/12, page 146, 01.16.2012, [2])的内容,下述脚本将检查/boot中文件的SHA-1散列值是否改变、/boot分区的inode、占用的磁盘块信息,以及主引导记录。该脚本不能完全阻止特定类型的攻击,但极大提升了攻击难度。脚本自身的配置文件不存储在未加密的/boot中。因此,对于攻击者,在系统锁定/关机的情况下,难以发现系统将在启动时检查/boot分区的内容。然而,对于有备而来的攻击者,可修改固件以在系统内核之上执行代码,劫持对boot分区的访问,呈现出文件未被篡改的假象。通常来讲,在固件以上的层级的安全措施都不能确保启动链可信,也不能确保发现并提示篡改迹象。

下述脚本可在此处获取(作者:Juergen Schmidt, ju at heisec.de;许可:GPLv2)。此外,也可通过chkbootAUR安装。建议使用AUR包,其提供了额外的辅助脚本。

Juergen Schmidt的原始脚本

[编辑 | 编辑源代码]

/usr/local/bin/chkboot_user.sh需要在登录后立即执行,故需要加入自动启动中。(例如,对于KDE:系统设置 -> 系统 -> 自动启动;对于GNOME 3,使用gnome-session-properties)。

由于在Arch Linux中,/boot的内容经常改变(例如,更新内核),建议使用如下脚本进行系统更新,以在系统更新时同时更新chkboot设置:

#!/bin/sh
#
# 注意:将<user> 替换为你的用户名,并使用sudo执行该脚本,以确保pacman、chkboot有所需的权限
#
echo "Pacman update [1] Quickcheck before updating" &
sudo -u <user> /usr/local/bin/chkboot_user.sh
/usr/local/bin/chkboot.sh
sudo -u <user> /usr/local/bin/chkboot_user.sh
echo "Pacman update [2] Syncing repos for pacman"
pacman -Syu
/usr/local/bin/chkboot.sh
sudo -u <user> /usr/local/bin/chkboot_user.sh
echo "Pacman update [3] All done, let us roll on ..."

AUR包

[编辑 | 编辑源代码]

在安装对应AUR包后,启动chkboot.service

可能需要在mkinitcpio钩子列表的最后添加chkboot,以便每次mkinitcpio重新生成initramfs时更新chkboot记录的哈希值。可通过修改/etc/mkinitcpio.conf,在HOOKS列表的最后添加chkboot实现。

AUR包中还附带了chkboot-desktopalert脚本,在检测到意料之外的/boot修改时以图形化窗口的形式弹出警告。可将该脚本加入桌面环境的启动项中。

mkinitcpio-chkcryptoboot

[编辑 | 编辑源代码]
警告:本钩子不会加密GRUB的核心代码(MBR部分)、EFI stub。若攻击者通过修改引导加载程序,在运行时篡改内核或initramfs,本钩子不能保证安全性。

mkinitcpio-chkcryptobootAUR是一个mkinitcpio钩子,将在早期用户空间中检查完整性,并在发现系统可能被篡改的痕迹时建议用户不要输入密码。本方案的安全性来自加密的boot分区和加密的根分区。并要求boot分区的密码和根分区的不同。加密的boot分区将由GRUBcryptodisk.mod模块解密。这样,可保护initramfs内核不受到离线攻击。并且,即使在受到篡改的机器上输入了/boot分区的密码,在chkcryptoboot钩子的提示下,也能确保根分区的安全性。(假设chkcryptoboot检测到了篡改,并且钩子本身没有在运行时被篡改)

本钩子需要>=2.00版本的grub。要保证安全性,需要使用专用的、有独立密码的加密/boot分区。

安装

[编辑 | 编辑源代码]

安装 mkinitcpio-chkcryptobootAUR。编辑/etc/default/chkcryptoboot.conf以进行配置。若要检测攻击者是否用自制的boot分区替换了原本的boot分区,设置CMDLINE_NAMECMDLINE_VALUE为某一秘密值。例如,可以按照安装过程中显示的建议使用两个哈希值。此外,还需要在/etc/default/grub中修改内核参数,传递相应的CMDLINE_NAMECMDLINE_VALUE。之后,修改/etc/mkinitcpio.confHOOKS=行,在encrypt之前插入chkcryptoboot。最后重新生成 initramfs

技术概述

[编辑 | 编辑源代码]

mkinitcpio-chkcryptobootAUR同时包含了安装、运行时mkinitcpio钩子。安装钩子将在每次重新生成initramfs时运行。对于UEFI启动的系统,将计算EFI stub($esp/EFI/grub_uefi/grubx64.efi)的哈希值;对于BIOS启动的系统,将计算GRUB安装到的磁盘的首446字节的哈希值,并将哈希值保存到initramfs中(initramfs保存在加密的/boot分区中)。当系统启动时,在GRUB中输入/boot分区的密码后,运行时钩子将重新计算对应的哈希值,将计算出的值与保存的值比对,若匹配,才提示输入根分区密码解密根分区。若不匹配,将显示类似如下的错误:

CHKCRYPTOBOOT ALERT!
CHANGES HAVE BEEN DETECTED IN YOUR BOOT LOADER EFISTUB!
YOU ARE STRONGLY ADVISED NOT TO ENTER YOUR ROOT CONTAINER PASSWORD!
Please type uppercase yes to continue:

除了计算引导加载程序的哈希值之外,钩子还将在运行时以及启动过程完成后,根据/etc/default/chkcryptoboot.conf中的配置检查当前运行内核的内核参数,以确认GRUB配置文件、boot分区未被非法替换。

注意,对于BIOS启动的系统,本钩子计算并检查的是GRUB第一阶段引导加载程序(安装在引导设备的前446字节)的哈希值,并未检查第二阶段引导加载程序(core.img)。

除了上述脚本外,也可以使用AIDE进行哈希值检查。AIDE的配置文件非常灵活,可进行高度自定义配置。

上述介绍的其它方法虽然可以满足大部分用户的需求,但并未解决所有与未加密的/boot分区有关的安全问题。为了构建经过完全验证的启动链,学术界提出了一种名为POTTS的实现,旨在实施 STARK 认证框架。

POTTS 概念验证(proof-of-concept)使用 Arch Linux 作为基础发行版,并实现了一个包含以下组件的系统启动链:

  • POTTS - 用于展示一次性验证消息的启动菜单。
  • TrustedGrub - GRUB Legacy实现,通过TPM芯片的PCR寄存器验证内核和initramfs。
  • TRESOR - 内核补丁,实现了AES加密,并在启动过程中,将主密钥保存在CPU寄存器中,而非内存中。

基于Arch Linux (2013-01及之后版本的ISO镜像)的安装配置方法已作为研究的一部分发布。注意,在尝试之前,应当了解软件仓库中并没有相关实现工具,并且该方法的日常维护非常繁琐。

使用GPG、LUKS、OpenSSL加密的密钥文件

[编辑 | 编辑源代码]

如下论坛帖子讨论了使用了双因素身份验证、GPG或OpenSSL加密的密钥文件,而不是明文密钥文件。拓展了System Encryption using LUKS with GPG encrypted keys中的讨论:

注意:

  • 以上方法只适用于只有两个主分区的情况:在配置加密时必要的boot分区以及主LVM分区。在LVM中,可以按需求任意创建逻辑卷(至少应当有根逻辑卷、swap逻辑卷、home逻辑卷)。只使用两个主分区的好处是只需要一个密钥文件,并且可以使用休眠功能(休眠到加密的swap分区)。若要使用休眠功能,/etc/mkinitcpio.conf中的钩子配置应类似:
    HOOKS=( ... usb usbinput <etwo或者ssldec> encrypt(若使用openssl) lvm2 resume ... )
    ,并在内核参数中加入
    resume=/dev/<VolumeGroupName>/<LVNameOfSwap>
  • 若需要临时在某处保存解密后的密钥文件,不要保存到未加密的磁盘中。最好确保只保存到内存中(例如,/dev/shm)。
  • 若要使用GPG加密的密钥文件,需要静态编译1.4版本的GnuPG。或者,使用gnupg1AUR并相应修改钩子实现。
  • 对于上述第二个帖子(OpenSSL加密密钥文件),更新OpenSSL可能破坏自定义的ssldec钩子。

远程解锁分区

[编辑 | 编辑源代码]

对于需要远程登录启动中的系统,在启动早期阶段远程提供解密LUKS加密的分区(例如,根分区及其它分区)的密码的情况,可以使用mkinitcpio钩子配置网络接口并启动SSH服务器。下文中介绍的某些包提供了mkinitcpio 编译钩子,以简化配置。下面将介绍了在本机控制台输入密码的基础上,增加远程解密的方法。

注意:
  • 需要确保使用内核对网络设备的命名(例如,eth0),而不是udev的命名(例如,enp1s0)。
  • 默认情况下,可预测网络接口名(Predictable Network Interface Names)在启动过程后期才激活并修改网络设备的命名。可使用dmesg检查有关网络的内核模块的日志以确定网络设备的原始命名(例如,eth0)。
  • 可能需要将以太网卡无线网卡所需的模块添加到MODULES列表中。

基于BusyBox的initramfs(使用mkinitcpio)构建

[编辑 | 编辑源代码]

mkinitcpio-netconf和/或mkinitcpio-pppAUR为基于BusyBox的initramfs提供了网络支持。对于SSH服务器,则可使用mkinitcpio-dropbearmkinitcpio-tinyssh。注意,上述钩子不会安装shell,故可能按需安装mkinitcpio-utils。对于上述包的任何组合方式,下述操作步骤都适用,些许不同之处将单独指出。

  1. 若没有已有的SSH密钥对,在客户端系统上生成密钥对。生成的密钥对将用于登录远程主机。
    注意:
    • tinyssh只支持不设置密码的Ed25519ECDSA密钥。若使用mkinitcpio-tinyssh,只能选择这两种密钥类型之一。
    • 0.0.3-5版本的mkinitcpio-dropbear与现有的移除了dss的dropbear实现不兼容。详情及修复方法参见Github
  2. 根据选择的SSH服务器,将SSH公钥插入到远程主机的/etc/dropbear/root_key//etc/tinyssh/root_key文件中(对于刚刚生成的密钥对,SSH公钥存储在.pub结尾的文件中,公钥一般被复制到远程主机以实现无密码登录)。
    提示:可依照此方法添加其它所需的SSH公钥。也可以直接复制远程主机的~/.ssh/authorized_keys文件,但需要检查文件内容以仅包含需要执行远程解锁操作的公钥。添加公钥后,需要使用mkinitcpio重新生成initrd。另见OpenSSH#保护
  3. 修改/etc/mkinitcpio.conf的"HOOKS"列表,在filesystems文件系统钩子前添加<netconf 和/或 ppp> <dropbear 或 tinyssh> encryptssh三个钩子(encryptssh钩子代替了encrypt钩子)。并重新生成 initramfs
    注意:不需要mkinitcpio-nfs-utils提供的net钩子。
  4. 配置所需的cryptdevice=内核参数,并在引导加载程序配置中添加恰当的ip=内核参数。例如,若要使用DHCP分配IP地址,可设置
    ip=dhcp
    ,这一般要求DHCP服务器始终为需要远程解密的主机分配相同的IP地址,否则进行SSH连接较为繁琐。也可以使用静态IP地址:
    ip=192.168.1.1:::::eth0:none
    。此外,还可指定子网掩码和网关:
    ip=192.168.1.1::192.168.1.254:255.255.255.0::eth0:none
    注意:若使用0.0.4及更新版本的mkinitcpio-netconf,还可通过在ip=中设置多个参数,分别配置多个网络接口。但是,不能同时设置“全局配置”和针对某一网络接口的配置(例如,不能与ip=dhcp混用)。必须显式地指定网络接口:
    ip=ip=192.168.1.1:::::eth0:none:ip=172.16.1.1:::::eth1:none
    若使用DHCP,可能需要添加netconf_timeout=内核参数,避免在DHCP服务不可用时,netconf无限期地尝试通过DHCP获取IP地址。ip=参数的详细介绍可参见Mkinitcpio#使用_net。设置完成后,更新引导加载程序配置。
  5. 最后,重启远程主机并尝试使用SSH连接, 显式地指定使用root用户连接 (即使远程主机的系统配置中关闭了root账户,也可在initrd中使用,以解锁远程主机)。若远程主机在启动后使用openssh提供远程SSH连接,并且使用mkinitcpio-dropbear提供initramfs中的SSH服务端支持,则在连接到initramfs中的SSH服务端时,不会显示SSH主机密钥变更的警告,因为mkinitcpio-dropbear将使用和OpenSSH相同的SSH密钥(注意,dropbear不支持Ed25519密钥,若openssh使用了Ed25519密钥,连接时仍会显示密钥不匹配警告) 。对于mkinitcpio-tinyssh,其附带了tinyssh-convert脚本,以使用与openssh相同的密钥(当前仅支持使用Ed25519密钥)。有时,可能需要手动使用tinyssh-convert将密钥复制到/etc/tinyssh/sshkeydir。注意,无论使用何种方式复制ssh密钥,都需要确保密钥已由ssh守护进程生成,即,至少通过systemd单元启动过一次OpenSSH。配置完成后,重启远程主机并通过SSH连接,应当能看到输入密码以解锁设备的提示符;输入密码并等待系统启动完成后,可以使用普通用户进行正常的SSH连接

使用Wi-Fi

[编辑 | 编辑源代码]

netconf钩子一般用于以太网连接。若远程主机只连接无线网络,并且要通过Wi-Fi进行远程连接以解锁设备,需要在netconf钩子前运行相关配置无线网络的钩子,可以使用预定义的钩子,也可以创建自定义钩子。

预定义钩子
[编辑 | 编辑源代码]

可以安装预定义钩子:

  1. 安装mkinitcpio-wifiAUR
  2. 根据网络属性创建wpa_supplicant配置,以设置Wi-Fi连接:
    wpa_passphrase "ESSID" "passphrase" > /etc/wpa_supplicant/initcpio.conf
  3. 修改/etc/mkinitcpio.conf,在netconf钩子前添加wifi钩子。应当能自动检测到与Wi-Fi相关的模块。若没有自动检测到,手动将需要的模块加入MODULES中。
  4. 添加ip=:::::wlan0:dhcp内核参数
  5. 重新生成 initramfs
  6. 更新引导加载程序配置。
自定义钩子
[编辑 | 编辑源代码]

下例展示了使用USB Wi-Fi适配器,连接到WPA2-PSK保护的Wi-Fi网络的方法。对于其它配置(例如,连接到WEP网络、使用其它initramfs生成工具),可能需要相应调整某些步骤。

  1. 修改/etc/mkinitcpio.conf
    • 添加Wi-Fi适配器所需的内核模块。
    • 包含wpa_passphrasewpa_supplicant二进制文件。
    • net钩子前添加wifi钩子(该钩子即是需要创建的自定义钩子,也可以使用其它名称。
      MODULES=(module)
      BINARIES=(wpa_passphrase wpa_supplicant)
      HOOKS=(base udev autodetect ... wifi net ... dropbear encryptssh ...)
  2. /etc/initcpio/hooks/wifi中创建wifi钩子:
    run_hook ()
    {
    # Sleep a couple of seconds so wlan0 is setup by kernel
    sleep 5

    # Set wlan0 to up
    ip link set wlan0 up

    # Associate with Wi-Fi network
    # 1. Save temp config file
    wpa_passphrase "network ESSID" "pass phrase" > /tmp/wifi

    # 2. Associate
    wpa_supplicant -B -D nl80211,wext -i wlan0 -c /tmp/wifi

    # Sleep a couple of seconds so that wpa_supplicant finishes connecting
    sleep 5

    # wlan0 should now be connected and ready to be assigned an ip by the net hook
    }

    run_cleanuphook ()
    {
    # Kill wpa_supplicant running in the background
    killall wpa_supplicant

    # Set wlan0 link down
    ip link set wlan0 down

    # wlan0 should now be fully disconnected from the Wi-Fi network
    }
  3. /etc/initcpio/install/wifi中创建钩子安装文件:
    build ()
    {
    add_runscript
    }
    help ()
    {
    cat<<HELPEOF
    Enables Wi-Fi on boot, for dropbear ssh unlocking of disk.
    HELPEOF
    }
  4. 添加ip=:::::wlan0:dhcp内核参数。注意移除ip=:::::eth0:dhcp,以避免冲突。
  5. (可选)可额外添加使用ip=:::::eth0:dhcp内核参数以进行以太网连接的启动加载项。
  6. 重新生成 initramfs
  7. 更新引导加载程序配置。

注意检查系统的Wi-Fi配置,避免系统启动完成后无法远程连接。

若在initramfs中无法连接到Wi-Fi网络,可尝试增加脚本中的sleep时间。

基于systemd的initramfs(使用mkinitcpio)构建

[编辑 | 编辑源代码]

mkinitcpio-systemd-extrasAUR为基于systemd的initramfs提供了一整套用于网络配置、远程SSH连接的构建钩子。对于远程会话,可进行配置以使用busybox的dash完全访问initramfs环境,也可只显示密码输入提示符。

最小配置如下:

/etc/mkinitcpio.conf
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole sd-network block sd-tinyssh sd-encrypt filesystems fsck)
SD_TINYSSH_COMMAND="systemd-tty-ask-password-agent --query --watch"

对于上述配置,当使用mkinitcpio构建initramfs时,将复制systemd-networkd配置文件,并尝试从已有的TinySSH、OpenSSH配置中复制/转换SSH密钥。系统需要安装tinyssh(但不需要启用)。若系统不使用systemd-networkd,需要额外配置参数,详见mkinitcpio-systemd-extrasAUR文档

注意:
  • 在默认情况下,可预测网络接口名(例如,enp2s0)在initramfs阶段不可用。故直接复制的systemd-networkd配置文件很可能无法正常工作。解决方法可参见mkinitcpio-systemd-extras wiki
  • 还可使用mkinitcpio-systemd-tool实现在启动早期远程连接,并解密LUKS卷。该方式与上述介绍完全不同,需要额外添加systemd-tool钩子,并且在相应systemd单元的[X-SystemdTool]节中进行配置。有关该方式实现以及配置的文档非常有限。

基于systemd的initramfs(使用dracut)构建

[编辑 | 编辑源代码]

若使用dracut而不是mkinitcpio构建initramfs,可能需要使用dracut-sshd

设置用于重启的一次性密钥

[编辑 | 编辑源代码]

还可以通过设置临时密钥文件,以在无需手动终端交互输入密码的情况下实现远程设备的重启。密钥文件需要放置在能在启动时被内核访问的位置,并设置cryptkey启动参数,同时,还需要通过"cryptsetup luksAddKey"命令添加密钥文件。

使用passless-bootAUR可方便地完成上述操作。若想要自行实现,也可参照该工具的的README。注意,在使用前,请阅读Security considerations中的讨论。

固态硬盘的Discard/TRIM支持

[编辑 | 编辑源代码]

固态硬盘用户应当注意,在默认情况下,除非指定discard选项,否则dm-crypt的device mapper不会启用TRIM命令支持。

由于可能的安全问题,device mapper维护者表示永远不会在dm-crypt设备上默认启用TRIM支持。[3][4] 在启用TRIM的设备上,通过释放的磁盘块信息,有可能推断出加密设备上层使用的文件系统。cryptsetup开发者在该博客中介绍了启用TRIM可能导致的问题。还需注意,相关风险可能叠加。例如,若设备仍使用之前的加密方式(cryptsetup < 1.6.0)加密(--cipher aes-cbc-essiv),相比现在的默认选项,释放的磁盘块信息可能泄露更多配置信息。

依据配置方式,可分情况详细讨论:

  • 若设备使用默认的dm-crypt LUKS模式加密:
    • 在默认情况下,LUKS头存储在设备头部,启用TRIM支持有利于保护对LUKS头的修改。例如,在撤销了一个可能泄露的LUKS密码后,在未启用TRIM支持的情况下,仍可能从设备上读取旧的LUKS头,直到其被另一操作覆盖。若设备在此情况下失窃,则理论上,攻击者可能能够使用某种方法找到旧的LUKS头,并使用泄露的LUKS密码获得主密钥解密设备。详见cryptsetup FAQ, section 5.19 What about SSDs, Flash and Hybrid Drives?Full disk encryption on an ssd
    • 若认为启用TRIM可能造成的信息泄露比上述LUKS头有关问题更加严重,则应当保持TRIM关闭的状态。另见安全地擦除磁盘#闪存
  • 若设备使用dm-crypt plain模式加密,或者单独存储LUKS头
    • 若需要可抵赖性,则根据本节开头提到的内容,永远不应启用TRIM支持。否则,TRIM可能造成的信息泄露会使得plain模式加密没有意义。
    • 若无需可抵赖性,则在可接受本节开头提到的安全风险的情况下,可以为了性能启用TRIM支持。
警告:在启用TRIM前,需确保相应设备完全支持TRIM指令,否则可能造成数据丢失。详见固态硬盘#TRIM

除了启用dm-crypt的discard支持外,还应定期执行fstrim(8);或者,在/etc/fstab中指定discard挂载选项。详见固态硬盘#TRIM

对于LUKS2加密的设备,可通过打开设备时使用--allow-discards --persistent挂载选项启用TRIM支持。使用该命令后,allow-discards将写入LUKS2头中,之后打开设备时无需再手动指定。

注意:
  • 通过cryptsetup --persistent设置新的持久性标志时,将替代所有已设置的持久性标志,而不是在原有基础上新增一个标志。故若还设置了其它标志,需要一同指定。
  • 若使用不经过dm-crypt的OPAL加密(详见cryptsetup-luksFormat(8)--hw-opal-only选项),则文件系统和底层磁盘间没有dm-crypt层,discard请求将直接传递到底层磁盘,无需手动启用discard支持。
# cryptsetup --allow-discards --persistent open /dev/sdaX root

若设备已经打开,上述open操作将会报错。此时,应当使用cryptsetup-refresh(8)命令:

# cryptsetup --allow-discards --persistent refresh root

可通过cryptsetup luksDump输出的LUKS2头信息确保标志已经持久性设置:

# cryptsetup luksDump /dev/sdaX | grep Flags
Flags:          allow-discards

LUKS1及plain模式

[编辑 | 编辑源代码]

对于LUKS1或者plain模式加密的设备,需要在每次打开设备时手动指定选项启用TRIM支持。

要在启动时启用TRIM支持,设置如下内核参数

若使用encrypt钩子:

cryptdevice=/dev/sdaX:root:allow-discards

若使用sd-encrypt钩子(基于systemd的initramfs):

rd.luks.options=discard
注意:对于在initramfs镜像/etc/crypttab(对应/etc/crypttab.initramfs)中挂载的文件系统,设置rd.luks.options=discard内核选项没有作用。必须在/etc/crypttab.initramfs中设置discard选项。

对于通过/etc/crypttab解密的设备,指定discard选项,例如:

/etc/crypttab
luks-123abcdef-etc UUID=123abcdef-etc none discard

若手动使用命令解锁设备,使用--allow-discards

例如,可使用--allow-discards手动打开设备,并手动执行fstrim命令:

# cryptsetup --allow-discards open /dev/sdaX root

总是可以通过dmsetup table的输出判断设备是否通过discard选项打开:

# dmsetup table
luks-123abcdef-etc: 0 1234567 crypt aes-xts-plain64 000etc000 0 8:2 4096 1 allow_discards

关闭工作队列以提高SSD性能

[编辑 | 编辑源代码]

固态硬盘用户应当注意,在默认情况下,device mapper并未启用关闭内部读取、写入工作队列的功能。即,块设备将不使用no_read_workqueueno_write_workqueue选项挂载。

在Cloudflare内部进行的关于总体磁盘加密性能的研究(Speeding up Linux disk encryption)中,提出了no_read_workqueueno_write_workqueue标志。研究显示,dm-crypt内部的读取和写入队列降低了SSD的性能。尽管磁盘操作队列对于机械硬盘适用,但对于SSD,禁用操作队列,直接同步写入数据可使吞吐量翻倍,并将IO操作等待延迟降低一半。相关补丁已经被上游接受,并在5.9及更新版本的linux中可用[5]

提示:linux-zen 默认关闭了dm-crypt工作队列。

对于使用crypttab解密的LUKS设备,要关闭工作队列,按需指定相应的no-read-workqueueno-write-workqueue选项。例如:

/etc/crypttab
luks-123abcdef-etc UUID=123abcdef-etc none no-read-workqueue

要同时关闭读取和写入工作队列,同时指定两个标志:

/etc/crypttab
luks-123abcdef-etc UUID=123abcdef-etc none no-read-workqueue,no-write-workqueue

对于LUKS2加密的设备,可以使用--persistent选项,将--perf-no_read_workqueue--perf-no_write_workqueue设置为打开设备时的默认值。例如:

# cryptsetup --perf-no_read_workqueue --perf-no_write_workqueue --persistent open /dev/sdaX root

若设备已经打开,上述open操作将会报错。此时,应当使用cryptsetup-refresh(8)命令。例如:

# cryptsetup --perf-no_read_workqueue --perf-no_write_workqueue --persistent refresh root

可通过cryptsetup luksDump输出的LUKS2头信息确保标志已经持久性设置:

# cryptsetup luksDump /dev/sdaX | grep Flags
Flags:          no-read-workqueue

总是可以通过dmsetup table的输出判断为设备指定的选项:

# dmsetup table
luks-123abcdef-etc: 0 1234567 crypt aes-xts-plain64 000etc000 0 8:2 4096 1 no_read_workqueue
注意:通过cryptsetup --persistent设置新的持久性标志时,将替代所有已设置的持久性标志,而不是在原有基础上新增一个标志。故若还设置了其它标志,需要一同指定。

例如,通过cryptsetup同时指定no_read_workqueueno_write_workqueue

# cryptsetup --perf-no_read_workqueue --perf-no_write_workqueue --persistent refresh root

可以通过dmsetup table的输出判断为设备指定的选项:

# cryptsetup luksDump /dev/sdaX | grep Flags
Flags:          no-read-workqueue no-write-workqueue

使用encrypt钩子解密多个设备

[编辑 | 编辑源代码]
注意:本节仅关注在早期用户空间(initramfs阶段)解密多个设备。对于后期用户空间(在切换root后)解密的配置,参见Dm-crypt/系统配置#在后期用户空间中解密
提示:sd-encrypt钩子支持解密多个设备。可通过内核命令行或/etc/crypttab.initramfs指定需要解密的设备。详见Dm-crypt/系统配置#使用systemd-cryptsetup-generator

encrypt钩子仅支持单个cryptdevice=条目(archlinux/mkinitcpio/mkinitcpio#231)。在有多硬盘的系统中,由于dm-crypt自身不能跨越物理设备,需要配置多个加密物理分区,此时encrypt将造成限制。例如,对于“在LUKS上配置LVM”方案,整个LVM都存在于LUKS映射器之上。对于仅有单个硬盘的系统,只需解密一个设备,不会出现问题。但是,若之后想通过添加设备,增加LVM的大小呢?在不修改encrypt钩子的情况下,无法实现这一目的。

之后的小节介绍了克服这一限制的替代方案。第一小节介绍了如何将“在LVM上配置LUKS”方案拓展到新增的硬盘上。第二小节介绍了,在不使用LVM的配置中,如何修改encrypt钩子以解密多个磁盘。

将LVM拓展到多个磁盘上

[编辑 | 编辑源代码]

管理多个磁盘,并以此提供分区灵活性,是LVM的特色功能。若将LVM作为dm-crypt的第一个映射器(first mapper),则LVM也可与dm-crypt共同使用(“在LVM上配置LUKS”)。在此配置中,加密设备在逻辑卷中创建(可能分别对每个加密卷使用不同的密钥)。下面将介绍如何将本配置拓展到另一磁盘。

警告:请注意,虽然改变文件系统大小似乎是常规操作,操作过程中仍可能出现问题,同时,下面介绍的方式也可能不适用于某些特定配置。通常来讲,扩容文件系统可能遇到的问题比收缩文件系统小,尤其是在类似本例的多层映射配置中。

添加新设备

[编辑 | 编辑源代码]

首先,可能需要按照dm-crypt/准备磁盘中的步骤准备磁盘。 然后,创建用于拓展LVM的分区(例如,将设备所有空间分配到/dev/sdY1中,并设置分区类型为8E00 (Linux LVM))。 之后,将新磁盘/分区添加到已有的LVM卷组中,例如:

# pvcreate /dev/sdY1
# vgextend MyStorage /dev/sdY1

拓展逻辑卷

[编辑 | 编辑源代码]

接下来,将最终分配新的磁盘空间到逻辑卷中。注意需要首先卸载要扩容的逻辑卷。如果需要扩容cryptdevice设定的根分区,需要在Arch安装ISO中执行下述操作。

在本例中,假定需要拓展用于/home(逻辑卷名称为homevol)的逻辑卷:

# umount /home
# fsck /dev/mapper/home
# cryptsetup close /dev/mapper/home
# lvextend -l +100%FREE MyStorage/homevol

在拓展逻辑卷后,拓展LUKS容器:

# cryptsetup open /dev/MyStorage/homevol home
# umount /home      # 在/home可能被自动挂载的情况下,作为安全措施
# cryptsetup --verbose resize home

最后,修改文件系统大小:

# e2fsck -f /dev/mapper/home
# resize2fs /dev/mapper/home

若需要,可重新挂载/home,其容量已被拓展:

# mount /dev/mapper/home /home

注意,cryptsetup resize操作不会影响密钥。

修改encrypt钩子以用于多个分区

[编辑 | 编辑源代码]

注意,sd-encrypt原生支持解密多个分区。此外,若通过该方式解密的多个分区密码相同,sd-encrypt还将自动尝试输入的密码,而无需多次输入。因此,使用sd-encrypt可能比下面介绍的方式更加方便。

跨越多个分区的根文件系统

[编辑 | 编辑源代码]

可以通过修改encrypt钩子以在启动时解密跨越多个分区的根文件系统。一种方式如下:

# cp /usr/lib/initcpio/install/encrypt /etc/initcpio/install/encrypt2
# cp /usr/lib/initcpio/hooks/encrypt  /etc/initcpio/hooks/encrypt2
# sed -i "s/cryptdevice/cryptdevice2/" /etc/initcpio/hooks/encrypt2
# sed -i "s/cryptkey/cryptkey2/" /etc/initcpio/hooks/encrypt2

添加cryptdevice2=启动选项(可能还需添加cryptkey2=选项),在mkinitcpio.conf中添加encrypt2钩子,并重新生成initramfs。详见Dm-crypt/系统配置

非根分区

[编辑 | 编辑源代码]

可能还有使用encrypt钩子解密非根分区的需求。Arch并不原生支持这一功能,但可通过修改/lib/initcpio/hooks/encrypt中的cryptdev、cryptname值方便地实现。(cryptdev对应要解密的分区,例如/dev/sd*;cryptname对应解密后的映射名称)。

使用encrypt钩子的优势在于其自动化了非根分区的解密。虽然可通过/etc/crypttab文件达到相同目的,但若使用外部密钥文件(即,密钥文件不存储在系统硬盘中,而在可移动设备上),需要确保USB、火线(FireWire)等外部设备在尝试挂载加密分区前被挂载,这至少需要修改/etc/fstab中的挂载顺序,相关配置较为麻烦。

当然,若cryptsetup包更新,则需要重新修改上述脚本。此外,与/etc/crypttab不同,默认情况下仅能通过encrypt钩子解密一个分区,但通过进一步修改,应当能实现解密多个分区的功能。

在加密系统时使用分离的LUKS头

[编辑 | 编辑源代码]

本例使用了与dm-crypt/加密整个系统#Plain_dm-crypt类似的设置,应当先阅读其中的内容。

若使用分离的LUKS头,则加密的块设备自身将只含有加密数据,而不含LUKS头信息。若攻击者不知道分离的LUKS头的存在,则可实现可抵赖加密。这与使用plain dm-crypt类似,但优势在于仍可使用多密码、密钥派生等LUKS高级功能。此外,使用分离的LUKS头还提供了某种程度上的双重因素认证,并且相比使用GPG、OpenSSL加密密钥文件更加简单,同时可以使用内建的密码提示符进行多次尝试。详见静态数据加密#加密的元数据

在继续之前,参见可用的加密选项。使用如下命令创建加密的系统分区和LUKS头文件:

# dd if=/dev/zero of=header.img bs=16M count=1
# cryptsetup luksFormat --offset 32768 --header header.img /dev/sdX
提示:--offset选项指定了加密数据在设备上的起始位置。在设备开头预留一定空间可便于之后将分离的LUKS头重新附加到设备上。该选项的单位是512字节的扇区,详见cryptsetup-luksFormat(8)

打开加密容器:

# cryptsetup open --header header.img /dev/sdX enc

接下来,按照“在LUKS上配置LVM”中的步骤进行。之后,在可移动设备上配置boot分区(若不将boot分区放置在可移动设备上,使用分离的LUKS头没有意义)。

header.img移动到boot分区:

# mv header.img /mnt/boot

按照安装指南中的步骤继续安装,直到配置mkinitcpio步骤前(此时应当已经arch-chroot到加密的系统中)。

提示:由于整个系统硬盘都只含有看似随机的加密数据,没有分区表、分区,因而没有UUIDLABEL。但仍可通过id、路径进行持久化命名。例如,可以使用硬盘id(可在/dev/disk/by-id/中查看)。

有两种方法使得initramfs支持分离的LUKS头。

使用systemd钩子

[编辑 | 编辑源代码]

设置如下内核参数

rd.luks.name=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX=enc rd.luks.options=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX=header=/header.img:UUID=ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ rd.luks.data=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX=/dev/disk/by-id/your-disk_id
  • XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX替换为LUKS超级块的UUID。该UUID可通过cryptsetup luksDump header.img或者blkid -s UUID -o value header.img获得。
  • UUID=ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ修改为存放分离的LUKS头文件的块设备的UUID。

除了指定rd.luks内核参数以外,还可通过/etc/crypttab.initramfs文件指定:

/etc/crypttab.initramfs
enc	/dev/disk/by-id/your-disk_id	none	header=/header.img:UUID=ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ

接下来,修改/etc/mkinitcpio.conf使用systemd,并添加用于读取分离的LUKS头文件存储的文件系统的模块。例如,若存储在FAT卷中:

/etc/mkinitcpio.conf
...
MODULES=(vfat)
...
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block sd-encrypt lvm2 filesystems fsck)
...

重新生成 initramfs

注意:
  • 若使用了/etc/crypttab.initramfs进行配置,则无需添加上述内核参数。/etc/crypttab.initramfs将被复制到initramfs中的/etc/crypttab
  • 避免同时使用rd.luks内核参数和/etc/crypttab.initramfs文件,否则可能导致冲突。详见Dm-crypt/系统配置#使用systemd-cryptsetup-generator

修改encrypt钩子

[编辑 | 编辑源代码]

下文介绍了如何修改encrypt钩子,以便使用分离的LUKS头。

目前,需要修改encrypt钩子以使得cryptsetup使用分离的LUKS头(archlinux/mkinitcpio/mkinitcpio#234;本方法的基本思路和框架来自该BBS帖子)。将修改钩子的副本,以免mkinitcpio的更新覆盖所做的改动。

# cp /usr/lib/initcpio/hooks/encrypt /etc/initcpio/hooks/encrypt2
# cp /usr/lib/initcpio/install/encrypt /etc/initcpio/install/encrypt2
/etc/initcpio/hooks/encrypt2 (around line 52)
warn_deprecated() {
    echo "The syntax 'root=${root}' where '${root}' is an encrypted volume is deprecated"
    echo "Use 'cryptdevice=${root}:root root=/dev/mapper/root' instead."
}

local headerFlag=false
for cryptopt in ${cryptoptions//,/ }; do
    case ${cryptopt} in
        allow-discards)
            cryptargs="${cryptargs} --allow-discards"
            ;;
        header)
            cryptargs="${cryptargs} --header /boot/header.img"
            headerFlag=true
            ;;
        *)
            echo "Encryption option '${cryptopt}' not known, ignoring." >&2
            ;;
    esac
done

if resolved=$(resolve_device "${cryptdev}" ${rootdelay}); then
    if $headerFlag || cryptsetup isLuks ${resolved} >/dev/null 2>&1; then
        [ ${DEPRECATED_CRYPT} -eq 1 ] && warn_deprecated
        dopassphrase=1

之后,修改mkinitcpio.conf,添加encrypt2lvm2钩子。同时还需在FILES中添加header.img;在MODULES中添加loop。并按需添加其它所需内容:

/etc/mkinitcpio.conf
...
MODULES=(loop)
...
FILES=(/boot/header.img)
...
HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block encrypt2 lvm2 filesystems fsck)
...

上述配置通过将LUKS头文件复制到initramfs中,使得LUKS头在启动过程中可用,以便解密系统,而无需再挂载另一个USB设备以获取LUKS头文件,降低了配置的复杂性。配置完成后,重新生成initramfs。

接下来,配置引导加载程序,指定cryptdevice=,并传递新增的header选项:

cryptdevice=/dev/disk/by-id/your-disk_id:enc:header

对于在USB存储介质上配置的/boot分区,可参见dm-crypt/加密整个系统#安装完成后的操作