Rearranging AWS SSO Multi-Account Environment

A Comprehensive Guide to Streamline AWS SSO Permissions using AI and Terraform. (Part-1)

Tomer Zamir
8 min readJul 22, 2024
An origami-styled AI bot sitting at a desk. The bot is throwing envelopes into a folder labeled “Done,” and a laptop is open in front of it. A cloud shape is positioned above the bot’s head.

Intro

Managing user permissions in the cloud can become overwhelming over time. Properly handling permissions using Infrastructure as Code (IAC) and automation streamlines this process. It ensures permissions are presented clearly and allows for effective auditing and rollback of changes. Utilizing tools like Git, Jenkins, and AI helps create a smooth workflow for updating cloud permissions. This setup enables efficient change tracking and provides the ability to review permission changes effectively. A strong permissions-managing solution becomes crucial in complex multi-account organizations.

Manual processes are prone to errors and inconsistencies, making it difficult to track changes and ensure compliance with security policies. Each modification to user access or permissions requires meticulous documentation and verification, which is time-consuming and often lacks transparency. This can lead to potential security vulnerabilities and inefficiencies as DevSecOps teams struggle to keep up with the dynamic nature of user roles and access requirements.

Terraform’s approach allows clear, version-controlled configurations that are clear to understand. With Terraform, changes to SSO permissions can be tracked and audited effortlessly, providing a robust framework for managing access policies.

In the upcoming series of blog posts, I’ll guide you through the process of creating a robust AWS Single Sign-On (SSO) ecosystem using Terraform. We’ll automate this setup with a Jenkins pipeline and leverage AWS’s generative AI capabilities through the Claude LLM module to create an expert Terraform bot. Additionally, we’ll develop a user-friendly self-service permission system integrated with Slack for handling permission requests, all built with AWS serverless components to ensure maximum cost-efficiency.

A glimpse into the future

Gear Up!

Before diving into the automation process, ensure you have the following prerequisites:

  1. A configured identity provider connected with AWS SSO.
  2. Identity provider permissions to synchronize groups with AWS SSO.
  3. AWS IAM Role authorized to modify SSO permissions.
  4. Jenkins instance.

P1 Architecture

TF.module

The Terraform module setup operates under the premise that all resources are exclusively created and managed by a Terraform-dedicated role. Manual modifications (ClickOps) are strictly prohibited and restricted by AWS Service Control Policies (SCPs). Only specific actions on resources are permitted, ensuring consistent and controlled management through Terraform.

The module's primary functionality relies on pre-defined dynamic data statements that include permissions and variable placeholders. These data statements are tailored to meet the organization’s specific requirements and cover the most frequently requested permissions.
The module’s configuration sets up the following AWS SSO resources:

  1. A permission set.
  2. An inline policy with custom or dynamic permissions.
  3. AWS-managed polices attachments.
  4. Policy assignment of the permission set to either a user or a group (determined by the input) within a specified AWS account.
resource "aws_ssoadmin_permission_set" "permission_set" {
name = var.permission_set_name
instance_arn = local.instance_arn
session_duration = var.session_duration
tags = var.tags
description = var.permission_set_description
}

resource "aws_ssoadmin_permission_set_inline_policy" "permission_set_inline_policy" {
count = var.custom_policy != null || var.dynamic_permissions != null ? 1 : 0
inline_policy = data.aws_iam_policy_document.dynamic_permissions.json
instance_arn = aws_ssoadmin_permission_set.permission_set.instance_arn
permission_set_arn = aws_ssoadmin_permission_set.permission_set.arn
}

resource "aws_ssoadmin_managed_policy_attachment" "permission_set_managed_policy" {
for_each = toset(var.aws_managed_policies)
instance_arn = local.instance_arn
managed_policy_arn = "arn:aws:iam::aws:policy/${each.value}"
permission_set_arn = aws_ssoadmin_permission_set.permission_set.arn
}

resource "aws_ssoadmin_account_assignment" "permission_set_account_assignment" {
instance_arn = aws_ssoadmin_permission_set.permission_set.instance_arn
permission_set_arn = aws_ssoadmin_permission_set.permission_set.arn
principal_id = contains([var.identity_name], "@") ? data.aws_identitystore_user.user_id[0].id : data.aws_identitystore_group.group_id[0].id
principal_type = contains([var.identity_name], "@") ? "USER" : "GROUP"
target_id = var.account_id
target_type = "AWS_ACCOUNT"
}
variable "aws_managed_policies" {
type = list(string)
default = []
description = "AWS Managed policy names"
}

