IAM & Access Control
TSC mapping: CC5 (Control Activities), CC6.1 (Logical Access Security), CC6.2 (Access Provisioning), CC6.3 (Access Removal)
IAM is the highest-evidence area for SOC 2 auditors. Auditors will pull IAM Credential Reports, review permission policies, verify MFA enforcement, and check access key ages. Get this layer right before anything else.
1. Root Account Hardening
The root account has unrestricted access to everything in the account. It must be locked down and effectively never used.
Enable MFA on root:
# Verify root MFA status
aws iam get-account-summary --query 'SummaryMap.AccountMFAEnabled'
# 1 = enabled, 0 = not enabled
Remove root access keys (required):
# Check if root has active access keys
aws iam get-account-summary --query 'SummaryMap.AccountAccessKeysPresent'
# Must be 0 — delete any root keys immediately via the console
AWS Config rules to enable:
| Rule | What it checks |
|---|---|
root-account-mfa-enabled | Root MFA is active |
iam-root-access-key-check | No active root access keys exist |
Reference: IAM best practices — root user →
2. MFA for All IAM Users
Every IAM user with AWS Console access must have MFA enabled. For programmatic-only users, enforce through policy.
Enforce MFA via an IAM policy condition (attach to all users/groups):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyAllExceptMFASetup",
"Effect": "Deny",
"NotAction": [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:ResyncMFADevice",
"sts:GetSessionToken"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Audit current MFA status:
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F, '{print $1, $4, $8}' | column -t
# Columns: user, password_enabled, mfa_active
AWS Config rules:
| Rule | What it checks |
|---|---|
iam-user-mfa-enabled | All IAM users with passwords have MFA |
mfa-enabled-for-iam-console-access | Console-access users require MFA |
Reference: Enabling MFA devices →
3. Password Policy
aws iam update-account-password-policy \
--minimum-password-length 14 \
--require-symbols \
--require-numbers \
--require-uppercase-characters \
--require-lowercase-characters \
--allow-users-to-change-password \
--max-password-age 90 \
--password-reuse-prevention 24 \
--hard-expiry
Reference: IAM password policy →
4. IAM Identity Center (Preferred for Human Access)
For SOC 2, the preferred pattern is zero long-lived IAM users for engineers. Use IAM Identity Center (formerly AWS SSO) to federate access from your identity provider (Okta, Azure AD, Google Workspace) and issue short-lived credentials via role assumption.
Why this satisfies CC6.2 and CC6.3 better than IAM users:
- Provisioning and deprovisioning is automated via SCIM from your IdP.
- Access is time-limited — sessions expire, no permanent credentials.
- Multi-account access is centrally managed through Permission Sets.
Enable via AWS Organizations:
# Enable IAM Identity Center (done once per org)
aws sso-admin list-instances
# If empty, enable via the console: IAM Identity Center → Enable
Recommended Permission Sets for segregation of duties:
| Permission Set | Managed Policy | Who gets it |
|---|---|---|
ReadOnlyAccess | ReadOnlyAccess | All engineers (default) |
DeveloperAccess | Custom — allow dev/staging, deny prod | Developers |
OpsAccess | Custom — allow ops actions, no IAM | SRE/Ops |
SecurityAuditor | SecurityAudit | Security team |
AdminAccess | AdministratorAccess | Break-glass only, MFA required |
Reference: IAM Identity Center → · SCIM provisioning →
5. IAM Access Key Management
Long-lived IAM access keys are a common audit finding. Keep them to a minimum and rotate them automatically.
Audit key age:
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d | \
awk -F, 'NR>1 {print $1, $9, $11, $14}' | column -t
# Columns: user, access_key_1_active, access_key_1_last_rotated, access_key_2_active
Disable keys older than 90 days:
aws iam update-access-key \
--access-key-id AKIAIOSFODNN7EXAMPLE \
--status Inactive \
--user-name <username>
AWS Config rules:
| Rule | What it checks |
|---|---|
access-keys-rotated | Keys rotated within 90 days (configurable) |
iam-user-unused-credentials-check | Credentials not used in 90 days |
iam-no-inline-policy-check | No inline policies on users, groups, or roles |
Reference: Access key best practices → · Config rule: access-keys-rotated →
6. Least Privilege — IAM Policy Authoring
Auditors will sample IAM policies. Overly permissive policies (Action: "*", Resource: "*") are the most common SOC 2 finding.
Principles:
- Use AWS managed policies only as a starting point — scope down with customer-managed policies.
- Use
ResourceARNs, not*, wherever possible. - Use
Conditionblocks to restrict by MFA, IP, VPC, or time. - Use permission boundaries to cap the maximum permissions a role can grant.
Example — scoped S3 policy for an application role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-app-bucket-prod/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-app-bucket-prod"
}
]
}
Use IAM Access Analyzer to surface overly permissive policies:
# Create an analyzer for the account
aws accessanalyzer create-analyzer \
--analyzer-name soc2-analyzer \
--type ACCOUNT
# List active findings (external access)
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:<region>:<account>:analyzer/soc2-analyzer \
--filter '{"status": {"eq": ["ACTIVE"]}}'
Reference: IAM Access Analyzer → · Permission boundaries →
7. Quarterly Access Reviews
SOC 2 CC6.3 requires evidence that access is removed when no longer needed. The standard way to evidence this is a documented quarterly access review.
Generate review data:
# Export IAM credential report (shows all users, key ages, last activity)
aws iam generate-credential-report && sleep 5
aws iam get-credential-report --query 'Content' --output text | base64 -d > iam-review-$(date +%Y-%m-%d).csv
# List all roles and their last used date
aws iam list-roles --query 'Roles[*].[RoleName,RoleLastUsed.LastUsedDate]' --output table
# List all policies attached to a user
aws iam list-attached-user-policies --user-name <username>
Review process:
- Export credential report and role last-used data.
- Flag any user not active in 90+ days and any role not assumed in 180+ days.
- Send to team leads for confirmation — did person/role still need access?
- Remove or disable access for any entry team leads cannot justify.
- Document the review outcome and retain for audit evidence.
SOC 2 Evidence Checklist for IAM
| Evidence item | How to export |
|---|---|
| IAM Credential Report | aws iam get-credential-report |
| Root MFA status | aws iam get-account-summary |
| Password policy | aws iam get-account-password-policy |
| IAM Identity Center assignments | Exported from Identity Center console |
| AWS Config IAM rule compliance history | AWS Config console → Rules → Compliance timeline |
| IAM Access Analyzer findings | aws accessanalyzer list-findings |
| Quarterly access review records | Retained internally (spreadsheet, ticketing system) |
Official references: