IAM policy evaluation logic explained with examples

A mental model for how IAM policies grant and deny access in an AWS account

Author's image
Tamás Sallai
9 mins

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 Effects. 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:

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policiesDeniedDeniedAllowedAllowedResource-based policyIs there a Deny?No denyAllows?Is there an Allow?Identity-, and resource-based policies evaluation

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”.

Learn the services needed to build a serverless HTTP-based API on AWS from our free email course.

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:

userbucket«policy»{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/text.txt"}aws s3api get-objectUser with an identity policy

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:

RequestPrincipal: <iam>/userAction: s3:GetObjectResource:<bucket>/text.txtIdentity policy{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/text.txt"}The identity policy matches

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”:

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policies{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/text.txt"}DeniedDeniedAllowedAllowedResource-based policyIs there a Deny?No denyAllows?Is there an Allow?The identity-based policy allows the operation

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.

userbucket«policy»{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/text.txt"}«policy»{"Effect": "Deny","NotPrincipal": {"AWS":"<iam>/bucket_admin"},"Action": "s3:GetObject","Resource": "<bucket>/text.txt"}aws s3api get-objectThe bucket has a policy

The request is made by user who is not bucket_admin, so the bucket policy is in effect.

RequestPrincipal: <iam>/userAction: s3:GetObjectResource:<bucket>/text.txtIdentity Policy{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/text.txt"}Resource policy{"Effect": "Deny","NotPrincipal": {"AWS":"<iam>/bucket_admin"},"Action": "s3:GetObject","Resource": "<bucket>/text.txt"}Both policies apply

In the evaluation flow, the request is denied due to the explicit deny.

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policies{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/text.txt"}DeniedDeniedAllowedAllowedResource-based policy{"Effect": "Deny","NotPrincipal": {"AWS": "<iam>/bucket_admin"},"Action": "s3:GetObject","Resource": "<bucket>/text.txt"}Is there a Deny?No denyAllows?Is there an Allow?The resource policy denies the operation

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.

bucket_adminbucket«policy»{"Effect": "Deny","NotPrincipal": {"AWS": "<iam>/bucket_admin"},"Action": "s3:GetObject","Resource": "<bucket>/text.txt"}aws s3api get-objectThe user has no access when there is no allow policy

When the bucket_admin user makes the request, no policies apply.

RequestPrincipal: <iam>/bucket_adminAction: s3:GetObjectResource:<bucket>/text.txtResource policy{"Effect": "Deny","NotPrincipal": {"AWS":"<iam>/bucket_admin"},"Action": "s3:GetObject","Resource": "<bucket>/text.txt"}Does not applyThe policy does not apply to the bucket_admin user

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”.

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policiesDeniedDeniedAllowedAllowedResource-based policyIs there a Deny?No denyAllows?Is there an Allow?No policy allows the operation

Let’s add a statement to the bucket policy to give access to the bucket_admin user!

bucket_adminbucket«policy»{"Effect": "Deny","NotPrincipal": {"AWS":"<iam>/bucket_admin"},"Action": "s3:GetObject","Resource": "<bucket>/text.txt"}«policy»{"Effect": "Allow","Principal": {"AWS":"<iam>/bucket_admin"},"Action": ["s3:GetObject"],"Resource": "<bucket>/text.txt"}aws s3api get-objectA resource policy can allow an action

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.

RequestPrincipal: <iam>/bucket_adminAction: s3:GetObjectResource:<bucket>/text.txtResource Policy{"Action": ["s3:GetObject"],"Effect": "Allow","Principal": {"AWS": "<iam>/bucket_admin"},"Resource": "<bucket>/text.txt"}Resource policy{"Effect": "Deny","NotPrincipal": {"AWS":"<iam>/bucket_admin"},"Action": "s3:GetObject","Resource": "<bucket>/text.txt"}The new policy matches

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.

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policiesDeniedDeniedAllowedAllowedResource-based policypolicy: {"Action": ["s3:GetObject"],"Effect": "Allow","Principal": {"AWS": "<iam>/bucket_admin"},"Resource": "<bucket>/text.txt"}Is there a Deny?No denyAllows?Is there an Allow?A resource-based policy allows the operation

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:

bucketsecret.txtaccess=secretrestricted.txtaccess=restricteduser«policy»{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/*","Condition": {"StringEquals": {"s3:ExistingObjectTag/access": "secret"}}}aws s3api get-objectA policy can define conditions

When the user tries to get an object that is tagged with access=secret, the policy matches:

RequestPrincipal: <iam>/userAction: s3:GetObjectResource:<bucket>/secret.txts3:ExistingObjectTag/access: secretIdentity Policy{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/*","Condition": {"StringEquals": {"s3:ExistingObjectTag/access": "secret"}}}The object tag matches the value

With a policy that allows the operation, the final decision is to allow.

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policiespolicy: {"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/*","Condition": {"StringEquals": {"s3:ExistingObjectTag/access": "secret"}}}DeniedDeniedAllowedAllowedResource-based policyIs there a Deny?No denyAllows?Is there an Allow?The policy allows the operation

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.

RequestPrincipal: <iam>/userAction: s3:GetObjectResource:<bucket>/restricted.txts3:ExistingObjectTag/access: restrictedIdentity Policy{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/*","Condition": {"StringEquals": {"s3:ExistingObjectTag/access": "secret"}}}The object has a different tag value

And as no policy allows the operation it is denied.

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policiesDeniedDeniedAllowedAllowedResource-based policyIs there a Deny?No denyAllows?Is there an Allow?No policy allows the operation

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!

bucketsecret.txtaccess=secretrestricted.txtaccess=restricteduseraccess=restricted«policy»{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/*","Condition": {"StringEquals": {"s3:ExistingObjectTag/access":"${aws:PrincipalTag/access}"}}}aws s3api get-objectTag-based access control

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.

RequestPrincipal: <iam>/userAction: s3:GetObjectResource:<bucket>/restricted.txts3:ExistingObjectTag/access: restrictedaws:PrincipalTag/access: restrictedIdentity Policy{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/*","Condition": {"StringEquals": {"s3:ExistingObjectTag/access": "${aws:PrincipalTag/access}"}}}The principal and the resource tag matches

As there is an Allow policy, the request is allowed.

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policies{"Action": ["s3:GetObject"],"Effect": "Allow","Resource": "<bucket>/*","Condition": {"StringEquals": {"s3:ExistingObjectTag/access": "${aws:PrincipalTag/access}"}}}DeniedDeniedAllowedAllowedResource-based policyIs there a Deny?No denyAllows?Is there an Allow?The operation 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:

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policiesDeniedDeniedDeniedAllowedAllowedResource-based policyIs there a Deny?No denyAllows the identity?Allows the account?Is there an Allow?Some resource-based policy must delegate to the account

Let’s see how a user can assume a role when its trust policy specifies the account!

roleuser«policy»{"Action": ["sts:AssumeRole"],"Effect": "Allow","Resource": "<role>",}«policy»{"Action": ["sts:AssumeRole"],"Effect": "Allow","Principal": {"AWS": "arn:aws:iam::<accountid>:root"}}aws sts assume-roleA role's trust policy allows the account

The evaluation logic considers both policies, and the combination of them allows the operation.

Collect all applicable policiesFilters by Principal, Resource, Action, ConditionIdentity-based policies{"Action": ["sts:AssumeRole"],"Effect": "Allow","Resource": "<role>"}DeniedDeniedDeniedAllowedAllowedResource-based policy{"Effect": "Allow","Principal": {"AWS": "arn:aws:iam::<accountid>:root"},"Action": "sts:AssumeRole"}Is there a Deny?No denyAllows the identity?Allows the account?Is there an Allow?The operation is allowed

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.

20 October 2020
In this article