AWS IAM Identity Center: Managing Multi-Account Access the Right Way

Most AWS environments start with a single account and a handful of IAM users. Then the team grows, the number of accounts multiplies, and suddenly you have engineers managing five different sets of credentials across development, staging, and production. Someone leaves the company and you spend three hours hunting down every account they had access to. Someone else gets phished and the attacker has permanent access credentials with no expiry.

AWS IAM Identity Center, previously called AWS SSO, is the managed solution to this problem. It gives you a single place to manage human access across every account in your AWS Organization. Users log in once, see every account and role they are authorized for, and get short-lived credentials that expire automatically. No long-lived access keys. No per-account IAM users. No manual access reviews hunting through twelve accounts.

In this article I will walk you through building a proper multi-account access architecture with IAM Identity Center, permission sets, group-based assignments, and the Terraform setup that keeps everything in code.

How IAM Identity Center Works

IAM Identity Center runs at the AWS Organization level. You enable it once in your management account and it applies across every account in the organization. It connects to an identity source, which can be the built-in Identity Center directory, an external IdP like Okta or Azure AD, or AWS Managed Microsoft AD.

Users and groups come from your identity source. Permission sets define what someone can do inside an AWS account, essentially a wrapper around IAM policies. Account assignments connect a user or group, a permission set, and an AWS account together. The result is: this group of people, with these permissions, in this account.

When a user logs in through the IAM Identity Center portal or the AWS CLI with SSO, they receive temporary credentials scoped to the specific account and role they selected. Those credentials are valid for a session duration you configure, typically between 1 and 12 hours, and then they expire. There are no permanent credentials to rotate, leak, or forget to revoke.

Setting Up the Organization Structure

Before configuring Identity Center, your accounts need to be organized. A clean account structure makes access management significantly simpler.

resource "aws_organizations_organization" "main" {
  aws_service_access_principals = [
    "sso.amazonaws.com",
    "organizations.amazonaws.com"
  ]

  feature_set = "ALL"
}

resource "aws_organizations_organizational_unit" "workloads" {
  name      = "Workloads"
  parent_id = aws_organizations_organization.main.roots[0].id
}

resource "aws_organizations_organizational_unit" "production" {
  name      = "Production"
  parent_id = aws_organizations_organizational_unit.workloads.id
}

resource "aws_organizations_organizational_unit" "non_production" {
  name      = "NonProduction"
  parent_id = aws_organizations_organizational_unit.workloads.id
}

resource "aws_organizations_account" "prod_app" {
  name      = "prod-application"
  email     = "aws-prod-app@yourcompany.com"
  parent_id = aws_organizations_organizational_unit.production.id

  lifecycle {
    ignore_changes = [iam_user_access_to_billing]
  }
}

resource "aws_organizations_account" "dev_app" {
  name      = "dev-application"
  email     = "aws-dev-app@yourcompany.com"
  parent_id = aws_organizations_organizational_unit.non_production.id

  lifecycle {
    ignore_changes = [iam_user_access_to_billing]
  }
}

resource "aws_organizations_account" "shared_services" {
  name      = "shared-services"
  email     = "aws-shared-services@yourcompany.com"
  parent_id = aws_organizations_organizational_unit.workloads.id

  lifecycle {
    ignore_changes = [iam_user_access_to_billing]
  }
}

Separate production and non-production accounts live in separate OUs. This matters because Service Control Policies can be applied at the OU level, giving you a hard boundary that prevents non-production accounts from ever accessing production resources, regardless of what IAM policies allow.

Permission Sets

Permission sets are the core of what IAM Identity Center manages. Each permission set becomes an IAM role in the target account when you assign it. Here are the permission sets that cover most team needs:

data "aws_ssoadmin_instances" "main" {}

locals {
  sso_instance_arn  = tolist(data.aws_ssoadmin_instances.main.arns)[0]
  identity_store_id = tolist(data.aws_ssoadmin_instances.main.identity_store_ids)[0]
}

resource "aws_ssoadmin_permission_set" "administrator" {
  name             = "AdministratorAccess"
  instance_arn     = local.sso_instance_arn
  session_duration = "PT4H"
  description      = "Full administrative access. Production accounts only for senior engineers."
}

resource "aws_ssoadmin_managed_policy_attachment" "administrator" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.administrator.arn
  managed_policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

resource "aws_ssoadmin_permission_set" "developer" {
  name             = "DeveloperAccess"
  instance_arn     = local.sso_instance_arn
  session_duration = "PT8H"
  description      = "Standard developer access. Read-write on application services, no IAM or billing."
}

resource "aws_ssoadmin_permission_set_inline_policy" "developer" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.developer.arn

  inline_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "ApplicationServices"
        Effect = "Allow"
        Action = [
          "ec2:Describe*",
          "ecs:*",
          "lambda:*",
          "s3:*",
          "rds:Describe*",
          "rds:ListTagsForResource",
          "elasticache:Describe*",
          "elasticache:List*",
          "logs:*",
          "cloudwatch:*",
          "xray:*",
          "ssm:GetParameter",
          "ssm:GetParameters",
          "ssm:GetParametersByPath",
          "secretsmanager:GetSecretValue",
          "secretsmanager:DescribeSecret"
        ]
        Resource = "*"
      },
      {
        Sid    = "DenyIAMAndBilling"
        Effect = "Deny"
        Action = [
          "iam:*",
          "organizations:*",
          "aws-portal:*",
          "account:*"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_ssoadmin_permission_set" "readonly" {
  name             = "ReadOnlyAccess"
  instance_arn     = local.sso_instance_arn
  session_duration = "PT8H"
  description      = "Read-only access. For auditors, on-call engineers, and stakeholders."
}

resource "aws_ssoadmin_managed_policy_attachment" "readonly" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.readonly.arn
  managed_policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

resource "aws_ssoadmin_permission_set" "data_engineer" {
  name             = "DataEngineerAccess"
  instance_arn     = local.sso_instance_arn
  session_duration = "PT8H"
  description      = "Access scoped to data services. Glue, Athena, S3, Redshift."
}

resource "aws_ssoadmin_permission_set_inline_policy" "data_engineer" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.data_engineer.arn

  inline_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "glue:*",
          "athena:*",
          "s3:*",
          "redshift:*",
          "redshift-data:*",
          "lakeformation:GetDataAccess",
          "logs:*",
          "cloudwatch:GetMetricData",
          "cloudwatch:ListMetrics"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_ssoadmin_permission_set" "billing_viewer" {
  name             = "BillingViewer"
  instance_arn     = local.sso_instance_arn
  session_duration = "PT4H"
  description      = "Billing and cost explorer access only. For finance and management."
}

resource "aws_ssoadmin_permission_set_inline_policy" "billing_viewer" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.billing_viewer.arn

  inline_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "aws-portal:ViewBilling",
          "ce:*",
          "budgets:ViewBudget",
          "account:GetAccountInformation"
        ]
        Resource = "*"
      }
    ]
  })
}

Session duration is set per permission set, not globally. The administrator permission set has a 4-hour session. When you are doing sensitive work in production, shorter sessions reduce the window of exposure if credentials are compromised. Developer sessions run for 8 hours since engineers need uninterrupted working time.

The developer permission set explicitly denies IAM and billing actions even though they are not granted. The explicit deny overrides any other policy that might grant those permissions, including policies attached through other mechanisms. This is a defense-in-depth measure.

Groups and Account Assignments

Assign permissions to groups, not individual users. When engineers join or leave teams, you update group membership in one place and access changes everywhere.

resource "aws_identitystore_group" "platform_engineers" {
  display_name      = "platform-engineers"
  description       = "Platform and infrastructure team"
  identity_store_id = local.identity_store_id
}

resource "aws_identitystore_group" "developers" {
  display_name      = "developers"
  description       = "Application development team"
  identity_store_id = local.identity_store_id
}

resource "aws_identitystore_group" "data_engineers" {
  display_name      = "data-engineers"
  description       = "Data engineering and analytics team"
  identity_store_id = local.identity_store_id
}

