In a nutshell
Level: Advanced (with a beginner on-ramp) · Time: ~61 min read
Imagine a corporate campus with dozens of buildings. Each building is an AWS account. The old, painful way to let people in was to cut a physical key for every person for every building — thousands of keys, impossible to track, quietly copied, and when someone leaves the company you can never be sure you collected them all. That is exactly what happens when you create an IAM user with an access key for each person in each account: long-lived credentials scattered everywhere, no single off-switch, and one leaked key is a standing breach.
IAM Identity Center (the service AWS used to call “AWS SSO”) replaces the pile of keys with a badge system. Every employee carries one badge — their normal corporate identity from Microsoft Entra ID, Okta, or Ping. They tap it at a central security desk (the AWS access portal), and the door reader at each building checks their record live and issues a time-limited visitor pass (short-lived credentials, valid an hour or a few, never stored) for exactly the rooms their role allows. When HR marks someone a leaver, the badge stops working in every building at once. No keys to collect.
Three ideas carry the whole lesson. A permission set is a named access level programmed into the badge system — “Operator”, “Auditor”, “Admin” — that Identity Center turns into a matching IAM role inside each account. An assignment is the three-way rule “this group gets this permission set in this account.” And ABAC (attribute-based access control) lets the badge also carry your department so that doors open only to rooms tagged for your department — one rule that scopes hundreds of teams to their own resources without a separate badge type per team.
If you are brand new: you do not need to have built any of this to follow along. Read it as “how humans should get into a well-run multi-account AWS estate, and why the obvious way is the wrong way.” If you are experienced: this lesson pins down the mechanics — the generated AWSReservedSSO_* roles, the SAML/SCIM split, aws:PrincipalOrgID trust conditions, ABAC session tags, centralized root access, and trusted identity propagation — with copy-correct Terraform and JSON.
Prerequisites. You should be comfortable with the idea of multiple AWS accounts under AWS Organizations and with basic IAM (users, roles, policies). If those are fuzzy, skim IAM fundamentals and the Organizations landing-zone lesson first. This is part 6 of the landing-zone series, so the account chassis (management, Log-Archive, Audit accounts, account vending) is assumed to exist.
After this lesson you will be able to (1) explain why IAM users for humans are an anti-pattern and what replaces them; (2) design a small permission-set catalog by job function and express it as Terraform; (3) federate Identity Center to an external IdP with SAML for sign-in and SCIM for provisioning; (4) write a correct cross-account trust policy with aws:PrincipalOrgID and ExternalId; (5) scope one operator role to every team with an ABAC policy; and (6) reason about centralized root access, trusted identity propagation, and where Amazon Cognito fits instead.
Where this fits
Parts 1–2 of AWS Landing Zone & Control Tower built the multi-account chassis (AWS Organizations, the management/Log-Archive/Audit accounts, account vending) and the managed orchestration on top of it (Control Tower’s landing zone, Account Factory, the controls library); the network and governance parts then gave those accounts connectivity and guardrails. This is part 6, and it answers the question every one of those accounts immediately raises: how do humans actually get in? The wrong answer — IAM users with long-lived access keys minted account-by-account — is the single most common way a beautifully-governed landing zone rots into an ungovernable sprawl of credentials. The right answer is AWS IAM Identity Center (the service formerly called AWS SSO) as the single front door to every account in the org, federated to your corporate identity provider, handing out short-lived, role-based, least-privilege access through permission sets, scoped by attribute-based access control (ABAC). Identity Center is, in fact, one of the artifacts Control Tower stands up for you at landing-zone setup — this part is about designing it deliberately rather than accepting the default and walking away.

