AWS Lesson 58 of 123

AWS IAM Identity Center at Scale: Permission Sets, ABAC, and Federated Multi-Account Access

IAM users do not scale. The moment you run more than a handful of accounts, long-lived access keys and per-account logins become a breach waiting to happen and an audit you will fail. IAM Identity Center — the service formerly called AWS SSO — is the fix: one place to map workforce identities to accounts, short-lived credentials only, and access expressed as reusable permission sets instead of thousands of hand-rolled IAM roles. The strategic win is not “single sign-on.” It is that access becomes data — a small set of permission sets and group-to-account assignments you can review, diff in Git, and recertify on a cadence. That is the thing you simply cannot do with sprawling per-account IAM.

This guide builds that system end to end: external IdP federation over SAML and SCIM, permission-set design with managed and customer-managed policies, attribute-based access control (ABAC) that collapses N per-team permission sets into one, the Terraform that makes the model reviewable, the aws sso login workflow your engineers actually live in, and the CloudTrail/recertification audit trail your security team will ask for. Because this is a reference you will return to mid-rollout and mid-incident, every moving part is laid out as a scannable table — the prose explains the mechanism, the tables enumerate every option, default, limit and failure mode.

By the end you will stop thinking in roles and start thinking in assignments. When a new team onboards, the answer is “add them to a group in your IdP” — zero AWS changes. When an auditor asks “who used the admin role in the payments account last week,” the answer is one Athena query. And when access drifts, the answer is a Terraform plan, not a multi-day spelunk through forty near-identical policies.

What problem this solves

Plain IAM works beautifully in one account and falls apart across many. With twenty accounts and fifty engineers you face a combinatorial mess: per-account IAM users (or per-account roles assumed from a hub), long-lived keys that must be rotated and inevitably leak, and access that lives in console clicks nobody remembers making. There is no single place to answer “what can this person reach,” no short-lived-by-default credential, and no way to recertify access without reading policies by hand.

What breaks without this: a leaver keeps working credentials for weeks because off-boarding touched the IdP but not AWS. A copy-pasted iam:* statement hides in one of forty permission templates and nobody can prove the others are clean. An engineer hoards an access key in a dotfile because “the tool needs keys,” and that key ends up in a public repo. An audit asks for the access model and the team produces a spreadsheet that was stale the day it was written. Each of these is a direct consequence of access being configuration scattered per account rather than data managed centrally.

Who hits this: any organization past a single account — which is every organization that took the AWS multi-account guidance seriously. It bites hardest on platform teams running a landing zone (dozens of accounts, every team needing scoped access), regulated shops that must recertify access quarterly, and anyone who already runs a workforce IdP (Entra ID, Okta, Ping) and is duplicating its joiner/mover/leaver process inside AWS. The fix is to make the IdP the authority for who, Identity Center the authority for what-where, and to express the whole thing as code.

To frame the whole field before the deep dive, here is every concern this article covers, the question it forces, and the one place you act:

Concern What goes wrong without it First question to ask Where you act The core move
Identity source Duplicated joiner/leaver process; access drift Do you run a workforce IdP? Identity Center → Settings Federate an external IdP, don’t manage users in AWS
Authentication (SAML) Users can’t sign in / wrong user matched Does NameID match SCIM userName? IdP enterprise app Standardize both on UPN or email
Provisioning (SCIM) Groups absent; can’t pre-assign Are groups visible before login? IdP provisioning config Push only assigned groups; store the token
Permission sets Inline-policy sprawl; over-broad roles Reference or embed the policy? create-permission-set Prefer customer-managed references + a boundary
ABAC N near-identical sets diverge Can one set serve many teams? Attribute config + policy Map IdP attrs to session tags
Assignments Orphaned access nobody granted Group or user? Console or code? Terraform Assign to groups, express as IaC
Credential workflow Static keys on disk How do engineers get creds? ~/.aws/config aws sso login with a shared sso-session
Audit & recert Stale, unprovable access model Who used what, and is it still needed? CloudTrail + IaC diff Query CloudTrail; recert against code

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand IAM fundamentals: the difference between an IAM user, an IAM role, an identity-based policy and a resource-based policy, how a trust policy lets a principal assume a role, and how policy evaluation resolves an explicit Deny over any Allow. If that is shaky, read AWS IAM Fundamentals: Users, Roles, Policies & Evaluation and AWS IAM Least Privilege & Permission Boundaries first — boundaries in particular are load-bearing here. You should also know what an AWS Organization, an OU and an account are, and have a landing zone or at least multiple accounts to assign across.

This sits at the identity layer of a multi-account landing zone. It assumes the org and OU structure from AWS Control Tower: Multi-Account Landing Zone, and it pairs with the preventive-guardrail layer in AWS Organizations: SCPs, Guardrails & Delegated Admin — SCPs cap the account, the permission-set boundary caps the role, and the policies grant; the same layered model. For workload (non-human) identity it complements IAM Cross-Account Roles: External ID, Confused Deputy & Session Policies; Identity Center is for humans, those roles are for services.

A quick map of who owns and confirms each layer, so you escalate to the right team fast:

Layer What lives here Who usually owns it What it can break
Workforce IdP Users, groups, MFA, SAML/SCIM config Identity / IT team Sign-in (NameID), provisioning (SCIM scope)
Identity Center instance Identity source, ABAC config, home Region Platform / security Org-wide access; home-Region is permanent
Permission sets Policies, boundary, session duration Platform / security Over-broad access; provisioning failures
Assignment model (group, set, account) tuples in IaC Platform team Orphaned or missing access
Member accounts Provisioned AWSReservedSSO_* roles, baseline policies Account owners Missing named policies → provisioning fails
Audit pipeline CloudTrail, Athena/Lake, last-accessed Security / SOC Inability to prove who-did-what

Core concepts

Everything in Identity Center reduces to a single relationship. Hold this and the rest follows.

The assignment is the atom. An assignment is the triple (principal) × (permission set) × (target account), where the principal is a user or group from the identity source, the permission set is a template, and the target is any member account in the Organization. When you create an assignment, Identity Center provisions an IAM role named AWSReservedSSO_<PermissionSetName>_<hash> into the target account and keeps it in sync. Your users never touch that role directly — they authenticate once at the access portal, pick an account-plus-role tile, and Identity Center hands back short-lived STS credentials for the provisioned role.

A permission set is a role template, not a role. One permission set assigned to 40 accounts creates 40 IAM roles, each kept consistent by the service. Edit the permission set and you must re-provision for the change to land everywhere. This is the single biggest mental shift for people fluent in plain IAM: you are authoring a template the service stamps out, not a live object.

Credentials are always short-lived. Session duration is capped by the permission set (PT1H to PT12H) and the portal/CLI handles refresh. There is no IAM user, no access key, nothing to rotate anywhere in this model — the entire point.

Identity Center is Organizations-wide and singular. You enable it once from the management account, in a single home Region, and you should immediately delegate day-to-day administration to a dedicated account so you stop living in the management account. The home Region cannot be changed without deleting and recreating the instance, which destroys every assignment.

ABAC turns the IdP into the authority for what, not just who. By mapping IdP attributes (department, cost center) to session tags, one permission set can serve many teams: a tag-conditioned policy lets each engineer touch only the resources tagged for their own team. This collapses N near-identical permission sets into one.

The vocabulary in one table

Pin down every moving part before the deep sections. The glossary repeats these for lookup; this is the mental model side by side:

Term One-line definition Where it lives Why it matters
Instance The single Identity Center deployment One home Region Org-wide; home Region is permanent
Identity source Where users/groups come from Built-in / IdP / AD External IdP is the enterprise answer
Identity store The directory Identity Center reads Inside the instance SCIM populates it; you query it by ID
Permission set A role template The instance Becomes an AWSReservedSSO_* role per account
Assignment (principal) × (set) × (account) The instance The unit of access; your model
Provisioned role AWSReservedSSO_<set>_<hash> Each target account What the user actually assumes
Session tag IdP attribute on the STS session The token aws:PrincipalTag/* for ABAC
Permissions boundary A ceiling on the provisioned role The permission set Caps what the role can ever do
SAML Authentication / sign-in assertion IdP ↔ Identity Center Proves who at sign-in
SCIM Provisioning of users/groups IdP → Identity Center Makes groups exist before login
Access portal The user’s tile picker *.awsapps.com/start Where humans choose account + role
Delegated admin A non-mgmt account that administers IC A dedicated account Keeps you out of the mgmt account

How this differs from plain IAM — the table that resets your instincts

If you know IAM cold, these are the assumptions you must drop:

You’re used to (plain IAM) In Identity Center The trap if you forget
Creating a role you then edit live Creating a template you re-provision Editing the set doesn’t update existing roles until you re-provision
Long-lived access keys Short-lived STS only Looking for “where are the keys” — there are none
Per-account roles you wire by hand One set fanned across accounts by assignment Hand-building roles the service would build for you
Trust policy you author Trust policy the service manages Trying to edit the AWSReservedSSO_* trust policy
One account’s blast radius Org-wide blast radius from one set An over-broad set is over-broad in every assigned account
Tags you set on roles Session tags from the IdP at sign-in Hard-coding team in the policy instead of using PrincipalTag
aws configure with keys aws configure sso with a session Pasting AWS_ACCESS_KEY_ID exports into a dotfile

Limits, quotas, and the errors you will actually hit

The real numbers that constrain the design (treat these as the shape of the limits, not guaranteed exact values — confirm in Service Quotas):

Limit / quota Approximate value Why it matters
Session duration PT1HPT12H The hard cap on a permission set’s session
Managed policies per permission set up to ~20 Caps how many AWS managed policies you attach
Permissions boundary per set 1 Exactly one boundary, managed or customer-managed
Inline policy size size-capped (KB) Why large policies should be customer-managed
Permission sets per instance a few hundred The reason ABAC’s collapse-to-one matters at scale
Accounts per assignment call 1 Why you loop / for_each over accounts
SCIM bearer token lifetime ~1 year Expiry silently stops provisioning
Home Region exactly 1, permanent Can’t change without recreating the instance
Provisioning model asynchronous Assignments return IN_PROGRESS, not instant

The API errors you meet running aws sso-admin / aws identitystore, what each means, and the fix:

Error Where it appears Likely cause Fix
ResourceNotFoundException create-account-assignment Wrong instance ARN / store ID Re-capture from list-instances
ConflictException create-permission-set Name already exists Reuse or rename the set
AccessDeniedException any sso-admin call Not in mgmt/delegated-admin acct Run from the delegated admin
ValidationException (session) create-permission-set Bad SessionDuration format Use ISO-8601 PT#H
Provisioning FAILED assignment status Named policy/boundary missing in acct Bake policy into baseline; re-provision
ThrottlingException bulk loops Too many calls too fast Back off; batch via Terraform
Sign-in “user not found” Access portal NameID ≠ SCIM userName Standardize the attribute
Empty list-groups identitystore SCIM not pushing groups Fix SCIM scope/token

The identity source: directory vs external IdP vs AD

Before federation, decide the identity source. Identity Center supports three, and the choice is close to irreversible in practice (migrating sources re-maps every principal). For any real org the answer is external IdP — your joiner/mover/leaver process already runs there, and duplicating it in AWS is exactly how access drifts.

Identity source Who manages users/groups Auth mechanism Best for Hard limit / gotcha
Identity Center directory Identity Center (built-in store) Identity Center sign-in (+ optional MFA) No corporate IdP; small org or lab You now run a second user store; no enterprise SSO
External identity provider Entra ID, Okta, Ping, etc. SAML 2.0 + SCIM 2.0 The common enterprise case NameID must match SCIM userName exactly
AWS Managed Microsoft AD AWS Managed AD or on-prem via AD Connector Kerberos/LDAP via the directory AD is your source of truth, not moving to a cloud IdP Operational overhead of running a directory

Enable the service and confirm your home Region and delegation up front. Run this once from the management account, in your chosen home Region:

# List the instance (creates implicitly on first console enable).
aws sso-admin list-instances --region us-east-1

# Delegate administration to a dedicated account so you stop using the mgmt account.
aws organizations register-delegated-administrator \
  --account-id 222222222222 \
  --service-principal sso.amazonaws.com

list-instances returns the Instance ARN and the Identity Store ID — you pass these to almost every command below, so capture them:

export SSO_INSTANCE_ARN=$(aws sso-admin list-instances \
  --query 'Instances[0].InstanceArn' --output text)
export IDENTITY_STORE_ID=$(aws sso-admin list-instances \
  --query 'Instances[0].IdentityStoreId' --output text)

The instance-level settings you decide once, and how permanent each is:

Setting What it controls Changeable later? Consequence of getting it wrong
Home Region Where the instance lives No (delete + recreate) Recreating destroys every assignment
Identity source Directory / IdP / AD Yes, but disruptive Switching re-maps every principal
Delegated admin account Who can administer day-to-day Yes Living in the mgmt account = larger blast radius
Instance type Org instance vs account instance No (org is the default) Account instances can’t span the Organization
MFA policy (directory source) When/how MFA is enforced Yes Weak MFA on a privileged front door

One home Region, full stop. Pick the Region close to your IdP and your admins, enable it from the management account, and treat it as permanent. Then register a delegated admin and never administer Identity Center from the management account again.

Federating an external IdP over SAML + SCIM

Two protocols do two distinct jobs, and conflating them is the most common mistake new teams make:

You need both. SAML without SCIM means groups never appear in Identity Center and just-in-time-created users cannot be pre-assigned to accounts.

Dimension SAML 2.0 SCIM 2.0
Job Authentication (sign-in) Provisioning (directory sync)
Direction Browser → IdP → Identity Center IdP → Identity Center (push)
When it runs Every sign-in On a schedule / on change in the IdP
Carries Identity assertion (NameID + attributes) User and group objects
Without it Nobody can sign in Groups/users absent; can’t pre-assign
Key artifact IdP metadata, ACS URL, NameID SCIM endpoint URL, bearer token
Failure smell “user not found” at login Empty list-groups

The metadata swap

The exchange is a metadata swap. In the Identity Center console under Settings → Identity source → Change to External identity provider, you download the Identity Center SAML metadata and ACS URL, and upload your IdP’s metadata. In your IdP you create an enterprise application (Entra ID has a gallery app literally named “AWS IAM Identity Center”; Okta has the equivalent) and paste the Identity Center values in.

The assertion contract that matters: Identity Center expects the user’s identifier in the SAML Subject (NameID), and it must match the SCIM-provisioned userName exactly. If SAML sends UPN but SCIM provisions mail, logins fail with a “user not found” style error even though the directory looks correct. Standardize both on the same attribute — UPN or email is the usual choice.

The three common IdPs and where each maps to the Identity Center contract — the screen names differ but the job is identical:

Step Entra ID Okta Ping (PingOne / PingFederate)
Enterprise app Gallery app “AWS IAM Identity Center” OIN app “AWS IAM Identity Center” Connection / SP config
NameID source Unique User Identifier claim Application username format Subject mapping
Attribute mapping Enterprise app → Attributes & Claims App → Sign On → attribute statements Attribute contract
SCIM scope Provisioning → Sync assigned only Provisioning → To App + push groups Provisioning channel
Group sync Add groups to the app Group push rules Group provisioning
MFA / Conditional Access Conditional Access policy Okta sign-on policy / MFA Authentication policy

The artifacts you exchange, where each comes from, and where it goes:

Artifact Produced by Pasted into Purpose
Identity Center SAML metadata XML Identity Center IdP enterprise app Tells the IdP how to talk SAML to IC
ACS (Assertion Consumer Service) URL Identity Center IdP enterprise app Where the IdP posts the assertion
Issuer / Audience URI Identity Center IdP enterprise app Identifies the IC relying party
IdP SAML metadata XML IdP Identity Center Tells IC how to validate the IdP’s assertions
NameID claim mapping IdP (IdP config) Must equal SCIM userName
SCIM endpoint URL Identity Center IdP provisioning config Where the IdP pushes the directory
SCIM bearer token Identity Center (shown once) IdP provisioning config Authenticates the SCIM push

SCIM provisioning and scoping

For SCIM, Identity Center generates a SCIM endpoint URL and a bearer access token; you paste both into the IdP’s provisioning configuration:

SCIM endpoint : https://scim.<region>.amazonaws.com/<tenant-id>/scim/v2/
Bearer token  : <one-time secret shown once; store in your secrets manager>

Then scope provisioning to only the groups that need AWS — never sync the entire directory. In Entra ID that is the application’s Users and groups assignment combined with provisioning Scope = Sync only assigned users and groups; in Okta it is the app’s group-push rules.

Verify provisioning landed before going further:

# Groups pushed by SCIM should appear here.
aws identitystore list-groups \
  --identity-store-id "$IDENTITY_STORE_ID" \
  --query 'Groups[].[DisplayName,GroupId]' --output table

# And users.
aws identitystore list-users \
  --identity-store-id "$IDENTITY_STORE_ID" \
  --query 'Users[].[UserName,UserId]' --output table

The SCIM settings that bite if you get them wrong:

SCIM setting Recommended Why Failure if wrong
Provisioning scope Sync only assigned groups Avoid syncing 10,000 irrelevant users Identity store bloat; privacy/exposure
userName attribute Same as SAML NameID Sign-in matches the provisioned user “User not found” at login
Group push Only AWS-relevant groups Assignments target only real groups Phantom groups; confusing assignments
Bearer-token storage Secrets Manager, day one Token is shown exactly once Lost token → regenerate, re-enter
Token expiry tracking Calendar reminder ~1 month out Tokens expire (~1 year) New hires silently stop appearing
Deprovisioning Enable (disable on unassign) Leavers lose access automatically Orphaned access after off-boarding

The SCIM bearer token expires (around one year) and is shown exactly once. Put a calendar reminder a month out and store the token in Secrets Manager the moment it is generated. A silently expired SCIM token means new hires stop appearing in AWS and nobody notices until a ticket lands.

Designing permission sets: managed vs inline policies and session duration

A permission set carries up to four things, each becoming part of the provisioned role:

  1. AWS managed and/or customer-managed policies — attached by reference.
  2. An inline policy — embedded directly in the permission set.
  3. A permissions boundary — a ceiling on the role (highly recommended for any non-admin set).
  4. Session settingsSessionDuration (ISO-8601, PT1H to PT12H) and relay state.

Design principle: reference, do not embed. Prefer attaching a customer-managed policy by name over pasting a large inline policy. An inline policy is duplicated into every provisioned role and only updates when you re-provision; a customer-managed policy is maintained once (as a single named object you control via IaC) and the role references it. AWS managed policies are fine for coarse, well-known roles (read-only, billing) but too broad for anything privileged.

The four policy attachment styles, and when each is right:

Attachment style How it’s stored Update mechanism When to use Limit / gotcha
AWS managed policy Reference to an AWS-owned ARN AWS maintains it Coarse, well-known roles (ReadOnlyAccess, billing) Too broad for privileged sets
Customer-managed policy reference Reference by name You maintain it in each account (via IaC) The preferred path for bespoke grants Named policy must exist in every target account
Inline policy Embedded in the permission set Re-provision the set The few bespoke statements not worth a managed policy Duplicated into every role; size-capped
Permissions boundary A reference (managed or customer-managed) Re-provision the set Every non-admin set Caps the role; does not grant

Create a clean, minimal permission set with a customer-managed policy, an inline policy for the few bespoke statements, a boundary, and a one-hour session:

# 1. Create the permission set with a short session.
aws sso-admin create-permission-set \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --name "PlatformReadOnly" \
  --description "Read-only platform access, 1h sessions" \
  --session-duration "PT1H"

export PS_ARN=$(aws sso-admin list-permission-sets \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --query 'PermissionSets[?contains(@, `ps-`)]' --output text | head -n1)

# 2. Attach an AWS managed policy by ARN (coarse baseline).
aws sso-admin attach-managed-policy-to-permission-set \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --permission-set-arn "$PS_ARN" \
  --managed-policy-arn "arn:aws:iam::aws:policy/ReadOnlyAccess"

# 3. Reference a CUSTOMER managed policy (must exist by this name in every target account).
aws sso-admin attach-customer-managed-policy-reference-to-permission-set \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --permission-set-arn "$PS_ARN" \
  --customer-managed-policy-reference Name=platform-readonly-extra,Path=/

# 4. Add a permissions boundary so the role can never exceed this ceiling.
aws sso-admin put-permissions-boundary \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --permission-set-arn "$PS_ARN" \
  --permissions-boundary CustomerManagedPolicyReference={Name=platform-boundary,Path=/}

The same thing in Terraform, which is how you should actually manage it:

resource "aws_ssoadmin_permission_set" "platform_readonly" {
  name             = "PlatformReadOnly"
  description      = "Read-only platform access, 1h sessions"
  instance_arn     = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  session_duration = "PT1H"
}

resource "aws_ssoadmin_managed_policy_attachment" "ro" {
  instance_arn       = aws_ssoadmin_permission_set.platform_readonly.instance_arn
  managed_policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
  permission_set_arn = aws_ssoadmin_permission_set.platform_readonly.arn
}

resource "aws_ssoadmin_permissions_boundary_attachment" "boundary" {
  instance_arn       = aws_ssoadmin_permission_set.platform_readonly.instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.platform_readonly.arn
  permissions_boundary {
    customer_managed_policy_reference {
      name = "platform-boundary"
      path = "/"
    }
  }
}

A customer-managed policy reference is by name. The policy named platform-readonly-extra must already exist in every account you assign this permission set to, or provisioning fails for that account. This is exactly why landing-zone teams bake these named policies into account baselines (Control Tower customizations, StackSets, or Terraform) before assigning permission sets.

Session duration is a security control

Session duration is a security control, not a convenience knob. Map duration to blast radius — the more an identity can do, the shorter the window a stolen session is useful:

Permission set Suggested SessionDuration Rationale
Break-glass / org admin PT1H Minimize the window a stolen session is useful
Platform / infra engineer PT2H to PT4H Balance interruptions against exposure
Read-only / auditor PT8H Low blast radius, long analysis sessions
CI-bound human approver PT1H Privileged action, tight window
Data analyst (query only) PT4H to PT8H Long sessions, narrow surface
Contractor / third party PT1H Tighten exposure for non-employees

The permission-set fields, their valid ranges, and the limit on each:

Field Values / format Default When to change Limit / note
--name String, ≤32 chars Always Becomes part of the role name
--session-duration PT1HPT12H (ISO-8601) PT1H Match blast radius Hard cap is 12 hours
--relay-state A URL none Land users on a console page Console deep-link only
Managed policies ARNs none Coarse baselines Up to ~20 managed policies per set
Customer-managed refs Name + path none Bespoke grants Must exist in every target account
Inline policy JSON document none Few bespoke statements Size-capped; re-provision to update
Permissions boundary One managed or CM reference none Every non-admin set One boundary per set

A starter catalog of permission sets most orgs end up with — name, what it grants, its boundary, and its session — so you design the set of sets, not one at a time:

Permission set Grants (policy) Boundary Session Assign to
OrgAdmin (break-glass) AdministratorAccess none (by design) PT1H A tiny break-glass group
PlatformAdmin Infra services, no iam:* write Deny iam:*/organizations:* PT2H Platform engineers
PlatformReadOnly ReadOnlyAccess + extras read-only ceiling PT8H Platform + on-call
EngineerStandard (ABAC) Tag-conditioned compute/storage allowed-services ceiling PT4H All engineers (one group)
DataAnalyst Athena/Glue/S3 read, query only no-write ceiling PT8H Analytics group
Billing Billing + Cost Explorer billing-only ceiling PT4H Finance group
SecurityAudit SecurityAudit + read read-only ceiling PT8H Security/SOC

