DEV Community

Cover image for AWS IAM + S3 Demystified: Map Every S3 CLI Command to Its Required Permission
BRUNO SOUZA
BRUNO SOUZA

Posted on

AWS IAM + S3 Demystified: Map Every S3 CLI Command to Its Required Permission

If you have ever stared at an AccessDenied error and played permission roulette, adding one action at a time until something works, this post is for you.

We are going to take a short, real S3 bash script, map every single CLI command to the exact IAM permission it requires, and write the smallest possible policy that grants only those actions.


What IAM is (in 60 seconds)

AWS IAM (Identity and Access Management) answers three questions for every API call:

Question IAM concept
Who is calling? Identity (user, role, or group)
What are they allowed to do? Policy (JSON allow/deny rules)
On which resource? Resource ARN

The evaluation logic is simple but strict:

  1. Default deny — everything is blocked unless explicitly allowed
  2. Explicit Allow — grants access
  3. Explicit Deny — always wins, even over an Allow

That third rule is why least-privilege matters: the more permissions you hand out, the harder it is to audit which Deny you actually need.


The script we are dissecting

# create a bucket
aws s3api create-bucket --bucket hello-bucket --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1

aws s3api put-bucket-acl --bucket hello-bucket --acl public-read

# upload a file to the bucket
aws s3 cp hello.txt s3://hello-bucket/hello.txt

# list the contents of the bucket
aws s3 ls s3://hello-bucket/

# delete the file from the bucket
aws s3 rm s3://hello-bucket/hello.txt

# download the file from the bucket
aws s3 cp s3://hello-bucket/hello.txt ./hello.txt

# delete the bucket
aws s3api delete-bucket --bucket hello-bucket
Enter fullscreen mode Exit fullscreen mode

Seven commands. Seven IAM permission checks. Let us go through each one.


Command-to-permission map

# Command IAM action required
1 aws s3api create-bucket s3:CreateBucket
2 aws s3api put-bucket-acl s3:PutBucketAcl
3 aws s3 cp (upload) s3:PutObject
4 aws s3 ls s3:ListBucket
5 aws s3 rm s3:DeleteObject
6 aws s3 cp (download) s3:GetObject
7 aws s3api delete-bucket s3:DeleteBucket

One thing most beginners miss: S3 permissions split across two resource levels.

  • Bucket-level actions (CreateBucket, ListBucket, PutBucketAcl, DeleteBucket) apply to arn:aws:s3:::bucket-name
  • Object-level actions (PutObject, GetObject, DeleteObject) apply to arn:aws:s3:::bucket-name/*

Getting this wrong is one of the most common sources of AccessDenied errors, even when the right action is listed.


The minimal IAM policy

Replace hello-bucket with your actual bucket name.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BucketLevelAccess",
      "Effect": "Allow",
      "Action": [
        "s3:CreateBucket",
        "s3:ListBucket",
        "s3:PutBucketAcl",
        "s3:DeleteBucket"
      ],
      "Resource": "arn:aws:s3:::hello-bucket"
    },
    {
      "Sid": "ObjectLevelAccess",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::hello-bucket/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

No wildcards. No s3:*. Just the seven actions the script actually calls.


Line-by-line walkthrough

1. Create a bucket

aws s3api create-bucket --bucket hello-bucket --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1
Enter fullscreen mode Exit fullscreen mode
  • IAM check: s3:CreateBucket on arn:aws:s3:::hello-bucket
  • The LocationConstraint is required for every region except us-east-1, which is the global default

2. Set a public ACL

aws s3api put-bucket-acl --bucket hello-bucket --acl public-read
Enter fullscreen mode Exit fullscreen mode
  • IAM check: s3:PutBucketAcl
  • ⚠️ 2026 note: AWS now enforces Object Ownership with ACLs disabled by default on new buckets. This command will throw AccessControlListNotSupported unless you explicitly set Object Ownership to BucketOwnerPreferred or ObjectWriter. For demos, skip the ACL step entirely and use bucket policies if you need public access.
  • Public ACLs are also blocked by Block Public Access settings at the account level — double-check yours.

3. Upload an object

aws s3 cp hello.txt s3://hello-bucket/hello.txt
Enter fullscreen mode Exit fullscreen mode
  • IAM check: s3:PutObject on arn:aws:s3:::hello-bucket/*
  • Make sure the local file exists before running the script

4. List objects

aws s3 ls s3://hello-bucket/
Enter fullscreen mode Exit fullscreen mode
  • IAM check: s3:ListBucket on arn:aws:s3:::hello-bucket
  • Note the resource is the bucket, not the objects — a common policy mistake

5. Delete an object

aws s3 rm s3://hello-bucket/hello.txt
Enter fullscreen mode Exit fullscreen mode
  • IAM check: s3:DeleteObject on arn:aws:s3:::hello-bucket/*

6. Download an object

aws s3 cp s3://hello-bucket/hello.txt ./hello.txt
Enter fullscreen mode Exit fullscreen mode
  • IAM check: s3:GetObject on arn:aws:s3:::hello-bucket/*

7. Delete the bucket

aws s3api delete-bucket --bucket hello-bucket
Enter fullscreen mode Exit fullscreen mode
  • IAM check: s3:DeleteBucket on arn:aws:s3:::hello-bucket
  • The bucket must be empty before this succeeds. S3 will return a BucketNotEmpty error otherwise.

A production-ready version of the script

The original script has a few rough edges worth smoothing out before using it in a real environment. The corrected version below uses a timestamp suffix to avoid S3's global naming conflicts, ensures the file is downloaded before it is deleted, and removes the ACL step that fails by default on modern AWS accounts.

#!/usr/bin/env bash
set -euo pipefail

BUCKET="hello-bucket-$(date +%s)"
REGION="eu-west-1"
LOCAL_FILE="test.txt"
OBJECT_KEY="hello.txt"

# 1. Create bucket
aws s3api create-bucket \
  --bucket "$BUCKET" \
  --region "$REGION" \
  --create-bucket-configuration "LocationConstraint=$REGION"

# 2. Upload file
aws s3 cp "$LOCAL_FILE" "s3://$BUCKET/$OBJECT_KEY"

# 3. List objects
aws s3 ls "s3://$BUCKET/"

# 4. Download file (before deleting it)
aws s3 cp "s3://$BUCKET/$OBJECT_KEY" "./downloaded-$OBJECT_KEY"

# 5. Remove the object, then the bucket
aws s3 rm "s3://$BUCKET/$OBJECT_KEY"
aws s3api delete-bucket --bucket "$BUCKET"
Enter fullscreen mode Exit fullscreen mode

The mental model I use before every AWS CLI command

Before writing or running any AWS CLI call, I ask three questions:

Which exact API action is this command calling?

Is the permission needed at the bucket level, object level, or both?

What is the narrowest resource ARN I can use?

That habit is what keeps IAM policies auditable. The moment you write s3:* or use "Resource": "*", you lose the ability to reason about what the policy actually permits.


Key takeaways

  • Every AWS CLI command maps to at least one IAM action — knowing the mapping is the core skill
  • S3 permissions split across two resource ARN patterns: bucket (:::name) and objects (:::name/*)
  • Modern AWS accounts disable ACLs by default — do not rely on put-bucket-acl in new scripts
  • Least privilege is not just a best practice; it is the only way to write policies you can actually audit

Top comments (0)