IAM Policy to list an S3 bucket, except for the top-level (root) of the bucket
Tags: aws, security
Published: 2020-02-12 23:37:34

I saw a question on StackOverflow ([Allow S3 bucket top-level listing to specific users only](https://stackoverflow.com/q/60193252/174777)) that posed an interesting challenge:

- Allow a user to list the contents of an Amazon S3 bucket
- But... Don't let them list the top-level of the bucket

I'm always up for a challenge!

**IAM Policy Variables**

The question reminded me a bit of [IAM Policy Elements: Variables and Tags](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html), which allows the construction of an IAM Policy that inserts a username in appropriate spots:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": ["s3:ListBucket"],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::mybucket"],
      "Condition": {"StringLike": {"s3:prefix": ["${aws:username}/*"]}}
    },
    {
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::mybucket/${aws:username}/*"]
    }
  ]
}
```

This policy says:

- Let the user list/get/put objects
- But only in a folder that matches my IAM User username

That's close, but sort of opposite of "list everything except the top-level".

**Playing with the ARN**

Next, I tried playing with the ARN to see whether I could grant permission for _any subfolder_ within a bucket:

`arn:aws:s3:::BUCKET-NAME/*/*`

Granting permission for that ARN is saying _"Only for a path within the bucket that contains a slash"_. Thus, it should work for sub-folders but not the top-level.

Unfortunately, the `s3:ListBucket` permission **applies to the bucket itself** rather than _the contents of the bucket_. When granting `s3:ListBucket`, you must provide the ARN of the bucket without using `/*`.

**Playing with the Prefix**

This led me to the idea using the `Prefix` to specify which folders can be used. The excellent [Grant IAM User Access to a Folder in an Amazon S3 Bucket](https://aws.amazon.com/premiumsupport/knowledge-center/s3-folder-user-access/) article from AWS Support shows some examples of how to use prefixes, such as:

```json
{
     "Action": ["s3:ListBucket"],
     "Effect": "Allow",
     "Resource": ["arn:aws:s3:::BUCKET-NAME"],
     "Condition":{"StringEquals":{"s3:prefix":["","media"]}}
}
```

This permits listing of the bucket, but only the top-level folder, or the `media` folder.

This is getting closer!

I then tried applying negative logic with `StringNotEquals`, stating that listing was permitted if the prefix was not empty:

```json
{
     "Action": ["s3:ListBucket"],
     "Effect": "Allow",
     "Resource": ["arn:aws:s3:::BUCKET-NAME"],
     "Condition":{"StringNotEquals":{"s3:prefix":""}}
}
```

This correctly denied my listing the root directory, but permitted listing a sub-folder. However, it also allowed me to do this:

`aws s3 ls s3://BUCKET-NAME/a`

This command would list all objects that started with `a`. This was permitted because the prefix was not empty. Therefore, the solution is invalidated because people could try every letter/number as a prefix to view the contents of the top-level of the bucket.

**The Solution**

More playing led me to **this solution:**

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::BUCKET-NAME",
            "Condition": {"StringLike": {"s3:prefix": ["*/*"]}}
        }
    ]
}
```

This says that listing the bucket is **permitted if the `Prefix` contains a slash**. This prevents listing of the top-level of the bucket and allows any sub-folder to be listed.

It also requires that at least one slash exists in the path, so this will work:

`aws s3 ls s3://BUCKET-NAME/folder/`

However, this will _not_ work since it does not contain a slash:

`aws s3 ls s3://BUCKET-NAME/folder`

You'll also find my solution on StackOverflow: [Allow S3 bucket top-level listing to specific users only
](https://stackoverflow.com/a/60196839/174777)