variable "account_id" {
type = string
description = "AWS Account ID"
}

variable "permission_set_name" {
type = string
description = "SSO permission set"
}

variable "identity_name" {
type = string
description = "User's full email address or IdP group name"
}

variable "custom_policy" {
type = object({
Version = string
Statement = list(object({
Effect = string
Action = list(string)
Resource = list(string)
Condition = optional(map(any))
}))
})
default = null
description = "Permission set inline policy (JSON)"
}

variable "session_duration" {
default = "PT12H"
type = string
description = "SSO Session duration"
}

variable "tags" {
default = {
managed_by = "Terraform"
}
description = "Resource tags"
type = map(any)
}

variable "permission_set_description" {
default = "managed by Terraform"
type = string
description = "SSO Permission set description"
}

variable "dynamic_permissions" {
type = object({
s3_buckets_read = optional(list(string))
s3_buckets_write = optional(list(string))
s3_buckets_delete = optional(list(string))
allowed_read_sqs = optional(list(string))
allowed_write_sqs = optional(list(string))
allowed_delete_sqs = optional(list(string))
})
default = null
}
locals{
identity_store_id = tolist(data.aws_ssoadmin_instances.idp.identity_store_ids)[0]
instance_arn = tolist(data.aws_ssoadmin_instances.idp.arns)[0]
}

data "aws_ssoadmin_instances" "idp" {}

data "aws_identitystore_user" "user_id" {
count = contains([var.identity_name], "@") ? 1 : 0
identity_store_id = local.identity_store_id
alternate_identifier {
unique_attribute {
attribute_path = "UserName"
attribute_value = var.identity_name
}
}
}

data "aws_identitystore_group" "group_id" {
count = !contains([var.identity_name], "@") ? 1 : 0
identity_store_id = local.identity_store_id
alternate_identifier {
unique_attribute {
attribute_path = "DisplayName"
attribute_value = var.identity_name
}
}
}

data "aws_iam_policy_document" "dynamic_permissions" {
dynamic "statement" {
for_each = can(length(var.dynamic_permissions.s3_buckets_read) > 0) ? ["apply"] : []
content {
actions = [
"s3:Get*",
"s3:List*",
"s3-object-lambda:Get*",
"s3-object-lambda:List*"
]
sid = "S3AllowReadBuckets"
effect = "Allow"
resources = flatten([
for bucket_name in var.dynamic_permissions.s3_buckets_read : [
"arn:aws:s3:::${bucket_name}/*",
"arn:aws:s3:::${bucket_name}"
]
])
}
}

# Write S3 Buckets
dynamic "statement" {
for_each = can(length(var.dynamic_permissions.s3_buckets_write) > 0) ? ["apply"] : []
content {
actions = [
"s3:Get*",
"s3:List*",
"s3-object-lambda:Get*",
"s3-object-lambda:List*",
"s3:PutObject*"
]
sid = "S3AllowWriteBuckets"
effect = "Allow"
resources = flatten([
for bucket_name in var.dynamic_permissions.s3_buckets_write : [
"arn:aws:s3:::${bucket_name}/*",
"arn:aws:s3:::${bucket_name}"
]
])
}
}

# Delete S3 Buckets
dynamic "statement" {
for_each = can(length(var.dynamic_permissions.s3_buckets_delete) > 0) ? ["apply"] : []
content {
actions = [
"s3:Get*",
"s3:List*",
"s3-object-lambda:Get*",
"s3-object-lambda:List*",
"s3:PutObject*",
"s3:DeleteObject*"
]
sid = "S3AllowDeleteBuckets"
effect = "Allow"
resources = flatten([
for bucket_name in var.dynamic_permissions.s3_buckets_delete : [
"arn:aws:s3:::${bucket_name}/*",
"arn:aws:s3:::${bucket_name}"
]
])
}
}

# List S3 Buckets
dynamic "statement" {
for_each = can(length(var.dynamic_permissions.s3_buckets_read) > 0) || can(length(var.dynamic_permissions.s3_buckets_write) > 0) || can(length(var.dynamic_permissions.s3_buckets_delete.s3_buckets) > 0) ? ["apply"] : []
content {
actions = [
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketPolicyStatus",
"s3:GetBucketAcl",
"s3:GetAccountPublicAccessBlock",
"s3:ListAccessPoints",
"s3:GetBucketLocation",
"s3:ListAllMyBuckets"
]
sid = "S3AllowListBuckets"
effect = "Allow"
resources = ["*"]
}
}

# SQS Read Access
dynamic "statement" {
for_each = can(length(var.dynamic_permissions.sqs_read) > 0) ? ["apply"] : []
content {
sid = "AllowSQSReadAccess"
effect = "Allow"
actions = [
"sqs:ReceiveMessage"
]
resources = [for sqs_name in var.dynamic_permissions.sqs_read : "arn:aws:sqs:us-west-1:${var.account_id}:${sqs_name}"]
}
}

# SQS Write Access
dynamic "statement" {
for_each = can(length(var.dynamic_permissions.sqs_write) > 0) ? ["apply"] : []
content {
sid = "AllowSQSWriteAccess"
effect = "Allow"
actions = [
"sqs:StartMessageMoveTask",
"sqs:CancelMessageMoveTask",
"sqs:ChangeMessageVisibility",
"sqs:ReceiveMessage",
"sqs:SendMessage"
]
resources = [for sqs_name in var.dynamic_permissions.sqs_write : "arn:aws:sqs:us-west-1:${var.account_id}:${sqs_name}"]
}
}

# SQS Delete Access
dynamic "statement" {
for_each = can(length(var.dynamic_permissions.sqs_delete) > 0) ? ["apply"] : []
content {
sid = "AllowSQSDeleteAccess"
effect = "Allow"
actions = [
"sqs:StartMessageMoveTask",
"sqs:CancelMessageMoveTask",
"sqs:ChangeMessageVisibility",
"sqs:ReceiveMessage",
"sqs:SendMessage",
"sqs:DeleteMessage",
"sqs:PurgeQueue"
]
resources = [for sqs_name in var.dynamic_permissions.sqs_delete : "arn:aws:sqs:us-west-1:${var.account_id}:${sqs_name}"]
}
}
}