ABAC: IdP attributes as session tags, and tag-conditioned policies

This is where Identity Center stops being “SSO with extra steps” and becomes a force multiplier. Attribute-based access control lets one permission set serve many teams by passing IdP attributes as session tags, then writing policies whose conditions reference those tags. Instead of PlatformAccess-TeamA, PlatformAccess-TeamB, … you have one PlatformAccess set and the attribute decides what it can touch.

First, turn on attributes for access control on the instance and map IdP attributes to session-tag keys:

aws sso-admin create-instance-access-control-attribute-configuration \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --instance-access-control-attribute-configuration \
  'AccessControlAttributes=[{Key=team,Value={Source=["${path:enterprise.department}"]}},{Key=project,Value={Source=["${path:enterprise.costCenter}"]}}]'

The ${path:...} values pull from the SAML assertion / SCIM attributes your IdP sends (you also choose which attributes flow in the IdP’s attribute-mapping screen). After this, every session carries aws:PrincipalTag/team and aws:PrincipalTag/project.

The IdP attributes worth mapping to session tags, and what each enables once it’s a PrincipalTag:

Session-tag key Typical ${path:...} source Enables a policy that… Watch-out
team enterprise.department Scopes resources to the owning team Empty if dept unset → denies
project enterprise.costCenter Scopes to a project / cost code Standardize the cost-center format
env a custom IdP attribute Restricts to dev/stage/prod Often better done with account-level SCPs
region a custom IdP attribute Restricts API calls to a Region Pair with aws:RequestedRegion
email name.familyName / mail Owner-tags resources at create PII in tags — keep minimal
costCenter enterprise.costCenter Drives showback/chargeback tags Must match finance’s taxonomy

