Modularizing AWS SSO User Assignment with Terraform

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 Terraform

Let's start with handling user assignment individually with terraform first. Here I have 2 requests:

  1. 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.
  2. 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.

Streamlining AWS SSO in Complex Multi-Account Environments

Welcome to Zack's Blog

Join me for fun journey about ##AWS ##DevOps ##Kubenetes ##MLOps

  • Latest Posts