Prevent Privilege Escalation with IAM Permissions Boundary: A Practical Guide
Published: 2022-12-30 10:03:50

AWS provides great documentation to use IAM [policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html) and [permissions boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html). But there is not a straight through example available how to implement it for a common situation. In this blog post, I’ll describe how to use boundary policies in the following situation: we want to give our developers a lot of freedom in a development AWS account, but some things must be blocked and remain blocked for them.

Before we dive in to the solution, let’s first discover what “privilege escalation” means. In many situations we want our users to be able to create IAM users, groups and/or roles. The problem is that our users can then create new users or roles with more permissions than they originally had. Or, they can update their own user, group membership and this way get more permissions.

According to the “hands-off” best practice, most production AWS accounts users have none or very limited permissions. All changes are deployed using an automated CI/CD pipeline. Users might have read only permissions, and some specific actions. But they certainly can’t create users or roles without using the CI/CD pipeline.

In this blog post we’re looking at a shared developer AWS account. Where developers can build and test their solutions rapidly and in small increments. In other words; we don’t want to ask them to write perfect CDK, Terraform or CloudFormation code and test & deploy it in slow pipelines every small change they make. The challenge is to find the right balance between security and a great developer experience.

With permissions boundary we can give developers access to do a lot of things, within certain boundaries. 

## Permissions Boundary

The effective permissions for a user or role are all attached policies including the effective permissions boundary. Take a look at the diagram below ([source](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html)).

![IAM Policies Evaluation Logic](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b34iq4thpqz7oisrvm4g.png)

Let’s work with an example. I have a user “Jeff” who is allowed to assume the role “Developer”. Attached to this role is a Managed Policy called “DeveloperAccess”. Ec2 and S3 are just example services developers use, the list is usually much longer.

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

Developers also get `iam:*` permissions. We would like our developers to be able to manage IAM users and roles for their co-workers, 3rd parties, or to use in their own applications. In case AWS launches new services or features, we would like the developers to use those permissions too. To block some services / actions, we could add a very simple permissions boundary:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AdministratorAccess",
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    },
    {
      "Sid": "BlockAccess",
      "Effect": "Deny",
      "Action": ["supportplans:*", "account:*", "billing:Modify*"],
      "Resource": "*"
    }
  ]
}
```

This will work fine, the user won’t get access to `supportplans:*`, `account:*` or `billing:Modify*`. But, the user has permissions to remove the permissions boundary, to update it, to create a new user without a permissions boundary, etc. 

So what we want more than that. We want to:

1. Allow developers to create new users and roles, but block the action without the a permissions boundary.
2. Allow developers to give themselves more permissions to manage new AWS services.
3. Block a list of services and actions, or regions etc, and make sure the developers cannot change this block list.
4. Deny AssumeRole on IAM roles other than the Developer role. We likely have more roles to manage their Developer account, and we don’t want our developers to use this role.
5. Prevent updating or deleting the Permissions Boundary.
6. Prevent deleting or replacing the Permissions Boundary on users and roles.

To fix that to the “DeveloperAccess” managed policy, I’ll create another managed policy and call it “DefaultBoundaryPolicy”. Take a look at the diagram and DefaultBoundaryPolicy below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rn30kjbvb68emi5ref8e.png)

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AdministratorAccess",
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    },
				{
      "Sid": "BlockAccess",
      "Effect": "Deny",
      "Action": ["supportplans:*", "account:*", "billing:Modify*"],
      "Resource": "*"
    },
    {
      "Sid": "LimitAssumeRolePermissions",
      "Effect": "Deny",
      "Action": "sts:AssumeRole",
      "Resource": [
        "arn:aws:iam::<ACCOUNT_ID>:role/Deployment",
        "arn:aws:iam::<ACCOUNT_ID>:role/BreakingGlass",
      ]
    },
    {
      "Sid": "DenyCreateUpdateWithoutPermissionBoundary",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:UpdateUser",
        "iam:CreateRole",
        "iam:UpdateRole"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "iam:PermissionsBoundary": "arn:aws:iam::<ACCOUNT_ID>:policy/DefaultBoundaryPolicy"
        }
      }
    },
    {
      "Sid": "DenyPutAndDeletePermissionsBoundary",
      "Effect": "Deny",
      "Action": [
        "iam:DeleteUserPermissionsBoundary",
        "iam:PutUserPermissionsBoundary",
        "iam:PutRolePermissionsBoundary",
        "iam:DeleteRolePermissionsBoundary"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyDeleteOrUpdateBoundaryPolicy",
      "Effect": "Deny",
      "Action": [
        "iam:DeletePolicy",
        "iam:DeletePolicyVersion",
        "iam:CreatePolicyVersion",
        "iam:SetDefaultPolicyVersion"
      ],
      "Resource": "arn:aws:iam::<ACCOUNT_ID>:policy/DefaultBoundaryPolicy"
    }
  ]
}
```