TF.env

The following folder structure simplifies the AWS SSO environment by organizing resources and configurations in a clear, logical manner. Separating the configurations based on group names and further dividing them into individual accounts ensures that each group’s access and permissions are distinctly defined and easily manageable. The use of dedicated account .tf files for each account within a group allows for precise control and easier troubleshooting. This structure promotes clarity and streamlines the management of access policies, making the entire AWS SSO environment more efficient and easier to maintain.

terraform-environments/
└── sso-account/
├── idp-group-name-1/
│ ├── account-1.tf
│ ├── account-2.tf
│ ├── account-3.tf
│ ├── account-4.tf
│ ├── main.tf
│ └── provider.tf
└── idp-group-name-2/
├── account-1.tf
├── account-2.tf
├── account-3.tf
├── account-4.tf
├── main.tf
└── provider.tf
module "<group_name>_<account_name>" {
source = "path_to_source_module"
permission_set_name = "<group_name>_<account_name>"
account_id = "account_id"
aws_managed_policies = [
"AWSSupportAccess",
"job-function/ViewOnlyAccess"
]
identity_name = "<group_name>"
dynamic_permissions = {
s3_buckets_write = ["bucket_name_1", "bucket_name_2"]
}
}
terraform {
required_version = "=1.9.2"
required_providers {
aws = {
source = "hashicorp/aws"
version = "=5.59.0"
}
}
}

provider "aws" {
region = "us-east-1"
assume_role {
role_arn = "arn:aws:iam::<sso-account-id>:role/<ROLE_AUTHORIZED_FOR_SSO_MODIFICATIONS>"
session_name = "SSOAutomationSession"
}
}

Jenkins Pipeline

This Jenkins pipeline automates the deployment of a Terraform code by typing the group’s name. Initially, it installs tfswitch to manage Terraform versions. The pipeline proceeds to initialize the Terraform configuration, followed by generating an execution plan to preview the actions Terraform will perform. Before applying the changes, an approval stage is included, where the user must type apply to proceed. If approved and running on the main branch, the pipeline applies the configuration.

