AWS IAM and Set Theory: Inside Out IAM Policies

During Re:Invent and Re:Enforce, speakers on the topic of AWS IAM will often reference the PARC model as a method for writing IAM policies. PARC is an acronym for Principals, Actions, Resources, and Conditions, and is thought of like this: A Principal in AWS is allowed (or denied) the right to use these Actions, on these Resources, under these Conditions.

It makes sense to start writing policies with Actions, since we often phrase the question of granted permissions by asking “what do you want to do?” This encourages you to start by listing all the actions you want to take in AWS, and then later scratch your head trying to identify relevant resources and conditions.

The AWS Service Authorisation Reference, which lists all available IAM actions and permissions for all AWS services, is also structured in a way that encourages us to think using PARC. However, another method appears if you read between the lines. One that defies common sense, and yet manages to get the job done. Helpfully, it is a method that also provides a perfect excuse to write the third instalment in my series on AWS IAM and set theory.

Why use set theory with IAM?

A theoretical understanding of set operations is directly applicable to the practical application of permissions in AWS. In the first article we discovered the “IAM permissions space,” which is the set of all possible actions and permissions in AWS. We found that we could write useful and dynamic policies by utilising the Action and NotAction keywords in policy statements. Doing so allowed us to describe different subsets of the permissions space by using either inclusive or exclusive language. We discovered methods to apply set theory operations like unions, intersections, and compliments within our IAM policies.

In the second article we found that we could apply this new understanding of permissions to the construction of Service Control Policies across our organisation. And in a thrilling twist, we discovered the “resource” and “condition” spaces, which respectively, are the set of all AWS resources and the set of all conditions that apply to them.

In this article we are going to explore the relationship between the permissions and resource spaces, and discover a new “inside out” method for writing IAM policies.

What is the AWS resource space?

All resources in AWS are identified by an Amazon resource name, often abbreviated to ARN. The ARN contains information about the AWS account, region and service that the resource belongs to, as well as a unique identifier for a specific resource. The AWS resource space is the set of all ARNs.

When we are writing IAM policies, we are describing subsets of the resource space by including items in a policy’s Resource list. Just like we discovered with the permission space, we can choose to explicitly identify resources by listing their exact ARNs, or implicitly select larger subsets by using an asterisk * wildcard somewhere in the ARN structure. We can also use exclusive language to describe the compliment of a set of resources by using the NotResource key.

All resource types in in AWS have their own ARN pattern. If we wanted to describe the set of all EC2 instances for a particular region and AWS account, we could include an element like this in our policy:

...
  "Resource": [
      "arn:aws:ec2:ap-southeast-2:111122223333:instance/*"
    ],
...

Likewise, if we wanted to describe the set of all AWS resources to the exclusion of those EC2 instances, then we could us the NotResource key like so:

...
  "NotResource": [
      "arn:aws:ec2:ap-southeast-2:111122223333:instance/*"
    ],
...

The relationship between the permission and resource spaces

At a minimum, a single statement within an IAM policy contains the Effect, Action and Resource elements. As discussed, the Action and Resource elements contain a description of a set of IAM actions and a set of AWS resources.

You can think of an administrator in AWS as someone who has the capability to perform any action on any resource. They would have a policy like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["*"],
      "Resource": ["*"]
    }
  ]
}

We can restrict the set of IAM actions by choosing specific items to include the Action list, like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
          "s3:*",
          "ec2:*"
        ],
      "Resource": ["*"]
    }
  ]
}

If the administrator policy is saying, “allow all actions on all resources,” then we might think that this policy is saying:

“Allow any S3 or EC2 actions on all resources.”

This isn’t strictly correct. Not all resources are related to or affected by S3 or EC2 actions. For example, it’s doesn’t make sense to call the EC2 command “RunInstances” on an Amplify “Apps” resource. So perhaps our policy is trying to say this:

“Allow any S3 or EC2 actions on any S3 or EC2 resources.”

And indeed, the Service Authorisation Reference agrees with us:

All actions and resources that are included in one statement must be compatible with each other. If you specify a resource that is not valid for the action, any request to use that action fails, and the statement's Effect does not apply.

To think about this in a maths-y way, it would seem that our policy is enforcing some sort of relationship between the permissions space and the resource space. Choosing a subset of actions from the permissions space appears to limit the set of resources that are relevant to our policy. If we only allow EC2 actions, then necessarily only EC2 service resources will be compatible.

A map between the permission and resource spaces

