Last updated: Jan 26, 2024
Reading timeยท11 min
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.
To demo using IAM Roles in CDK, let's provision a stack that consists of a single IAM role.
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:
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.
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.
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.
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).
The role's trust policy (describes who can assume the role) includes the API
Gateway service, as we specified in the assumedBy
prop.
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.
In order to add policies to a role by provisioning a separate CloudFormation resource we have to use the addToPolicy method.
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:
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.
The policy has also been applied to the IAM role.
In order to attach a Managed Policy to a role, after we've created the role, we have to use the addManagedPolicy method.
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:
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.
In order to attach an Inline Policy to an IAM Role after the role has been created, we need to use the attachInlinePolicy method.
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:
npx aws-cdk deploy
The inline policy has been created as a separate CloudFormation resource and it has been attached to the role.
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.
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.
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:
If multiple principals are added to a policy, they will be merged together.
To delete the resources we've provisioned, issue the destroy
command.
npx aws-cdk destroy
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:
I've also written an article on how to provision an IAM Policy 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.
Let's look at an example, where we create a permissions boundary and attach it to an IAM role.
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:
sqs
related actions on all resources.Let's first run the synth
command to see what we get from the console.log
call:
The ARN is a token, in other words, an encoded value that will get resolved at deployment time.
Let's run the deploy
command:
npx aws-cdk deploy
If we look at the role in the IAM console, we can see that the permissions boundary has been 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.
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.
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.
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.
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.
A permissions boundary is a managed IAM policy, which means that we can add additional policy statements to it.
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:
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:
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.
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.
If we try to set a second permissions boundary on a role or a user, it will simply replace the previous permissions boundary.
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.
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:
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);
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.
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:
To delete the provisioned resources, run the destroy
command.
npx aws-cdk destroy
You can learn more about the related topics by checking out the following tutorials: