v0.2.0-draft AI Drafted

AWS IAM Identity Center Hardening Guide

Identity Last updated: 2026-08-08

AWS identity management hardening for IAM Identity Center including MFA, permission sets, and account access

View:

Overview

AWS IAM Identity Center (formerly AWS SSO) is the recommended service for managing workforce access to AWS accounts and applications. As the central identity service for AWS Organizations, IAM Identity Center security configurations directly impact cloud access security.

Intended Audience

  • Security engineers managing AWS access
  • Cloud administrators configuring IAM Identity Center
  • Platform engineers managing AWS Organizations
  • GRC professionals assessing cloud identity

How to Use This Guide

  • L1 (Crawl): Essential controls for all organizations
  • L2 (Walk): Enhanced controls for security-sensitive environments
  • L3 (Run): Strictest controls for regulated industries

Scope

This guide covers AWS IAM Identity Center security including MFA enforcement, permission sets, identity sources, and session policies.


Table of Contents

  1. Authentication & MFA
  2. Identity Source Configuration
  3. Permission Management
  4. Monitoring & Compliance
  5. Compliance Quick Reference

1. Authentication & MFA

1.1 Enforce Multi-Factor Authentication

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.5
NIST 800-53 IA-2(1)

Description

Require MFA for all IAM Identity Center users — enforced natively when the identity source is the Identity Center directory, AWS Managed Microsoft AD, or AD Connector, and enforced at the external IdP when one is connected.

Correction (2026-08-08) — where MFA is actually enforced. AWS documents that IAM Identity Center’s native MFA configuration is “currently not supported for external identity providers.” If your identity source is an external IdP (see 2.1), the MFA settings described below do not apply to those users and MFA must be enforced in the IdP itself. Earlier revisions of this guide recommended both native MFA enforcement (1.1) and an external IdP (2.1) as L1 controls without noting that the first is inert under the second — implement whichever matches your identity source, and treat 2.1 as carrying the MFA obligation when an external IdP is in use. Sources: Enable MFA · Configure MFA

Rationale

Why This Matters:

  • MFA adds a second factor beyond the password, so a phished or stolen credential alone cannot reach the AWS access portal
  • IAM Identity Center is the single front door to every account in the organization — one MFA-less login compromise can expose the entire AWS estate
  • FIDO2 security keys and authenticator apps resist phishing and SIM-swap attacks that defeat SMS-delivered codes
  • Enforcing MFA on every sign-in closes the gap where long-lived sessions skip re-verification

Attack Prevented: Credential theft, phishing, credential stuffing, MFA bypass, account takeover

Prerequisites

  • IAM Identity Center enabled in management account
  • AWS Organizations configured
  • Admin access to IAM Identity Center

ClickOps Implementation

Step 0: Determine Which Path Applies

  1. Navigate to: IAM Identity CenterSettingsIdentity source
  2. If the source is Identity Center directory, AWS Managed Microsoft AD, or AD Connector, continue with Steps 1-3 below
  3. If the source is an external identity provider, stop here — configure MFA in that IdP instead, and record the IdP’s enforcement policy as the evidence for this control

Step 1: Access MFA Settings

  1. Navigate to: IAM Identity CenterSettingsAuthentication
  2. Find MFA configuration — note that MFA is on by default for directory users, so the task is usually verification and tightening rather than initial enablement

Step 2: Configure MFA Requirement

  1. Select Require MFA
  2. Configure enforcement:
    • Every sign-in (recommended)
    • Context-aware
  3. Save changes

Step 3: Configure MFA Types

  1. Enable authenticator apps
  2. Enable hardware TOTP devices
  3. Enable FIDO2 security keys (recommended)
  4. Disable SMS if possible
  5. Note the per-user registration ceiling: a user may register a maximum of 8 MFA devices

Time to Complete: ~30 minutes


1.2 Configure Session Duration

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.2
NIST 800-53 AC-12

Description

Configure session duration limits across the three distinct session types IAM Identity Center maintains — interactive (portal) sessions, permission-set role sessions, and application sessions — recognizing that they expire independently of each other.

Rationale

Why This Matters:

  • Shorter sessions shrink the window in which a hijacked token or unattended session can be abused
  • Long-lived portal and permission-set sessions let a stolen session token stay valid long after a user leaves, undermining deprovisioning
  • Tighter durations on privileged permission sets force frequent re-authentication for the highest-blast-radius access
  • The three session types are independent — revoking one does not terminate the others, so a single “disable the user” action does not immediately cut access

Attack Prevented: Session hijacking, token replay, unattended-workstation abuse, standing access after offboarding

The Three Session Types

Session type Documented maximum Behavior on user revocation
Interactive (AWS access portal) session Up to 90 days Ends when the interactive session is terminated
Permission-set role session (AWS account access) Up to 12 hours Not affected — revoking, disabling, or deleting the user does not end an existing account session
Application session Refreshed automatically about every 1 hour; ends roughly 30 minutes after the interactive session ends Ends shortly after the interactive session

The 12-hour exposure window. AWS documents that removing a user’s access — including disabling or deleting the user — does not terminate permission-set role sessions already in flight. Those sessions run to their configured duration, which can be as long as 12 hours. Offboarding and incident response therefore require an explicit step beyond user deletion: shorten permission-set session durations in advance, and on suspected compromise revoke the active sessions and/or apply a deny policy to the assumed role rather than assuming user deletion is sufficient.

Kiro and long interactive sessions. AWS documents that Kiro sessions extend the interactive session duration to 90 days, and that depending on when your organization enabled the relevant capability this may be enabled by default. Verify the current interactive session setting rather than assuming a short default.

No SAML Single Logout. IAM Identity Center supports SAML Single Logout in neither direction — signing out of the external IdP does not sign the user out of the AWS access portal, and signing out of the portal does not sign them out of the IdP. Do not treat IdP logout as a session-termination control.

Source: IAM Identity Center — Authentication concepts

ClickOps Implementation

Step 1: Configure the Interactive (Portal) Session

  1. Navigate to: SettingsAuthentication
  2. Read the current session duration value before changing it — it may already be set to a long duration
  3. Reduce it to the shortest duration your workforce can tolerate; a 90-day interactive session is rarely defensible outside of specific tooling requirements

Step 2: Configure Permission Set Sessions

  1. Edit each permission set
  2. Set session duration — the maximum is 12 hours
  3. Apply materially shorter durations (1 hour or less) to administrative and other high-blast-radius permission sets, since these sessions survive user deletion

Step 3: Document the Revocation Procedure

  1. Record that user deletion alone leaves account sessions live for up to their configured duration
  2. Define the incident step that actually cuts access — terminating the active sessions and/or attaching a deny policy to the affected role
  3. Test the procedure so responders are not discovering the gap during an incident

1.3 Configure Attribute-Based Access Control

Profile Level: L2 (Walk)

Framework Control
CIS Controls 6.8
NIST 800-53 AC-3

Description

Enable ABAC for fine-grained access control.

Rationale

Why This Matters:

  • ABAC scopes access by matching user and session attributes (department, project, environment) to resource tags, enforcing least privilege dynamically
  • Tag-based conditions stop users from reaching resources outside their team or environment even when they hold a broad permission set
  • Attribute-driven policies scale without spawning an ever-growing set of bespoke permission sets per team or project

Attack Prevented: Lateral movement, over-broad access, privilege creep, cross-environment resource access

ClickOps Implementation

Step 1: Enable ABAC

  1. Navigate to: SettingsAttributes for access control
  2. Enable attributes
  3. Configure attribute mappings

Step 2: Use in Permission Sets

  1. Create ABAC-aware policies
  2. Reference user attributes
  3. Implement tag-based access

Code Pack: Terraform
hth-aws-iam-identity-center-1.03-configure-abac.tf View source on GitHub ↗
# Enable ABAC attributes for Identity Center
# Maps identity provider attributes to session tags for fine-grained access control
resource "aws_ssoadmin_instance_access_control_attributes" "abac" {
  instance_arn = local.sso_instance_arn

  attribute {
    key = "Department"
    value {
      source = ["${path.root}/Department"]
    }
  }

  attribute {
    key = "CostCenter"
    value {
      source = ["${path.root}/CostCenter"]
    }
  }

  attribute {
    key = "Project"
    value {
      source = ["${path.root}/Project"]
    }
  }
}

# Example permission set using ABAC for department-scoped access
resource "aws_ssoadmin_permission_set" "department_scoped" {
  instance_arn     = local.sso_instance_arn
  name             = "DepartmentScopedAccess"
  description      = "ABAC-enabled permission set scoped by department tag"
  session_duration = "PT4H"

  tags = {
    ManagedBy = "how-to-harden"
    Control   = "1.3-configure-abac"
  }
}

# Inline policy enforcing ABAC tag matching
resource "aws_ssoadmin_permission_set_inline_policy" "department_abac_policy" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.department_scoped.arn

  inline_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowDepartmentScopedAccess"
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:ListBucket"
        ]
        Resource = "*"
        Condition = {
          StringEquals = {
            "s3:ExistingObjectTag/Department" = "$${aws:PrincipalTag/Department}"
          }
        }
      }
    ]
  })
}

2. Identity Source Configuration

2.1 Configure External Identity Provider

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 6.3, 12.5
NIST 800-53 IA-2, IA-8

Description

Connect to external IdP for centralized identity.

Rationale

Why This Matters:

  • Centralizing authentication in your corporate IdP enforces MFA, conditional access, and password policy consistently on every AWS login
  • The built-in Identity Center directory lacks the risk-based and conditional-access controls a dedicated IdP provides
  • A single source of truth means offboarding in the IdP immediately cuts AWS access, eliminating orphaned local accounts
  • SAML federation removes locally stored AWS credentials that are a prime target for theft

Attack Prevented: Credential theft, orphaned-account access, inconsistent MFA enforcement, identity sprawl

This control carries the MFA obligation. IAM Identity Center’s native MFA configuration is not supported for external identity providers (enable-mfa). Once you connect an external IdP, the settings in 1.1 no longer govern those users — MFA, conditional access, and session policy must all be enforced in the IdP, and that is what an assessor should be shown as evidence.

ClickOps Implementation

Step 1: Change Identity Source

  1. Navigate to: SettingsIdentity source
  2. Click Change identity source
  3. Select external identity provider

Step 2: Configure SAML/SCIM

  1. Configure SAML 2.0 settings
  2. Enable automatic provisioning (SCIM)
  3. Configure attribute mappings

Step 3: Test and Migrate

  1. Test authentication
  2. Migrate users from Identity Center directory
  3. Verify access preserved

2.2 Configure Automatic Provisioning

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.3
NIST 800-53 AC-2

Description

Enable SCIM for automatic user provisioning.

Rationale

Why This Matters:

  • SCIM automatically deprovisions departed users and updates group membership, eliminating standing access from stale accounts
  • Manual provisioning drifts over time, leaving orphaned users and incorrect group assignments that widen the attack surface
  • Group sync keeps permission-set assignments aligned with IdP and HR reality, enforcing least privilege as roles change

Attack Prevented: Orphaned-account access, privilege creep, offboarding gaps, unauthorized standing access

ClickOps Implementation

Step 1: Enable SCIM

  1. Navigate to: SettingsIdentity source
  2. Enable automatic provisioning
  3. Generate SCIM endpoint and token

Step 2: Configure IdP

  1. Configure SCIM in identity provider
  2. Map user attributes
  3. Enable group sync

2.3 Restrict Account Instances of IAM Identity Center

Profile Level: L2 (Walk)

Framework Control
CIS Controls 4.1, 6.8
NIST 800-53 CM-7, AC-3

Description

Prevent member accounts in your AWS Organization from standing up their own account instances of IAM Identity Center, which would create identity sources outside the organization instance your security team governs.

Changed default. AWS documents that for organizations that enabled IAM Identity Center after 2023-11-15, the ability for member accounts to create their own account instances is enabled by default. Organizations that enabled Identity Center before that date have it disabled by default. Check which side of that line you are on rather than assuming. Source: Enable IAM Identity Center

Changed enablement options (August 2026). New IAM Identity Center enablement now offers Single-Region, Multi-Region, and Custom configurations. Two consequences worth deciding deliberately rather than accepting:

  • Choosing Multi-Region automatically creates a multi-Region customer managed KMS key, and AWS KMS charges apply — this interacts directly with 4.3, including its lockout risk
  • Multi-account permissions is now an optional capability that is enabled by default. If your deployment uses IAM Identity Center only for application authentication and never for AWS account access, disable it — leaving it on keeps an unused account-access surface live

Sources: Enable IAM Identity Center · IAM Identity Center document history

Rationale

Why This Matters:

  • An account instance created in a member account is a separate identity source with its own users, assignments, and applications — invisible to the controls applied to the organization instance
  • A team that stands up its own instance can grant application access on identities the central security team never provisioned, deprovisions, or reviews (a shadow-IdP problem, not merely a duplicate-tooling problem)
  • Because this is enabled by default for organizations onboarded after 2023-11-15, the exposure is usually inherited rather than deliberately chosen
  • Centralizing on the organization instance is what makes MFA policy, session policy, and access reviews actually complete

Attack Prevented: Shadow identity provider creation, ungoverned application access grants, identity sprawl outside central review, bypass of organization-wide access policy

Prerequisites

  • AWS Organizations with all features enabled
  • Service control policy (SCP) authoring permissions in the management account
  • An inventory of any existing account instances

ClickOps Implementation

Step 1: Inventory Existing Account Instances

  1. Determine whether any member account has already created an account instance of IAM Identity Center
  2. For each one found, identify its owner, its identity source, and the applications assigned through it
  3. Plan migration of any legitimate use onto the organization instance before restricting creation

Step 2: Restrict Creation via Service Control Policy

  1. In the management account, navigate to: AWS OrganizationsPoliciesService control policies
  2. Create or edit an SCP that denies the Identity Center instance-creation action for member accounts
  3. Attach the SCP to the organizational units that should never host their own instance (in most estates, all of them)

Step 3: Confirm the Restriction

  1. From a member account, attempt to create an account instance and confirm it is denied
  2. Record the SCP as the compensating control in your access-management documentation

Validation & Testing

  1. Re-run the account-instance inventory and confirm no new instances appear
  2. Verify the SCP is attached to every relevant OU, including newly created ones
  3. Confirm new-account provisioning automation attaches the SCP by default

Compliance Mappings

Framework Control ID Control Description
SOC 2 CC6.1 Logical access is restricted to authorized identity sources
NIST 800-53 CM-7 Least functionality — unnecessary service capability disabled
NIST 800-53 AC-3 Access enforcement through a governed identity source
CIS Controls v8 4.1 Secure configuration process for enterprise assets and software

3. Permission Management

3.1 Configure Permission Sets

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6

Description

Create least-privilege permission sets.

Rationale

Why This Matters:

  • Least-privilege permission sets limit what any assigned user can do, shrinking the blast radius of a compromised login
  • Permissions boundaries cap the maximum permissions a set can grant, blocking privilege-escalation paths even via inline policies
  • Broadly assigned permissive sets (such as AdministratorAccess) turn any account takeover into full organizational compromise

Attack Prevented: Privilege escalation, lateral movement, blast-radius expansion, excessive standing permissions

ClickOps Implementation

Step 1: Review Permission Sets

  1. Navigate to: Permission sets
  2. Review existing permission sets
  3. Identify overly permissive sets

Step 2: Create Least-Privilege Sets

  1. Create custom permission sets
  2. Use AWS managed policies where possible
  3. Apply inline policies for restrictions

Step 3: Configure Permissions Boundary

  1. Apply permissions boundaries
  2. Limit maximum permissions
  3. Prevent privilege escalation

Code Pack: Terraform
hth-aws-iam-identity-center-3.01-configure-permission-sets.tf View source on GitHub ↗
# Read-only permission set for auditors and compliance teams
resource "aws_ssoadmin_permission_set" "read_only" {
  instance_arn     = local.sso_instance_arn
  name             = "HTH-ReadOnlyAccess"
  description      = "Read-only access for auditors -- 1-hour session limit"
  session_duration = "PT1H"

  tags = {
    ManagedBy = "how-to-harden"
    Control   = "3.1-configure-permission-sets"
    Profile   = "L1"
  }
}

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

# Developer permission set with scoped access
resource "aws_ssoadmin_permission_set" "developer" {
  instance_arn     = local.sso_instance_arn
  name             = "HTH-DeveloperAccess"
  description      = "Scoped developer access -- no IAM or Organizations changes"
  session_duration = "PT4H"

  tags = {
    ManagedBy = "how-to-harden"
    Control   = "3.1-configure-permission-sets"
    Profile   = "L1"
  }
}

resource "aws_ssoadmin_managed_policy_attachment" "developer_poweruser" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.developer.arn
  managed_policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
}

# Admin permission set with short session and boundary
resource "aws_ssoadmin_permission_set" "admin" {
  instance_arn     = local.sso_instance_arn
  name             = "HTH-AdminAccess"
  description      = "Full admin access -- 1-hour session, restricted to break-glass"
  session_duration = "PT1H"

  tags = {
    ManagedBy = "how-to-harden"
    Control   = "3.1-configure-permission-sets"
    Profile   = "L1"
  }
}

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

# Deny inline policy to restrict admin from modifying CloudTrail or GuardDuty
resource "aws_ssoadmin_permission_set_inline_policy" "admin_guardrails" {
  instance_arn       = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.admin.arn

  inline_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenySecurityServiceModification"
        Effect = "Deny"
        Action = [
          "cloudtrail:DeleteTrail",
          "cloudtrail:StopLogging",
          "guardduty:DeleteDetector",
          "guardduty:DisassociateFromMasterAccount",
          "access-analyzer:DeleteAnalyzer"
        ]
        Resource = "*"
      }
    ]
  })
}
Code Pack: API Script
hth-aws-iam-identity-center-3.01-configure-permission-sets.sh View source on GitHub ↗
# List all permission sets and their configurations
info "3.1 Retrieving all permission sets..."
PS_ARNS=$(sso_admin list-permission-sets \
  | jq -r '.PermissionSets[]' 2>/dev/null) || {
  fail "3.1 Failed to list permission sets"
  increment_failed
  summary
  exit 0
}

PS_COUNT=0
LONG_SESSION_COUNT=0
MAX_RECOMMENDED_DURATION="PT4H"

for PS_ARN in ${PS_ARNS}; do
  PS_DETAIL=$(sso_admin describe-permission-set \
    --permission-set-arn "${PS_ARN}" 2>/dev/null) || continue

  PS_NAME=$(echo "${PS_DETAIL}" | jq -r '.PermissionSet.Name')
  SESSION_DURATION=$(echo "${PS_DETAIL}" | jq -r '.PermissionSet.SessionDuration')
  PS_COUNT=$((PS_COUNT + 1))

  # Check for overly long session durations (> 4 hours)
  DURATION_SECONDS=$(echo "${SESSION_DURATION}" | sed 's/PT//;s/H/*3600+/;s/M/*60+/;s/S//;s/+$//' | bc 2>/dev/null || echo "0")
  if [ "${DURATION_SECONDS}" -gt 14400 ]; then
    warn "3.1 Permission set '${PS_NAME}' has long session: ${SESSION_DURATION}"
    LONG_SESSION_COUNT=$((LONG_SESSION_COUNT + 1))
  fi

  # List managed policies attached
  MANAGED_POLICIES=$(sso_admin list-managed-policies-in-permission-set \
    --permission-set-arn "${PS_ARN}" \
    | jq -r '.AttachedManagedPolicies[].Name' 2>/dev/null || echo "none")

  info "3.1   ${PS_NAME} (session: ${SESSION_DURATION}, policies: ${MANAGED_POLICIES})"
done
# Flag permission sets with AdministratorAccess or overly broad policies
info "3.1 Checking for overly broad permission sets..."
ADMIN_PS_COUNT=0

for PS_ARN in ${PS_ARNS}; do
  PS_DETAIL=$(sso_admin describe-permission-set \
    --permission-set-arn "${PS_ARN}" 2>/dev/null) || continue
  PS_NAME=$(echo "${PS_DETAIL}" | jq -r '.PermissionSet.Name')

  MANAGED_POLICIES=$(sso_admin list-managed-policies-in-permission-set \
    --permission-set-arn "${PS_ARN}" \
    | jq -r '.AttachedManagedPolicies[].Arn' 2>/dev/null || true)

  if echo "${MANAGED_POLICIES}" | grep -q "AdministratorAccess"; then
    warn "3.1 Permission set '${PS_NAME}' has AdministratorAccess -- apply least privilege"
    ADMIN_PS_COUNT=$((ADMIN_PS_COUNT + 1))
  fi
done

if [ "${ADMIN_PS_COUNT}" -gt 0 ]; then
  warn "3.1 ${ADMIN_PS_COUNT} permission set(s) use AdministratorAccess -- review for least privilege (AC-6)"
else
  pass "3.1 No permission sets use AdministratorAccess"
fi
Code Pack: Sigma Detection Rule
hth-aws-iam-identity-center-3.01-permission-set-modified.yml View source on GitHub ↗
detection:
    selection:
        eventSource: sso.amazonaws.com
        eventName:
            - 'CreatePermissionSet'
            - 'UpdatePermissionSet'
            - 'DeletePermissionSet'
            - 'PutInlinePolicyToPermissionSet'
            - 'DeleteInlinePolicyFromPermissionSet'
            - 'AttachManagedPolicyToPermissionSet'
            - 'DetachManagedPolicyFromPermissionSet'
            - 'AttachCustomerManagedPolicyReferenceToPermissionSet'
            - 'DetachCustomerManagedPolicyReferenceToPermissionSet'
            - 'PutPermissionsBoundaryToPermissionSet'
            - 'DeletePermissionsBoundaryFromPermissionSet'
    condition: selection
fields:
    - eventTime
    - eventName
    - userIdentity.arn
    - sourceIPAddress
    - requestParameters.permissionSetArn
    - requestParameters.name

3.2 Configure Account Assignments

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6

Description

Assign access to AWS accounts.

Rationale

Why This Matters:

  • Assigning users only the accounts they need contains a compromised identity to a limited scope rather than the whole organization
  • Group-based assignments make access auditable and revocable in one place instead of scattered per-user grants
  • Regular reviews catch accumulated and unnecessary cross-account access before an attacker can exploit it

Attack Prevented: Lateral movement across accounts, excessive access, privilege creep, unauthorized account access

ClickOps Implementation

Step 1: Review Assignments

  1. Navigate to: AWS accounts
  2. Review current assignments
  3. Identify unnecessary access

Step 2: Apply Least Privilege

  1. Assign minimum required accounts
  2. Use groups for assignments
  3. Regular access reviews

Code Pack: API Script
hth-aws-iam-identity-center-3.02-configure-account-assignments.sh View source on GitHub ↗
# Audit account assignments across all permission sets and accounts
info "3.2 Retrieving permission sets..."
PS_ARNS=$(sso_admin list-permission-sets \
  | jq -r '.PermissionSets[]' 2>/dev/null) || {
  fail "3.2 Failed to list permission sets"
  increment_failed
  summary
  exit 0
}

# Get all AWS accounts in the organization
info "3.2 Retrieving organization accounts..."
ACCOUNTS=$(aws_json organizations list-accounts \
  | jq -r '.Accounts[] | select(.Status == "ACTIVE") | .Id' 2>/dev/null) || {
  warn "3.2 Cannot list org accounts -- verify organizations:ListAccounts permission"
  ACCOUNTS=""
}

TOTAL_ASSIGNMENTS=0
USER_DIRECT_ASSIGNMENTS=0

for PS_ARN in ${PS_ARNS}; do
  PS_DETAIL=$(sso_admin describe-permission-set \
    --permission-set-arn "${PS_ARN}" 2>/dev/null) || continue
  PS_NAME=$(echo "${PS_DETAIL}" | jq -r '.PermissionSet.Name')

  for ACCOUNT_ID in ${ACCOUNTS}; do
    ASSIGNMENTS=$(sso_admin list-account-assignments \
      --account-id "${ACCOUNT_ID}" \
      --permission-set-arn "${PS_ARN}" 2>/dev/null) || continue

    ASSIGNMENT_LIST=$(echo "${ASSIGNMENTS}" | jq -r '.AccountAssignments[]' 2>/dev/null) || continue

    # Count and classify assignments
    ACCOUNT_ASSIGNMENT_COUNT=$(echo "${ASSIGNMENTS}" | jq '.AccountAssignments | length' 2>/dev/null || echo "0")
    TOTAL_ASSIGNMENTS=$((TOTAL_ASSIGNMENTS + ACCOUNT_ASSIGNMENT_COUNT))

    # Flag direct user assignments (should use groups instead)
    USER_ASSIGNMENTS=$(echo "${ASSIGNMENTS}" \
      | jq '[.AccountAssignments[] | select(.PrincipalType == "USER")] | length' 2>/dev/null || echo "0")
    if [ "${USER_ASSIGNMENTS}" -gt 0 ]; then
      warn "3.2 Account ${ACCOUNT_ID} / PS '${PS_NAME}': ${USER_ASSIGNMENTS} direct user assignment(s) -- use groups instead"
      USER_DIRECT_ASSIGNMENTS=$((USER_DIRECT_ASSIGNMENTS + USER_ASSIGNMENTS))
    fi
  done
done
# Verify management account has restricted access
info "3.2 Checking management account assignments..."
MGMT_ACCOUNT=$(aws_json organizations describe-organization \
  | jq -r '.Organization.MasterAccountId' 2>/dev/null) || {
  warn "3.2 Cannot determine management account -- verify organizations:DescribeOrganization permission"
  increment_applied
  summary
  exit 0
}

MGMT_ASSIGNMENTS=0
for PS_ARN in ${PS_ARNS}; do
  ASSIGNMENTS=$(sso_admin list-account-assignments \
    --account-id "${MGMT_ACCOUNT}" \
    --permission-set-arn "${PS_ARN}" 2>/dev/null) || continue
  COUNT=$(echo "${ASSIGNMENTS}" | jq '.AccountAssignments | length' 2>/dev/null || echo "0")
  MGMT_ASSIGNMENTS=$((MGMT_ASSIGNMENTS + COUNT))
done

if [ "${MGMT_ASSIGNMENTS}" -gt 2 ]; then
  warn "3.2 Management account (${MGMT_ACCOUNT}) has ${MGMT_ASSIGNMENTS} assignments -- limit access (AC-6(1))"
else
  pass "3.2 Management account access is appropriately restricted (${MGMT_ASSIGNMENTS} assignments)"
fi
Code Pack: Sigma Detection Rule
hth-aws-iam-identity-center-3.02-account-assignment-changed.yml View source on GitHub ↗
detection:
    selection:
        eventSource: sso.amazonaws.com
        eventName:
            - 'CreateAccountAssignment'
            - 'DeleteAccountAssignment'
            - 'ProvisionPermissionSet'
    condition: selection
fields:
    - eventTime
    - eventName
    - userIdentity.arn
    - sourceIPAddress
    - requestParameters.permissionSetArn
    - requestParameters.targetId
    - requestParameters.principalType
    - requestParameters.principalId

3.3 Protect Privileged Access

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6(1)

Description

Additional controls for privileged access.

Rationale

Why This Matters:

  • Administrative permission sets carry the highest blast radius, so per-session MFA and short durations limit how a stolen admin session can be abused
  • Separating admin sets from day-to-day access enforces deliberate, auditable elevation instead of always-on privilege
  • Restricting and reviewing admin assignments prevents the privilege accumulation that gives attackers a high-value target

Attack Prevented: Privileged account takeover, admin session hijacking, standing admin privilege, privilege escalation

ClickOps Implementation

Step 1: Create Privileged Permission Sets

  1. Create separate admin permission sets
  2. Apply shorter session duration
  3. Require MFA for every session

Step 2: Limit Admin Assignments

  1. Restrict admin access to required users
  2. Use groups for admin access
  3. Regular privileged access reviews

3.4 Govern the sso:account:access Scope on Customer Managed Applications

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.4, 6.8
NIST 800-53 AC-6, AC-3, SC-28

Description

Treat the sso:account:access scope — added for customer managed applications on 2026-06-19 — as a privileged grant, because it cannot be narrowed once given. Restrict which applications may request it, and enforce strict handling of the tokens it produces.

Rationale

Why This Matters:

  • AWS documents that this scope grants the application access to all AWS accounts and roles available to the authenticated user — it is not scopable to a subset, so the only meaningful control is deciding which applications receive it at all
  • Because the grant is all-or-nothing, an application that needs one account effectively receives the user’s entire account footprint, converting a narrow integration into a broad standing capability
  • Only the management account or a delegated administrator account can configure this, which makes the configuration itself a high-value target
  • The resulting bearer tokens are the actual credential — leaking one in a log, a URL, or browser-accessible storage hands an attacker the same breadth of access

Attack Prevented: Over-broad application access grants, token theft and replay, cross-account privilege escalation via a compromised integration, credential exposure through logs and URLs

Prerequisites

  • Management account or delegated administrator access
  • An inventory of customer managed applications configured in IAM Identity Center

ClickOps Implementation

Step 1: Inventory Applications Requesting the Scope

  1. Navigate to: IAM Identity CenterApplicationsCustomer managed
  2. For each application, review whether account access is enabled and which scopes it requests
  3. Record the business justification for each application holding sso:account:access

Step 2: Apply an Approval Gate

  1. Require documented security review before enabling account access for any customer managed application — there is no narrower alternative to fall back on
  2. Restrict who can perform this configuration to a small named group in the management or delegated administrator account
  3. Remove the scope from any application that cannot justify organization-wide account reach

Step 3: Enforce Token-Handling Requirements

  1. Store tokens server-side only — never in browser local storage, session storage, or anywhere client-accessible
  2. Never write tokens to application logs, error messages, or telemetry
  3. Pass the token in the x-amz-sso_bearer_token header, never as a URL query parameter (URLs land in proxy logs, referrer headers, and browser history)
  4. Monitor CloudTrail for the associated authorization and account-access events (see 4.1)

Validation & Testing

  1. Confirm the application inventory lists a reviewed justification for every holder of the scope
  2. Grep application logs and log-aggregation indexes for bearer-token patterns to confirm none are being written
  3. Verify tokens are transmitted only in the documented header by inspecting the integration’s outbound requests
  4. Confirm CloudTrail records the account-access events and that they are alertable

Compliance Mappings

Framework Control ID Control Description
SOC 2 CC6.1 Logical access restricted to authorized applications
SOC 2 CC6.3 Access grants are reviewed and justified
NIST 800-53 AC-6 Least privilege for application access grants
NIST 800-53 SC-28 Protection of credentials at rest and in transit
CIS Controls v8 5.4 Restrict administrator privileges to dedicated accounts

Source: Enable account access for customer managed applications


4. Monitoring & Compliance

4.1 Configure CloudTrail Logging

Profile Level: L1 (Crawl)

Framework Control
CIS Controls 8.2
NIST 800-53 AU-2

Description

Enable CloudTrail for IAM Identity Center events.

Rationale

Why This Matters:

  • CloudTrail records authentication events, permission changes, and account assignments, providing the audit trail needed to detect and investigate misuse
  • Without comprehensive logging, attacker activity such as new admin assignments or anomalous sign-ins goes undetected
  • Retained logs support forensic reconstruction after an incident and meet compliance evidence requirements

Attack Prevented: Undetected privilege changes, unnoticed unauthorized access, delayed breach detection, audit gaps

ClickOps Implementation

Step 1: Verify CloudTrail

  1. Ensure organization trail enabled
  2. Verify IAM Identity Center events captured
  3. Configure log retention

Step 2: Monitor Key Events

  1. Authentication events
  2. Permission changes
  3. Account assignments

Code Pack: Terraform
hth-aws-iam-identity-center-4.01-configure-cloudtrail-logging.tf View source on GitHub ↗
# S3 bucket for CloudTrail logs with encryption and lifecycle
resource "aws_s3_bucket" "cloudtrail_logs" {
  bucket        = var.cloudtrail_bucket_name
  force_destroy = false

  tags = {
    ManagedBy = "how-to-harden"
    Control   = "4.1-configure-cloudtrail-logging"
  }
}