To test this out in your AWS account, follow these steps. Before you start, the Permissions Boundary section in the AWS Management Console is somewhat hidden at the bottom of the page where you select managed policies. So remember: scroll all the way down!

1. Create a managed policy: DefaultBoundaryPolicy and copy the json document above.
    1. Replace the <ACCOUNT-ID> for your AWS Account ID.
    2. Enter the name: DefaultBoundaryPolicy. (Note: when the IAM role “Developer” and/or the managed policy “DefaultBoundaryPolicy” already exists in your account, update these names accordingly.)
    3. Update the list of roles you want block. In case you only want to block the Developer role in this example, replace the resources block for “NotResource: “<the arn of the developer role”).
2. Create an IAM Role: “Developer”.
    1. Attach an inline policy for the services the developer is allowed to use in this example
    2. Attach DefaultBoundaryPolicy as the permissions boundary
    3. Make sure MFA required is checked
3. Create an IAM User: “Jeff” with a strong password
    1. Allow the user to assume the role Developer
    2. Attach the DefaultBoundaryPolicy
    3. Enable MFA on the user
    4. Login as the user
4. Assume the role created in step 2, and try to:
    1. Create a user or role without a permissions boundary (it supposed to fail)
    2. Now with the DefaultBoundaryPolicy attached (it supposed to succeed)
    3. Delete or update the managed policy: DefaultBoundaryPolicy (it supposed to fail)
    4. Try to delete or change the boundary policy on the Developer Role (it supposed to fail)

## Some learnings

While doing this, I learned a couple of things:

- When working with identity-based policies, it’s better to work implicit deny, because an explicit deny can not be overwritten with an allow. In case of permissions boundaries, you will work with deny policies a lot more.
- Permissions Boundaries can only be attached to IAM Users and IAM Roles. So not to IAM Groups or Resources (Resource Based Policies). When I was writing the initial blog post, I forgot about it, so designed the solution and permissions with IAM Groups in mind. But no worries; a deny in a user’s permissions boundary, also applies to policies from groups. All policies attached to the user and all groups are combined during the permission evaluation.
- Somebody asked me: why are you not using Service Control Policies to protect your account? Some things on AWS account level should be blocked using SCPs. For example: disabling the root account user, or preventing users to turn off CloudTrail, etc. The things we would like to block with boundary policies only apply to a group of users in the AWS account, not to all the users. Some users should be able to manage the DefaultBoundaryPolicy.
- You could choose to deploy the DefaultBoundaryPolicy in a CI/CD pipeline, accessible to the developer. So they are able to manage everything themselves. Of course make sure the CI/CD pipeline contains a 4-eyes principle approval and compliance tests.

## Conclusion

With this solution, you are able to give developers a lot of freedom and responsibility, but there are of course “boundaries” to that. We want to block access to certain services or actions, and we want to block privilege escalation.

Leave a comment or let me know if you have feedback!

Photo by <a href="https://unsplash.com/@kevin_butz?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Kevin Butz</a> on <a href="https://unsplash.com/photos/wN56Bsh_Kfc?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>