diff --git a/content/blog/aws/identity-center.md b/content/blog/aws/identity-center.md new file mode 100644 index 0000000..f7ede3d --- /dev/null +++ b/content/blog/aws/identity-center.md @@ -0,0 +1,315 @@ +--- +title: 'AWS Identity Center with OpenTofu/Terraform' +description: 'Centralized access control with free sub-accounts' +date: '2025-07-01' +tags: +- 'AWS' +- 'OpenTofu' +- 'Terraform' +--- + +## Introduction + +Many people make mistakes and create immediate tech debt when they get started +with AWS. They rightfully fear that everything is going to be expensive and try +to keep it under control by cramming everything in a single VPC in a single AWS +Account. + +This is a big mistake though, because creating multiple AWS accounts (which are +administrative objects isolating everything) is free, and using IAM Identity +Center (successor to AWS Single Sign-On) is a great way to centralize access +control to a multitude of AWS accounts. Separating each environment or project +into its own sub-account is a great way to control security boundaries as well +as keep an eye on costs per AWS sub-account. + +I recently took the time to advise a former colleague and friend about getting +started on AWS: here are the resulting notes. + +## Bootstrapping AWS Identity Center + +Sadly not everything can be automated with OpenTofu/Terraform. The initial +bootstrap must be performed by clicking through the AWS web console: +- Login to your root admin account. +- Select the primary AWS region you will operate in. +- Use the Search Bar to navigate to the IAM Identity Center console. +- Click on the orange `Enable` button to activate Identity Center for your + organization. +- Configure your Identity Source. You can use the default AWS Identity Center + directory like I do, or connect an external identity provider. +- Configure the AWS access portal URL as well as your Instance name. +- Configure Multi Factor Authentication. + +With these settings out of the way, everything else can be automated with +OpenTofu/Terraform. + +## Creating sub-accounts + +I use something close to the following input variable in order to manage my +additional AWS accounts: + +``` hcl +variable "aws_accounts" { + description = "AWS accounts to manage." + nullable = false + type = map(object({ + email = string + ou = optional(string, null) + })) +} +``` + +Here is an example `terraform.tfvars` file provisioning this data structure: + +``` hcl +aws_accounts = { + core = { + email = "julien.dessaux+aws-core@adyxax.eu" + ou = "core-engineering" + } + root = { + email = "julien.dessaux+aws-root@adyxax.eu" + } + tests = { + email = "julien.dessaux+aws-tests@adyxax.eu" + ou = "core-engineering" + } +} +``` + +You might ask yourselves why I use an `email` attribute that looks pretty easy +to derive consistently from the account name. I do this because creating and +deleting AWS accounts is easy! Though the account names can be reused, the email +address cannot. + +After a few years of projects creations and deletions, you will happen to reuse +an account name and will need a different email address. This data structure +helps me remember that and I keep a list of former accounts email addresses in a +comment above this structure. + +I manage the Organization Units (OUs) with: +``` hcl +locals { + ous = toset([for name, info in var.aws_accounts : + info.ou if info.ou != null + ]) +} + +data "aws_organizations_organization" "org" {} + +resource "aws_organizations_organizational_unit" "ou" { + for_each = local.ous + + name = each.key + parent_id = data.aws_organizations_organization.org.roots[0].id +} +``` + +And I manage the AWS accounts using the following configuration: + +``` hcl +data "aws_ssoadmin_instances" "root" {} + +locals { + identity_center_arn = data.aws_ssoadmin_instances.root.arns[0] + identity_center_store_id = data.aws_ssoadmin_instances.root.identity_store_ids[0] +} + +resource "aws_organizations_account" "main" { + for_each = var.aws_accounts + + close_on_deletion = true + email = each.value.email + name = each.key + parent_id = each.value.ou != null ? aws_organizations_organizational_unit.ou[each.value.ou].id : null + + lifecycle { + ignore_changes = [role_name] + } +} +``` + +The `ignore_changes` lifecycle entry allows importing existing accounts into +this automation, which I do for the root account itself. + +## Managing user accounts + +I use something close to the following input variable in order to manage +Identity Center user accounts: + +``` hcl +variable "users" { + description = "Users to manage accounts for." + nullable = false + type = map(object({ + admin = object({ + aws = bool + }) + display_name = optional(string, null) + email = string + family_name = optional(string, null) + given_name = optional(string, null) + })) +} +``` + +Here is an example `terraform.tfvars` file provisioning this data structure: + +``` hcl +users = { + julien-dessaux = { + admin = { aws = true } + email = "julien.dessaux@adyxax.org" + } +} +``` + +The following local variable augments the user accounts by setting defaults for +the optional fields. I use the convention that all usernames follow the +`-` format and this handles cases where a user's display +name, family name or given name do not exactly fit this scheme: + +``` hcl +locals { + users = { for username, info in var.users : + username => merge( + info, + info.display_name == null ? { display_name = title(replace(username, "-", " ")) } : {}, + info.family_name == null ? { family_name = split(" ", title(replace(username, "-", " ")))[1] } : {}, + info.given_name == null ? { given_name = split(" ", title(replace(username, "-", " ")))[0] } : {}, + ) + } +} +``` + +Creating the actual IAM Identity Center user is done with: + +``` hcl +resource "aws_identitystore_user" "main" { + for_each = local.users + + display_name = each.value.display_name + emails { + primary = true + value = each.value.email + } + identity_store_id = local.identity_center_store_id + name { + family_name = each.value.family_name + given_name = each.value.given_name + } + user_name = each.key +} +``` + +Now that we have users provisioned, let's grant them permissions on our +infrastructure. + +## Granting admin access + +An IAM Identity Center group can be created with: + +``` hcl +resource "aws_identitystore_group" "admin" { + display_name = "admin" + identity_store_id = local.identity_center_store_id +} +``` + +Assigning members to this group is a matter of: + +``` hcl +resource "aws_identitystore_group_membership" "admin" { + for_each = { for username, info in var.users : + username => info if info.admin.aws + } + + group_id = aws_identitystore_group.admin.group_id + identity_store_id = local.identity_center_store_id + member_id = aws_identitystore_user.main[each.key].user_id +} +``` + +Permissions are granted through permission sets of attached policies: + +``` hcl +resource "aws_ssoadmin_permission_set" "admin" { + instance_arn = local.identity_center_arn + name = "admin" + session_duration = "PT12H" +} + +resource "aws_ssoadmin_managed_policy_attachment" "admin" { + instance_arn = local.identity_center_arn + managed_policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" + permission_set_arn = aws_ssoadmin_permission_set.admin.arn +} + +resource "aws_ssoadmin_account_assignment" "admin" { + for_each = aws_organizations_account.main + + instance_arn = local.identity_center_arn + permission_set_arn = aws_ssoadmin_permission_set.admin.arn + principal_id = aws_identitystore_group.admin.group_id + principal_type = "GROUP" + target_id = each.value.id + target_type = "AWS_ACCOUNT" +} +``` + +## Generating your AWS CLI configuration file + +I rely on the following OpenTofu/Terraform template to generate my +`~/.aws/config` file: + +``` hcl +[default] +region = eu-west-3 +sso_session = adyxax + +[sso-session adyxax] +sso_start_url = https://adyxax.awsapps.com/start +sso_region = eu-west-3 +sso_registration_scopes = sso:account:access + +%{~for name, id in accounts} +[profile ${name}] +sso_account_id = ${id} +sso_role_name = admin +sso_session = adyxax +%{endfor~} +``` + +Using this template, I output my configuration with: + +``` hcl +output "aws_config" { + value = templatefile("./aws_config", { + accounts = { for name, info in aws_organizations_account.main : + name => info.id + } + }) +} +``` + +Each morning, I log in with: + +``` shell +aws sso login +``` + +To access a specific account, I use the `--profile` CLI flag: + +``` shell +aws --profile core s3 ls +``` + +## Conclusion + +Starting your AWS journey with multiple accounts and centralized access +management as shown in this article will help you avoid quite a few pitfalls. +Though there are a few clicks to perform for the initial setup, everything +important can be automated quite well. + +I recommend everyone to make the effort to commit to this approach from the +beginning in order to have a scalable, secure and cost-effective AWS environment +at your disposal.