resource "aws_s3_bucket_versioning" "cloudtrail_logs" {
  bucket = aws_s3_bucket.cloudtrail_logs.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "cloudtrail_logs" {
  bucket = aws_s3_bucket.cloudtrail_logs.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = var.cloudtrail_kms_key_id
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_public_access_block" "cloudtrail_logs" {
  bucket                  = aws_s3_bucket.cloudtrail_logs.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail_logs" {
  bucket = aws_s3_bucket.cloudtrail_logs.id
  rule {
    id     = "archive-old-logs"
    status = "Enabled"
    transition {
      days          = 90
      storage_class = "GLACIER"
    }
    expiration {
      days = 365
    }
  }
}

# S3 bucket policy allowing CloudTrail to write
data "aws_caller_identity" "current" {}

resource "aws_s3_bucket_policy" "cloudtrail_logs" {
  bucket = aws_s3_bucket.cloudtrail_logs.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AWSCloudTrailAclCheck"
        Effect = "Allow"
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        }
        Action   = "s3:GetBucketAcl"
        Resource = aws_s3_bucket.cloudtrail_logs.arn
      },
      {
        Sid    = "AWSCloudTrailWrite"
        Effect = "Allow"
        Principal = {
          Service = "cloudtrail.amazonaws.com"
        }
        Action   = "s3:PutObject"
        Resource = "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/*"
        Condition = {
          StringEquals = {
            "s3:x-amz-acl" = "bucket-owner-full-control"
          }
        }
      }
    ]
  })
}

# Organization-level CloudTrail capturing SSO management events
resource "aws_cloudtrail" "sso_audit" {
  name                          = "hth-sso-audit-trail"
  s3_bucket_name                = aws_s3_bucket.cloudtrail_logs.id
  is_organization_trail         = var.is_organization_trail
  is_multi_region_trail         = true
  include_global_service_events = true
  enable_log_file_validation    = true
  kms_key_id                    = var.cloudtrail_kms_key_id

  event_selector {
    read_write_type           = "All"
    include_management_events = true
  }

  tags = {
    ManagedBy = "how-to-harden"
    Control   = "4.1-configure-cloudtrail-logging"
  }
}
Code Pack: API Script
hth-aws-iam-identity-center-4.01-configure-cloudtrail-logging.sh View source on GitHub ↗
# Verify at least one CloudTrail trail is logging management events
info "4.1 Listing CloudTrail trails..."
TRAILS=$(aws_json cloudtrail describe-trails) || {
  fail "4.1 Failed to describe CloudTrail trails"
  increment_failed
  summary
  exit 0
}

TRAIL_COUNT=$(echo "${TRAILS}" | jq '.trailList | length' 2>/dev/null || echo "0")

if [ "${TRAIL_COUNT}" -eq 0 ]; then
  fail "4.1 No CloudTrail trails found -- SSO events will not be logged (AU-2)"
  increment_failed
  summary
  exit 0
fi

LOGGING_TRAIL_COUNT=0
MGMT_EVENT_TRAIL_COUNT=0

