1. Overview

When working with AWS, we often need to grant access to users, applications, or other AWS accounts for various resources and services.

AWS Identity and Access Management (IAM) offers several flexible and secure ways to manage these permissions, including using roles. One of the key components of an IAM role is the AssumeRolePolicyDocument property, which defines who can assume the role and under what conditions.

In this article, we’ll explore the AssumeRolePolicyDocument property in detail, covering its purpose, syntax, and various use cases.

2. What is the AssumeRolePolicyDocument?

The AssumeRolePolicyDocument property of an IAM role defines the trust relationship between the role and the entities allowed to assume it.

When creating an IAM role using CloudFormation, we specify the AssumeRolePolicyDocument as a property of the role. Without this property, the role becomes inaccessible and useless.

The trust policy consists of one or more Statement, each containing the following elements:

  • Effect: specifies whether the Statement allows or denies access, the two possible values are Allow and Deny
  • Principal: defines the entity that can assume the role
  • Action: the operation that the Principal is allowed to perform. The Action varies depending on the scenario and the type of Principal assuming the role.

It’s important to note that this trust policy differs from the IAM policies attached to a role. While the trust policy defines who can assume the role, the IAM policies determine what permissions the role grants to the assuming Principal.

3. Use Cases of AssumeRolePolicyDocument

To better understand the AssumeRolePolicyDocument property, let’s look at some common use cases that demonstrate its application.

3.1. AWS Service Access

One of the most common use cases is granting access to AWS services to perform actions on our behalf.

For our demonstration, we’ll take an example where we want to allow an AWS Lambda function to access objects in our Amazon S3 bucket:

AWSTemplateFormatVersion: 2010-09-09

Parameters:
    S3BucketName:
        Type: String

