Understanding policies in AWS

As I learned more about AWS, and tried to get projects to work, I started to realize how important it is to understand the permissions and polices system. If I didn't have the proper permissions assigned to resources like an EC2 instance, or a Lambda function, then things would not work. And in many cases the CloudTrail logs would not help in understanding the problems with the permission settings.

In order to get things to work, I must admit that I sometimes allowed permissions that were more than what was required. In other words, I was breaking the principal of least privilege.

That's when I realized that I needed to revisit permissions are applied in AWS, and that meant that I needed to understand AWS policies. There are a few types of policies that I'll be covering in this article:

Permission Policies

A permission policy defines what a user, group, or role can (and cannot) do in AWS.

A permission policy specifies a Statement which consists of:

Here's very simple permission policy as an example:

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

This policy allows all S3 actions on any bucket resource (which is very permissive and may not adhere to the least privilege principle!). After defining a policy such as this one, you would then attach it to one or more principals. Then that principal would have full access to all your S3 buckets

A principal refers to an entity that can make a request to AWS resources.

Types of Principals

Identity-based policies:

The policy above is an identity based policy because it would be attached to an identity. Let's assume that we attach it to a role that we create, named 'S3BucketAdmin', which we'll use in upcoming examples.

A trust policy is attached to a role and lists the principals that are allowed to assume the role. But, in order for the principal to actually assume the role, it must have an identity-based policy that states that it assumes the role

Let's say we update the trust policy of the S3BucketAdmin role to look like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:user/alice"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

This trust policy declares that Alice is a trusted entity and is eligible to assume the S3BucketAdmin role.

In order to actually assume the S3BucketAdmin role, Alice's user would need to have this identity-based policy attached to her user account:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::123456789012:role/S3BucketAdmin"
    }
  ]
}

Alice can now assume the S3BucketRole and receive a token from the Security Token Service (AWS STS) that will grant temporary permissions that are specified in the role's permission policy

Managed Policies

AWS has many pre-made policies, known as managed polices which you can assign/attach to users, groups, or roles. Using these polices can be easier that creating your own.

The AdministratorAccess managed policy looks like this:

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

The AdministratorAccess policy is very straight forward, it give permission to do any action on any resource. But many of the managed policies are very fine-grained and include many statements.

When you use the AWS console, the CLI, or the SDK to attach one of the managed policies to a user, group, or role, AWS will store metadata that links the ARN of the user, group, or role to the policy.

Resource-based Policies

A resource-based policy is one that is attached to a resource (unlike identity-based policies which are attached to identities). This is different than assgining a role to a resource. The policy itself is attached to a resource. The policy specifies the principals that can access the resource and the permissions that they'll have on that resource.

Identity-based policies can be attached to many identities, while a resource-based policy is attached to a single resource.

Here is a resource-based policy that allows the users Bob and Alice to get objects from the S3 bucket named some-bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadForAliceAndBob",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::123456789012:user/alice",
          "arn:aws:iam::123456789012:user/bob"
        ]
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::some-bucket/*"
    }
  ]
}

Even though the bucket is specified in the policy, the policy must still be attached to the bucket. You could do this in the Permission tab for the bucket in the AWS console.

Here's a resource-based policy that allows anyone to get objects from 'my-public-bucket' (this allows read access to anyone who has the URL to the bucket):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPublicRead",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-public-bucket/*"
    }
  ]
}

Because the Principal is set to a wildcard, this policy allows public access to read a specific S3 bucket.

If a resource-based policy does not specify a Principal, it is implied that it applies to all identities (which is the same as setting the Principal to a wildcard). These are known as anonymous or unrestricted policies.

A common use for a resource-based policy is to attach it to an S3 bucket which allows CloudTrail to add objects to it. This would be the bucket where CloudTrail writes its logs, and it might look like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AWSCloudTrailWrite",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudtrail.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::cloudtrail-logs/*",
    }
  ]
}

Resource-based policies can be used to allow access to resources from other AWS accounts. For example, this policy could be attached to 'my-shared-bucket' which allows a role (EC2AccessS3Role) that belongs to a differnt AWS account (the acount id is 222222222222):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAccountBEC2RoleAccess",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::222222222222:role/EC2AccessS3Role"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-shared-bucket/*"
    }
  ]
}

You can combine identity and resource-based policies, for example an identity-based policy might grant an IAM user broad permissions to an AWS account, but a resource-based policy on a specific S3 bucket could further restrict access to only certain actions or specific identities.

There are other types of policies, such as session, permission boundary, service control policies (SCP), but I'm not worried about them right now.

There's also inline policies that are attached directly to a role or resource and cannot be re-used.

Note that you can use AWS CloudTrail to track how access and policies are being used by users and roles.