
备份一台你真正能恢复的 VPS
几乎每个运营服务器的人,手上都有一份自称是“备份”的东西。真正恢复过的人却少得多,而这两句话之间的距离,正是数据真正消失的地方。这里要讲的,是如何做出一份能扛过三件真正会毁掉服务器的事——你自己的手、一次入侵,以及一个你不再掌控的账号——同时又不会悄悄把一个已验证的身份,绑到一台本该匿名的机器上。
支撑这整张网络运转的每一个字节,我们都留着三份副本,而且是真的会去恢复——挑一个普普通通的星期二,主动去做,次数比我们在真正紧急情况下需要恢复的次数还要多。这不是为了自我感动的勤勉。这是唯一能确认仓库里存的是一份备份,而不是一个名字听着让人安心的加密乱码文件夹的办法。
下面这些建议,故意显得不够“时髦”。它不涉及带仪表盘的备份产品,不涉及按月订阅,也不涉及支持工单队列,因为这些东西里的每一样都意味着一个账号,而一个账号就是一个姓名、一张卡和一个司法辖区——这三样东西,你可能已经费了不少功夫,才让它们跟这台服务器扯不上关系。它只需要第二台机器、大约四十行 shell,再加上一个几乎没人能坚持下来的习惯。
整套流程,一个晚上应该就能做完。用到的工具以 Debian 13 和 Ubuntu 24.04 为准;换成 RHEL 系发行版,把对应命令替换一下也完全通用。如果你的服务器是新开的,应该先做完第一小时加固,再看这一篇,而不是反过来——给一台已经归别人所有的机器做备份,不过是在替别人保全劳动成果。
在一台租来的服务器上,真正毁掉数据的是什么
随便问谁为什么要做备份,得到的答案通常是“万一硬盘坏了呢”。而在一台现代 VPS 上,这恰恰是最不可能发生在你身上的事情之一。你的存储卷跑在 NVMe 做的冗余阵列上,宿主机一旦出问题,虚拟化层会在你察觉之前就把你迁移走;硬盘故障是服务商的问题,而且是一个已经被解决掉的问题。把整套备份策略都建立在这个假设上,就像给一栋会被水淹的房子配灭火器。
真正会拿走别人数据的东西,按出现频率大致排一下是这样的。第一位,而且遥遥领先:你自己。一条因为变量展开成了空值而变了味的 rm -rf。因为两个终端标签页长得一模一样,在错误的那个窗口敲下的 DROP DATABASE。一个被跑了两遍的迁移脚本。一次 docker compose down -v,那个 -v 纯粹是手滑的肌肉记忆。这些都不是什么稀罕事,是每个星期二都会发生的日常。
第二位,是会删掉自己数据的应用程序。一次跑到一半就失败的破坏性迁移升级。一个清空了某个目录的插件,而那个目录其实并不是缓存。一次配错了路径的日志轮转。第三位,是入侵——而且要注意,这时攻击者是主动想要把你的备份也弄没,这一点会从根本上改变设计思路,我们在下面还会回过头来讲。第四位,在无 KYC 主机上尤其容易被忽略:丢掉账号。你人不在的时候余额刚好归零,密码管理器里从没记下来的一条条目,一个被你弃用的邮箱地址。我们会在余额归零几小时内暂停服务,七天后彻底销毁,而且这个规则故意设计得毫不通融——正是这套算法,才让我们可以不去问你是谁。这里没有客服能帮你核实身份、给你破例,因为压根就没有身份可核实。
最后这一类,正是隐私优先型主机和主流主机的分界线,也应该成为你整个方案的出发点。每一项让这台服务器难以被归因到你身上的措施,同时也会让任何人——包括我们自己——都很难再把它还给你。而备份,正是把这笔交易从一种风险,变成一种可以自己选择的东西。
为什么快照不是备份
快照非常好用,你也应该用它。在每次内核升级前、每次数据库迁移前,以及每次你要做一件宁愿留个后悔药的事情之前,都打一个快照。它能在几秒钟内恢复,几乎不花什么钱,一年里能帮你省下十几个下午的功夫。
但它依然不是备份,原因是结构性的,不管服务商的宣传怎么说都改变不了。它活在同一个账号里面:谁能登录你的控制面板,谁就能把它删掉;账号一旦停摆,快照也跟着一起消失。它处在同一个故障域:同一个服务商,同一套控制平面,往往还是同一个存储集群,这意味着一次糟糕的事件,很可能把原件和副本一起带走。而且它是不透明的:对一台正在运行的机器打快照,会原封不动地捕捉到数据库写到一半的那个瞬间,跟捕捉其他任何东西一样忠实,所以对一台繁忙的 MySQL 服务器做快照恢复,得到的是一次崩溃恢复现场,而不是一个干净的数据库。
老生常谈的 3-2-1 原则——三份副本,两种介质,其中一份在异地——被人念叨的时候,很少有人注意到中间那句话放到租来的基础设施上其实毫无意义。你根本没有两种介质。你有的是一个虚拟块设备,加另一个虚拟块设备,而这两个说到底都是别人家的 SAN。真正从一开始就最要紧、也能在搬到 VPS 上之后依然成立的那部分是:至少要有一份副本,放在一次糟糕事件绝对够不到的地方。换一个服务商,或者至少换一个地区、换一套凭证、换一个账号;如果你选择离岸托管的原因是法律层面而非技术层面的,那还要换一个司法辖区,这样一张法院令,就没法同时砸到两份副本头上。
备份目的地,本身也是你威胁模型的一部分
这一节是其他备份教程里都没有的内容,但如果你这台服务器是用 Monero 付的款,这一节恰恰是最重要的一节。
互联网上随处可见的默认建议,都是把备份 push 到某个对象存储:Backblaze B2、Amazon S3、Wasabi,或者通过 rclone 传到 Google Drive。便宜、耐用、能用,这些都没错。但它也会用一条命令,把你之前做的一大堆功夫悄悄抵消掉。开那个存储账号,需要一张卡,通常还需要一份身份证件。从第一次快照上传的那一刻起,那家服务商手里就握着一份带时间戳的记录,把你的姓名和支付信息和你服务器的 IP 地址绑在一起,每小时刷新一次,永久保存。你验证身份的对象根本不是我们,而是他们——然后你自己在这两者之间,画上了一条连线。
更糟的是后半段,也是大多数人会漏掉的部分。给那些上传授权的 API 密钥,就是一个躺在生产服务器上的文件。任何拿到那台机器 root 权限的人——或者任何依法拿到那块硬盘的人——得到的不只是你的数据。他们得到的是一把凭证,一次 API 调用就能把它解析成一个计费身份。这台本该匿名的服务器,就这样变成了一块指向你银行账户的路牌。
这不代表对象存储这条路本身是错的,只是说它应该是一个经过权衡的决定,而不是默认选项。以下三条出路,按照它们在多大程度上还能保住你原本追求的那份匿名性排序:
- 第二台无 KYC 的 VPS,最好和生产环境不在同一个司法辖区,用同一笔加密货币余额支付。这样目的地继承的是源头的匿名属性,而不是跟它对着干。这也是我们自己的做法,本指南后面的内容也是按这个思路来写的。
- 接受加密货币、且不要求身份信息的存储服务商。这类服务商确实存在,规模通常比较小;在相信它的宣传之前,先自己确认一遍支付路径是不是真的不涉及身份信息,也要提前查清楚出站流量的收费标准,别等到真要恢复数据的时候才发现。
- 一台你自己实体持有的机器,躲在你自己的网络后面,通过 WireGuard 把数据拉过来。匿名性上无可挑剔,可用性上则很一般,这也意味着你的恢复速度,就是你家里的上传带宽。当第三份副本还说得过去,当唯一一份就很勉强。
不管你选哪一条路,存放备份的账号都不应该和存放生产环境的账号共用凭证、邮箱地址或找回途径。整件事的关键就在于,一次登录信息泄露,绝不能同时波及两边。这和在别处把身份分开管理是同一种纪律,只是被用在了系统里最不起眼的这个角落。
Push、pull,以及勒索软件为什么总能找到你的备份
几乎每一篇备份教程,最后给出的都是同一种架构:生产服务器上有个 cron 任务,认证到一个远程仓库,然后往里写数据。简单,能用,但有一个几乎没人提起的特性——生产服务器上留着一把能删掉整个仓库的凭证。清理旧快照需要删除权限,所以那把跑你夜间任务的钥匙,同时也是那把能把保险柜掏空的钥匙。
想想这在一次入侵中意味着什么。拿到 root 权限的攻击者根本不用费力去找你的备份;你已经很贴心地给他们留好了一把能用的凭证,外加一份写明了仓库位置的配置文件。在触发任何看得见的动静之前,先把备份找出来、逐一销毁,这不是什么理论上的高级技巧——而是标准操作,因为这正是把一起单纯的入侵事件,变成一场谈判的关键一步。你那个夜间任务,其实一直在替对方打前站。
这里有三种设计,它们之间的差别,完全在于哪把钥匙放在哪台机器上。
普通 push 就是上面说的那种。生产环境拥有读、写、删的全部权限。方便是方便,但恰恰会在你最需要备份的那种场景下,彻底失效。只有在你唯一真正担心的威胁是自己手滑的时候,才适合用它。
Append-only push 保留同样的架构形状,但去掉了那个危险的动词。仓库由某个懂协议、并且拒绝删除操作的程序来提供服务:restic 用 rest-server --append-only,Borg 则通过 authorized_keys 强制指定 borg serve --append-only。生产环境可以创建新快照,但没法删掉旧的。清理工作放到之后再做,从别的地方、用另一把钥匙来完成。花大约二十分钟就能带来这么大的改善,对大多数人来说,做到这一步就够了。
Pull 把这个连接方向整个反过来。备份主机通过 SSH 主动连进生产环境,把需要的东西拷过来,再在自己本地磁盘上运行备份工具。生产环境上完全不持有任何备份凭证——那台机器上根本没有什么可找的,因为它压根不知道自己的备份存在哪里。这是最强的一种安排,也是下面手把手步骤要搭建的方案。
老实说,pull 也不是没有代价。你只是把那把钥匙挪了个地方,而不是彻底消灭它:备份主机现在持有一把能连进生产环境的 SSH 密钥,所以备份主机一旦被攻破,攻击者就能顺势摸进正在跑的系统。这笔交易依然划算——备份主机什么服务都不跑,除了 SSH 什么都不对外暴露,比起一台面向公网的 Web 服务器,目标小得多——但终究是一笔交易,而剩下的大部分缺口,可以靠把这把钥匙强制绑定到一条只读命令上来补上,这样它就只能用来读文件,别的什么都做不了。
选工具:restic、Borg,还是纯 rsync
三个工具基本就能覆盖所有情况,选择起来其实远没有论坛帖子里吵得那么纠结。
restic 默认就加密,能跨快照去重,支持 SFTP、S3、REST 等十几种后端,而且就是一个能扔到任何机器上都能跑的静态二进制文件。它的仓库格式是按内容寻址的,所以打一次快照很便宜,不管有多少台机器往里发送数据,相同的内容也只会存一份。它的代价是真实存在的,但并不算大:需要的内存跟仓库索引的大小成正比,而且一次被打断的运行会留下一个过期锁,下一次运行会拒绝跨过去,直到你用 restic unlock 把它清掉。这是默认推荐的选择,下面的步骤用的也是它。
Borg 去重去得更彻底,压缩率也更高,在存有几百万个小文件的仓库上明显更快。它的代价是两端强耦合:两头都得装上 Borg,而且版本要对得上号,一个仓库实际上是为一个客户端设计的,也没有原生的对象存储后端,得加一层辅助工具才行。如果你的数据源是一个庞大的邮件存档,或者一个塞满小文件的文件系统,并且两台机器都在你自己手里,Borg 能帮你省下一半的空间。它的 --append-only 模式,也是这两个工具里,把这个思路实现得最干净的一个。
rsync 不是备份工具,硬把它当成备份工具用,结果往往就是一份跟原目录一模一样、被完整镜像下来的损坏副本。它没有版本管理,不会去重,静态数据也不加密;rsync --delete 会以线速把你的失误同步到副本上。但它确实是在你控制的两台机器之间搬运字节的正确工具,而这恰恰就是它在 pull 架构里要干的活,等字节落地之后,再由 restic 来提供版本管理和加密。让每个工具做它该做的事。
有一件事不要做:不要自己用 tar 加一个带日期的文件名来手搓一套备份系统。它大概能撑四个月,直到某天磁盘被塞满,因为从来没有任何东西过期;或者直到你发现,一个 40 GB 的数据集每晚全量拷贝一份,一个月下来就是 1.2 TB 的存储空间,而你花钱买下这些空间,换来的不过是三十份几乎一模一样的东西。
该备份什么——以及会背叛你的那些数据库
本能反应是把整个文件系统都备份下来。忍住这个冲动。根文件系统里大部分都是发行版的软件包,九十秒就能重装回来,把它们也备份进去,花的是存储空间、传输时间,更糟的是还要占用注意力——一份没人愿意去测试的 40 GB 备份,还不如一份每个季度都会被拿来恢复一次的 900 MB 备份来得有用。
真正没法重建出来的东西,其实没几样:/etc(你全部的配置,也是让重建一台机器只需要一个小时、而不是一个周末的原因)、/home 和 /root、/srv 和 /var/www、/var/lib/ 和 /opt/ 下的应用状态、具名的 Docker 卷、你自己写的 cron 和 systemd unit,还有你的数据库转储文件。跳过 /proc、/sys、/dev、/run、/tmp、/var/cache、交换文件、套接字,以及 /var/lib/docker/overlay2——最后这一个可以直接从你的 compose 文件重建出来,而且往往还是磁盘上单个体积最大的目录。
接下来是那个会悄悄毁掉恢复效果的部分。在数据库运行的同时去拷贝它的数据目录,得到的这一堆文件,十有八九是没法用的。数据库引擎把状态放在内存里,往多个文件里按一个讲究先后顺序的方式写数据,而你的拷贝操作要花上好几分钟去遍历这棵目录树,在不同的时刻抓到不同的文件。InnoDB 有时候能从这种状态里崩溃恢复过来,有时候不能;而这个问题往往要等到几个月后的一次恢复才会暴露出来——偏偏那天你的心情已经够糟了。
正确做法是通过数据库引擎自身来导出,然后备份导出的文件:
# MariaDB / MySQL — InnoDB gets a consistent snapshot from one transaction
mariadb-dump --single-transaction --quick --routines --triggers --events \
--all-databases | zstd -T0 > /var/backups/db/all-$(date -u +%F).sql.zst
# PostgreSQL — pg_dump is consistent by design; grab roles separately
pg_dumpall --globals-only > /var/backups/db/globals.sql
pg_dump -Fc -Z6 mydb > /var/backups/db/mydb.dump
# SQLite — never cp a live database; WAL mode makes that a torn file
sqlite3 /var/lib/app/app.db ".backup /var/backups/db/app.db"这里有两点补充值得记住。--single-transaction 只对 InnoDB 保证一致性——如果还有表是 MyISAM 引擎,它会在事务之外被拷贝,可能跟其余部分对不上,所以要么把这些表转换掉,要么在导出时把它们锁住。另外,一旦你开始做转储,就要把实时数据目录从文件级备份里排除出去。两边都备份,等于在一份好的副本旁边,又存了一份体积巨大的撕裂副本,将来恢复的时候就会面对两个候选项,其中一个还悄悄是错的。
换成容器也不改变这个原则,只是路径变了:用 docker compose exec -T db mariadb-dump … 这样的方式跑转储命令,然后备份生成的那个文件,而不是它底下的那个卷。
加密密钥应该放在哪里
客户端加密,正是你可以把数据放到一台不是它原产机器上的整个理由所在。restic 和 Borg 都会在数据离开源头之前就完成加密,所以备份主机上存的是密文,可以被当成不受信任的存储来对待。但这个特性到底值多少钱,完全取决于你怎么处理那把密钥,一分都不会多。
常见的翻车方式,令人沮丧地典型。仓库密码放在生产服务器上的 /etc/restic/env 里,这没问题,自动化也确实需要这样。然后生产环境恰好就是你丢掉的那部分——被销毁、被查扣,或者干脆随着账号一起没了——你面前摆着几百 GB 的加密数据块,然后才慢慢意识到,那把钥匙的唯一副本,恰恰就放在你原本想要防止丢失的那个东西里面。
所以:服务器上的密码只是一份工作副本,永远不是正式记录。正式记录应该放在一个比服务器本身活得更久的地方——一个自身也在别处有备份的密码管理器保险库,或者写在纸上锁进抽屉,或者两样都做。把仓库地址和精确的恢复命令一并写在旁边,因为一句脱离上下文的密码短语,十八个月后你翻出来时,就是一道要在压力下才能解开的谜题。再花五分钟验证一遍:换一台不同的机器,只用密码管理器里存的东西,对着仓库跑一次 restic snapshots。如果能跑通,你才算真的有一份备份。如果它还需要某个只存在于生产环境上的东西,那你有的只是一个非常昂贵的文件夹。
两个工具都支持在同一个仓库上使用多把密钥(restic key add),这是在不共享原始密码短语的前提下,让清理任务或者第二个管理员也能访问仓库的干净做法。另外,如果生产环境的磁盘用 LUKS 加了密,一定要让这两把密钥真正分开存放——把 restic 的密码短语存进 LUKS 卷里,同时把 LUKS 的密码短语存进 restic 仓库里,这是一个两头都会锁死的死循环。
保留策略:只留一周的陷阱
保留策略看起来像是个存储成本问题,实际上是个“问题要多久才会被发现”的问题。真正重要的数字,不是你愿意花多少磁盘空间,而是一个问题在你的系统里能潜伏多久都不被发现。不管这个时间有多长,你手上最老的那份备份,都必须比它更老。
七份每日快照听起来挺充裕,实际用起来却很单薄。一张没人查询的损坏表、一个行为异常的 cron 任务在悄悄慢慢删数据、一次潜伏了一个月才露出马脚的入侵——这些情况暴露出来所需的时间,通常都比一周长,如果你的历史记录只有七天深,那你手上留着的每一份快照其实都已经被污染了。真实入侵事件里,潜伏时间动辄以周计算。便宜的月度快照,就是专门针对这一点的防御手段,而去重技术能让它们变得非常便宜:保留一整年的月度快照,增加的存储量只是一份完整副本的一小部分,因为只有真正发生变化的数据块,才会被多存一份。
restic forget \
--keep-daily 7 --keep-weekly 5 --keep-monthly 12 --keep-yearly 2 \
--prune这样一套阶梯——一周的每日快照、一个月的每周快照、一年的每月快照,再加上几年的每年快照——加起来大约是二十六份快照,落到典型数据上,占用的空间也远不到一份完整副本的两倍。除非你有具体理由要偏离这个方案,否则我们会建议把它当成默认配置。
有两点操作上的提醒。不带 --prune 的 forget 只会去掉标签,空间并不会被真正释放,要等到你执行 prune 才行——这会让盯着一块怎么都不见缩小的磁盘的人感到意外。而 --prune 需要仓库的删除权限,所以在 append-only 或者 pull 架构里,它不会在生产服务器上运行。它运行在备份主机上,或者从你的笔记本电脑上执行,用的是一把生产环境从没见过的钥匙。这种分离正是整件事的意义所在;不要为了图最后一步的方便,就把它撤销掉。
把它自动化,同时别让失败悄无声息
经典的备份灾难,从来都不是某个任务崩溃了。而是一个任务不知不觉停止运行、也没告诉任何人,直到十一个月后某个正需要它的人才发现。下面的每一个环节,存在的目的都是让这种结局彻底变得不可能。
用 systemd 定时器,而不是 cron。这样你能在 journal 里拿到带退出状态的真实日志,配上 Persistent=true,机器关机期间错过的那次运行,会在下次开机时补跑,而不是永远被跳过;再配上 RandomizedDelaySec,一整批机器就不会在凌晨 03:00 整点一起挤爆备份主机。cron 的失败通知,是发到本地邮箱的一封邮件,而在一台现代服务器上,这封邮件哪儿都到不了。
# /etc/systemd/system/backup.timer
[Unit]
Description=Nightly encrypted backup
[Timer]
OnCalendar=*-*-* 03:17:00
RandomizedDelaySec=1800
Persistent=true
[Install]
WantedBy=timers.target接下来加一个“死人开关”(dead-man's switch),这是整篇指南里价值最高的一行。在脚本的最末尾——备份成功之后,而不是之前——向一个监控服务发一次 HTTPS 请求,这个服务预期每天都能收到你的消息,一旦收不到就会报警。这把通知逻辑整个反过来了:不再依赖失败去主动生成一条消息,而是让“沉默”本身变成警报。一个已经死了三天的任务,会变成你收件箱里的一封邮件,而不是危机降临时才被发现的真相。如果你不想再开一个第三方账号,可以把这个监控服务自己搭在备份主机上;也就几行配置,加上它自己的一个定时器。
最后是几个不起眼的运维隐患,每一个我们都至少踩过一次。一次被打断的运行会留下一把锁,之后每次运行都会失败并报出同一条消息,你大概到第四个晚上就懒得再看了——处理 restic unlock 要有意识地去做,而不是条件反射式地清掉。目的地磁盘一满,所有任务都会失败,直到有人注意到为止;所以要对剩余空间报警,而不只是对任务状态报警。一把到期的 SSH 密钥、一次轮换过的主机密钥、一条在做别的事情时顺手加上的 nftables 规则——每一个都会悄无声息地掐断 pull 连接。还有,要用 systemctl enable --now,而不是只用 start:一个从没被 enable 过的定时器,会一直运行得很漂亮,直到第一次重启之后,就再也不会运行了。
恢复演练
以上这一切,都只是准备工作。真正把它变成一份备份的,是这一步——也是几乎所有人都会跳过的这一步。
每个季度,部署一台全新的 VPS——$5 档的 Starter 就绰绰有余,计费按小时、精确到秒,整个演练下来也就几美分。往里面恢复数据时,只能用你在真实灾难中会有的东西:仓库地址、密码管理器里的密码短语,以及写下来的操作步骤。刻意不要用任何来自生产环境的东西,因为在你正在演练的这个场景里,生产环境已经不存在了。把应用跑起来,把 hosts 文件里的一条记录指向新 IP,点点看能不能用。然后把这台机器销毁掉。
# on a throwaway machine, with nothing but the passphrase
export RESTIC_REPOSITORY=sftp:[email protected]:/srv/restic/prod
restic snapshots # can you even see the history?
restic restore latest --target /restore
restic check --read-data-subset=10% # verify stored blocks, not just metadata它揪出来的问题,从来不是你事先能想到的那种。是那个待在 include 列表没覆盖到的目录里的配置文件。是那份因为密码改了、脚本又没检查退出状态,已经连续五周都是零字节的数据库转储。是那个怎么都启动不起来的应用,因为它的密钥其实是编排工具持有的一个环境变量,从来就没在文件系统里出现过。是那条没有写进文档的 DNS 记录。是那张必须先重新签发、443 端口才会有响应的证书。这些问题的每一个,在一个清闲的下午修好只要十分钟,但要是在凌晨三点碰上,就是丑陋的两个钟头。
把这次演练从头到尾花了多长时间记下来。这个数字——而不是备份频率——才是你真正的恢复时间,也是将来有人问你“得停机多久”时,唯一诚实的答案。再加一个月度定时任务去跑 restic check --read-data-subset=5%:它会读取并校验实际存储数据块里滚动抽取的一个样本,而不只是索引,这才是在还有一份好副本可以退回去用的时候,找出静默损坏的办法。如果哪天你要把整台服务器搬到新的主机商,一次演练过的恢复,其实也就等于把大半个迁移工作都提前做完了。
- 盘点真正没法重建的东西
在动用任何工具之前,先把清单写出来。把机器过一遍,对每个目录都问一句:如果它消失了,我能不能靠包管理器、一个 git 仓库或者一份 compose 文件把它重新造出来?能的话,它就不属于备份的范畴。剩下的东西,通常远比大家想象的要少——配置、用户数据、应用状态、数据库转储。
# the fast way to find what is actually big and stateful du -x -h -d2 / 2>/dev/null | sort -rh | head -30 docker volume ls # named volumes are state; overlay2 is not把盘点结果整理成两份明确的文件,
/etc/restic/include.txt和/etc/restic/exclude.txt。明确列出来的清单,胜过写得再巧妙的find表达式,因为它可以被审阅,也因为服务器上冒出一个新目录时,要不要备份它应该是一个主动做出的决定,而不是被悄悄自动收进来。 - 在另一个司法辖区部署备份目标机
再开一台 VPS,选一个跟生产环境不同的地区——如果生产环境在巴黎,就把副本放到雷克雅未克或者布加勒斯特。关键在于,不能有任何一次法律事件或物理事件同时波及两台机器。一个 $5/月的 Starter 套餐带 80 GB 的 NVMe 存储,经过去重之后,足够装下一台典型小型服务器相当长的一段历史;选 12 个月的计费周期,价格还能减半。用同一笔加密货币余额支付,这样目的地继承的就是源头的匿名性,而不是跟它唱反调。
如果想要凭证完全隔离,就给它开一个跟生产环境不同的账号。然后像对待其他任何机器一样把它加固好——仅密钥登录的 SSH,默认拒绝的防火墙——除此之外什么都不要装。这台机器的价值就在于它足够无聊:没有 Web 服务器,除了 SSH 没有其他开放端口,没有任何东西能被人从互联网上拿来利用。
- 从备份主机到生产环境,开一扇只读的门
这一步,正是让整套方案变成 pull 的关键。在备份主机上,生成一把专用密钥。然后把公钥那一半装到生产环境上,并配上一条强制命令,让这把密钥只能做一件事:读文件。它没法打开 shell,没法转发端口,也没法写任何东西。
# on the backup host ssh-keygen -t ed25519 -a 100 -f ~/.ssh/pull_prod -C "pull@backup" # on production, in /root/.ssh/authorized_keys — one line command="/usr/bin/rrsync -ro /",restrict ssh-ed25519 AAAAC3Nz… pull@backuprrsync是随 rsync 一起分发的(Debian 13 上在/usr/bin/rrsync;较早的版本在/usr/share/doc/rsync/scripts/rrsync),加上-ro之后,它会拒绝除读取以外的一切操作。restrict这一个词,就能同时关掉端口转发、agent 转发、PTY 分配和 X11。在真正依赖这道牢笼之前,先验证它是不是真的关得住——执行ssh -i ~/.ssh/pull_prod root@production,必须拿不到 shell 才对。 - 在生产环境上按自己的节奏导出数据库
生产环境依然要负责一件事:把一致性的转储文件,生成到一个暂存目录里,等着被 pull 收走。这个过程完全不需要任何备份凭证,而这正是整套设计能够成立的原因。
# /usr/local/sbin/dump-db.sh (chmod 700) set -euo pipefail D=/var/backups/db; install -d -m 700 "$D" mariadb-dump --single-transaction --quick --routines --triggers --events \ --all-databases | zstd -T0 > "$D/all.sql.zst.tmp" mv "$D/all.sql.zst.tmp" "$D/all.sql.zst"注意这里“先写临时文件、再改名”的手法:不管时机怎么凑巧,pull 都不可能收到一份写到一半的转储文件。
set -euo pipefail不是摆设——没有它,一次失败的mariadb-dump会把一个空流传给zstd,而后者还是会执行成功,最后你得到的是一个合法的压缩文件,里面却什么都没有。这是备份在悄无声息间变得毫无用处的头号常见原因。让它跑在自己的定时器上,比 pull 提前半小时执行。 - 把数据拉取到备份主机
在备份主机上,用 rsync 把生产环境 include 列表里的内容同步进一棵暂存目录树。只有发生变化的数据块会真正过网络,所以第一次运行之后,这个过程既快又便宜。
# /usr/local/sbin/pull.sh (chmod 700, runs on the BACKUP host) set -euo pipefail [email protected] # -r is spelled out on purpose: with --files-from, -a does NOT imply # recursion, and without it you silently copy empty directories. rsync -aHAX -r --delete --numeric-ids \ -e "ssh -i /root/.ssh/pull_prod -o BatchMode=yes" \ --files-from=/etc/backup/include.txt \ --exclude-from=/etc/backup/exclude.txt \ "$SRC:/" /srv/staging/prod/-aHAX会保留硬链接、ACL 和扩展属性,--numeric-ids则能让所有权信息,在/etc/passwd内容不同的两台机器之间依然保持有意义。这里用--delete是安全的,恰恰因为暂存区本身不是备份——真正带版本历史的数据,存在下一步要建的 restic 仓库里,所以哪怕一次删除操作被同步进了暂存区,依然可以从昨天的快照里恢复回来。 - 初始化仓库,并把密钥离线存好
还是在备份主机上,创建一个本地的 restic 仓库,把暂存目录树备份进去。本地意味着关键路径上没有网络,没有可以被偷走的远程凭证,恢复速度也能跑满磁盘的速度。
apt install -y restic install -d -m 700 /etc/backup openssl rand -base64 32 > /etc/backup/pass # write this into your password manager NOW chmod 600 /etc/backup/pass export RESTIC_REPOSITORY=/srv/restic/prod export RESTIC_PASSWORD_FILE=/etc/backup/pass restic init把这个密码短语拷进密码管理器,旁边再写上仓库路径和恢复命令。然后验证一遍:换一台第三方机器,只用密码管理器里的内容,跑一次
restic -r sftp:backup@…:/srv/restic/prod snapshots。如果能列出快照,说明这把钥匙是真的能用来恢复的。如果它还需要某个只存在于某台服务器上的东西,现在就把它解决掉,别等以后才发现。 - 把它排上日程,并让沉默本身变成警报
把 pull、restic 的备份操作,还有清理(prune),一起打包进一个脚本,交给一个 systemd 定时器去驱动。注意这里跑
forget --prune是安全的,因为仓库本来就名正言顺地归备份主机所有——生产环境从头到尾都没有持有过删除凭证。# tail of /usr/local/sbin/backup.sh restic backup /srv/staging/prod --tag nightly restic forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 \ --keep-yearly 2 --prune curl -fsS -m 10 --retry 3 https://hc.example.net/ping/<uuid> # only on successsystemctl daemon-reload systemctl enable --now backup.timer systemctl list-timers backup.timer # confirm the next run is where you expect这条
curl命令只有在它前面每一条命令都成功执行之后才会跑起来,靠的是set -e。另一端的监控服务预期每天都能收到一次 ping,收不到就报警,这就把一个悄悄停掉的任务,变成了一封邮件,而不是日后才被“考古”发现的秘密。用enable --now,而不是只用start——一个从没被 enable 过的定时器,命运就是撑到第一次重启为止。 - 跑一次恢复演练,并把耗时记录下来
在你真的会去看的地方,设一个每季度提醒一次的日程。部署一台一次性的 Starter,只靠密码短语和写下来的操作步骤把数据恢复进去,把应用跑起来,确认它提供的确实是真实数据,然后销毁这台机器。按小时计费意味着整场演练下来也就几美分。
restic restore latest --target /restore zstd -dc /restore/var/backups/db/all.sql.zst | mariadb systemctl start nginx app curl -H 'Host: example.com' http://127.0.0.1/health把耗费的时间和遇到的每一个意外都记下来,然后把这些意外修进备份脚本里,而不是只记在自己脑子里。这个耗时数字,才是你真正的恢复目标;在你亲自测量出这个数字之前,你说出口的任何数字,都只是猜测。
把副本放在哪里
| 目的地 | 匿名性代价 | 花费 | 恢复速度 | 能扛住什么 |
|---|---|---|---|---|
| 第二台无 KYC 的 VPS,不同地区 | 没有——用同一笔加密货币余额支付,全程没有任何身份信息 | $5/月,选 12 个月周期再减半 | 快——数据中心到数据中心,1–10 Gbps | 扛得住账号丢失、入侵、单一司法辖区出问题,以及你自己的失误 |
| 服务商快照 | 没有 | 便宜 | 几秒钟 | 几乎什么都扛不住——同一个账号、同一个故障域,两边一起完蛋 |
| 对象存储(B2 / S3 / Wasabi) | 高——账号上留着卡和证件信息,服务器上的 API 密钥又指向这个账号 | 存储很便宜,但恢复时的出站流量费会咬你一口 | 快,前提是你能接受出站流量账单 | 扛得住入侵和账号丢失,代价是有一个真实姓名被绑在了这台机器上 |
| 接受加密货币的存储服务商 | 低,前提是支付路径真的不涉及身份信息——要自己验证,不要想当然 | 中等 | 差异很大——在真正用到之前,先查清楚出站流量限额 | 能扛住大多数情况,但交易对手风险比你自己掌控的 VPS 要高 |
| 通过 WireGuard 拉取的家用 NAS | 没有——没有任何东西以未加密或可被追溯的形式离开你自己的网络 | 用你已经有的硬件 | 慢——上限就是你家里的上传带宽 | 除了你家本身,什么都扛得住;当唯一副本很勉强,当第三份副本则很出色 |
| 什么都不做,或者“反正代码在 git 里” | 没有 | 免费 | 永远不会有恢复这回事 | 什么都扛不住。Git 保存的是你的代码;它不保存你的数据库、你的上传文件,也不保存你的 /etc |
常见问题解答
服务商的快照不已经算是一种备份了吗?
不算,而且这个区别不是在咬文嚼字。快照和它复制的那台服务器,活在同一个账号里,用着同一套凭证,处在同一个故障域中——所以账号一丢,它就活不下来,而且任何能进你控制面板的人,都能把它和原件一起删掉。它还是从一台正在运行的机器上打出来的,这意味着一个繁忙的数据库会在写到一半时被捕捉下来。把快照当成做危险操作之前的“撤销”按钮来用;真正的备份,要用一份异地加密仓库来承担。
restic 还是 Borg?我该用哪个?
没有特别理由的话,用 restic。它默认加密,目的地不需要装任何东西,支持所有值得一用的后端,而且就是一个静态二进制文件。Borg 去重和压缩都做得更好,在有几百万个小文件的文件系统上明显更快,但它需要两端都装好、版本还要对上号,而且实际上是按“一个仓库对应一个客户端”设计的。数据源是庞大的邮件存档,而且两台机器都在你自己手里:选 Borg。除此之外的情况:选 restic。
备份目标机需要多大的磁盘空间?
对于一个用了本指南这套保留阶梯的去重仓库——7 份每日、5 份每周、12 份每月、2 份每年——大致按你实际要备份的数据量的两到三倍来预留空间,而不是按整台服务器的大小来算。二十六份快照不等于二十六份完整副本,因为只有发生变化的数据块才会被再存一次。一个真实状态只有 15 GB 的小型站点,放在 80 GB 的 Starter 套餐上绰绰有余,还能留出好几年的历史记录空间。
多开一台 VPS,账单不会翻倍吗?
只有当备份目标机配置和生产环境一样时才会,而它本来就不应该一样。它不跑任何应用,也不处理任何流量;它需要的是磁盘,不是 CPU。一台 $30 的生产服务器配一台 $5 的 Starter,只占账单的六分之一,选 12 个月周期还能再打对折。跟丢掉一切的代价比起来,这是账单上最便宜的一行——而且这台目标机还能兼职当恢复演练的排练场。
仓库密码应该存放在哪里?
存在一个自身也在别处有备份的密码管理器里,或者写在纸上,或者两样都做——旁边再放上仓库地址和精确的恢复命令。服务器 /etc 里的那一份,只是给自动化用的工作副本,从来都不是正式记录。要好好验证一遍:找一台既不是生产环境、也不是备份主机的机器,只用密码管理器里的内容,跑一次 restic snapshots。如果还需要别的什么东西,说明你手上这份备份还不能算是真正可恢复的。
备份应该多久跑一次?
先问自己愿意重做多少工作。对大多数服务器来说,每天一次就对了:最多损失一天的数据,而且一次夜间任务的行为也很容易推理清楚。繁忙的数据库需要更频繁一点——通常的做法是数据库每小时转储一次,而完整的文件级备份仍然保持每天一次。再往上加频率,带来的收益,还不如把同样的精力花在拉长保留时间和真正做一次恢复演练上,真正的风险恰恰就藏在那里。
如果服务器磁盘用 LUKS 加密了,还能备份吗?
可以,而且这两者是互补关系,不是重复劳动。LUKS 保护的是机器关机之后的那块磁盘;备份保护的是你不被删除、损坏和整台机器丢失所伤。备份已挂载的文件系统,跟备份一块没加密的磁盘做法完全一样——数据出去的路上 restic 还会再加一次密,所以仓库放在不受信任的存储上也是安全的。让这两把密钥真正分开存放:只把 LUKS 的密码短语存进 restic 仓库里,或者只把 restic 的密码短语存进 LUKS 卷里,都是一个两头都会锁死的死循环。
如果我的服务器被攻破了,备份还安全吗?
这完全取决于你几个月前做的一个决定。如果生产服务器上留着一把对仓库拥有删除权限的凭证,那答案就是不安全——攻击者会先把备份找出来销毁掉,因为这正是把一起入侵事件变成一场谈判的关键一步。如果仓库是 append-only 的,或者是备份主机主动 pull、生产环境完全不持有任何备份凭证,那么历史记录就能保全下来,你可以恢复到入侵发生之前的某个快照。这就是 pull 架构存在的全部理由,也是你应该趁现在还用不上它的时候,就先把它用上的原因。