In the first article we developed the idea that an IAM policy is a function that maps the permission space onto the Allow/Deny set. The function is an implementation of AWS' policy evaluation logic.

Based on our discussion so far, it might seem intuitive to think that an IAM policy is also a map between the permission space and the resource space. Since choosing a set of actions limits a set of compatible resources. But this is not correct!

Writing policies isn’t about listing actions or resources, it’s about the logic of granted permissions. The intention of a policy is to determine whether an IAM principal is allowed or denied the ability to perform a chosen action. This is done by application of AWS' policy evaluation logic through the function of an IAM policy.

Previously we had only considered subsets of the permissions space as the function’s input. We need to expand the definition of our function to accept multiple variables. Namely, IAM actions and AWS resources. The function will continue to map these inputs onto the Allow/Deny set.

AWS' policy evaluation logic as a function between the permission space and the Allow/Deny set

Functions of multiple variables

In mathematics, a multivariate function maps from a set of sets… to another set. Maths is a lot easier in 1 dimension. I’ll explain some more set theory, and then we can try this definition again.

Let A be some set of objects, and B another set of objects. You can then talk about ordered pairs (a, b), where a and b are elements of the sets A and B respectively. Now think about an arbitrary number of sets (A, B, C, …), and talk about ordered tuples (a, b, c, …) where each element is from its corresponding set.

If you go mad, you can then think of the set of ordered tuples for an arbitrary collection of sets. If you do this, you will have invented a thing called the Cartesian product. In this Wikipedia diagram, you can see the Cartesian product of a set containing elements {x,y,z} with the set containing elements {1,2,3}. In this example the product is a square shaped set containing elements like (y,3).

It is common to express the Cartesian product as AxB, which is pronounced “A cross B,” or “A crossed with B.”

An example of the Cartesian product

In mathematics a multivariate function is a map from the Cartesian product of a collection of sets, to another set.

If we expand our understanding of AWS' policy evaluation logic be a function that takes both actions and resources as input, then the domain of our function becomes the Cartesian product of the permissions space with the AWS resource space. That is, the set of all ordered pairs of actions and resources.

When we’re writing policies, we’re describing subsets of this Cartesian product, and mapping them onto the Allow/Deny set.

The Cartesian product of the permission and resource spaces mapped onto the Allow/Deny set

Let’s take a moment to re-consider our previous examples. This snippet is describing the set of all EC2 and S3 actions crossed with the set of all AWS resources. The policy evaluation logic then maps this subset of the Cartesian product onto the Allow/Deny set.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
          "s3:*",
          "ec2:*"
        ],
      "Resource": ["*"]
    }
  ]
}

This subset of the permission cross resource space contains many incompatible pairs of actions and resources. The policy’s effect will not apply to any such pairs. We can think of them as being implicitly mapped to “Deny” by the policy evaluation logic.

As always, we could choose to write logically broken policies, and the evaluation logic will still return a valid result. For example:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
          "s3:*"
        ],
      "Resource": [
          "arn:aws:amplify:*:*:*"
        ]
    }
  ]
}

This is describing the set of all S3 actions crossed with the set of all Amplify resources. None of these actions and resources are compatible, so no allowable action has been described, and the policy fails to have an effect.

Analysing the Cartesian product

Suppose you wanted to create EC2 instances, with little care to place any constraints on the details. A perfectly acceptable PARC policy would look like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
          "ec2:RunInstances"
        ],
      "Resource": [
          "*"
        ]
    }
  ]
}

Based on our new understanding of policy evaluation logic, we can deduce the subset of action and resource pairs being described in this policy (by reading the service authorisation reference for EC2):

Actions, Resource types
RunInstances, image*
RunInstances, instance*
RunInstances, network-interface*
RunInstances, security-group*
RunInstances, subnet*
RunInstances, capacity-reservation
RunInstances, elastic-gpu
RunInstances, elastic-inference
RunInstances, group
RunInstances, key-pair
RunInstances, launch-template
RunInstances, license-configuration
RunInstances, placement-group
RunInstances, snapshot
RunInstances, volume
RunInstances, NaN (no resource)

What this tells us is that, when you call the RunInstances API, you are potentially affecting a number of different resources. The policy you are using needs to grant you permission to act on those resources.

If we were to add more actions to this policy, then the set of affected resources could potentially increase in size. For example, by adding ec2:CreateSecurityGroup to our list of actions, the policy would also be describing the ordered pairs (CreatSecurityGroup, security-group) and (CreateSecurityGroup, vpc).

