AWS IAM Identity Center Hardening Guide
AWS identity management hardening for IAM Identity Center including MFA, permission sets, and account access
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
- Authentication & MFA
- Identity Source Configuration
- Permission Management
- Monitoring & Compliance
- 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
- Navigate to: IAM Identity Center → Settings → Identity source
- If the source is Identity Center directory, AWS Managed Microsoft AD, or AD Connector, continue with Steps 1-3 below
- 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
- Navigate to: IAM Identity Center → Settings → Authentication
- 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
- Select Require MFA
- Configure enforcement:
- Every sign-in (recommended)
- Context-aware
- Save changes
Step 3: Configure MFA Types
- Enable authenticator apps
- Enable hardware TOTP devices
- Enable FIDO2 security keys (recommended)
- Disable SMS if possible
- 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
- Navigate to: Settings → Authentication
- Read the current session duration value before changing it — it may already be set to a long duration
- 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
- Edit each permission set
- Set session duration — the maximum is 12 hours
- 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
- Record that user deletion alone leaves account sessions live for up to their configured duration
- Define the incident step that actually cuts access — terminating the active sessions and/or attaching a deny policy to the affected role
- 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
- Navigate to: Settings → Attributes for access control
- Enable attributes
- Configure attribute mappings
Step 2: Use in Permission Sets
- Create ABAC-aware policies
- Reference user attributes
- Implement tag-based access
Code Pack: Terraform
# 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
- Navigate to: Settings → Identity source
- Click Change identity source
- Select external identity provider
Step 2: Configure SAML/SCIM
- Configure SAML 2.0 settings
- Enable automatic provisioning (SCIM)
- Configure attribute mappings
Step 3: Test and Migrate
- Test authentication
- Migrate users from Identity Center directory
- 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
- Navigate to: Settings → Identity source
- Enable automatic provisioning
- Generate SCIM endpoint and token
Step 2: Configure IdP
- Configure SCIM in identity provider
- Map user attributes
- 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
- Determine whether any member account has already created an account instance of IAM Identity Center
- For each one found, identify its owner, its identity source, and the applications assigned through it
- Plan migration of any legitimate use onto the organization instance before restricting creation
Step 2: Restrict Creation via Service Control Policy
- In the management account, navigate to: AWS Organizations → Policies → Service control policies
- Create or edit an SCP that denies the Identity Center instance-creation action for member accounts
- 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
- From a member account, attempt to create an account instance and confirm it is denied
- Record the SCP as the compensating control in your access-management documentation
Validation & Testing
- Re-run the account-instance inventory and confirm no new instances appear
- Verify the SCP is attached to every relevant OU, including newly created ones
- 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
- Navigate to: Permission sets
- Review existing permission sets
- Identify overly permissive sets
Step 2: Create Least-Privilege Sets
- Create custom permission sets
- Use AWS managed policies where possible
- Apply inline policies for restrictions
Step 3: Configure Permissions Boundary
- Apply permissions boundaries
- Limit maximum permissions
- Prevent privilege escalation
Code Pack: Terraform
# 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
# 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
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
- Navigate to: AWS accounts
- Review current assignments
- Identify unnecessary access
Step 2: Apply Least Privilege
- Assign minimum required accounts
- Use groups for assignments
- Regular access reviews
Code Pack: API Script
# 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
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
- Create separate admin permission sets
- Apply shorter session duration
- Require MFA for every session
Step 2: Limit Admin Assignments
- Restrict admin access to required users
- Use groups for admin access
- 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
- Navigate to: IAM Identity Center → Applications → Customer managed
- For each application, review whether account access is enabled and which scopes it requests
- Record the business justification for each application holding
sso:account:access
Step 2: Apply an Approval Gate
- Require documented security review before enabling account access for any customer managed application — there is no narrower alternative to fall back on
- Restrict who can perform this configuration to a small named group in the management or delegated administrator account
- Remove the scope from any application that cannot justify organization-wide account reach
Step 3: Enforce Token-Handling Requirements
- Store tokens server-side only — never in browser local storage, session storage, or anywhere client-accessible
- Never write tokens to application logs, error messages, or telemetry
- Pass the token in the
x-amz-sso_bearer_tokenheader, never as a URL query parameter (URLs land in proxy logs, referrer headers, and browser history) - Monitor CloudTrail for the associated authorization and account-access events (see 4.1)
Validation & Testing
- Confirm the application inventory lists a reviewed justification for every holder of the scope
- Grep application logs and log-aggregation indexes for bearer-token patterns to confirm none are being written
- Verify tokens are transmitted only in the documented header by inspecting the integration’s outbound requests
- 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
- Ensure organization trail enabled
- Verify IAM Identity Center events captured
- Configure log retention
Step 2: Monitor Key Events
- Authentication events
- Permission changes
- Account assignments
Code Pack: Terraform
# 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
# 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
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
- Create analyzer for organization
- Review findings
- Remediate external access
Code Pack: Terraform
# 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
# 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
- In the management account, in the Identity Center Region, navigate to: KMS → Customer managed keys → Create key
- Select Symmetric, and select Multi-Region key if multi-Region operation is plausible — this cannot be changed afterward
- 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
- Attach an SCP or key policy statement denying
kms:ScheduleKeyDeletionandkms:DisableKeyon this key to everyone except a narrowly scoped break-glass principal - Enable key rotation and confirm CloudTrail is recording KMS events for the key
- Alert on any disable, deletion-schedule, or policy-change event against the key at critical severity
Step 3: Document the Break-Glass Path
- 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
- Identify an out-of-band administrative path into the management account that does not depend on IAM Identity Center
- Test that path before enabling the CMK, not after
Step 4: Enable the Key on Identity Center
- Navigate to: IAM Identity Center → Settings
- Configure the encryption setting to use your customer managed key
- Validate that managed applications in use still authenticate correctly — compatibility is not universal
Validation & Testing
- Confirm CloudTrail shows Identity Center decrypt operations against your key, proving the CMK is in use
- Confirm the deletion/disable restriction is enforced by attempting the action with a non-break-glass principal
- Re-test each managed application after enabling and after any key policy change
- 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:
- IAM Identity Center User Guide
- IAM Identity Center Security
- Authentication concepts (session types and durations)
- Enable MFA · Configure MFA
- Enable IAM Identity Center (instance types and account instances)
- Enable account access for customer managed applications
- Customer managed keys for IAM Identity Center
- IAM Identity Center document history
- Security Best Practices in IAM
- Security Reference Architecture - IAM Identity Center
- AWS IAM Best Practices
API & Developer Tools:
- IAM Identity Center API Reference
- AWS CLI - SSO Admin Commands
- AWS SDKs (Boto3, JavaScript, Go, Java, .NET, etc.)
- AWS CloudFormation IAM Identity Center Resources
- GitHub Organization (aws)
Compliance Frameworks:
- SOC 1/2/3 Type II (12-month audit periods) — via AWS Artifact
- ISO/IEC 27001:2022, ISO 27017:2015, ISO 27018:2019 — via AWS ISO Certified
- FedRAMP (High, Moderate baselines) — via AWS Compliance Programs
- PCI DSS Level 1, HIPAA, HITRUST, DoD SRG, CSA STAR
- Full Compliance Programs List
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?
- Report outdated information: Open an issue with tag
content-outdated - Propose new controls: Open an issue with tag
new-control - Submit improvements: See Contributing Guide