resource "aws_identitystore_group" "readonly_users" {
  display_name      = "readonly-users"
  description       = "Auditors, on-call, and stakeholders with read access"
  identity_store_id = local.identity_store_id
}

resource "aws_ssoadmin_account_assignment" "platform_prod_admin" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.administrator.arn
  principal_id       = aws_identitystore_group.platform_engineers.group_id
  principal_type     = "GROUP"
  target_id          = aws_organizations_account.prod_app.id
  target_type        = "AWS_ACCOUNT"
}

resource "aws_ssoadmin_account_assignment" "developers_dev_access" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.developer.arn
  principal_id       = aws_identitystore_group.developers.group_id
  principal_type     = "GROUP"
  target_id          = aws_organizations_account.dev_app.id
  target_type        = "AWS_ACCOUNT"
}

resource "aws_ssoadmin_account_assignment" "developers_prod_readonly" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.readonly.arn
  principal_id       = aws_identitystore_group.developers.group_id
  principal_type     = "GROUP"
  target_id          = aws_organizations_account.prod_app.id
  target_type        = "AWS_ACCOUNT"
}

resource "aws_ssoadmin_account_assignment" "data_engineers_dev" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.data_engineer.arn
  principal_id       = aws_identitystore_group.data_engineers.group_id
  principal_type     = "GROUP"
  target_id          = aws_organizations_account.dev_app.id
  target_type        = "AWS_ACCOUNT"
}

The pattern here is deliberate. Developers get full developer access in non-production and read-only access in production. Platform engineers get administrator access in production. Data engineers get scoped access to their specific services. Nobody gets more than they need.

Connecting an External Identity Provider

If your organization uses Okta, Azure AD, or another SAML 2.0 IdP, you can connect it to IAM Identity Center. Users then log in with their corporate credentials and the IdP handles authentication. IAM Identity Center handles authorization.

For Okta specifically, the setup involves creating an AWS IAM Identity Center application in Okta, downloading the SAML metadata, and configuring SCIM provisioning so users and groups sync automatically from Okta to the Identity Center directory.

resource "aws_ssoadmin_managed_policy_attachment" "external_idp" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.developer.arn
  managed_policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
}

With SCIM provisioning enabled, when you add a user to the “developers” group in Okta, they automatically appear in the Identity Center developers group and inherit all the account assignments attached to that group. When they leave the company and their Okta account is deprovisioned, their Identity Center access disappears automatically. No manual offboarding checklist. No missed accounts.

CLI Access with SSO

Engineers working from the terminal need CLI access. The AWS CLI v2 has built-in SSO support. Here is the configuration pattern:

[sso-session company]
sso_start_url  = https://your-company.awsapps.com/start
sso_region     = us-east-1
sso_registration_scopes = sso:account:access

[profile dev-developer]
sso_session   = company
sso_account_id = 111122223333
sso_role_name  = DeveloperAccess
region        = us-east-1
output        = json

[profile prod-readonly]
sso_session   = company
sso_account_id = 444455556666
sso_role_name  = ReadOnlyAccess
region        = us-east-1
output        = json

[profile prod-admin]
sso_session   = company
sso_account_id = 444455556666
sso_role_name  = AdministratorAccess
region        = us-east-1
output        = json

To authenticate, an engineer runs:

aws sso login --sso-session company

This opens a browser window, the engineer authenticates with their corporate credentials, and the CLI receives temporary credentials for all configured profiles. From that point, switching between accounts is just switching profiles:

aws s3 ls --profile dev-developer
aws ec2 describe-instances --profile prod-readonly
aws ecs list-clusters --profile prod-admin

The credentials expire according to your session duration setting. When they expire, the engineer runs the login command again. No long-lived access keys anywhere on the filesystem.

Service Control Policies: The Hard Guardrails

IAM Identity Center controls human access. Service Control Policies control what is possible inside an account, regardless of what any IAM policy allows. SCPs are the safety net that cannot be bypassed.