The two ABAC condition keys you will live in, and what each means:

Condition key Resolves to Use it to
aws:PrincipalTag/<key> The session tag from the IdP attribute Identify who the caller is (their team)
aws:ResourceTag/<key> The tag on the resource being touched Identify what the resource belongs to
aws:RequestTag/<key> A tag supplied in the create request Enforce tagging at create time
aws:TagKeys The set of tag keys in the request Require/forbid specific tag keys

Now write one policy that is parameterized by the tag. The classic pattern: engineers may manage only resources tagged with their own team, and must tag what they create.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ManageOwnTeamInstances",
      "Effect": "Allow",
      "Action": [
        "ec2:StartInstances",
        "ec2:StopInstances",
        "ec2:RebootInstances"
      ],
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/team": "${aws:PrincipalTag/team}"
        }
      }
    },
    {
      "Sid": "EnforceTagOnCreate",
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringEquals": {
          "aws:RequestTag/team": "${aws:PrincipalTag/team}"
        }
      }
    }
  ]
}

The same PlatformAccess permission set, assigned to a group spanning every team, now self-segments: a user whose IdP department is payments can only touch team=payments resources, with zero per-team policy authoring. This is the single highest-leverage move in the whole system — it collapses N permission sets into one and makes the IdP the authority for what as well as who.

ABAC versus the RBAC-style (one-set-per-team) approach, head to head:

Dimension RBAC (one set per team) ABAC (one set + session tags)
Permission-set count N (grows with teams) 1
Onboarding a new team New set + assignments Add to a group in the IdP
Drift risk High (sets diverge) Low (one policy)
Source of “what” Each policy, hand-authored IdP attribute
Audit surface N policies to read 1 policy + tag hygiene
Failure mode Copy-paste over-grants Untagged resources deny access
Best for A few stable roles Many similar teams

ABAC is only as trustworthy as your tagging discipline. If resources are not tagged, the condition denies (correctly) and engineers are locked out. Pair ABAC with an SCP or a RunInstances-time tag-enforcement policy so resources cannot be created without the team tag. Untagged resources are the failure mode, not malicious users.

Scaling assignments with groups, automation, and boundaries

Two non-negotiable rules at scale:

  1. Assign permission sets to groups, never to individual users. A user’s AWS access then changes the instant they join or leave a group in your IdP — no AWS ticket, no human in the loop. User-level assignments are how you end up with orphaned access nobody remembers granting.
  2. Express assignments as code. The set of (group, permission set, account) tuples is your access model; it belongs in version control with review, not in console clicks.

Creating an assignment by hand looks like this (note it provisions asynchronously):

# Resolve the group's principal id from the identity store.
GROUP_ID=$(aws identitystore get-group-id \
  --identity-store-id "$IDENTITY_STORE_ID" \
  --alternate-identifier 'UniqueAttribute={AttributePath=DisplayName,AttributeValue=platform-engineers}' \
  --query 'GroupId' --output text)

aws sso-admin create-account-assignment \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --permission-set-arn "$PS_ARN" \
  --principal-type GROUP \
  --principal-id "$GROUP_ID" \
  --target-type AWS_ACCOUNT \
  --target-id 333333333333

For real fleets, drive this from Terraform so the model is reviewable and idempotent. The pattern below fans one permission set across many accounts for one group:

locals {
  platform_accounts = ["333333333333", "444444444444", "555555555555"]
}

data "aws_ssoadmin_instances" "this" {}

resource "aws_ssoadmin_account_assignment" "platform" {
  for_each = toset(local.platform_accounts)

  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.platform_readonly.arn

  principal_id   = data.aws_identitystore_group.platform.group_id
  principal_type = "GROUP"

  target_id   = each.value
  target_type = "AWS_ACCOUNT"
}

The Terraform resources that express the whole model, so you know which block does what:

Terraform resource / data source Represents Key arguments
data.aws_ssoadmin_instances The instance (ARN + store ID) (none)
aws_ssoadmin_permission_set A permission set name, session_duration
aws_ssoadmin_managed_policy_attachment An AWS managed policy on a set managed_policy_arn
aws_ssoadmin_customer_managed_policy_attachment A customer-managed reference name, path
aws_ssoadmin_permission_set_inline_policy The inline policy on a set inline_policy (JSON)
aws_ssoadmin_permissions_boundary_attachment The boundary on a set customer_managed_policy_reference
aws_ssoadmin_account_assignment One (principal, set, account) principal_id, target_id
data.aws_identitystore_group A group’s principal ID alternate_identifier

The assignment-management choices and their trade-offs:

Choice Console click aws sso-admin CLI Terraform / IaC
Reviewable No Scriptable, but not reviewed Yes (PRs)
Idempotent Manual You handle it Yes
Drift detection None None terraform plan
Scales to 100s of accounts Painful Looped, fragile for_each
Recertification Manual read Export script Diff against code
Recommended for One-off / lab Bootstrapping Production

Combine this with the permissions boundary from the design section on every non-admin permission set. The boundary guarantees that even if a referenced policy is later widened by mistake, the provisioned role cannot exceed the ceiling. SCPs cap the account, the boundary caps the role, the policies grant — the same layered model as the rest of your org:

Control plane Scope What it does Who owns it
SCP / RCP The whole account / OU Caps the maximum permissions in the account Org / security
Permissions boundary The provisioned role Caps what this role can ever do Permission-set author
Identity policy (in the set) The provisioned role Grants permissions Permission-set author
Resource policy / tag condition The specific resource Allows/denies per resource or tag Resource owner
Session policy A single session Further narrows at assume time Rare for IC; common for assume-role

CLI and credential workflows: aws sso login, profiles, sessions

Engineers do not live in the portal; they live in a terminal. The modern flow uses SSO token provider profiles in ~/.aws/config, which share one cached login across every account/role you use.

Bootstrap it once interactively:

aws configure sso
# Prompts for the SSO start URL, SSO Region, then lets you pick account + role.
# It writes an [sso-session] block plus a [profile ...] block.

The resulting config — note the sso-session block is shared, so a single aws sso login covers all profiles that reference it:

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

[profile platform-prod]
sso_session = kloudvin
sso_account_id = 333333333333
sso_role_name = PlatformReadOnly
region = us-east-1

[profile platform-admin]
sso_session = kloudvin
sso_account_id = 333333333333
sso_role_name = PlatformAdmin
region = us-east-1

Daily use:

# Authenticate once (opens a browser, device-code flow). Token is cached.
aws sso login --sso-session kloudvin

# Now every profile sharing that session just works, with short-lived creds.
aws sts get-caller-identity --profile platform-prod
aws s3 ls --profile platform-admin

# When done, drop the cached token.
aws sso logout

The credentials minted here are temporary STS credentials capped by the permission set’s session duration and refreshed transparently from the cached SSO token — there is nothing long-lived on disk to leak. For tools that cannot speak SSO natively, aws configure export-credentials --profile platform-prod emits temporary creds (still short-lived) you can feed to a subprocess, rather than baking keys.

The ~/.aws/config keys for an SSO profile, and what each does:

Config key Belongs in Meaning Note
sso_start_url [sso-session] The access-portal URL One per Identity Center instance
sso_region [sso-session] The instance’s home Region Not your workload Region
sso_registration_scopes [sso-session] OIDC scopes for the token sso:account:access
sso_session [profile] Which sso-session to use Lets many profiles share one login
sso_account_id [profile] Target account The account behind the tile
sso_role_name [profile] The permission-set name Resolves to AWSReservedSSO_*
region [profile] Default Region for API calls Your workload Region

The credential commands and when to reach for each:

Command What it does When to use
aws configure sso Interactive bootstrap of session + profile First-time setup on a machine
aws sso login --sso-session <s> Opens browser, caches the SSO token Start of day / when the token expires
aws sso logout Clears the cached SSO token End of session on a shared machine
aws sts get-caller-identity Shows the assumed-role ARN Verify who you currently are
aws configure export-credentials Emits temporary creds (env/JSON) Feeding a tool that can’t speak SSO
aws configure list-profiles Lists configured profiles Discover what’s wired up

Never paste export AWS_ACCESS_KEY_ID=... from a permission set into a dotfile. The entire point of Identity Center is that there is no static key. If a tool “needs keys,” reach for export-credentials (ephemeral) or a workload role — not an IAM user.

Auditing access: CloudTrail, access reports, and recertification

Access you cannot audit is access you do not control. Three layers stack here.

1. CloudTrail. Sign-ins and role assumption surface as events. The federated role assumption shows up as AssumeRoleWithSAML (or the Identity Center-issued session), and the actual API calls in the member account carry the AWSReservedSSO_... role in userIdentity. Query it in Athena / CloudTrail Lake to answer “who used PlatformAdmin in account 333 last week”:

SELECT eventtime, useridentity.arn, eventname, sourceipaddress
FROM cloudtrail_logs
WHERE useridentity.arn LIKE '%AWSReservedSSO_PlatformAdmin%'
  AND eventtime > '2026-06-01T00:00:00Z'
ORDER BY eventtime DESC;

2. Identity Center access reports / last-accessed. Use the per-account-assignment and per-user views to find dormant access. The single most valuable signal is last-accessed: a group assigned a privileged permission set to an account that nobody has used in 90 days is access you should revoke.

3. Recertification. Because access is data, recertification is a diff, not an investigation. Export the current assignments and review them on a cadence:

# Enumerate every permission set provisioned into an account, then who is assigned.
for PS in $(aws sso-admin list-permission-sets-provisioned-to-account \
      --instance-arn "$SSO_INSTANCE_ARN" --account-id 333333333333 \
      --query 'PermissionSets[]' --output text); do
  echo "== $PS =="
  aws sso-admin list-account-assignments \
    --instance-arn "$SSO_INSTANCE_ARN" \
    --account-id 333333333333 \
    --permission-set-arn "$PS" \
    --query 'AccountAssignments[].[PrincipalType,PrincipalId]' --output text
done

Where each audit signal lives and what it answers:

Audit source Answers How to reach it Cadence
CloudTrail (org trail) “Who did what in which account?” Athena / CloudTrail Lake on AWSReservedSSO_* On demand / continuous
Sign-in logs “Who signed in, from where?” CloudTrail AssumeRoleWithSAML + IdP logs On demand
Last-accessed report “Is this access still used?” IC console / iam last-accessed Monthly
Assignment export “Who is granted what?” list-account-assignments loop Quarterly
Terraform state / plan “Does live match intended?” terraform plan Per change + quarterly
IdP group membership “Who is in the group?” IdP admin / SCIM Continuous (it’s the source)

The key userIdentity fields in an Identity Center CloudTrail event:

Field Example What it tells you
userIdentity.type AssumedRole The call used an assumed (SSO) role
userIdentity.arn .../AWSReservedSSO_PlatformAdmin_abc/jane@corp Which permission set and which human
userIdentity.sessionContext attributes.creationDate When the session began
eventName RunInstances The actual API action
sourceIPAddress 203.0.113.4 Where the call came from
recipientAccountId 333333333333 Which member account

Feed that into the same Git-reviewed Terraform as your source of truth: anything in the live export that is not in code is drift to investigate; anything in code that a recertifier rejects is a PR to remove. Quarterly for privileged sets, semi-annually for read-only is a defensible baseline for most compliance regimes.

Architecture at a glance

The diagram traces the path a human’s access actually takes, left to right, and pins the five failure points that bite. Start at the workforce IdP (Entra ID or Okta): it authenticates the human over SAML and pushes the directory over SCIM, so groups exist in Identity Center before anyone signs in — and the NameID it asserts must equal the SCIM-provisioned userName (badge 1, the silent sign-in killer). Next, Identity Center itself, living in one permanent home Region in the delegated-admin account: it holds the ABAC config that maps IdP attributes to session-tag keys (badge 2 — if that’s off, every tag-conditioned policy denies) and the permission set that is a role template, not a role. The assignment model(group) × (permission set) × (account), expressed as Terraform — provisions an AWSReservedSSO_* role into every target member account, each capped by a permissions boundary (badge 3 — a missing named policy or boundary in one account fails provisioning there).

Follow the path right and the role lands in each member account (badge 4 — if you edited the set but didn’t re-provision, the role is stale or absent), the user assumes it, and Identity Center stamps the IdP attributes as aws:PrincipalTag/* on the short-lived STS session. Finally, one tag-conditioned policy lets the engineer touch only resources whose team tag matches their own PrincipalTag (badge 5 — an untagged resource denies access, which feels like a bug but is the system working). The whole flow is auditable because every API call in the member account carries the AWSReservedSSO_* role in CloudTrail’s userIdentity. Read the badges as the diagnostic map: federate → provision → assume → authorize, with the exact symptom, confirm step and fix for each hop.

AWS IAM Identity Center multi-account ABAC architecture: a workforce IdP (Entra ID or Okta) federates in over SAML for authentication and SCIM for provisioning, with the SAML NameID required to match the SCIM userName; Identity Center in one permanent home Region holds the identity store, the access-control attribute configuration that maps IdP attributes such as department and cost center to session-tag keys team and project, and permission sets that are role templates with PT1H to PT12H session durations; an assignment model of group-by-permission-set-by-account expressed as Terraform provisions AWSReservedSSO_* IAM roles into every member account, each capped by a permissions-boundary ceiling that denies iam:; at sign-in the user assumes the provisioned role and Identity Center stamps IdP attributes as aws:PrincipalTag/team on the short-lived STS session, and one tag-conditioned policy lets engineers manage only resources whose team tag equals their own PrincipalTag, with every action auditable in CloudTrail via the AWSReservedSSO_ role in userIdentity; five numbered badges mark the NameID mismatch, missing session tag, missing customer-managed policy or boundary, absent provisioned role after an edit, and untagged-resource lockout

Real-world scenario

A platform team at Northwind Logistics, running ~120 accounts behind a Control Tower landing zone, had drifted into per-team permission sets: DataEng-Prod, DataEng-Dev, Payments-Prod, and so on — 14 teams × 3 environments, north of 40 permission sets, each a near-copy of its neighbors. Every new team meant a fresh batch of sets and dozens of assignments, and the sets diverged over time because nobody edited all 40 consistently. The breaking constraint arrived as an audit finding: three of those sets still granted iam:* from a long-forgotten copy-paste, and the team could not prove the others were clean without reading 40 policies by hand. The recertification reviewers, faced with 40 documents, were rubber-stamping.

They collapsed it to one EngineerStandard permission set plus ABAC. The IdP department attribute became a session tag, the per-team resource permissions became a single tag-conditioned policy, and the environment split moved to account-level SCPs instead of separate sets (prod accounts in a Prod OU, dev in a Dev OU, each with its own SCP). The decisive control was a permissions boundary on the consolidated set that made iam:* structurally impossible to grant, so the audit finding could never recur regardless of future policy edits:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BoundaryAllowedServices",
      "Effect": "Allow",
      "Action": ["ec2:*", "s3:*", "dynamodb:*", "logs:*", "cloudwatch:*"],
      "Resource": "*"
    },
    {
      "Sid": "NeverIam",
      "Effect": "Deny",
      "Action": ["iam:*", "organizations:*", "account:*"],
      "Resource": "*"
    }
  ]
}

The migration was not free. Two problems surfaced in week one. First, a wave of engineers were suddenly denied on resources they clearly owned — because those resources had never carried a team tag, and the ABAC condition (correctly) denied. The team backfilled tags with a one-off script and added a RunInstances-time RequestTag enforcement plus an SCP so nothing untagged could be created going forward. Second, the consolidated set referenced a customer-managed policy that existed in only 90 of the 120 accounts, so provisioning failed in 30; the fix was to bake the named policy into the account baseline (a Control Tower customization) and re-provision.

The decisive control was a permissions boundary on the consolidated set that made iam:* structurally impossible to grant. Outcome: 40-plus permission sets became 1, onboarding a new team became adding them to a group in the IdP (zero AWS changes), and the explicit Deny on iam:* in the boundary meant the audit finding was provably unrepeatable across all 120 accounts at once. Recertification dropped from a multi-day, 40-policy read to a single Terraform plan against the assignment model — and the reviewers actually engaged with it, because there was one policy and a list of group-to-account tuples, not a wall of near-duplicates.

The before/after, because the shape of the win is the lesson:

Dimension Before (per-team RBAC) After (one set + ABAC)
Permission sets 40+ 1
Onboarding a team New set + ~3 assignments Add to an IdP group
iam:* risk Hidden in copy-paste Boundary Deny, structurally impossible
Recertification effort Multi-day, read 40 policies One terraform plan
Environment split Separate sets Account-level SCPs
Failure mode introduced Untagged resources deny (fixed by enforcement)
Audit finding recurrence Likely Provably impossible org-wide

Advantages and disadvantages

The centralized, template-and-assignment model both solves the multi-account access problem and introduces a few sharp edges you must respect. Weigh it honestly:

Advantages (why this model helps) Disadvantages (why it bites)
Short-lived STS only — no access keys to rotate or leak A permission set is a template; edits don’t land until you re-provision
Access becomes data — review, diff, and recertify in Git The model lives in two systems (IdP for who, IC for what) you must keep in sync
One ABAC set replaces N per-team sets ABAC fails closed on untagged resources — looks like a bug, locks engineers out
The IdP is the single joiner/mover/leaver authority A SCIM-token expiry or scope mistake silently stops provisioning
Org-wide reach from one place; assignments fan across accounts Org-wide blast radius too — an over-broad set is over-broad everywhere
Provisioned roles and trust policies are service-managed Customer-managed references must pre-exist in every target account
CloudTrail carries the human in AWSReservedSSO_* for clean audit Home Region is permanent; getting it wrong means recreate + lose assignments

The model is right for any organization past a single account that runs (or will run) a workforce IdP and wants to ship access as code rather than operate per-account IAM. It bites hardest on teams that treat permission sets like live roles (forgetting to re-provision), adopt ABAC without tag enforcement (untagged-resource lockouts), or let the SCIM pipeline rot (silent off-boarding gaps). Every disadvantage is manageable — but only if you know it exists, which is the point of the tables in this article.

Hands-on lab

Stand up a permission set, create an ABAC attribute configuration, assign to a group, and verify the provisioned role and short-lived login — all driveable from a single admin shell. This assumes Identity Center is already enabled with an identity source and you have admin in the management or delegated-admin account. We tear everything down at the end; the only cost is negligible (Identity Center has no per-assignment charge).

Step 1 — Capture the instance identifiers.

export SSO_INSTANCE_ARN=$(aws sso-admin list-instances \
  --query 'Instances[0].InstanceArn' --output text)
export IDENTITY_STORE_ID=$(aws sso-admin list-instances \
  --query 'Instances[0].IdentityStoreId' --output text)
echo "Instance: $SSO_INSTANCE_ARN"
echo "Store:    $IDENTITY_STORE_ID"

Expected: both variables non-empty (an ssoins-... ARN and a d-... store ID).

Step 2 — Create a read-only permission set with a 1-hour session.

aws sso-admin create-permission-set \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --name "LabReadOnly" \
  --description "Lab read-only, 1h" \
  --session-duration "PT1H"

export PS_ARN=$(aws sso-admin list-permission-sets \
  --instance-arn "$SSO_INSTANCE_ARN" --output text \
  --query 'PermissionSets' | tr '\t' '\n' | tail -n1)
echo "Permission set: $PS_ARN"

Step 3 — Attach a coarse AWS managed policy.

aws sso-admin attach-managed-policy-to-permission-set \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --permission-set-arn "$PS_ARN" \
  --managed-policy-arn "arn:aws:iam::aws:policy/ReadOnlyAccess"

Step 4 — Turn on ABAC and map an attribute to a session tag.

aws sso-admin create-instance-access-control-attribute-configuration \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --instance-access-control-attribute-configuration \
  'AccessControlAttributes=[{Key=team,Value={Source=["${path:enterprise.department}"]}}]'

Expected: no error. Confirm it stuck:

aws sso-admin describe-instance-access-control-attribute-configuration \
  --instance-arn "$SSO_INSTANCE_ARN" --output json

Step 5 — Resolve a group and assign the set to one account. Use a group your SCIM sync (or directory) already created:

GROUP_ID=$(aws identitystore get-group-id \
  --identity-store-id "$IDENTITY_STORE_ID" \
  --alternate-identifier 'UniqueAttribute={AttributePath=DisplayName,AttributeValue=lab-engineers}' \
  --query 'GroupId' --output text)

aws sso-admin create-account-assignment \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --permission-set-arn "$PS_ARN" \
  --principal-type GROUP --principal-id "$GROUP_ID" \
  --target-type AWS_ACCOUNT --target-id 333333333333

The call returns an assignment with Status: IN_PROGRESS — provisioning is async.

Step 6 — Verify the provisioned role exists in the target account. Sign in to that account (or use a profile for it) and look for the reserved role:

aws sso login --sso-session lab
aws iam list-roles \
  --query "Roles[?starts_with(RoleName,'AWSReservedSSO_LabReadOnly')].RoleName" \
  --profile lab-target
aws sts get-caller-identity --profile lab-target

Expected: a single AWSReservedSSO_LabReadOnly_<hash> role name, and get-caller-identity returns an assumed-role ARN containing it.

Validation checklist. The lab steps mapped to what each proves:

Step What you did What it proves
1 Captured instance + store IDs The instance exists and you can address it
2–3 Created a set + attached a managed policy A permission set is a template you compose
4 Enabled ABAC, mapped team Session tags come from an IdP attribute
5 Assigned the set to a group + account The assignment is the unit of access
6 Found AWSReservedSSO_* and assumed it The set provisions a real role; creds are short-lived

Teardown (remove everything you created).

# Delete the assignment (also async).
aws sso-admin delete-account-assignment \
  --instance-arn "$SSO_INSTANCE_ARN" \
  --permission-set-arn "$PS_ARN" \
  --principal-type GROUP --principal-id "$GROUP_ID" \
  --target-type AWS_ACCOUNT --target-id 333333333333

# Then delete the permission set, and (optionally) the ABAC config.
aws sso-admin delete-permission-set \
  --instance-arn "$SSO_INSTANCE_ARN" --permission-set-arn "$PS_ARN"
aws sso-admin delete-instance-access-control-attribute-configuration \
  --instance-arn "$SSO_INSTANCE_ARN"

Cost note. Identity Center has no per-user or per-assignment charge; this lab costs effectively nothing. Delete the assignment before the permission set, since you cannot delete a set with live assignments.

Common mistakes & troubleshooting

This is the playbook — the part you bookmark. First as a scannable table you can read mid-rollout, then the expanded reasoning for the entries that bite hardest.

# Symptom Root cause Confirm (exact cmd / path) Fix
1 SAML succeeds but sign-in fails “user not found” NameID ≠ SCIM userName Compare assertion NameID to identitystore list-users UserName Standardize both on UPN/email; re-test
2 Groups never appear in Identity Center SCIM not configured / scoped out aws identitystore list-groups is empty Enable SCIM; scope to assigned groups; check token
3 New hires stop appearing weeks later SCIM bearer token expired IdP provisioning shows auth errors Regenerate token; store in Secrets Manager; reminder
4 Tag-conditioned policy denies everyone Session tag empty describe-instance-access-control-attribute-configuration; decode STS token Enable ABAC config; map the IdP attribute
5 A clearly-owned resource is denied Resource has no team tag aws ec2 describe-tags shows no team Backfill tags; enforce RequestTag + SCP
6 Provisioning fails for some accounts Customer-managed policy/boundary missing there aws iam get-policy in the account 404s Bake named policy into baseline; re-provision
7 Edited a set, change didn’t take effect Forgot to re-provision the template Role in account still has old policy provision-permission-set to re-sync
8 Account tile appears but assume fails Role not yet provisioned / wrong session duration iam list-roles shows no AWSReservedSSO_* Wait for async provision; re-provision if stuck
9 Leaver still has working access User-level assignment, or SCIM deprovision off list-account-assignments shows the user Assign to groups; enable SCIM deprovisioning
10 Region-lock SCP severed sign-in/assume SCP denies sts/iam outside one Region CloudTrail explicit Deny from a member acct Carve out iam/sts/sso in NotAction; keep us-east-1
11 Break-glass admin itself denied Protective Deny with no carve-out iam simulate-principal-policy returns Deny Exempt the break-glass role via ArnNotLike
12 aws sso login opens browser but creds fail Wrong sso_region / stale token aws configure list-profiles; check sso_region Set home Region; aws sso logout then re-login
13 Console session times out too fast/slow SessionDuration wrong for the role describe-permission-set session duration Set PT1HPT12H to match blast radius
14 Can’t change the home Region Home Region is permanent Console shows it greyed Plan a new instance + re-create assignments

The expanded form, for the entries that cost the most time:

1. SAML succeeds at the IdP but Identity Center rejects sign-in. Root cause: the SAML Subject (NameID) does not match the SCIM-provisioned userName — e.g. SAML sends UPN, SCIM provisioned mail. Confirm: read the assertion’s NameID (browser SAML trace or IdP test-sign-in), then aws identitystore list-users --identity-store-id "$IDENTITY_STORE_ID" --query 'Users[].UserName' and compare exactly. Fix: standardize both protocols on the same attribute (UPN or email), update the IdP NameID mapping and SCIM attribute mapping, and re-test sign-in.

4. A tag-conditioned policy denies every user, not just the wrong team. Root cause: aws:PrincipalTag/team is empty because the access-control attribute configuration is off, or the IdP isn’t sending the mapped attribute (department/cost center). Confirm: aws sso-admin describe-instance-access-control-attribute-configuration --instance-arn "$SSO_INSTANCE_ARN"; then decode the STS session token (or check CloudTrail requestParameters) to see whether the PrincipalTag is present. Fix: run create-instance-access-control-attribute-configuration, map the IdP attribute in the IdP’s attribute screen, and confirm the tag is present before blaming the policy.

6. Provisioning fails for a subset of accounts. Root cause: a customer-managed policy or boundary referenced by the permission set does not exist (by that name) in those accounts, so the role can’t be built there. Confirm: in the failing account, aws iam get-policy --policy-arn arn:aws:iam::<acct>:policy/<name> returns NoSuchEntity; the assignment status shows the failure reason. Fix: bake the named policy into every account baseline (Control Tower customization / StackSet / Terraform) before assigning, then provision-permission-set to retry.

7. You edited a permission set and the change didn’t take effect. Root cause: a permission set is a template — editing it does not update already-provisioned roles until you re-provision. Confirm: in a target account, inspect the AWSReservedSSO_* role’s attached policies and see the old version. Fix: aws sso-admin provision-permission-set --instance-arn "$SSO_INSTANCE_ARN" --permission-set-arn "$PS_ARN" --target-type ALL_PROVISIONED_ACCOUNTS to re-sync everywhere.

10. A Region-lock SCP severs Identity Center sign-in or assume. Root cause: a DenyOutsideApprovedRegions-style SCP denies sts:*/iam:*/sso:* org-wide because the Region you authenticate through (often us-east-1) isn’t allowed. Confirm: from inside a member account, the call returns an explicit Deny; CloudTrail shows the SCP as the cause. Fix: add iam, sts, sso, organizations, route53, cloudfront, support to the SCP’s NotAction, and keep us-east-1 allowed even for a single-Region org. (This is the same global-services carve-out covered in AWS Organizations: SCPs, Guardrails & Delegated Admin.)

Best practices

Security notes

The security controls that also prevent the operational failures above — secure and resilient pull the same direction:

Control Mechanism Secures against Also prevents
Permissions boundary Deny iam:* in the boundary Privilege escalation Audit iam:* findings recurring
Short-lived STS only No access keys in the model Leaked long-lived keys Rotation breakage
MFA at the IdP FIDO2/WebAuthn enforced Credential phishing
SCIM scoping + token in vault Assigned-groups-only sync Directory over-exposure Silent off-boarding gaps
Immutable org CloudTrail Object Lock WORM Audit tampering Lost who-did-what evidence
Break-glass ArnNotLike carve-out Exempt the emergency role Self-lockout Inability to recover after a bad guardrail
Tag-on-create enforcement RequestTag + SCP ABAC tag-dodging Untagged-resource lockouts (with backfill)

Cost & sizing

The good news on cost: IAM Identity Center has no per-user, per-permission-set, or per-assignment charge. The service itself is free. What you pay for is the surrounding machinery — the audit trail, the IdP licensing, and any directory you run — not the access model.

A rough monthly picture for a mid-size org (120 accounts, ~300 engineers):

Cost driver What you pay for Rough INR / month Note
IAM Identity Center The access model itself ₹0 No per-user/assignment charge
Org CloudTrail (mgmt events) First copy of management events ₹0 Free tier for one copy
CloudTrail Lake / data events Per-GB ingestion + retention ~₹8,000–40,000 Scales with account count + verbosity
S3 Log Archive (Object Lock) WORM storage of trails ~₹2,000–10,000 Cheap per-GB; grows over time
Athena queries Per-TB scanned on audit ~₹1,000–5,000 Partition + compress to cut scanning
AWS Managed Microsoft AD (if used) Per-directory hourly ~₹25,000–35,000 Avoided entirely with an external IdP
IdP licensing (Entra P2 / Okta) Per-user workforce identity Varies by seat Exists with or without AWS

