Nebulaworks Insight Content Card Background - Ilya trigubenko sand

Nebulaworks Insight Content Card Background - Ilya trigubenko sand

Leveraging AWS AssumeRole for Cross-Account Federation

April 17, 2020 Earl Giffen

A quick introduction in using Terraform to configure AWS Security Token Service for assuming roles in separate AWS accounts.

Recent Updates

Managing access and permissions across an environment in a cloud provider requires a unified strategy. The best approach for this is to federate the management of these controls and give access only to those resources that are required. Specifically in AWS, due to Amazon advocating for the use of multiple AWS accounts for deployments, the complexity of a centralized approach becomes a daunting prospect.

The access and control restrictions within a cloud provider are collectively known as identity and access management or IAM for short. IAM is important because it helps to secure and control access to all cloud resources outside traditional network boundaries. Essentially, IAM is your security control mechanism for cloud platforms. Think about it this way: a cloud engineer may need to manage resources within multiple accounts. You, the cloud engineer or administrator wouldn’t want to recreate his access controls and keys and such for each account. Can you imagine an environment spanning dozens of accounts, each with 10,000 users? Wouldn’t it be a whole lot easier to create the user once and assign him a known set of permissions given the environments and roles he will need to access? The same could be said for IT or developer users with completely different sets of access requirements than those of our cloud engineer. It is also simple to add MFA (multi-factor authentication) to the centralized accounts and leverage it without having to reauthorize with each child account. What about auditing your security controls? It’s much simpler to audit one set of accounts versus thousands.

Within AWS, there is a mechanism called assumerole that supports granting these complex permissions across multiple AWS accounts. We can federate our users into one account and then create a set of permissions for those users and assign them to a role in a child account. The federated user can then “assume” this “role,” hence the name. This is a much more manageable endeavor. Leveraging AWS assumerole for cross-account federation allows us to build simple and secure environments in the cloud.

Let’s take a look at how to build one of these environments with Terraform utilizing assumerole for IAM.

Requirements

For this exercise, you will need Terraform installed and access to 2 AWS accounts. The Terraform configurations for these accounts will have the following directory structure:

- primary
|   |
|   |__ main.tf
|
|
- child
    |
    |
    |__ main.tf

You will need to be able to create IAM resources within each of those accounts. As above, each of these accounts will be provisioned as two separate terraform roots in separate directories. For the sake of simplicity, we will refer to the federated account as primary and the account we are assuming roles in as the child. You will need to create two directories with these names. You will also need the AWS CLI installed for verifying that the roles were created and functioning properly. Configuring the AWS CLI and Terraform is beyond the scope of this article but you can find instructions for Terraform here and for the AWS CLI here.

Getting started with IAM AssumeRole

So how do we create roles that we can actually use in a production environment?

  1. We need to create our IAM user in the primary account. We also need an IAM policy. Create a main.tf file in the primary directory with the following resources:
primary/main.tf
provider "aws" {
  region = "us-east-1"
}

#
# AWS IAM Policy Resources
#

data "aws_iam_policy_document" "assume_role_child_default" {
  statement {
    actions = ["sts:AssumeRole"]

    resources = [
      "arn:aws:iam::<YOUR CHILD ACCOUNT ID>:role/users_assume_role_default",
    ]
  }
}

resource "aws_iam_policy" "assume_role_child_user" {
  name   = "assume-role-child-user"
  policy = "${data.aws_iam_policy_document.assume_role_child_default.json}"
}

resource "aws_iam_policy_attachment" "assume_role_child_user" {
  name       = "assume-role-child-user"
  policy_arn = "${aws_iam_policy.assume_role_child_user.arn}"

  groups = [
    "${aws_iam_group.child_users.id}",
  ]
}


#
# AWS IAM Users and Groups
#

resource "aws_iam_user" "boaty_mcboatface" {
  name          = "boaty.mcboatface"
  force_destroy = true
}

resource aws_iam_group "child_users" {
  name = "child-users"
}

resource "aws_iam_group_membership" "child_users" {
  name = "child-users"

  users = [
    "${aws_iam_user.boaty_mcboatface.name}",
  ]

  group = "${aws_iam_group.child_users.name}"
}

After the files are created, run terraform plan to verify the changes followed by terraform apply to create them in AWS.

As you can see we are creating an IAM user and adding that user to a group. We are also creating an assumerole policy and attaching it to the group so that our user is able to assume the role. We can add more permissions to these groups as necessary but for now, the only permission we care about is the assumerole. This is only one half of the equation though as we need resources for our child account.

  1. Create the resources for the role being assumed in the child account. Again, these will be provisioned in a separate terraform root from the resources created in step 1 and it will be in the child directory. Create a main.tf file in the child directory with the following resources:
child/main.tf
provider "aws" {
  region = "us-east-1"
}

#
# AWS IAM Policies
#

data "aws_iam_policy_document" "users_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::<PRIMARY ACCOUNT ID>:root"]
    }
  }
}

data "aws_iam_policy_document" "child_users_access" {
  statement {
    actions   = ["*"]
    resources = ["*"]

    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"

      values = [
        "arn:aws:iam::<PRIMARY ACCOUNT ID>:group/child-users",
      ]
    }
  }
}

resource "aws_iam_policy" "child_users_access" {
  name   = "child-users-access"
  policy = "${data.aws_iam_policy_document.child_users_access.json}"
}

resource "aws_iam_policy_attachment" "child_users_access" {
  name       = "child-users-access"
  policy_arn = "${aws_iam_policy.child_users_access.arn}"

  roles = [
    "${aws_iam_role.child_users.id}"
  ]
}

#
# AWS IAM Roles
#

resource "aws_iam_role" "child_users" {
  name               = "users_assume_role_default"
  assume_role_policy = "${data.aws_iam_policy_document.users_assume_role.json}"
}

Again, we are going to run terraform plan to verify the changes and terraform apply to create the resources.

As you can see we don’t create a user here but rather a role. We then attach the policy to the role signifying that it will be utilizing assumerole from another account and the associated permissions the role will have in the child account.

  1. After these two sets of Terraform resources have been applied we are going to need to verify that we can assume the role and have the proper permissions. To verify with the AWS CLI, you will need to create a keypair for the aws user you have created and have the following two files configured in the .aws directory in your home directory:
${HOME}/.aws/credentials
[default]
aws_access_key_id = <YOUR NEW USER'S ACCESS KEY ID>
aws_secret_access_key = <YOUR NEW USER'S SECRET ACCESS KEY>
${HOME}/.aws_config
[default]
region=us-east-1

[profile child]
role_arn = arn:aws:iam::<CHILD ACCOUNT ID>:role/users_assume_role_default
source_profile = default
  1. From the AWS CLI, run an STS get-caller-identity command to verify the user making the API call.
~$ aws sts get-caller-identity
{
    "UserId": "AIDASCHIHZBF5IBBSRTHB",
    "Account": "<PRIMARY ACCOUNT ID>",
    "Arn": "arn:aws:iam::<PRIMARY ACCOUNT ID>:user/boaty.mcboatface"
}

This proves that we can access the user we created. Now if you run the same command with a profile flag set, you will get a different output:

~$ aws sts get-caller-identity --profile child
{
    "UserId": "AROASCHIHZBFQ4QHQJLCB:botocore-session-1586546865",
    "Account": "<CHILD ACCOUNT ID>",
    "Arn": "arn:aws:sts::<CHILD ACCOUNT ID>:assumed-role/users_assume_role_default/botocore-session-1586546865"
}

Notice the difference in the caller and the account ids between the two calls? Once the profile is set to that of the child role, all calls will be made under that set of permissions.

In Conclusion

Now that you configured and utilized AssumeRole, hopefully, you can see tons of possibilities and use-cases for it. From SSO for authentication to your federated users to multiple roles per-account for strict access management, the possibilities really are endless. The example I provided here today is enough to get you off to the races. With these few resources providing the model, you can build out a production-ready federated IAM deployment in no time!

In today’s industry, you will more often than not find growing numbers of roles and users. Across something as potentially large and complex as an enterprise-scale cloud infrastructure deployment, a cohesive strategy for IAM management is necessary. Centralizing IAM users and then federating out access to the requisite parts of the infrastructure where access is required will allow you to maintain order and keep visibility on your users. It will allow you, the cloud engineer or administrator to easily work as a gatekeeper for your systems, ensuring their security and ultimately their stability. Work to organize your infrastructure as much as possible. Leveraging AWS assumerole for cross-account federation is a great place to start.

Insight Authors

Nebulaworks - Wide/concrete light half gray

Looking for a partner with engineering prowess? We got you.

Learn how we've helped companies like yours.