Resources:
    BaeldungOpsRole:
        Type: AWS::IAM::Role
        Properties:
            RoleName: baeldung-ops-role
            Policies:
                - PolicyName: s3-bucket-access
                  PolicyDocument:
                      Version: 2012-10-17
                      Statement:
                          - Effect: Allow
                            Action: s3:GetObject
                            Resource: !Sub arn:aws:s3:::${S3BucketName}/*
            AssumeRolePolicyDocument:
                Version: 2012-10-17
                Statement:
                    - Effect: Allow
                      Principal:
                          Service: lambda.amazonaws.com
                      Action: sts:AssumeRole

In our CloudFormation template, we define a role named baeldung-ops-role. We first attach an inline policy granting read access to objects in our specified S3 bucket.

Then, we specify the AssumeRolePolicyDocument property that allows the AWS Lambda service to assume this role.

The Parameters section in our CloudFormation template allows us to define input values that we can pass to our template during stack creation. In this example, we’ve defined the S3BucketName parameter, which we’ll use to specify the name of our S3 bucket.

We use the !Sub function to dynamically substitute the ${S3BucketName} with the actual bucket name provided as a parameter. By using Parameters and the !Sub function, we make our template more flexible and reusable across different environments.

It’s important to note that we used lambda.amazonaws.com as the service Principal for AWS Lambda. However, each AWS service has its own unique service Principal name. This unofficial list maintained by the community can be a helpful reference when working with other services.

Now that we’ve defined our template, we can create our CloudFormation stack using the AWS CLI:

aws cloudformation create-stack \
    --stack-name baeldung-cloudformation-tutorial-template \
    --template-body file://lambda_s3_read_access_role.yaml \
    --parameters ParameterKey=S3BucketName,ParameterValue=baeldung-ops-tutorials \
    --capabilities CAPABILITY_NAMED_IAM

In our create-stack command, we provide the following parameters:

  • stack-name: to specify the name of our CloudFormation stack
  • template-body: to specify the path to our CloudFormation template file
  • parameters: to specify the S3BucketName parameter and provide the value of our S3 bucket name

We also provide the value CAPABILITY_NAMED_IAM in the capabilities parameter. This is required when our template includes IAM resources with custom names, such as the baeldung-ops-role IAM role in our example. By providing this capability, we acknowledge that we’re aware of and intend to create IAM resources with specific names, rather than letting CloudFormation generate random names for us.

3.2. Cross-Account Access

Another important use case for the AssumeRolePolicyDocument is enabling cross-account access, a common pattern in multi-account AWS environments.

This allows users from one AWS account to access resources in another account that we own or trust. In this delegation scenario, the account that owns the resources is called the trusting account, and the account that is granted access is known as the trusted account.

Let’s consider an example where we want an IAM user named baeldung-ops-user in our trusted account to assume an IAM role named baeldung-ops-role in our trusting account. To achieve this, we’ll define the AssumeRolePolicyDocument property for our role:

Parameters:
    TrustedAccountId:
        Type: String
    IAMUserName:
        Type: String

Resources:
    BaeldungOpsRole:
        Type: AWS::IAM::Role:
        Properties:
            RoleName: baeldung-ops-role
            AssumeRolePolicyDocument:
                Version: 2012-10-17
                Statement:
                    - Effect: Allow
                      Principal:
                          AWS: !Sub arn:aws:iam::${TrustedAccountId}:user/${IAMUserName}
                      Action: sts:AssumeRole

We define the TrustedAccountId and IAMUserName as Parameters in our template to allow us to specify the trusted account-id and the IAM user-name at stack creation time.

In our trust policy, we again use the !Sub function to construct the ARN of our IAM user from our trusted account as the Principal. By doing so, we’re explicitly allowing this user to assume the role in our trusting account.

It’s also important to note that our baeldung-ops-user must have permission to assume the intended role in our trusting account in its IAM policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::trusting-account-id:role/baeldung-ops-role"
        }
    ]
}

3.3. Federated Access

Federated access allows users to access AWS resources using their existing identities from external identity providers (IdPs). This eliminates the need to create and manage separate IAM users for each individual. To enable federated access in our trust policy, we specify the IdP as the trusted Principal.

When working with federated access, we use different AssumeRole actions depending on the type of IdP. For web identity providers like Amazon Cognito or Google, we use the sts:AssumeRoleWithWebIdentity action. For SAML 2.0-based IdPs such as Okta or Azure AD, we use the sts:AssumeRoleWithSAML action.

Let’s look at an example where we want to allow users authenticated through Amazon Cognito to assume an IAM role:

Resources:
    BaeldungOpsRole:
        Type: AWS::IAM::Role
        Properties:
            RoleName: baeldung-ops-role
            AssumeRolePolicyDocument:
                Version: 2012-10-17
                Statement:
                    - Effect: Allow
                      Principal:
                          Federated: cognito-identity.amazonaws.com
                      Action: sts:AssumeRoleWithWebIdentity

In our trust policy, we specify cognito-identity.amazonaws.com as the federated Principal, indicating that Amazon Cognito is the trusted IdP.

3.4. Role Chaining

Role chaining is a powerful feature that allows an IAM role to assume another IAM role, enabling us to delegate access across different levels of trust. This is particularly useful when we need to manage permissions granularly.

To understand how role chaining works, let’s consider an example where we have an IAM role named baeldung-ops-role that needs to assume another IAM role named baeldung-ops-elevated-role. We’ll achieve this using the AssumeRolePolicyDocument property for the latter:

Parameters:
    IAMRoleName:
        Type: String

Resources:
    BaeldungOpsElevatedRole:
        Type: AWS::IAM::Role
        Properties:
            RoleName: baeldung-ops-elevated-role
            AssumeRolePolicyDocument:
                Version: 2012-10-17
                Statement:
                    - Effect: Allow
                      Principal:
                          AWS: !Sub arn:aws:iam::${AWS::AccountId}:role/${IAMRoleName}
                      Action: sts:AssumeRole

We define the IAMRoleName as a Parameter and use the !Sub function in our trust policy to construct the ARN of the role that can assume baeldung-ops-elevated-role.

Note that we’re using the ${AWS::AccountId} pseudo parameter here, which is automatically populated by CloudFormation with the AWS account-id in which the stack is being created.

Similar to the cross-account access scenario, the baeldung-ops-role must also have the necessary permissions to assume the intended role in its IAM policy.

By using role chaining, we can create a hierarchy of roles with varying levels of permissions, but we should always follow the principle of least privilege and only grant the necessary permissions at each level of the role chain.

4. Restricting Access With Conditions

In addition to specifying the trusted Principal in our AssumeRolePolicyDocument property, we can further restrict access by using the optional Condition element in our trust policy Statement.

The Condition element allows us to define circumstances under which a Principal can assume an IAM role. This provides an extra layer of security and granular control.

We can use various condition keys to restrict access based on factors related to the request and resource details.

Let’s revisit our federated access example from earlier and add a Condition restricting access to a specific Cognito identity pool:

Parameters:
    CognitoIdentityPoolId:
        Type: String

# IAM role definition
Condition:
    StringEquals:
        cognito-identity.amazonaws.com:aud: !Ref CognitoIdentityPoolId

We use the StringEquals condition operator to ensure the aud claim from the Cognito token matches our identity pool ID. This prevents users from other identity pools from assuming the role, even if they’re authenticated through Amazon Cognito.

In our Parameters, we specify the CognitoIdentityPoolId which we reference in our Condition block using the !Ref function.

We can also combine multiple conditions to create more complex access control rules. For instance, to restrict access to our AWS Lambda function from earlier to a specific IP range and source ARN:

Parameters:
    LambdaSourceCIDR:
        Type: String 
    LambdaFunctionName:
        Type: String

# IAM role definition
Condition:
    IpAddress:
        aws:SourceIp: !Ref LambdaSourceCIDR
    ArnLike:
        aws:SourceArn: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunctionName}

Here, we use the IpAddress condition operator to limit access to the CIDR block of the VPC where our private Lambda function is deployed and the ArnLike condition operator to ensure that the request originates from our intended Lambda function.

5. Conclusion

In this article, we explored the AssumeRolePolicyDocument property in AWS IAM roles and its significance in managing access to our AWS resources.

We discussed various use cases, such as granting access to AWS services, enabling cross-account access, allowing federated users to assume roles, and implementing role chaining.

Finally, we looked at how the Condition element is used to further restrict access in our trust policy Statement.

By carefully crafting the AssumeRolePolicyDocument property, we can create fine-grained IAM roles that ensure our AWS resources are secure and accessible only to the intended entities.

All the CloudFormation templates used in this article are available over on GitHub.