Do you and your colleagues have trouble keeping your IAM policies up to date? It is possible to write powerful policies that will grow with your changing requirements. We just need a little help from a 100-year-old maths topic.
This is an intermediate-level exploration of AWS IAM which proposes new ways to understand and write IAM policies. To get the most out of this article you should be familiar with the core concepts of AWS IAM:
- roles,
- principals,
- policies and their elements, and
- be happy that AWS’ policy evaluation logic diagram exists (even if it is hard to follow).
We will also discuss several mathematical concepts relating to set theory. I try my best to explain these sections as clearly and concisely as possible.
This article is planned as the first instalment in a series. We will continue by investigating Service Control Policies, Permission Boundaries, and IAM policies’ condition and resource keys. I hope you will join me for this journey, and I look forward to discussing these topics with you either in person or online.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"a4b:Get*",
"a4b:List*",
"a4b:Search*",
...
}
On the other hand, have you seen the AWS Managed PowerUserAccess policy? Check it out:
PowerUserAccess
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"NotAction": [
"iam:*",
"organizations:*",
"account:*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"iam:CreateServiceLinkedRole",
"iam:DeleteServiceLinkedRole",
"iam:ListRoles",
"organizations:DescribeOrganization",
"account:ListRegions"
],
"Resource": "*"
}
]
}
The entire snippet is only 25 lines long, yet it allows (almost) all the 1,600 actions of ReadOnlyAccess and grants you all sorts of Write access as well! It’s quite remarkable, and understanding how the policy works will open a door to a new approach for creating powerful IAM policies.
Both ReadOnlyAccess and PowerUserAccess are examples of polices that use “Allow” statements. This is what the "Effect": "Allow"
key on line 5 of both policies does (and again on line 14 in the PowerUserAccess policy).
An important difference between the policies occurs on line 6. Notice that one states Action
and the other NotAction?
The ReadOnlyAccess policy effectively says “allow this set of actions.” If we wanted to diagram this statement we could do so like this. Here the rectangle contains all actions in AWS, and the coloured circle contains those described by the policy.
The interesting thing about the PowerUserAccess policy is that the core statement effectively says “allow not this set of actions,” which is the effect of the “NotAction” keyword. We could draw that statement like this. Here the circle contains all IAM, Organisations and Account actions, which are not allowed. The rest of diagram is coloured in to indicate that every other action in AWS is allowed.
“Set theory is commonly employed as a foundational system for the whole of mathematics”
The big concepts are these:
If A and B are sets that contain items, then
- The complement of A is the set of items not in A
- The union of A and B is the set of items in either A or B, and
- The intersection of A and B is the set of items in both A and B
Let’s identify some sets of actions in IAM and learn how policies are used to describe them. Then we can investigate their complements, unions, and intersections.
Actions in IAM are grouped by service and prefixed with a word or phrase, like account, cloud9, ec2, iam, and so on. You can review the IAM actions for each service in the AWS documentation. On its own the IAM Permission Space is effectively a big list of strings, containing a finite number of actions.
We describe subsets of the IAM permission space with the Action and NotAction key words.
So far we have seen examples of sets of actions described explicitly by their presence in a list, or implicitly with the use of wildcards. For example, “Action”: [ec2:*] is the set of all EC2 actions.
We could start thinking about complements, unions and intersections in the IAM Permission Space, but using IAM is about more than listing actions. It is about the logic of granted permissions. It is about describing whether an IAM principal is allowed or denied the ability to perform an action.
Remember a little while ago when I said we wouldn’t get too deep into the maths? I lied.
So here’s an important concept: Policies in IAM are functions that map between the IAM Permission Space and the Allow/Deny set.
The Allow/Deny set contains two values: ["Allow", "Deny"].
The function considers all actions described by a policy and assigns them to either “Allow” or “Deny” but not both. We could write logically broken policies that both allow and deny a chosen action, but the function will still work as expected. Later on we’ll see that this logic is generalised to evaluate all policies that act on an IAM principal.
The policy evaluation logic grants us the ability to colour in our diagrams. In the example below, the coloured circle represents the set of allowed actions for some policy. The star represents an action being mapped to “Allow” in the Allow/Deny set. The circle is outside the set of allowed actions, and subsequently gets mapped to “Deny.”
In a policy, we describe a set of IAM actions using the “Action” or “NotAction” key. Then we map those actions to the Allow/Deny set using the “Effect” key, which is either set to Allow or Deny.
This policy allows EC2 actions on line 5, and denies S3 actions on line 12.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:*",
],
"Resource": "*"
},
{
"Effect": "Deny",
"Action": [
"s3:*",
],
"Resource": "*"
}
]
}
This diagram represents the previous code snippet. The set of ec2:*
actions are allowed, and so the contents of the green circle are mapped to “Allow.” The set of s3:*
actions are explicitly denied, so the contents of the red circle are mapped to “Deny.” Everything else in the permission space is also mapped to “Deny.”
...
"Action": [
"ec2:*",
"s3:*"
],
...
Earlier I mentioned that the policy evaluation function considers all policies that act on an IAM principal. This means that we could construct two separate policies, one each for EC2 and S3, and attaching both policies to an IAM principal would grant access to the union of actions in both services.
Here’s a useful example of set unions: The PowerUserAccess policy is quite restrictive in the IAM service. You could consider attaching both PowerUserAccess and ReadOnlyAccess to an IAM role, which would grant that principal access to actions that fall into the union of those two policies, meaning the role would have easier read access to IAM.
At the start of this section we admitted that there often won’t be a simple way to list the elements in the intersection of A and B. But recall from the “Understanding AWS IAM policy evaluation logic” section that in the case of intersections, we are actually more interested in how the permission space ends up being mapped onto the Allow/Deny set. It turns out that there are several ways to write a policy that will map the intersection of two sets to “Allow” and everything else to “Deny.” I will describe one method. See whether you can work out a second.
We know that we need at least one "Effect": "Allow"
statement in our policy. Let’s write a statement that allows all actions in A. Then, we know that we do not want to allow elements of A that are not in B. Therefore, we should add a second statement that denies all actions that aren’t in B. Making a policy that looks like this:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
A
],
"Resource": "*"
},
{
"Effect": "Deny",
"NotAction": [
B
],
"Resource": "*"
}
]
}
Here is a complementary diagram:
The circle A is coloured in orange to indicate that we will “Allow all actions in A.” Everything that is not in the circle B is struck through to represent the statement “Deny all actions not in B.” The result is that the only section of the diagram where permissions will map to “Allow” is precisely the intersection of A and B.
Suppose you wanted an IAM principal to have read only access for actions with the ec2
service prefix. You can achieve that by first attaching the ReadOnlyAccess policy to the principal, and then a second policy that denies all actions not in [ ec2:* ]
as demonstrated below.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"NotAction": [
"ec2:*",
],
"Resource": "*"
}
]
}