IAM Role Examples in AWS CDK - Complete Guide

avatar
Borislav Hadzhiev

Last updated: Jan 26, 2024
11 min

banner

# Table of Contents

  1. Creating IAM Roles in AWS CDK
  2. Avoid Circular Dependency with inline Policies and IAM Roles
  3. Attach a Managed Policy to an IAM Role after Role Creation
  4. Attach an Inline Policy to an IAM Role after Role Creation
  5. Add a Principal to an IAM Role after Role Creation
  6. Importing Existing IAM Roles in AWS CDK
  7. Using Permissions Boundaries in AWS CDK
  8. Set a Permissions Boundary on a Role in AWS CDK
  9. Set a Permissions Boundary on IAM User in AWS CDK
  10. Adding Policy Statements to a Permissions Boundary in AWS CDK
  11. Importing an Existing Permissions Boundary in AWS CDK
  12. Attaching a second Permissions Boundary Overrides the First one
  13. Removing a Permissions Boundary in AWS CDK

# Creating IAM Roles in AWS CDK

IAM Roles are collections of policies that grant specific permissions to access resources.

To create an IAM Role in AWS CDK we have to use the Role construct.

The code for this article is available on GitHub

To demo using IAM Roles in CDK, let's provision a stack that consists of a single IAM role.

lib/cdk-starter-stack.ts
import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ๐Ÿ‘‡ Create ACM Permission Policy const describeAcmCertificates = new iam.PolicyDocument({ statements: [ new iam.PolicyStatement({ resources: ['arn:aws:acm:*:*:certificate/*'], actions: ['acm:DescribeCertificate'], }), ], }); // ๐Ÿ‘‡ Create Role const role = new iam.Role(this, 'example-iam-role', { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), description: 'An example IAM role in AWS CDK', inlinePolicies: { DescribeACMCerts: describeAcmCertificates, }, managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( 'AmazonAPIGatewayInvokeFullAccess', ), ], }); } }

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

  1. We created an IAM Policy document, named describeAcmCertificates. A Policy Document is a collection of Policy Statements.

    In this case, the only Policy Statement we've passed to the document grants DescribeCertificate action permission for Amazon Certificate Manager resources.

  2. We created an IAM Role. The props we've passed to the role are:

  • assumedBy - the IAM Principal, which can assume the role. In this case, this is the API Gateway service.

    We can specify different types of principals, common ones include:

  • description - a short description of the IAM role.

  • inlinePolicies - a map of policies that will be inlined into the role. The name of the inline policy is taken from the key of the map, in our case DescribeACMCerts. The inline policies are created with the role, which can sometimes cause circular dependencies. We will see how we can attach policies after role creation later in the article.

  • managedPolicies - a list of managed policies to associate with the role.

We don't have to provide inlinePolicies and managedPolicies props as these are optional. We can attach policies to the role after the role has been created

Note that we didn't provide a role name, so a unique name will be automatically generated for us, based on the role's logical ID.

Let's deploy the CDK Stack.

shell
npx aws-cdk deploy

The CloudFormation stack has provisioned a single IAM role. The role has 2 Permission policies (policies that describe the permissions of the role).

permission policies role

The role's trust policy (describes who can assume the role) includes the API Gateway service, as we specified in the assumedBy prop.

trust policy role

# Avoid Circular dependencies with inline Policies and IAM Roles

Policies we attach using the inlinePolicies prop on the role are created when the IAM role is created. Depending on the resources we have to reference in the IAM role, this can lead to a circular dependency.

The code for this article is available on GitHub

In order to add policies to a role by provisioning a separate CloudFormation resource we have to use the addToPolicy method.

lib/cdk-starter-stack.ts
import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐Ÿ‘‡ add an Inline Policy to the Role role.addToPolicy( new iam.PolicyStatement({ actions: ['logs:CreateLogGroup', 'logs:CreateLogStream'], resources: ['*'], }), ); } }

Let's run the deploy command:

shell
npx aws-cdk deploy

After the resources have been provisioned, we can see that our stack now consists of 2 resources - the IAM Role and a separately provisioned IAM Policy.

policy after role creation

The policy has also been applied to the IAM role.

permission policies role 2

# Attach a Managed Policy to an IAM Role after Role Creation

In order to attach a Managed Policy to a role, after we've created the role, we have to use the addManagedPolicy method.

lib/cdk-starter-stack.ts
import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐Ÿ‘‡ add a Managed Policy to role role.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName( 'service-role/AmazonAPIGatewayPushToCloudWatchLogs', ), ); } }

The addManagedPolicy method attaches a managed policy to the role.

