使用 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) 创建一个名为 20210326site01 数据集快照,数据集位于名为 web 的存储池中。

$ zfs snapshot -r web/site01@20210326

程序本身通常位于 /sbin/ 目录,因此要么需要将该目录添加到 PATH 环境变量中,要么直接使用绝对路径。增量快照可以通过 -i 选项基于初始完整快照进行构建。然而,OpenZFS 的管理超出了本书的范围,这里仅讨论系统间传输的两种方法。一种是通过中间文件传输,另一种是更直接的通过管道传输。两种方法都使用 zfs sendzfs receive,参与的帐户必须具有正确的 OpenZFS 授权权限。在发送时,使用 send 和快照相关的 OpenZFS 存储池。在接收时,使用 createmountreceive 操作相关的存储池。

通过文件将 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 的完整指南、教程,

甚至专门的书籍。

Last modified: Sunday, 19 January 2025, 9:46 PM