Identity vs Resource-based AWS IAM Policies

by Cezary Czekalski, posted 18/08/2021

 

Identity and Access Management (IAM) is a global AWS service dedicated to managing access to AWS services and resources. It’s a cornerstone of AWS, but it’s also very powerful and there are many ways to achieve the same goal. In this post, I’m going to look at two approaches to securing access to AWS resources. Let’s first start by looking at some fundamental IAM concepts.

 

Key IAM Concepts

An IAM user is an entity that you create in AWS to represent the person or application that uses it to interact with AWS. Users can be given access to the AWS console or to AWS APIs. Given the appropriate permissions, users can invoke actions on AWS infrastructure resources. Users can also be assigned to groups – a collection of IAM users. Groups let you specify permissions for multiple users, which can make it easier to manage the permissions for those users. IAM users can be assigned to many different groups (one user can be assigned to many groups, but a group itself cannot be assigned to another group).

IAM permissions are assigned via policies. There is a principle which states that what is not explicitly allowed is denied by default and it’s a good idea to prevent accidentally being over-privileged.

An IAM role is an IAM identity that has specific permissions. It is similar to an IAM user, in that it is an AWS identity with permission policies that determine what the identity can and cannot do in AWS. However, instead of being uniquely associated with one person, a role is intended to be assumable by anyone who needs it.

A policy is a set of permissions written in JSON format. There are six types of policies, but this post will focus on two of them; identity-based policies, and resource-based policies.

 

Identity-based Policies

Identity-based policies grant permissions to an identity. An identity-based policy dictates whether an identity to which this policy is attached is allowed to make API calls to particular AWS resources or not. For example, the following policy would allow a user to invoke any Get or List request on any S3 resource.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:Get*",
                "s3:List*"
            ],
            "Resource": "*"
        }
    ]
}

The key elements of the policy definition are:

  • Effect – whether to allow or deny the action(s)
  • Action – the API(s) the policy applied to requests on resources (‘*’ means wildcard)
  • Resource – identifier of the resource(s) to which the policy applies. Resources are identified by an Amazon Resource Name (ARN), or by wildcard.

 

Resource-based Policies

Resource-based policies grant permissions to the principal that is specified in the policy. They specify who or what can invoke an API from a resource to which the policy is attached.

For example, the policy below specifies that S3 events on the bucket arn:aws:s3:::test-bucket-cezary can be handled by the Lambda (lambda-s3) in account id 1234567890 in eu-west-1 region.

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "s3-event-cezary_for_lambda-s3",
      "Effect": "Allow",
      "Principal": {
        "Service": "s3.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:eu-west-1:1234567890:function:lambda-s3",
      "Condition": {
        "StringEquals": {
          "AWS:SourceAccount": "1234567890:"
        },
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:s3:::test-bucket-cezary"
        }
      }
    }
  ]
}

In this case, the principal is “a caller” who can invoke a particular action on the specific resource arn:aws:s3:::test-bucket-cezary.

Now that we’ve covered these two policy types, let’s set up some demo policies and explore the types of issues that can result when mixing the two policy types and setting too restrictive or too permissive access.

Demo Environment

Assuming you have AWS account setup, create two IAM users:

  • An “admin user” with the following policies attached
    • AdministratorAccess – This policy allows for almost unrestricted access to all services. This identity-based policy is available by default. It is created & managed by AWS. It’s also one of the so-called AWS managed policies.
  • A “restricted user” with the following policies attached
    • IAMReadOnlyAccess – Read-only access to the IAM console
    • ListAllMyBuckets (Permission) – list all S3 buckets (but not their content). You need to create an identity policy to set permission inside of it.
    • AWSCloudShellFullAccess – Provides fully featured access to CloudShell in AWS Console

The policy for the admin user is:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "*",
            "Resource": "*"
        }
    ]
}

The policy for the restricted user is:

{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Effect":"Allow",
         "Action":[
            "iam:GenerateCredentialReport",
            "iam:GenerateServiceLastAccessedDetails",
            "iam:Get*",
            "iam:List*",
            "iam:SimulateCustomPolicy",
            "iam:SimulatePrincipalPolicy"
         ],
         "Resource":"*"
      }
   ]
}{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Sid":"VisualEditor0",
         "Effect":"Allow",
         "Action":"s3:ListAllMyBuckets",
         "Resource":"*"
      }
   ]
}{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Action":[
            "cloudshell:*"
         ],
         "Effect":"Allow",
         "Resource":"*"
      }
   ]
}

