AWS IAM Permissions Boundaries Are Incomplete Without Propagation

Ben Kehoe
4 min readOct 29, 2021

--

Permissions boundaries are great in concept: an administrator can place restrictions on what IAM principals can possibly do, while letting developers determine the actual least privilege policy that principals should have — and this includes the principal for the developers themselves. But I don’t believe they are useful in their current form.

We’ll set aside for this article the way that permissions boundaries can be circumvented for IAM roles; read this article for details on that.

In a serverful application running mostly on something like EC2, ECS, or Kubernetes is probably not going to need a ton of different IAM roles for their application. If the number of roles is few and rarely changing, roles could be provisioned by an administrator, with permissions boundaries, and then the developer would get permissions for IAM.AttachRolePolicy permissions but not IAM.CreateRole.

In serverless development, each Lambda function, Step Functions state machine, API Gateway integration, etc. should all have their own IAM role, so that their policies can be narrowly scoped to just what’s needed for that individual resource. Indeed, if you’re using SAM, an AWS::Serverless::Function implicitly creates an IAM role. Trying to put an administrator in that loop would be a nightmare; either the developer is trying to reuse a small number of roles in a bunch of different places, shredding least privilege, or as they iterate on their architecture they are constantly asking for new roles.

What we need is for the developers to be able to create IAM roles themselves. However, if we want to use permissions boundaries we can’t just grant iam:CreateRole; if a developer’s permissions boundary denies them access to EC2 (we should all aspire to this 😁), they can just create a role that does have access to EC2, and either assume it directly or assign it to a Lambda function and then invoke that function.

Permissions boundaries has a solution for this: the iam:PermissionsBoundary context key. You can create a policy for a developer that requires they put a specific permissions boundary on any role they create:

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateOrChangeOnlyWithBoundary",
"Effect": "Allow",
"Action": "iam:CreateRole",
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/MyBoundaryPolicy"
}
}
}
]
}

Note: this only shows iam:CreateRole for simplicity. All operations on a role, such as iam:AttachRolePolicy, include the iam:PermissionsBoundary context key, so you can limit, for example, the ability to attach policies to only roles with a specific permissions boundary.

The permissions boundary used in this condition could be the same or different from the permissions boundary on the developer.

This looks great, until you realize that this is only an IAM policy. The actual CreateRole API call needs to have the permissions boundary policy ARN explicitly specified for the call to be allowed. As it is designed today, the developer needs to explicitly understand and participate in the propagation of permissions boundaries.

This bad. First, the permissions boundary ARN from the policy needs to be communicated to the developer, who needs to remember it. And then everywhere that they create infrastructure, they need to pass that value in.

It gets worse. A given developer has a permissions boundary that the policy enforces they must use with created roles, but a different developer might have a policy enforcing a different permissions boundary. So the boundary now becomes a parameter.

So every CloudFormation template, every CDK program, every infrastructure setup script, needs to take a permissions boundary policy ARN as an optional parameter — because some developers or CI/CD pipelines may not be required to specify a permissions boundary — and everybody needs to be aware of the value they are required or not to use.

This is a giant headache for everyone involved, and in general I don’t think it’s worth the trouble.

Now, imagine that when you put a permissions boundary on a principal, you could also specify a transitive permissions boundary. Like transitive role session tags and the source identity property, this boundary would be placed on all created principals without it having to be specified in the API call — and probably without being able to be overridden (I’m less sure about this).

With a transitive permissions boundary, developers no longer need to be active participants in permissions boundary propagation. Their CloudFormation templates define their infrastructure without reference to permissions boundaries — note that a transitive permissions boundary may cause that infrastructure to fail to have the permissions defined in the template, but that’s exactly the point of permissions boundaries. Different developers may have different transitive boundaries, and a CI/CD pipeline may have none. All of it just works in the background.

Until something like transitive permissions boundaries exists, permissions boundaries will remain mostly unusable for organizations where developers need to create IAM principals as part of their normal software development process. As always, if you’ve got questions, you can find me on Twitter.

--

--