What resources are relevant to your IAM policies?

The relationship between the IAM permission space and the AWS resource space is made more complex by the fact that, in order to perform some actions, you must also have permission to affect a specific set of resources. You might have noticed in the list of resources affected by the RunInstances action, that some are marked with an asterisk.

Actions, Resource types
RunInstances, image*
RunInstances, instance*
RunInstances, network-interface*
RunInstances, security-group*
RunInstances, subnet*

The authorisation reference is telling us that at a minimum we must have permission to affect this set of resources if we want to use the RunInstances action.

In our policy we had granted ourselves access to all AWS resources using the asterisk in the list of resources. If we wanted to further pursue the PARC method, we would need to specifically identify these required resources and others that we are attempting to interact with when we call RunInstances.

What are inside out IAM policies?

In contrast to the PARC method for writing policies, the Inside Out method asks that you describe only a set of resources using the Resource or NotResource keys (the latter of which is not recommended), and only include broad subsets in the Action or NotAction keys (or just reference the whole permissions space).

For example, this policy snippet provides enough permissions to launch an EC2 instance and attach a volume:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "*"
            ],
            "Resource": [
                "arn:aws:ec2:*:*:instance/*",
                "arn:aws:ec2:*:*:network-interface/*",
                "arn:aws:ec2:*:*:subnet/*",
                "arn:aws:ec2:*:*:security-group/*",
                "arn:aws:ec2:*::image/*",
                "arn:aws:ec2:*:*:volume/*"
            ]
        }
    ]
}

Applying our understanding of the Cartesian product, this policy is asking for the set of all actions crossed with the set of all instance, network-interface, subnet, security-group, image, and volume resources. Only a small subset of EC2 actions are compatible with these resources, and RunInstances is an example of one such action.

Nifty.

It turns out that this policy also lets you terminate instances, create and delete tags (which is very handy), attach and detach volumes, disassociate a route table from a subnet, and much more!

… Hang on a second… disassociating a route table doesn’t sound like an action we wanted to allow. Just how big is this “small subset” of compatible EC2 actions?

Let’s take another moment to consider what this inside out policy is trying to say:

“Allow all actions for this set of resources.”

We can calculate the relevant subset of IAM actions by the following steps:

  1. Take the Cartesian product of the IAM permission space with this list of AWS resources,

  2. Filter out all the incompatible action resource pairs,

  3. For each action that we have found, check the set of required resources associated with that action. If those resources are a subset of the resources listed in our policy, then we will have sufficient permissions to perform the action. If a required resource is missing, then we cannot perform the action,

  4. Finally, there are actions for which no specific resource is required, but instead are optional. If any resources in our list are present for such an action, then that action will be available to us.

I don’t think this is a task that is possible to complete by hand. Indeed, figuring out the complete Cartesian product isn’t a worthwhile activity, since the majority of its elements will be irrelevant.

However, I was able to produce a Python script that takes a list of resources as an input, compares it against data from the AWS service authorisation reference documentation, and returns a set of allowed actions.

I won’t include the details, but in brief I made use of the Pandas library, which has a useful read_htmlfunction. You can use this to extract tabulated data from a webpage, which is very handy for extracting information from the AWS service authorisation reference.

Running the 6 resources from our Inside Out policy through the script returned 88 actions!

Perhaps this doesn’t provide a good first impression of inside out policies, since granting access to 88 actions when we only wanted to allow RunInstances is not a good implementation of “least privilege.” But, there are some useful inside out policies that benefit from this ability to describe large subsets of the permissions cross resource space with fewer lines of code.

Practical inside out policies

It is left as an exercise for the reader to consider all the possible combinations of Action, NotAction, Resource, and NotResource, against all combinations of explicit or implicit descriptions of sets in the permission and resource spaces (which is a task I recommend completing, because it’s quite fun).

In our last article we discussed SCPs, and learnt that the AWS Control Tower service uses SCPs to protect its own resources from being tampered with. It does so with a policy like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalARN": "arn:aws:iam::*:role/AWSControlTowerExecution"
        }
      },
      "Action": [
        "s3:DeleteBucket"
      ],
      "Resource": [
        "arn:aws:s3:::aws-controltower*"
      ],
      "Effect": "Deny",
      "Sid": "GRAUDITBUCKETDELETIONPROHIBITED"
    }
  ]
}

