OpenSSH
使用 OpenSSH 和密钥实现安全自动备份
通过使用 OpenSSH 和密钥,可以实现安全的自动化备份。rsync(1)
、tar(1)
和 dump(8)
是大多数备份方法的基础。很多人认为必须允许远程 root 访问才能进行备份,但这是一个误区。如果需要 root 权限,可以使用 sudo(8)
,或者对于 zfs(8)
,可以使用 OpenZFS 授权系统。请记住,在备份数据经过测试并能够可靠地恢复之前,它不能算作备份副本。
使用 rsync(1)
进行备份
rsync(1)
通常用于本地和远程备份。它速度快且灵活,通过增量复制,只传输更改的部分,从而避免浪费时间重新复制已在目标位置的数据。这是通过它的著名算法实现的。在远程工作时,需要一些额外的加密支持,通常做法是通过 SSH 隧道进行传输。
自 2004 年以来,rsync(1)
默认使用 SSH,因此以下命令将通过 SSH 进行连接,无需额外设置:
$ rsync -a fred@server.example.org:./archive/ \
/home/fred/archive/.
如果需要传递额外的选项给 SSH 客户端,仍然可以显式指定使用 SSH:
$ rsync -a -e 'ssh -v' \
fred@server.example.org:./archive/ \
/home/fred/archive/.
对于某些类型的数据,如果两端的 CPU 都能处理额外的工作,使用压缩(-z
)可以显著加速传输。但压缩有时也会导致传输速度变慢。因此,压缩需要在实际操作中测试,以确定它是有助于传输还是妨碍它。
使用密钥进行 rsync
备份
由于 rsync(1)
默认使用 SSH,因此它也可以通过 SSH 密钥进行认证,只需使用 -e
选项指定额外的 SSH 客户端选项。通过这种方式,可以指定一个特定的 SSH 密钥文件供 SSH 客户端在建立连接时使用。
$ rsync --exclude '*~' -avv \
-e 'ssh -i ~/.ssh/key_bkup_rsa' \
fred@server.example.org:./archive/ \
/home/fred/archive/.
如果需要,还可以通过 SSH 客户端的配置文件或在命令中以相同的方式传递其他配置选项。此外,如果将密钥添加到代理中,则只需要输入一次密钥的密码。这样做在现代桌面环境中的交互式会话中非常方便。在自动化脚本中,代理必须通过显式的 socket 名称传递给脚本,并通过 SSH_AUTH_SOCK
环境变量进行访问。
使用 sudo(8)
获取 root 级别的 rsync(1)
访问权限
有时备份过程需要访问不同于登录账户的账户。这通常是 root 账户,为了最小化权限,通常会禁止通过 SSH 直接访问 root 账户。在这种情况下,rsync(1)
可以在远程机器上使用 sudo(8)
调用。
假设你正在从服务器备份到客户端。客户端上的 rsync(1)
使用 ssh(1)
连接到服务器上的 rsync(1)
。客户端通过 -v
参数启动 rsync(1)
,以查看具体传递到服务器的参数。要将这些参数集成到服务器的 sudo(8)
配置中,需要在 SSH 客户端中运行,增加一个级别的详细信息,以查看需要使用哪些选项:
$ rsync \
-e 'ssh -v \
-i ~/.ssh/key_bkup_rsa \
-t \
-l bkupacct' \
--rsync-path='sudo rsync' \
--delete \
--archive \
--compress \
--verbose \
bkupacct@server:/var/www/ \
/media/backups/server/backup/
在这里,--rsync-path
参数告诉服务器用 sudo rsync
替代 rsync(1)
。-e
参数指定使用的远程 shell 工具,这里是 ssh(1)
。在被 rsync(1)
客户端调用的 SSH 客户端中,-i
参数指定了要使用的密钥。这与是否使用认证代理无关。可以为不同的任务使用不同的密钥。
要查看 /etc/sudoers
文件中的精确设置,可以在客户端上使用 -v
选项运行 SSH。使用模式时要小心,以免匹配过多内容。
调整这些设置可能需要一个迭代的过程。可以不断修改服务器上的 /etc/sudoers
文件,并观察详细输出,直到它按预期工作。最终,/etc/sudoers
文件中将允许 rsync(1)
以最小选项运行。
rsync(1)
使用 sudo(8)
进行远程备份的步骤
以下示例基于从远程系统获取数据,即将远程系统上的 /source/directory/
复制到本地的 /destination/directory/
。但是,这些步骤也适用于反向方向,只是某些选项会有所不同,--sender
会被省略。不管怎样,下面的例子中的复制粘贴将无法直接生效。
准备工作:创建专用帐户和密钥
创建一个仅用于备份的单一用途帐户,并为该帐户创建一对密钥。然后,确保你可以使用 SSH(带有或不带密钥)登录该帐户。
$ ssh -i ~/.ssh/key_bkup_ed25519 bkupacct@www.example.org
在服务器上的帐户名为 bkupacct
,而私钥 Ed25519
位于客户端的 ~/.ssh/key_bkup_ed25519
。在服务器上,bkupacct
账户属于 backups
组。如果需要,请参考公共密钥认证部分。
公共密钥 ~/.ssh/key_bkup_ed25519.pub
必须复制到远程系统上 bkupacct
账户的 ~/.ssh/authorized_keys
文件中,并放置在正确的位置。接下来,确保服务器上的以下目录由 root 拥有,并属于 backups
组,且组可读但不可写,绝对不可对全体用户可读:~
和 ~/.ssh/
目录,以及 ~/.ssh/authorized_keys
文件(这假设你没有使用 ACL)。这是设置远程系统权限的一种方式:
$ sudo chown root:bkupacct ~
$ sudo chown root:bkupacct ~/.ssh/
$ sudo chown root:bkupacct ~/.ssh/authorized_keys
$ sudo chmod u=rwx,g=rx,o= ~
$ sudo chmod u=rwx,g=rx,o= ~/.ssh/
$ sudo chmod u=rwx,g=r,o= ~/.ssh/authorized_keys
现在可以开始配置了。
第一步:配置 sudoers(5)
使 rsync(1)
可以通过 sudo(8)
在远程主机上工作
在这种情况下,数据保持在远程机器上。backups
组暂时需要完全访问权限,以便查找和设置稍后锁定权限时使用的特定选项。
%backups ALL=(root:root) NOPASSWD: /usr/bin/rsync
这是一个过渡步骤,重要的是,这一行不应长时间保持不变。
在该配置存在期间,请确保通过 --rsync-path
选项测试 rsync(1)
是否可以与 sudo(8)
一起工作:
$ rsync --rsync-path='sudo rsync' \
-aHv bkupacct@www.example.org:/source/directory/ /destination/directory/
传输应该没有错误、警告或额外的密码输入。
第二步:使用密钥进行身份验证再次进行相同的传输,确保两者可以一起使用
$ rsync -e 'ssh -i ~/.ssh/key_bkup_ed25519' --rsync-path='sudo rsync' \
-aHv bkupacct@www.example.org:/source/directory/ /destination/directory/
如果需要,请参考公共密钥认证部分。
第三步:收集连接细节,必要时调优 sudoers(5)
$ rsync -e 'ssh -E ssh.log -v -i ~/.ssh/key_bkup_ed25519' \
--rsync-path='sudo rsync' \
-aHv bkupacct@www.example.org:/source/directory/ /destination/directory/
$ grep -i 'sending command' ssh.log
第二个命令,带有 grep(1)
,应该会产生如下输出:
debug1: Sending command: rsync --server --sender -vlHogDtpre.iLsfxCIvu . /source/directory/
这段长字符串和目录名非常重要,因为它们将用于稍微调整 sudoers(5)
。记住,在这些示例中,数据从远程机器的 /source/directory/
复制到本地的 /destination/directory/
。
以下是符合上述公式的设置,假设帐户属于 backups
组:
%backups ALL=(root:root) NOPASSWD: /usr/bin/rsync --server --sender -vlHogDtpre.iLsfxCIvu . /source/directory/
这一行调整了 sudoers(5)
文件,使得备份账户能够以 root 权限运行 rsync(1)
,但仅限于其应该运行的目录,并且不能在系统上随意操作。
更多的细节优化可以稍后进行,但这些是锁定 sudoers(5)
的基本步骤。此时,你几乎完成了,尽管这个过程还可以进一步自动化。确保本地存储的备份数据对他人不可访问。
第四步:测试 rsync(1)
与 sudo(8)
通过 ssh(1)
连接,验证 sudoers(5)
中的设置是否正确
$ rsync -e 'ssh -i ~/.ssh/key_bkup_ed25519' --rsync-path='sudo rsync' \
-aHv bkupacct@www.example.org:/source/directory/ /destination/directory/
此时,备份应该能够正确运行。
第五步:通过在 authorized_keys
文件中使用 command="..."
选项锁定密钥,只能用于备份任务
command="/usr/bin/rsync --server --sender -vlHogDtpre.iLsfxCIvu . ./source/directory" ssh-ed25519 AAAAC3Nz...aWi
之后,密钥仅用于备份任务。它是在 sudoers(5)
文件中设置的权限基础上增加的一层额外保护。
通过这种方式,你可以使用 rsync(1)
实现自动化远程备份,并拥有 root 级别的访问权限,但避免了远程 root 登录。尽管如此,仍需密切关注私钥,因为它仍然可以用于获取远程备份,而备份中可能包含敏感信息。
从头到尾,这个过程需要对细节非常关注,但只要一步步进行,完全可以做到。设置反向方向的备份过程类似。对于从本地到远程的情况,将省略 ---sender
选项,目录会有所不同。
rsync
协议的其他实现
openrsync(1)
是 rsync
协议第 27 版的一个洁净房间重实现,它是由 samba.org 实现的 rsync(1)
支持的版本。自 OpenBSD 6.5 版本以来,它已包含在 OpenBSD 的基础系统中。它以不同的名称调用,因此如果远程系统上运行 openrsync(1)
,而本地系统上运行 samba.org 的 rsync(1)
,则必须通过名称指定 --rsync-path
选项:
$ rsync -a -v -e 'ssh -i key_rsa' \
--rsync-path=/usr/bin/openrsync \
fred@server.example.org:/var/www/ \
/home/fred/www/
反向操作,从 openrsync(1)
开始连接到远程系统上的 rsync(1)
,则不需要进行任何调整。
使用 tar(1)
进行备份
tar(1)
是创建档案的常见选择。但是,由于它是全文件和目录复制,rsync(1)
通常在更新或增量备份时更加高效。
以下命令将在本地机器上创建一个 /var/www/
目录的 tar 包,并通过管道将其发送到远程机器的 stdin,最终被存储为 backup.tar
文件:
$ tar cf - /var/www/ | ssh -l fred server.example.org 'cat > backup.tar'
还有许多变体:
$ tar zcf - /var/www/ /home/*/www/ \
| ssh -l fred server.example.org 'cat > $(date +"%Y-%m-%d").tar.gz'
这个例子会做相同的操作,但还会获取用户 WWW 目录,使用 gzip(1)
压缩 tar 包,并根据当前日期标记生成的文件。它也可以使用密钥:
$ tar zcf - /var/www/ /home/*/www/ \
| ssh -i key_rsa -l fred server.example.org 'cat > $(date +"%Y-%m-%d").tgz'
从远程机器到本地的操作同样简单,tar(1)
可以找到远程机器上的内容并将 tar 包存储在本地:
$ ssh fred@server.example.org 'tar zcf - /var/www/' > backup.tgz
或者以下是一个更复杂的例子,运行 tar(1)
在远程机器上,但将 tar 包存储在本地:
$ ssh -i key_rsa -l fred server.example.org 'tar jcf - /var/www/ /home/*/www/' \
> $(date +"%Y-%m-%d").tar.bz2
总之,使用 tar(1)
进行备份的关键是利用 stdout 和 stdin 通过管道和
重定向进行数据传输。
使用 tar(1)
备份文件但不创建 tar 包
有时,只需传输文件和目录,而不必在目标位置创建 tar 包。除了在源机器上写入 stdin 之外,tar(1)
还可以在目标机器上从 stdin 读取,来一次性传输整个目录层次结构。
$ tar zcf - /var/www/ | ssh -l fred server.example.org "cd /some/path/; tar zxf -"
如果方向相反,则命令如下:
$ ssh 'tar zcf - /var/www/' | (cd /some/path/; tar zxf - )
但是,这些命令每次运行时都会复制所有内容。因此,在许多情况下,使用上一节中描述的 rsync(1)
可能是更好的选择,因为在后续运行中它只会复制更改。此外,根据数据类型、网络条件和可用的 CPU,压缩可能是一个好主意,可以在 tar(1)
或 ssh(1)
本身中进行。
使用 dump
备份
远程使用 dump(8)
的方式与使用 tar(1)
类似。可以从远程服务器复制到本地服务器。
$ ssh -t source.example.org 'sudo dump -0an -f - /var/www/ | gzip -c9' > backup.dump.gz
注意,sudo(8)
的密码提示可能不可见,必须盲打。
或者,也可以反方向操作,从本地服务器复制到远程:
$ sudo dump -0an -f - /var/www/ | gzip -c9 | ssh target.example.org 'cat > backup.dump.gz'
再次提醒,dump(8)
的初始输出中可能会隐藏密码提示,但它仍然存在,只是不可见。
使用 zfs(8)
快照备份
OpenZFS 可以轻松地创建完整或增量快照,这也是写时复制(copy-on-write)的有益副作用。这些快照可以通过 SSH 传输到另一个系统或从另一个系统传输。此方法对于备份或恢复数据都非常有效。然而,带宽是一个考虑因素,快照必须足够小,以便适应实际的网络连接。OpenZFS 支持压缩复制,这样在磁盘上已压缩的块在传输过程中仍然保持压缩,减少了使用其他进程重新压缩的需求。传输可以是到或从常规文件或另一个 OpenZFS 文件系统。显然,较小的快照使用较少的带宽,因此传输速度更快。
首先需要一个完整的快照,因为增量快照只包含部分数据,并且要求它们依赖的基础快照存在。以下命令使用 zfs(8)
创建一个名为 20210326
的 site01
数据集快照,数据集位于名为 web
的存储池中。
$ zfs snapshot -r web/site01@20210326
程序本身通常位于 /sbin/
目录,因此要么需要将该目录添加到 PATH
环境变量中,要么直接使用绝对路径。增量快照可以通过 -i
选项基于初始完整快照进行构建。然而,OpenZFS 的管理超出了本书的范围,这里仅讨论系统间传输的两种方法。一种是通过中间文件传输,另一种是更直接的通过管道传输。两种方法都使用 zfs
send
和 zfs receive
,参与的帐户必须具有正确的 OpenZFS 授权权限。在发送时,使用 send
和快照相关的 OpenZFS 存储池。在接收时,使用 create
、mount
和 receive
操作相关的存储池。
通过文件将 OpenZFS 快照传输到远程文件系统
可以将快照传输到本地或远程系统上的文件,这种方法不需要在任何系统上使用特权访问,但运行 zfs
的帐户必须具有通过 zfs allow
授予的正确的 OpenZFS 内部权限。下面的例子展示了如何从远程系统下载一个非常小的快照:
$ ssh fred@server.example.org '/sbin/zfs send -v web/site01@20210326' > site01.openzfs
full send of web/site01@20210326 estimated size is 1.72M
total estimated size is 1.72M
如果复制增量快照,也需要复制其基础的完整快照。因此,需要确保这是一个完整的快照,而不仅仅是一个增量快照。
稍后,恢复快照时,只需执行反向操作。此时,数据从文件中检索并通过 SSH 发送到 zfs(8)
。
$ cat site01.openzfs | ssh fred@server.example.org '/sbin/zfs receive -v -F web/site01@20210326'
receiving full stream of web/site01@20210326 into web/site01@20210326
received 1.85M stream in 6 seconds (316K/sec)
这是可能的,因为该通道在没有 PTY 的情况下启动,如直接运行程序时那样。注意,目标的 OpenZFS 数据集必须先通过 zfs(8)
卸载,然后在传输后重新挂载。
反方向传输
如果要将数据从本地系统传输到远程系统,只需调整组件的顺序:
$ /sbin/zfs send -v web/site01@20210326 | ssh fred@server.example.org 'cat > site01.openzfs'
full send of web/site01@20210326 estimated size is 1.72M
total estimated size is 1.72M
然后,恢复过程也需要类似的调整:
$ ssh fred@server.example.org 'cat site01.openzfs' | /sbin/zfs receive -v -F web/site01@20210326
receiving full stream of web/site01@20210326 into web/site01@20210326
received 1.85M stream in 6 seconds (316K/sec)
为了避免在这些操作中使用 root 帐户,运行 zfs(8)
的帐户必须在 OpenZFS 授权系统中拥有适当的权限。
直接将 OpenZFS 快照传输到远程文件系统
另外,快照可以通过 SSH 传输到远程计算机上的文件系统。这种方法需要特权访问,并且会不可逆地替换远程系统自从快照以来所做的所有更改。
$ zfs send -v pool/www@20210322 | ssh fred@server.example.org 'zfs receive -F pool/www@20210322'
因此,如果远程系统使用可移动硬盘,则可以通过此方式更新它们。
$ ssh fred@server.example.org 'zfs send -v pool/www@20210322' | zfs receive -F pool/www@20210322
再次强调,远程帐户必须已经获得所需的 OpenZFS 内部权限。
反方向传输
如果要将数据从远程系统传输到本地系统,只需调整组件的顺序:
$ ssh fred@server.example.org 'zfs send -v pool/www@20210322' | zfs receive -F pool/www@20210322
或者:
$ zfs send -v pool/www@20210322 | ssh fred@server.example.org 'zfs receive -F pool/www@20210322'
与前述相同,通过使用 OpenZFS 授权系统,可以避免在传输过程中使用 root 账户。
缓冲 OpenZFS 传输
有时 CPU 和网络会交替成为文件传输过程中的瓶颈。mbuffer(1)
工具可以在 CPU 超前网络时,保持数据的稳定流动。关键是为缓冲区分配足够的空间,以确保即使 CPU 超前,网络仍能保持数据流动。
$ cat site01.zfs | mbuffer -s 128k -m 1G \
| ssh fred@server.example.org 'mbuffer -s 128k -m 1G | /sbin/zfs receive -v -F web/site01'
传输的总结信息:
summary: 1896 kiByte in 0.2sec - average of 7959 kiB/s
receiving full stream of web/site01@20210326 into web/site01@20210326
in @ 2556 kiB/s, out @ 1460 kiB/s, 1024 kiB total, buffer 0% full
summary: 1896 kiByte in 0.8sec - average of 2514 kiB/s
received 1.85M stream in 2 seconds (948K/sec)
有关 OpenZFS 和快照管理的更多细节超出了本书的范围。实际上,有很多关于 OpenZFS 的完整指南、教程,
甚至专门的书籍。