引言:备份系统的失败,往往不是因为 Restic 崩溃
Restic 本身的可靠性不是问题——它的去重、快照、加密机制在开源备份领域已是标杆。但我在客户审计中发现,90% 的 VPS 备份系统死法惊人地相似:
- S3 桶被误删或计费账号被盗,备份随数据一起蒸发
- 备份密钥和备份数据放在同一台机器上,被勒索软件一波带走
- 只有一层"复制",没有异地容灾,机房断网/宕机时备份和源一起不可用
- 从未做过一次"真实恢复",直到出事才发现备份根本恢复不出来
- 备份桶没有对象锁,攻击者拿到写权限后可以删掉最近快照
Restic 是"工具",不是"策略"。本文的核心是把备份系统当安全资产设计,用 6 层纵深防御 + 2-1-1 策略 + 可验证恢复,让数据灭失的概率在工程上趋近于零。
一、威胁模型:先想清楚你要防什么
| 威胁 | 概率 | 杀伤力 | 需要的层 |
|---|---|---|---|
| 误操作删库/删桶 | 高 | 致命 | 客户端加密 + 对象锁 |
| 计费账号被盗,桶被删 | 中 | 致命 | 异地复制 + 独立账号 |
| 勒索软件感染 VPS | 中 | 致命 | 密钥分离 + 对象锁 |
| 机房故障/断网 | 低 | 高 | 异地容灾 |
| 恢复时密钥丢失 | 中 | 致命 | 密钥多重备份 |
| 备份"看起来在"实际损坏 | 中 | 高 | 定期可恢复性演练 |
关键洞察:每一层独立失效都不致命,六层同时失效的概率低到可以忽略。 这就是纵深防御的价值。
二、6 层加密与防护体系
第 1 层:客户端 AES-256-CTR 加密(Restic 原生)
export RESTIC_REPOSITORY=s3:minio-endpoint:backup-bucket
export RESTIC_PASSWORD_FILE=~/.config/restic/password
restic -r $RESTIC_REPOSITORY init
restic -r $RESTIC_REPOSITORY backup /etc /home /var/lib/docker
Restic 默认使用 AES-256-CTR + 目录/文件元数据哈希。即使 S3 桶被整个下载走,攻击者拿到的只是密文。
验证点:restic check --read-data 确认仓库未损坏。
第 2 层:S3 服务端默认加密(SSE-S3)
aws s3api put-bucket-encryption \
--bucket backup-bucket \
--server-side-encryption-configuration '{
"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]
}'
即使客户端加密被绕过(例如有人从服务器内存截获明文),服务端仍会对静态数据加一层。Cloudflare R2 上 SSE 无额外费用,MinIO 同样原生支持。
第 3 层:KMS 信封加密(可选,针对合规要求)
对需要 SOC2/等保 2 的场景,用 SSE-KMS + 独立 KMS key:
aws s3api put-bucket-encryption \
--bucket backup-bucket \
--server-side-encryption-configuration '{
"Rules":[{"ApplyServerSideEncryptionByDefault":{
"SSEAlgorithm":"aws:kms",
"KMSMasterKeyID":"arn:aws:kms:.../key/backup-master"
}}]
}'
R2 没有 KMS,可用 restic + 自建 Vault 做 key 管理,或接受 SSE-S3 作为该层替代。
第 4 层:桶级对象锁(Object Lock)
aws s3api put-bucket-object-lock-configuration \
--bucket backup-bucket \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"GOVCOMPLIANCE","Days":90}}}'
GOVCOMPLIANCE 模式下,即使 root 账号也无法删除 90 天内的对象。勒索软件"删最近备份"的攻击直接失效。
第 5 层:异地复制(Cross-Region Replication)
主桶(eu-central-1 / 国内 1 区)
│ CRR 异步复制
▼
灾备桶(us-east-1 / 国内 2 区 或 Cloudflare R2)
R2 的 CRR 免费且无出口流量费,是 1C2G VPS 上最划算的异地容灾方案。MinIO 集群场景用 mc replicate。
成本:R2 存储 $0.015/GB/月,100GB 备份约 $1.5/月;CRR 无额外费。
第 6 层:密钥分离(最容易被忽略)
~/.config/restic/password ──► 加密后存 1Password
打印一份纸质 存家庭保险箱
另存一份到 异地 VPS 的只读路径
三条铁律:
- 密钥永远不在生成备份的那台 VPS 上明文存在(用
RESTIC_PASSWORD_FILE+ 定期清理,或每次从 1Password CLI 拉取) - 纸质备份放物理位置不同的保险箱
- 异地 VPS 只读挂载,权限最小化
三、2-1-1 备份策略:把"量"也管起来
2-1-1 原则:至少 2 份备份、分布在 1 种不同介质上、保留 1 份异地。
| 副本 | 位置 | 介质 | 保留策略 |
|---|---|---|---|
| #1 | 主 VPS 本地 | 块级 restic 仓库(NFS/本地盘) | 保留 7 天每日 |
| #2 | 主桶(S3/R2 同区) | S3 对象 | 保留 30 天每日 + 12 个月每月 |
| #3 | 灾备桶(异地) | S3 对象(CRR 复制) | 保留 12 个月每月 + 3 年每年 |
Restic 的保留策略一条命令搞定:
restic -r $RESTIC_REPOSITORY forget --keep-daily 7 \
--keep-weekly 4 --keep-monthly 12 --keep-yearly 3 --prune
成本核算(1C2G VPS,100GB 数据):
- 本地块级仓库:用 SSD 盘剩余空间,$0
- 主桶 R2:$1.5/月
- 灾备桶 R2:$1.5/月
- 合计:$3/月,对比 Pico 等托管备份 SaaS $15-30/月,月省 $12-27,且数据主权在你手里。
四、可恢复性演练:备份不是"存了就算"
每月 1 号跑一次自动化恢复演练,脚本化验证"备份真的能救回来":
#!/usr/bin/env bash
# /usr/local/bin/backup-drill.sh
set -euo pipefail
DRILL_DATE=$(date +%Y%m%d)
DRILL_DIR=/tmp/drill-$DRILL_DATE
restic -r $RESTIC_REPOSITORY restore latest --target $DRILL_DIR
# 验证 3 个关键路径
test -d "$DRILL_DIR/etc/nginx" || { echo "FAIL: nginx configs missing"; exit 1; }
test -f "$DRILL_DIR/home/app/.env" || { echo "WARN: .env not backed up"; }
diff -rq $DRILL_DIR/etc /etc 2>/dev/null | head -5
# 校验一个随机文件的哈希
SAMPLE=$(find $DRILL_DIR/home -type f 2>/dev/null | shuf -n1)
[ -n "$SAMPLE" ] && sha256sum "$SAMPLE" | tee /var/log/backup-drill.log
# 清理 30 天前的演练产物
find /tmp -maxdepth 1 -name "drill-*" -mtime +30 -exec rm -rf {} +
echo "DRILL PASSED $DRILL_DATE" >> /var/log/backup-drill.log
加进 cron:
0 3 1 * * /usr/local/bin/backup-drill.sh >> /var/log/backup-drill.log 2>&1
关键:把演练日志接入 Uptime Kuma 或 Prometheus,连续 2 次失败就告警。备份系统的可用性也要被监控。
五、完整部署脚本:一键搭好 6 层 + 2-1-1
#!/usr/bin/env bash
# restic-six-layer-setup.sh — run on the source VPS
set -euo pipefail
# 0. 安装
curl -fsSL https://restic.net/latest/install.sh | sudo bash
# 1. 客户端密钥(分离存放)
mkdir -p ~/.config/restic
openssl rand -hex 16 > ~/.config/restic/password
chmod 600 ~/.config/restic/password
# 把密码导入 1Password CLI 或打印存档
op item create "restic-backup-key" --field text=password=$(cat ~/.config/restic/password)
# 2. 主桶(R2 示例)
R2_BUCKET="vps-backup"
mc alias set r2 R2_ACCESS_KEY_ID R2_SECRET_KEY https://<uid>.r2.cloudflarestorage.com
mc mb -p private r2/$R2_BUCKET
mc ilm rule add --bucket $R2_BUCKET --transition 30 --expire 365 # 可选生命周期
mc api put-bucket-object-lock-configuration --bucket $R2_BUCKET \
--retention-mode GOVCOMPLIANCE --retention-period 90d
# 3. 异地灾备桶 + CRR
mc mb r2/vps-backup-dr
mc replicate enable r2/$R2_BUCKET r2/vps-backup-dr
# 4. 初始化 restic
export RESTIC_REPOSITORY="s3:r2:..." # 用 restic 原生 s3 后端,配 R2 端点
export RESTIC_PASSWORD_FILE=~/.config/restic/password
restic -r $RESTIC_REPOSITORY init
restic -r $RESTIC_REPOSITORY check
# 5. 首次全量备份
restic -r $RESTIC_REPOSITORY backup /etc /home /var/lib/docker \
--exclude-file /etc/restic/excludes.d/*
# 6. 保留策略
cat > /etc/restic/retain.conf <<EOF
restic -r $RESTIC_REPOSITORY forget --keep-daily 7 --keep-weekly 4 \
--keep-monthly 12 --keep-yearly 3 --prune
EOF
chmod +x /etc/restic/retain.conf
# 7. crontab 汇总
cat > /etc/cron.d/restic-backup <<EOF
# 每日 02:30 增量备份
30 2 * * * root /etc/restic/backups-job.sh >> /var/log/restic-backup.log 2>&1
# 每月 1 号 03:00 保留策略
0 3 1 * * root /etc/restic/retain.conf >> /var/log/restic-retention.log 2>&1
# 每月 1 号 03:30 可恢复性演练
30 3 1 * * root /usr/local/bin/backup-drill.sh >> /var/log/backup-drill.log 2>&1
EOF
R2 用 restic 原生 s3 后端时,
RESTIC_REPOSITORY写成:s3:https://<uid>.r2.cloudflarestorage.com/vps-backup RESTIC_S3_PROVIDER=custom RESTIC_S3_ACCESS_KEY / RESTIC_S3_SECRET_KEY / RESTIC_S3_REGION=auto
六、监控与告警接入
| 指标 | 工具 | 告警条件 |
|---|---|---|
| 备份任务 exit code | Prometheus + restic restic-exporter | 连续 2 次非 0 |
| 仓库大小增长 | restic stats | 7 天增长 > 50% |
| 演练通过状态 | 脚本写 /var/log + Uptime Kuma 文本检查 | 演练失败 |
| CRR 复制延迟 | S3 控制台 / mc 查询 | 延迟 > 24h |
| 对象锁状态 | aws s3api get-bucket-object-lock-configuration | 缺失 |
restic-exporter 一行起:
docker run -d --name restic-exporter \
-v ~/.config/restic:/root/.config/restic:ro \
-e RESTIC_REPOSITORY=s3:... \
quay.io/restic/restic-exporter:latest
七、省钱账本(1C2G VPS,100GB 数据)
| 项目 | 自制方案 | 托管 SaaS(Pico/ServerBackup) |
|---|---|---|
| 备份工具 | Restic(开源 $0) | 订阅 |
| 主桶存储 | R2 $1.5/月 | 含在订阅里 |
| 灾备桶 + CRR | R2 $1.5/月 | 通常不送异地 |
| 出口流量 | R2 $0 | 按量 $0.12/GB |
| 密钥管理 | 1Password(家订阅摊薄) | 平台托管 |
| 月合计 | ~$3 | $15-30 |
月省 $12-27,年省 $144-324。 更重要的是:数据主权在你手里,随时可以整体迁移,不会被 SaaS 厂商锁死。
八、常见坑与排错
restic check报unexpected lock:上一次备份进程被 kill,手动restic unlock后重试。- CRR 延迟 24h+:R2 的 CRR 是尽力而为,大对象(>10GB 的 Docker 镜像层)可考虑改用
mc mirror主动同步。 - 对象锁 90 天后忘记改:
mc api put-bucket-object-lock-configuration只能加不能减,到期后用 lifecycle 规则 + 删除策略,不要直接解除。 - 密钥在 1Password 里被同步到被入侵的 Mac:把 1Password 主密钥改成离线 PIN + 密钥文件分离存放,或用 age 加密一份放在异地 VPS。
- 演练永远"通过":检查脚本里的
diff -rq是否真的在跑。加一行if [ "$(find $DRILL_DIR -type f | wc -l)" -lt 100 ]; then exit 1; fi。
结语:备份是保险,演练是验保单
Restic + S3 + 6 层防护 + 2-1-1 策略 + 月度演练,构成一套完整的数据保险系统。核心不是"备份了没",而是"出事后能否在 30 分钟内恢复"。把这套东西自动化跑起来之后,你的 VPS 数据灭失风险在工程上就趋近于零,而每月成本只有 $3。
下一步:把本文的 6 层清单贴到你的运维 Wiki 首页。每季度 review 一次,把演练日志纳入团队 OKR。备份系统的可恢复性,是你给自己签的那份保险单——签字之后,要真的去验保单。
