skip to Main Content

Summary

As mentioned above, I want to create an IAM role that can ONLY read / write objects to a SPECIFIC sub-folder in the S3 bucket.

However, I’m unable to despite following the examples here and here

Policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Condition": {
                "StringEquals": {
                    "s3:prefix": "levelOne=A/levelTwo=B/levelThree=C"
                }
            },
            "Action": [
                "s3:Get*",
                "s3:List*",
                "S3:Put*"
            ],
            "Resource": "arn:aws:s3:::insert_bucket_name",
            "Effect": "Allow"
        },
        {
            "Condition": {
                "StringNotEquals": {
                    "s3:prefix": "levelOne=A/levelTwo=B/levelThree=C"
                }
            },
            "Action": [
                "s3:Get*",
                "s3:List*",
                "S3:Put*"
            ],
            "Resource": "arn:aws:s3:::insert_bucket_name",
            "Effect": "Deny"
        }
    ]
}

Testing setup

AWS config file

[profile mybucketrole]
role_arn = arn:aws:iam::insert_account_id:role/insert_role_name
source_profile = default

Test command

aws s3api put-object 
--bucket insert_bucket_name
--key levelOne=A/levelTwo=B/levelThree=C/123456789.jsonl
--profile mybucketrole

I would expect that the above command succeeds since I’m writing an object to the levelOne=A/levelTwo=B/levelThree=C prefix, but I keep getting an

An error occurred (AccessDenied) when calling the PutObject operation: Access Denied

NOTE: I set up CloudTrail for my S3 bucket to verify that the intended role is being used by the test command (and indeed it is)

Other things I’ve tried

  • Replacing StringEquals + StringNotEquals with StringLike + StringNotLike (respectively) per here

  • Changing the resources to

    "Resource": [
       "arn:aws:s3:::insert_bucket_name",
       "arn:aws:s3:::insert_bucket_name/*"
    ]
    

What am I missing!?

3

Answers


  1. Chosen as BEST ANSWER

    I was able to get the behavior I wanted with the following permissions policies and CDK code.

    NOTE:

    • All of the following are role-based policies
    • The bucket policy was kept empty

    WRITE policies

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "s3:Put*",
                "Resource": "arn:aws:s3:::insert_bucket_name/level_one=A/level_two=B/level_three=C/*",
                "Effect": "Allow"
            }
        ]
    }
    

    READ policies

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": "s3:Get*",
                "Resource": "arn:aws:s3:::insert_bucket_name/level_one=A/level_two=B/level_three=C/*",
                "Effect": "Allow"
            },
            // NOTE: 
            // I had to be permissive here because otherwise, S3 will return an AccessDenied error instead of NoSuchKey when requesting nonexistent S3 objects. 
            // See the CDK code comments for details
            {
                "Action": "s3:List*",
                "Resource": "arn:aws:s3:::insert_bucket_name",
                "Effect": "Allow"
            }
        ]
    }
    

    CDK code

    return new Role(this, roleName, {
                roleName: roleName,
                assumedBy: new CompositePrincipal(new AccountPrincipal(props.stageProps.awsAccountId)),
                maxSessionDuration: Duration.hours(12),
                inlinePolicies: {
                    "READ": new PolicyDocument({
                        statements: [
                            new PolicyStatement({
                                effect: Effect.ALLOW,
                                actions: ["s3:Get*"],
                                resources: [
                                    new Array(
                                        this.[insert_bucket_variable].bucketArn,
                                        "levelOne=A",
                                        "levelTwo=B",
                                        "levelThree=C",
                                        "*",
                                    ).join("/")
                                ],
                            }),
                            new PolicyStatement({
                                effect: Effect.ALLOW,
                                actions: ["s3:List*"],
                                resources: [
                                    // First off, our resource here has no wildcards because the ListBucket API action (i.e. the API behind our favorite ListObjectsV2 boto3 call)
                                    // acts on the bucket resource itself vs. object resources (like the (Get|Put)Object API actions). 
                                    // However, we don't restrict visibility here because if we do, we'll see AccessDenied errors
                                    // if/when we make a GetObject API call against the bucket for a nonexistent object 
                                    // (see https://stackoverflow.com/a/56027548/16409315). That would be SUPER confusing to 
                                    // debug so we want to avoid it -- even if it means that this IAM role can 
                                    // list collections of converted labels from other labeling sources. That being said, this role still 
                                    // wouldn't be able to actually download any of those collections because of the s3:Get* permissions so it's not a huge concession.
                                    this.[insert_bucket_variable].bucketArn,
                                ],
                            }),
                        ]
                    }),
                    "WRITE": new PolicyDocument({
                        statements: [
                            new PolicyStatement({
                                effect: Effect.ALLOW,
                                actions: ["s3:Put*"],
                                resources: [
                                    new Array(
                                        this.[insert_bucket_variable].bucketArn,
                                        "levelOne=A",
                                        "levelTwo=B",
                                        "levelThree=C",
                                        "*",
                                    ).join("/")
                                ],
                            }),
                        ]
                    })
                },
            });
    

  2. The resource needs to be arn:aws:s3:::insert_bucket_name/* to apply to objects within the bucket instead of the bucket itself. Depending on which operations you want to allow the resource needs to be ["arn:aws:s3:::insert_bucket_name", "arn:aws:s3:::insert_bucket_name/*"]

    At least the Allow part could also be arn:aws:s3:::insert_bucket_name/levelOne=A/levelTwo=B/levelThree=C/* – but not sure if I would prefer that because now it is structurally different compared to the Deny.

    Login or Signup to reply.
  3. Amazon S3 buckets are private by default. Therefore, there is no need to use Deny unless you are wanting to override permissions that were otherwise granted by another IAM policy (eg preventing an Admin who has access to all S3 buckets from accessing an HR confidential bucket).

    Therefore, you could use a policy like this:

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Action": [
                    "s3:Get*",
                    "s3:List*",
                    "S3:Put*"
                ],
                "Resource": "arn:aws:s3:::insert_bucket_name/levelOne=A/levelTwo=B/levelThree=C/*",
                "Effect": "Allow"
            }
        ]
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search