Skip to main content

Prevent permissions escalation with permissions boundary

Let's imagine a scenario where we want to give full administrative permissions to bob@astran.io, so that be can manage anything related to IAM, but prevent him from accessing any data on S3.

To start with let's remove bob@astran.io from the marketing group that we created earlier.

aws --profile astran iam remove-user-from-group --user-name bob@astran.io --group-name marketing

Now let's create a new policy in a file named iam-admin-policy with the following content:

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

And let's create that policy with the following command:

aws --profile astran iam create-policy --policy-name iamAdminPolicy --policy-document file://iam-admin-policy

This should give us the following output:

{
"Policy": {
"PolicyName": "iamAdminPolicy",
"PolicyId": "ANPA0D9040E6158F48FCA4389896C53EA2B0",
"Arn": "arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/iamAdminPolicy",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 0,
"CreateDate": "2024-08-22T15:14:22.426880+00:00",
"UpdateDate": "2024-08-22T15:14:22.426880+00:00"
}
}

Let's create a iamAdmin group, attach that policy to that group and add bob@astran.io in that group

aws --profile astran iam create-group --group-name iamAdmin
aws --profile astran iam attach-group-policy --group-name iamAdmin --policy-arn arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/iamAdminPolicy
aws --profile astran iam add-user-to-group --user-name bob@astran.io --group-name iamAdmin

If we now try to list all users with our bob@astran.io user, it should work. Creating a bucket however should fail.

PARTITION="demo"
aws --endpoint-url https://${PARTITION}.iam.astran.io iam list-users
aws --endpoint-url https://${PARTITION}.s3.astran.io s3 mb s3://test-3

Output:

{
"Users": [
{
"Path": "/",
"UserName": "bob@astran.io",
"UserId": "AIDAC654348CB5834CDE8B4A2011F31CD2CB",
"Arn": "arn:demo:iam::ce09c61d-afac-404f-a96b-ebbbced80013:user/bob@astran.io",
"CreateDate": "2025-03-27T08:43:40.813640+00:00"
}
]
}
make_bucket failed: s3://test-3 An error occurred (AccessDenied) when calling the CreateBucket operation: You don't have permission to access this resource
note

When you authenticate with a user using the LoginWithWebIdentity action, your temporary credentials are valid for an hour by the default. If you get an error that says Invalid token: Token expired, please redo the authentication part.

There is a flaw in our thinking however. Since that user has full access to IAM, nothing is preventing him from creating a new policy that also gives him full access to S3, attach it to himself, and then remove the old policy. In fact he could even create a new version of that existing policy that gives him full access to S3, let's do that right now with bob@astran.io credentials.

Edit the iam-admin-policy file with the following content:

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

Now let's create a new version of the existing policy, set it as the default one and try to create that bucket again:

PARTITION="demo"
aws --endpoint-url https://${PARTITION}.iam.astran.io iam create-policy-version --policy-arn arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/iamAdminPolicy --policy-document file://iam-admin-policy --set-as-default
aws --endpoint-url https://${PARTITION}.s3.astran.io s3 mb s3://test-3

You should get the following output:

{
"PolicyVersion": {
"VersionId": "v2",
"IsDefaultVersion": true,
"CreateDate": "2024-08-22T16:02:23.460863+00:00"
}
}
make_bucket: test-3

In order to prevent bob@astran.io from elevating his permissions, we need to use a feature called permissions boundary.

Creating a permissions boundary

A permissions boundary is a policy that defines a maximum set of permissions that a user can have.

A permissions boundary does NOT give any permissions to a user.

The way they work is that the intersection of permissions from the permissions boundary and the policies is the effective set of permissions a user has.

Effective permissions

Let's create a permission boundary that gives full access to iam. In a file named iam-admin-permissions-boundary paste the following:

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

And let's create that policy with the following command:

aws --profile astran iam create-policy --policy-name iamAdminPermissionsBoundary --policy-document file://iam-admin-permissions-boundary --path /permissionsBoundary/

This should give us the following output:

{
"Policy": {
"PolicyName": "iamAdminPermissionsBoundary",
"PolicyId": "ANPA0D9040E6158F48FCA4389896C53EA2B0",
"Arn": "arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary",
"Path": "/permissionsBoundary/",
"DefaultVersionId": "v1",
"AttachmentCount": 0,
"CreateDate": "2024-08-22T15:14:22.426880+00:00",
"UpdateDate": "2024-08-22T15:14:22.426880+00:00"
}
}
note

We used a new path parameter in that last API call. This allows you to prefix a resource with a path in the ARN after the resource type. This makes it easier to organize and manage your resources. Most IAM resources (users, groups, roles, policies...) have a path parameter that can be set upon creation.

Now let's apply that permissions boundary to our bob@astran.io user:

aws --profile astran iam put-user-permissions-boundary --user-name bob@astran.io --policy-arn arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary

If we try to create a new bucket test-4 we can see that it fails this time:

# Replace with the name of the partition you are using
PARTITION="demo"
aws --endpoint-url https://${PARTITION}.s3.astran.io s3 mb s3://test-4

Output:

make_bucket failed: s3://test-4 An error occurred (AccessDenied) when calling the CreateBucket operation: You don't have permission to access this resource

You might be wondering but why is it failing ? After all, nothing in the permissions boundary stipulates that you can't do any action on S3. Let's not forget that a permissions boundary is a policy, therefore the same rules apply, by default everything is implicitly denied.

There's another problem however, in the current state of things, nothing prevents bob@astran.io from removing the permissions boundary from his user, or to even create a new user that does not have this permissions boundary.

We're going to have to create a more robust permissions boundary in order to avoid all of that.

Creating a bullet proof permissions boundary

Here's a more thorough permissions boundary that we are going to apply to bob@astran.io.

# Replace the content of this variable with your permissions boundary arn
PERMISSIONS_BOUNDARY_ARN="arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary"
ACCOUNT_ID=$(echo ${PERMISSIONS_BOUNDARY_ARN} | cut -d':' -f 5)
PARTITION=$(echo ${PERMISSIONS_BOUNDARY_ARN} | cut -d':' -f 2)

cat <<EOF > iam-admin-permissions-boundary
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iam:*",
"Resource": "*"
},
{
"Effect": "Deny",
"Action": [
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:CreatePolicyVersion",
"iam:SetDefaultPolicyVersion"
],
"Resource": [
"${PERMISSIONS_BOUNDARY_ARN}"
]
},
{
"Effect": "Deny",
"Action": [
"iam:DeleteUserPermissionsBoundary",
],
"Resource": [
"arn:${PARTITION}:iam::${ACCOUNT_ID}:user/*",
],
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "${PERMISSIONS_BOUNDARY_ARN}"
}
}
},
{
"Effect": "Deny",
"Action": [
"iam:PutUserPermissionsBoundary",
],
"Resource": [
"arn:${PARTITION}:iam::${ACCOUNT_ID}:user/*",
],
"Condition": {
"StringNotEquals": {
"iam:PermissionsBoundary": "${PERMISSIONS_BOUNDARY_ARN}"
}
}
},
{
"Effect": "Deny",
"Action": [
"iam:CreateUser",
],
"Resource": [
"arn:${PARTITION}:iam::${ACCOUNT_ID}:user/*",
],
"Condition": {
"StringNotEquals": {
"iam:PermissionsBoundary": "${PERMISSIONS_BOUNDARY_ARN}"
}
}
},
{
"Effect": "Deny",
"Action": "iam:CreateRole",
"Resource": "*"
}
]
}
EOF

Let's go over all the statement in that permissions boundary.

The following statement says that the user can perform any action on IAM.

      {
"Effect": "Allow",
"Action": "iam:*",
"Resource": "*"
}

The following statement prevents the user from altering this permissions boundary.

      {
"Effect": "Deny",
"Action": [
"iam:DeletePolicy",
"iam:DeletePolicyVersion",
"iam:CreatePolicyVersion",
"iam:SetDefaultPolicyVersion"
],
"Resource": [
"arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary"
]
},

The following statement prevents the user from removing the permission boundary arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary from any users.

      {
"Effect": "Deny",
"Action": [
"iam:DeleteUserPermissionsBoundary"
],
"Resource": [
"arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:user/*"
],
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary"
}
}
},

The statement prevents the user from applying a permissions boundary on any user that is not arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary.

      {
"Effect": "Deny",
"Action": [
"iam:PutUserPermissionsBoundary"
],
"Resource": [
"arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:user/*"
],
"Condition": {
"StringNotEquals": {
"iam:PermissionsBoundary": "arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary"
}
}
},

The following statement prevents the user from creating a user with a permissions boundary that is not arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary.

      {
"Effect": "Deny",
"Action": [
"iam:CreateUser"
],
"Resource": [
"arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:user/*"
],
"Condition": {
"StringNotEquals": {
"iam:PermissionsBoundary": "arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary"
}
}
},

The following statement prevents the user from creating a role. The reason is that at the moment, permissions boundary are not implemented for roles.

      {
"Effect": "Deny",
"Action": "iam:CreateRole",
"Resource": "*"
}

Let's now create a new version of the existing permissions boundary with this new one instead.

aws --profile astran iam create-policy-version --policy-arn arn:demo:iam::ce04d61d-afac-504f-a96b-ebbbced80013:policy/permissionsBoundary/iamAdminPermissionsBoundary --policy-document file://iam-admin-permissions-boundary --set-as-default

If we now try to remove the permissions boundary from our bob@astran.io user, we'll get an error.

# Replace with the name of the partition you are using
PARTITION="demo"
aws --endpoint-url https://${PARTITION}.iam.astran.io delete-user-permissions-boundary --user-name bob@astran.io

Output:

An error occurred (AccessDenied) when calling the DeleteUserPermissionsBoundary operation: You don't have permission to access this resource