IAM policy evaluation logic explained with examples
A mental model for how IAM policies grant and deny access in an AWS account
How IAM evaluates requests
IAM follows a defined course when it decides whether a given request is allowed or denied. The basic configuration block is IAM policies, which contain statements that grant/deny permissions.
The first step IAM does is it constructs a request context which contains all the details, such as who is making the request (the Principal), what is the action (Action), and on which resource (Resource), along with a bunch of others metadata, such as the IP address, whether the identity is authenticated with MFA, and several other things. The exact parameters are unfortunately not visible, and the only available resource is the reference documentation. This makes it more of a trial-and-error to get some insight into the values for a request.
Thinking of requests as lists of name-value pairs helps understanding how policy statements match them.
In the next step, IAM collects all the statements that match the request context. Each statement has a set of filters, in the form of the Action
, Principal
, Resource
, and Condition
properties. The statements that match the request context will be included in the policy evaluation logic.
Whether the request is allowed or denied depends on the type of matching statements and their Effect
s. The two most important policy types are
resource- and identity-based policies, as all the others (SCP, permissions boundary, session policy) are for specific use-cases and they are optional in the
evaluation flow.
The decision for most resources follow this process:
In this article, we'll look into a few policies and see how IAM reaches a conclusion when it evaluates them.
We'll use a 2-step process for each scenario. First, we construct the request context from the known values and see which policies apply to that request. Second, we'll use the above process to see what decision IAM reaches and why.
I'll use policy and statement interchangeably. A policy is a container for statements and the statements are the permissions, but in everyday speech, it's more natural to say "a policy allows this action" than "a statement allows this action".
Identity policies to allow access
Let's start with a simple example! A user has a policy that allows access to an object in an S3 bucket:
The user then gets the object using the AWS CLI:
aws s3api get-object --bucket <bucket> --key <key >(cat)
This sends a request to an AWS API signed with the user's keys. IAM then starts to evaluate access.
The request contains the user, the action, and the resource. In the policy, the Principal is implied, as it is attached to the user. All the filters in the policy match the request context:
Now that we know that this policy applies to this request, let's start the flow and see how a decision is made!
There is no policy with "Effect": "Deny"
. There is no resource-based policy either, so the execution reaches the identity-based policy evaluation step.
Since there is a policy that has an "Effect": "Allow"
, the final decision is "Allow":
Resource policy to deny access
Let's see an example of how a resource-based policy can restrict access! In this example, the S3 bucket has a bucket policy (which is a resource-based policy) to deny access for all users except for a specific one.
The request is made by user who is not bucket_admin, so the bucket policy is in effect.
In the evaluation flow, the request is denied due to the explicit deny.
Resource policy to allow access
Let's see how things change when the bucket_admin user makes the request! The deny policy does not apply, but neither does the user's is identity policy.
When the bucket_admin user makes the request, no policies apply.
In the evaluation flow, no policy denies access but none allows it either. The result will be a deny, which is called "an implicit deny".
Let's add a statement to the bucket policy to give access to the bucket_admin user!
In this case, the deny still does not apply, because of the NotPrincipal
element, but the other statement does because the Resource
, the Action
,
and the Principal
all match.
The evaluation flow reaches the resource-based policy check as there is no explicit Deny. And as there is a statement with an Allow, the request is allowed.
Using conditions
A Condition
block allows more fine-grained filtering. This is a diverse topic as there are many
global and
service-specific condition keys and which
ones are included in the request context is an opaque process.
Let's see an easy example involving object tags!
To give access to all object that has access=secret
tag to a user is possible with the s3:ExistingObjectTag/access
condition key:
When the user tries to get an object that is tagged with access=secret
, the policy matches:
With a policy that allows the operation, the final decision is to allow.
But let's see what happens when the target object has a different tag value!
The policy won't match because the Condition does not match the request context.
And as no policy allows the operation it is denied.
Tag-based access control
Instead of hardcoding a tag value, a tag-based access control scheme (commonly known as Attribute-Based Access Control, or ABAC for short) can use tags defined for the principal. This makes a highly scalable permission scheme, as adding and removing tags from resources and identities grants and removes the permissions instead of having to modify the policies attached to them.
Let's see an example where the user has an access=restricted
tag attached, and a policy that allows reading objects with the same tag!
The request context contains both the resource and the principal tags, so the Condition can match them with the placeholder in the IAM policy. In this case,
both the resource and the user have access=restricted
.
As there is an Allow policy, the request is allowed.
This example showed that when the tag value is equal for the resource and the identity the policy matches. It's easy to see how it doesn't when the two values are different.
Restricted resources
In the S3 bucket example, either the identity- or the resource-based policy is enough to give access. This is the case for most resource types, but there are exceptions. An IAM role's trust policy needs to allow the action explicitly, it's not enough that the identity policy allows it.
The resource policy can allow in two ways. It can allow the user explicitly, such as "Principal": "<iam>/user"
. In this case, the operation is allowed and
there is no need for an identity policy. This is how it works for less strict resources.
The other way is to allow the account in the form of "Principal": "arn:aws:iam::<accountid>:root"
. This delegates access control to the identity policies
to decide. If a trust policy does not allow either the requesting identity or the account then the request is denied.
This is how the policy evaluation flow looks like for these resources:
Let's see how a user can assume a role when its trust policy specifies the account!
The evaluation logic considers both policies, and the combination of them allows the operation.
Another resource that has strict policies is KMS keys. The AWS documentation mentions that since a key policy controls management access to it you can lock yourself out and have to contact support to regain control.
Conclusion
IAM policies control access to everything inside an AWS account and they are the main security controls. I found that thinking of them in terms of a request context and a control flow simplifies a lot.