Safely delegating role creation in AWS
AWS IAM permission boundaries allow you to safely delegate user and role creation permissions
We’ve recently used AWS permission boundaries over on the Punk Security CTF and they are a safety mechanism that few people understand how to leverage.
In our CTF, players have to attack our AWS account to find a “flag”. To do this, and prevent them from seeing other people’s flags, we needed to provide each player with their very own AWS role to assume. This needed to be done at the time they launched the challenge, and the challenges ran in ECS, so we needed to allow the ECS challenge container to create the players’ IAM role. Sounds simple enough, but it’s a security nightmare.
We can use AWS permission boundaries to prevent our IAM role-creating ECS container from creating new super-privileged roles!
AWS Permission Boundaries is a feature that allows you to define the maximum permissions that can be granted to a user or role. They act as a “boundary” that restricts what actions can be performed by a user or role within your AWS environment.
Let’s take a look at our implementation, which is via Terraform as we find it slightly more readable than native IAM.
Step 1. Create your permission boundary
In our game, we wanted the player to have a role which can access AWS Route53 to view DNS records.
We create our permission boundary to this effect:
data "aws_iam_policy_document" "per_challenge_role_permission_boundary" {
statement {
effect = "Allow"
actions = [ "route53:GetHostedZone", "route53:ListHostedZones", "route53:ListResourceRecordSets" ]
resources = ["*"]
}
}
This IAM statement is used to define an IAM policy, which we will use as the permission boundary.
This policy doesn’t actually let the user or role do anything, it just limits them.
Step 2. Let our container create roles, but only with the boundary
So this is the clever bit.
Our container needs to create roles, so we let it do that BUT only if it applies the boundary too.
data "aws_iam_policy_document" "allow-setup-to-create-roles" {
statement {
effect = "Allow"
actions = [ "iam:CreateRole" ]
resources = ["*"]
condition {
# created role must have permission boundary
test = "ForAllValues:StringEquals"
variable = "iam:PermissionsBoundary"
values = [aws_iam_policy.per_challenge_role_permission_boundary.arn]
}
}
Now if this container tries to create a role without the boundary, it will not be able to.
If it creates a role with lots of extra permissions, those permissions simply won’t work!
Possible use cases?
The traditional use case for permission boundaries is the delegation of responsibility from the full AWS admin to another user, such as a team leader or IT service desk.
How do you allow one user to create another without them being able to elevate their own privileges? With permission boundaries forced on any users or roles they create.
As we move to more cloud-native services, where applications are tightly coupled with the cloud service itself, we will see more ephemeral roles being created to fill the niche.
In our CTF, we needed to create a thing and allow exactly one user to interact with it. Not once, but potentially thousands of times and on-demand within about 10 seconds. This is a problem solved by automation so let’s get a lambda to do it (or a container-hosted setup script in our case) but how do you keep the Lambda in check?
If you do not keep that Lambda in check, then anyone with permission to edit the Lambda now has full administrative access to your AWS account. This is a big problem, and one we observe all the time. Luckily AWS provides mechanisms and controls to ensure you can mitigate these risks.