在Ubuntu Server上使用ZFS加密存储数据

2020-09-01 Technical Salty Fish 0条

本文是我在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文件系统开机时自动挂载:

  1. zfs-mount
  2. zfs-import-cache
  3. zfs-share
  4. zfs-zed
  5. zfs.target
  6. 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支持三种密钥格式:hexrawpassphrase,这里采用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.targetWants=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.servicelocal-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.serviceBefore=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多次才成功,可能是网络确实太烂的缘故吧~(胡乱甩锅)~

systemdPR#13754中已经对oneshot类型的service开放了Restart=on-failure的选项,不过我目前使用的v237还没有整合这个更新;手动更新systemd也不是一件容易的事,因此目前先采用循环的方式应对。

结语

本文略微探索了一下在Linux上使用加密ZFS文件系统的基本操作,重点是实现了远程自动获取密钥、解密文件系统后再销毁密钥的功能。很惭愧,就做了一点微小的工作,谢谢大家!