备份仓库安全、不可变性与删除治理
集群里的加密器拿到备份控制器的云凭证后,没有尝试破解任何快照。它列出对象仓库,删除最新备份和旧版本,再停用复制任务。事故响应团队重建了 Kubernetes,却发现每个 RestorePoint 都指向已经不存在的对象。备份每天成功、校验任务也一直绿色,真正的共同故障域却是“生产写入、仓库删除和密钥管理都握在同一个身份手里”。
另一个团队走向相反极端:所有对象进入最长 WORM 保留,连测试备份、重复上传和包含个人数据的错误副本也不能删除。容量持续增长,合规删除请求无法按约定完成,最终只能等待锁到期。不可变性阻止攻击者删数据,也同样阻止管理员纠错;安全设计必须同时回答谁能写、谁能读、谁能删、谁能解密,以及锁住之后怎样合法退出。
仓库不是一个桶,而是六条相互制约的链
对象存储桶只是载体。一个可恢复仓库至少同时存在写入、读取、枚举、删除、加解密和复制六类动作。把它们授予同一个备份控制器,部署最省事,却让一次 Pod 越权、CI Token 泄露或供应链入侵拥有完整销毁能力。
备份控制器 --Put/AbortMultipart--> 主仓库 --版本/WORM--> 历史对象
恢复执行器 --Get/List-----------> 主仓库 --KMS Decrypt--> 明文恢复流
生命周期身份 --DeleteVersion----> 到期版本
复制身份 ----GetVersion/Replicate--> 隔离账号仓库 --独立 KMS--> 灾难副本
审计身份 ----只读配置与日志------> 告警、对账与恢复演练生产集群中的 writer 通常需要创建对象、完成分段上传和读取少量自身元数据,不应拥有删除历史版本、缩短保留期、修改 bucket policy、关闭版本控制或管理 KMS key 的权限。restore reader 平时可以不存在,演练或事故时通过审批取得短期读取和解密能力。删除由生命周期服务或独立管理员执行,KMS key policy 再建立一条与对象 ACL/IAM 不同的授权边界。
版本控制保存同一 key 的多个 version,能抵抗普通覆盖和 delete marker,却不能抵抗拥有 DeleteObjectVersion 的攻击者。WORM/Object Lock 给具体对象版本加 retain-until 或 legal hold,锁期内拒绝相应删除。跨账号复制把副本放到生产凭证不能管理的账号和密钥域。三者不是替代关系:版本负责历史,WORM 约束删除,跨账号副本拆开故障域。
先建立资产与威胁清单
开始配置前,先为每个仓库记录这些事实:bucket/container 标识、账号和区域、备份工具、对象前缀、版本状态、默认保留模式、复制目标、KMS key、写身份、读身份、删身份、审计日志位置、预计日增量、最长恢复窗口和数据分类。不要把 bucket 名和一张“已启用锁定”的截图当成完整资产记录。
威胁至少覆盖五条路径:
生产集群被接管,攻击者沿备份 ServiceAccount 或静态云密钥进入仓库。对象存储账号被接管,攻击者修改 policy、生命周期、复制和保留设置。KMS 管理员禁用或计划删除 key,使完整对象变成不可恢复密文。
备份软件自身错误地覆盖、prune 或批量删除合法恢复点。合规删除与 WORM 冲突,数据到期后仍因历史版本、复制副本或 legal hold 长期存在。
恢复点清单还要和仓库实物对账。备份平台里的 Completed 只说明它认为上传完成;真正的证据包括 object version ID、大小、checksum、加密 key ID、retention、复制状态,以及使用独立身份下载并恢复后的业务校验。
选择一个可控实验仓库
下面以 Amazon S3 API 展示具体命令,因为它能把 Versioning、Object Lock、KMS 和跨账号复制放在同一条可观察链上。其他对象存储的动作名称和不可逆规则可能不同,应换成对应厂商正式文档与 CLI。S3 的 Versioning、Object Lock、复制和 SSE-KMS 权限应在配置旁就地核对。
先按 AWS CLI 官方安装说明安装受支持的 CLI,再确认当前调用身份和区域。实验必须使用专用账号或专用 bucket,不能拿现有生产仓库测试删除和锁定。
aws --version
aws sts get-caller-identity
aws configure get region
export AWS_REGION='<approved-region>'
export LAB_BUCKET='<globally-unique-lab-bucket>'
export LAB_KMS_KEY_ARN='<lab-kms-key-arn>'不要把 access key 写进 Kubernetes Secret 清单、shell history 或 CI 日志。Kubernetes 中的备份控制器优先使用云厂商工作负载身份,把专用 ServiceAccount 绑定到 writer role;本地 CLI 使用短期 SSO/角色会话。若产品只支持静态密钥,应把它视为迁移信号,并至少放入外部密钥系统、设置短有效期、审计读取和自动轮换。
创建实验 bucket 时显式启用 Object Lock 能力,并确认 versioning:
aws s3api create-bucket \
--bucket "$LAB_BUCKET" \
--region "$AWS_REGION" \
--object-lock-enabled-for-bucket \
<region-specific-create-bucket-arguments>
aws s3api get-bucket-versioning --bucket "$LAB_BUCKET"
aws s3api get-object-lock-configuration --bucket "$LAB_BUCKET"不同区域的 create-bucket 参数存在差异,使用 CLI 帮助和 S3 区域文档生成实际命令。预期 versioning 为 Enabled,Object Lock 配置可读取。若组织策略、账号权限或区域能力拒绝创建,应停止实验,不要退回一个没有锁能力的 bucket 却继续声称验证了 WORM。
实验默认采用短保留的 governance mode。它允许拥有专门 bypass 权限的受控管理员处理测试资产;compliance mode 的删除约束更强,不适合在尚未验证容量、合规和退出流程时直接启用。任何默认保留都应先在一次性 bucket 演练,因为缩短策略不一定能释放已经带独立保留期的对象版本。
cat > object-lock.json <<'EOF'
{
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "GOVERNANCE",
"Days": 1
}
}
}
EOF
aws s3api put-object-lock-configuration \
--bucket "$LAB_BUCKET" \
--object-lock-configuration file://object-lock.jsonDays: 1只是一次性实验值,生产保留期由法律要求、恢复窗口、RPO、攻击发现时间和容量预算共同决定。
把写、读、删和 KMS 拆成四种身份
Writer 只能追加恢复点
writer policy 可以允许目标前缀的 PutObject、分段上传及必要的 bucket 位置查询,显式拒绝删除版本、改变锁和修改 bucket policy。具体备份产品可能需要列举、读取索引或删除临时 multipart,采用前应从官方权限表和 CloudTrail 实际调用收敛,而不是直接授予 s3:*。
下面 JSON 使用 AWS IAM policy language 版本标识:2012-10-17;它不是策略创建或更新时间。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteBackupPrefix",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::<backup-bucket>",
"arn:aws:s3:::<backup-bucket>/clusters/<cluster-id>/*"
]
},
{
"Sid": "DenyDestructiveAdministration",
"Effect": "Deny",
"Action": [
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:PutBucketVersioning",
"s3:PutObjectLockConfiguration",
"s3:PutBucketPolicy"
],
"Resource": [
"arn:aws:s3:::<backup-bucket>",
"arn:aws:s3:::<backup-bucket>/*"
]
}
]
}显式 Deny 可以限制同一身份从其他附加策略获得的危险动作,但组织 SCP、permission boundary、bucket policy 和 key policy 仍要一起审查。若备份工具必须 prune,给它另一个受生命周期审批控制的 role,不要向常驻 writer 增加删除权限。
SSE-KMS 写入通常需要对目标 key 使用 kms:GenerateDataKey,restore reader 需要 kms:Decrypt。分段上传、复制和特定 SDK 路径可能需要附加动作,应从 KMS/S3 官方权限说明与 CloudTrail 失败事件确定。writer 不应拥有 kms:DisableKey、kms:ScheduleKeyDeletion 或修改 key policy 的能力。
Restore reader 按需读取与解密
reader 只在恢复演练或事故窗口激活,允许列出受控前缀、读取指定版本和使用目标 KMS key 解密。它不能写入主仓库、删除对象、修改锁或管理 key。恢复到隔离账号时,reader 与目标集群 writer 也应是两个身份,防止恢复程序反向污染源仓库。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketVersioning"],
"Resource": "arn:aws:s3:::<backup-bucket>"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:GetObjectVersion", "s3:GetObjectRetention"],
"Resource": "arn:aws:s3:::<backup-bucket>/clusters/<cluster-id>/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "<backup-kms-key-arn>"
}
]
}Deleter 只处理已经到期的版本
删除身份由生命周期服务或受审批的运维角色承担。它需要识别 version ID、retention、legal hold、复制状态和备份目录引用,不能只按 object key 删除。对 governance mode 的 bypass 权限尤其敏感:拥有它的主体可能在锁期内删除版本,应采用短期会话、双人审批、条件约束和独立告警;常驻备份 Pod 不应获得该权限。
compliance mode 与 legal hold 用于更强的保留约束,但它们会改变纠错能力。legal hold 没有普通到期日,解除动作本身要有案件/审批关联。生产启用前必须用真实的删除、恢复、复制和 key 轮换流程演练,而不是只测试 put-object-lock-configuration 返回成功。
KMS 管理员不能顺手读取备份
KMS key administrator 负责轮换、策略和生命周期,不应因为管理 key 就自动获得 S3 GetObject;restore reader 能解密,也不应获得 key 管理动作。key policy、IAM policy、grant 和组织策略共同决定最终权限,单看 IAM role 页面容易漏掉跨账号 grant。
主仓库和隔离副本使用不同账号、不同 key 和不同管理员。若两边都用同一个可禁用 key,加密会把两个存储故障域重新绑在一起。轮换 key 前,确认旧对象继续可解密、复制能使用新 key、恢复工具保存的是 key ARN/metadata 而非错误别名假设。
正向实验:写得进、读得到、删不掉
先切换到 writer role,上传一个带唯一内容的对象并记录返回的 version ID、ETag、checksum 与 KMS key。不要上传真实集群 Secret;使用随机测试文件。
printf 'restore-proof-%s\n' "$(openssl rand -hex 16)" > restore-proof.txt
sha256sum restore-proof.txt
aws s3api put-object \
--bucket "$LAB_BUCKET" \
--key clusters/lab/restore-proof.txt \
--body restore-proof.txt \
--server-side-encryption aws:kms \
--ssekms-key-id "$LAB_KMS_KEY_ARN" \
--checksum-algorithm SHA256
aws s3api list-object-versions \
--bucket "$LAB_BUCKET" \
--prefix clusters/lab/restore-proof.txt预期得到非空 VersionId,对象 retention 继承 bucket 默认规则。随后仍以 writer 尝试删除具体版本和修改锁配置;两项都应得到 AccessDenied,CloudTrail 应记录被拒绝的 principal、action、bucket 与 version ID。
export VERSION_ID='<returned-version-id>'
aws s3api delete-object \
--bucket "$LAB_BUCKET" \
--key clusters/lab/restore-proof.txt \
--version-id "$VERSION_ID"
aws s3api put-object-lock-configuration \
--bucket "$LAB_BUCKET" \
--object-lock-configuration file://object-lock.json再切换到短期 restore reader,下载指定版本并比较 checksum:
aws s3api get-object \
--bucket "$LAB_BUCKET" \
--key clusters/lab/restore-proof.txt \
--version-id "$VERSION_ID" \
restored-proof.txt
sha256sum restored-proof.txt两份摘要应一致。reader 删除对象、上传覆盖对象或禁用 KMS key都应失败。这个实验同时证明 writer 不能销毁、reader 能恢复、reader 不能写回;只做其中一项不能证明分权闭环。
反向实验:一个宽权限身份怎样摧毁版本链
在另一个不承载正式恢复点、没有 Object Lock 的一次性 bucket 中,创建同名对象的多个版本,再用拥有 DeleteObjectVersion 的测试 role 枚举并逐版本删除。预期普通 delete marker 仍可通过指定旧 version ID 读取,而删除具体版本后无法恢复。
aws s3api put-bucket-versioning \
--bucket '<unlocked-disposable-bucket>' \
--versioning-configuration Status=Enabled
printf 'v1\n' > sample.txt
aws s3api put-object --bucket '<unlocked-disposable-bucket>' --key sample.txt --body sample.txt
printf 'v2\n' > sample.txt
aws s3api put-object --bucket '<unlocked-disposable-bucket>' --key sample.txt --body sample.txt
aws s3api delete-object --bucket '<unlocked-disposable-bucket>' --key sample.txt
aws s3api list-object-versions --bucket '<unlocked-disposable-bucket>' --prefix sample.txt接下来以测试 deleter 删除列出的具体版本。结果应直接揭示:版本控制能恢复覆盖和普通删除,却挡不住具有版本删除权的攻击者。反向实验完成后删除该一次性 bucket;不要在正式仓库验证勒索路径。
另一条稳定反例是停用测试 KMS key后尝试读取密文。对象仍在、版本和锁也都存在,但 restore reader 会因无法解密而失败。这说明“对象不可删”不等于“恢复能力不可破坏”。实验 key 恢复可用后,再次下载并校验 checksum,随后撤销测试 role。
跨账号副本要能在源账号失守后独立恢复
复制目标应位于生产集群凭证不能登录和管理的账号,启用版本控制,使用目标账号自己的 KMS key、bucket policy 和审计日志。复制 role 只允许从指定源前缀读取版本并向指定目标复制;它不能修改目标锁、删除目标版本或管理目标 key。
建立复制后,不要只看规则状态。上传带 Object Lock metadata 和 KMS 加密的测试对象,保存源/目标 version ID、checksum、retention、replication status 和 key ID。然后从目标账号的 restore reader 下载副本,在不使用源账号会话和源 KMS key的条件下完成校验。
源对象的 delete marker 是否复制、对象锁 metadata 如何复制、现有对象是否需要批量复制、复制失败如何重试,都受具体服务配置影响。按 S3 replication requirements逐项配置并对真实对象验证,不能根据“Replication rule enabled”推断历史恢复点已经隔离。
勒索演练可以安全地撤销源 writer、暂停源 bucket 的正常访问,再从目标账号恢复一份测试备份。禁止为了证明隔离而在正式源仓库批量删除。需要保留的证据是:源账号无权限改变目标 policy/lock/key,复制延迟落在 RPO 内,目标版本可读,目标恢复不依赖源账号仍存活。
勒索防护要关注攻击者的时间线
攻击者不一定立即删除备份。它可能先窃取长期凭证、等待保留窗口自然滚动,再修改生命周期或停止复制。告警因此不能只看大量 DeleteObject,还要覆盖 bucket policy、versioning、Object Lock、lifecycle、replication、KMS policy/grant、key disable/delete schedule、异常 List/Get 和跨账号 trust 变化。
每轮仓库对账至少比较:备份平台恢复点数、对象版本数、锁定版本数、复制完成数、复制失败/延迟、未加密对象、未知 KMS key、孤儿 multipart、到期未删版本和没有平台引用的孤儿对象。趋势中的突然下降、复制长期停滞或 KMS key 集中变化都应阻断“备份健康”结论。
break-glass 访问要能在日常身份系统受损时使用,但不能成为常驻万能管理员。它应有硬件或离线保护、双人启用、短会话、只读优先和独立审计;演练结束立即撤销 session、验证 policy 未漂移,并对使用期间的每个读取和管理动作复核。
合规删除与 WORM 必须在写入前协调
WORM 保留期内的对象版本可能依法或按监管要求不能修改,同时隐私或合同规则又可能要求删除特定主体数据。等数据写入长锁仓库后才处理冲突,技术上可能已经没有立即删除路径。数据分类、法定保留、legal hold、地域、复制和删除时限必须在对象进入仓库前确定。
可执行的删除流程从数据定位开始:备份目录要能把业务主体或数据集映射到受影响的恢复点、version ID、复制副本、导出副本和 KMS key。随后判断每个版本是否仍在法定保留或 legal hold,能立即删除的交给 deleter,不能删除的进入受审计的到期队列,并限制所有非必要恢复使用。锁到期后删除源与副本的具体版本,再验证 list versions、inventory、平台目录和恢复尝试都不再返回该数据。
“销毁 KMS key”等于删除数据的说法要谨慎。共享 key 会同时破坏大量合法恢复点,key 删除通常有等待与恢复语义,bucket metadata 和密文对象仍可能存在,而且监管是否接受 cryptographic erasure 取决于制度、算法、密钥副本与审计证据。若需要按租户或数据域加密删除,应在设计阶段控制 key 粒度,并避免让一个 key 的销毁超出目标数据集合。
缩短 bucket 默认保留期也不会自动改写已经带 retain-until 的对象版本。legal hold 解除、governance bypass、compliance mode 到期和生命周期删除是不同动作,审批与证据应分别保存。任何自动化批量删除前先在 inventory 上冻结候选 version ID,并用 dry-run 报告核对平台引用和复制状态。
容量和成本不能只看当前对象大小
版本、WORM 和跨账号复制会把逻辑备份大小放大为多个不可立即回收的物理版本。容量模型至少包含每日全量/增量、变更率、分段上传残留、保留层级、WORM 最短锁期、复制份数、跨区域传输、inventory/审计日志、恢复演练副本和 KMS 请求。备份软件的 prune 成功不代表被锁对象已经释放容量。
用对象 inventory 计算各 retention class 的版本字节、到期时间分布和复制状态,再将恢复窗口、攻击发现窗口与法规保留映射到策略。告警应关注“到期未删字节持续增长”“无平台引用但仍锁定的字节”“复制积压年龄”和“恢复演练临时卷未清理”,而不是只看 bucket 总大小。
云存储、请求、检索、复制、出口、KMS 和审计服务价格会随区域与产品层级变化。预算评审从厂商当前定价页按实际调用和容量测算,并把提前删除费、归档取回时间、最低存储期等限制纳入恢复演练;不要在架构模板里写死单价。
升级、迁移和退出从恢复证据倒推
备份工具升级时,比较新旧版本使用的对象布局、metadata、checksum、加密方式、multipart 行为、prune 语义和所需权限。先让 writer 在新前缀写入,旧 reader 与新 reader 分别恢复;确认跨账号复制和 WORM metadata 都正确后,再收敛旧写入。直接改 bucket policy 可能让旧 controller 卡在重试中,也可能迫使团队临时恢复宽权限。
KMS 轮换采用“新对象写新 key、旧 key 保持可解密、后台按策略重加密或自然到期”的可观察迁移。轮换窗口同时测试主仓库和副本仓库,记录每个恢复点实际 key ID。只有目录中不再有受保护版本依赖旧 key,且合规与保留责任允许,才能进入 key 退役。
迁移到另一种对象存储时,不能只复制当前 object key。必须保留版本、checksum、创建顺序、保留/hold、加密映射、备份目录和工具所需 metadata,再从目标仓库执行隔离恢复。源仓库继续只读保留到回滚窗口结束,期间阻止两套生命周期同时删除同一恢复点。
退出仓库时先停止新备份和复制,冻结待保留/待删除 inventory,确认目标仓库已恢复验证,再按 legal hold 与 retention 分批删除到期版本。随后撤销 Kubernetes ServiceAccount 的工作负载身份、writer/reader/deleter/replication role、bucket policy trust、KMS grant 和告警订阅。最后删除空 bucket、无依赖 key、复制配置、inventory 清单与临时恢复资源,并核对账单和云资产清单不再出现遗留资源。
实验 bucket 的清理必须等待 governance retention 到期,或由受控测试管理员使用明确的 bypass 流程删除指定 version;不能用 writer 凭证清理,因为“writer 删不掉”正是实验需要保留的结论。先列出所有 version 和 delete marker,再逐项清理,最后删除 bucket、测试 role、KMS grant、本地文件和 shell 环境变量。
aws s3api list-object-versions --bucket "$LAB_BUCKET"
rm -f restore-proof.txt restored-proof.txt object-lock.json sample.txt
unset LAB_BUCKET LAB_KMS_KEY_ARN VERSION_ID AWS_REGION一个仓库只有在生产 writer 被接管后仍不能销毁历史版本、源账号失守后隔离副本仍能独立解密恢复、锁期结束后又能按合规合同删除指定版本时,才同时具备勒索韧性和长期可治理性。
