Permissions Boundary Examples in AWS CDK - Complete Guide

avatar

Borislav Hadzhiev

Tue Apr 27 20215 min read

banner

Photo by Colton Duke

In order to attach a permissions boundary to a user or role, create a managed IAM policy and set it as a boundary on the IAM entity.

Table of Contents #

  1. Using Permissions Boundaries in AWS CDK
  2. Set a Permissions Boundary on a Role in AWS CDK
  3. Set a Permissions Boundary on IAM User in AWS CDK
  4. Adding Policy Statements to a Permissions Boundary in AWS CDK
  5. Importing an Existing Permissions Boundary in AWS CDK
  6. Attaching a second Permissions Boundary Overrides the First one
  7. Removing a Permissions Boundary in AWS CDK

Using Permissions Boundaries in AWS CDK #

Permissions boundaries are used to prevent privilege escalation by creating new roles. They are managed IAM policies we attach to roles or users, that cap the permissions of the IAM entity.

In order to attach a permissions boundary to a user or a role, we have to create an IAM managed policy using the ManagedPolicy construct and set it as a permissions boundary on the IAM entity.

Set a Permissions Boundary on a Role in AWS CDK #

The code for this article is available on GitHub

Let's look at an example, where we create a permissions boundary and attach it to an IAM role.

lib/cdk-starter-stack.ts
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ๐Ÿ‘‡ Create Permissions Boundary
    const boundary1 = new iam.ManagedPolicy(this, 'permissions-boundary-1', {
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.DENY,
          actions: ['sqs:*'],
          resources: ['*'],
        }),
      ],
    });

    // ๐Ÿ‘‡ Create role and attach the permissions boundary
    const role = new iam.Role(this, 'example-iam-role', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      description: 'An example IAM role in AWS CDK',
      permissionsBoundary: boundary1,
    });

    console.log(
      'role boundary arn ๐Ÿ‘‰',
      role.permissionsBoundary?.managedPolicyArn,
    );
  }
}

Let's go over what we did in the code snippet:

  1. we created a managed IAM policy and added a policy statement which forbids any sqs related actions on all resources.
  2. we created an IAM role, to which we attach our permissions boundary
  3. we printed the arn of the permissions boundary by accessing it on the role

Let's first run the synth command to see what we get from the console.log call:

role boundary arn

The arn is a token, in other words an encoded value that will get resolved at deployment time.

Let's execute a deployment:

shell
npx cdk deploy

If we look at the role in the IAM console we can see that the permissions boundary has been set:

permissions boundary set

With the permissions boundary attached the role can only perform actions that are allowed by the permissions boundary and the permissions policies attached to it.

Next, we are going to create an IAM user, to which we'll attach the permissions boundary after the user has been created.

Set a Permissions Boundary on IAM User in AWS CDK #

The code for this article is available on GitHub

In order to set a permissions boundary on an IAM User in AWS CDK, we are going to create a user and attach the permissions boundary to it, using the apply method on the PermissionsBoundary class.

lib/cdk-starter-stack.ts
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ... rest

    // ๐Ÿ‘‡ Create a user, to which we will attach the boundary
    const user = new iam.User(this, 'example-user');

    // ๐Ÿ‘‡ attach the permissions boundary to the user
    iam.PermissionsBoundary.of(user).apply(boundary1)
  }
}

In the code snippet we've used the apply method on the PermissionsBoundary class. The method takes a single parameter - a managed IAM policy.

Note that we could've also attached the permissions boundary when instantiating the User class by setting the permissionsBoundary prop.

Let's execute a deployment:

shell
npx cdk deploy

If we take a look at the user in the IAM console, we can see that the permissions boundary has been set:

user boundary set

With the permissions boundary attached to the user, the user can only perform actions that are allowed by the permissions boundary and the permissions policies.

Next, we are going to add more policy statements to our permissions boundary:

Adding Policy Statements to a Permissions Boundary in AWS CDK #

The code for this article is available on GitHub

A permissions boundary is a managed IAM policy, which means that we can add additional policy statements to it.

lib/cdk-starter-stack.ts
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ... rest

    // ๐Ÿ‘‡ Add Policy Statements to the Permissions Boundary
    boundary1.addStatements(
      new iam.PolicyStatement({
        effect: iam.Effect.DENY,
        actions: ['kinesis:*'],
        resources: ['*'],
      }),
    );
  }
}

In the code snippet we've added a new policy statement to our permissions boundary. This statement will influence the permissions we can set on our role and user entities.

Let's execute a deployment:

shell
npx cdk deploy

If we take a look at the permissions boundary on the user or role, we can see that kinesis actions are also denied:

updated permissions boundary

Importing an Existing Permissions Boundary in AWS CDK #

The code for this article is available on GitHub

In order to import and use an existing permissions boundary in CDK, we have to use the fromManagedPolicyName or fromManagedPolicyArn static methods on the ManagedPolicy construct.

Let's take a look at an example that uses the fromManagedPolicyName method.

lib/cdk-starter-stack.ts
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ...rest

    // ๐Ÿ‘‡ Used to import an already existing Permissions Boundary
    const externalBoundary = iam.ManagedPolicy.fromManagedPolicyName(
      this,
      'external-boundary-id',
      'YOUR_MANAGED_POLICY_NAME',
    );

    // ๐Ÿ‘‡ apply the external permissions boundary to the role
    iam.PermissionsBoundary.of(role).apply(externalBoundary);
  }
}

In the code snippet, we've imported the managed policy using the fromManagedPolicyName method and we've applied it as a permissions boundary on a role.

Attaching a second Permissions Boundary overrides the first one #

If we try to set a second permissions boundary on a role or user, it will simply replace the previous permissions boundary.

lib/cdk-starter-stack.ts
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ...rest

    // ๐Ÿ‘‡ attaching a second permissions boundary to a role replaces the first
    const boundary2 = new iam.ManagedPolicy(this, 'permissions-boundary-2', {
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.DENY,
          actions: ['ses:*'],
          resources: ['*'],
        }),
      ],
    });

    iam.PermissionsBoundary.of(user).apply(boundary2);
  }
}

In the code snippet we've created an IAM managed policy, which denies access to ses actions. We've applied the managed policy as a permissions boundary on an IAM user.

Let's execute a deployment:

shell
npx cdk deploy

If we take a look at the current permissions boundary of the IAM user in our stack, we can see that the new permissions boundary, which denies ses related actions has overridden the previous one, which denied kinesis and sqs actions:

overridden permissions boundary

In case you would like to attach a permissions boundary to all of a Stack's roles, you can pass the stack as the scope:

iam.PermissionsBoundary.of(stack).apply(boundary1);

Removing a Permissions Boundary in AWS CDK #

In order to remove an error boundary from an AWS resource (i.e. Lambda function, role, user, etc), we have to use the clear method on the PermissionsBoundary class.

import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';

export class CdkStarterStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ...rest

    // ๐Ÿ‘‡ remove the permission boundary from the User
    iam.PermissionsBoundary.of(user).clear();
  }
}

In the code snippet we've removed the permissions boundary that we previously set on the user.

Let's execute a deployment:

shell
npx cdk deploy

If we now take a look at the user in the IAM console, we can see that the permissions boundary has been removed:

permissions boundary removed

Clean up #

To delete the provisioned resources, run the destroy command:

shell
npx cdk destroy

Further Reading #

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee