# AWS Fundamentals 03 — IAM Roles and a Private S3 Workflow

**CloudComputingTutor · Beginner · 25-minute reading + 45–60-minute lab**  
**Prerequisites:** `aws-fundamentals-02`; basic file paths and shell quoting.  
**Outcome:** Give a temporary EC2 role permission to write/read/delete objects under one S3 prefix, prove allowed and denied behavior, and remove the complete lab.  
**Publication state:** New teaching material; these steps were checked against AWS documentation, not run against a live account. Sources checked **2026-09-18**.

## 1. Shutterbird needs a permission boundary

Shutterbird's temporary machine can now be managed, but it cannot automatically access the company's photos. Today the fictional team gives it a smaller task: place a training note under `uploads/` in one private bucket, retrieve that note, and leave other prefixes alone.

The note represents an object-processing workflow without personal data or a production application. Success has two sides. The intended operation works. An operation outside the intended scope fails. Testing only the successful upload would miss half of the requirement.

You will create a dedicated bucket through the console, attach an original narrow policy to the lab role, use the AWS CLI on EC2, and inspect the results. No access key needs to be copied into a file. This is the first step toward an application identity whose job is written down precisely.

## 2. Role, trust, permission, and session

An IAM role describes permissions that an authorized principal can assume. The role's **trust policy** answers who may assume it. Its **permissions policies** answer what the resulting identity may do. An instance profile supplies the role to EC2. The AWS CLI can obtain temporary role credentials through EC2's metadata service automatically. You inspect the caller identity, not the underlying credentials. [EC2 role workflow](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html), [CLI instance credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-metadata.html)

Our existing role trusts the EC2 service and has the SSM permissions needed for management. We will add an S3 policy for one lab bucket. The human who edits this role has a different identity from the machine that performs the object requests. Keep browser provisioning and terminal workload actions separate in your notes.

This lesson assumes the bucket and role belong to the same account and there are no additional bucket policies, permission boundaries, endpoint policies, session restrictions, or organization controls denying the requests. Those controls can further restrict access. The absence of an allow normally means denial; an applicable explicit deny wins. We do not remove organizational safeguards to force a tutorial to pass. [S3 policy model](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-policy-language-overview.html)

## 3. Buckets and keys: the addressing model

A bucket is an S3 container in a selected Region. An object has a key, such as `uploads/observation.txt`, plus its data and metadata. The slash makes the key convenient to browse, but S3 prefixes are not Linux directories. A console “folder” presentation should not lead you to assume Unix ownership or `chmod` controls S3 access. [S3 prefixes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-prefixes.html)

Two ARN shapes matter. The bucket itself is `arn:aws:s3:::BUCKET_NAME`. An object namespace beneath it is `arn:aws:s3:::BUCKET_NAME/uploads/*`. Listing operates on the bucket, with a prefix condition. Reading/writing/deleting an object uses the object ARN. Mixing those shapes is a common reason a plausible policy does not work.

Privacy and encryption also answer different questions. Encryption at rest protects stored data through an encryption mechanism. IAM and resource settings determine which requests may access it. An encrypted object does not automatically have the right access policy. We use SSE-S3 encryption and a private bucket so we can study the permission question without introducing KMS key permissions. [Default encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/default-bucket-encryption.html)

## 4. Preflight and cost boundaries

Use the learning account, Region, budget, and network route from AWS Fundamentals 02. If that lesson's instance was terminated, follow its preflight and Steps 1–4 to recreate the dedicated instance, role/profile, and security group; stop before cleanup. If continuing immediately in the same supervised session, record the reused IDs. Never leave the machine running overnight just to preserve continuity.

Your console identity needs permission to create/configure/delete this lab bucket, view its contents, and add/remove an inline policy on the dedicated instance role. The EC2 profile must initially have only the lesson's SSM policy, with no broad S3 policy. Keep the existing no-inbound/outbound-HTTPS network design and functioning STS/S3 regional endpoint connectivity. If an instructor provisions resources, they must preserve these assumptions for the denied tests to mean anything.

Budget for EC2, EBS, public IPv4, S3 storage, S3 requests, and applicable transfer. Use one tiny text object and complete cleanup in this session. Current prices and account credits vary; budget notifications can be delayed. The role is intentionally allowed to delete its training objects for cleanup. A production uploader may need a different permission set.

## 5. Step 1 — Create a private training bucket

In the S3 console choose **Create bucket**, **General purpose**, and your recorded Region. Use a unique lowercase name such as `cct-shutterbird-lab-` followed by a random suffix; bucket names must meet AWS naming requirements and be available. Record the actual name without adding private personal information to it.

Keep **Object Ownership: Bucket owner enforced**, which disables ACLs. Keep all four **Block Public Access** settings enabled. Leave versioning disabled and Object Lock off for this disposable exercise. Use the default **SSE-S3** encryption setting. Leave static website hosting disabled and do not add a bucket policy. Add a project tag if your permissions allow it. Create the bucket and inspect the saved settings. [S3 getting started](https://docs.aws.amazon.com/AmazonS3/latest/userguide/GetStartedWithS3.html#creating-bucket), [ownership](https://docs.aws.amazon.com/AmazonS3/latest/userguide/about-object-ownership.html), [Block Public Access](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html)

Expected evidence: one empty general-purpose bucket, correct Region, no public access, ACLs disabled, SSE-S3, and no versioning. If an organization mandates versioning or KMS, use the tabletop route or an instructor-adjusted lab with its corresponding permissions and cleanup steps. Do not override those controls.

## 6. Step 2 — Identify the caller and observe the baseline

Open the EC2 browser Session Manager shell. The standard Amazon Linux 2023 image includes AWS CLI v2; confirm with `aws --version`. If unavailable, stop and check the selected AMI with your instructor rather than copying an unreviewed installer. [AL2023 CLI](https://docs.aws.amazon.com/linux/al2023/ug/awscli2.html)

Replace the two example values below with your recorded Region and actual bucket name, keeping quotes:

```bash
LAB_REGION='us-east-1'
LAB_BUCKET='replace-with-your-actual-bucket-name'
aws sts get-caller-identity --region "$LAB_REGION" --no-cli-pager
aws s3api list-objects-v2 \
  --bucket "$LAB_BUCKET" \
  --prefix 'uploads/' \
  --region "$LAB_REGION" \
  --no-cli-pager
```

Expected identity: an `assumed-role` ARN containing your dedicated EC2 role name. The listing should return **AccessDenied** before adding the S3 policy. If it succeeds, investigate unexpected existing permission; do not claim a valid baseline. Record the error without exposing sensitive identifiers publicly. `get-caller-identity` tells you who sent the request; it does not enumerate every permission that identity possesses. [Caller identity reference](https://docs.aws.amazon.com/cli/latest/reference/sts/get-caller-identity.html)

## 7. Step 3 — Add a policy for one prefix

In the browser IAM console, open the **dedicated EC2 role** from your inventory. Choose Add permissions → Create inline policy → JSON. Replace **both** instances of `REPLACE_WITH_LAB_BUCKET` below with the exact bucket name. Review the JSON and save it as `CCTUploadsOnly`. Leave the SSM managed policy attached.

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListUploadsPrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::REPLACE_WITH_LAB_BUCKET",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["uploads/*"]
        }
      }
    },
    {
      "Sid": "ManageTrainingObjects",
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::REPLACE_WITH_LAB_BUCKET/uploads/*"
    }
  ]
}
```

Read the policy aloud as two sentences. First: this role may list this bucket when the requested prefix starts with `uploads/`. Second: it may put, get, and delete objects whose keys start with `uploads/`. There is no permission to list all account buckets, modify the bucket's privacy settings, or create another bucket. Wait briefly for IAM changes to propagate, then retry the previous listing. An empty successful response is expected. A broader console browsing permission is unnecessary for these specific API calls. [S3 identity-policy examples](https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-policies-s3.html)

## 8. Step 4 — Write, list, retrieve, compare

Use a new local workspace and one harmless sentence. Run each command separately:

```bash
LAB_DIR=$(mktemp -d /tmp/cct-s3-workflow.XXXXXX)
printf '%s\n' 'Shutterbird training object: role-based access works.' > "$LAB_DIR/observation.txt"
aws s3api put-object \
  --bucket "$LAB_BUCKET" \
  --key 'uploads/observation.txt' \
  --body "$LAB_DIR/observation.txt" \
  --region "$LAB_REGION" \
  --no-cli-pager
aws s3api list-objects-v2 \
  --bucket "$LAB_BUCKET" \
  --prefix 'uploads/' \
  --region "$LAB_REGION" \
  --no-cli-pager
aws s3api get-object \
  --bucket "$LAB_BUCKET" \
  --key 'uploads/observation.txt' \
  --region "$LAB_REGION" \
  --no-cli-pager "$LAB_DIR/download.txt"
cmp "$LAB_DIR/observation.txt" "$LAB_DIR/download.txt"
cat "$LAB_DIR/download.txt"
```

Expected: upload returns response metadata; listing includes the exact key; retrieval writes the local download; `cmp` succeeds silently with exit status zero; `cat` shows your sentence. Record the commands and their observed outcomes. A returned ETag is useful metadata, but do not assume every S3 ETag is an MD5 digest in all encryption/upload configurations. Here the byte comparison is the test you actually performed. [PutObject](https://docs.aws.amazon.com/cli/latest/reference/s3api/put-object.html), [GetObject](https://docs.aws.amazon.com/cli/latest/reference/s3api/get-object.html)

## 9. Step 5 — Prove the boundary

Keep the same role, bucket, and session. Request a listing outside the permitted prefix:

```bash
aws s3api list-objects-v2 \
  --bucket "$LAB_BUCKET" \
  --prefix 'private/' \
  --region "$LAB_REGION" \
  --no-cli-pager
```

Expected: **AccessDenied**. It should not merely return an empty list. This test is read-only and creates no outside-prefix data. It checks the listing boundary; the policy review separately shows the object-action boundary. An additional optional negative test is `get-object` for a known instructor-created object outside `uploads/`, but do not create extra objects or infer missing-object behavior from ambiguous status codes in this introductory lab.

If an allowed request fails, inspect exact bucket spelling, ARN shape, prefix case, Region, active role, policy propagation, and higher-level restrictions. If the forbidden listing succeeds, inspect unexpected permissions before continuing. Never disable Block Public Access to fix a role-based request. Public access is not required for an authorized role to read a private object. [ListObjectsV2 reference](https://docs.aws.amazon.com/cli/latest/reference/s3api/list-objects-v2.html)

## 10. Step 6 — Delete exactly what you created

Save your redacted evidence. Delete the exact training object and list the same prefix again:

```bash
aws s3api delete-object \
  --bucket "$LAB_BUCKET" \
  --key 'uploads/observation.txt' \
  --region "$LAB_REGION" \
  --no-cli-pager
aws s3api list-objects-v2 \
  --bucket "$LAB_BUCKET" \
  --prefix 'uploads/' \
  --region "$LAB_REGION" \
  --no-cli-pager
```

Expected: the key is absent. Through the console, confirm the dedicated bucket is empty and delete that exact bucket. The instance role deliberately lacks bucket deletion permission, so use your authorized provisioning identity. This procedure assumes versioning was never enabled; versioned buckets require deleting versions and delete markers too. [Object deletion](https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-object.html), [bucket deletion](https://docs.aws.amazon.com/AmazonS3/latest/userguide/delete-bucket.html)

Remove `CCTUploadsOnly` from the lab role. End the session and complete AWS Fundamentals 02's cleanup: terminate the recorded instance, verify EBS removal/address release, and remove only the dedicated security group and unused role/profile. Keep shared networking and the budget. Record IDs and completion time, then revisit cost data after it updates.

## 11. Offline route and assessment

Without AWS, use the policy as a tabletop case. Make a request table with columns for identity, action, resource, prefix, predicted result, and reason. Evaluate: list `uploads/`; list `private/`; get `uploads/observation.txt`; put `archive/observation.txt`; delete the bucket. Predicted results are allow, deny, allow, deny, deny under the stated assumptions. Explain why listing and object actions use different ARN shapes.

On any local terminal with `printf`, `mktemp`, and `cmp`, you may create and compare two local text files to practice the observation method. That is a local file exercise, not proof of S3 behavior. Label every unexecuted API result “predicted.”

Submit the identity diagram, original role-policy JSON with your private identifiers redacted for sharing, baseline denial, allowed object round trip, forbidden-prefix denial, and teardown record. Tabletop learners submit predicted equivalents. Next topics build on this foundation: VPC routing, CloudWatch observations, and repeatable infrastructure. The central skill is already present: state a narrow job, implement its permission boundary, test both sides, and remove the experiment.