Now let’s create a bucket via our admin user and test these two policies. You can do this via the command line, or if you don’t want to install the AWS Command Line Interface (CLI), via CloudShell.

# Create a bucket
aws s3api create-bucket --bucket YOUR_GLOBALLY_UNIQUE_NAME
--region YOUR_CHOSEN_REGION
--create-bucket-configuration LocationConstraint=YOUR_CHOSEN_REGION

# Response
{"Location": "http://YOUR_GLOBALLY_UNIQUE_NAME.s3.amazonaws.com/"}    
# Prohibit public access
aws s3api put-public-access-block --bucket YOUR_GLOBALLY_UNIQUE_NAME
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"
# Put one object into a bucket (optional)
aws s3api put-object --bucket YOUR_GLOBALLY_UNIQUE_NAME
--key YOUR_FILE --body YOUR_FILE
# List your buckets (optional)
aws s3api list-buckets
#Response
{  "Buckets": [
        {
            "Name": "YOUR_GLOBALLY_UNIQUE_NAME",
            "CreationDate": "2021-06-12T11:12:55+00:00"
        },...

Policies Problems and How to Avoid Them

Having created and configured the bucket check from either console or via an API call that this bucket policy (the resource-based policy) is empty.

aws s3api get-bucket-policy --bucket YOUR_GLOBALLY_UNIQUE_NAME --query Policy --output text

Ok, now we are ready to list the content of our bucket.

# As the admin user
aws s3 ls s3://YOUR_GLOBALLY_UNIQUE_NAME
# Response: 2021-06-15 15:13:33     612870 mountains.jpg
# As the restricted user
aws s3 ls s3://YOUR_GLOBALLY_UNIQUE_NAME
# Response: An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied

As you can see, the restricted user does not have permission to list the objects in the bucket. To fix this you could use either Identity-based policies or resource-based policies (or both, but that would be redundant). Let’s go ahead and create a resource-based policy. To do this, we could use the console, but let’s use the AWS Policy Generator instead.

Go to the AWS console and select the bucket you created earlier. Next go to permissions, choose bucket policy, and then edit and policy generator. After filling the template click Add Statement and then Generate policy.

In this case I’ve chosen a bucket policy, because I know that I will add more users in the future.

Copy and paste policy to bucket settings

{
  "Id": "Policy1624125393305",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1624125391986",
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::YOUR_GLOBALLY_UNIQUE_NAME",
      "Principal": {
        "AWS": [
          "arn:aws:iam::ACCOUNT_ID:user/read-only-console-user"
        ]
      }
    }
  ]
}

Sometimes you have to wait a bit for the policy to kick in. Now you should be able to list content of your bucket.

aws s3 ls s3://YOUR_GLOBALLY_UNIQUE_NAME
#Response: 2021-06-15 15:13:33     612870 mountains.jpg

What will happen when we explicitly deny something? Remember that IAM is very strict. Explicit denial is a real, true, strict rejection and it doesn’t matter whether you allowed something earlier. When you say you want to block it, it’s blocked.

The easiest way to check it is to add resource-based policy to our bucket.

{
    "Version": "2012-10-17",
    "Id": "Policy1623531542448",
    "Statement": [
        {
            "Sid": "Stmt1623531541369",
            "Effect": "Deny",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::YOUR_GLOBALLY_UNIQUE_NAME"
        }
    ]
}

Let’s test it

#IAM Admin bucket perspective
aws s3 ls s3://YOUR_GLOBALLY_UNIQUE_NAME
#Response: An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied

Remember, You can always simulate your calls in the IAM console like in this screenshot. It’s safer especially in production environments. This simulator takes into account already set identity-based, resource-based policies. That’s why I used an already created bucket and not a fake one.

Yet I think that testing it manually is much more fun even though it’s much more dangerous. Don’t you think?

If you wonder, You can brick your access to the bucket even for IAM Admin by attaching this policy.

WARNING: Don’t try this at home!

{
    "Version": "2012-10-17",
    "Id": "Policy1623526933543",
    "Statement": [
        {
            "Sid": "Stmt1623526931104",
            "Effect": "Deny",
            "Principal": {
                "AWS": "*"
            },
            "Action": "*",
            "Resource": "arn:aws:s3:::YOUR_GLOBALLY_UNIQUE_NAME"
        }
    ]
}

Then you would need to switch to the root account and remove this policy.

 

Final words

Policies can be quite tricky but they are also very powerful. I hope this post served as a useful introduction to some of the concepts. For more information be sure to check out the AWS Identity and Access Management documentation. It’s really good and It has helped me a lot many times. Have fun!