AWS IAM Deep Dive. Chapter 3: Credential evaluation, Principals, Conditions, Policy variables

Karen Tovmasyan

by Karen Tovmasyan

Feb 17, 2022


Credential evaluation

One of my favorite interview questions when it comes to AWS knowledge is about IAM. The question is simple:

You have an EC2 instance with Instance profile, which has associated IAM Role. The Role policies allow S3 FullAccess. On that EC2 instance, you have a Docker container running, with hardcoded IAM keys of a user who has an inline policy allowing S3 ReadOnly access. Which privileges this app have?

You see, there are still people out there who do not know how IAM credentials are evaluated, yet it is really simple to understand and remember.

When the application is invoking AWS API using AWS SDK, it evaluates credentials in the following workflow:

  1. Credentials hardcoded in the app (e.g. assigned in the client like boto3.client("ec2", aws_access_key="foo", aws_secret_access_key="bar", aws_session_token="foobar") ).
  2. If those credentials are not provided SDK will check environment variables.
  3. If environment variables are not provided SDK will check ~/.aws/credentials file.
  4. If that file is not present, SDK will call (Instance meta-data) for credentials.
  5. If IAM section of meta-data is empty — you get “no credentials” exception.

So a proper answer to the question above is “S3 ReadOnly”. But I usually expect a candidate to explain why it is like that.

As I said this is fairly simple but really important to know. Let’s move on to the next part.


From the previous chapter, you already know that Principal is an entity which is allowed to perform API calls, stated in the Policy. Let’s look at the example policy again.

2  "Version": "2012-10-17",  
3  "Sid": "AllowAssumeRole",  
4  "Statement": [   
5    {  
6      "Effect": "Allow",  
7      "Action": "sts:AssumeRole",  
8      "Principal": "AWS": "arn:aws:iam::1234567890:user/user_name"  
9    }  
10  ]  

In this Policy IAM user “user_name”, which belongs to AWS Account with ID 1234 is allowed to invoke sts:AssumeRole method.

Principals can be:

  • AWS Account
  • IAM entity — Role or User for example
  • AWS Service
  • Everyone
  • And few other entities like federated users, etc.

The full list of allowed Principals is here.

Specifying an AWS Account in the Principal section allows us to not only give access to calls within that account but also enable Cross-Account Roles.

For example, if you have a Production AWS account with ID 1234 and Development AWS account with ID 5678, you can enable access to Role from Dev to Prod by adding Development Account ID in the Principal.

1\# This Assume Role Policy Document is in Prod account  
3  "Version": "2012-10-17",  
4  "Sid": "AllowAssumeRole",  
5  "Statement": [   
6    {  
7      "Effect": "Allow",  
8      "Action": "sts:AssumeRole",  
9      "Principal": { "AWS": "5678" }  
10    }  
11  ]  

Note that in this example you give the whole account access to the Role. Since it’s quite dangerous, it is wise to specify entity ARNs in the principal (and we can use lists for that:

2  "Version": "2012-10-17",  
3  "Sid": "AllowAssumeRole",  
4  "Statement": [   
5    {  
6      "Effect": "Allow",  
7      "Action": "sts:AssumeRole",  
8      "Principal": {   
9        "AWS": [  
10          "arn:aws:iam::5678:user/user1",  
11          "arn:aws:iam::5678:user/user2"  
12        ]  
13      }  
14    }  
15  ]  

Another Principal I’d like to focus on is the AWS Service Principal. You add “trusted” AWS Services to principals by specifying their Full-Service Name . Let’s say we need to give Lambda Function access to the Role to work with S3 (Get and Put Objects for example). Then our Assume Role Policy Document will look like this:

2  "Version": "2012-10-17",  
3  "Sid": "AllowAssumeRole",  
4  "Statement": [   
5    {  
6      "Effect": "Allow",  
7      "Action": "sts:AssumeRole",  
8      "Principal":   
9        {   
10          "Service": ""  
11        }  
12    }  
13  ]  

Service is the only Principal which does not allow wildcards. So you can't do "Service": "*".

Another thing to know is that the Service entity is limited within the account. That means that only services in that exact account can assume the role and you can not allow service in account A to assume the role in account B.

Last but not least — a Principal called “Everyone”. There are 2 types of “everyone”

  • "AWS": "*"
  • "*"

The difference between these two is the limit. If you add “AWS” at the beginning that will allow public access to a policy from all AWS “containers” and other accounts.

While the wildcard without “AWS” in front allows anonymous access. While you might think it is crazy, think of the case of Static Website Hosting on S3. In order to give access to S3 contents (HTML, CSS, JS, fonts, pictures, etc) you need to allow ReadOnly Allow action for Bucket objects. And since S3 Bucket Policy is also IAM policy (but associated with the bucket), you do the same actions as with regular IAM Policies:

2  "Version":"2012-10-17",  
3  "Statement": [  
4    {  
5        "Sid":"PublicReadOnly",  
6        "Effect":"Allow",  
7        "Principal": "*",  
8        "Action": [ "s3:GetObject" ],  
9        "Resource": [ "arn:aws:s3:::mybucket/*" ]  
10    }  
11  ]  


Now, this is where things get serious. Conditional policy elements specify conditions (sic!) in which circumstances API call can be made. Conditions are the parts of Statement and you can have multiple Conditions in one Statement or in multiple Statements. There are tons of use cases for them, but I will cover only the most popular of them, while I encourage you to go through the list of all Conditional Operators.

Conditions have a key and a value. The key is a variable which is replaced by another value during the policy evaluation. For example this condition

1"Condition" : { "StringEquals" : { "aws:username" : "johndoe" }}


  • Which patterns must match (String in this case)
  • A “key” — IAM user name
  • A “value” — “johndoe”

Which means this condition will check if the user who’s making a call is “johndoe”.

Now, we can have conditions with multiple values to the key (they act as OR) and with multiple keys (which is AND).

Condition evaluation (from AWS documentation)

For example, we want to check that only specific users will assume AdminRole only from specific IP address and only if logged in with MFA device.

2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Effect": "Allow",
6      "Principal": {
7        "AWS": [
8          "arn:aws:iam::acct_id:user/user1",
9          "arn:aws:iam::acct_id:user/user2"
10        ]
11      },
12      "Action": "sts:AssumeRole",
13      "Condition": {
14        "IpAddress": {
15          "aws:SourceIp": ""
16        },
17        "Bool": {
18          "aws:MultiFactorAuthPresent": "true"
19        }
20      }
21    }
22  ]

