LUKS磁盘加密实战:密钥槽、自动解锁与性能代价

为什么我最后决定给服务器做磁盘加密

搬服务器这件事,大多数站长一辈子也就经历一两次。真正让我下决心做磁盘加密的,是一次报废旧机器的经历:那台机器租了三年的独立服务器,到期不续了,服务商说硬盘会被回收处理。硬盘上有什么?数据库里几万条用户邮箱和加密后的密码、站长的 SSH 私钥、备份仓库的密码文件、还有各种 API 的密钥配置文件。

服务商说「会做数据擦除」,但我没办法验证这句话。更实际的威胁其实不是服务商,而是更日常的两种场景:一是虚拟化平台上的磁盘快照和备份,运维人员有权限直接挂载查看;二是机器被人从机房物理带走(小服务商跑路、机器被扣押这类事并不罕见)。这两种情况下,只要磁盘没有加密,拿走硬盘的人拔下来挂到另一台机器上,cat /etc/shadow 就能开始干活。

LUKS 解决的就是这个问题——「磁盘离开这台机器就是一堆随机字节」。这篇文章讲的是我在真实环境里做这件事的完整过程,包括几个不做足功课就会把自己锁在门外的坑。

先想清楚 LUKS 能保护什么,不能保护什么

这一节很重要。很多人做完加密之后安全感受到极大提升,但实际上防住的只是很窄的一类攻击。混淆边界会导致真正的风险被忽略。

能防住的

离线攻击。 这是 LUKS 的核心价值。磁盘被拔走、快照被导出、虚拟机镜像被复制——只要没有密钥,磁盘内容就是密文。这包括:报废机器、磁盘返修、快照泄露、虚机镜像盗取。

误删后的数据恢复。 这个好处常被忽略。加密之后硬盘上的数据是密文,即使有人用恢复软件扫盘,也扫不出明文。对于处理过敏感数据的旧盘,这算是个附带收益。

防不住的

机器正在运行时的入侵。 服务器开机后,LUKS 卷是解锁状态,挂载在系统里,任何有 root 权限的进程都能照常读到明文。加密对「服务器被入侵」这件事没有任何帮助——那要靠 SSH 收口、补丁、权限管理。

内存中的密钥。 解锁之后,卷的密钥(master key)会常驻内核内存,用于加解密每一次读写。理论上可以通过 DMA 攻击(Thunderbolt、PCIe 设备)或者冷启动攻击从内存里捞出来,但对你我这种级别的目标基本不现实。这是安全模型中「足够好」的边界。

swap 分区。 这是个真问题。如果 swap 没有加密,内存里的明文数据(包括从磁盘读出的文件内容、数据库页)可能被换出到未加密的 swap 分区上。所以做 LUKS 时,swap 必须一起加密,或者干脆不用 swap(用 zram)。

应用程序自己的日志和备份。 磁盘加密只保护「静态数据」,如果你的数据库在每天凌晨通过 mysqldump 备份到另一台机器上,那个备份里就是明文。加密必须覆盖数据流经的每一个存储点,否则等于没做。

三种架构:全盘、分区与文件级

动手之前要先选架构。这不是技术优劣问题,而是「你的运维能力能承受哪种」的问题。

方案一:LUKS 加密整块数据盘(推荐入门)

系统盘不动,只把 /var/lib/mysql 或者整个 /data 目录所在的独立磁盘加密。这是投入产出比最高的方案:敏感数据集中在一块盘上,开机时手动或者自动解锁,系统本身照常启动。

好处非常明显:系统盘出问题时不需要先解锁就能进去修。这是我最看重的一点。做过全盘加密的人都懂,一旦 initramfs 出问题,你会面对一个连 shell 都没有的启动界面,只能靠救援模式。

方案二:LUKS 加密根分区(全盘加密)

包括 / 在内的整个系统加密,开机时需要输入密码,由 initramfs 里的程序解锁。它的安全边界更完整,但代价是运维复杂度陡增:远程重启必须有带外管理(IPMI、VNC、KVM),或者要配置网络解锁(Dropbear in initramfs、Clevis + Tang)。

对独立服务器站长来说,如果服务商提供 VNC 控制台,这条路是可行的;如果只有 SSH,一旦重启就会永久失联,强烈不建议。

方案三:文件级加密(eCryptfs / gocryptfs)

只加密某些文件,比如密钥目录、用户上传目录。优点是粒度细、不需要分区规划,缺点是性能损失大(每次读写都要经过 FUSE 层),而且文件名、目录结构等元数据保护有限。适合「只有几个文件敏感」的场景,不适合数据库。

我的选择是方案一。下面全部按这个走。

动手:从零加密一块数据盘

假设机器上新增了一块 /dev/vdb(云上就是新挂的数据盘),准备加密后挂到 /data。

第零步:确认盘是空的

这一步看着废话,但我见过真实事故:有人把已有数据的盘直接 luksFormat 了,因为「我以为那块新盘」。格式化命令不会问你「里面有没有数据」,它只问你要不要覆盖。

lsblk -f
df -h
mount | grep vdb

确认目标盘没有挂载、没有文件系统、不在 /etc/fstab 里。如果里面有数据,先备份——加密会不可逆地销毁现有内容。

第一步:格式化 LUKS 卷

# 检查 cryptsetup 是否可用
apt install -y cryptsetup

# 格式化(默认用 LUKS2 + aes-xts-plain64,已是最佳实践)
cryptsetup luksFormat --type luks2 /dev/vdb

# 确认这是个 LUKS 卷
cryptsetup isLuks /dev/vdb && echo "OK: 是 LUKS 卷"
cryptsetup luksDump /dev/vdb

luksFormat 会要求你输入 YES(大写)然后设置密码。这个密码忘掉就是数据永久丢失,没有任何后门。

关于密码的一个实际建议:这是一台服务器,密码不会每天输入,所以不要用「方便记忆」的思路设计它。用密码管理器生成一个 40 位以上的随机串,存到密码管理器里,同时抄一份到离线介质。人为记得住的口令,对离线暴力破解的抵抗力远不如随机串——尤其考虑到攻击者拿到磁盘后可以用 GPU 集群慢慢试。

第二步:创建密钥文件(关键一步)

服务器重启不可能每次手动输密码,所以必须有一个密钥文件用于自动解锁。做法是给 LUKS 卷添加两个「密钥槽」:一个是你记住的密码(用于应急),一个是密钥文件(用于自动解锁,存在根分区上)。

# 生成 4KB 随机密钥
mkdir -p /etc/luks-keys && chmod 700 /etc/luks-keys
dd if=/dev/urandom of=/etc/luks-keys/data.key bs=512 count=8
chmod 400 /etc/luks-keys/data.key

# 把密钥文件加为第二个密钥槽
cryptsetup luksAddKey /dev/vdb /etc/luks-keys/data.key

# 查看密钥槽状态
cryptsetup luksDump /dev/vdb | grep -A3 Keyslots

这里有个重要的安全讨论:密钥文件放在未加密的根分区上,等于把钥匙和锁放在一起。 严格来说这削弱了防护——攻击者拿到整台机器的两块盘,还是能解锁。但对「只拿走数据盘」「只泄露快照」这两类实际威胁,它仍然有效,而这是最常见的场景。

如果你要防的是「整机被拿走」,那必须做全盘加密 + 远程解锁(Tang/Clevis 或者 initramfs 里的 Dropbear)。这是个取舍,取决于你对威胁的判断。我的结论是:有密钥文件的 LUKS 比没有 LUKS 好得多,而完美的方案根本不会有人坚持用下去。

第三步:打开、建文件系统、挂载

# 打开卷,指定一个映射名(会出现在 /dev/mapper/ 下)
cryptsetup luksOpen /dev/vdb data_crypt --key-file /etc/luks-keys/data.key

# 建文件系统(数据盘用 xfs,大文件性能好)
mkfs.xfs /dev/mapper/data_crypt

# 挂载
mkdir -p /data
mount /dev/mapper/data_crypt /data
df -h /data

注意:mkfs 要作用在 /dev/mapper/data_crypt 上,不是 /dev/vdb。写错这一条会把 LUKS header 直接毁掉,磁盘变成一块「有加密头但没有文件系统的废盘」,只能重新格式化。

第四步:把挂载关系写进 fstab

这里最容易踩坑。LUKS 卷的 UUID 和文件系统的 UUID 是两个不同的东西:

# LUKS 容器的 UUID(用于 crypttab)
cryptsetup luksUUID /dev/vdb

# 文件系统的 UUID(用于 fstab)
blkid -s UUID -o value /dev/mapper/data_crypt

然后写两个文件:

# /etc/crypttab
# 格式:映射名  设备  keyfile  options
data_crypt  UUID=<上面查到的LUKS-UUID>  /etc/luks-keys/data.key  luks,discard

# /etc/fstab
UUID=<上面查到的FS-UUID>  /data  xfs  defaults,nofail  0 2

两个细节值得说清楚。第一,luks,discard 里的 discard 是给 SSD 传 TRIM 指令用的。这里有安全争议:TRIM 会暴露「哪些块是空的」这一信息,理论上可以推断文件大小分布;但不加 discard,SSD 用久了会因为不知道哪些块可回收而性能衰减。我选择加,因为对个人站来说,性能衰减的实际损失远大于「块使用模式泄露」的理论风险。想更稳妥可以用 cryptsetup --allow-discards --persistent 配合定期 fstrim,而不是实时 discard。

第二,nofail 必须有。如果某天 LUKS 解锁失败(密钥文件丢了、磁盘掉了),没有 nofail 的系统会卡在启动阶段进入 emergency mode,SSH 都进不来。nofail 让它启动失败也能继续,你至少能 SSH 上去看日志。这个参数救过我一次——磁盘从云平台掉线,机器自动重启后没卡死,我得以进去重新挂载。

第五步:验证重启

做完上面所有配置,先不要直接 reboot。用 cryptdisks_start 单独测试:

umount /data
cryptsetup luksClose data_crypt
cryptdisks_start data_crypt
mount /data && echo "挂载成功"
df -h /data

这一步能过,重启基本就没问题。但还是要保留一条带外通道(VNC 或者已在线的会话)再重启,因为 crypttab 的语法错误只有启动时才会暴露。

做完加密之后,有些运维习惯必须改

这是我认为最容易被忽略的部分。加密不是一个「配置完就忘了」的动作,它会改变一系列日常运维流程。

备份必须加密,否则等于没做

最讽刺的情况是:磁盘加密做得很好,然后每天凌晨 mysqldump 出一个明文 SQL 文件,rclone 推到对象存储上。攻击者根本不用碰你的磁盘。备份的加密有两个层面:传输加密(TLS,rclone 自带)和静态加密(restic/borg 的仓库加密)。如果你用 restic,它默认就是加密的;如果用 mysqldump + gzip,至少再套一层 gpg --encrypt。

swap 的处理

# 查看 swap 是不是加密的
swapon --show
cat /proc/swaps
lsblk -f | grep -i swap

如果 swap 在未加密的分区上,那么数据库的页缓存、内存里的私钥都可能被写进去。两条路:一是 zram(压缩内存盘,不落盘,最省事),二是加密 swap 分区。个人站服务器内存普遍 2~4G,用 zram 就够了:

apt install -y zram-tools
# /etc/default/zramswap 里设 ALGO=zstd, PERCENT=50
systemctl restart zramswap
swapon --show

注意:如果原来有 swap 分区,要先 swapoff 并把它从 fstab 里去掉,否则两份 swap 会混着用。

密钥的轮换与备份

LUKS 的密钥槽是可以独立管理的,这给了你轮换能力:

# 查看所有密钥槽
cryptsetup luksDump /dev/vdb

# 添加新密码
cryptsetup luksAddKey /dev/vdb

# 删除指定槽(比如旧密码所在的槽 0)
cryptsetup luksKillSlot /dev/vdb 0

什么时候该轮换?人员变动(有人知道密码)、怀疑密钥文件泄露、或者就是单纯想定期换。每次轮换后立刻验证还能打开卷,不要先删旧密钥——luksKillSlot 是不可撤销的,删错了就永久锁死。

另外,LUKS header 的备份是个专业操作:cryptsetup luksHeaderBackup /dev/vdb --header-backup-file /root/vdb-header.img。它的作用是「header 被误写坏时恢复」,但注意——这个备份文件本身就含有全部密钥槽(包括加密后的密钥),拿到它 + 密码就能解锁盘。所以它必须离线保存,不能放在同一台机器上。

性能实测:损失了多少

AES-XTS 在现代 CPU 上有 AES-NI 硬件指令加速,理论开销很小。我实测过(4 核云主机、NVMe SSD、fio 4K 随机写):

