AWS IAM Patterns That Survive a Security Review
Least-privilege isn't a slogan. The IAM patterns we apply on every production AWS account, the anti-patterns we rip out, and policies that pass audit.
IAM is one of the few things in AWS where “good enough for now” eventually costs you something — either in an incident, an audit failure, or a 2am page about a compromised credential. We’ve shipped AWS platforms across regulated industries (hospital management, banking automation, government workloads) and seen the IAM patterns that hold up under scrutiny and the ones that quietly create risk.
Here’s how we structure IAM on every production AWS account we deploy.
The shape that holds up#
Every production AWS account we ship has the same IAM shape:
- Identity Center (SSO) for humans. Never IAM users for people.
- IAM Roles for workloads. Never long-lived access keys for services.
- AssumeRole for cross-account access. Service Control Policies + permissions boundaries to keep the blast radius small.
- OIDC for CI/CD. GitHub Actions / GitLab CI assume roles via OIDC — no long-lived access keys in CI secrets.
- One account per environment per workload. Prod isolated from dev isolated from sandbox. AWS Organizations + SCPs enforce.
- CloudTrail + Config + GuardDuty + IAM Access Analyzer everywhere. Set once at org level.
This isn’t novel. It’s the AWS Well-Architected Security pillar applied with rigor. Where teams go wrong is skipping pieces because “we’ll add it later.”
Pattern 1: SSO + permission sets for humans#
Identity Center (formerly AWS SSO) is how humans access AWS. They log in to your identity provider (Okta, Azure AD, Google, etc.), Identity Center assumes the appropriate roles in target accounts.
The permission sets we typically define:
- AdministratorAccess — break-glass only. Mostly empty most of the time.
- PowerUserAccess — full access except IAM. Engineers who need to deploy infra.
- ReadOnlyAccess — for support, observability, on-call eyes-only access.
- BillingViewer — finance team.
- SecurityAuditor — security team can read everything including IAM config but cannot change it.
Each permission set is assigned per AWS account, with session duration that matches risk (production = 1 hour, sandbox = 8 hours).
What we never do: create IAM users for humans. Once an IAM user exists, you have to manage its lifecycle, password, MFA, access key rotation, and offboarding. SSO solves all of that via the identity provider.
Pattern 2: IAM Roles for every workload#
Every workload (EC2 instance, Lambda function, ECS task, EKS pod) has an IAM role attached. The role has only the permissions the workload needs.
- EC2: Instance profile with role attached.
- Lambda: Execution role attached at function creation.
- ECS: Task role + execution role (different concerns — execution role pulls images and writes logs; task role is what your container code sees).
- EKS: IAM Roles for Service Accounts (IRSA) — pods authenticate to AWS via their Kubernetes service account, mapped to an IAM role via OIDC. Or use EKS Pod Identity (newer, simpler).
What we never do: bake AWS access keys into application config, env vars, or container images. If you find them in git grep AWS_ACCESS_KEY_ID, you have an incident.
Pattern 3: Least privilege via IAM Access Analyzer#
“Least privilege” as a slogan is useless. The hard part is figuring out what permissions are actually needed.
The pattern that works:
- Start permissive in dev. New role gets a broad policy (e.g.,
AmazonS3FullAccess). - Run the workload for 1-2 weeks while AWS Access Analyzer (or CloudTrail event analysis) records what API calls are actually made.
- Use Access Analyzer’s “policy generation” to derive a least-privilege policy from observed activity.
- Apply the derived policy in staging, run another cycle, then promote to prod.
Manual policy crafting from first principles almost always either over-grants (because you’re guessing) or under-grants (and then you debug obscure permission errors). Activity-driven generation is far more reliable.
Pattern 4: Permissions boundaries for delegated administration#
When teams need to create their own IAM roles for their workloads, you don’t want them to be able to grant themselves admin. Permissions boundaries are the answer.
Permissions boundary = a policy that caps the maximum permissions any role/user can have, regardless of what’s attached.
Pattern:
- Platform team defines a
DeveloperRoleBoundarypolicy that allows the typical app needs (S3 read on app buckets, secrets read, SQS, etc.) but explicitly denies IAM write, EC2 instance creation outside specific instance types, etc. - Developers can create IAM roles, but the boundary requires they attach
DeveloperRoleBoundaryas the permissions boundary. - Effective permissions = intersection of role’s policy AND the boundary.
Net effect: developers self-serve role creation, platform retains control over the maximum blast radius.
Pattern 5: SCPs for organization-wide guardrails#
Service Control Policies (SCPs) at the AWS Organizations level apply to entire accounts. Common SCPs we apply:
- Deny use of root user (except specific account setup actions)
- Deny disabling CloudTrail / Config / GuardDuty
- Deny IAM user creation (force SSO + Roles)
- Deny use of access keys for users (force OIDC / temporary creds)
- Region restriction — deny actions in regions you don’t operate in (cuts blast radius from a stolen credential)
- Deny deletion of S3 buckets with
productiontag
SCPs are enforcement, not detection. They prevent the action from succeeding.
Pattern 6: OIDC for CI/CD#
For CI/CD systems (GitHub Actions, GitLab CI, CircleCI), use OIDC federation to assume an IAM role — no long-lived AWS access keys in CI secrets.
Configure once: GitHub OIDC provider in your AWS account, an IAM role with a trust policy that limits which repos/branches can assume it.
# GitHub Actions workflow
permissions:
id-token: write # required for OIDC
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: us-east-1
No access keys. Trust policy can scope to specific branches (token.actions.githubusercontent.com:sub claim) so only main branch deploys to production.
See our GitHub Actions vs GitLab CI piece for the broader CI tooling context.
Anti-patterns we rip out#
A few patterns we’ve audited our way out of:
* on Resource. A policy with Resource: "*" for non-list/describe actions is over-broad. Scope to specific ARNs even if it means more policy lines.
Mixing user policies and roles. “We have IAM users for CI and IAM roles for everything else.” Inconsistency makes auditing harder. Standardize on roles + OIDC.
Long-lived access keys with * access. Almost guaranteed to be a finding in any audit. Replace with temporary credentials via roles.
Cross-account access via shared credentials. Use sts:AssumeRole with appropriate trust policies. Never share access keys across accounts.
Granting iam:PassRole broadly. PassRole lets a principal attach a role to a resource. Granting it on Resource: "*" is privilege escalation waiting to happen. Scope to specific role ARNs.
Service-linked roles modified. AWS-managed service-linked roles (like AWSServiceRoleForEKS) shouldn’t be modified by humans. If you find someone has been editing them, alarm bells.
MFA “optional” for human access. MFA should be required at the IdP level for all human access. No MFA = no SSO session.
What good audit-readiness looks like#
A few things we make sure are in place before a security review:
- CloudTrail logging to a centralized S3 bucket in a dedicated logging account, with bucket policy denying deletes from any account except the security team.
- GuardDuty enabled in every account, in every region you don’t actively deny, with findings centralized.
- Config Rules enabled for common compliance checks (e.g.,
restricted-ssh,root-account-mfa-enabled,s3-bucket-public-read-prohibited). - IAM Access Analyzer running on every account, findings reviewed weekly.
- Permissions boundaries on all developer-created roles.
- Quarterly access reviews — list of who has access to what, reviewed by managers, attested.
- Access key rotation reports — even for the few legitimate long-lived keys, rotate every 90 days.
These aren’t optional for healthcare / finance / government workloads. They’re table stakes for the audit conversation.
Common compliance specifics#
For workloads we deploy in regulated environments:
- HIPAA (US healthcare): BAA signed with AWS, AWS Config rule
cloudtrail-encrypted-cloudwatch-logs-enabled, encryption at rest on all data stores, key rotation policies on KMS keys. - PCI-DSS (payments): Strict network segmentation, dedicated accounts for cardholder data environments, IAM Access Analyzer findings tracked.
- NRB (Nepal banking): On-prem capable architecture, audit log immutability, role-based access with quarterly attestation.
- GDPR (EU): Data residency via region restrictions in SCPs, deletion workflows that prove data was actually removed from snapshots and backups.
Each compliance regime translates into specific IAM requirements. The patterns above cover most of them; specifics need to be tuned per industry.
What we deploy by default#
For a new AWS platform build (the kind we do for hospitals, banks, and SaaS clients):
- AWS Organizations with separate OUs for security, infrastructure, prod workloads, non-prod workloads, sandbox.
- Identity Center (SSO) for all human access.
- OIDC federation for CI/CD.
- IRSA or EKS Pod Identity for EKS workloads.
- Permissions boundaries on all developer-created roles.
- Service Control Policies enforcing the guardrails above.
- CloudTrail + Config + GuardDuty + IAM Access Analyzer at org level.
- Security Hub centralizing findings.
- A
break-glassadmin role with explicit alerting on use.
The platform team owns this. Developers don’t touch it. The boundary between “platform” and “application” is bright and well-policed.
The pattern of patterns#
IAM is one of the few AWS areas where the marginal cost of doing it right is low and the marginal cost of doing it wrong is high. The patterns aren’t novel — they’re in the AWS Well-Architected Framework. The discipline of applying them consistently is what separates secure platforms from those that pass audit by accident.
The teams that ship AWS workloads that hold up aren’t the ones with the most sophisticated IAM. They’re the ones who treat IAM as a first-class platform concern, not as something the application teams figure out individually.
Least-privilege at scale is a platform discipline, not a per-app exercise. If you’re building a regulated platform on AWS and want a sanity check on the IAM posture, our cloud infrastructure team has shipped this pattern for healthcare, finance, and government clients. Tell us about the platform.