The sizing lesson Northwind learned: the expensive part of multi-account access is never the access mechanism (it’s free) — it’s the audit retention and the human cost of keeping the model clean. ABAC pays off precisely because it shrinks the model a human has to govern from 40 sets to one.

Interview & exam questions

1. What is the core data model of IAM Identity Center? An assignment = (principal) × (permission set) × (target account). The principal is a user or group from the identity source, the permission set is a template, and creating the assignment provisions an AWSReservedSSO_<set>_<hash> IAM role into the target account that the user assumes for short-lived STS credentials.

2. Why is a permission set “not a role”? It is a role template. One set assigned to 40 accounts produces 40 IAM roles, each kept in sync by the service. Editing the set does not change existing roles until you re-provision — the most common operational surprise for people fluent in plain IAM.

3. SAML vs SCIM — what does each do and why do you need both? SAML 2.0 handles authentication (the sign-in assertion proving who the user is). SCIM 2.0 handles provisioning (pushing users/groups from the IdP so they exist in Identity Center before anyone logs in). Without SCIM, groups never appear and you can’t pre-assign access; without SAML, nobody can sign in.

4. A user authenticates successfully at the IdP but Identity Center says “user not found.” What’s wrong? The SAML NameID does not match the SCIM-provisioned userName (e.g. UPN vs email). Standardize both protocols on the same attribute and re-test. The directory can look perfectly correct while sign-in still fails.

5. How does ABAC let one permission set serve many teams? You map IdP attributes (e.g. department) to session tags via the instance’s access-control attribute configuration. Each session then carries aws:PrincipalTag/team, and a single tag-conditioned policy (aws:ResourceTag/team == aws:PrincipalTag/team) lets each engineer touch only their own team’s resources — collapsing N per-team sets into one.

6. Your ABAC policy denies everyone, not just the wrong team. Most likely cause? aws:PrincipalTag/team is empty — the access-control attribute configuration is disabled or the IdP isn’t sending the mapped attribute. Confirm with describe-instance-access-control-attribute-configuration and by decoding the STS token; fix the config/mapping before blaming the policy. ABAC fails closed.

7. Why prefer customer-managed policy references over inline policies in a permission set? An inline policy is duplicated into every provisioned role and only updates on re-provision; a customer-managed policy is maintained once as a named object (via IaC) and referenced. The catch: the named policy must exist in every target account, or provisioning fails there.

8. What does a permissions boundary on a permission set guarantee? It caps what the provisioned role can ever do, independent of the granting policies. An explicit Deny iam:* in the boundary makes privilege escalation structurally impossible even if a referenced policy is later widened — which is how you make an audit finding provably unrepeatable.

9. Why assign permission sets to groups rather than users? Group assignments mean access changes the instant someone joins or leaves a group in the IdP — no AWS ticket, no human in the loop. User-level assignments produce orphaned access nobody remembers granting and survive off-boarding.

10. How do you audit “who used the admin role in account 333 last week”? Query CloudTrail (Athena / CloudTrail Lake) where userIdentity.arn LIKE '%AWSReservedSSO_PlatformAdmin%' and recipientAccountId = 333... over the window. The reserved role in userIdentity carries both the permission set and the human, so attribution is clean.

11. Can you change the home Region after the fact? No — the home Region is permanent. Changing it means deleting and recreating the instance, which destroys every assignment. Pick the Region near your IdP and admins up front and treat it as final.

12. A Region-lock SCP broke Identity Center sign-in. Why, and what’s the fix? The SCP denied sts/iam/sso outside one approved Region, but those are global-ish and authentication often flows through us-east-1. Add iam, sts, sso, organizations, route53, cloudfront, support to the SCP’s NotAction and keep us-east-1 allowed.

These map to AWS Certified Security – Specialty (SCS-C02)identity and access management, federation, and ABAC — and to Solutions Architect Professional (SAP-C02)design for organizational complexity, multi-account access. The audit angle touches SysOps (SOA-C02). A compact cert mapping for revision:

Question theme Primary cert Objective area
Assignment model, permission sets SCS-C02 Identity & access management
SAML/SCIM federation SCS-C02 / SAP-C02 Federated access; org complexity
ABAC + session tags SCS-C02 Fine-grained authorization
Permissions boundaries SCS-C02 Least privilege at scale
Multi-account assignment design SAP-C02 Design for organizational complexity
CloudTrail audit of access SOA-C02 / SCS-C02 Monitoring; incident response

Quick check

  1. What three things make up an Identity Center assignment, and what gets provisioned when you create one?
  2. A user signs in fine at Okta but Identity Center rejects them as “user not found.” What single mismatch causes this and how do you confirm it?
  3. You map department to a team session tag and write a tag-conditioned policy, but every engineer is denied. What did you most likely forget?
  4. You edit a permission set’s policy in the console but the change doesn’t reach the roles in your accounts. Why, and what’s the fix?
  5. Why assign permission sets to groups rather than individual users, and where does that access actually change when someone leaves a team?

Answers

  1. An assignment is (principal) × (permission set) × (target account) — a user or group, a template, and a member account. Creating it provisions an AWSReservedSSO_<set>_<hash> IAM role into that account, which the user assumes for short-lived STS credentials.
  2. The SAML NameID does not match the SCIM-provisioned userName (e.g. SAML sends UPN, SCIM provisioned email). Confirm by reading the assertion’s NameID and comparing it exactly to aws identitystore list-users UserName; fix by standardizing both protocols on the same attribute.
  3. You forgot to enable the access-control attribute configuration (or the IdP isn’t sending the attribute), so aws:PrincipalTag/team is empty and the condition denies everyone. Run create-instance-access-control-attribute-configuration, map the attribute, and verify the tag is present before blaming the policy. ABAC fails closed.
  4. A permission set is a template, not a live role; edits don’t propagate until you re-provision. Run aws sso-admin provision-permission-set ... --target-type ALL_PROVISIONED_ACCOUNTS to re-sync the role into every assigned account.
  5. Group assignments mean access changes the moment someone joins or leaves a group in the IdP — no AWS ticket, no orphaned grants. The change happens in your IdP (the source of truth); SCIM/group membership flows it to AWS automatically.

Glossary

Next steps

You can now centralize human access org-wide, federate an IdP, drive least privilege with ABAC, and audit it as data. Build outward:

awsiam-identity-centerssoabacidentity
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments