It’s a new beginning! Ermetic is now Tenable Cloud Security.

Diving Deeply into IAM Policy Evaluation – Highlights from AWS re:Inforce IAM433

One of the most talked-about sessions at AWS re:Inforce, and my favorite, was IAM433, on AWS IAM’s internal evaluation mechanisms

Noam Dahan By Noam Dahan
Diving Deeply into IAM Policy Evaluation – Highlights from AWS re:Inforce IAM433

At this year's AWS re:Inforce, session IAM433, AWS Sr. Solutions Architect Matt Luttrell and AWS Sr. Software Engineer for IAM Access Analyzer Dan Peebles delved into some of AWS IAM’s most arcane edge cases – 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.

In this article we review the content covered for the benefit of those who couldn’t attend and as a memory refresh for those who did. Many thanks to Matt and Dan for the excellent session and generously sharing their slides, 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
      Matt and Dan reminded us that, strictly speaking, IAM roles do not make requests. Role sessions, represented by temporary credentials for the roles, make the requests.
  • 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.
  • 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:

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

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?

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:

This model introduces, in addition to the familiar principals, 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:

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.

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. Dan and Matt 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.

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.

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.

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.

KMS and IAM

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.

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.

A good example is this 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 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 to which principal the resource policy grants permissions.

Lastly, IAM users

An IAM user usually 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.

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:

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:

Review of cross-account access

Let’s review three different approaches to cross-account access.

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

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

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

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.

 

Skip to content