Which says, “deny everyone the S3 action DeleteBucket on any bucket that starts with the name aws-controltower*, with the exception of principals using the AWSControlTowerExecution role." This SCP does an excellent job at protecting some S3 buckets from being deleted. It doesn’t stop those buckets from being modified, or stop someone from creating another bucket called aws-controltower-foobarbaz.

If we calculate the set of S3 actions that are compatible with the S3 bucket resource, we find a set of 50 actions:

CreateBucket
DeleteBucket
DeleteBucketPolicy
DeleteBucketWebsite
GetAccelerateConfiguration
GetAnalyticsConfiguration
GetBucketAcl
GetBucketCORS
GetBucketLocation
GetBucketLogging
GetBucketNotification
GetBucketObjectLockConfiguration
GetBucketOwnershipControls
GetBucketPolicy
GetBucketPolicyStatus

GetBucketPublicAccessBlock
GetBucketRequestPayment
GetBucketTagging
GetBucketVersioning
GetBucketWebsite
GetEncryptionConfiguration
GetIntelligentTieringConfiguration
GetInventoryConfiguration
GetLifecycleConfiguration
GetMetricsConfiguration
GetReplicationConfiguration
ListBucket
ListBucketMultipartUploads
ListBucketVersions

PutAccelerateConfiguration
PutAnalyticsConfiguration
PutBucketAcl

PutBucketCORS
PutBucketLogging
PutBucketNotification
PutBucketObjectLockConfiguration
PutBucketOwnershipControls
PutBucketPolicy
PutBucketPublicAccessBlock
PutBucketRequestPayment
PutBucketTagging
PutBucketVersioning
PutBucketWebsite
PutEncryptionConfiguration
PutIntelligentTieringConfiguration
PutInventoryConfiguration
PutLifecycleConfiguration
PutMetricsConfiguration
PutReplicationConfiguration

Now, suppose we wanted to modify this SCP so that only the ListBucket action was available. We could utilise our inside out policy method by flipping the Action keyword to NotAction, like so:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalARN": "arn:aws:iam::*:role/AWSControlTowerExecution"
        }
      },
      "NotAction": [
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::aws-controltower*"
      ],
      "Effect": "Deny",
      "Sid": "GRAUDITBUCKETDELETIONPROHIBITED"
    }
  ]
}

On its own, the NotAction set in this policy is describing the whole IAM permission space to the exclusion of s3:ListBucket. But, when we take the Cartesian product of the permission space against the set of S3 buckets whose name starts with aws-controltower, the actual list of denied IAM actions shrinks down to those 50 that are compatible with S3 bucket resources (minus ListBucket).

Limitations of inside out policies

The most troublesome issue with inside out policies is dealing with actions in the permissions space for which no resources are required or optional. For example, in the set of EC2 actions, the majority of “list” based actions that start with the word Describe are not compatible with any EC2 resources, like the DescribeInstances action.

These actions cause problems for inside out policies, because if we include anything other than an asterisk * in our Resource list, then we will not receive permission for any “resource-less” action. Take a moment to consider that this cuts both ways: if we include a resource-less action in our policy, then we cannot include any specific resources in that same policy chunk and must use an asterisk.

Imagine that you are building some resources using infrastructure as code. It would be expected that you would want to query the resources you have just deployed, and then utilise that data to inform other parts of your infrastructure. For example, deploying a VPC and then populating it with subnets. An inside out policy that acted on vpc and subnet resources wouldn’t grant you the correct permissions to run your code. This is because the DescribeVPCs and DescribeSubnets actions are not compatible with any resources. Fortunately, you always have the ability to include a separate PARC based statement that grants the Read Only permissions required to complete your task.

When to use inside out policies?

The goal of these articles is to expand the set of tools you have to write better IAM policies. Understanding the connection between actions and resources adds another layer of nuance to the types of decisions you can make about the logic of granted permissions.

If you are an engineer or developer, you might find effective ways to apply inside out policies to principals interacting with S3 bucket objects or Dynamo DB tables.

If you are a platform administrator, you might follow the example SCP above and find novel ways to protect large sets of resources from larger sets of actions.

Ultimately we want to craft policies that grant just the right amount of permissions to perform a required set of actions. Utilising the Action and Resource keys in our policies alone is not enough to achieve this goal. To reach least privilege we will need to dive deep into the most interesting part of the IAM puzzle: conditions. A topic we will tackle in the next IAM and set theory article.

02/17/2025