本文是我在Ubuntu Server上使用加密ZFS文件系统的技术笔记。
为什么使用ZFS
考虑使用ZFS一开始是因为它强大的快照功能,可以实现不额外占用硬盘空间的热备份。随着对ZFS的探索,逐渐发现ZFS还有很多其它非常棒的特性,比如自带RAID、加密等。另外,ZFS是个用了都说好的文件系统(滑稽)。
安装ZFS相关工具
Ubuntu server 20.04已经官方支持ZFS 0.8.3,因此本段内容不再适用。建议更新到20.04获得更优质的ZFS体验。
我的服务器使用的是Ubuntu 18.04 LTS,所以这里理论上只需要# apt install zfsutils-linux
。然而这样安装的是0.7.5版本,不支持加密功能。因此我下载了0.8.3的源码自己编译了一份。需要注意的是,# make install
安装的zpool
一运行就会报错zpool: symbol lookup error: zpool: undefined symbol: zpool_reopen_one
。我没有深入调查原因,不过使用checkinstall
生成的deb包可以安装。安装过程中dpkg报错:
Get:1 /home/saltyfish/zfs-0.8.3/zfs_0.8.3-1_amd64.deb zfs amd64 0.8.3-1 [10.5 MB]
(Reading database ... 148721 files and directories currently installed.)
Preparing to unpack .../zfs_0.8.3-1_amd64.deb ...
Unpacking zfs (0.8.3-1) ...
dpkg: error processing archive /home/saltyfish/zfs-0.8.3/zfs_0.8.3-1_amd64.deb (--unpack):
trying to overwrite '/sbin/fsck.ext4', which is also in package e2fsprogs 1.44.1-1ubuntu1.3
Errors were encountered while processing:
/home/saltyfish/zfs-0.8.3/zfs_0.8.3-1_amd64.deb
E: Sub-process /usr/bin/dpkg returned an error code (1)
实际上ZFS包会试图覆盖两个文件:/sbin/fsck.ext4
和/sbin/mkfs.ext4
。手动把这两个文件备份好,然后用# dpkg -i --force-overwrite zfs_0.8.3-1_amd64.deb
安装即可。
开机自动挂载ZFS分区
根据这里的说法,需要手动启用以下systemd
服务和target才能让ZFS文件系统开机时自动挂载:
zfs-mount
zfs-import-cache
zfs-share
zfs-zed
zfs.target
zfs-import.target
然而经测试,开机后依然不会自动挂载(未加密的)ZFS文件系统,zpool list
报告no pools available
,手动执行# zpool import
命令可以导入创建好的zpool。 看了一圈,发现是因为zfs-import-cache.service
挂了导致的:
~ > sudo systemctl status zfs-import-cache
● zfs-import-cache.service - Import ZFS pools by cache file
Loaded: loaded (/usr/lib/systemd/system/zfs-import-cache.service; enabled; vendor preset: enabled)
Active: inactive (dead)
Condition: start condition failed at Mon 2020-02-10 20:14:08 CST; 15min ago
└─ ConditionPathExists=/usr/local/etc/zfs/zpool.cache was not met
Docs: man:zpool(8)
错误是因为没有/usr/local/etc/zfs/zpool.cache
文件导致的,按理说创建zpool的时候会自动修改这个cache文件,所以我找了一波。结果发现这个文件不在/usr/local/etc/zfs
,而在/etc/zfs
!找到了问题的原因就好办了:
# ln -s /etc/zfs/zpool.cache /usr/local/etc/zfs/zpool.cache
重启,自动挂载(未加密的ZFS文件系统)成功。
使用ZFS
创建ZFS存储池
使用zpool
创建存储池:
# zpool create data /dev/sdb
其中/dev/sdb
是我计划使用ZFS的硬盘。创建后这个存储池会被自动挂载到/data
。
创建加密的ZFS文件系统分层结构
由于我比较在意数据安全,我希望存储在硬盘上的数据都经过加密。
创建密钥
ZoL支持三种密钥格式:hex
、raw
、passphrase
,这里采用raw
方式。首先生成一个256位(32bytes)的密钥:
# dd if=/dev/urandom of=/etc/zfs/data.key bs=1 count=32
创建文件系统
接下来就可以开始创建加密的ZFS文件系统了:
# zfs create -o encryption=on -o keyformat=raw -o keylocation=file:///etc/zfs/data.key data/mailserver
这个文件系统要用于存储我的邮件服务器相关数据,所以起名叫mailserver
。
开机自动加载密钥
加密的文件系统开机不会自动挂载,原因是挂载加密文件系统前需要先加载密钥。将密钥存储在本地跟不加密没什么区别,而作为服务器,维护过程中少不了远程重启,所以比较理想的方案是每次重启都从远程取得密钥,解密数据后再删除密钥。
通过常规方式删除密钥后,攻击者依然可以通过某些手段轻松恢复被删除的密钥文件。所以安全的方式是使用shred
覆盖文件内容一定次数后再删除。
遗憾的是虽然Solaris中的ZFS提供了从远程通过HTTPS获取密钥的功能,但这一功能在ZoL(ZFS on Linux)目前的最新版本0.8.3仍然没有实现。不过没关系,接下来我们自己实现它。
本地配置
我这里通过在systemd
里自己创建服务来实现远程取得密钥,完成挂载后再删除的功能。创建一个/usr/lib/systemd/system/zfs-load-key.service
文件:
[Unit]
Description=Load keys of encrypted ZFS filesystems
After=network-online.target
Wants=network-online.target
Before=zfs-mount.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=/bin/bash -c "/usr/bin/curl $(/bin/cat /etc/zfs/key-location) -o /etc/zfs/data.key"
ExecStart=/usr/local/sbin/zfs load-key -a
ExecStartPost=/usr/bin/shred -fzu -n 50 /etc/zfs/data.key
[Install]
WantedBy=zfs.target
使用随机数据覆盖50次是一个比较极端的做法。实际上目前被认为非常安全的Guttman标准也只覆写35次,被指“非常耗时, 并且覆写数据又存在大量冗余”。然而,鉴于密钥文件的特殊性(体积极小、重要性极高),我还是采用了覆写50次的保险做法。我临时存储密钥的硬盘是HDD,如果是SSD还要更麻烦一些。关于安全擦除的方式,参见这篇论文。(2020/2/17更新)
其中After=network-online.target
和Wants=network-online.target
指示在网络连接完全可用后执行本服务。注意network.target
不能达到相同的效果。保存后执行# systemctl daemon-reload
重建依赖关系,然后启用这项服务:
# systemctl enable zfs-load-key
远端配置
把密钥文件上传到一个安全的web服务器,并指定一个复杂的路径(可以使用脸滚键盘法),比如/regjuh3t498hi20n3g9/320fh9uewvboi0in
。我创建的zfs-load-key.service
指示从/etc/zfs/key-location
文件指定的地址获取密钥文件,因此需要编辑/etc/zfs/key-location
并填入密钥地址,如https://example.org/regjuh3t498hi20n3g9/320fh9uewvboi0in
。将这个文件的权限设为000
确保安全(不会影响启动过程中的读取)。
使用shred
抹除本地的密钥文件。密钥最好备份几份,存在安全的地方,比如加密的U盘里。并且在不需要重启的时候,在web服务器上禁止访问这个文件!
修改zfs-mount.service
经过上面的操作,重启后你会惊喜地发现:ZFS文件系统并没有自动挂载!经过排查,我发现这是因为zfs-mount.service
指定了在systemd-random-seed.service
和local-fs.target
前启动,而network.target
依赖于这两者。因此systemd
不可能在启动zfs-load-key.service
前达到network-online.target
,使得zfs-load-key.target
进入inactive (dead)
状态,开机时不被自动启动。常规上,文件系统的挂载确实应在启动网络前执行完毕,但这里我的ZFS文件系统只用来存储应用数据,所以并不一定需要这个依赖关系。编辑zfs-mount.service
,注释掉Before=systemd-random-seed.service
和Before=local-fs.target
,重建依赖关系后重启。现在就可以自动挂载了。
应对curl
错误导致的挂载失败(2020/2/13更新)
实际使用中,有时重启后ZFS文件系统依然不会自动挂载,经调查原因是curl
出错(一般为返回值7
)。为了尽可能减少此类错误对正常使用和维护的影响,我对zfs-load-key.service
略作调整,将ExecStartPre
改成了:
/bin/bash -c "until /usr/bin/curl $(/bin/cat /etc/zfs/key-location) -o /etc/zfs/data.key || [ $retry -eq 500 ]; do ((retry++)); echo "Retry" $retry; done"
相当于在保留Type=oneshot
的情况下自己实现了Restart=on-failure
,还额外加了尝试次数限制。这样既可以避免curl
不成功影响到后续一系列服务的启动,也可以避免当真的发生连接问题时无法开机。500次重试看起来有点吓人……但是实验中最多的一次尝试了400多次才成功,可能是网络确实太烂的缘故吧~(胡乱甩锅)~
systemd
在PR#13754中已经对oneshot
类型的service开放了Restart=on-failure的选项,不过我目前使用的v237还没有整合这个更新;手动更新systemd
也不是一件容易的事,因此目前先采用循环的方式应对。
结语
本文略微探索了一下在Linux上使用加密ZFS文件系统的基本操作,重点是实现了远程自动获取密钥、解密文件系统后再销毁密钥的功能。很惭愧,就做了一点微小的工作,谢谢大家!