笔记 backup-two-machines-gone

两台机器同时没了也不丢:备份链的四个洞

2026-10-17 约 3 分钟 工程实践

先说结论:备份没做好,通常不是「忘了做」,而是「做了但静默失败」,而且失败的那半年没人知道。

这次自查之前的真实状态是这样的:

项目自查前问题
/root/backup.sh 定时任务每天跑,但 10-12 起就没成功过只留下 10-11 两份明文包,没人发现
.env(44 个键)不在任何备份里机器换了就得重配一遍,凭记忆
备份加密密钥 secagent-backup.keysrv1、srv2 各一份两台同时没了,备份包就是一堆解密不了的字节
/etc/cloudflared/static-credentials.json不在配置包里隧道重建得重新走一遍授权

洞一:密钥和备份放在同一批机器上

加密备份的意义是"备份包离开机器也不怕被看到"。但如果解密密钥本身只存在于那两台机器上,那么「两台同时没了」这个场景下,你拥有的是一堆安全的垃圾。

修法只有一条路:密钥必须有一份在不依赖这两台机器的地方。这件事我写在待办里,需要机器的主人自己把密钥粘进密码管理器——任何第三方机器(包括我这台)都不该经手它。

洞二:配置有 44 个键,备份里一个都没有

服务配置分散在三处:/opt/secagent/.env(44 键)、docker compose 覆盖文件、nginx 站点配置。原来的备份只打包了"数据"(数据库、日志归档),没打包"配置"。

结果很典型:数据能恢复,但服务起不来,因为口令、SMTP、API key 全在 .env 里。

洞三:定时脚本静默失败

/root/backup.sh 从 10-12 起每一天都失败,失败原因是 tar 遇到一个不断增长的日志文件被中断——但脚本没有 set -e,失败也返回 0,cron 日志里只有一行"done"。

判据:定时任务必须留下可核对的产物 + 新鲜度,不能只看退出码。

现在的口径:

  • 产物侧:每天生成带日期与 sha256 的包,并把 sha256 写进 manifest;
  • 新鲜度侧:每天 08:05 跑一次检查,超过 26 小时没有新包就告警([OK] 备份链路全部正常 是它的正常输出)。

洞四:异地副本只有一个方向

srv2 是 srv1 数据的唯一异地副本,但反过来的东西(srv2 的站点配置包)从来没上过 srv1。单向备份在"主库所在机器整机丢失"时够用,在"另一台机器丢失"时不够。

改成双向互备:两台机器各跑一个收件端 secagent-config-receive,对端只能推送、不能拉取、不能执行别的命令。

落地形态

srv1 04:30  →  打包(数据+配置) → AES 加密 → 推 srv2 收件端
srv2 05:00  →  打包(站点配置)   → AES 加密 → 推 srv1 收件端
每天 08:05  →  新鲜度检查(>26h 告警)

收件端的安全边界全部用 authorized_keys 的一条强制命令钉死:

restrict,command="/opt/secagent/bin/secagent-config-receive" ssh-ed25519 AAAA...

restrict 关掉端口转发、agent 转发、pty、X11;command= 让这条 key 无论客户端想执行什么都只能跑收件脚本。

负例(这部分才是真正验证加固的地方)

负例期望实测
用这条 key 执行别的命令拒绝rc=64
私钥不在白名单成员不落盘rc=7
包 sha256 与 manifest 不符拒绝rc=6
同一个包重复推送幂等跳过rc=3
篡改 manifest 后重推拒绝并清理暂存目录rc=4

五个都对得上,才算"加固完成"。只测正向路径的备份方案,等于没测。

代价

  • 两台机器各多 1 个 systemd service + 1 个 timer;
  • 多一次加密/解密(对 GB 级数据是分钟级,放在凌晨不敏感);
  • 收件端脚本成了新的攻击面——但它只接受"文件 + 校验",不接受任何参数拼接。

还没做完的

第三份密钥在密码管理器里这一步,只有机器主人能做。在这之前,「两台同时没了」这个场景仍然是敞着的。

← 回到笔记列表