IAM Identity Center (SSO)
What it is
AWS IAM Identity Center is the AWS-managed service that provides workforce single sign-on across your entire AWS Organization and to SAML-/OIDC-enabled business applications. It replaced “AWS SSO” in name in 2022 but the concept is the same: a central place where a human authenticates once and is then presented with a portal of every AWS account and role they’re entitled to, plus any cloud apps you’ve wired in. Mechanically, Identity Center is an org-level service — you enable it from the management account (or, better, delegate its administration to a member account), it is anchored in a single home Region, and it sits on top of AWS Organizations so it can see and grant access into every member account at once.
It has three load-bearing concepts that the rest of this article builds on:
- An identity source — where users and groups come from. This is either the built-in Identity Center directory (Identity Center is its own small IdP), an external IdP federated via SAML 2.0 + SCIM (Microsoft Entra ID, Okta, Ping, Google Workspace, etc.), or AWS Managed Microsoft AD / AD Connector. You pick exactly one.
- Permission sets — named, reusable bundles of IAM policy that define what an assigned identity can do in an account. (Its own deep section below.)
- Assignments — the three-way binding of (user or group) × (permission set) × (AWS account) that grants access. Identity Center realizes each assignment by provisioning an IAM role named
AWSReservedSSO_<permission-set>_<hash>into the target account and managing its trust and policies for you.
When a user signs in to the AWS access portal and picks an account+role, Identity Center performs an sts:AssumeRoleWithSAML-style federation behind the scenes and drops them into the console with temporary credentials; the CLI/SDK path is aws sso login backed by the IAM Identity Center OIDC token flow. No IAM user, no access key, ever.
Why it matters
Identity Center is the layer where least privilege either becomes real across hundreds of accounts or quietly dies. Before it (and the old pattern many estates are still digging out of), human access meant one of two bad things: an IAM user per person per account (an explosion of credentials, no central off-boarding, long-lived keys that leak — see this site’s own history of leaked DB credentials for why that matters), or a single “jump” account with a forest of cross-account IAM roles that every team hand-maintained. Identity Center collapses both into one model:
| Property | IAM users (the anti-pattern) | IAM Identity Center |
|---|---|---|
| Credentials | Long-lived passwords + access keys per account | Short-lived (1–12 h) session tokens, no stored keys |
| Off-boarding | Delete the user in N accounts (always missed somewhere) | Deactivate once in the IdP; SCIM removes access org-wide |
| MFA | Configured (or not) per account | Enforced centrally; IdP MFA / Conditional Access flows through |
| Auditing | CloudTrail per account, no identity correlation | One identity (the IdP user) correlated across every account |
| Onboarding a new account | Re-create users/roles by hand | Assign a permission set to a group — done |
| Where policy lives | Scattered IAM, per account | Centralized, reusable permission sets |
The deeper reason it matters in a landing zone specifically: account vending (Part 1/2) produces accounts continuously, and Identity Center is the only sane way to grant the right humans access to a brand-new account at the moment it’s vended — AFT/Control Tower assigns a permission set to a group and the role materializes automatically. Identity is the human-facing twin of account vending.
How to do it well
- Delegate Identity Center administration out of the management account. Just as you push GuardDuty/Security Hub to the Audit account, register a member account (often a dedicated Identity or shared-services account) as the delegated administrator for Identity Center, so day-to-day permission-set and assignment work doesn’t require management-account access. A short list of tasks (e.g., changing the identity source, management-account assignments themselves) still must happen in the management account — know which.
- Pick the home Region on purpose and don’t move it. Identity Center is single-Region for its configuration; changing it means recreating all permission sets and assignments. Anchor it in your primary operating/residency Region (for an India estate,
ap-south-1). - Federate to your existing corporate IdP — don’t run a parallel directory of humans. The built-in directory is fine for a tiny org or a break-glass path, but for an enterprise your source of truth for who works here is already Entra ID / Okta. Federate (next section) so joiners/leavers are handled once, by HR-driven processes, and flow in via SCIM.
- Keep a break-glass path that does not depend on the IdP. If your SAML federation or the external IdP is down, you still need in. Maintain one or two emergency users in the built-in Identity Center directory (or a vaulted management-account root with MFA) with a high-privilege permission set, excluded from normal flows, credentials split and sealed, and a CloudTrail/EventBridge alarm that fires the instant they’re used.
- Turn on session controls. Set permission-set session duration to the shortest workable value, and enable Identity Center sign-in / sign-out logging to CloudTrail. Long 12-hour sessions on an admin permission set are a standing risk.
Concrete artifacts, decisions, and AWS tools
- Artifacts: an Identity Center deployment with a chosen identity source; the delegated-administrator registration; a documented home Region; the break-glass user/runbook; the access-portal URL distributed to staff; session-duration standards per permission-set tier.
- Decisions: identity source (external IdP vs AWS Managed AD vs built-in); which account is the Identity Center delegated admin; home Region; break-glass mechanism; default and maximum session durations.
- Tools/services: AWS IAM Identity Center, AWS Organizations (the substrate it grants across), AWS STS (the temporary-credential engine under every sign-in), AWS CloudTrail (sign-in and assignment audit), and AWS Control Tower (which enables Identity Center as part of the landing zone).
The sign-in flow, made concrete
The prose above says “a human authenticates once and gets short-lived credentials.” Here is exactly what that looks like from a laptop, so the abstraction becomes muscle memory.
A user opens the AWS access portal URL (e.g. https://meridian.awsapps.com/start), authenticates at the corporate IdP, and sees a tile for each account × role they’re entitled to. Clicking a tile opens the console with temporary credentials; there is no password for the account and no access key anywhere. For the CLI and SDKs, the same identity is wired up once with an sso-session block — the modern replacement for pasting access keys into ~/.aws/credentials:
# ~/.aws/config — generated by `aws configure sso`, edited here for clarity
[sso-session meridian]
sso_start_url = https://meridian.awsapps.com/start
sso_region = ap-south-1
sso_registration_scopes = sso:account:access
[profile payments-prod-operator]
sso_session = meridian
sso_account_id = 123456789012 # target AWS account (placeholder)
sso_role_name = WorkloadOperator # = the permission-set name
region = ap-south-1
output = json
sso_role_name is the permission-set name, not an IAM role you created — Identity Center maps it to the generated AWSReservedSSO_WorkloadOperator_<hash> role inside account 123456789012. To get credentials:
# One browser-based device authorization for the whole sso-session:
aws sso login --sso-session meridian
# Now every profile that references that session works, with no stored keys:
aws sts get-caller-identity --profile payments-prod-operator
# representative output:
# {
# "Account": "123456789012",
# "Arn": "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_WorkloadOperator_a1b2c3d4/alice@meridian.com"
# }
Read that Arn carefully — it is the single most useful fact in this lesson. The caller is an assumed-role session whose role name is AWSReservedSSO_<permission-set>_<hash> and whose session name is the user’s email. That is why CloudTrail across all 52 accounts can answer “what did Alice do everywhere?” from one identity, and why there is nothing to rotate or leak: aws sso login refreshes the token, the SDK silently exchanges it for fresh STS credentials, and when they expire (per the permission set’s session duration) the SDK just fetches more — until the portal session itself lapses or the user is deprovisioned. No IAM user, no access key, ever is not a slogan; it is the literal contents of ~/.aws.
Permission sets
What it is
A permission set is a named, account-agnostic template of IAM permissions that Identity Center materializes as an IAM role in each account you assign it to. It is the unit of “what can this person do here.” A permission set bundles up to four things:
- one or more AWS-managed policies (e.g.,
ReadOnlyAccess,PowerUserAccess, job-function policies likeBilling), - customer-managed policies by name (the policy of that name must already exist in each target account — a deliberate design that lets you reference account-local policies),
- an inline policy (JSON embedded directly in the permission set), and
- a permissions boundary (an IAM managed policy that caps the maximum effective permissions of the generated role, regardless of what the attached policies grant).
It also carries a session duration (1–12 hours) and an optional relay state (a deep-link landing page in the console after sign-in). When you assign permission-set × group × account, Identity Center creates/updates the AWSReservedSSO_<name>_<hash> role in that account with exactly those policies and a trust policy that only Identity Center’s federation can assume. Edit the permission set once and Identity Center re-provisions the role in every account it’s assigned to — that fan-out is the whole point.
Why it matters
Permission sets are where the principle of least privilege gets encoded as reusable, version-controllable artifacts instead of bespoke per-account IAM that nobody can audit. The failure modes they prevent are concrete: handing everyone AdministratorAccess everywhere “to unblock them,” or hand-crafting a slightly-different admin role in each of 80 accounts so effective permissions are unknowable. Because a permission set is defined once and projected into many accounts, it gives you a single place to tighten a permission and have it propagate, and a single name that shows up consistently in CloudTrail across the estate (AWSReservedSSO_PlatformAdmin_…), which makes “who can do what, where” answerable.
Permission sets are also the natural tiering mechanism: you design a small catalog of roles by job function and bind them to OUs/accounts by environment, so a Workload Deployer in non-prod is a different (more permissive) thing than the same human’s access in prod.
How to do it well
- Design a small, named catalog — not a permission set per team. A workable enterprise baseline is on the order of 6–10 permission sets, e.g.:
| Permission set | Backed by | Typical scope | Session |
|---|---|---|---|
Billing |
Billing managed policy |
Management/billing account, read in others | 1 h |
ReadOnly |
ReadOnlyAccess |
Every account (auditors, support) | 4 h |
SecurityAudit |
SecurityAudit + custom |
Audit account, read across org | 4 h |
WorkloadDeployer |
PowerUserAccess + boundary |
Non-prod workload accounts | 8 h |
WorkloadOperator |
scoped inline (no IAM write) | Prod workload accounts | 4 h |
NetworkAdmin |
scoped to VPC/TGW/Route53 | Networking account | 4 h |
PlatformAdmin |
AdministratorAccess + boundary |
Platform team, governed OUs | 2 h |
BreakGlassAdmin |
AdministratorAccess |
Emergency only, alerted | 1 h |
- Always attach a permissions boundary to the privileged sets. A boundary on
PlatformAdmin/WorkloadDeployer(e.g., deny IAM user creation, deny leaving the org, deny touching the org CloudTrail/KMS keys, deny non-approved Regions) means even an “admin” permission set can’t undermine the landing zone. The boundary is the intersection that survives careless inline policy edits. - Prefer scoped inline/customer-managed policies over
PowerUserAccess/AdministratorAccesswherever you can. Start from the job-function policies and least privilege, not from “admin minus a few denies.”WorkloadOperatorin prod, for instance, should grant deploy/operate actions but not IAM or KMS-key administration. - Use customer-managed policies (by name) for things that must differ per account — referenced in the permission set, defined locally in each account by your baseline (CfCT/AFT). This keeps the permission set portable while letting the actual policy vary (e.g., an account-local KMS key ARN).
- Differentiate prod from non-prod by which permission set is assigned, not by trust. The same engineer gets
WorkloadDeployerinpayments-devand onlyWorkloadOperatorinpayments-prod. Promotion of access is a separate, reviewed assignment. - Manage permission sets as code. Define them in Terraform (
aws_ssoadmin_permission_set+ policy attachments) or CloudFormation so the catalog is reviewed, diffed, and versioned — never click-built and forgotten. Short session durations and boundaries become enforceable standards in the module. - Re-provision after edits. Editing a permission set marks accounts as needing provisioning; ensure your pipeline (or the console “provision to all accounts”) actually pushes the change so the IAM roles don’t drift from the definition.
Concrete artifacts, decisions, and AWS tools
- Artifacts: the permission-set catalog as IaC; the permissions-boundary policy attached to privileged sets; the customer-managed policy templates baselined into accounts; a permission-set-to-OU/account assignment matrix; session-duration standards.
- Decisions: the catalog (names, backing policies, session durations); which sets get boundaries and what the boundary denies; inline/customer-managed vs managed-policy backing per set; per-environment permissiveness (deployer vs operator in prod).
- Tools/services: IAM Identity Center permission sets, AWS IAM (managed/customer-managed policies, permissions boundaries — the generated
AWSReservedSSO_*roles), AWS STS (session duration), and Terraform/CloudFormation to manage them as code.
A permission set as code, end to end
The section above argues you should manage permission sets as code. This is what that Terraform actually is — a complete WorkloadOperator, from the four policy layers through the account assignment, plus the IAM role it generates. Every account ID and GUID is a placeholder.
# Identity Center is a singleton per org; look it up rather than hard-coding ARNs.
data "aws_ssoadmin_instances" "this" {}
locals {
sso_instance_arn = tolist(data.aws_ssoadmin_instances.this.arns)[0]
identity_store_id = tolist(data.aws_ssoadmin_instances.this.identity_store_ids)[0]
}
resource "aws_ssoadmin_permission_set" "operator" {
name = "WorkloadOperator"
description = "Operate workloads in prod; no IAM or KMS-key administration"
instance_arn = local.sso_instance_arn
session_duration = "PT4H" # ISO-8601 duration, 1–12h. Short by design.
relay_state = "https://ap-south-1.console.aws.amazon.com/ecs/home"
}
# Layer 1 — an AWS-managed policy for broad read.
resource "aws_ssoadmin_managed_policy_attachment" "operator_ro" {
instance_arn = local.sso_instance_arn
permission_set_arn = aws_ssoadmin_permission_set.operator.arn
managed_policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}
# Layer 2 — a scoped inline policy for the operate verbs (NOT iam:* / kms:*).
data "aws_iam_policy_document" "operator_inline" {
statement {
sid = "OperateCompute"
effect = "Allow"
actions = ["ecs:UpdateService", "ecs:DescribeServices",
"ec2:StartInstances", "ec2:StopInstances", "ec2:RebootInstances"]
resources = ["*"]
}
}
resource "aws_ssoadmin_permission_set_inline_policy" "operator_inline" {
instance_arn = local.sso_instance_arn
permission_set_arn = aws_ssoadmin_permission_set.operator.arn
inline_policy = data.aws_iam_policy_document.operator_inline.json
}
# Layer 3 — a permissions boundary that CAPS the role no matter what the
# attached policies grant (deny leaving the org, touching org trails, etc.).
resource "aws_ssoadmin_permissions_boundary_attachment" "operator_boundary" {
instance_arn = local.sso_instance_arn
permission_set_arn = aws_ssoadmin_permission_set.operator.arn
permissions_boundary {
# A customer-managed policy of THIS name must exist in every target account.
customer_managed_policy_reference {
name = "LandingZoneBoundary"
path = "/"
}
}
}
# The account assignment: (group) × (permission set) × (account).
data "aws_identitystore_group" "operators" {
identity_store_id = local.identity_store_id
alternate_identifier { # v5 syntax; `filter` is deprecated
unique_attribute {
attribute_path = "DisplayName"
attribute_value = "aws-payments-prod-operators"
}
}
}
resource "aws_ssoadmin_account_assignment" "operator_payments_prod" {
instance_arn = local.sso_instance_arn
permission_set_arn = aws_ssoadmin_permission_set.operator.arn
principal_id = data.aws_identitystore_group.operators.group_id
principal_type = "GROUP"
target_id = "123456789012" # the payments-prod account (placeholder)
target_type = "AWS_ACCOUNT"
}
Two provider-version notes worth flagging because they bite people mid-upgrade. The aws_identitystore_group filter argument is deprecated in the v5 aws provider in favour of the alternate_identifier { unique_attribute { … } } block shown above — old modules copied off the internet still use filter and throw deprecation warnings (and eventually break). And a permissions boundary can be either a managed_policy_arn or a customer_managed_policy_reference (a policy by name that must pre-exist in each account, usually baselined by AFT/CfCT), but not both — the customer-managed form is what lets one permission set carry a boundary whose actual contents differ per account.
What Terraform actually produces. The moment apply finishes, Identity Center provisions an IAM role into account 123456789012 named AWSReservedSSO_WorkloadOperator_<random-hash>, under the reserved path /aws-reserved/sso.amazonaws.com/ap-south-1/, with the four policy layers attached and this trust policy — which you never write and must never hand-edit:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:saml-provider/AWSSSO_a1b2c3d4e5f6_DO_NOT_DELETE"
},
"Action": "sts:AssumeRoleWithSAML",
"Condition": { "StringEquals": { "SAML:aud": "https://signin.aws.amazon.com/saml" } }
}]
}
Edit the permission set once — say, drop ecs:UpdateService — and Identity Center re-provisions the role in every account the set is assigned to. That fan-out is the entire reason permission sets exist: one reviewed diff in Git, a hundred roles converge. The trap is forgetting to trigger the re-provision (the console’s “Provision to all accounts”, or your pipeline re-running apply), which lets the live roles drift from the definition.
External identity-provider federation
What it is
External IdP federation makes your corporate identity provider — Microsoft Entra ID, Okta, Ping, Google Workspace, etc. — the identity source for Identity Center, so the humans in AWS are the same humans HR already manages, with the same MFA and the same lifecycle. It is two protocols working together:
- SAML 2.0 handles authentication (sign-in): the IdP is the SAML Identity Provider, Identity Center is the Service Provider; you exchange metadata (entity IDs, the IdP’s signing certificate, the ACS URL) to establish trust. When a user opens the AWS access portal, they’re redirected to the IdP, authenticate there (subject to the IdP’s Conditional Access / MFA), and are returned with a signed SAML assertion.
- SCIM 2.0 handles provisioning (the directory): the IdP pushes users and groups into Identity Center automatically — creating them on hire, updating attributes, and deactivating them on termination — so Identity Center’s view of “who exists” stays in lock-step with the IdP without manual CSV imports.
You enable “external identity provider” as the source, upload metadata both ways, turn on automatic provisioning (SCIM) with the generated endpoint + bearer token, and configure the IdP’s AWS Identity Center enterprise app to provision the right users and groups.
Why it matters
This is the difference between an identity system that is governed by your existing joiner-mover-leaver process and one that drifts. With SCIM, an employee who leaves loses all AWS access org-wide the moment HR disables them in Entra/Okta — no orphaned IAM users, no “we forgot account 47.” With SAML, your MFA, phishing-resistant authenticators, and Conditional Access (device compliance, network location, risk-based step-up) are enforced at the IdP and apply to AWS automatically — you don’t rebuild any of it in AWS. And because groups flow in via SCIM, you assign permission sets to IdP groups (e.g., aws-payments-prod-operators), making access a function of group membership your IAM/HR systems already control. The alternative — maintaining a separate roster of humans inside Identity Center’s built-in directory — duplicates governance, fragments MFA, and re-creates the off-boarding gap you were trying to close.
A second mechanism worth naming: even without Identity Center, AWS supports direct SAML federation to IAM (an IAM SAML identity provider + roles assumed via AssumeRoleWithSAML). That still exists and is occasionally the right tool (e.g., a workload-specific federation), but for workforce access across a landing zone, Identity Center federation is the recommended path because it centralizes the role catalog (permission sets) and the cross-account fan-out that raw IAM SAML makes you build by hand per account.
How to do it well
| Decision | Recommendation | Why |
|---|---|---|
| Authentication source | External IdP via SAML 2.0 | Reuse corporate MFA + Conditional Access |
| Provisioning | Automatic (SCIM 2.0), not manual | Joiner/leaver lifecycle stays in sync; instant off-boarding |
| What you assign to | IdP-synced groups, never individuals | Access = group membership your HR/IAM already governs |
| MFA / step-up | Enforce at the IdP (Conditional Access / Okta policies) | Single place; phishing-resistant (FIDO2) flows through |
| Attributes for ABAC | Map IdP attributes (dept, cost-center, team) into the SAML assertion + SCIM | Powers attribute-based access control (next section) |
| Break-glass | Keep a non-federated path (built-in directory user / root) | Survive an IdP or federation outage |
- Provision groups, not just users, over SCIM — and keep the set of synced groups deliberate; syncing your entire corporate group catalog (thousands of groups) into Identity Center is noise. Sync the
aws-*access groups you actually assign. - Map the attributes you’ll need for ABAC at the same time you set up SAML/SCIM (see below) — retrofitting attribute flow after the fact means reconfiguring the IdP app and re-testing every assertion.
- Test the round-trip in a non-prod IdP/tenant first. SAML metadata, claim transformations, and SCIM mappings are fiddly; a bad assertion locks users out. Validate sign-in and a hire/terminate provisioning cycle before cutover.
- Decide guest/B2B handling explicitly. If contractors come in as Entra B2B guests, confirm how their attributes (and UPNs) arrive — guest UPNs can be mangled, which breaks attribute matching if you don’t account for it.
Concrete artifacts, decisions, and AWS tools
- Artifacts: the SAML trust (exchanged metadata, signing cert, entity IDs); the SCIM endpoint + token and the IdP enterprise-app provisioning config; the list of synced
aws-*groups; the attribute-mapping document (IdP claim → Identity Center attribute); the break-glass non-federated account. - Decisions: which IdP; SAML-only vs SAML+SCIM (always choose +SCIM for workforce); which groups to sync; which attributes to pass for ABAC; MFA/Conditional-Access posture at the IdP.
- Tools/services: IAM Identity Center external IdP / SCIM, Microsoft Entra ID (or Okta/Ping/Google) as the SAML IdP + SCIM provisioner, AWS STS (
AssumeRoleWithSAMLunder the hood), and optionally an IAM SAML identity provider for any direct-to-IAM federation edge cases.
Wiring the IdP: what SAML and SCIM actually exchange
“Exchange metadata, turn on SCIM” hides a lot. Here is the concrete handshake, because a wrong claim or a missed attribute locks people out or silently over-grants them.
The two protocols, and who holds what. SAML is a browser redirect for sign-in; SCIM is a background REST feed for the directory. They are independent — you can have SAML without SCIM (and you should not, for workforce access).
| Artifact | Direction | Held/produced by | Purpose |
|---|---|---|---|
| IdP SAML metadata (entity ID, signing cert, SSO URL) | IdP → Identity Center | Entra/Okta/Ping | Identity Center trusts the IdP’s signed assertions |
| Identity Center SAML metadata (ACS URL, SP entity ID) | Identity Center → IdP | Identity Center | The IdP knows where to post the assertion |
| SCIM endpoint URL + bearer token | Identity Center → IdP | Identity Center (rotate the token) | The IdP pushes users/groups over SCIM 2.0 |
| Attribute/claim mappings | configured in IdP app | you | Carry costCenter, team, email into AWS |
What a signed-in assertion carries. After the user authenticates at Entra, the browser posts a SAML assertion to Identity Center’s ACS URL. Stripped to essentials it says who the subject is and which attributes they carry:
<!-- representative SAML assertion attributes (namespaces trimmed) -->
<Subject><NameID Format="...emailAddress">alice@meridian.com</NameID></Subject>
<AttributeStatement>
<Attribute Name="https://aws.amazon.com/SAML/Attributes/AccessControl:team">
<AttributeValue>payments</AttributeValue>
</Attribute>
<Attribute Name="https://aws.amazon.com/SAML/Attributes/AccessControl:costCenter">
<AttributeValue>CC-4021</AttributeValue>
</Attribute>
</AttributeStatement>
Those AccessControl:<key> attribute names are the SAML path for turning IdP attributes into session tags for ABAC (the next-but-one section). With SCIM in the picture you usually don’t hand-craft these — you map them once in Identity Center’s Attributes for access control against the SCIM enterprise attributes (${path:enterprise.costCenter}, ${path:enterprise.department}), and Identity Center attaches them to every session.
Why SCIM is non-negotiable for workforce. SCIM is the difference between “sign-in works” and “the joiner/leaver lifecycle is real.” When HR disables Alice in Entra, SCIM sends a PATCH .../Users/<id> with active:false within minutes, and her AWS access — every account, every role — evaporates without anyone touching AWS. Two operational cautions: sync only the deliberate aws-* groups, not your entire corporate group tree (thousands of noise groups slow provisioning and clutter assignments); and the SCIM bearer token expires and must be rotated — a lapsed token silently stops provisioning, so terminations quietly stop propagating. Alarm on SCIM sync failures the same way you alarm on a failed backup.
Cross-account access
What it is
Cross-account access is how a principal in one account operates in another account. In a landing zone there are two distinct flavors, and conflating them is a classic mistake:
-
Human cross-account access is what Identity Center is: a user assigned a permission set in many accounts can switch between them in the access portal, each switch being a fresh, short-lived federated session into that account’s
AWSReservedSSO_*role. The human never “owns” a credential in the target account; STS issues a temporary one per session. This is the path for people. -
Workload/automation cross-account access is
sts:AssumeRolebetween IAM roles: a role (or service, or pipeline) in account A assumes a role in account B because B’s role trust policy names A’s principal as trusted, and A’s identity policy allowssts:AssumeRoleon B’s role ARN. Both sides must agree — trust policy on the resource (the role being assumed) and permission on the caller. This is the path for software: a central CI/CD account deploying into workload accounts, a security tooling account reading across the org, a logging pipeline writing to the Log Archive bucket.
The landing zone is full of pre-built cross-account roles you’ve already met: Control Tower’s AWSControlTowerExecution role (lets the management/CT plane act in each member account), the Audit account’s cross-account audit/response roles into every account, AFT’s execution roles, and the OrganizationAccountAccessRole that CreateAccount drops into new accounts. ABAC and aws:PrincipalOrgID conditions are what keep all of these scoped.
Why it matters
Cross-account access is simultaneously the thing that makes a multi-account estate usable (otherwise every account is an island) and the thing that, done sloppily, re-creates the blast radius the account boundary was supposed to give you. The discipline is to make every cross-account trust explicit, least-privilege, and org-scoped:
- For humans, Identity Center already does this correctly — temporary creds, central audit, no standing access. The win is simply using it instead of hand-built jump roles.
- For workloads, the danger is an over-broad trust policy — a role in a workload account that trusts
arn:aws:iam::<ci-account>:root(i.e., any principal in the CI account) withAdministratorAccess, or worse, a role trusting"AWS": "*". The fix is precise trust (a specific role ARN, not:rootwhen you can avoid it), anExternalIdfor any third-party trust, aaws:PrincipalOrgIDcondition so only principals in your org can assume it, and a tightly-scoped identity policy on the assumed role.
Getting this right is also what lets your security account reach in and your workload accounts not reach back — the asymmetry that keeps the audit/forensics path trustworthy.
How to do it well
- Humans go through Identity Center, full stop. No standing IAM users with cross-account
AssumeRolefor people. If a human needs another account, they get a permission set there. - Scope every workload trust policy precisely. Trust the specific role ARN that should assume, not the whole account
:root, when the source is known. AddConditionkeys:aws:PrincipalOrgID(= your org) to forbid out-of-org assumption,aws:PrincipalTag/*for ABAC, andsts:ExternalIdfor any cross-org/third-party (SaaS) trust to defeat the confused-deputy problem. - Prefer Identity Center / role assumption over any long-lived key sharing. Never copy access keys between accounts; that’s exactly the leaked-credential failure mode. Cross-account is always a trust relationship, never a shared secret.
- For CI/CD into many accounts, standardize a deployment role. A single, consistently-named
<org>-deployrole baselined into every account (via CfCT/AFT) with a trust policy that only your pipeline’s role + your org can assume, scoped to what deployment needs. The pipeline assumes it per target account. - For third-party SaaS (monitoring, FinOps, CSPM tools) always require an
ExternalIdand trust only the vendor’s documented principal — and review what they actually need rather than grantingReadOnlyAccessreflexively. - Use IAM Access Analyzer (delegated to the Audit account, Part 1) to surface roles that can be assumed from outside the org or have unused cross-account trust, and prune them.
- Govern with RCPs/SCPs at the perimeter. A Resource Control Policy can enforce that role trust across the org always carries
aws:PrincipalOrgID(a data-perimeter guardrail), so an over-broad trust policy is denied org-wide regardless of who wrote it.
| Cross-account need | Right mechanism | Key guardrail |
|---|---|---|
| A person works in many accounts | IAM Identity Center permission set | Short session, central audit, no standing creds |
| CI/CD deploys into workload accounts | sts:AssumeRole into a baselined deploy role |
Trust the pipeline role + aws:PrincipalOrgID; least-privilege policy |
| Security account reads/responds org-wide | Audit-account cross-account roles | Asymmetric trust; workloads can’t reach back |
| Third-party SaaS reads your accounts | AssumeRole with ExternalId |
ExternalId + vendor principal only; scoped policy |
| Control Tower / AFT acts in members | AWSControlTowerExecution / AFT roles |
Managed trust; don’t hand-edit |
Concrete artifacts, decisions, and AWS tools
- Artifacts: the standardized deploy-role definition (baselined into every account); the trust-policy standard (specific ARNs +
aws:PrincipalOrgID+ExternalIdwhere applicable); an inventory of cross-account roles; IAM Access Analyzer findings and remediation; the perimeter RCP/SCP enforcing org-scoped trust. - Decisions: which automation roles exist and their trust/permission scope;
ExternalIdpolicy for third parties; whether to enforceaws:PrincipalOrgIDvia RCP; the asymmetry rules for the security account. - Tools/services: AWS STS (
AssumeRole,AssumeRoleWithSAML,ExternalId), AWS IAM (role trust + identity policies,OrganizationAccountAccessRole), IAM Identity Center (human path), IAM Access Analyzer, AWS Organizations RCPs/SCPs (aws:PrincipalOrgIDperimeter), AWS Control Tower / AFT (the managed cross-account roles).
A cross-account trust, line by line
The prose calls for “precise trust + aws:PrincipalOrgID + ExternalId.” Those are three specific lines of JSON. Here is the standardized meridian-deploy role baselined into every account, and the SaaS variant, with the confused-deputy problem made concrete.
A trust relationship has two halves that must both agree. On the resource side, the role being assumed states who may assume it (its trust policy). On the caller side, the assuming principal’s identity policy must allow sts:AssumeRole on that role’s ARN. Miss either half and the assume fails with AccessDenied — a classic two-hours-lost bug.
// Trust policy ON the deploy role in a workload account (222222222222):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111111111111:role/meridian-cicd-pipeline" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:PrincipalOrgID": "o-abcd1234ef" }
}
}]
}
Three deliberate choices in nine lines. The principal is a specific role ARN, not arn:aws:iam::111111111111:root — :root would trust every principal in the CI account, so a compromised Lambda there could assume your deploy role. aws:PrincipalOrgID pins the caller to your organization, so even if the trusted ARN were somehow reproduced in an attacker’s account, the assume is denied because it isn’t in org o-abcd1234ef. And the role’s identity policy (separate) stays least-privilege — trusting the pipeline says nothing about what it can do once in.
// Identity policy ON the pipeline role (111111111111) — the caller half:
{ "Effect": "Allow", "Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::*:role/meridian-deploy" }
# The pipeline, per target account:
aws sts assume-role \
--role-arn arn:aws:iam::222222222222:role/meridian-deploy \
--role-session-name deploy-build-4821
# returns short-lived AccessKeyId / SecretAccessKey / SessionToken (expire in 1h)
The ExternalId for third parties. When the “caller” is a SaaS vendor (FinOps, CSPM, monitoring) you cannot name a role in your org — you must trust their account. That reopens the confused-deputy problem: the vendor serves many customers from one account, and a malicious customer could ask the vendor to assume your role. The fix is a per-customer shared secret the vendor must present:
// Trust policy for a SaaS reader role — note ExternalId, NOT PrincipalOrgID:
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<vendor-account-id>:root" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "sts:ExternalId": "meridian-7f3a-uniquely-issued" } }
}
The ExternalId is not a password (it isn’t secret from the vendor) — it’s an anti-confusion token that only your vendor configuration carries, so another of the vendor’s customers can’t trick it into assuming your role. Pair it with a genuinely scoped read policy; do not reflexively grant ReadOnlyAccess, which includes reading secrets metadata, S3 listings, and more than a cost tool needs. Everything here — precise ARNs, PrincipalOrgID, ExternalId, session-policy scoping — is drawn out further in Cross-account roles & the confused-deputy problem.
Attribute-based access control (ABAC)
What it is
Attribute-based access control (ABAC) is an authorization strategy where access is decided by attributes (tags) on the principal and on the resource, rather than by enumerating principal-to-resource grants. In AWS, the principal’s attributes arrive as session tags — aws:PrincipalTag/<key> — and resources carry resource tags — aws:ResourceTag/<key>; an IAM/permission-set policy then says, in effect, “allow this action when aws:PrincipalTag/team equals aws:ResourceTag/team.” One policy expresses “you may act on the resources of your own team” without ever naming a team.
Identity Center has first-class ABAC support: in Attributes for access control, you map identity attributes (from the external IdP’s SAML assertion, or from the directory) onto session tags that are attached to every Identity Center session. So costCenter, department, team, or project flowing in from Entra ID/Okta becomes aws:PrincipalTag/costCenter on the federated session — and your permission-set policies (and resource tags) can key off it.
Why it matters
ABAC is how identity scales without the policy/role count exploding. The role-based alternative (RBAC) needs a new permission set or assignment for every (team × environment × resource-scope) combination — for 70 teams that’s an unmanageable matrix. With ABAC, a single WorkloadOperator permission set can serve all 70 teams, because the policy self-scopes to whatever team tag the signed-in user carries, against resources tagged with the same team. It is the natural partner to a tag-disciplined estate: if your accounts and resources are already tagged (and they must be, for cost and governance — earlier parts of this series), ABAC turns those tags into the access boundary too.
Concretely, ABAC delivers:
- Fewer policies and permission sets — attributes do the segmenting, not enumeration.
- Self-service that stays governed — a new team needs only the right attribute and correctly-tagged resources; no new IAM artifact.
- Permissions that track reorganizations — move a person to a new team in the IdP, their
teamsession tag changes, and their effective access follows automatically. - Cleaner audit — CloudTrail shows the attributes that authorized the action.
How to do it well
- Decide the attribute taxonomy first and govern its source. Pick a small set of stable, meaningful attributes (
team/businessUnit,costCenter,project, maybeenvironment) that already exist authoritatively in your HR/IdP. ABAC is only as trustworthy as the attribute source — if anyone can self-set theirteam, ABAC is theater. Attributes must come from the IdP (HR-governed), not be user-editable. - Wire the attributes end-to-end: map them in the IdP’s SAML claim configuration, ensure they flow via SCIM, and enable them in Identity Center Attributes for access control so they become session tags. Then enforce tag-on-create (via SCPs/Config or a proactive Hook) so resources actually get the matching tags — ABAC fails open if resources are untagged and your policy only checks
ResourceTag. - Write the policy as an equality between principal and resource tags, e.g. permit the action only when
aws:ResourceTag/teamStringEqualsaws:PrincipalTag/team. Combine ABACConditions with RBAC permission sets (RBAC sets the verbs a role may use; ABAC scopes them to the right resources) — the two are complementary, not either/or. - Mind the gaps. ABAC doesn’t fit everything: not all services support tag-based authorization or
ResourceTagconditions uniformly, and some actions (e.g.,Describe*/List*) can’t be tag-scoped. Use ABAC for the resource-scoping it’s good at and fall back to scoped RBAC where services don’t support tags. - Protect the tags themselves. Since tags now grant access, control who can set/modify the controlling tags — use a permissions boundary / SCP so a principal can’t re-tag a resource (or its own session) to widen access. Tag mutation becomes a privileged action.
- Roll out on a pilot. Start ABAC on one or two well-tagged workload OUs, validate that the equality policies behave (and audit denied/allowed in CloudTrail) before making it the org-wide default scoping model.
| Concept | RBAC (role-based) | ABAC (attribute-based) |
|---|---|---|
| Access decided by | Which role/permission set you hold | Tags on principal vs tags on resource |
| Scales with | New role per (team × env × scope) — combinatorial | One policy; attributes segment — flat |
| New team onboarding | New permission set / assignment | Correct attribute + correctly-tagged resources |
| Reorg handling | Re-assign roles | Attribute changes in IdP; access follows |
| Best for | Coarse “what verbs” (deploy vs read) | Fine “which resources” (your team’s only) |
| Failure mode | Role explosion | Untrusted/missing tags → fails open/shut |
Concrete artifacts, decisions, and AWS tools
- Artifacts: the attribute taxonomy + its authoritative IdP source; the IdP SAML claim + SCIM attribute mappings; Identity Center Attributes for access control configuration; ABAC permission-set policies (principal-tag = resource-tag); tag-enforcement SCPs/Config rules/Hooks; the tag-mutation guardrail.
- Decisions: which attributes drive access and where they come from; which permission sets become ABAC-scoped vs stay RBAC; tag-on-create enforcement mechanism; who may mutate controlling tags; the pilot scope.
- Tools/services: IAM Identity Center (Attributes for access control → session tags), AWS IAM (
aws:PrincipalTag/aws:ResourceTagconditions, permissions boundaries), the external IdP (attribute source via SAML/SCIM), AWS Organizations (tag policies + SCPs to enforce tagging), and AWS Config / CloudFormation Hooks (tag-on-create assurance).
The ABAC policy, written out
“Permit the action when PrincipalTag/team equals ResourceTag/team” is one Condition block — and getting it exactly right is what lets a single operator permission set serve all 70 teams. Here is the whole chain: the session-tag mapping, the self-scoping policy, and the tag-on-create guardrail without which it fails open.
Step 1 — turn IdP attributes into session tags. In Terraform, aws_ssoadmin_instance_access_control_attributes enables ABAC on the Identity Center instance and maps each attribute to a session-tag key:
resource "aws_ssoadmin_instance_access_control_attributes" "abac" {
instance_arn = local.sso_instance_arn
attribute {
key = "team"
value { source = ["$${path:enterprise.department}"] } # SCIM enterprise attr
}
attribute {
key = "costCenter"
value { source = ["$${path:enterprise.costCenter}"] }
}
}
(The $${…} is Terraform escaping so the literal ${path:…} reaches AWS; it is not a Terraform variable.) After this, every federated session carries aws:PrincipalTag/team and aws:PrincipalTag/costCenter.
Step 2 — the self-scoping policy goes in the permission set’s inline policy. Note the variable on the right-hand side — it is resolved per-session from the principal’s own tag:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "OperateOnlyOwnTeamResources",
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances", "ec2:RebootInstances"],
"Resource": "*",
"Condition": {
"StringEquals": { "aws:ResourceTag/team": "${aws:PrincipalTag/team}" }
}
}]
}
One policy, no team named anywhere. Alice (session tag team=payments) can stop an EC2 instance tagged team=payments and is denied on one tagged team=logistics — and the same policy governs all 70 teams. Add a new team tomorrow and you write zero new IAM.
Step 3 — enforce tag-on-create, or ABAC fails open. The catch: if a resource is created untagged, the aws:ResourceTag/team key is absent and — depending on the action and how AWS evaluates a missing key — the condition can evaluate in surprising ways, and an untagged resource has no owner to scope to. So ABAC is only as sound as your tag discipline. Require the tag at creation with an SCP so untagged creates are simply denied:
// SCP fragment: deny RunInstances unless a `team` tag is supplied
{
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": { "Null": { "aws:RequestTag/team": "true" } }
}
And because tags now grant access, who may write tags becomes a privileged decision — a boundary or SCP must stop a principal re-tagging a resource (or influencing its own session tag) to widen access. Tag mutation is no longer cosmetic; it is an authorization event. The deeper multi-account treatment, including per-service ABAC coverage gaps, lives in IAM Identity Center permission sets & ABAC.
Real-world enterprise scenario
Meridian Logistics — the fictional pan-India 3PL from Part 1 (~2,100 employees, ~70 application teams, RBI/PCI exposure on payments and COD reconciliation, data-localization obligations, ~52 governed accounts vended via AFT) — reaches the identity phase. Their entire estate is governed but, until now, humans have been getting in through a patchwork of IAM users left over from the old single-account world. The CCoE’s North-Star for this phase: zero standing IAM users for humans across the org, instant org-wide off-boarding, and a single sign-on front door — with one operator permission set serving all 70 teams via ABAC.
IAM Identity Center (SSO). Control Tower had already enabled Identity Center in the home Region ap-south-1. Meridian delegates Identity Center administration to their identity shared-services account so the platform team manages permission sets without management-account access. They set the access portal URL (https://meridian.awsapps.com/start), define session-duration standards (admin sets 1–2 h, operator 4 h, read 4 h), and create two break-glass users in the built-in Identity Center directory holding a BreakGlassAdmin set, credentials split across two sealed safes, with an EventBridge → SNS alarm to the SOC on any use. Artifacts: delegated-admin registration, portal URL, session standards, break-glass runbook.
Permission sets. They author a 9-set catalog as Terraform (aws_ssoadmin_permission_set): Billing, ReadOnly, SecurityAudit, NetworkAdmin, WorkloadDeployer, WorkloadOperator, PlatformAdmin, DataEngineer, BreakGlassAdmin. Every privileged set (PlatformAdmin, WorkloadDeployer, NetworkAdmin) carries a permissions boundary that denies IAM-user creation, denies leaving the org, denies touching the org CloudTrail and central KMS keys, and denies non-approved Regions (ap-south-1/ap-south-2 only). Crucially, in prod workload accounts teams receive only WorkloadOperator (deploy/operate, no IAM/KMS-key write); the more-permissive WorkloadDeployer is assigned only in non-prod. Artifact: the permission-set catalog in Git + a boundary policy + an assignment matrix.
External IdP federation. Meridian’s source of truth is Microsoft Entra ID. They switch Identity Center’s identity source to external IdP, exchange SAML 2.0 metadata, and enable SCIM 2.0 so Entra pushes the aws-* access groups and users — a terminated employee now loses all AWS access the moment HR disables them in Entra. MFA and Conditional Access (device-compliant + FIDO2) are enforced at Entra and flow straight through. They map Entra attributes costCenter, team, and department into the SAML assertion and SCIM. Contractors arriving as Entra B2B guests had mangled UPNs; the team accounts for the de-mangling so attribute matching holds. Artifacts: SAML trust, SCIM config, synced-group list, attribute-mapping doc.
Cross-account access. Humans now exclusively use Identity Center — the legacy human IAM users are deleted. For automation, AFT baselines a standardized meridian-deploy role into every account whose trust policy names only the CI/CD pipeline’s role ARN and carries aws:PrincipalOrgID = o-merid…; the pipeline assumes it per target account. A perimeter RCP enforces that all role trust across the org includes aws:PrincipalOrgID, so no one can author an out-of-org trust. Their two SaaS tools (a FinOps platform and a CSPM scanner) are trusted only with ExternalId and a scoped read role. IAM Access Analyzer (delegated to the Audit account) runs continuously and flagged three legacy roles with :root trust, which they tightened. Artifacts: the deploy-role module, the trust-policy standard, the org-trust RCP, Access Analyzer findings.
ABAC. This is the lever that lets one WorkloadOperator set serve all 70 teams. They enable Identity Center Attributes for access control, mapping the team and costCenter claims to session tags (aws:PrincipalTag/team, aws:PrincipalTag/costCenter). AFT already enforces a mandatory team/costCenter tag-on-create (via SCP + a proactive CloudFormation Hook) so resources are tagged. The WorkloadOperator policy permits operate actions only where aws:ResourceTag/team StringEquals aws:PrincipalTag/team, so an engineer can act on their team’s resources and no other team’s — with no per-team permission set. A boundary prevents anyone re-tagging a resource to widen access. They piloted on the Workloads/NonProd OU, watched CloudTrail allow/deny for two weeks, then promoted ABAC to the org-wide scoping model. Artifact: the attribute taxonomy + mappings + ABAC policies + tag-enforcement controls.
Measurable outcome after one quarter: standing human IAM users dropped from ~1,400 across the estate to 0 (only the 2 break-glass Identity Center users remain outside the IdP); off-boarding latency fell from “ad-hoc, frequently-missed, per-account” to under 5 minutes org-wide (Entra disable → SCIM deprovision); the permission-set catalog stayed at 9 sets while serving 70 teams thanks to ABAC (the RBAC-only design would have needed 200+); 100% of human sessions are short-lived federated credentials with no access keys; Access Analyzer external-trust findings went to 0; and a quarterly access review that used to take the security team three weeks of per-account spelunking now reads off one Identity Center assignment report plus the IdP group memberships.
Going deeper
The core sections give you a production-ready human-access design. This section is for the reader who owns that design and has to defend it — the internals, the newer capabilities, the boundaries with other services, and the failure modes.
Centralized root access — retiring the one credential Identity Center never touched
Everything above governs federated human access. But every AWS account is still born with a root user — an email + password identity that owns the account and can do a handful of things no IAM policy can grant or deny (change the account’s contact/root email, close the account, restore certain locked resource policies). In a 52-account estate that is 52 root passwords to vault, MFA, and never leak — the one long-lived credential the whole federation model didn’t remove.
Centralized root access (an IAM capability, generally available since late 2024) closes that gap for member accounts and has two halves:
- Remove root credentials from member accounts. Enabled from the management account (or the IAM delegated administrator) via Organizations, this lets you strip the root password, root access keys, and root MFA from member accounts entirely — so there is simply no standing root credential in them to phish, reuse, or find in a leaked backup.
- Perform the rare root-only task centrally, just-in-time. When a genuine root action is needed — classically, unlocking an S3 bucket or SQS queue whose resource policy locked everyone out, or deleting a malformed one — you call
aws sts assume-rootto obtain a short-term session scoped to a single task policy (e.g.S3UnlockBucketPolicy,SQSUnlockQueuePolicy,IAMDeleteRootUserCredentials). No permanent root, no per-account password.
# Short-lived, task-scoped root session into a member account (no member root creds needed):
aws sts assume-root \
--target-principal 123456789012 \
--task-policy-arn arn:aws:iam::aws:policy/root-task/S3UnlockBucketPolicy
The management account’s own root still exists and must stay hardened (MFA, no access keys, alarmed) — but the long tail of member-account roots, historically the estate’s worst-audited credential, can go to zero. Think of it as the root-user twin of “no IAM users for humans.”
Trusted identity propagation — carrying the human identity into the data stack
Identity Center gets a user into an account as a role. But the moment they open Amazon QuickSight, query Amazon Redshift, run an EMR job, or read through S3 Access Grants / Lake Formation, the classic pattern collapsed everyone into a shared service role — and you lost per-user authorization and per-user audit exactly where the data is most sensitive.
Trusted identity propagation fixes this. Connected AWS analytics services request a token from Identity Center and pass the actual user/group identity downstream, so authorization and CloudTrail happen against Alice, not against a shared quicksight-reader role. Grants can then be keyed to Identity Center groups: a Lake Formation permission or an S3 Access Grant says “the aws-analysts group may read this database/prefix,” and QuickSight/Redshift/Athena honour it per person. For applications outside AWS, the same flow is opened up via a trusted token issuer (your IdP vouches for the user with an OAuth 2.0 / JWT token that Identity Center exchanges). The payoff is that “who queried this PII column?” has a real answer — the person, not a pool.
Identity Center vs Amazon Cognito — workforce identity is not customer identity
A recurring architecture mistake is reaching for the wrong identity service because both say “sign-in and federation.” They serve different populations:
| IAM Identity Center | Amazon Cognito | |
|---|---|---|
| Who signs in | Your workforce — employees, contractors | Your app’s customers — end users of a product you build |
| Also called | Workforce identity / SSO | CIAM (customer identity & access management) |
| Source of truth | Corporate IdP (Entra/Okta) or AD | Your app’s user pool (+ social / OIDC / SAML logins) |
| What they get access to | AWS accounts & internal apps (console, CLI, analytics) | Your application, and temporary AWS creds for the app via identity pools |
| Scale | Thousands of employees | Millions of consumers |
| Wrong use | Putting customers in Identity Center | Putting employee console access in Cognito |
Rule of thumb: if the human is on your payroll, Identity Center; if the human is your product’s customer, Cognito. They can coexist in one estate — Identity Center for the engineers operating the platform, Cognito user pools fronting the customer-facing app running on that platform.
Choosing the identity source — and the cost of changing it later
Identity Center allows exactly one identity source at a time, and the choice has long tails:
- External IdP (SAML + SCIM) — the recommended workforce default. Lifecycle is HR-driven; MFA/Conditional Access live at the IdP.
- AWS Managed Microsoft AD / AD Connector — when Active Directory is still the authority. Managed Microsoft AD runs real domain controllers in AWS (can trust on-prem); AD Connector is a stateless proxy to on-prem AD (no directory hosted in AWS). With AD as the source, provisioning is AD synchronization, not SCIM, and the feature set differs subtly (e.g. attribute handling).
- Built-in Identity Center directory — fine for a tiny org or a break-glass path, but it makes you the joiner/leaver process, which is the governance gap you’re trying to close.
The sharp edge: switching source is disruptive. Principals (users/groups) are identified by the current source, so changing from the built-in directory to Entra, or from AD to an IdP, generally means re-establishing group identities and re-creating assignments against the new principals. Pick the source deliberately at the start, in the same spirit as pinning the home Region — this is not a setting you toggle casually in production.
MFA, and the two different “sessions” people confuse
MFA lives wherever the identity source lives. With an external IdP, enforce MFA there (Entra Conditional Access, Okta sign-on policies) — prefer phishing-resistant FIDO2 / passkeys — and Identity Center simply trusts the resulting assertion; do not try to double-enforce in AWS. With the built-in directory or AD, Identity Center is the enforcer and offers context-aware (adaptive — challenge on a new device/context), always-on, and registration-required modes, supporting FIDO2 security keys, passkeys, and TOTP authenticator apps.
Two “durations” get conflated and they are not the same knob:
- Permission-set session duration (
PT1H–PT12H) governs how long the role’s temporary credentials last before the SDK must refresh them. - The access-portal / authentication session (default ~8 hours, configured separately) governs how long before the user must re-authenticate to the portal at all.
A 4-hour role session inside a 9-hour portal session means credentials silently refresh without a new login for the working day, then everything expires overnight. Tune both; don’t set a 12-hour admin role session and forget the portal side.
What the delegated administrator still cannot do
Delegating Identity Center administration to a shared-services/identity account (strongly recommended) moves most day-to-day work — creating permission sets, managing member-account assignments, editing the built-in directory — out of the management account. But a short list of actions stay in the management account by design:
- Enabling or deleting the Identity Center instance.
- Changing the identity source (e.g. built-in → external IdP).
- Assignments into the management account itself — you cannot delegate the power to grant access to the payer.
Knowing this list prevents the “why does this Terraform fail from the identity account?” surprise: those specific operations must run with management-account credentials.
Scale, quotas, and the failure modes that actually happen
- Provisioning fan-out has limits. The generated
AWSReservedSSO_*role is a normal IAM role and inherits IAM’s own caps — notably the limit on attached managed policies per role (10 by default, adjustable to 20) and total inline-policy size. A permission set that piles on managed policies can fail to provision into accounts; prefer a scoped inline policy over stacking many managed ones. Identity Center also caps permission sets provisioned per account (50 by default), which a per-team RBAC explosion will hit — another reason ABAC’s single operator set matters. - Edits require re-provision. Changing a permission set marks assigned accounts “needs provisioning”; if your pipeline doesn’t push it, live roles drift from the definition. Treat “provision to all accounts” as part of every change.
- The home Region is a one-way door. Identity Center configuration is single-Region; there is no move — only recreate.
- SCIM is a silent single point of failure. A lapsed SCIM bearer token stops provisioning without an obvious error, so terminations quietly stop propagating. Rotate the token on a schedule and alarm on sync failures.
- The IdP is your availability dependency. If SAML/the IdP is down, nobody signs in — which is precisely why the non-federated break-glass path (built-in directory admin and/or a hardened root) is mandatory, excluded from normal flows, and alarmed on use.
When raw IAM SAML federation is still the right tool
Identity Center is the workforce answer, but direct IAM SAML federation — an iam-saml-provider plus roles assumed via AssumeRoleWithSAML — has not gone away and is occasionally correct: a single-account or workload-specific federation, a machine/service SAML integration, or a niche where adopting Identity Center is disproportionate. The trade-off is exactly what Identity Center automates away: with raw IAM SAML you build the role catalog and the cross-account fan-out per account, by hand. For anything resembling “our staff need least-privilege access across many accounts,” that hand-work is the reason Identity Center exists — reserve raw IAM SAML for the genuinely single-purpose edge.
Deliverables & checklist
Common pitfalls
- Falling back to IAM users with access keys for humans. The moment someone creates a “service” IAM user for a person “just to unblock them,” you’ve reintroduced long-lived, leakable credentials and broken central off-boarding. Avoid it: make Identity Center the only human path, retire standing human IAM users, and use an SCP/permissions boundary that denies IAM-user creation in workload accounts.
- SAML without SCIM (manual provisioning). Sign-in works, but nobody is deprovisioned automatically — terminated staff linger, and you’re back to the off-boarding gap. Avoid it: always pair SAML (auth) with SCIM (provisioning) so the joiner/leaver lifecycle is HR-driven and instant.
- A permission set per team (RBAC explosion). Enumerating (team × environment × scope) produces hundreds of permission sets and assignments nobody can reason about. Avoid it: keep a small job-function catalog and use ABAC (principal-tag = resource-tag) to self-scope a single operator set across all teams.
- Over-broad cross-account trust. A role that trusts an entire account (
:root) — or"AWS":"*"— withAdministratorAccess, or a third-party trust with noExternalId, hands away the isolation the account boundary gave you. Avoid it: trust specific role ARNs, addaws:PrincipalOrgID(enforced org-wide via RCP), requireExternalIdfor SaaS, and run IAM Access Analyzer to catch external/unused trust. - ABAC on untrusted or missing tags. If principals can self-set the controlling attribute, or resources are created untagged, ABAC either grants too much or silently denies. Avoid it: source attributes from the IdP (HR-governed, not user-editable), enforce tag-on-create with SCP/Config/Hooks, and make tag mutation a privileged, boundary-gated action.
- Running everything from the management account / 12-hour admin sessions. Managing Identity Center and permission sets from the payer, with long sessions on
AdministratorAccess, maximizes blast radius. Avoid it: delegate Identity Center administration to a member account, attach boundaries to admin permission sets, and set short session durations with sign-in logging to CloudTrail.
Common beginner mistakes
These are conceptual traps — the wrong mental model a newcomer brings — as opposed to the architect-level anti-patterns in Common pitfalls above. Fixing the model prevents a whole class of errors.
-
“Identity Center is just a prettier login page.” It isn’t a page; it’s an authorization plane. Behind the portal it provisions and manages a real IAM role (
AWSReservedSSO_*) in every account you’re assigned to, mints short-lived STS credentials on each sign-in, and correlates your identity across the whole org in CloudTrail. The portal URL is only the front door of a much larger machine. -
“The permission set is the role — I’ll just edit the
AWSReservedSSO_*role in IAM.” The generated role is managed for you; hand-edits are wiped the next time the permission set re-provisions. The permission set is the source, the role is the output. Always change the permission set (ideally in Terraform), then re-provision — never touch the role directly. -
“SSO means one credential I can save in
~/.aws/credentials.” There is no long-lived credential to save.aws sso loginperforms a browser authorization and the SDK exchanges the resulting token for temporary STS credentials on demand. If you find yourself pasting an access key, you’ve left the model — that’s the IAM-user anti-pattern coming back. -
“Identity Center replaces IAM.” It sits on top of IAM and STS. The roles it generates are IAM roles; the policies in a permission set are IAM policies; cross-account still runs on STS
AssumeRole. You need IAM fluency to use Identity Center well — it automates the plumbing, it doesn’t abolish it. -
“Assigning someone to an account gives them full access to it.” An assignment grants exactly the permission set’s policies — which might be
ReadOnlyAccess. Access is the three-way product principal × permission set × account; the account half only says where, the permission-set half says what. -
“Tags are just labels — turning on ABAC is harmless.” Once a policy keys on
aws:ResourceTag/team, the tag grants access. Writing or changing that tag becomes an authorization event that must be locked down, and a resource created without the tag has no owner to scope to. ABAC turns your tagging discipline into your security boundary. -
“I’ll use Cognito for my team’s console access” (or “Identity Center for my app’s customers”). Wrong population. Identity Center = your workforce getting into AWS; Cognito = your product’s customers getting into your app. Swapping them creates a security and scaling mess.
Practice challenges
Work these top to bottom; they escalate from beginner to advanced and reuse the Meridian placeholders. No live account is needed — the goal is to produce the correct config, JSON, or Terraform and be able to say why.
1 (beginner) — Wire up CLI access. Your access portal is https://meridian.awsapps.com/start in ap-south-1. You want a profile that assumes the ReadOnly permission set in account 123456789012. Write the ~/.aws/config and the command that gets you signed in.
<details><summary>Solution</summary>
[sso-session meridian]
sso_start_url = https://meridian.awsapps.com/start
sso_region = ap-south-1
sso_registration_scopes = sso:account:access
[profile audit-ro]
sso_session = meridian
sso_account_id = 123456789012
sso_role_name = ReadOnly
region = ap-south-1
aws sso login --sso-session meridian
aws sts get-caller-identity --profile audit-ro
Why: sso_role_name is the permission-set name; aws sso login gets short-lived credentials with no stored access key.
</details>
2 (beginner→intermediate) — Name the generated role. A colleague asks you to “just edit the admin role in account 123456789012” that Identity Center created for the PlatformAdmin permission set. What is the role’s name pattern, and should you edit it?
<details><summary>Solution</summary>
The role is AWSReservedSSO_PlatformAdmin_<random-hash> under path /aws-reserved/sso.amazonaws.com/<region>/. Do not edit it — change the PlatformAdmin permission set and re-provision; direct edits are reverted on the next provision.
Why: the permission set is the source of truth; the role is generated output that Identity Center owns. </details>
3 (intermediate) — Author a permission set as code. Write Terraform for a SecurityAudit permission set: 4-hour session, the AWS-managed SecurityAudit policy attached, in the Identity Center instance.
<details><summary>Solution</summary>
data "aws_ssoadmin_instances" "this" {}
locals { arn = tolist(data.aws_ssoadmin_instances.this.arns)[0] }
resource "aws_ssoadmin_permission_set" "sec_audit" {
name = "SecurityAudit"
instance_arn = local.arn
session_duration = "PT4H"
}
resource "aws_ssoadmin_managed_policy_attachment" "sec_audit" {
instance_arn = local.arn
permission_set_arn = aws_ssoadmin_permission_set.sec_audit.arn
managed_policy_arn = "arn:aws:iam::aws:policy/SecurityAudit"
}
Why: session_duration is ISO-8601 (PT4H); the managed-policy ARN is a separate attachment resource, not an inline argument.
</details>
4 (intermediate→advanced) — Two trust policies. Write the trust policy for a meridian-deploy role that only your pipeline role arn:aws:iam::111111111111:role/meridian-cicd-pipeline in org o-abcd1234ef may assume; then the variant for a SaaS vendor in account 999999999999.
<details><summary>Solution</summary>
// Internal pipeline — specific ARN + org pin, no ExternalId needed:
{ "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111111111111:role/meridian-cicd-pipeline" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "aws:PrincipalOrgID": "o-abcd1234ef" } } }
// Third-party SaaS — trust their account root + a per-customer ExternalId:
{ "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::999999999999:root" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "sts:ExternalId": "meridian-7f3a-uniquely-issued" } } }
Why: internal callers are pinned by ARN + aws:PrincipalOrgID; a multi-tenant SaaS needs an ExternalId to defeat the confused-deputy problem.
</details>
5 (advanced) — One operator set for every team via ABAC. Write the inline policy that lets an operator stop/start EC2 only for their own team, the Terraform that maps the team attribute to a session tag, and name the control that stops it failing open.
<details><summary>Solution</summary>
{ "Effect": "Allow",
"Action": ["ec2:StartInstances","ec2:StopInstances"],
"Resource": "*",
"Condition": { "StringEquals": { "aws:ResourceTag/team": "${aws:PrincipalTag/team}" } } }
resource "aws_ssoadmin_instance_access_control_attributes" "abac" {
instance_arn = local.arn
attribute { key = "team" value { source = ["$${path:enterprise.department}"] } }
}
Missing control: tag-on-create enforcement (an SCP denying ec2:RunInstances when aws:RequestTag/team is null) plus a guardrail so principals can’t re-tag to widen access.
Why: the ${aws:PrincipalTag/team} variable self-scopes one policy to all teams; without enforced resource tags the ResourceTag check has nothing to match and ABAC fails open.
</details>
6 (advanced) — Break-glass into a locked-out account. Someone applied an S3 bucket policy in member account 123456789012 that denies everyone, including all IAM roles. You’ve removed member-account root credentials org-wide. How do you recover, and what standing access does this require?
<details><summary>Solution</summary>
Use centralized root access: from the management account (or IAM delegated admin), obtain a short-term, task-scoped root session — no member root credential needed:
aws sts assume-root \
--target-principal 123456789012 \
--task-policy-arn arn:aws:iam::aws:policy/root-task/S3UnlockBucketPolicy
It requires zero standing root access in the member account; the privileged session is just-in-time and task-limited. Keep an alarmed, non-federated Identity Center break-glass admin as the parallel path if the IdP is also down.
Why: unlocking a resource-based policy is a root-only action, but centralized root access performs it with a short-lived, single-purpose session instead of a permanent member-account root credential. </details>
Glossary
- IAM Identity Center — AWS’s managed workforce single-sign-on service (formerly AWS SSO); the central front door to every account in your Organization and to SAML/OIDC apps.
- Workforce identity — access for people on your payroll (employees, contractors), as opposed to your application’s customers.
- AWS access portal — the web page (e.g.
https://<name>.awsapps.com/start) where a signed-in user picks an account + role and is dropped into the console with temporary credentials. - Identity source — where Identity Center’s users and groups come from: the built-in directory, an external IdP (via SAML + SCIM), or Active Directory. Exactly one at a time.
- Permission set — a named, account-agnostic bundle of IAM policies (managed + customer-managed + inline + a permissions boundary) plus a session duration; Identity Center turns it into an IAM role in each assigned account.
- Account assignment — the three-way binding (user or group) × (permission set) × (account) that actually grants access.
AWSReservedSSO_*role — the IAM role Identity Center generates and manages in a target account for a permission set (path/aws-reserved/sso.amazonaws.com/…); never hand-edit it.- AWS STS — Security Token Service, the engine that issues the short-lived credentials behind every sign-in and every
AssumeRole. - Temporary / short-lived credentials — access keys + a session token that expire (here, 1–12h); the opposite of long-lived IAM access keys.
- SAML 2.0 — the browser-redirect protocol that handles authentication (sign-in) between your IdP and Identity Center.
- SCIM 2.0 — the background REST protocol that handles provisioning: the IdP pushes users/groups in, and deactivates them on termination.
- IdP (Identity Provider) — the system that authenticates your people (Microsoft Entra ID, Okta, Ping, Google Workspace).
- External IdP federation — making your corporate IdP the identity source for Identity Center, so AWS users are the same humans HR manages.
- ACS URL / entity ID — SAML endpoints and identifiers exchanged as “metadata” so the IdP and Identity Center trust each other.
- AWS Managed Microsoft AD / AD Connector — Directory Service options: a real managed Active Directory in AWS, or a stateless proxy to on-prem AD, respectively; either can be Identity Center’s source.
- MFA (multi-factor authentication) — a second sign-in factor; prefer phishing-resistant FIDO2 / passkeys. Enforced at the external IdP when one is used.
- Conditional Access — IdP-side rules (device compliance, location, risk) that gate sign-in; they flow through to AWS automatically under federation.
- Managed policy / inline policy / customer-managed policy — an AWS- or you-authored reusable policy; a policy embedded directly in one permission set; and a policy referenced by name that must pre-exist in each account.
- Permissions boundary — an IAM policy that caps the maximum effective permissions of a role, regardless of what its attached policies grant.
- Session duration — how long a permission set’s role credentials last (
PT1H–PT12H); distinct from the access-portal authentication session. - Relay state — an optional deep-link page the console lands on after sign-in via a permission set.
- Cross-account access — operating in account B from account A; for humans via Identity Center, for workloads via
sts:AssumeRole. AssumeRole/AssumeRoleWithSAML— STS calls that let a principal (or a SAML-federated user) take on a role and receive temporary credentials.- Trust policy vs identity policy — the resource-side statement of who may assume a role, vs the caller-side permission to make the assume call; both must agree.
ExternalId— a per-customer token a third-party SaaS must present when assuming your role, defeating the confused-deputy problem (a multi-tenant service tricked into acting for the wrong customer).aws:PrincipalOrgID— a condition key pinning “the caller belongs to my Organization,” used to forbid out-of-org role assumption.- ABAC (attribute-based access control) — deciding access by matching tags on the principal and the resource, instead of enumerating grants.
- RBAC (role-based access control) — deciding access by which role/permission set you hold; ABAC’s complement.
- Session tag /
aws:PrincipalTag/aws:ResourceTag— an attribute attached to the signed-in session; the condition keys for the principal’s tag and the resource’s tag that ABAC policies compare. - Attributes for access control — the Identity Center feature that maps IdP/directory attributes onto session tags.
- Tag-on-create — enforcing (via SCP/Config/Hooks) that resources are created with required tags, so ABAC has something to match and doesn’t fail open.
- Delegated administrator — a member account registered to run Identity Center day-to-day, keeping most work out of the management account.
- Home Region — the single Region that holds Identity Center’s configuration; effectively unchangeable once set.
- Break-glass — an emergency, non-federated access path (built-in directory admin or hardened root), excluded from normal flows and alarmed on use.
- Root user — the email-based identity that owns an AWS account and can perform a few actions no IAM policy governs.
- Centralized root access — the IAM capability to remove root credentials from member accounts and perform rare root-only tasks just-in-time via short-term sessions (
aws sts assume-root) scoped to a task policy. - Trusted identity propagation — carrying a user’s real Identity Center identity into analytics services (QuickSight, Redshift, EMR, Lake Formation, S3 Access Grants) for per-user authorization and audit; enabled via a trusted token issuer.
- Amazon Cognito — AWS’s customer identity service (CIAM): user pools for app sign-in and identity pools for handing app users temporary AWS credentials. For your product’s customers, not your workforce.
OrganizationAccountAccessRole/AWSControlTowerExecution— pre-built cross-account roles that let the management/Control Tower plane act inside member accounts.- RCP / SCP — Resource / Service Control Policies; org-wide guardrails that can, for example, force
aws:PrincipalOrgIDon all role trust (a data-perimeter control).
What’s next
With the human-access layer federated, least-privilege, and attribute-scoped, part 7 of the AWS Landing Zone & Control Tower series turns to centralized security operations — wiring GuardDuty, Security Hub, AWS Config, Detective, and Macie through the Audit account so the identities you just governed are continuously monitored and any anomalous access is detected and responded to org-wide.