Skip to main content

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.

Understanding two useful IAM Policies

Have you read the AWS Managed ReadOnlyAccess policy? It contains a single statement that lists over 1,600 distinct actions. This is an excellent demonstration of fine grained control in AWS IAM: Start with a blank page and list all the things a user is allowed to do. If an action is not in the list, then we’re not allowed to do it. Simple.

The top of the policy looks like this, and it continues for 1,600 lines.

ReadOnlyAccess

{
    "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.

The act of choosing “Action” versus “NotAction” as a method for deciding what is in or out of a policy is really powerful.

A lightbulb went off in my head when I first saw the PowerUserAccess policy: we could use set theory to write IAM policies!

Understanding identity and permissions with mathematics

Are you wondering what set theory is? It’s a wonderfully deep topic in mathematics. Fortunately the basics are easy to understand, and we won’t get into any serious theory in this article. We will learn the fundamentals before using our new tools to create interesting IAM policies.

Let’s talk about Venn diagrams. Happy little overlapping circles that describe the relationship between two or more groups of things. If I had to completely undermine an entire field of study, I would say that set theory is mostly concerned with pointing at one or more sections in a Venn diagram and asking, “How do you describe that?”

In contrast, the third paragraph of set theory’s Wikipedia article states

“Set theory is commonly employed as a foundational system for the whole of mathematics”

So, it’s a pretty important theory.

A “set” in set theory can be thought of as a collection of objects. You create a set by describing its contents.

There are all sorts of sets that contain numbers. It’s more fun to think about other silly sets  like the set of socks in your house, and how it is made up of two subsets: wearable socks and the set of socks that have been destroyed by your dog.

Drawing circles on a piece of paper, like a Venn diagram, is a natural way to understand sets. Imagine that each circle contains a collection of objects. Draw two circles, call one A and the other B, and then try to describe the entire piece of paper in terms of what is in A, B, or neither. With enough variations, you’ll get a chart like this:

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.

How actions are organised in IAM

Consider the set of all possible actions in AWS. All actions for all services. Let’s call this set the “IAM Permission Space.” Why? It sounds cool. But also because this set is large and an easy place to get lost in. We could draw part of the IAM Permission Space like this:

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.

Understanding AWS IAM policy evaluation logic

We need to talk about functions. I want us to develop a strong conceptual understanding of IAM’s policy evaluation logic. It is the key that unlocks the relationship between set theory and IAM.

If you’re reading this, it is probably the case that you’re some sort of engineer, developer, or security person who is used to thinking of functions as objects you create in code that have inputs and outputs.

In mathematics a function describes a map between two sets.

Consider sets A and B. Then a function, f: A → B, is a rule that assigns each value in A to a value in B. Functions do not have to map distinct values in A to distinct values in B. However, functions are not allowed to map a single value in A to multiple values in B.

In this diagram, the function f can send two stars in A to a single star in B. But it cannot send the single circle in A to different circles in B.

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

As we continue our investigations applying set theory to the IAM Permission Space, you will have to keep in mind that we are analysing two sets. First, the description of actions. Second, the evaluation of those actions into the Allow/Deny set.

When we talk about the complement of a set, we’re not only thinking about the complementary set of actions. We’re also thinking about whether the functional evaluation is complementary.

When we go on to talk about unions and intersections of sets, we will see that we’re actually more interested in the mapping of those subsets to the Allow/Deny set.

Complementary sets: defining the opposite set of IAM actions

Earlier we saw a method to describe the complement of a set by replacing a policy’s “Action” key with “NotAction.” If we had a policy statement like “Allow all actions in this list,” then the complement in the permission space would be “Allow all actions not in this list.”

It’s important to note that flipping the statement’s “Effect” parameter (i.e. Allow to Deny) would not yield the set’s complement. By default the policy evaluation function maps all actions to deny. If a policy had a single statement that said “Deny all actions in this list,” then all actions in the permission space would be mapped to “Deny.” Additionally, if a policy had a single statement “Deny all actions not in this list,” then it would also map all actions to “Deny.” These sorts of statements still have their use, so keep them in mind for later.

Another method for describing a set’s complement would be to list all AWS services to the exclusion of those in the initial list. For example, let contain some list of actions, and let B contain all the actions not in A. Then “Allow all actions in B” would describe the complement of “Allow all actions in A” The problem here is that manually identifying the elements of B could be a big job. For example, the complementary action list for

["ec2:*"] would contain a reference for each of the 200+ other service prefixes in the IAM Permission Space.

Complements are useful because they allow you to describe sets by flipping between inclusive or exclusive language. Here’s a silly analogy; you are at a party. You know that if you eat cake you’ll get sick. So you say to yourself “I can do anything at the party except eat cake.” This is a lot easier than listing all the things you could do at the party.

Unions of sets: adding IAM permissions together

Consider a policy that includes all EC2 and S3 actions. You could write a statement for each service, or you could list both services in a single statement’s action list, like so:

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

Intersections of sets: finding common IAM actions

When describing set complements earlier we ran into a problem. Manually calculating the complementary set of actions for any given list is a complex task. The same goes for set intersections in the IAM Permission Space. Given two sets, A and B, there is no neat tool that yields a list of actions in their intersection.

Furthermore, the majority of sets that we have looked at in this article are actually “disjoint” sets that share no elements. This is because actions that begin with the service prefix ec2 can’t possibly be in the same set as those that start with the s3 prefix. Usually, the intersection of disjoint sets creates a special case set called the “empty” set, which contains no elements.

At this point, I must admit our approach to set theory hasn’t been particularly rigorous. It’s more used as a vehicle to aid our conceptual understanding of a problem. With that spirit in mind we are going to wave our hands and gloss over the actual details of what the empty set is and how it works in set theory. It’s a bit complicated.

What does it mean for a set to be empty in the IAM Permission Space? It would be a list that contains no actions. If we were to apply our policy evaluation function to the empty set (and this is where the hand waving intensifies, both on a mathematical and an AWS level) the result would be that all actions in the permission space are mapped to “Deny.”

Let’s try to describe the intersection of two disjoint sets in the permission space in words: If we only want to allow actions that appear in both set and set B, but no such element exists, then logically we can allow no actions to the principal. Therefore all actions are denied.

The hand waving is done for now!

Let’s consider sets in the permission space that actually do share elements in common. We can construct a policy that only grants permission to those common elements.

Let and B be sets of the IAM Permissions Space, such that the intersection of and B is not empty.

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": "*"
        }
    ]
}

A new tool to understand IAM Policies

In this blog post we have explored IAM policies through the lens of set theory, in order to write scalable, powerful IAM policies more easily. We invented the IAM Permission Space, which is the set of all IAM actions, and began describing its contents. We learned about the IAM service’s policy evaluation logic, and how policies are functions that map elements of the IAM Permission Space into the Allow/Deny set. We learned that it is possible to describe set complements, unions and intersections using IAM policies, and we considered how we could use these tools to alter the effects of all the policies we write, using ReadOnlyAccess and PowerUserAccess as examples.

The irony of calling this a “new tool” is that set theory is well over 100 years old. We stand on the shoulders of giants.

If you find yourself managing an IAM policy that constantly requires updating to allow human users access to the correct services in your lower environments (i.e. sandbox/test), consider how the methods described above can be applied to the PowerUserAccess policy to allow or deny a broad range of services with fewer lines of code.

In the next part of this series we will learn about Service Control Policies and Permission Boundary Policies, and we’ll see how our new set theory mindset can help us understand their role in AWS IAM.

Acknowledgments

The following people contributed greatly to the quality of this piece, and I am grateful for their engagement and enthusiasm: Sarah Pelham, Paul Gear, Arjen Schwarz, and Ronan O’Brien.