Lab 185 · S3 · IAM · SNS

Working with Amazon S3: File Sharing with IAM & SNS

Three services wired together to share images with an external user: an S3 bucket scoped to the images/ prefix, an IAM group/policy that enforces least privilege on mediacouser, and an SNS topic that emails the administrator on every ObjectCreated:* and ObjectRemoved:* event.

Summary & architecture

The lab builds a small file-sharing system. An external user uploads or deletes product images inside cafe-23melo23/images/. Every mutation triggers an SNS notification that emails the admin. The interesting parts are which events fire (created / removed only, not get-object) and which operations the external user is denied (anything outside the prefix, anything that changes ACLs or bucket-level metadata).

1 · mediacouser Console or CLI uploads/deletes in the images/ prefix.
2 · S3 bucket Detects ObjectCreated:* / ObjectRemoved:* on prefix.
3 · SNS topic s3NotificationTopic receives the event JSON from S3.
4 · Email Subscribed admin gets the full Records[] payload.

S3 with prefix-scoped access

All GetObject/PutObject/DeleteObject calls are restricted to cafe-*/images/* via the mediaCoPolicy. Nothing else in the bucket is reachable.

SNS as the fan-out for change events

S3 cannot send email directly. The pattern is S3 → SNS → email subscription. The topic's access policy lets S3 publish; the subscription delivers to the admin.

What this proves

The denials are not bugs — they are the deliverable. GetBucketPublicAccessBlock and put-object-acl both fail on mediacouser, which is exactly what least privilege should look like.

Task 1 — CLI Host, bucket creation & initial sync

Configure the AWS CLI on the pre-provisioned CLI Host with the lab's voclabs credentials, create the share bucket, and push the three seed images into the images/ prefix.

Configure the CLI

aws configure was run with the lab's Access Key / Secret Access Key, region us-west-2, output format json. Standard step, no evidence needed — the next commands prove the profile is active.

Create the bucket and sync the seed images

Bucket name chosen: cafe-23melo23 (must start with cafe-, lowercase only, globally unique). Three product images were synced from ~/initial-images/ under the images/ prefix: Cup-of-Hot-Chocolate.jpg, Donuts.jpg, Strawberry-Tarts.jpg.

aws s3 mb s3://cafe-23melo23 --region 'us-west-2'

aws s3 sync ~/initial-images/ s3://cafe-23melo23/images

aws s3 ls s3://cafe-23melo23/images/ --human-readable --summarize
EC2 Instance Connect terminal showing aws configure, the failed mb attempt with angle brackets, the successful mb, the sync of three images, and ls --summarize showing 3 objects 1.1 MiB total
CLI Host terminal: aws configure with lab credentials → first mb attempt fails because the bracketed placeholder was kept literal → retry with the clean bucket name succeeds → sync uploads the three seed images → ls --summarize reports 3 objects, 1.1 MiB total.
Gotcha — angle brackets are bash redirection. The first attempt used aws s3 mb s3://<cafe-23melo23> with the brackets copied verbatim from the lab guide. Bash interpreted < as input redirection and tried to open a file called cafe-23melo23, which does not exist. Result: -bash: cafe-23melo23: No such file or directory. Fix: drop the angle brackets — they are placeholder syntax in the manual, not literal characters. This is the same trap with any <value> placeholder in a shell command.

Task 2 — IAM: the mediaco group & least privilege

The lab pre-creates a group called mediaco with two attached policies: IAMUserChangePassword (AWS-managed, lets the user rotate their own password) and mediaCoPolicy (custom, the interesting one). mediacouser is a member of the group and inherits both.

The three Sid statements in mediaCoPolicy

Sid 1 — AllowGroupToSeeBucketListInTheConsole

Allows s3:ListAllMyBuckets and s3:GetBucketLocation on *. Without this, the S3 console is empty when mediacouser signs in — they can't even see the bucket exists.

Sid 2 — AllowRootLevelListingOfTheBucket

Allows s3:ListBucket on arn:aws:s3:::cafe-*. This is what makes the bucket clickable in the console and lets the user enumerate the first-level prefixes (i.e. see the images/ folder).

Sid 3 — AllowUserSpecificActionsOnlyInTheSpecificPrefix

The core of the policy. Grants s3:GetObject, s3:PutObject, s3:DeleteObject (plus the *Version variants) on arn:aws:s3:::cafe-*/images/*. Everything not under images/ is implicitly denied, and so are operations like s3:PutObjectAcl or s3:GetBucketPublicAccessBlock.

Sign-in evidence as mediacouser

An access key was minted for mediacouser (kept for Task 5's CLI tests) and the console sign-in link was used in an incognito session to keep both identities active in parallel.

AWS Management Console header showing the user mediacouser signed in with Account ID 5876-7460-2776
AWS Console header confirming the second session is signed in as mediacouser, account 5876-7460-2776. The voclabs session stays open in a separate browser for the admin tasks.

Task 3 — Testing permissions: happy path & the expected deny

Three authorized operations from the console as mediacouser, then the unauthorized case that the policy is designed to block.

View — GetObject

Opened cafe-23melo23/images/Donuts.jpg from the S3 console. Worked — the policy grants s3:GetObject inside the prefix.

Upload — PutObject

Uploaded a local image into images/ via the console's Upload wizard. Worked — Sid 3 covers s3:PutObject on cafe-*/images/*.

Delete — DeleteObject

Deleted Cup-of-Hot-Chocolate.jpg using the typed-confirmation dialog. Worked — Sid 3 also covers s3:DeleteObject.

Permissions tab — DENIED

Opening the Permissions tab of the bucket returns Access denied on s3:GetBucketPublicAccessBlock. This is the policy working as intended.

S3 console Permissions tab on bucket cafe-23melo23, showing the Access denied banner with API response: User arn:aws:iam::587674602776:user/mediacouser is not authorized to perform s3:GetBucketPublicAccessBlock on resource arn:aws:s3:::cafe-23melo23 because no identity-based policy allows the action
Permissions tab of cafe-23melo23 while signed in as mediacouser. The error message is the deliverable: least privilege confirmed — the policy does not grant any bucket-level metadata or configuration action, so IAM denies it by default.
Why this denial is success, not failure. An external user (the media company) should be able to add / remove product images. They should not be able to inspect or change bucket-level settings, flip Block Public Access, or read access logs. The s3:GetBucketPublicAccessBlock denial proves the policy's scope is tight enough to refuse anything outside the contract.

Task 4 — SNS event notifications

Four moving parts: create the SNS topic, give S3 permission to publish to it, subscribe an email endpoint, and finally attach the event configuration to the bucket. No console screenshots from this segment — the JSON below is the evidence, and Task 5 closes the loop with the delivered emails.

4.1 — Create the topic

A Standard SNS topic named s3NotificationTopic was created in the same region as the bucket. The topic's ARN was copied for the next two JSON files.

4.2 — Topic access policy (S3PublishPolicy)

The topic needs an access policy that lets the S3 service principal (s3.amazonaws.com) publish messages, but only when those messages originate from this bucket. The ArnLike condition on aws:SourceArn is what enforces that scoping.

{
  "Version": "2008-10-17",
  "Id": "S3PublishPolicy",
  "Statement": [
    {
      "Sid": "AllowPublishFromS3",
      "Effect": "Allow",
      "Principal": {
        "Service": "s3.amazonaws.com"
      },
      "Action": "SNS:Publish",
      "Resource": "arn:aws:sns:us-west-2:587674602776:s3NotificationTopic",
      "Condition": {
        "ArnLike": {
          "aws:SourceArn": "arn:aws:s3:*:*:cafe-23melo23"
        }
      }
    }
  ]
}

4.3 — Email subscription

Created an Email subscription to the topic with a personal address as the endpoint. AWS sent the standard confirmation email — clicking Confirm subscription activated the endpoint. From this point on the topic has one confirmed deliverable subscriber.

4.4 — Wire the bucket to the topic

The event-notification configuration JSON was written into s3EventNotification.json on the CLI Host. It subscribes to two event families and filters on the images/ prefix only — anything outside images/ is silently ignored by the rule.

{
  "TopicConfigurations": [
    {
      "TopicArn": "arn:aws:sns:us-west-2:587674602776:s3NotificationTopic",
      "Events": ["s3:ObjectCreated:*", "s3:ObjectRemoved:*"],
      "Filter": {
        "Key": {
          "FilterRules": [
            { "Name": "prefix", "Value": "images/" }
          ]
        }
      }
    }
  ]
}
aws s3api put-bucket-notification-configuration \
  --bucket cafe-23melo23 \
  --notification-configuration file://s3EventNotification.json

Immediately after attaching the configuration, S3 sent a sanity-check event: a payload with "Event": "s3:TestEvent" arrived at the mailbox. That message confirms the topic-access-policy chain works end-to-end before any real object event fires.

Task 5 — End-to-end test from mediacouser via CLI

Switched the CLI Host's profile to mediacouser's access keys (second aws configure pass) and exercised put, get, delete, and the deny case for put-object-acl. Only put and delete generate SNS emails — get does not, because the filter only matches ObjectCreated:* / ObjectRemoved:*.

Operation Command SNS email? Result
Put aws s3api put-object --bucket cafe-23melo23 --key images/Caramel-Delight.jpg --body ~/new-images/Caramel-Delight.jpg Yes — ObjectCreated:Put OK · returns ETag
Get aws s3api get-object --bucket cafe-23melo23 --key images/Donuts.jpg Donuts.jpg No — reads are not in the filter OK · file downloaded
Delete aws s3api delete-object --bucket cafe-23melo23 --key images/Strawberry-Tarts.jpg Yes — ObjectRemoved:Delete OK · object removed
Put ACL (unauthorized) aws s3api put-object-acl --bucket cafe-23melo23 --key images/Donuts.jpg --acl public-read AccessDenied · expected
Gmail inbox showing two AWS Notifications emails. The first contains an SNS Records payload with eventName ObjectCreated:Put for object key images/Caramel-Delight.jpg and size 239148 bytes. The second contains eventName ObjectRemoved:Delete for object key images/Strawberry-Tarts.jpg
Two SNS deliveries from no-reply@sns.amazonaws.com. Top message: ObjectCreated:Put for images/Caramel-Delight.jpg (239,148 bytes). Bottom message: ObjectRemoved:Delete for images/Strawberry-Tarts.jpg. Each payload carries the full Records[] array with bucket name, owner principalId, source IP, and request IDs.
Why get-object is intentionally silent. The event configuration in §4.4 only subscribes to ObjectCreated:* and ObjectRemoved:*. s3:ObjectAccessed:* exists but was deliberately not registered — flooding the admin with one email per read would be useless noise. The filter is part of the design, not a missing piece.

Errors & expected denials — the proof

Three documented failures during the lab. The first is a real user mistake worth keeping as a lesson. The other two are the policy doing its job.

① Literal angle brackets in aws s3 mb

Symptom
-bash: cafe-23melo23: No such file or directory right after running aws s3 mb s3://<cafe-23melo23> --region 'us-west-2'
Root cause
The lab guide uses <cafe-xxxnnn> as a placeholder. Bash sees < as input redirection and tries to open a file named cafe-23melo23 before aws ever runs.
Fix
Re-run without the angle brackets: aws s3 mb s3://cafe-23melo23 --region 'us-west-2'

Lesson: never paste a manual's <placeholder> syntax into a shell verbatim. The brackets aren't decoration — they're metacharacters. Same trap applies to $, !, *, {, } in unquoted strings.

s3:GetBucketPublicAccessBlock denied as mediacouser

Symptom
S3 console Permissions tab shows Access denied with the API response naming the user, the action, and the resource.
Root cause
The mediaCoPolicy only grants object-level actions inside cafe-*/images/*. Bucket-level metadata calls are not in any allow statement, so IAM's default-deny applies.
Fix
None — this is the desired behavior. The external user is not supposed to inspect or change bucket configuration.

Lesson: a clean denial on a sibling action is the cleanest signal that a policy is correctly scoped. If this had succeeded, the policy would be too permissive and need tightening.

put-object-acl --acl public-read denied as mediacouser

Symptom
An error occurred (AccessDenied) when calling the PutObjectAcl operation: Access Denied
Root cause
s3:PutObjectAcl is not in the mediaCoPolicy allow set. Even though the object lives inside the user's allowed prefix, changing its visibility is a separate action that was deliberately excluded.
Fix
None — expected. An external partner uploading product photos has no business making them publicly readable.

Lesson: S3 access control has multiple orthogonal axes — GetObjectPutObjectAcl, PutObjectPutBucketPolicy. A well-written policy enumerates only the operations the role actually needs, even when they look related.

Concepts & takeaways

CLI reference

Phase Command Purpose
CLI setup aws configure Switch the CLI Host between voclabs and mediacouser credentials.
Bucket create aws s3 mb s3://<bucket> --region 'us-west-2' Create the share bucket. Region required for buckets outside us-east-1.
Bulk upload aws s3 sync ~/initial-images/ s3://<bucket>/images One-way mirror of a local folder into a prefix.
Inventory aws s3 ls s3://<bucket>/images/ --human-readable --summarize List the prefix with size totals.
Wire events aws s3api put-bucket-notification-configuration --bucket <bucket> --notification-configuration file://s3EventNotification.json Attach the SNS event configuration to the bucket.
Put (mediacouser) aws s3api put-object --bucket <bucket> --key images/<file> --body <path> Authorized — triggers ObjectCreated:Put.
Get (mediacouser) aws s3api get-object --bucket <bucket> --key images/<file> <out> Authorized — does not trigger an email.
Delete (mediacouser) aws s3api delete-object --bucket <bucket> --key images/<file> Authorized — triggers ObjectRemoved:Delete.
ACL (mediacouser) aws s3api put-object-acl --bucket <bucket> --key images/<file> --acl public-read Denied — proves the policy excludes ACL mutations.