AWS Security Blog

How to restrict Amazon S3 bucket access to a specific IAM role

April 2, 2021: In the section “Granting cross-account bucket access to a specific IAM role,” we’ve updated the second policy to fix an error.


I am a cloud support engineer here at AWS, and customers often ask me how they can limit Amazon S3 bucket access to a specific AWS Identity and Access Management (IAM) role. In general, they attempt to do this the same way that they would with an IAM user: use a bucket policy to explicitly Deny all Principals (users and roles) to which they do not want to grant access. The drawback with this approach is the required maintenance of the bucket policy. If a new IAM user were added to the account with “s3:*” for the Action, the user would be granted access to the bucket. Rather than specify the list of users whose access you want to block, you can invert the logic and leverage the NotPrincipal element in the bucket policy’s Deny statement. This element creates an explicit Deny for any user that is not listed in its value.

However, this inverted logic approach proves problematic with IAM roles because the role’s Principal value is composed of two Amazon Resource Names (ARNs), the role ARN and the assumed-role ARN. The role ARN is the identifier for the IAM role itself and the assumed-role ARN is what identifies the role session in the logs. When using the NotPrincipal element, you must include both ARNs for this approach to work, and the second of these ARNs should include a variable name. Normally you would specify a wildcard where the variable string would go, but this is not allowed in a Principal or NotPrincipal element. In this blog post, I show how you can restrict S3 bucket access to a specific IAM role or user within an account using Conditions instead of with the NotPrincipal element. Even if another user in the same account has an Admin policy or a policy with s3:*, they will be denied if they are not explicity listed. You can use this approach, for example, to configure a bucket for access by instances within an Auto Scaling group. You can also use this approach to limit access to a bucket with a high-level security need.

Solution overview

The solution in this post uses a bucket policy to regulate access to an S3 bucket, even if an entity has access to the full API of S3. The following diagram illustrates how this works for a bucket in the same account.

Diagram illustrating how this solution works for a bucket in the same account

  1. The IAM user’s policy and the role’s user policy grant access to “s3:*”.
  2. The S3 bucket policy restricts access to only the role.
  3. Both the IAM user and the role can access buckets in the account. The role is able to access both buckets, but the user can access only the bucket without the bucket policy attached to it. Even though both the role and the user have full “s3:*” permissions, the bucket policy negates access to the bucket for anyone that has not assumed the role.

The main difference in the cross-account approach is that every bucket must have a bucket policy attached to it to. The following diagram illustrates how this works in a cross-account deployment scenario.

Diagram illustrating how the solution works in a cross-account deployment scenario

  1. The IAM role’s user policy and the IAM users’ policy in the bucket account both grant access to “s3:*”
  2. The bucket policy denies access to anyone if their user:id does not equal that of the role, and the policy defines what the role is allowed to do with the bucket.
  3. The bucket policy allows access to the role from the other account.
  4. The IAM user and role can access the bucket without the Deny in the bucket policy. The role can access both buckets because the Deny is only for principals whose user:id does not equal that of the role.

Understanding the NotPrincipal element and how to use it

You can use the NotPrincipal element of an IAM or S3 bucket policy to limit resource access to a specific set of users. This element allows you to block all users who are not defined in its value array, even if they have an Allow in their own IAM user policies. As a result, if you have a user that should have access to all buckets except one in S3, you can define this on the bucket itself without having to edit the user’s IAM policy stack.

For an IAM role, however, this is more complex because the role is defined by two ARNs in its Principal: the role ARN and the assumed-role ARN. The role ARN (arn:aws:iam::ACCOUNTNUMBER:role/ROLE-NAME) is static and independent of who has initiated the role session. (Throughout this post, remember to replace placeholder information with your own account information.) The assumed-role ARN (arn:aws:sts::ACCOUNTNUMBER:assumed-role/ROLE-NAME/ROLE-SESSION-NAME) will vary depending on what is defined for the role session name. You can see this by looking at the following Identity element in an AWS CloudTrail entry for an API call made by a user who has assumed a role.

{
  "type": "AssumedRole",
  "principalId": "AROAJI4AVVEXAMPLE:ROLE-SESSION-NAME",
  "arn": "arn:aws:sts::ACCOUNTNUMBER:assumed-role/ROLE-NAME/ROLE-SESSION-NAME",
  "accountId": "ACCOUNTNUMBER",
  "accessKeyId": "ASIAEXAMPLEKEY",
  "sessionContext": {
    "attributes": {
      "mfaAuthenticated": "false",
      "creationDate": "XXXX-XX-XXTXX:XX:XXZ"
    },
    "sessionIssuer": {
      "type": "Role",
      "principalId": "AROAJI4AVV3EXAMPLEID",
      "arn": "arn:aws:iam::ACCOUNTNUMBER:role/ROLE-NAME",
      "accountId": "ACCOUNTNUBMER",
      "userName": "ROLE-SESSION-NAME"
    }
  }
}

Within this Identity element, you see both the role ARN and the assumed-role ARN. The ROLE-SESSION-NAME could potentially change based on who has assumed the role. The principalId value also includes this information, but is formatted in a way that will be usable outside of a Principal element of a bucket policy. I will use this information when authoring the bucket policy.

Granting same-account bucket access to a specific role

When accessing a bucket from within the same account, it is not necessary to use a bucket policy in most cases. This is because a bucket policy defines access that is already granted by the user’s direct IAM policy. S3 bucket policies are usually used for cross-account access, but you can also use them to restrict access through an explicit Deny, which would be applied to all principals, whether they were in the same account as the bucket or within a different account.

Each IAM entity (user or role) has a defined aws:userid variable. You will need this variable for use within the bucket policy to specify the role or user as an exception in a conditional element. An assumed-role’s aws:userId value is defined as UNIQUE-ROLE-ID:ROLE-SESSION-NAME (for example, AROAEXAMPLEID:userdefinedsessionname).

To get AROAEXAMPLEID for the IAM role, do the following:

  1. Be sure you have installed the AWS CLI, and open a command prompt or shell.
  2. Run the following command: aws iam get-role –role-name ROLE-NAME.
  3. In the output, look for the RoleId string, which begins with AROA.You will be using this in the bucket policy to scope bucket access to only this role.

In the preceding CloudTrail code example, this ID is the principalId element. The value of this element is important, because the AWS policy variables can also be checked as strings in an IAM policy. Rather than specifying the role and assumed-role ARNs in a NotPrincipal element, you can use the aws:userId value in a StringNotLike condition with a wildcard string. Within the value of aws:userId, you will also want to add the root user of the account so that the bucket does not become completely inaccessible if the defined role is deleted. The root account’s userId is the account number.

Using the AROAEXAMPLEID that you just retrieved via the AWS CLI, your can create conditional logic for the bucket policy to scope the access of the bucket down to only users that are using this role when accessing the bucket. Using conditional logic rather than a NotPrincipal element allows for the use of a wildcard string to allow for any role session name to be accepted.

Now that you have the ID of the role to which you want to allow access, you need to block the access of other users from within the same account as the bucket. The policy to block access to the the bucket and its objects for users that are not using the IAM role or root account credentials would look like the following.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::MyExampleBucket",
        "arn:aws:s3:::MyExampleBucket/*"
      ],
      "Condition": {
        "StringNotLike": {
          "aws:userId": [
            "AROAEXAMPLEID:*",
            "111111111111"
          ]
        }
      }
    }
  ]
}

You can use this same policy for IAM users as well. An IAM user has a unique ID starting with AIDA that you can use for this purpose. To find this unique ID:

  1. With the AWS CLI installed, open a command prompt or shell.
  2. Run the command: aws iam get-user -–user-name USER-NAME
  3. In the output, look for the userId string, which will begin with AIDAEXAMPLEID.

When you have identified the userId string, you can place it in the “aws:userId” condition array, as shown in the following example.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::MyExampleBucket",
        "arn:aws:s3:::MyExampleBucket/*"
      ],
      "Condition": {
        "StringNotLike": {
          "aws:userId": [
            "AROAEXAMPLEID:*",
            "AIDAEXAMPLEID",
            "111111111111"
          ]
        }
      }
    }
  ]
}

