Deploy IAM Roles across an AWS Organization as code
In environments with multiple AWS accounts, managing roles can be a challenge. AWS IAM offers us a robust solution for managing roles within each account, but when it comes to consistently implementing roles across all accounts in an organization, the task can become complex.
In this post, we will see how to automatically deploy IAM roles across all accounts in an AWS organization as code, using CloudFormation, Organizations, and Terraform.
It is quite common to have tools that require specific IAM permissions in the accounts where we want to implement them, such as an internal tool that needs access to all our accounts. When it comes time to update these permissions, maintaining them can be quite a challenge. With the help of AWS Organizations and AWS CloudFormation StackSets, we can solve this problem.
To explain the steps, we will use a specific example.
Our goal is to deploy the role security-cspm
, with two policies:
SecurityAudit
: AWS managed policy.security-cspm-sechub-import
: Customer managed policy, with the following permissions:
Requirements
We will need to have an AWS organization created, and our accounts must be members of this organization.
Once we have the organization, we will need to enable the Enable All Features option. If you manage your organization as code , you can create the organization and enable all features using the aws_organizations_organization resource:
Enable Trusted Access
Next, we will need to enable the AWS CloudFormation StackSets integration with AWS Organizations.
For some reason unknown to us, this step requires manual action . Enabling trusted access through Terraform or from the organizations menu is not sufficient.
You need to go to the CloudFormation console > StackSets and click on Activate trusted access.
If you used Terraform to create your organization, you will need to add the following service principal in the aws_service_access_principals argument of the aws_organizations_organization resource, e.g.:
resource "aws_organizations_organization" "org" {
aws_service_access_principals = [
...
"member.org.stacksets.cloudformation.amazonaws.com",
...
]
feature_set = "ALL"
}
Warning
Remember that you must include the new service principal along with the existing ones. To verify which are already enabled, you can use the following command:
Delegate CloudFormation StackSets
To avoid performing deployments from the management account of our organization, we will delegate the administration of StackSets to another account, from which we will create and manage the necessary CloudFormation resources.
Tip
Remember that we should try to minimize the use of the management account as much as possible. Well Architected Framework - Security Pillar SEC01-BP02
To delegate the service, you can use the following Terraform resource:
resource "aws_organizations_delegated_administrator" "stacksets" {
account_id = var.security_account_id
service_principal = "member.org.stacksets.cloudformation.amazonaws.com"
}
Alternatively, you can use the following command if you do not manage the management account as code:
aws organizations register-delegated-administrator \
--service-principal=member.org.stacksets.cloudformation.amazonaws.com \
--account-id="securityAccountId"
This action only needs to be done once, and from a single region. You can verify that the delegation has been successfully applied with the following command:
aws organizations list-delegated-administrators \
--service-principal=member.org.stacksets.cloudformation.amazonaws.com
Output
{
"DelegatedAdministrators": [
{
"Id": "222222222222",
"Arn": "arn:aws:organizations::111111111111:account/o-myorgid/222222222222",
"Email": "[email protected]",
"Name": "security-account",
"Status": "ACTIVE",
"JoinedMethod": "CREATED",
"JoinedTimestamp": "2024-10-11T16:30:05.938000+02:00",
"DelegationEnabledDate": "2024-10-11T16:48:07.033000+02:00"
}
]
}
Deploy Cross-Account IAM Roles
Once we have the service integrated with AWS Organizations and delegated to our account, the first thing we need to do is define the resources in a CloudFormation template, to do this, we will create a role.yaml file, which we will later reference from Terraform. The following file declares the IAM role and policies that we want:
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
RoleName:
Type: String
Description: IAM Role Name
RoleDescription:
Type: String
Description: IAM Role Description
Default: ""
PolicyName:
Type: String
Description: IAM Managed read and write policy name
PolicyDescription:
Type: String
Description: IAM Managed Policy Description
Default: ""
IAMPath:
Type: String
Description: IAM Role and Policy Path
Default: "/"
TrustedAccount:
Type: String
Description: AWS trusted account ID
TrustedRole:
Type: String
Description: Allowed IAM Role ARN
Resources:
Role:
Type: 'AWS::IAM::Role'
Properties:
RoleName: !Ref RoleName
Description: !Ref RoleDescription
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS:
!Ref TrustedAccount
Action:
- 'sts:AssumeRole'
Condition:
ArnLike:
'aws:PrincipalArn': !Ref TrustedRole
Path: !Ref IAMPath
ManagedPolicyArns:
- !Ref RolePolicy
- arn:aws:iam::aws:policy/SecurityAudit
RolePolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Ref PolicyName
Description: !Ref PolicyDescription
Path: !Ref IAMPath
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: SecurityHubImport
Effect: Allow
Action:
- securityhub:BatchImportFindings
Resource: '*'
Next, we will need to define the CloudFormation StackSet, using the file we just created as the template:
data "aws_caller_identity" "this" {}
resource "aws_cloudformation_stack_set" "cspm_role" {
name = "security-cspm-role"
description = "Deploy Security CSPM Role across all organization accounts"
permission_model = "SERVICE_MANAGED" // (1)!
call_as = "DELEGATED_ADMIN" // (2)!
auto_deployment {
enabled = true // (3)!
}
capabilities = [
"CAPABILITY_NAMED_IAM", // (4)!
]
template_body = file("${path.root}/role.yaml") // (5)!
parameters = { // (6)!
RoleName = "security-cspm"
RoleDescription = "Audit and Import findings to SecurityHub"
PolicyName = "security-cspm-sechub-import"
PolicyDescription = "Allow import SecurityHub findings"
IAMPath = "/security/cspm/"
TrustedAccount = data.aws_caller_identity.this.id // (7)!
TrustedRole = "arn:aws:iam::${data.aws_caller_identity.this.id}:role/my-source-role"
}
}
- Allows deploying CloudFormation resources in all accounts with a role managed by AWS Organizations.
- Specifies that the operation is performed from the delegated account.
- Enables automatic deployment of the role in new accounts added to the organization.
- Required when the template contains IAM resources managed by us.
- The template we created earlier.
- Defines the parameters specified in our
role.yaml
template. - Uses the data source to automatically pass the account from which this code is executed.
And finally, we define the StackSet instances that will create the resource in the accounts:
data "aws_organizations_organization" "this" {}
resource "aws_cloudformation_stack_instances" "cspm_role" {
stack_set_name = aws_cloudformation_stack_set.cspm_role.name
call_as = "DELEGATED_ADMIN"
deployment_targets {
organizational_unit_ids = [ data.aws_organizations_organization.this.roots[0].id ] // (1)!
}
}
- We use the data source to automatically pass the organization ID.
Tip
It is recommended to use the operation_preferences
arguments to improve efficiency when creating stacks in organizations with many accounts. This will allow us to parallelize account operations. For example:
resource "aws_cloudformation_stack_set" "cspm_role" {
...
operation_preferences {
max_concurrent_percentage = 50
failure_tolerance_percentage = 50
}
...
}
resource "aws_cloudformation_stack_instances" "cspm_role" {
...
operation_preferences {
concurrency_mode = "SOFT_FAILURE_TOLERANCE"
max_concurrent_percentage = 50
}
...
}
Once we have the code, we can run a terraform plan
& terraform apply
, and we're done!
As a summary, we leave you the following diagram with the architecture we have created:
Terraform Module
If you want to simplify this whole process, you can create a Terraform module like the one we have published. This way, you can reuse all the code by simply changing the template and parameters.
The module is available in the Terraform registry, so you can use it in your code as follows:
data "aws_caller_identity" "this" {}
data "aws_organizations_organization" "this" {}
module "cspm_role" {
source = "unicrons/organization-iam-role/aws"
stack_set_name = "security-cspm-role"
stack_set_description = "Deploy Security CSPM Role across all organization accounts"
template_path = "${path.root}/role.yaml"
template_parameters = {
RoleName = "security-cspm"
RoleDescription = "Audit and Import findings to SecurityHub"
PolicyName = "security-cspm-sechub-import"
PolicyDescription = "Allow import SecurityHub findings"
IAMPath = "/security/cspm/"
TrustedAccount = data.aws_caller_identity.this.id
TrustedRole = "arn:aws:iam::${data.aws_caller_identity.this.id}:role/my-source-role"
}
organizational_unit_ids = [ data.aws_organizations_organization.this.roots[0].id ]
}
To see all available configuration options, take a look at the input parameters.
And that's all folks! If you have any questions or comments, feel free to reach out to us.
Saludos, and may the force be with you.