In the company, we use AWS SSO for user authentication; when a new user is created from Azure AD, it will automatically synced by AWS IAM Identity Center and Azure AD integration, and then our team will need to handle the SSO user assignment to put them in the required AWS accounts with requested permission sets, it became a pain when such requests coming more frequently and every time an individual user or a whole team with different accounts and permission requirements need to be fulfilled, so how to handle this efficiently become my recent topic.
So far, I have tried shell script and Python script to read the user name, AWS account ID, and permission sets ARN from a CSV file, then complete the task with AWScli. However it is not smart enough when the request or scenario changes. I have to adjust the script every time.
Managing individual request via TerraformLet's start with handling user assignment individually with terraform first. Here I have 2 requests:
- A user "[email protected]", under a group called "AD-RDS-READ-ONLY" in AWS IAM Identity Center, I need to create a permission set in AWS account "123456789", and assign to this user.
- The second request is from the security team, we have 3 security team members ([email protected], [email protected], [email protected]), under a group called "AD-ACM-FULL-ACCESS" in AWS IAM Identity Center, they all need full access for AWS certificate manager access, for all of our 3 AWS accounts (12345678901, 12345678902, and 12345678903).
vim main.tf # For Request 1 for RDS read-only access for 1 user in 1 AWS account # for AWS in ap-southeast-2 provider "aws" { region = "ap-southeast-2" } # Define the AWS SSO Instance ARN data "aws_ssoadmin_instances" "main" {} resource "aws_ssoadmin_permission_set" "request1" { instance_arn = data.aws_ssoadmin_instances.main.arns[0] name = "RDS-ReadOnly" description = "Read-only access to RDS resources" session_duration = "PT1H" # Add the policies you need for this permission set managed_policies = [ "arn:aws:iam::aws:policy/AmazonRDSReadOnlyAccess", ] } resource "aws_ssoadmin_account_assignment" "request1" { instance_arn = data.aws_ssoadmin_instances.main.arns[0] permission_set_arn = aws_ssoadmin_permission_set.request1.arn principal_id = "[email protected]" principal_type = "USER" target_id = "123456789" # Replace with your AWS Account ID target_type = "AWS_ACCOUNT" } # Ensure the user is part of the required group data "aws_identitystore_group" "request1" { identity_store_id = data.aws_ssoadmin_instances.main.identity_store_id display_name = "AD-RDS-READ-ONLY" } resource "aws_ssoadmin_group_membership" "request1" { identity_store_id = data.aws_ssoadmin_instances.main.identity_store_id group_id = data.aws_identitystore_group.request1.group_id user_ids = ["[email protected]"] } # handle request 2 for ACM full access for whole security team in all 3 AWS accounts vim main.tf # use existing main.tf file # provider "aws" { # region = "ap-southeast-2" # } # data "aws_ssoadmin_instances" "main" {} # Create the Permission Set for ACM Full Access resource "aws_ssoadmin_permission_set" "acm_full_access" { instance_arn = data.aws_ssoadmin_instances.main.arns[0] name = "ACM-FullAccess" description = "Full access to AWS Certificate Manager" session_duration = "PT1H" managed_policies = [ "arn:aws:iam::aws:policy/AWSCertificateManagerFullAccess", ] } # List of AWS account IDs # here we use terraform "locals", "dynamic" and "for_each" to loop SSO user assignment for security team within all AWS accounts locals { aws_account_ids = ["12345678901", "12345678902", "12345678903"] } # Security team members locals { security_team_members = ["[email protected]", "[email protected]", "[email protected]"] } # Ensure the users are part of the required group data "aws_identitystore_group" "acm_full_access_group" { identity_store_id = data.aws_ssoadmin_instances.main.identity_store_id display_name = "AD-ACM-FULL-ACCESS" } resource "aws_ssoadmin_group_membership" "acm_full_access_membership" { identity_store_id = data.aws_ssoadmin_instances.main.identity_store_id group_id = data.aws_identitystore_group.acm_full_access_group.group_id user_ids = local.security_team_members } # Assign the Permission Set to Each User for Each Account resource "aws_ssoadmin_account_assignment" "acm_full_access_assignments" { for_each = { for acc_id in local.aws_account_ids : acc_id => acc_id } instance_arn = data.aws_ssoadmin_instances.main.arns[0] permission_set_arn = aws_ssoadmin_permission_set.acm_full_access.arn principal_type = "USER" target_type = "AWS_ACCOUNT" dynamic "assignment" { for_each = local.security_team_members content { principal_id = assignment.value target_id = each.key } } }Terraform Modularity
How about the Terraform module, as I will get different user assignment requests with different permission sets and AWS accounts? I guess a Terraform module for SSO user assignment is the best way to make the Terraform code more clean and reusable. There are many benefits to infrastructure as code with modularity. It can reduce code duplication, is easy to update, and has a clear code structure, which fits my AWS SSO user assignment task and challenge perfectly.
To achieve this, I will need to create a folder called "sso_user_assignment_module", inside the folder it will contain:
A "main.tf" file to define the resources for creating permission sets and assigning them to users# modules/sso_account_assignment/main.tf provider "aws" { region = var.aws_region } data "aws_ssoadmin_instances" "main" {} resource "aws_ssoadmin_permission_set" "this" { for_each = var.permission_sets instance_arn = data.aws_ssoadmin_instances.main.arns[0] name = each.key description = each.value.description session_duration = each.value.session_duration managed_policies = each.value.managed_policies } resource "aws_ssoadmin_account_assignment" "this" { for_each = { for ps_key, ps_value in var.permission_sets : ps_key => ps_value.accounts } instance_arn = data.aws_ssoadmin_instances.main.arns[0] permission_set_arn = aws_ssoadmin_permission_set.this[each.key].arn principal_type = "USER" target_type = "AWS_ACCOUNT" dynamic "assignment" { for_each = each.value.users content { principal_id = assignment.value target_id = each.value.account_id } } } data "aws_identitystore_group" "this" { for_each = var.groups identity_store_id = data.aws_ssoadmin_instances.main.identity_store_id display_name = each.key } resource "aws_ssoadmin_group_membership" "this" { for_each = var.groups identity_store_id = data.aws_ssoadmin_instances.main.identity_store_id group_id = data.aws_identitystore_group.this[each.key].group_id user_ids = each.value }A "variables.tf" to define the input variables for the module
# variables.tf vim variables.tf provider "aws" { region = var.region } variable "sso_instance_arn" { description = "The ARN of the AWS SSO instance" type = string } variable "assignments" { description = "Map of account IDs to users and their permission sets" type = map(list(object({ principal_id = string permission_set_arn = string }))) } variable "region" { description = "AWS region" type = string default = "ap-southeast-2" } module "sso_account_assignments" { source = "./modules/sso_account_assignment" for_each = var.assignments sso_instance_arn = var.sso_instance_arn account_id = each.key users = each.value }A "outputs.tf" file to define the outputs of the module.
vim outputs.tf # define outputs of permission_set_arns and group_ids output "permission_set_arns" { value = { for k, v in aws_ssoadmin_permission_set.this : k => v.arn } } output "group_ids" { value = { for k, v in data.aws_identitystore_group.this : k => v.group_id } }
Now we need to create a Terraform configuration that uses this module and set the environment variables accordingly. Go back to the root folder, create a root main.tf
file to call the module and pass the necessary variables.
cd .. vim main.tf # the root main.tf file module "sso_permission_sets" { source = "./modules/aws_sso_permission_sets" aws_region = var.aws_region permission_sets = var.permission_sets groups = var.groups } # Optionally output the values output "permission_set_arns" { value = module.sso_permission_sets.permission_set_arns } output "group_ids" { value = module.sso_permission_sets.group_ids }root "variables.tf" file to define the input variables for the root configuration.
# the root variables.tf vim variables.tf variable "aws_region" { description = "The AWS region to use." type = string default = "ap-southeast-2" } variable "permission_sets" { description = "A map of permission sets with their configurations." type = map(object({ description = string session_duration = string managed_policies = list(string) accounts = map(object({ account_id = string users = list(string) })) })) } variable "groups" { description = "A map of groups with their associated user emails." type = map(list(string)) }now is the place we can reuse the module to create the root "terraform.tfvars" which provides the actual values for the variables to define each assignment request. In future we only set each request here as environment variables, and then apply the terraform module.
aws_region = "ap-southeast-2" permission_sets = { # The 1st request RDS read-only permission sets and user assignment redefine in the module using variables "RDS-ReadOnly" = { description = "Read-only access to RDS resources" session_duration = "PT1H" managed_policies = [ "arn:aws:iam::aws:policy/AmazonRDSReadOnlyAccess", ] accounts = { "123456789" = { account_id = "123456789" users = ["[email protected]"] } } } # the 2nd request security full access for ACM redefine in the module using variables "ACM-FullAccess" = { description = "Full access to AWS Certificate Manager" session_duration = "PT1H" managed_policies = [ "arn:aws:iam::aws:policy/AWSCertificateManagerFullAccess", ] accounts = { "12345678901" = { account_id = "12345678901" users = ["[email protected]", "[email protected]", "[email protected]"] }, "12345678902" = { account_id = "12345678902" users = ["[email protected]", "[email protected]", "[email protected]"] }, "12345678903" = { account_id = "12345678903" users = ["[email protected]", "[email protected]", "[email protected]"] } } } # Add 3rd request a developer needs S3 full access for 2 AWS accounts redefine in the module using variables "S3-ModifyAccess" = { description = "Modify access to S3 buckets" session_duration = "PT1H" managed_policies = [ "arn:aws:iam::aws:policy/AmazonS3FullAccess", ] accounts = { "12345678902" = { account_id = "12345678902" users = ["[email protected]"] }, "12345678903" = { account_id = "12345678903" users = ["[email protected]"] } } } } groups = { "AD-RDS-EAD-ONLY" = ["[email protected]"] "AD-ACM-FULL-ACCESS" = ["[email protected]", "[email protected]", "[email protected]"] # Optionally add a group for the developer, if needed: # "AD-S3-Modify-Access" = ["[email protected]"] }Conclusion
Now, we can achieve the task individually via terraform code and a Terraform module to handle the creation of AWS SSO users. This setup combines all three requests into a single Terraform configuration, leveraging the reusable module for creating permission sets and managing user assignments, it is more efficient, dynamic, and reusable. In future, we only define permission sets and maintain new users and assignments in the environment variables .tf file, then run Terraform apply to get the job done. The change also can be tracked when leveraging Git as version control.