# 未加密盘
fio --name=t --filename=/dev/vdb --direct=1 --bs=4k --rw=randwrite \
    --size=2G --runtime=30 --time_based --ioengine=libaio --iodepth=32

# 加密卷(在 /dev/mapper/data_crypt 上跑)
fio --name=t --filename=/dev/mapper/data_crypt --direct=1 --bs=4k --rw=randwrite \
    --size=2G --runtime=30 --time_based --ioengine=libaio --iodepth=32

实测结果:IOPS 从约 24000 降到约 21000,下降 12% 左右;顺序读写几乎无差别(相差 2% 以内)。对一台跑 Nginx + MySQL 的站点,这个损失基本感知不到——瓶颈通常在 MySQL 的 fsync 和 PHP 执行上,不在磁盘加密。

真正需要注意的 CPU 占用:如果你的实例是老款 CPU 或者没开 AES-NI(grep aes /proc/cpuinfo 确认),软件加解密会吃掉大量 CPU,那就得重新评估。cryptsetup benchmark 可以先跑个基准看看硬件能力。

几个我踩过的坑

坑一:忘了 LUKS 卷不会自动 trim,SSD 越用越慢。 加密卷下 fstrim 需要 discard 支持。如果 crypttab 里没加 discard,fstrim /data 会报 the discard operation is not supported。补救办法是 cryptsetup --allow-discards --persistent refresh data_crypt,然后 fstrim -v /data 验证。这条建议写进定时任务,每月跑一次。

坑二:云平台快照 + LUKS 的组合有个陷阱。 云上做磁盘快照时,快照的是整个块设备(包括 LUKS header),所以恢复出来的盘还能用原来的密钥解锁——这是好事。但如果你在扩容之后做了快照,又回滚了快照,分区表会退回旧状态,cryptsetup 可能报 Device /dev/vdb is too small。扩容 LUKS 必须按顺序来:先 cryptsetup resize,再 xfs_growfs,两步都不能省。

坑三:luksClose 时提示 device is busy。 几乎一定是还有进程在用那个目录。别用 luksClose -f 强制关——那会在文件系统没卸载干净的情况下断开块设备,有损坏文件系统的风险。正确做法是 umount /data 后确认 lsof +D /data 为空,再关闭。如果 umount 提示 busy,先 fuser -vm /data 找出是谁。

坑四:/etc/crypttab 里的映射名和 /etc/fstab 里的设备路径必须一致。 fstab 里写 /dev/mapper/data_crypt,crypttab 里映射名就必须是 data_crypt。我就见过因为改名漏改一处,导致开机时「解密成功但挂载失败」,系统进 rescue mode 的情况。

坑五:luksFormat 默认的 PBKDF 迭代次数很高,老 CPU 上解锁很慢。 8 核以上的新机器通常 1~2 秒,但如果你在很老的 VPS 上做,可能等十几秒甚至更久。这是刻意的设计(增加暴力破解成本),不是 bug。如果确实太慢,可以在 luksFormat 时用 --pbkdf-memory 等参数调整,但我不建议降低安全性来换几秒启动时间。

什么时候我会说「别做」

技术方案都有适用边界,LUKS 也不例外。以下几种情况我会建议直接放弃:

没有带外管理、且需要重启的机器。 如果做全盘加密又没有 VNC/IPMI,任何一次意外重启都是永久失联。这种环境下只做数据盘加密,系统盘绝不动。

无法保证密钥备份的场合。 如果运维只有你一个人,且你「相信自己能记住」,那密码丢失的概率远大于被物理攻击的概率。密码管理器 + 离线介质 + 定期验证,这三件事做不到就别开始。

磁盘 I/O 已经是瓶颈的老机器。 如果 CPU 很弱、磁盘已经很慢,加密的 12% 开销叠加在上面可能让站点明显变慢。先解决 I/O 问题,再考虑加密。

只是「感觉应该安全一点」。 加密解决的是特定威胁(离线访问),如果你的真实威胁是「服务器被入侵挂马」,那这种投入应该放在更新补丁、收敛 SSH、加固上传目录上。安全投入要按威胁排序,不是按「听起来高级」排序。

Last modification:October 24th, 2026 at 08:27 pm

Leave a Comment