Note that both conditions must match the value, otherwise users won’t be able to assume the role.

Another example is the case I face quite often. You remember that one of the Principal entities is AWS Service. When you specify AWS Service Full Name, such as , that means that all Lambda functions can assume the role. Now, I don’t want that. I am paranoid (I used to work with banks and trading companies, hope you understand my childhood traumas) and I want specific functions to work with only specific roles.

What I can do is to add ARN of the function to the Condition:

2  "Version": "2012-10-17",
3  "Statement": {
4    "Effect": "Allow",
5    "Principal": {
6      "Service": ""
7    },
8    "Action": "sts:AssumeRole",
9    "Condition": {
10      "ArnEquals": {
11        "aws:SourceArn": "arn:aws:lambda:REGION:ACCOUNTID:function:name"
12      }
13    }
14  }

Not all services support that, so make sure to test before rolling out.

But it doesn’t only come to roles. Let’s say you want to limit instance types which your users can launch.

2  "Version": "2012-10-17",
3  "Statement": {
4    "Sid": "RunInstance",
5    "Effect": "Allow",
6    "Action": "ec2:RunInstances",
7    "Resource": "*",
8    "Condition": {
9      "StringLikeIfExists": {
10        "ec2:InstanceType": [
11          "t1.*",
12          "t2.*",
13          "t3.*"
14        ]
15      }
16    }
17  }

Last but not least: I promised to give an example of limiting access to an S3 Bucket only during a specific time range. The thing here is that Date Conditions need to have both date and time. That is quite frustrating that ISO 8601 is not satisfied completely (If you found a way to use only time, please share with me). So the date condition looks like the following:

2  "Version": "2012-10-17",
3  "Statement": {
4    "Effect": "Allow",
5    "Action": "sts:AssumeRole",
6    "Principal": {
7      "AWS": "ACCT_ID"
8    },
9    "Condition": {
10      "DateLessThan": {
11        "aws:CurrentTime": "2013-06-30T17:00:00Z"
12      },
13      "DateGreaterThan": {
14        "aws:CurrentTime": "2013-06-30T09:00:00Z"
15      }
16    }
17  }

This will limit access between 9 a.m. and 5 p.m., but only for a specific date. The only workaround I found is to deploy a Lambda function, which runs every day at midnight and replaces the date with current day, but that’s a dangerous hack (what if Lambda fails to run?)

Again, there are plenty of things you can do with conditions, so dig into the docs and see if there are some solutions for your use cases.

Policy variables

Now the final part of this chapter is about Policy Variables.

IAM Policy Variables allow you to render policies on the fly without duplicating the same entries over and over again.

Let’s say we have an S3 Bucket which acts as a home directory for multiple users.

2      \\  
3       |_ David/  
4       |_ John/  
5       |_ Ivan/  
6       |_ Tom/  
7       # and so on

What we want to do is to limit the uploading/downloading of objects for users only from and to their home dir. If we go with “classic” IAM policies each user will have a policy like this:

1\# Policy for David  
3   "Version": "2012-10-17",  
4   "Statement": {  
5     "Effect": "Allow",  
6     "Action": [  
7       "s3:GetObject",  
8       "s3:PutObject",  
9       "s3:DeleteObject"  
10     ]  
11     "Resource": "arn:aws:s3:::home_dir_bucket/David/*  
12   }  

We’d have to make multiple policies, each for a user, which is a perfect example of unnecessary duplication. Instead, we can create a policy with variable and associate with every user or a single group:

2   "Version": "2012-10-17",  
3   "Statement": {  
4     "Effect": "Allow",  
5     "Action": [  
6       "s3:GetObject",  
7       "s3:PutObject",  
8       "s3:DeleteObject"  
9     ]  
10     "Resource": "arn:aws:s3:::home_dir_bucket/${aws:username}/*  
11   }  

In Conditions part, you’ve seen statements like aws:CurrentTime or aws:SourceIP — this is another variant of variables usage.

There's too much to cover here, but you can read about example usage in this documentation page.


In this chapter, we covered more advanced topics on IAM.

We’ve learned how credential evaluation works, what kind of Principals can we have, what are Conditions and Policy Variables and what are use cases for them.

In the next and final chapter: Policy Evaluation Logic, Permission Boundaries and few words on Identity Federation.

As usual, if you found a mistake, feel free to drop me a message!


    Related Articles


    Join the beta waitlist

    Enter your email to get notified when our product becomes available to try.

    Sign Up for the community

    Create your member profile to get involved with our content, programs, and events.