Diving Deeply into IAM Policy Evaluation: Highlights from AWS re:Inforce IAM433
One of the most talked-about sessions at AWS re:Inforce was IAM433, which discussed AWS IAM’s internal evaluation mechanisms.
At this year's Amazon Web Services (AWS) re:Inforce conference, during their session IAM433, presenters Matt Luttrell, AWS Senior Solutions Architect, and Dan Peebles, AWS Senior Software Engineer for IAM Access Analyzer, delved into some of most arcane edge cases in AWS Identity and Access Management (IAM) – and why they behave as they do.
The session took a deep dive into AWS IAM internal evaluation mechanisms never shared before and revealed a new model for representing the AWS permission evaluation process. As the Tweet below shows, attendees saw great value in the session.
IAM433 has a good explanation of how and why permissions boundaries can be circumvented by resource policies. There’s a repeat tomorrow but it’s not recorded (chalk talk). This presentation should be made public and linked from the docs! #reInforce #awsreinforce #awswishlist
— Ben Kehoe (@ben11kehoe) July 26, 2022
In this article we review the content covered in the session for the benefit of those who couldn’t attend and as a memory refresh for those who did. Many thanks to Luttrell and Peebles for the excellent session and for generously sharing their slides, which are featured extensively here.
Definitions
Before starting, Matt and Dan set down a few clear definitions:
Policy evaluation outcomes
Every policy evaluation has one of three outcomes:
- Implicit deny — Denial due to the lack of an allow statement allowing the action.
- Explicit deny — Denial due to a matching deny statement. As you may recall, in AWS any deny statement overrides all allow statements.
- Explicit allow — The result of matching allow statements.
Note: No “implicit allow” exists; rather, anything not explicitly allowed is implicitly denied.
IAM principals that make requests
- Role session:
- arn:aws:sts::123456789012:assumed-role/MyRole/MySession
Luttrell and Peebles reminded us that, strictly speaking, IAM roles do not make requests. Role sessions, represented by temporary credentials for the roles, make the requests.
- arn:aws:sts::123456789012:assumed-role/MyRole/MySession
- IAM user:
- arn:aws:iam::123456789012:user/MyUser
- Federated user (using sts:GetFederationToken):
- arn:aws:sts::123456789012:federated-user/MyUser
Note: Despite the name, federated users are not directly involved in the standard SSO federation process. Rather, they are temporary sessions (with temporary credentials) derived from IAM users.
- arn:aws:sts::123456789012:federated-user/MyUser
- Anonymous
- Root
Authorization context
The next piece of the policy evaluation puzzle is the full authorization context, a property bag of any information that can be used in policy evaluation (as described by Becky Weiss in another highly recommended session, IAM301: AWS IAM deep dive).
Principal: AROADBQP57FF2AEXAMPLE
Action: s3:CreateBucket
Resource: arn:aws:s3:::my-bucket
Context:
- aws:UserId=AROADBQP57FF2AEXAMPLE:BobsSession
- aws:PrincipalAccount=123456789012
- aws:PrincipalOrgId=o-example
- aws:PrincipalARN=arn:aws:iam::123456789012:role/Bob
- aws:MultiFactorAuthPresent=false
- aws:CurrentTime=...
- aws:EpochTime=...
- aws:SourceIp=...
- aws:PrincipalTag/dept=123
- aws:PrincipalTag/project=blue
- aws:RequestTag/dept=123
The next thing that happens in policy evaluation is essentially a matching process on the property bag. In this example, the statement matches the authorization context: the action is the same, the resource matches the “*” wildcard, the condition on aws:PrincipalTag matches the value blue, and so. This statement allows this authorization request:
{
"Effect": "Allow",
"Action": "s3:CreateBucket",
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/project": "blue"
}
}
}
Conditions
Conditions are defined by an operator, keys and values. Different values in brackets are treated as OR conditions, and different operators are treated as AND statements:
Special cases and “gotchas”
Operators for multi-valued keys
Question: What’s wrong with the condition in this policy?
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": "*",
"Condition": {
"ForAllValues:StringEquals": {
"aws:PrincipalTag/Team": "infrastructure"
}
}
Answer: The problem is the usage of the ForAllValues operator! When the key is absent from the authorization context (as with a role that is not tagged with the key “Team”), the condition evaluates to true. Formally speaking, this is because the empty set is a subset of all sets. “For all values in A x is true” is true if the group A is the empty set.
Recommended solution: Only use ForAllValues and ForAnyValue on multi-valued keys, such as aws:TagKeys. These keys are explicitly identified in the documentation as having the type “ArrayOfString”. Note: A single-valued key on which you want to accept multiple values, such as “aws:PrincipalTag/Team”: [“Research”, “Product”, “Marketing”], is still a single-value key and should not be used with ForAllValues/ForAnyValue. In general, it is advisable to always be aware of the empty subset behavior of ForAllValues and be very careful of using ForAllValues in an allow statement. You can read more about multi-valued keys in the AWS documentation.
Missing anonymous principal key
Question: What’s the issue with this bucket policy?
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:PutObject",
"Resource": "*",
"Condition": {
"ForAllValues:StringEquals": {
"aws:PrincipalArn": "arn:aws:iam::123456789012:role/MyRole"
}
}
}
Answer: There is a principal which does not have the key “aws:PrincipalArn”: the anonymous principal. This policy will grant the PutObject permission to the anonymous principal, giving public access to PutObject on the bucket.
Confused deputy protection
Question: If the condition in this resource-based bucket policy was missing, what would be the problem?
{
"Effect": "Allow",
"Principal": {
"Service": "cloudtrail.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"ArnEquals": {
"aws:SourceArn": "arn:aws:cloudtrail:region:accountid:trail/name"
}
}
}
Answer: It would be possible to use the CloudTrail service as a confused deputy! The AWS service principal “cloudtrail.amazonaws.com” writes CloudTrail logs. This service principal name is the same for all accounts. Without the SourceArn condition, a CloudTrail trail from another account would also have permissions to write to your bucket.
A new model for permission evaluation
Remember this well-known chart?
Image Source: AWS
Although an AWS classic, the chart doesn’t fully represent how AWS IAM evaluates permissions. And take note: That unassuming orange node (in the Resource-based policies column) hides a lot of complexity.
A new model
The presenters introduced a new mental model for understanding AWS IAM policy evaluation:
Image Source: Re:Invent Session IAM433
In addition to the familiar principals, this model introduces the new virtual boundary principal. This principal cannot be referenced directly in IAM policies or used directly; it exists as an internal representation of the principal, which is evaluated according to permissions boundary policies.
The rules:
- An explicit DENY always overrides an ALLOW
- An explicit ALLOW must exist at all nodes in the evaluation chain to be allowed
But the above chart doesn’t cover all the complexity; let’s introduce a resource policy:
Image Source: Re:Invent Session IAM433
These are the possible values in the “Principal” element of a policy. What does each mean and represent?
- All principals:T
- *
- Account principal:
- arn:aws:iam::111111111111:root
- 111111111111
- Role principal:
- arn:aws:iam::111111111111:role/MyRole
- Boundary principal:
- This is a virtual principal used internally in evaluation, it cannot be referred to directly in IAM policy language
- Session principal:
- arn:aws:sts::111111111111:assumed-role/MyRole/MyRoleSession
When the resource doesn’t have a resource policy, the behavior is the same as in the original evaluation chain: an explicit Allow must exist at all nodes.
Image Source: Re:Invent Session IAM433
Let’s go deeper on resource policies. What happens when the resource policy allows a principal? The action needs to be allowed by the service control policies and only by the policies in the evaluation chain that come after the principal allowed by the resource policy. Yes, this is the mind-bending part of the model. Luttrell and Peebles demonstrated with a few examples.
Consider this bucket policy, which may look familiar to many of you:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my_bucket/*"
}
The above policy actually allows the account principal. So, for the permission evaluation to reach an Allow outcome, the policy needs to be allowed in an SCP, in an identity-based policy, in the session policy and, if it exists, in the permissions boundary.
Image Source: Re:Invent Session IAM433
Question: What if the policy allows the role as the principal?:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/MyRole"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my_bucket/*"
}
Answer: In the above policy, the resource policy allows the role principal so we don’t need an Allow in the identity-based policy! This is what creates the familiar behavior in which an Allow in the resource policy is sufficient even in the absence of an Allow in an identity-based policy.
Image Source: Re:Invent Session IAM433
Question: What would change if the allowed principal were a role session?
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:sts::111111111111:assumed-role/MyRole/MySession"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my_bucket/*"
}
Answer: The resource policy directly allows the session principal, so we don’t need an Allow in the permissions boundary or session policy.
Image Source: Re:Invent Session IAM433
However, remember: Deny statements are still checked for all relevant policies, even when Allow statements aren’t. An explicit Deny in the permissions boundary, for example, would still lead to a Deny.
Image Source: Re:Invent Session IAM433
KMS and IAM
Key management service (KMS) and IAM are special: In both KMS and IAM, the identity-only path is closed — the resource policy must allow the action. So a role with the AdministratorAccess managed policy will be unable to access a KMS key that does not explicitly allow it access.
Image Source: Re:Invent Session IAM433
Conversely, an identity without an identity-based KMS permission is able to perform actions that the resource policy allows as long as the resource policy specifies the role or the role session as the principal.
Image Source: Re:Invent Session IAM433
A good example is the following statement, used in the aws/lambda KMS AWS-managed key:
{
"Sid": "Allow access through AWS Lambda for all principals in the account that are authorized to use AWS Lambda",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:CreateGrant",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:CallerAccount": "120252999260",
"kms:ViaService": "lambda.us-east-2.amazonaws.com"
}
}
}
The policy allows the action for principals in the same account that do not have KMS permissions, as long as the requests are made via AWS Lambda (this mechanism is called Forward Access Sessions (FAS) and was covered in a fascinating talk on FAS by AWS’s Colm MacCárthaigh).
The cross-account case
In the cross-account case, all permission gates must be passed through on both the identity and resource sides, regardless of which principal the resource policy grants permissions to.
Image Source: Re:Invent Session IAM433
IAM users
Lastly, IAM users. Usually, an IAM user does not have a session. It is, therefore, the principal, represented by the actual credentials performing the actions, that has the session; on the chart below, this entity appears to the right of the virtual boundary principal.
Image Source: Re:Invent Session IAM433
Authorization context: Principal evaluation
The actual principal in an IAM statement is represented by a unique id, such as (in the case of a role): AROADBQP57FF2AEXAMPLE. This role's unique ID is the identity against which the identity-based policies are evaluated. The boundary principal is virtual and cannot be directly referenced, and the session principal is represented by AROADBQP57FF2AEXAMPLE:BobsRoleSession.
Practical IAM policies
The principal field of an IAM policy is evaluated and internally represented by the unique identifiers described above. Understanding this, we can examine the differences between using the “Principal” field of an IAM policy and the aws:PrincipalArn condition key.
The first difference is deletion behavior. If the role is deleted, the friendly ARN representing the role will be replaced by the unique identifier. If the initial policy looks like this:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/MyRole"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my_bucket/*"
}
When you delete the role, the unique identifier will no longer be translated to a role ARN in the console:
{
"Effect": "Allow",
"Principal": {
"AWS": "AROADBQP57FF2AEXAMPLE"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my_bucket/*"
}
Conversely, the role named in the principal field must exist before you can save the policy. The role named in the aws:PrincipalArn condition does not have to exist.
If we want to create a resource policy that references a role that does not yet exist, this is one possible policy:
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:root"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my_bucket/*",
"Condition": {
"ArnEquals": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:role/MyRole"
}
}
}
Question: What is the difference in how the policy above and this policy will be evaluated?
{
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my_bucket/*",
"Condition": {
"ArnEquals": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:role/MyRole"
}
}
}
(Note: We changed the principal to “*”.)
Answer: Recall the new model, in which the first policy ("arn:aws:iam::111111111111:root") requires all principals to be allowed:
Image Source: Re:Invent Session IAM433
However, the second policy directly matches all principals, including the session principal, so in the same-account case does not require an allow in an identity-based policy, permissions boundary and session policy:
Image Source: Re:Invent Session IAM433
Review of cross-account access
Let’s review three different approaches to cross-account access.
Trust the entire account
Trust the entire account:
"Principal": {
"AWS": "arn:aws:iam::222222222222:root"
}
- Trusts the entire account, as an identity in the account that has the appropriate permissions can access the resource
- Does not cause an availability issue if a role is deleted or recreated, as the role ARN is not referenced in the policy, and the account principal being referenced is immutable
- Can be useful if you do not own the account you are granting access to, because you do not control the roles in that account
Trust a specific role
Trust a specific role:
"Principal": {
"AWS": "arn:aws:iam::222222222222:role/MyRole"
}
- Trusts a single role in the account, as its role principal is being directly granted access
- Can cause an availability issue if the role is recreated, as upon removal the role will be displayed by its unique identifier, and a new role with the same name will have a different unique identifier
- Can be useful if you own the account you are granting access to, as you both know the role principal you are referencing, and you control its permissions
Account trust in the principle field
Account trust in the principal field, PrincipalArn condition:
"Principal": {
"AWS": "arn:aws:iam::222222222222:root"
},
"Condition": {
"ArnEquals": {
"aws:PrincipalArn": "arn:aws:iam::222222222222:role/MyRole"
}
}
- Trusts a single role in the account, as the condition limits access to only the principal ARN referenced
- Does not cause an availability issue if the role is recreated, as the access check is being performed on the ARN level, and a new role recreated in the account with the same name would still have the same ARN
- Involves the risk that someone in that account can create a role with same name
- Can be useful if you own the account you are granting access to, as you are familiar with internal role naming and control role permissions
Question: Why is access denied when using this policy?
{
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:InstanceType": "t3.small"
}
}
}
Answer: The RunInstances API authorizes multiple resources and only one of those resources supports the ec2:InstanceType condition key.
The RunInstances action has multiple resources:
"arn:aws:ec2:*:*:image/*",
"arn:aws:ec2:*:*:network-interface/*",
"arn:aws:ec2:*:*:security-group/*",
"arn:aws:ec2:*:*:subnet/*",
"arn:aws:ec2:*:*:volume/*",
…
For each of these resources, the action is evaluated separately. All the evaluations must pass for the action to be allowed:
Image Source: Re:Invent Session IAM433
The action is allowed for the instance resource:
Principal: AROADBQP57FF2AEXAMPLE
Action: ec2:RunInstances
Resource: arn:aws:ec2:…:…:instance/i-123456
Context:
- aws:UserId=AROADBQP57FF2AEXAMPLE:BobsSession
- aws:PrincipalAccount=123456789012
- aws:PrincipalOrgId=o-example
- aws:PrincipalARN=arn:aws:iam::123456789012:role/Bob
- aws:MultiFactorAuthPresent=false
- aws:CurrentTime=...
- aws:EpochTime=...
- aws:SourceIp=...
- aws:PrincipalTag/dept=123
- aws:PrincipalTag/project=blue
- ec2:InstanceType=t3.small
However, it is not allowed for resources that do not have the ec2:InstanceType property, such as the subnet resource:
Principal: AROADBQP57FF2AEXAMPLE
Action: ec2:RunInstances
Resource: arn:aws:ec2:…:…:subnet/subnet-1
Context:
- aws:UserId=AROADBQP57FF2AEXAMPLE:BobsSession
- aws:PrincipalAccount=123456789012
- aws:PrincipalOrgId=o-example
- aws:PrincipalARN=arn:aws:iam::123456789012:role/Bob
- aws:MultiFactorAuthPresent=false
- aws:CurrentTime=...
- aws:EpochTime=...
- aws:SourceIp=...
- aws:PrincipalTag/dept=123
- aws:PrincipalTag/project=blue
- ec2:VPC=VPC-123
For the policy to work we can use the modifier “IfExists”, as in this policy:
{
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": "*",
"Condition": {
"StringEqualsIfExists": {
"ec2:InstanceType": "t3.small"
}
}
}
Note: While this form of policy is useful in this case, we must be very careful when using the IfExists modifier in “Allow” statements, as they can lead to accidentally overly wide permissions.
Alternatively, we can construct separate statements for different parts of the authorization flow:
{
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"ec2:InstanceType": "t3.small"
}
}
},
{
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*:*:image/*",
"arn:aws:ec2:*:*:network-interface/*",
"arn:aws:ec2:*:*:security-group/*",
"arn:aws:ec2:*:*:subnet/*",
"arn:aws:ec2:*:*:volume/*",
…
]
}
Using NotPrincipal
A common IAM misconfiguration is using NotPrincipal in an allow statement:
{
"Effect": "Allow",
"NotPrincipal": {
"AWS": "arn:aws:iam::111111111111:role/MyUnallowedRole"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}
First, this is a misconfiguration because the statement allows any AWS principal from any account to access the bucket except for MyUnallowedRole — this is essentially public access. Additionally, as per the permission model presented in the talk, even MyUnallowedRole will be allowed to access the resource because its session principal arn:aws:sts::111111111111:assumed-role/MyUnallowedRole/role-session-name also matches the NotPrincipal statement.
Question: What happens when we have NotPrincipal with a deny?
{
"Effect": "Deny",
"NotPrincipal": {
"AWS": "arn:aws:iam::111111111111:role/MyRole"
},
"Action": "*",
"Resource": "*"
}
Answer: At first glance, this seems fine. However, upon examining the mental model we see that the account principal, boundary principal and session principal also match the NotPrincipal condition, so will match the deny statement. Therefore, when MyRole performs the action, the action will be explicitly denied.
Working around the issue — Take 1: We might consider explicitly excluding all the principals involved:
{
"Effect": "Deny",
"NotPrincipal": {
"AWS": [
"arn:aws:iam::111111111111:root",
"arn:aws:iam::111111111111:role/MyRole",
"arn:aws:sts::111111111111:assumed-role/MyRole/MyRoleSession"
]
},
"Action": "*",
"Resource": "*"
}
However, if you have a permissions boundary attached, this approach will also not work! Why? The boundary principal has not been explicitly excluded (because we have no way of referring to it in the principal field), and the action will still be explicitly denied.
Working around the issue — Take 2: We can use the PrincipalArn condition key to build this policy:
{
"Effect": "Deny",
"Principal": "*",
"Action": "*",
"Resource": "*",
"Condition": {
"ArnNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::111111111111:role/Myrole"
}
}
}
This statement will match all involved principals, including the boundary principal. This is the recommended approach, and readers should use it and avoid using “NotPrincipal”.
Summary
In this tour-de-force of confounding conditions, double and triple negatives, and principals matched and unmatched, we learned about a more accurate model of how IAM evaluates permissions internally. The model allows us to attain a deeper understanding of AWS IAM permissions evaluations and why certain edge cases behave as they do.
Related Articles
- Cloud
- Cloud