for TRAIL_ARN in $(echo "${TRAILS}" | jq -r '.trailList[].TrailARN' 2>/dev/null); do
  TRAIL_NAME=$(echo "${TRAILS}" | jq -r --arg arn "${TRAIL_ARN}" '.trailList[] | select(.TrailARN == $arn) | .Name')

  # Check if trail is actually logging
  STATUS=$(aws_json cloudtrail get-trail-status --name "${TRAIL_ARN}" 2>/dev/null) || continue
  IS_LOGGING=$(echo "${STATUS}" | jq -r '.IsLogging' 2>/dev/null || echo "false")

  if [ "${IS_LOGGING}" = "true" ]; then
    LOGGING_TRAIL_COUNT=$((LOGGING_TRAIL_COUNT + 1))
  else
    warn "4.1 Trail '${TRAIL_NAME}' exists but is NOT logging"
    continue
  fi

  # Check if trail captures management events (which include SSO events)
  EVENT_SELECTORS=$(aws_json cloudtrail get-event-selectors --trail-name "${TRAIL_ARN}" 2>/dev/null) || continue

  # Check both basic and advanced event selectors
  HAS_MGMT=$(echo "${EVENT_SELECTORS}" | jq '
    (.EventSelectors // [] | any(.IncludeManagementEvents == true)) or
    (.AdvancedEventSelectors // [] | any(.FieldSelectors[] |
      select(.Field == "eventCategory") | .Equals[] == "Management"))
  ' 2>/dev/null || echo "false")

  if [ "${HAS_MGMT}" = "true" ]; then
    pass "4.1 Trail '${TRAIL_NAME}' is logging management events (includes SSO)"
    MGMT_EVENT_TRAIL_COUNT=$((MGMT_EVENT_TRAIL_COUNT + 1))
  else
    warn "4.1 Trail '${TRAIL_NAME}' does not capture management events"
  fi
done
# Verify recent SSO events are present in CloudTrail
info "4.1 Checking for recent SSO events in CloudTrail..."
SSO_EVENTS=$(aws_json cloudtrail lookup-events \
  --lookup-attributes "AttributeKey=EventSource,AttributeValue=sso.amazonaws.com" \
  --max-results 5 2>/dev/null) || {
  warn "4.1 Cannot query CloudTrail events -- verify cloudtrail:LookupEvents permission"
  summary
  exit 0
}

EVENT_COUNT=$(echo "${SSO_EVENTS}" | jq '.Events | length' 2>/dev/null || echo "0")

if [ "${EVENT_COUNT}" -gt 0 ]; then
  pass "4.1 Found ${EVENT_COUNT} recent SSO events in CloudTrail"
  echo "${SSO_EVENTS}" | jq -r '.Events[] | "  - \(.EventName) by \(.Username // "unknown") at \(.EventTime)"' 2>/dev/null || true
else
  warn "4.1 No recent SSO events found -- this may indicate a new deployment or logging gap"
fi
Code Pack: Sigma Detection Rule
hth-aws-iam-identity-center-4.01-cloudtrail-sso-events.yml View source on GitHub ↗
detection:
    selection_idp_changes:
        eventSource: sso.amazonaws.com
        eventName:
            - 'AssociateDirectory'
            - 'DisassociateDirectory'
            - 'CreateInstanceAccessControlAttributeConfiguration'
            - 'UpdateInstanceAccessControlAttributeConfiguration'
            - 'DeleteInstanceAccessControlAttributeConfiguration'
    selection_mfa_changes:
        eventSource: sso.amazonaws.com
        eventName:
            - 'RegisterMfaDevice'
            - 'DeregisterMfaDevice'
            - 'UpdateMfaDevice'
    selection_instance_changes:
        eventSource: sso.amazonaws.com
        eventName:
            - 'CreateInstance'
            - 'DeleteInstance'
            - 'UpdateInstance'
    condition: selection_idp_changes or selection_mfa_changes or selection_instance_changes
fields:
    - eventTime
    - eventName
    - userIdentity.arn
    - userIdentity.principalId
    - sourceIPAddress
    - errorCode
    - errorMessage

4.2 Configure Access Analyzer

Profile Level: L2 (Walk)

Framework Control
CIS Controls 5.4
NIST 800-53 AC-6

Description

Use IAM Access Analyzer for policy validation.

Rationale

Why This Matters:

  • Access Analyzer surfaces policies that grant unintended external or cross-account access before an attacker finds them
  • Automated policy validation catches over-permissive grants and misconfigurations that manual review routinely misses
  • Continuous findings let teams remediate excessive access proactively rather than after an incident

Attack Prevented: Unintended external access, cross-account exposure, policy misconfiguration, excessive permissions

ClickOps Implementation

Step 1: Enable Access Analyzer

  1. Create analyzer for organization
  2. Review findings
  3. Remediate external access

Code Pack: Terraform
hth-aws-iam-identity-center-4.02-configure-access-analyzer.tf View source on GitHub ↗
# Organization-level IAM Access Analyzer for cross-account visibility
resource "aws_accessanalyzer_analyzer" "organization" {
  analyzer_name = "hth-org-access-analyzer"
  type          = var.analyzer_type

  tags = {
    ManagedBy = "how-to-harden"
    Control   = "4.2-configure-access-analyzer"
  }
}

# Archive rule to auto-suppress known trusted cross-account access
resource "aws_accessanalyzer_archive_rule" "trusted_org_accounts" {
  analyzer_name = aws_accessanalyzer_analyzer.organization.analyzer_name
  rule_name     = "trusted-organization-accounts"

  filter {
    criteria = "isPublic"
    eq       = ["false"]
  }

  filter {
    criteria = "resourceType"
    eq       = ["AWS::IAM::Role"]
  }
}

# CloudWatch alarm for new Access Analyzer findings
resource "aws_cloudwatch_metric_alarm" "access_analyzer_findings" {
  alarm_name          = "hth-access-analyzer-new-findings"
  alarm_description   = "Alert on new IAM Access Analyzer findings (AC-6)"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "ActiveFindings"
  namespace           = "AccessAnalyzer"
  period              = 300
  statistic           = "Maximum"
  threshold           = 0
  treat_missing_data  = "notBreaching"

  dimensions = {
    AnalyzerName = aws_accessanalyzer_analyzer.organization.analyzer_name
  }

  alarm_actions = var.alarm_sns_topic_arns

  tags = {
    ManagedBy = "how-to-harden"
    Control   = "4.2-configure-access-analyzer"
  }
}
Code Pack: API Script
hth-aws-iam-identity-center-4.02-configure-access-analyzer.sh View source on GitHub ↗
# Check for existing IAM Access Analyzer instances
info "4.2 Listing Access Analyzer instances..."
ANALYZERS=$(aws_json accessanalyzer list-analyzers 2>/dev/null) || {
  fail "4.2 Failed to list analyzers -- verify accessanalyzer:ListAnalyzers permission"
  increment_failed
  summary
  exit 0
}

ANALYZER_COUNT=$(echo "${ANALYZERS}" | jq '.analyzers | length' 2>/dev/null || echo "0")
ACTIVE_COUNT=0
ORG_ANALYZER=false

for i in $(seq 0 $((ANALYZER_COUNT - 1))); do
  ANALYZER=$(echo "${ANALYZERS}" | jq ".analyzers[${i}]")
  NAME=$(echo "${ANALYZER}" | jq -r '.name')
  STATUS=$(echo "${ANALYZER}" | jq -r '.status')
  TYPE=$(echo "${ANALYZER}" | jq -r '.type')

  if [ "${STATUS}" = "ACTIVE" ]; then
    ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
    info "4.2   Active analyzer: ${NAME} (type: ${TYPE})"
    if [ "${TYPE}" = "ORGANIZATION" ]; then
      ORG_ANALYZER=true
    fi
  else
    warn "4.2   Analyzer '${NAME}' is ${STATUS} -- should be ACTIVE"
  fi
done
# Create an organization-level Access Analyzer
info "4.2 Creating organization-level Access Analyzer..."
CREATE_RESULT=$(aws_json accessanalyzer create-analyzer \
  --analyzer-name "hth-org-access-analyzer" \
  --type ORGANIZATION 2>/dev/null) || {
  # Fall back to account-level if org-level fails (not org management account)
  warn "4.2 Organization-level analyzer failed -- trying account-level..."
  CREATE_RESULT=$(aws_json accessanalyzer create-analyzer \
    --analyzer-name "hth-account-access-analyzer" \
    --type ACCOUNT 2>/dev/null) || {
    fail "4.2 Failed to create Access Analyzer"
    increment_failed
    summary
    exit 0
  }
}

NEW_ARN=$(echo "${CREATE_RESULT}" | jq -r '.arn // empty' 2>/dev/null || true)
if [ -n "${NEW_ARN}" ]; then
  pass "4.2 Created Access Analyzer: ${NEW_ARN}"
  increment_applied
else
  fail "4.2 Analyzer creation returned empty ARN"
  increment_failed
fi
# Review active Access Analyzer findings
info "4.2 Checking for active findings..."
for i in $(seq 0 $((ANALYZER_COUNT - 1))); do
  ANALYZER=$(echo "${ANALYZERS}" | jq ".analyzers[${i}]")
  NAME=$(echo "${ANALYZER}" | jq -r '.name')
  ARN=$(echo "${ANALYZER}" | jq -r '.arn')
  STATUS=$(echo "${ANALYZER}" | jq -r '.status')

  [ "${STATUS}" != "ACTIVE" ] && continue

  FINDINGS=$(aws_json accessanalyzer list-findings \
    --analyzer-arn "${ARN}" \
    --filter '{"status": {"eq": ["ACTIVE"]}}' 2>/dev/null) || continue

  FINDING_COUNT=$(echo "${FINDINGS}" | jq '.findings | length' 2>/dev/null || echo "0")

  if [ "${FINDING_COUNT}" -gt 0 ]; then
    warn "4.2 Analyzer '${NAME}' has ${FINDING_COUNT} active finding(s) -- review for over-permissive access"
  else
    pass "4.2 Analyzer '${NAME}' has no active findings"
  fi
done

4.3 Use a Customer Managed KMS Key for Identity Center Data

Profile Level: L3 (Run)

Framework Control
CIS Controls 3.11
NIST 800-53 SC-12, SC-13, SC-28

Description

Encrypt IAM Identity Center data with a customer managed AWS KMS key (CMK) instead of the AWS-owned key used by default. Released 2025-09-17, this gives you key-level audit trails and revocation — and, in exchange, a hard operational dependency you must plan for.

Rationale

Why This Matters:

  • The default is an AWS-owned key, which produces no key policy you control and no key usage record in your own account
  • A CMK gives you CloudTrail visibility into every decrypt operation against Identity Center data, plus a key policy you can scope and a revocation path you own
  • Regulated environments frequently require customer-controlled key material and demonstrable key lifecycle governance for identity data
  • The trade-off is real and must be accepted knowingly: the key becomes a single point of failure for authentication itself

Lockout warning — read before enabling. AWS states that if the KMS key is deleted, or disabled, or its permissions are altered such that IAM Identity Center can no longer use it, administrators and users are locked out of IAM Identity Center. This is not a degraded mode; it is loss of access to the service that fronts every AWS account. Pair this control with an explicit key-deletion restriction and a documented break-glass path before enabling it.

Attack Prevented: Unaudited access to identity data at rest, inability to revoke access to encrypted data, non-compliance with customer-managed-key requirements

Prerequisites

  • The key must be a symmetric KMS key
  • The key must be in the same Region as the IAM Identity Center instance
  • The key must be created in the management account
  • If you plan to operate multi-Region, the key must be a multi-Region key — a single-Region key cannot be converted to multi-Region later, so this decision is effectively permanent
  • Verify application compatibility: not all managed applications support customer managed keys

ClickOps Implementation

Step 1: Create the Key

  1. In the management account, in the Identity Center Region, navigate to: KMSCustomer managed keysCreate key
  2. Select Symmetric, and select Multi-Region key if multi-Region operation is plausible — this cannot be changed afterward
  3. Author a key policy granting IAM Identity Center the operations it requires and no more

Step 2: Protect the Key Before You Depend On It

  1. Attach an SCP or key policy statement denying kms:ScheduleKeyDeletion and kms:DisableKey on this key to everyone except a narrowly scoped break-glass principal
  2. Enable key rotation and confirm CloudTrail is recording KMS events for the key
  3. Alert on any disable, deletion-schedule, or policy-change event against the key at critical severity

Step 3: Document the Break-Glass Path

  1. Write down exactly what happens and who does what if the key becomes unusable — administrators will not be able to log in to fix it through Identity Center
  2. Identify an out-of-band administrative path into the management account that does not depend on IAM Identity Center
  3. Test that path before enabling the CMK, not after

Step 4: Enable the Key on Identity Center

  1. Navigate to: IAM Identity CenterSettings
  2. Configure the encryption setting to use your customer managed key
  3. Validate that managed applications in use still authenticate correctly — compatibility is not universal

Validation & Testing

  1. Confirm CloudTrail shows Identity Center decrypt operations against your key, proving the CMK is in use
  2. Confirm the deletion/disable restriction is enforced by attempting the action with a non-break-glass principal
  3. Re-test each managed application after enabling and after any key policy change
  4. Rehearse the break-glass path on a schedule

Compliance Mappings

Framework Control ID Control Description
SOC 2 CC6.1 Logical access to encrypted identity data is controlled
NIST 800-53 SC-12 Cryptographic key establishment and management
NIST 800-53 SC-13 Use of validated cryptographic mechanisms
NIST 800-53 SC-28 Protection of information at rest
CIS Controls v8 3.11 Encrypt sensitive data at rest

Source: Customer managed keys for IAM Identity Center


5. Compliance Quick Reference

SOC 2 Trust Services Criteria Mapping

Control ID IAM Identity Center Control Guide Section
CC6.1 MFA enforcement 1.1
CC6.2 Permission sets 3.1
CC6.6 Session duration 1.2
CC7.2 CloudTrail logging 4.1

NIST 800-53 Rev 5 Mapping

Control IAM Identity Center Control Guide Section
IA-2(1) MFA 1.1
IA-8 External IdP 2.1
AC-2 SCIM provisioning 2.2
AC-6 Permission sets 3.1
AC-6 sso:account:access scope governance 3.4
CM-7 Account-instance restriction 2.3
SC-12, SC-28 Customer managed KMS key 4.3
AU-2 CloudTrail 4.1

Appendix A: References

Official AWS Documentation:

API & Developer Tools:

Compliance Frameworks:

Security Incidents:

  • 2025 — IAM Eventual Consistency Exploitation Research: Researchers disclosed that AWS IAM’s eventual consistency model creates a 3-4 second window where deleted access keys remain functional, enabling persistence techniques. AWS applied development fixes and documentation updates in April 2025. (GBHackers Report)
  • November 2025 — Compromised IAM Credentials Mining Campaign: Attackers used compromised IAM user credentials with admin-like privileges to conduct large-scale crypto mining across EC2 instances, detected by GuardDuty. (The Hacker News Report)
  • Note: These are AWS-wide IAM incidents, not specific to IAM Identity Center itself. AWS manages infrastructure patching for Identity Center as a managed service.

Changelog

Date Version Maturity Changes Author
2026-08-08 0.2.0 draft Currency pass. Corrections: 1.1 now states that native MFA configuration is not supported for external identity providers, resolving the contradiction between 1.1 and 2.1 — with an external IdP, MFA must be enforced at the IdP; added the MFA-on-by-default note for directory users and the 8-device registration ceiling; 2.1 gained the matching cross-reference. Rewrote 1.2 around the real three-session model (interactive up to 90 days, permission-set role sessions up to 12 hours and independent of user revocation, application sessions ~1h refresh ending ~30min after the interactive session), including the 12-hour post-deletion exposure window, the Kiro 90-day extension, and the absence of SAML Single Logout in either direction. New controls: 2.3 restrict member-account instances of IAM Identity Center via SCP (enabled by default for orgs enabled after 2023-11-15) with the August 2026 enablement-option changes (Single/Multi-Region/Custom; Multi-Region auto-creates a multi-Region CMK with KMS charges; multi-account permissions now an optional capability enabled by default); 3.4 govern the sso:account:access scope on customer managed applications (2026-06-19, grants all accounts and roles of the authenticated user, non-restrictable, management/delegated-admin only) with token-handling requirements; 4.3 customer managed KMS key (2025-09-17) with its requirements, the administrator/user lockout warning, and break-glass planning. Removed the CIS AWS Foundations Benchmark from this vendor’s hardening_docs — v5.0.0 covers IAM users, root, and access keys, not IAM Identity Center. Tier 3/4 not surveyed this pass. Claude Code (Opus 5)
2026-06-29 0.1.1 draft Add cheat-sheet Description and Rationale for all controls Claude Code (Opus 4.8)
2025-02-05 0.1.0 draft Initial guide with MFA, permission sets, and monitoring Claude Code (Opus 4.5)

Contributing

Found an issue or want to improve this guide?