When using the fromAwsManagedPolicyName method, we have to provide the name of the managed policy.

However, some managed policy names have a prefix of service-role, job-function, and other managed policy names have no prefix at all. We have to include the managed policy prefix when invoking the fromAwsManagedPolicyName method.

In this case, we've included the service-role/ prefix. The easiest way to see if the policy has a prefix is to look at the ARN of the policy:

arn managed policy

In the screenshot, we can see that the service-role/ prefix is present in the Policy ARN, therefore we should include it.

After I run the npx aws-cdk deploy command, I can see that the AmazonAPIGatewayPushToCloudWatchLogs managed policy has been attached to the role.

attached-managed-policy-role

# Attach an Inline Policy to an IAM Role after Role Creation

The code for this article is available on GitHub

In order to attach an Inline Policy to an IAM Role after the role has been created, we need to use the attachInlinePolicy method.

lib/cdk-starter-stack.ts
import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐Ÿ‘‡ attach an Inline Policy to role role.attachInlinePolicy( new iam.Policy(this, 'cw-logs', { statements: [ new iam.PolicyStatement({ actions: ['logs:PutLogEvents'], resources: ['*'], }), ], }), ); } }

We attached an inline IAM Policy that grants a single action to CloudWatch logs resources to the principal of the role.

Let's deploy the changes:

shell
npx aws-cdk deploy

The inline policy has been created as a separate CloudFormation resource and it has been attached to the role.

inline policy role

# Add a Principal to an IAM Role after Role Creation

In order to add a Principal to an IAM Role after the role has been created we have to modify the assumeRolePolicy property of the role.

lib/cdk-starter-stack.ts
import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ... rest // ๐Ÿ‘‡ Add the Lambda service as a Principal role.assumeRolePolicy?.addStatements( new iam.PolicyStatement({ actions: ['sts:AssumeRole'], effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal('lambda.amazonaws.com')], }), ); } }

We modified the assume role policy document of the role to allow the lambda service to assume the role.

Let's deploy the changes.

shell
npx aws-cdk deploy

If we take a look at the Trust Relationship of the role, we can see that the lambda service has been added as a principal:

updated role trust policy

If multiple principals are added to a policy, they will be merged together.

# Clean up

To delete the resources we've provisioned, issue the destroy command.

shell
npx aws-cdk destroy

# Importing Existing IAM Roles in AWS CDK

In order to import an existing IAM Role in CDK, we have to use the fromRoleArn static method on the Role construct.

import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; export class CdkStarterStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // ๐Ÿ‘‡ import existing IAM Role const importedRole = iam.Role.fromRoleArn( this, 'imported-role', `arn:aws:iam::${cdk.Stack.of(this).account}:role/Existing-Role-Name`, {mutable: false}, ); console.log('importedRole ๐Ÿ‘‰', importedRole.roleName); } }

We used the fromRoleArn method to import an external IAM Role in our CDK stack. The third parameter we passed to the method is the ARN of the IAM role we want to import.

The mutable prop specifies whether the imported role can be modified by attaching policies to it. By default, the mutable prop is set to true.

It doesn't make much sense to import a role and then modify its permissions, so most of the time it's best to avoid this behavior.

If I run the cdk synth command to run the code from the snippet with a role ARN that exists in my account, I can see that the role name is resolved at synthesis time:

role name resolved

I've also written an article on how to provision an IAM Policy 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.

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-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; 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 sample:

  1. We created a managed IAM policy and added a policy statement that
    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 run the deploy command:

shell
npx aws-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 permission policies attached to it.

Let's 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-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; 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) } }

We 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 run the deploy command.

shell
npx aws-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.

# 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-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; 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: ['*'], }), ); } }

We 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 run the deploy command:

shell
npx aws-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-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; 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); } }

We imported the managed policy by using the fromManagedPolicyName method and 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 a user, it will simply replace the previous permissions boundary.

lib/cdk-starter-stack.ts
import * as iam from 'aws-cdk-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; 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); } }

We created an IAM-managed policy, which denies access to ses actions. We applied the managed policy as a permissions boundary on an IAM user.

Let's run the deploy command.

shell
npx aws-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 need 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-lib/aws-iam'; import * as cdk from 'aws-cdk-lib'; 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(); } }

We removed the permissions boundary that we previously set on the user.

Let's issue the deploy command.

shell
npx aws-cdk deploy

If we 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 aws-cdk destroy

# Additional Resources

You can learn more about the related topics by checking out the following tutorials:

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.

Copyright ยฉ 2024 Borislav Hadzhiev