Granting cross-account bucket access to a specific IAM role

In the previous section, I showed you how to restrict S3 bucket access to specific IAM roles or users within the same account. Now I will show you how to restrict access to specific users and roles in another account. When granting cross-account bucket access to an IAM user or role, you must define what that IAM user or role is allowed to do with that access. Previously on the AWS Security Blog, Jim Scharf wrote about the permissions needed to allow an IAM entity to access a bucket via the CLI/API and the console. Using the information found in this previous blog post, the CLI/API–level access bucket policy would look like the following.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/ROLENAME"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::MyExampleBucket"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/ROLENAME"
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::MyExampleBucket/*"
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::MyExampleBucket",
                "arn:aws:s3:::MyExampleBucket/*"
            ],
            "Condition": {
                "StringNotLike": {
                    "aws:userId": [
                        "AROAEXAMPLEID:*",
                        "111111111111"
                    ]
                }
            }
        }
    ]
}

Service actions required for console-level access, such as what would be used with IAM Switch Role functionality to the console, are shown in the following policy.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/ROLENAME"
            },
            "Action": [
                "s3:GetBucketLocation"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/ROLENAME"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::MyExampleBucket"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::111111111111:role/ROLENAME"
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::MyExampleBucket/*"
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::MyExampleBucket",
                "arn:aws:s3:::MyExampleBucket/*"
            ],
            "Condition": {
                "StringNotLike": {
                    "aws:userId": [
                        "AROAEXAMPLEID:*",
                        "111111111111"
                    ]
                }
            }
        }
    ]
}

To grant API/CLI access to an IAM user in another account you would need to add the AIDAEXAMPLEID for the IAM user to the “aws:userId” condtion like we did in the previous section. In addition to the “aws:userId” condition, you would also need to add the IAM user’s full ARN to the Principal element of these policies. Note that you cannot grant cross-account console access to an IAM user because that user would need to assume a role in the destination account, but you can grant access to the bucket through the API/CLI. This would look as follows.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": [
                {
                    "AWS": [
                        "arn:aws:iam::222222222222:role/ROLENAME",
                        "arn:aws:iam::222222222222:user/USERNAME"
                    ]
                }
            ],
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::MyExampleBucket"
        },
        {
            "Effect": "Allow",
            "Principal": [
                {
                    "AWS": [
                        "arn:aws:iam::222222222222:role/ROLENAME",
                        "arn:aws:iam::222222222222:user/USERNAME"
                    ]
                }
            ],
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::MyExampleBucket/*"
        },
        {
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::MyExampleBucket",
                "arn:aws:s3:::MyExampleBucket/*"
            ],
            "Condition": {
                "StringNotLike": {
                    "aws:userId": [
                        "AROAEXAMPLEID:*",
                        "AIDAEXAMPLEID",
                        "111111111111"
                    ]
                }
            }
        }
    ]
}

In addition to including role permissions in the bucket policy, you need to define these permissions in the IAM user’s or role’s user policy. The permissions can be added to a customer managed policy and attached to the role or user in the IAM console, with the following policy document.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListAllMyBuckets",
        "s3:GetBucketLocation"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::MyExampleBucket"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::MyExampleBucket/*"
    }
  ]
}

By following the guidance in this post, you can restrict S3 bucket access to a specific IAM role or user in your local account and cross account, even if the user has an Admin policy or a policy with s3:*. There are many applications of this logic and the requirements may differ across use cases. You can use this approach, for example, to set up a bucket for access by instances within an Auto Scaling group. You can also use this approach to limit access to a bucket with a high-level security need, as in a bucket with personnel records and account information. Keep in mind that it is always best to grant permissions only to resources that are required to perform necessary tasks.

If you have comments about this post, submit them in the “Comments” section below. If you have questions, please start a new thread on the IAM forum.

– Chris

Want more AWS Security how-to content, news, and feature announcements? Follow us on Twitter.