备份也可能被攻击?香港服务器的备份网络隔离与 WORM 只读对象存储实战:架构设计、MinIO 对象锁、Restic/pgBackRest 落地部署与优化

那天是香港机房凌晨 2:17。NOC 的报警把我从椅子上“弹”起来:多台业务节点磁盘写入陡增、异常加密进程暴涨。我们第一时间切断对外暴露的管理口子,登录上去一看,果然是勒索家族的新变种在“扫目录”。更让我出冷汗的是——攻击者已经尝试扫到了我们的备份挂载点。幸好半年前我把备份通道做了彻底隔离,并把对象存储切到了 WORM(Write Once Read Many)不可变模式:即使拿到密钥也删不掉、改不掉。那一夜,我们靠着“物理/逻辑隔离 + 对象锁”保住了底线。第二天,我把这套方案从头到尾梳理了一遍,写成了下面这篇教程。
场景与目标
- 环境:香港机房,CentOS 7(内核 3.10,systemd),裸金属 + KVM 混合。
目标:
- 生产网与备份网二层/三层彻底隔离,最小可达、仅出不入;
- 备份目标落地到 S3 兼容对象存储(MinIO),启用 对象锁(Object Lock)/WORM;
- 客户端用 Restic(文件/镜像/容器卷)和 pgBackRest(PostgreSQL)直写 S3;
- 全链路 TLS、时钟一致性、最小权限与审计;
- 提供完整可复现部署步骤、优化参数、踩坑记录与演练脚本。
架构一览(文字拓扑)
[生产业务网 VLAN10 10.10.10.0/24]
| (无路由直达备份网)
| ┌─────────────────────────────┐
└──> [备份代理/Client 节点] eth1(备份网) │
静态路由只到 10.160.0.0/22 │
↓ │
[备份网络 VLAN160 10.160.0.0/22] │ ← 二层独立
↓ │
[MinIO 集群 x4 节点] │ (S3:9000 / Console:9001)
│ │
└──(可选)→[离线/异地复制桶] ┘
要点:生产网无法发起到备份网的“横向移动”;只有客户端通过第二张备份网卡以受限出向访问 MinIO 的 S3 端口;MinIO 集群不回通生产网。
硬件与网络清单
MinIO 集群(示例)
| 节点 | 机型/CPU | 内存 | 系统盘 | 数据盘(12 块) | 网卡 |
|---|---|---|---|---|---|
| S3-01..04 | Dell R6525 / EPYC 7313P | 128G | 2×SSD RAID1 | 12× 8TB SATA (7200rpm) | 2×10GbE(其中1口接 VLAN160) |
存储布局:4 节点 × 12 盘,MinIO 分布式纠删码(EC),容量与冗余兼顾。对象锁对底层文件系统无特殊要求(建议 XFS)。
备份网络与地址规划
| 名称 | VLAN | 网段 | 用途 |
|---|---|---|---|
| 生产网 | 10 | 10.10.10.0/24 | 业务 |
| 管理网 | 20 | 10.10.20.0/24 | out-of-band 管理 |
| 备份网 | 160 | 10.160.0.0/22 | 仅备份流量(S3 9000/TCP) |
部署步骤(可复现)
基础:时钟 & TLS
为什么先做它们? 对象锁对时间极其敏感,时钟漂移会导致保留期/删除判定出错。
Chrony(CentOS 7)
yum install -y chrony
cat >/etc/chrony.conf <<'EOF'
server ntp.hk.example.org iburst
allow 10.160.0.0/22
logmeasurements yes
logchange 0.5
makestep 1.0 3
EOF
systemctl enable --now chronyd
chronyc sources -v
内部 CA/TLS(最简单可用:先用企业内 CA 签发给 MinIO,每节点放置 ~/.minio/certs/public.crt / private.key)
1. 备份网络在客户端与 MinIO 上线
客户端节点(示例 eth1 为备份口):
nmcli con add type ethernet ifname eth1 con-name backup-eth1
nmcli con mod backup-eth1 ipv4.addresses 10.160.1.23/22 ipv4.gateway 10.160.0.1 ipv4.method manual
nmcli con up backup-eth1
只允许出向到 MinIO(firewalld/iptables 二选一):
iptables
# 备份网口名按实际替换
IF=eth1
MINIO_SUBNET=10.160.2.0/24
iptables -P OUTPUT DROP
iptables -A OUTPUT -o $IF -p tcp -d $MINIO_SUBNET --dport 9000 -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# 允许 DNS/NTP 指向备份网的特定服务器(如有)
iptables -A OUTPUT -o $IF -p udp --dport 123 -d 10.160.0.10 -j ACCEPT
iptables -A OUTPUT -o $IF -p udp --dport 53 -d 10.160.0.11 -j ACCEPT
# 最后放行必要 ICMP 以便排障
iptables -A OUTPUT -o $IF -p icmp -j ACCEPT
service iptables save
注意:不要把 MinIO 的 9001 控制台口子暴露到生产网。控制台仅在管理网/跳板机访问。
2. 安装并启动 MinIO(分布式)
4 台节点每台准备 12 个数据挂载点 /mnt/disk{1..12},确保 noatime。
安装
useradd -r -s /sbin/nologin minio
curl -L https://dl.min.io/server/minio/release/linux-amd64/minio -o /usr/local/bin/minio
chmod +x /usr/local/bin/minio
mkdir -p /etc/minio /var/minio
chown -R minio:minio /var/minio
systemd 单元(每台)
cat >/etc/systemd/system/minio.service <<'EOF'
[Unit]
Description=MinIO Object Storage
After=network-online.target
Wants=network-online.target
[Service]
User=minio
Group=minio
EnvironmentFile=-/etc/minio/minio.env
ExecStart=/usr/local/bin/minio server \
--address ":9000" \
--console-address ":9001" \
http://s3-{1...4}.bk.local/mnt/disk{1...12}
LimitNOFILE=65536
Restart=always
[Install]
WantedBy=multi-user.target
EOF
cat >/etc/minio/minio.env <<'EOF'
MINIO_ROOT_USER=minio-root
MINIO_ROOT_PASSWORD='强口令_至少24位_含MFA提示'
MINIO_BROWSER=on
EOF
systemctl daemon-reload
systemctl enable --now minio
命名解析:在专用备份 DNS(或 /etc/hosts)里把 s3-1..4.bk.local 指到 10.160.2.11..14。
3. 创建带对象锁(WORM)的桶
关键:对象锁需要在建桶时启用;随后再开版本控制与默认保留期。
安装 mc(Client)
curl -L https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
chmod +x /usr/local/bin/mc
mc alias set bk https://s3-vip.bk.local:9000 minio-root '强口令'
建桶(开启对象锁)
# 每类备份一个桶,便于差异化保留策略
mc mb --with-lock bk/daily
mc mb --with-lock bk/weekly
mc mb --with-lock bk/monthly
# 开启版本控制
mc version enable bk/daily
mc version enable bk/weekly
mc version enable bk/monthly
设置默认保留(示例:日备 Governance 15 天,月备 Compliance 365 天)
# Governance:可由具备 bypass 权限的账号在特殊流程下删除
mc retention set --default governance --period 15d bk/daily
# Weekly/Monthly 更严格
mc retention set --default governance --period 60d bk/weekly
mc retention set --default compliance --period 365d bk/monthly
(可选)生命周期管理:清理旧版本、非当前版本
# 非当前版本 180 天后清理,避免版本爆炸
mc ilm add --id noncurrent-180 --noncurrent-expire-days 180 bk/daily
验证 WORM:向 bk/monthly 上传对象后,立刻尝试删除,预期得到“保留中不可删除”的错误。
4. 最小权限的访问凭证
给备份客户端单独发 AK/SK,只允许写入各自的前缀路径,不允许列出其他主机的数据。
策略(MinIO 兼容 S3 Policy)示例:policy-backup-client.json
{
"Version":"2012-10-17",
"Statement":[
{
"Effect":"Allow",
"Action":["s3:PutObject","s3:AbortMultipartUpload","s3:ListBucketMultipartUploads"],
"Resource":["arn:aws:s3:::daily","arn:aws:s3:::daily/web01/*"]
},
{
"Effect":"Allow",
"Action":["s3:PutObjectRetention","s3:PutObjectLegalHold"],
"Resource":["arn:aws:s3:::daily/web01/*"]
}
]
}
应用策略并创建用户:
mc admin policy add bk backup-client policy-backup-client.json
mc admin user add bk restic-web01 AKIAWEB01 SECRETWEB01
mc admin policy set bk backup-client user=restic-web01
禁止给客户端 s3:DeleteObject;对象锁配合不存在删除权限,双重保障。
5. Restic 客户端(文件/目录/容器卷)
安装
curl -L https://github.com/restic/restic/releases/download/v0.16.4/restic_0.16.4_linux_amd64.bz2 | bunzip2 > /usr/local/bin/restic
chmod +x /usr/local/bin/restic
环境变量与仓库
cat >/etc/restic.env <<'EOF'
export RESTIC_PASSWORD='web01-强口令'
export AWS_ACCESS_KEY_ID='AKIAWEB01'
export AWS_SECRET_ACCESS_KEY='SECRETWEB01'
export RESTIC_REPOSITORY='s3:https://s3-vip.bk.local:9000/daily/web01'
export RESTIC_COMPRESSION=max
export RESTIC_PASSWORD_COMMAND=
EOF
. /etc/restic.env
restic init
首次备份
. /etc/restic.env
restic backup \
/etc \
/var/www \
/var/lib/docker/volumes \
--host web01 --tag daily --one-file-system --verbose
systemd 定时(每天 02:30)
cat >/etc/systemd/system/restic-backup.service <<'EOF'
[Unit]
Description=Restic Backup (S3 WORM)
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/restic.env
ExecStart=/usr/local/bin/restic backup /etc /var/www /var/lib/docker/volumes --host web01 --tag daily --one-file-system
ExecStartPost=/usr/local/bin/restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune
Nice=10
IOSchedulingClass=best-effort
EOF
cat >/etc/systemd/system/restic-backup.timer <<'EOF'
[Unit]
Description=Restic Backup Timer
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable --now restic-backup.timer
说明:即使 forget/prune 发起删除,请求也会被对象锁拒绝,直到保留期结束。这能防御“拿到密钥的恶意删库”。
性能优化要点
- 备份网是 10GbE:并发上传自动按 CPU 与 I/O 调度,必要时 RESTIC_CACHE_DIR=/var/cache/restic 放 NVMe。
- 首次大备份用 --files-from 分批;对巨量小文件可考虑先打包 tar --listed-incremental 再喂 restic(视恢复粒度而定)。
6. PostgreSQL 使用 pgBackRest 直接写 S3(对象锁同享)
安装与基本配置(示例 PG14)
yum -y install pgbackrest
useradd -r -s /bin/false pgbackrest
mkdir -p /var/lib/pgbackrest
chown -R pgbackrest:pgbackrest /var/lib/pgbackrest
/etc/pgbackrest/pgbackrest.conf(关键是 s3+x-minio)
[global]
repo1-type=s3
repo1-path=/monthly/pg-main
repo1-s3-endpoint=s3-vip.bk.local:9000
repo1-s3-region=us-east-1
repo1-s3-bucket=monthly
repo1-s3-verify-tls=y
repo1-s3-uri-style=path
repo1-s3-key=AKIAPG
repo1-s3-key-secret=SECRETPG
[pg-main]
pg1-path=/var/lib/pgsql/14/data
start-fast=y
process-max=8
启用 PG 归档与全备
# postgresql.conf
archive_mode = on
archive_command = 'pgbackrest --stanza=pg-main archive-push %p'
# 创建仓库&全备
pgbackrest --stanza=pg-main --log-level-console=info stanza-create
pgbackrest --stanza=pg-main --type=full backup
建议:PG 的全量/增量按周落 weekly 桶(Governance 60d),月度基线进 monthly 桶(Compliance 365d)。
7. 监控与告警(最有用的几个)
- MinIO:/minio/v2/metrics/cluster 暴露 Prometheus 指标(磁盘可用率、healing、纠删组健康)。
- 对象锁验证作业:每日抽样对随机对象执行删除,期望 失败;成功则报警(说明策略被破坏)。
- 备份作业:Restic/pgBackRest 的退出码 + 备份体量变化(异常骤降/暴涨)→ 报警。
关键优化技巧
- 隔离优先于加密:很多团队做了 TLS/加密,却忽视拓扑隔离。我的实践是“第二网卡 + 独立 VLAN + 单向出向 + 静态路由”,让威胁在生产网“碰不到备份端口”。
- 时钟一致性是对象锁生命线:Chrony 要设 makestep,并把 MinIO 与客户端都指向同一 NTP。我们踩过一次 1.7 秒漂移,导致保留期判断异常。
治理 Governance vs. 合规 Compliance:
- 日/周备用 Governance,需要紧急空间回收时,走带审计的 bypass 流程(MFA + 审批)。
- 月/年基线用 Compliance,谁都删不掉,连集群管理员也不行。
- 分桶分前缀:按主机名或服务名做前缀 daily/web01/*,避免“列表可见性”干扰和权限错配。
- 大对象优先:对海量小文件,尽量先归档为 tar 分片(例如 1~4GB),提升 S3 PUT 吞吐并减少 listing 的开销。
- 演练写进时间表:不是“能备份”而是“能在 SLA 时间内恢复”。把 RTO/RPO 写入 runbook,并定期按它演练。
我遇到的坑 & 现场解法
坑 1:对象锁没在建桶时启用
复现:先 mc mb bk/daily,后面再想开对象锁→行不通。
解法:删除重建(或建新桶迁移),务必用 --with-lock 一次到位。
坑 2:时钟漂移导致删除“蒙混过关”
一次升级后,NTP 源切换,MinIO 與客户端各飘了 1~2 秒。极短时间窗口内,某些删除操作被错误接受。
解法:统一 NTP、加 makestep;同时增加外部作业每日验证“删除应失败”。
坑 3:误把控制台 9001 暴露到生产网
某次变更把 9001 暴露给了 jump 之外的网段。扫描器第二天就来打招呼。
解法:严格 ACL,仅管理网可达;在 MinIO 前放专用管理反代,启用 IP 白名单与 MFA。
坑 4:Restic 忘记设缓存目录
大量小文件下 CPU sys% 飙升。
解法:RESTIC_CACHE_DIR 指到 NVMe,显著降低 PUT 建连与索引开销。
验证与演练
1) 不可变性验证
# 上传、加保留,再试删除
mc cp /etc/hosts bk/monthly/test/hosts
mc retention set governance --period 30d bk/monthly/test/hosts
mc rm bk/monthly/test/hosts # 预期:失败(409/AccessDenied)
2) 恢复演练(Restic)
. /etc/restic.env
# 列版本/快照
restic snapshots --host web01 --tag daily
# 还原到隔离目录
restic restore <SNAP_ID> --target /restore/web01/
3) 恢复演练(pgBackRest)
pgbackrest --stanza=pg-main restore --type=time "--target=2025-09-20 12:30:00" --delta
基线指标(示例)
| 指标 | 目标 |
|---|---|
| 日备完成时间 | ≤ 2 小时 |
| 失败率(7 日滑窗) | 0 |
| 恢复演练频率 | 每月一次(含抽样数据一致性校验) |
| 删除验证 | 每日随机抽样 10 个对象,应全部失败 |
最终检查清单(上线前)
- 生产网完全不可达 MinIO 的 9000/9001
- 客户端只允许出向到 10.160.0.0/22:9000
- MinIO 桶 with-lock + version 开启
- Governance/Compliance 默认保留期已经设置且生效
- AK/SK 最小权限,无 DeleteObject
- Chrony 同步、时钟差 < 200ms
- 监控与审计落地,删除应失败的作业已跑通
- 恢复演练记录与 RTO/RPO 达标
把“备份”当成最后一道生产防线
凌晨那次,我们虽然“中招”,但因为备份网走的是独立物理通道、MinIO 桶是不可变的,攻击者够不到也删不掉。我从机房回酒店的路上想明白了一件事:备份不是“有一份就行”,而是要把它当成生产的一部分去设计、去演练、去守护。
如果你正在香港机房做这套方案,不妨就按上面的清单,一项一项落地。等哪天真的遇上“夜半惊魂”,你会庆幸今天做过的每一个小决定。
附:示例吞吐记录(取一台 Web 节点首备)
| 数据集 | 大小 | 文件数 | 用时 | 平均写入 |
|---|---|---|---|---|
/var/www |
480 GB | 2,130,000 | 1h52m | ~4.3 Gbps |
/etc+配置 |
1.9 GB | 41,200 | 2m40s | ~100 Mbps |
注:10GbE 备份网,Restic 默认并发;启用 NVMe 缓存后小文件段用时下降约 23%。
参考命令速查(Cheat Sheet)
# MinIO
mc alias set bk https://s3-vip.bk.local:9000 admin pass
mc mb --with-lock bk/daily && mc version enable bk/daily
mc retention set --default governance --period 15d bk/daily
mc ilm add --noncurrent-expire-days 180 bk/daily
# Restic
export RESTIC_REPOSITORY=s3:https://s3-vip.bk.local:9000/daily/web01
export RESTIC_PASSWORD=...
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
restic init && restic backup /etc /var/www --tag daily
restic snapshots && restic restore <ID> --target /restore/
# pgBackRest
pgbackrest --stanza=pg-main --type=full backup
pgbackrest --stanza=pg-main restore --delta
——以上即为我在香港机房实际落地“备份网络隔离 + WORM 不可变对象存储”的完整实操与优化记录。