Amazon S3

Read-only access to one S3 bucket

Let a role list one bucket and download its objects, including SSE-KMS objects, and nothing else in S3.

Published · Updated .

The problem

A reporting job, a support engineer or a partner integration needs to read the contents of one bucket. The quick fixes are the AWS managed policy AmazonS3ReadOnlyAccess or a hand-written s3:Get* on "*". Both work, and both quietly grant read access to every bucket in the account, including the ones that hold Terraform state, CloudTrail logs and database exports.

Reading a bucket takes two S3 permissions on two different resource types, plus a KMS permission when objects use a customer managed key. Failing read-only policies usually get one of those three wrong. This example covers each and proves the result without writing to the bucket.

The policies

Identity policy: read one bucket

IAM identity-based policy. Attach to: The IAM role (or user or group) that reads the bucket, as an inline or customer managed policy

Identity policy: read one bucket · IAM identity-based policyOpen in Inline Policy Generator
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListTheBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket"
    },
    {
      "Sid": "ReadObjects",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket/*"
    },
    {
      "Sid": "DecryptThroughS3Only",
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "s3.us-east-1.amazonaws.com"
        }
      }
    }
  ]
}

Statement by statement

  • s3:ListBucket authorizes ListObjectsV2 and ListObjects, which act on the bucket, so the Resource is the bare bucket ARN; amzn-s3-demo-bucket/* here would match nothing. It also changes error codes: without it, a GetObject for a missing key returns 403 instead of 404, so missing-file bugs look like permission bugs.
  • s3:GetObject acts on objects, so the Resource is bucket/*. It also covers HeadObject. It reads the current version only: an older version by versionId needs s3:GetObjectVersion, and object tags need s3:GetObjectTagging.
  • SSE-KMS objects under a customer managed key download only if the caller may use kms:Decrypt on that key. kms:ViaService limits the grant to decrypt calls S3 makes on the caller's behalf in us-east-1, so the role can't use the key directly. Drop this statement for SSE-S3 (the default for new objects since January 2023).

Optional add-on: browse the bucket in the S3 console

IAM identity-based policy. Attach to: The same principal, only if people will use the S3 console rather than the CLI or an SDK

Optional add-on: browse the bucket in the S3 console · IAM identity-based policyOpen in Inline Policy Generator
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ConsoleBucketPicker",
      "Effect": "Allow",
      "Action": [
        "s3:ListAllMyBuckets",
        "s3:GetBucketLocation"
      ],
      "Resource": "*"
    }
  ]
}

Statement by statement

  • The console's bucket list calls ListBuckets, which has no resource to scope (hence "*"), and it calls GetBucketLocation to route requests to the bucket's Region. This exposes the names of every bucket in the account, not their contents; the CLI and SDKs don't need it.

Before you start

  • The bucket and the reading principal are in the same account (111122223333). For a reader in another account, the bucket policy must also grant the access; see the cross-account guide.
  • You know how the objects are encrypted (step 1). If a customer managed KMS key is used, its key policy must let IAM policies in the account grant access, which the default key policy does.
  • Nothing else denies the access: no explicit Deny in the bucket policy, an SCP, an RCP or a permissions boundary.

Deploy it step by step

  1. Find out which key, if any, decrypts the objects. Default encryption tells you what new uploads get; older objects or uploads with explicit headers can use another key, so check a sample object too. AES256 means no KMS statement is needed. aws:kms means copy the key ARN into the third statement, unless it is the AWS managed key aws/s3: its key policy already lets same-account principals decrypt through S3, and it can't be used across accounts.
    Commands (replace <placeholders>)
    aws s3api get-bucket-encryption --bucket amzn-s3-demo-bucket
    aws s3api head-object --bucket amzn-s3-demo-bucket --key <existing-key> --query '[ServerSideEncryption,SSEKMSKeyId]'
  2. Check that the key policy delegates to IAM. An identity policy can grant kms:Decrypt only if the key policy lets the account delegate. Look for the default statement that gives arn:aws:iam::111122223333:root kms:*. If it isn't there, the key administrator must add the role to the key policy instead.
    Commands (replace <placeholders>)
    aws kms get-key-policy --key-id <key-id> --policy-name default --output text
  3. Validate and attach the policy. Save the first policy as policy.json, replace the key ARN and Region, and run validate-policy, which catches problems such as an action that doesn't match its resource type. Then attach it inline. Add the console statement only for console users.
    Commands (replace <placeholders>)
    aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://policy.json
    aws iam put-role-policy --role-name <role-name> --policy-name ReadOneBucket --policy-document file://policy.json

Verify it without changing anything

These checks only read state, simulate, or use dry runs, so they are safe to run in any account. Exact error wording varies by service and over time.

Simulate the bucket and object permissions

The IAM policy simulator evaluates the document without touching S3. Ask about the bucket ARN and an object ARN separately. Including write actions proves they are denied without risking a write.

Commands (replace <placeholders>)
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names s3:ListBucket --resource-arns arn:aws:s3:::amzn-s3-demo-bucket --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names s3:GetObject s3:PutObject s3:DeleteObject --resource-arns arn:aws:s3:::amzn-s3-demo-bucket/reports/2026-10.csv --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text

Expected: s3:ListBucket and s3:GetObject return allowed; s3:PutObject and s3:DeleteObject return implicitDeny. If GetObject comes back implicitDeny, the object statement's Resource is probably missing the /* suffix.

Confirm the KMS condition matters

Simulate kms:Decrypt twice: once with the kms:ViaService value S3 sends, once without it. The difference shows that the role can decrypt only through S3.

Commands (replace <placeholders>)
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names kms:Decrypt --resource-arns arn:aws:kms:us-east-1:111122223333:key/<key-id> --context-entries ContextKeyName=kms:ViaService,ContextKeyValues=s3.us-east-1.amazonaws.com,ContextKeyType=string --query 'EvaluationResults[].EvalDecision' --output text
aws iam simulate-custom-policy --policy-input-list file://policy.json --action-names kms:Decrypt --resource-arns arn:aws:kms:us-east-1:111122223333:key/<key-id> --query 'EvaluationResults[].[EvalDecision,MissingContextValues]' --output json

Expected: allowed for the first call. The second should not be allowed; the simulator typically reports implicitDeny and may list kms:ViaService as a missing context value. Neither call evaluates the key policy, so a real download (next check) is still needed.

Read for real, then check what else the role can do

Run these with the role's own credentials; get-caller-identity confirms which identity the CLI is using. Copying an object to stdout performs a full GET, the call that has to decrypt; a HEAD request doesn't prove the KMS part. simulate-principal-policy evaluates every identity policy attached to the role, so it catches a broad managed policy attached earlier.

Commands (replace <placeholders>)
aws sts get-caller-identity --profile <profile>
aws s3api list-objects-v2 --bucket amzn-s3-demo-bucket --max-keys 5 --profile <profile>
aws s3 cp s3://amzn-s3-demo-bucket/<existing-key> - --profile <profile> | head -c 300
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::111122223333:role/<role-name> --action-names s3:PutObject --resource-arns arn:aws:s3:::amzn-s3-demo-bucket/<existing-key> --query 'EvaluationResults[].EvalDecision' --output text

Expected: The caller ARN shows the assumed role, the listing returns up to five keys, and the object's first bytes print. A KMS problem shows up only on the download, as an AccessDenied error; for same-account callers S3 usually names the denied action. The simulation should print implicitDeny; allowed means another attached policy grants writes.

Common mistakes and troubleshooting

Object actions on the bucket ARN, or the reverse

s3:GetObject on arn:aws:s3:::amzn-s3-demo-bucket and s3:ListBucket on amzn-s3-demo-bucket/* both pass JSON validation and both grant nothing. IAM Access Analyzer's validate-policy flags this mismatch, which is one reason to run it before attaching.

Testing from the wrong identity

If the CLI picks up an administrator's default profile, every read succeeds and proves nothing. Pin the profile on each command.

The S3 console shows errors even though the CLI works

The console needs s3:ListAllMyBuckets to draw the bucket list. Even with the add-on, tabs such as Properties and Permissions may show insufficient-permission notices; that is expected and doesn't block downloads.

KMS denials that look like S3 denials

Listing works and plain objects download, but some objects fail with AccessDenied. Those are usually objects encrypted under a key the role can't use, often a key from another account or a different key than the bucket default. head-object shows which key each object uses.

Limitations and production considerations

  • Read access to a bucket is read access to everything in it, including objects added later. If the bucket mixes data of different sensitivity, scope the object ARN to a prefix (see the prefix example) or split the data into separate buckets.
  • s3:ListBucket without a condition reveals every key name, and key names often contain customer IDs.
  • The simulator evaluates only the policies you give it, or for simulate-principal-policy the role's attached policies. It does not read the bucket policy or the KMS key policy, so a real read with the role's credentials remains the final test.

Official AWS documentation

Account IDs, bucket and organization names are AWS documentation placeholders. Test in a non-production account before use (see the methodology).