SA Pro Lab 03 — Immutable, replicated backups with S3 Object Lock
Scaffold: 3/5. The destination bucket is built and already Object-Lock-protected, and the source bucket is versioned and ready. You write the two things that connect them: an IAM replication role, and the replication rule on the source bucket that copies every object into the guarded destination.
The scenario
An attacker who compromises your AWS account can delete your data, and then delete your backups, and then delete the versions of your backups. A backup that lives in the same trust boundary as the thing it protects is not really a backup. What you want is a second copy that the compromised account still cannot destroy: a copy somewhere the delete simply is not permitted, for anyone, until a retention clock runs out.
S3 gives you the two halves. Replication makes the second copy automatically, on every write, without a batch job to forget or a schedule to miss. Object Lock makes that copy immutable: a version under a retention lock cannot be overwritten or deleted until the lock expires, and in the strictest mode not even the root user can shorten it. Put the two together and every object you write lands, within seconds, in a bucket where it is safe from a delete issued by the very account that wrote it.
This lab is same-Region so it fits in one CloudFormation stack you can stand up and tear down in a few minutes. In production the destination bucket lives in another Region, or another account entirely, so a blast radius that takes out one Region or one account leaves the immutable copy untouched. That is the same configuration: the replication rule points at a destination bucket ARN, and it does not care whether that bucket is in this Region, another Region, or an account you control separately. The role’s trust and permissions are identical; only the destination changes.
What’s provided
src/template.yaml— the starter. It builds the destination bucket with versioning and S3 Object Lock (default retentionGOVERNANCE, one day), and the source bucket with versioning. It has a TODO where the replication role and the source’s replication rule belong.solution/template.yaml— the same template, complete.scripts/— deploy, test, and teardown.
Your task
Open src/template.yaml and fill the gap:
- A replication role. An
AWS::IAM::RoleS3 assumes to do the copy. Trust the service principals3.amazonaws.com. Permissions are the standard replication set: on the source,s3:GetReplicationConfigurationands3:ListBucket(bucket ARN) pluss3:GetObjectVersionForReplication,s3:GetObjectVersionAcl, ands3:GetObjectVersionTagging(bucket ARN +/*); on the destination,s3:ReplicateObject,s3:ReplicateDelete, ands3:ReplicateTags(destination ARN +/*). - A replication rule on the source bucket:
Roleset to the role’s ARN, and oneRulewithStatus: Enabled,Priority: 0,DeleteMarkerReplicationdisabled, an empty-prefixFilter, and aDestinationof!GetAtt DestinationBucket.Arn. Leave encryption at the bucket default (SSE-S3): no KMS key, so no extra grant.
One detail decides whether the stack even builds: the source bucket’s replication
config references the role, and the role’s policy needs the source bucket’s ARN. If
you write that ARN with !GetAtt SourceBucket.Arn you create a dependency cycle
(source needs role, role needs source). Build the source ARN from a !Sub string
using the same name the bucket uses, and the cycle disappears.
Run it
# Defaults: stack sa-pro-lab-03, region ap-southeast-2.
./scripts/deploy.sh # deploys src/template.yaml
./scripts/test.sh # writes an object, checks it replicated and is locked
./scripts/teardown.sh # empties both buckets (bypassing GOVERNANCE) and deletes
With the starter template, test.sh writes the object, waits, and reports that
nothing replicated, because the source has no replication rule yet. Add the role and
the rule, redeploy, and run the test again.
What success looks like
./scripts/test.sh prints:
PASS: the object replicated to the guarded bucket, and Object Lock refused to delete the protected version.
It gets there by proving both behaviours: the object copies to the destination within
about a minute, and a delete-object on the replicated version (without a governance
bypass) is refused by Object Lock.
Reveal the solution
Deploy the reference template without editing anything:
SRC=solution ./scripts/deploy.sh && ./scripts/test.sh
GOVERNANCE vs COMPLIANCE
The destination’s default retention is GOVERNANCE mode. In GOVERNANCE, a caller who
holds s3:BypassGovernanceRetention can delete a locked version by asking for the
bypass, which is exactly what teardown.sh does so the lab cleans up in minutes. In
production you want COMPLIANCE: no one, not even the root user, can delete or shorten
a COMPLIANCE-locked version until its retention expires. That is true immutability, and
it is also why a COMPLIANCE bucket cannot be torn down on demand. Swap the mode to
COMPLIANCE and you have the real thing, at the cost of a bucket you must wait out
rather than delete.
What you just learned
- A backup inside the same account and Region as its source shares that source’s blast radius. Immutability and separation are what make it a backup and not just a copy.
- S3 replication makes the second copy on every write, no schedule to miss. The replication role is assumed by S3 itself; its permissions are read-on-source, write-on-destination, and nothing more.
- Object Lock protects a version. GOVERNANCE can be bypassed with a permission and an explicit flag; COMPLIANCE cannot be bypassed by anyone until retention expires.
- Object Lock can only be enabled when a bucket is created, and it requires versioning. You cannot turn it on for an existing bucket later.
- Same-Region and cross-Region (or cross-account) replication are the same configuration. The rule points at a destination bucket ARN; where that bucket lives is a deployment choice, not a code change.
Next
The rest of the SA Pro lab track is listed in labs/README-sa-pro.md.