快照不是备份
你控制面板里的快照确实是个很有用的东西,在每一次有风险的改动之前都该打一个。但它不是备份,而这个区别并非咬文嚼字——它恰恰就是你想要在其中活下来的那整份情况清单。快照存放在同一台主机上,同一个账户之内,藏在与它所保护的那台机器完全相同的凭据背后。它只能很好地回答一个问题:我该怎么撤销刚才这二十分钟做的事?其他任何问题它都答不了。账户没了,快照也就跟着没了。地区不可达,回滚也就跟着不可达。入侵者摸到了控制面板,也就摸到了快照。而如果数据在打快照的那一刻本来就已经损坏,你也只是原封不动、忠实地把这份损坏保存了下来。
人们最常把两样东西错认成备份,道理是一样的。RAID 不是备份:它防的是硬盘物理损坏,而 rm -rf 这样的操作,它只会原速同步到镜像盘上。数据复制出于同样的原因,也不是备份——它的设计目的就是尽快让第二份副本和第一份完全一致,哪怕第一份副本刚刚被摧毁也不例外。在一个无 KYC 的主机服务商这里,账户这一条格外突出:注册时得到的只是一个用户名、一个密码和八个恢复码,全程没有邮箱或证件参与,所以一旦弄丢了它们,也没有客服阶梯可爬。这正是这类产品按设计该有的样子。这也意味着,真正要紧的那份副本,必须是你可能弄丢的那些凭据根本够不着的那一份。
3-2-1 法则,为一台没有人能追溯身份的服务器重写
老规矩依然成立:任何你在乎的数据都要有三份副本,分别存放在两套不同的系统上,其中至少一份放在异地。人们现在常写作 3-2-1-1-0 的这个现代扩展版本,补上了在 2026 年最要紧的两条:其中一份副本必须不可更改或处于离线状态,而且每次校验都必须零错误。落到单台服务器上,这句话的意思是:第一份副本是正在运行的数据本身;第二份副本是服务商侧的快照,或者每日加密备份这项附加服务,它不费你一点力气就能应对凌晨两点那种常见的手滑失误;第三份副本,是放在第二台机器上、位于另一个地区的加密仓库,第一台机器对它没有任何删除权限。只有第三份副本,能在第一台机器的账户丢失之后依然存活;也只有第三份副本,才真正属于你——因为没有人能被强迫交出它。
"异地"到底意味着什么,值得认真对待。同一栋楼里的另一个机架,在任何要紧的意义上都算不上异地;而同一套法律管辖之下的另一个地区,也只完成了一半——一份能触及其中一台机器的法律文书,不应该自动就能触及另一台。有 6 个地区可供选择,这是你在部署时一次性做出、之后无需再重新考虑的决定。不过有一点值得直说:这里的两台服务器,终究还是同一个服务商名下的两台服务器,无论隔离做得多好,这都是一种相关联的风险。如果你的威胁模型里确实包含了失去我们这一种情况,那么第三份副本就应该放在完全别处——家里的一台机器、朋友机架上的一个位置、另一个大洲上的另一家主机商。无论目的地放在哪里,本指南里的一切操作都完全一样;唯一会变的,只是某一个环境变量里的地址。
SP·03在数据离开这台机器之前就完成加密
目的地应该是一个只负责存放它自己读不懂的字节的地方。这不是你从某个服务商那里接受下来的一项政策,而是你自己构建出来的一种属性:数据在源端就完成分块、压缩、加密并加上认证,真正跨越网络传输的内容早已是一团不可读的密文。Borg 的 repokey 和 keyfile 模式会用处于计数器模式(counter mode)下的 AES-256 加密每一个数据块并加以认证,而带 -blake2 后缀的变体则把 HMAC-SHA256 换成了 BLAKE2b,在现代 64 位 CPU 上,速度提升是实打实可以测量出来的。文件内容、文件名,以及列出这些文件的归档清单,全都是加密的;目的地那边留存的,只是一堆编了号的分段文件和一个它自己也解释不了的索引。restic 用不同的实现方式,给你的是同样的效果。不管选哪一个,正确的心智模型都是:你租的是磁盘空间,而不是信任。
这里要老实交代两点。第一,仍然会泄露出去的东西:谁掌握着目的地那块磁盘,谁就能看到有多少数据在什么时候抵达。仓库大小和写入时间就算内容看不见,本身也是可见的,这对大多数人无关紧要,但对少数人不是。第二,目的地上的静态加密——也就是在备份机上启用 LUKS——防的是硬盘被搬出机房这种情况,而不是防运行中的主机本身,所以它是对客户端加密的补充,而不是替代。再往下,是真正会毁掉人的那部分:用 repokey 模式时,密钥材料就存放在仓库内部,由你的密码短语包裹保护,所以一旦丢了仓库,密钥也就跟着丢了;用 keyfile 模式时,密钥只存在源端,所以一旦丢了源端,你做过的每一份归档也就全都丢了。不管选哪一种,在你把密钥导出、并把导出的那份放到既不是源端也不是目的地的第三个地方之前,都谈不上安全。这件事要在第三步里就做掉,而不是"以后再说"。
Borg、restic、rclone:选一个,并且清楚为什么
这里有两个工具都算是正确答案,真正决定怎么选的是你的目的地是什么。Borg 做的是块级去重、压缩和认证加密,而且——这也是下文以它为例的原因——它自带一个真正的服务端仅追加模式,只要走普通 SSH 就能免费获得,不用跑守护进程,也不用额外开端口。它需要两端都装上 borg,说的是它自己的协议。restic 提供同样的保证,但不绑定特定后端:SFTP、S3 兼容的对象存储、Backblaze B2,或者它自己的 rest-server。在一个普通的 SFTP 目标上什么都不用装,这一点很方便,但这样一来,仅追加特性就得依赖 rest-server --append-only 或者存储桶策略,而不是依赖 SSH 限制。经验法则是:目的地是你自己掌控的服务器时用 Borg;目的地是对象存储、或者你想用同一个工具覆盖好几种后端时,用 restic。
不该用什么,也值得说清楚,因为这是最常见的翻车方式。rclone 是一个同步工具。它的 crypt remote 确实能给你客户端加密,但同步这个动作会把删除操作也一并同步过去——你在凌晨三点手滑删掉的文件,三点一刻就会在目的地被忠实地删掉,而这恰恰就是你原本想要在其中活下来的那个故障场景。用它把一个已经做好的 Borg 或 restic 仓库再推送到第三个地方,是很好的用法;但它本身不是备份。放在 cron 里裸跑的 rsync --delete,也是同样的问题——那不过是一面镜子,披着备份的外衣。用 tar | gpg 打包发送到远程路径,倒确实算得上是真正的备份,但没有去重、没有保留逻辑,恢复时还得沿着一整条增量链慢慢走,才能取回一个文件。用那些真正为这件事而设计的工具;本指南里提到的每一个发行版,都已经把它们打好了包。
仅追加,否则入侵者连你的备份也会一并删掉
这里有一个场景,能把备份体系和备份脚本区分开来。有人拿到了你生产机的 root 权限——通过应用程序、某个依赖项、一把泄露的密钥,具体怎么进来的不重要。称职的勒索软件会做的第一件事,以及一个称职的人类攻击者会做的第一件事,都是去找备份,因为一个手里握着可用备份的受害者,既不用付钱,也用不着慌。你那个每晚跑的任务是无人值守运行的,所以它的凭据必然就放在那台机器上。如果这个任务能删除归档,入侵者也就能删除归档,而你恰恰会在最需要它们的那一刻才发现这一点。这不是一个假设出来的故障模式;这是常态。
修复方法很小,却是本指南里最重要的一行。在目的地这一端,把自动化用的那把密钥,钉死在 authorized_keys 里的一条命令上:command="borg serve --append-only --restrict-to-path /srv/borg",restrict。这把密钥现在就只能做一件事——往一个路径下的仓库里追加数据。它拿不到 shell,转发不了端口,列不出你的文件系统,也删不掉哪怕一份归档。restrict 关键字(OpenSSH 7.2 及以上版本)一个词就把 pty 分配和所有类型的转发统统关掉,所以这条限制不会因为以后加了什么新选项就悄悄失效。接下来是大多数教程都会跳过的一点实话:仅追加生效期间,borg prune 和 borg compact 看起来会成功执行,实际上却什么都释放不了——那次删除会被记录成一笔事务,下一次仅追加会话一开始就会把它回滚掉。所以,保留策略需要另外一把不受限制的密钥,由你在笔记本电脑上手动使用,绝不能存放在源端。两把密钥,两项工作:夜间那把只能写;维护那把能删,而且放在被入侵的那台机器根本够不到的地方。这把维护密钥还要留着应付另一种情况——一个跑到一半被杀掉的任务会留下一个残留的锁,而 borg break-lock 本身也是一次写操作。
第二份副本应该放在哪里
备份是唯一一种延迟完全不重要的工作负载,所以不必理会那种把机器放在离用户近一点的地方的本能,而应该反过来,为法律管辖和独立性去选地方。和生产环境不在同一个国家,是底线;处于不同的法律体系之下,则更好。我们的各个地区,回答的是各不相同的问题——罗马尼亚是旗舰地区,DMCA 通知在这里根本不会被处理;瑞士身处欧盟之外,背靠异常严格的数据保护法规;冰岛有 IMMI 框架撑腰;巴拿马没有强制性的数据留存法律,也不会给外国的请求开绿色通道;马来西亚能让一份副本彻底待在五眼联盟的势力范围之外;荷兰则是网络对等互联枢纽。该如何选择离岸地区?一文把这些取舍都讲透了;对一个备份目标来说,答案通常就是"哪里不是生产环境所在,就放哪里"。每个套餐的流量都不限量计费,所以第一次全量上传的速度,只取决于源端读自己磁盘的速度,而不用为了配额精打细算。
要多大的规格,其实没有大家想象的那么夸张。去重加上 zstd 压缩,意味着仓库通常只有源端的一小部分大小,而且第一次运行之后,只有发生变化的数据块才会传输——一台繁忙的、有 40 GB 数据、变化速度正常的服务器,每晚增量通常也就是几百兆,所以一年份的每日归档,占用的磁盘空间,比一年份的每日 tar 包要少得多。梯度里最小的那档套餐,就已经是一个相当合格的目标了;只有当你要为某个体量确实很大的东西保留很长的历史记录时,才有必要往上升级,具体的规格请以套餐页面为准,而不要相信写在某篇指南里、迟早会过时的数字。关于这台机器本身,有一条规矩:别让它干别的事。没有 Web 服务器,没有数据库,除了靠密钥登录的 SSH 之外没有任何公开服务。一个还顺带跑着副业项目的备份目标,同时也就继承了那个副业项目的攻击面,所以在它接收第一份归档之前,就应该先经过完整的部署后第一小时处理。
SP·07没恢复过的备份,只是个传闻
备份体系很少会闹出很大动静地失效。它们失效的原因,可能是某条排除规则悄悄把整个数据目录吞掉了,也可能是数据库在还有写入正在进行时被逐个文件复制,导致转储恢复出来是一张面目全非的表,还可能是某次发行版升级之后,定时任务已经默默失败了六个星期,而没有人去看邮件队列。能查出这些问题的,只有一种测试:恢复。要定期去做:随机挑一份归档,解压到一个临时目录,挑几个文件和生产环境做 diff,把数据库从转储里拉起来跑一条查询,然后记下整个过程花了多长时间。这个数字,才是你真正的恢复时间——而不是你自己以为的那个——也是唯一一个值得你在凌晨三点拿出来对自己说的数字。至少要有一次,从一台空白机器开始做这场演练,因为那才是真实场景:一台全新的 VPS、密码管理器里的一句密码短语、你不知道存在哪儿的那份导出密钥,除此之外什么都没有。
监控这件事,也该用你对待整套体系里其他部分那种怀疑态度去看待。常见的建议是找一个第三方的 dead-man's-switch 服务,让你的任务每次成功时都向它打卡一次,可这么做等于在悄悄告诉一个外部服务你的主机名、你的运行计划,以及你的基础设施什么时候出了问题——而这样的东西,绑在一台你特意买来、故意不留下任何身份信息的机器上,实在有点别扭。你根本不需要它。数据是否新鲜,完全可以在不需要密钥的情况下,从目的地那一侧去检查:仓库里最新的分段文件自带时间戳,所以在备份机上放一段五行的 cron 脚本,一旦二十五小时没有新数据抵达就报警,这既不花一分钱,也不泄露任何东西。完整性也有一种不需要密钥的检查方式——borg check --repository-only 在目的地本地运行,校验分段结构,全程看不到你的任何数据。至于更深入的 --verify-data 校验,偶尔从源端用维护密钥跑一次就够了,密钥本来就该待在那儿。
成本几何,以及它在整套体系中的位置
这笔账根本算不上势均力敌。第二台 VPS,价格从 $8.00/月起,用同一笔预付费余额支付,充值最低 $30.00 起,大约 15 min 就能上线——拿这个去对比丢掉第一台机器上所有数据的代价,完全不在一个量级上。让它和服务商侧的备份一起跑,而不是用它取而代之:每日加密备份这项附加服务,能在你什么都不用做、凌晨三点也不用多想一秒的情况下,处理删错了目录这种日常失误;而你在这里搭起来的这一套,是当账户、地区或者服务商本身出问题消失时,唯一还留在你手里的那份副本。它们应对的是不同的故障,谁也替代不了谁,而这正是数到三这件事的全部意义。
最后,值得把备份放回它在整个格局里的位置上来说清楚,因为这是四道彼此独立失效的防线。主机服务商知道你什么,这是第一道,而在这里,答案几乎为零——一个用户名和一份加密货币余额,这正是匿名支付主机费用一文的主题。适用哪一国的法律,是第二道,这由硬件所在的位置决定,而不是由你所在的位置决定。这台机器本身允许什么,是第三道,也就是部署后的第一小时要做的事,完全只能靠你自己去配置。而什么能在这台机器之外存活下来,是第四道——也是唯一一道你许给未来的自己的承诺,唯一一道不会有人提醒你、直到为时已晚那天才追悔莫及的防线。今晚花一个小时,再在日历上安排一次恢复演练。事情就是这么多,也就这么简单。
SP·09分步操作
-
01
部署备份目标机,别让它干别的事
在一个不是生产环境所在的地区部署第二台 VPS,把它当成一台单一用途的专用设备来对待。在它上面照常跑一遍标准的第一小时清单——仅密钥 SSH、两个 IP 协议族都默认拒绝的防火墙、无人值守的安全更新——除此之外什么都不开。这台机器上唯一的入站端口,就是 SSH。
ssh root@198.51.100.7 apt update && apt full-upgrade -y hostnamectl set-hostname vault-01 apt install -y borgbackup ufw unattended-upgrades ufw default deny incoming && ufw default allow outgoing ufw limit 22/tcp && ufw enable
-
02
创建一个受限的 borg 账户,配两把密钥
在源端生成两对密钥:一对给每晚的任务用(不设密码短语——因为它得无人值守运行),另一对给维护用,只保存在你的笔记本电脑上。在目标机上,创建一个无特权的
borg用户,把每一把密钥都钉死在一条强制命令上。任务密钥带--append-only;维护密钥不带。挡住源端入侵者、不让他抹掉你历史记录的,就是这一个文件。# on the source ssh-keygen -t ed25519 -N "" -f /root/.ssh/borg_job -C "job@edge-01" # on your laptop ssh-keygen -t ed25519 -f ~/.ssh/borg_maint -C "maint@laptop" # on the target adduser --disabled-password --gecos "" borg install -d -m 700 -o borg -g borg /home/borg/.ssh /srv/borg cat > /home/borg/.ssh/authorized_keys <<'EOF' command="borg serve --append-only --restrict-to-path /srv/borg",restrict ssh-ed25519 AAAA...job@edge-01 command="borg serve --restrict-to-path /srv/borg",restrict ssh-ed25519 AAAA...maint@laptop EOF chown borg:borg /home/borg/.ssh/authorized_keys chmod 600 /home/borg/.ssh/authorized_keys
-
03
初始化仓库,然后把密钥从两台机器上都转移出去
用维护密钥,从你的笔记本电脑上完成初始化——创建仓库这个动作本身并不是一次追加。每台源主机对应一个独立仓库,能让保留策略变得简单,也能把影响范围控制得很小。然后把密钥导出两次、存成两种格式,并把这两份导出结果都放到既不是源端也不是目标机的地方:密码管理器、一个加密的 U 盘、抽屉里的一张纸都行。一个密钥只存在于自身内部的仓库,能不能救回来,全凭运气。
export BORG_REPO='ssh://borg@198.51.100.7/srv/borg/edge-01' export BORG_RSH='ssh -i ~/.ssh/borg_maint' borg init --encryption=repokey-blake2 borg key export :: /tmp/edge-01.borgkey borg key export --paper :: /tmp/edge-01.paper # move both off this machine, then shred the copies shred -u /tmp/edge-01.borgkey /tmp/edge-01.paper
-
04
在读盘之前,先冻结应用状态
把一个正在运行的数据库目录逐个文件复制下来,得到的东西看起来像数据库,恢复起来却像犯罪现场。要先做转储(dump),再备份这份转储,并且把原始数据目录排除在外。同样的道理适用于任何磁盘格式你自己控制不了的东西:要么把它转储出来,要么就在打快照的那几秒钟里把它停下来。
install -d -m 700 /var/backups/dumps mariadb-dump --single-transaction --quick --all-databases \ > /var/backups/dumps/mariadb.sql # PostgreSQL: # sudo -u postgres pg_dumpall > /var/backups/dumps/pg.sql chmod 600 /var/backups/dumps/*.sql
-
05
写好备份任务,挂到定时器上
脚本要写得越无聊越好,一旦出问题也要让人立刻察觉。密码短语通过
BORG_PASSCOMMAND从一个权限为 600 的文件里读取,这样它就永远不会出现在进程列表里。归档命名要带上主机名和 ISO 格式的时间戳,方便排序。注意这个脚本里没有什么:没有prune,没有delete——反正任务密钥本来也执行不了这些操作。带Persistent=true的定时器,能在重启之后自动补跑一次;而一个随机延迟,能防止你所有的机器在同一秒钟一起上传。cat > /usr/local/sbin/borg-backup.sh <<'EOF' #!/bin/bash set -euo pipefail export BORG_REPO='ssh://borg@198.51.100.7/srv/borg/edge-01' export BORG_RSH='ssh -i /root/.ssh/borg_job -o BatchMode=yes' export BORG_PASSCOMMAND='cat /root/.config/borg/passphrase' borg create --stats --compression zstd,6 --one-file-system \ --exclude-caches --exclude '/var/cache/*' \ --exclude '/var/lib/mysql' --exclude '/home/*/.cache' \ ::'{hostname}-{now:%Y-%m-%dT%H:%M}' \ /etc /root /home /srv /var/www /var/backups/dumps EOF chmod 700 /usr/local/sbin/borg-backup.sh cat > /etc/systemd/system/borg-backup.service <<'EOF' [Unit] Description=Off-site Borg backup [Service] Type=oneshot Nice=10 IOSchedulingClass=idle ExecStart=/usr/local/sbin/borg-backup.sh EOF cat > /etc/systemd/system/borg-backup.timer <<'EOF' [Unit] Description=Nightly off-site Borg backup [Timer] OnCalendar=*-*-* 03:17:00 RandomizedDelaySec=900 Persistent=true [Install] WantedBy=timers.target EOF systemctl daemon-reload systemctl enable --now borg-backup.timer -
06
用维护密钥执行 prune,在目标机上执行 compact
保留策略没法从源端运行,因为源端那把密钥是仅追加的,它做的任何删除都会被悄悄回滚掉。要改成从你的笔记本电脑上运行,节奏随你自己定——每月一次就绰绰有余。
prune负责决定保留哪些归档;真正把磁盘空间要回来的,是compact。先跑一次--dry-run,把清单看一遍,再放手让它删东西。export BORG_REPO='ssh://borg@198.51.100.7/srv/borg/edge-01' export BORG_RSH='ssh -i ~/.ssh/borg_maint' borg prune --dry-run --list \ --keep-daily 7 --keep-weekly 4 --keep-monthly 6 borg prune --list --stats \ --keep-daily 7 --keep-weekly 4 --keep-monthly 6 borg compact --progress
-
07
跑一遍恢复演练,然后写进运维手册
要在真正需要恢复之前,就先演练一遍恢复。把一份真实的归档解压到一个临时目录,挑几个文件和原始文件做对比,再把数据库转储加载进一个用完即弃的 schema 里。然后加上两项不需要你操心就能自动运行的检查:目标机上一个不需要密钥的新鲜度告警,以及一次周期性的完整性校验。最后,把你在最糟糕的那一天会需要的三个事实写下来——导出的密钥放在哪里、密码短语放在哪里,以及下面这几条准确的命令。
borg list :: mkdir -p /var/tmp/drill && cd /var/tmp/drill borg extract --list ::edge-01-2026-08-31T03:17 etc/nginx diff -r etc/nginx /etc/nginx && echo 'restore OK' # on the target, no key needed: borg check --repository-only /srv/borg/edge-01 find /srv/borg/edge-01/data -type f -mmin -1500 | head -1 # empty = stale

