AWS Lesson 84 of 123

AWS Landing Zone: Identity & Access (IAM Identity Center) — SSO, Permission Sets, External IdP Federation, Cross-Account Access, and ABAC

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.

AWS Landing Zone & Control Tower — animated overview

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:

  1. 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.
  2. Permission sets — named, reusable bundles of IAM policy that define what an assigned identity can do in an account. (Its own deep section below.)
  3. 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

Concrete artifacts, decisions, and AWS tools

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:

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

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

Concrete artifacts, decisions, and AWS tools

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:

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

Concrete artifacts, decisions, and AWS tools

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:

  1. 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.

  2. Workload/automation cross-account access is sts:AssumeRole between 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 allows sts:AssumeRole on 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:

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

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

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:

How to do it well

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

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:

# 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:

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:

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:

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

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

  1. 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.
  2. 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.
  3. 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.
  4. Over-broad cross-account trust. A role that trusts an entire account (:root) — or "AWS":"*" — with AdministratorAccess, or a third-party trust with no ExternalId, hands away the isolation the account boundary gave you. Avoid it: trust specific role ARNs, add aws:PrincipalOrgID (enforced org-wide via RCP), require ExternalId for SaaS, and run IAM Access Analyzer to catch external/unused trust.
  5. 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.
  6. 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.

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

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.

AWSLanding ZoneIdentity & Access (IAM Identity Center)Enterprise
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