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

Your task

Open src/template.yaml and fill the gap:

  1. A replication role. An AWS::IAM::Role S3 assumes to do the copy. Trust the service principal s3.amazonaws.com. Permissions are the standard replication set: on the source, s3:GetReplicationConfiguration and s3:ListBucket (bucket ARN) plus s3:GetObjectVersionForReplication, s3:GetObjectVersionAcl, and s3:GetObjectVersionTagging (bucket ARN + /*); on the destination, s3:ReplicateObject, s3:ReplicateDelete, and s3:ReplicateTags (destination ARN + /*).
  2. A replication rule on the source bucket: Role set to the role’s ARN, and one Rule with Status: Enabled, Priority: 0, DeleteMarkerReplication disabled, an empty-prefix Filter, and a Destination of !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

Next

The rest of the SA Pro lab track is listed in labs/README-sa-pro.md.