How to create an IAM user with read-only access to one S3 bucket

TL;DR

Create an IAM user with no console access, attach an inline policy that allows s3:ListBucket and s3:GetBucketLocation on arn:aws:s3:::my-bucket and s3:GetObject on arn:aws:s3:::my-bucket/*, then create an access key. That is the entire least-privilege policy: no s3:ListAllMyBuckets, no wildcard resources, nothing that can write or delete. Skip the AmazonS3ReadOnlyAccess managed policy; it reads every bucket in the account. When you paste the keys into S3 Viewer it notices the user cannot list buckets, asks you to name the one it can read, checks it with HeadBucket, and connects.

Steps

Step-by-step.

  1. 01

    Decide what read-only has to cover

    Listing and downloading is the baseline: s3:ListBucket, s3:GetBucketLocation and s3:GetObject. Add s3:GetObjectVersion and s3:ListBucketVersions if the bucket is versioned and old versions matter, and kms:Decrypt on the key if objects use SSE-KMS. Leave out s3:ListAllMyBuckets: it can only be granted on * and reveals every bucket name in the account.
  2. 02

    Write the policy

    Two statements, two ARNs. Bucket-level actions go on arn:aws:s3:::my-bucket; object-level actions go on arn:aws:s3:::my-bucket/*. Swapping them is the classic way to end up with Access Denied. Replace my-bucket in both places.
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "ListTheBucket",
          "Effect": "Allow",
          "Action": ["s3:ListBucket", "s3:GetBucketLocation"],
          "Resource": "arn:aws:s3:::my-bucket"
        },
        {
          "Sid": "ReadObjects",
          "Effect": "Allow",
          "Action": ["s3:GetObject", "s3:GetObjectVersion"],
          "Resource": "arn:aws:s3:::my-bucket/*"
        }
      ]
    }
  3. 03

    Create the user and attach the policy (console)

    IAM → UsersCreate user. Name it for its job, for example s3-viewer-readonly, and leave “Provide user access to the AWS Management Console” unchecked. On the permissions step choose Attach policies directly Create policy, switch to the JSON tab, paste the policy above, name it S3ReadOnly-my-bucket, then return to the user wizard, refresh the list, tick the new policy and finish.
  4. 04

    Or do the same from the CLI

    Save the JSON as s3-readonly.json. An inline policy (put-user-policy) is deleted with the user, which is what you want for a single-purpose identity.
    aws iam create-user --user-name s3-viewer-readonly
    aws iam put-user-policy \
      --user-name s3-viewer-readonly \
      --policy-name S3ReadOnly-my-bucket \
      --policy-document file://s3-readonly.json
  5. 05

    Create an access key

    Open the user → Security credentials Create access key. Pick “Application running outside AWS” or “Third-party service”, then copy both values. The secret is shown exactly once. A user can hold two keys at a time, which is what makes rotation possible later.
    aws iam create-access-key --user-name s3-viewer-readonly
    # "AccessKeyId": "AKIA...", "SecretAccessKey": "..."  (shown once)
  6. 06

    Test the boundary

    Prove that it can read the bucket and nothing else. The second and third commands should fail with AccessDenied.
    aws configure --profile readonly          # paste the new keys
    
    aws s3 ls s3://my-bucket/ --profile readonly              # works
    aws s3 ls --profile readonly                              # AccessDenied (expected)
    aws s3 cp ./x.txt s3://my-bucket/x.txt --profile readonly # AccessDenied (expected)
  7. 07

    Connect it in S3 Viewer

    Connect server → Endpoint https://s3.us-east-1.amazonaws.com (use the bucket's region), pick the region, paste the access key ID and secret. Because the user cannot list buckets, S3 Viewer shows a Bucket names field: type my-bucket, press Check and it verifies the name with HeadBucket, then Connect. The keys are encrypted in your browser with the server's public key before they are sent. Teammates you invite afterwards get a Viewer or Editor role on top of this key and never see the AWS credentials.
  8. 08

    Harden and maintain it

    Narrow the grant to a prefix with an s3:prefix condition if the user only needs one folder. Add an aws:SourceIp condition when the caller has a fixed address. Rotate keys by creating the second key, swapping it in, then deactivating and deleting the first. For humans, prefer roles through IAM Identity Center; reserve IAM users with static keys for tools like this that need them, one user per tool.
Under the hood

What's actually happening.

S3 permissions come in two shapes. Bucket-level actions (ListBucket, GetBucketLocation, ListBucketVersions) are evaluated against the bucket ARN, arn:aws:s3:::my-bucket. Object-level actions (GetObject, PutObject, DeleteObject) are evaluated against object ARNs, which is why they need the /* suffix. A single statement cannot cover both, so the smallest correct policy always has two. ListAllMyBuckets is a third shape, account-wide with no resource to scope, which is why least-privilege policies leave it out.


S3 Viewer is built for keys like this. When you connect, it first tries ListBuckets; on AccessDenied it switches to asking for bucket names and calls HeadBucket on each one, so a bucket-scoped or prefix-scoped key works without any wildcard grants. The key is encrypted in the browser with the server's public key before it leaves the page, and every request S3 Viewer signs on your behalf is still bounded by this IAM policy: a Viewer in the app cannot do more than the key can.


The practical pattern for teams is one read-only IAM user per connected bucket, then S3 Viewer invites for people. IAM stays small, the AWS key never leaves the server, and access changes are made in the app instead of in the IAM console.

FAQ

Common questions.

What is the minimum IAM policy for read-only access to one S3 bucket?

Two Allow statements: s3:ListBucket and s3:GetBucketLocation on arn:aws:s3:::my-bucket, and s3:GetObject (plus s3:GetObjectVersion for versioned buckets) on arn:aws:s3:::my-bucket/*. Nothing else. That lets the user list and download objects in that bucket and do nothing anywhere else in the account.

Do I need s3:ListAllMyBuckets for a read-only user?

No. s3:ListAllMyBuckets only powers the ListBuckets call (aws s3 ls with no bucket) and can only be granted on Resource "*", so it exposes every bucket name in the account. Tools that need it fail without it; S3 Viewer instead asks you to type the bucket names and verifies each with HeadBucket.

Should I just attach the AmazonS3ReadOnlyAccess managed policy?

Not for one bucket. AmazonS3ReadOnlyAccess grants s3:Get* and s3:List* on every bucket in the account, including buckets created later. Write the two-statement inline policy instead so the key can only reach the bucket you intend.

Why does aws s3 ls give Access Denied with my read-only user?

Because with no bucket argument it calls ListBuckets, which needs s3:ListAllMyBuckets, a permission this policy deliberately omits. Run aws s3 ls s3://my-bucket/ instead. Access Denied there would mean the ListBucket statement is on the wrong ARN.

How do I limit read-only access to one folder in the bucket?

Keep s3:ListBucket on the bucket ARN but add a Condition of StringLike s3:prefix: ["reports/*", "reports/"], and change the object statement's Resource to arn:aws:s3:::my-bucket/reports/*. The user can then list and read only under reports/. Our guide on granular S3 permissions walks through it.

Should this be an IAM user or an IAM role?

A role, whenever the caller can assume one: AWS compute, federated humans through IAM Identity Center, or cross-account access. An IAM user with a long-lived access key is the right tool only for external software that needs static credentials, which is exactly the case for connecting a bucket to a self-hosted browser. Keep it single-purpose and rotate the key.

How do I create read-only credentials for a Cloudflare R2 bucket?

In the Cloudflare dashboard open R2 → Manage R2 API Tokens → Create API token, choose the Object Read only permission and scope it to the specific bucket. Cloudflare shows an Access Key ID and Secret Access Key for the S3 API. Connect with endpoint https://<account-id>.r2.cloudflarestorage.com and region auto; because the token is bucket-scoped it cannot list buckets, so S3 Viewer will ask for the bucket name.

Can I give a teammate read-only access without creating an IAM user for them?

Yes. Connect the bucket once with a read-only key, then invite the teammate to that server in S3 Viewer with the Viewer role. They sign in with their email, see only the buckets you grant, and never receive AWS credentials; revoking them is one click and does not touch IAM.

Can a read-only user open encrypted objects?

Objects encrypted with SSE-S3 (the default) need nothing extra. Objects encrypted with SSE-KMS need kms:Decrypt on the key in the user's policy and a key policy that allows the user; otherwise every download returns Access Denied even though listing works.
Use S3 Viewer for this

Skip the CLI. Try it in the browser.

S3 Viewer turns the steps above into a single click. Open source, self-hostable, free.