pipeline {
agent any
options {
ansiColor('xterm')
}
parameters {
string(name: 'GROUP_NAME', defaultValue: '', description: 'SSO Group Name to deploy')
}
stages {
stage('Install tfswitch') {
steps {
sh """
curl -L https://raw.githubusercontent.com/warrensbox/terraform-switcher/release/install.sh | bash
"""
}
}
stage('Install Terraform using tfswitch') {
steps {
dir("<terraform-environments>/<sso-account-name>/${GROUP_NAME}") {
sh """
tfswitch
"""
}
}
}
stage('Init') {
steps {
dir("<terraform-environments>/<sso-account-name>/${GROUP_NAME}") {
script {
try {
echo 'Terraform Init'
sh 'terraform init'
} catch (Exception ex) {
echo 'Exception occurred during Init: ' + ex.toString()
currentBuild.result = 'FAILURE'
error "The pipeline failed to initialize"
}
}
}
}
}
stage('Plan') {
steps {
dir("<terraform-environments>/<sso-account-name>/${GROUP_NAME}") {
script {
try {
echo 'Terraform Plan'
sh 'terraform plan'
} catch (Exception ex) {
echo 'Exception occurred during Plan: ' + ex.toString()
currentBuild.result = 'FAILURE'
error "The pipeline failed to plan"
}
}
}
}
}
stage('Approval') {
steps {
script {
def userInput = input message: 'Do you want to apply the changes?', ok: 'Apply', parameters: [string(defaultValue: '', description: 'Type "Apply" to proceed', name: 'Approval')]
if (userInput != 'Apply') {
error 'User did not approve the changes.'
}
}
}
}
stage('Apply') {
when {
branch 'main'
}
steps {
dir("<terraform-environments>/<sso-account-name>/${GROUP_NAME}") {
script {
try {
echo 'Terraform Apply'
sh 'terraform apply -input=false -auto-approve'
} catch (Exception ex) {
echo 'Exception occurred during Apply: ' + ex.toString()
currentBuild.result = 'FAILURE'
error "The pipeline failed to apply"
}
}
}
}
}
}
post {
always {
cleanWs()
}
}
}

SCP!

Suggested SCP for AWS SSO account hardening to prevent manual changes and Terraform drifts:

{
"Statement": [
{
"Action": [
"sso:AssociateProfile",
"sso:AttachCustomerManagedPolicyReferenceToPermissionSet",
"sso:AttachManagedPolicyToPermissionSet",
"sso:CreateAccountAssignment",
"sso:CreateInstanceAccessControlAttributeConfiguration",
"sso:CreatePermissionSet",
"sso:DeleteAccountAssignment",
"sso:DeleteInlinePolicyFromPermissionSet",
"sso:DeletePermissionSet",
"sso:DeleteProfile",
"sso:DetachCustomerManagedPolicyReferenceFromPermissionSet",
"sso:DetachManagedPolicyFromPermissionSet",
"sso:DisassociateProfile",
"sso:ProvisionPermissionSet",
"sso:PutInlinePolicyToPermissionSet",
"sso:PutPermissionsPolicy",
"sso:UpdateApplicationProfileForAWSAccountInstance",
"sso:UpdateInstanceAccessControlAttributeConfiguration",
"sso:UpdateSsoConfiguration"
],
"Condition": {
"StringNotLike": {
"aws:PrincipalArn": [
"arn:aws:iam::${aws:PrincipalAccount}:role/stacksets-exec-*",
"arn:aws:iam::${aws:PrincipalAccount}:role/AWSControlTowerExecution",
"arn:aws:iam::${aws:PrincipalAccount}:role/<ROLE_AUTHORIZED_FOR_SSO_MODIFICATIONS>"
]
}
},
"Effect": "Deny",
"Resource": [
"*"
],
"Sid": "DenySSOManualChanges"
}
],
"Version": "2012-10-17"
}

Conclusion

Streamlined configurations for multi-account environments using Terraform automation bring efficiency and consistency and support compliance with your organization’s security policies. With AWS SSO, managing access to AWS resources across multiple accounts becomes straightforward. This ensures users have the correct permissions based on their roles and responsibilities

Review and update your AWS SSO configurations regularly to adapt to changes in your organizational structure or access requirements. This practice ensures that users maintain appropriate access to AWS resources while keeping the environment secure and compliant.

Please note that the suggested configuration is not intended for production use. Do NOT implement these configurations in a production environment until you have thoroughly hardened and tightened the permission sets. Customize the dynamic statements to fit your organization’s specific needs and most frequently requested permissions. Ensure that all security measures are in place to protect your AWS resources and maintain a secure environment.

--

--