resource "aws_organizations_policy" "prevent_root_usage" {
  name        = "PreventRootAccountUsage"
  description = "Prevents use of root account credentials"
  type        = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid      = "DenyRootUser"
        Effect   = "Deny"
        Action   = "*"
        Resource = "*"
        Condition = {
          StringLike = {
            "aws:PrincipalArn" = "arn:aws:iam::*:root"
          }
        }
      }
    ]
  })
}

resource "aws_organizations_policy" "deny_region_lockout" {
  name        = "DenyUnauthorizedRegions"
  description = "Restricts resource creation to approved regions"
  type        = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenyUnapprovedRegions"
        Effect = "Deny"
        NotAction = [
          "iam:*",
          "organizations:*",
          "support:*",
          "sts:*",
          "cloudfront:*",
          "route53:*",
          "waf:*"
        ]
        Resource = "*"
        Condition = {
          StringNotEquals = {
            "aws:RequestedRegion" = ["us-east-1", "eu-west-1"]
          }
        }
      }
    ]
  })
}

resource "aws_organizations_policy_attachment" "prevent_root_all" {
  policy_id = aws_organizations_policy.prevent_root_usage.id
  target_id = aws_organizations_organization.main.roots[0].id
}

resource "aws_organizations_policy_attachment" "region_lockout_workloads" {
  policy_id = aws_organizations_policy.deny_region_lockout.id
  target_id = aws_organizations_organizational_unit.workloads.id
}

The region lockout SCP prevents any resource from being created outside your approved regions. This matters for compliance, cost visibility, and security. Without it, a compromised credential can spin up resources in regions you never monitor. The NotAction list excludes global services like IAM and CloudFront that are not region-specific.

Access Reviews and Auditing

IAM Identity Center integrates with AWS CloudTrail automatically. Every login, every credential issuance, and every role assumption is logged. You can query these events in CloudWatch Logs Insights or Athena against your CloudTrail S3 bucket.

fields @timestamp, userIdentity.userName, eventName, sourceIPAddress, responseElements.credentials.expiration
| filter eventSource = "sso.amazonaws.com"
| filter eventName in ["Authenticate", "GetRoleCredentials"]
| sort @timestamp desc
| limit 100

For access reviews, generate a report of all current account assignments with a simple AWS CLI query:

aws sso-admin list-account-assignments \
  --instance-arn "arn:aws:sso:::instance/ssoins-xxxxxxxxx" \
  --account-id 111122223333 \
  --permission-set-arn "arn:aws:sso:::permissionSet/ssoins-xxxxxxxxx/ps-xxxxxxxxx" \
  --query "AccountAssignments[*].{Principal:PrincipalId,Type:PrincipalType}" \
  --output table

Running this quarterly across all accounts and comparing it against your expected group membership catches access that was granted for a specific purpose and never removed, a pattern that creates unnecessary risk over time.

Closing Thoughts

IAM Identity Center is one of those services that pays for itself immediately in reduced operational overhead. The number of support tickets asking to reset credentials drops to zero. Offboarding takes one action instead of a checklist across twelve accounts. Auditors get a single pane of glass instead of a spreadsheet built from screenshots.

The key decisions are straightforward. Use groups, not individual assignments. Define permission sets by job function, not by person. Set session durations based on sensitivity. Apply SCPs at the OU level to create hard boundaries that no IAM policy can override. Connect your corporate IdP so provisioning and deprovisioning happen automatically.

If you are still managing long-lived IAM access keys and per-account IAM users across multiple accounts, moving to IAM Identity Center is the single highest-impact security improvement you can make to your AWS environment this year.

Enjoy the cloud.

Osama


#AWS #IAMIdentityCenter #AWSSSO #CloudSecurity #MultiAccount #AWSOrganizations #IdentityManagement #Terraform #InfrastructureAsCode #CloudArchitecture #DevSecOps #AmazonWebServices #SolutionsArchitect #CloudNative #CloudComputing #TechBlog #ZeroTrust #AccessManagement #ServiceControlPolicies #CloudGovernance

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.