IAM Patterns That Scale
Part 2 covered STS and temporary credentials. This part gets into the patterns that make IAM work at scale: conditions for precision, ABAC for dynamic access, permission boundaries for safe delegation, and SCPs for organizational guardrails.
If Part 1 was “what IAM is” and Part 2 was “how credentials work,” this is “how to build something maintainable.”

IAM Conditions
Conditions are how you add precision to IAM policies. Instead of “allow S3 access,” you can say “allow S3 access only from our corporate IP range, during business hours, with encryption required.”
Condition Structure
Every condition has three parts:
"Condition": {
"ConditionOperator": {
"ConditionKey": "value"
}
}The operator defines how to compare. The key defines what to compare. The value defines what to compare against.
Common Condition Operators
String operators handle text comparison. StringEquals performs exact case-sensitive matching while StringNotEquals checks for mismatch. When you need pattern matching with wildcards, use StringLike and StringNotLike.
For Amazon Resource Names, ArnEquals checks exact ARN match and ArnLike allows pattern matching within ARN structures.
Numeric operators work as expected: NumericEquals, NumericLessThan, and NumericGreaterThan compare numbers. Date operators follow the same pattern with DateEquals, DateLessThan, and DateGreaterThan for time-based conditions.
IP address operators are essential for network restrictions. IpAddress validates that the source IP falls within a CIDR range, while NotIpAddress ensures the source IP falls outside the range.
The Bool operator performs true/false checks. The Null operator is useful for checking whether a condition key exists at all in the request.
For multi-value conditions, ForAllValues requires every value in a request to match, while ForAnyValue requires at least one value to match.
Global Condition Keys
These work across all AWS services:
| Key | Use Case |
|---|---|
aws:SourceIp | Restrict by IP/CIDR |
aws:SourceVpc | Restrict to VPC |
aws:SourceVpce | Restrict to VPC endpoint |
aws:PrincipalTag/key | Check caller’s tags |
aws:ResourceTag/key | Check resource tags |
aws:RequestTag/key | Check tags in request |
aws:PrincipalOrgID | Restrict to organization |
aws:PrincipalAccount | Restrict to specific account |
aws:MultiFactorAuthPresent | Require MFA |
aws:MultiFactorAuthAge | MFA recency |
aws:CurrentTime | Time-based access |
aws:SecureTransport | Require HTTPS |
aws:CalledVia | Check calling service |
Service-Specific Condition Keys
Each service has its own keys:
S3: s3:prefix, s3:x-amz-acl, s3:ExistingObjectTag, s3:x-amz-server-side-encryption
EC2: ec2:ResourceTag, ec2:InstanceType, ec2:Region, ec2:MetadataHttpTokens
DynamoDB: dynamodb:Attributes, dynamodb:LeadingKeys
KMS: kms:ViaService, kms:CallerAccount, kms:EncryptionContext
Practical Condition Examples
Restrict to corporate IPs:
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"IpAddress": {
"aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"]
}
}
}Allow only during business hours:
{
"Condition": {
"DateGreaterThan": {"aws:CurrentTime": "2026-01-01T09:00:00Z"},
"DateLessThan": {"aws:CurrentTime": "2026-01-01T17:00:00Z"}
}
}Force encryption on S3 uploads:
{
"Effect": "Deny",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"Null": {
"s3:x-amz-server-side-encryption": "true"
}
}
}This denies uploads where encryption is not specified. The Null operator checks if the key is absent.
Require requests through VPC endpoint:
{
"Effect": "Deny",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-1234567890abcdef0"
}
}
}ABAC: Attribute-Based Access Control
ABAC uses tags instead of explicit resource ARNs. One policy can cover hundreds of resources if they’re tagged correctly.
RBAC vs ABAC
With Role-Based Access Control (RBAC), you create roles like “Developer” that can access dev resources and “Admin” that can access everything. Each resource needs explicit policy references, which means updating policies every time you add resources.
Attribute-Based Access Control (ABAC) flips this model. Instead of referencing specific resources, you reference attributes. Users with Project=Phoenix can access any resource tagged Project=Phoenix. When someone creates a new resource and tags it appropriately, access works automatically. No policy updates required. This is why ABAC scales so much better than RBAC in large environments.
The ABAC Model in AWS
Four types of tags matter for ABAC. Principal tags are the tags attached to the IAM user or role making the request. Resource tags are on the AWS resource being accessed. Request tags are the tags being applied in the current API call, like when you’re creating a new resource and tagging it. Session tags are passed during role assumption and become part of the temporary credentials.
ABAC Policy Pattern
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances"],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Project": "${aws:PrincipalTag/Project}",
"ec2:ResourceTag/Environment": "${aws:PrincipalTag/Environment}"
}
}
}]
}This says: users can only start/stop EC2 instances where the instance’s Project and Environment tags match their own. A developer tagged Project=Phoenix, Environment=dev can manage Phoenix dev instances but not production instances or instances from other projects.
Setting Up ABAC
Tag the role:
aws iam tag-role --role-name DeveloperRole \
--tags Key=Project,Value=Phoenix Key=Environment,Value=devTag the resources:
aws ec2 create-tags --resources i-1234567890abcdef0 \
--tags Key=Project,Value=Phoenix Key=Environment,Value=devCreate the ABAC policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:RebootInstances"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Project": "${aws:PrincipalTag/Project}"
}
}
},
{
"Effect": "Allow",
"Action": "ec2:Describe*",
"Resource": "*"
}
]
}Session Tags
You can pass tags during role assumption:
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/MyRole \
--role-session-name MySession \
--tags Key=Department,Value=Engineering Key=CostCenter,Value=CC-123These session tags become available as aws:PrincipalTag/Department and aws:PrincipalTag/CostCenter for the duration of the session.
Why ABAC Matters
The practical benefits are significant. One ABAC policy can replace dozens of resource-specific policies because you’re matching on attributes rather than ARNs. Teams get self-service capability because they can create resources within their tagged boundaries without waiting for IAM changes. Auditing becomes simpler because tag-based access is easy to understand and review. And everything scales automatically since new resources inherit access based on their tags.
Permission Boundaries
Permission boundaries solve the delegation problem: how do you let developers create IAM roles without letting them create admin roles?

The Delegation Problem
You want developers to create Lambda execution roles for their functions. But you don’t want them creating roles with admin access or roles that can modify IAM itself.
How Boundaries Work
Effective Permissions = Identity Policy ∩ Permission BoundaryThe intersection means a permission must be allowed by BOTH the identity policy AND the boundary. The boundary sets a ceiling that can never be exceeded.
Creating a Permission Boundary
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*",
"dynamodb:*",
"lambda:*",
"logs:*",
"sqs:*",
"sns:*"
],
"Resource": "*"
},
{
"Effect": "Deny",
"Action": [
"iam:*",
"organizations:*",
"account:*"
],
"Resource": "*"
}
]
}This boundary allows common application services but blocks IAM, Organizations, and Account management.
Applying the Boundary
# Apply to an existing role
aws iam put-role-permissions-boundary \
--role-name DeveloperRole \
--permissions-boundary arn:aws:iam::123456789012:policy/DeveloperBoundary
# Apply to an existing user
aws iam put-user-permissions-boundary \
--user-name developer \
--permissions-boundary arn:aws:iam::123456789012:policy/DeveloperBoundaryForcing Boundaries on Created Roles
You can also require developers to attach boundaries to any roles they create:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:DeleteRole",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:PutRolePolicy",
"iam:DeleteRolePolicy"
],
"Resource": "arn:aws:iam::123456789012:role/app-*",
"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "arn:aws:iam::123456789012:policy/DeveloperBoundary"
}
}
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::123456789012:role/app-*",
"Condition": {
"StringEquals": {
"iam:PassedToService": "lambda.amazonaws.com"
}
}
}
]
}This policy says:
- Developers can create/modify roles that start with
app- - But only if those roles have the DeveloperBoundary attached
- They can only pass roles to Lambda
Any role the developer creates will be constrained by the same boundary they’re constrained by. Privilege escalation blocked.
Boundary vs SCP
| Aspect | Permission Boundary | SCP |
|---|---|---|
| Scope | Single identity | Entire account/OU |
| Applied to | Users, Roles | Accounts |
| Grants permissions | No | No |
| Management account exempt | N/A | Yes |
Service Control Policies (SCPs)
SCPs are organization-level guardrails. They set the maximum permissions available to an entire account.
SCP Fundamentals
SCPs don’t grant permissions. They only restrict what’s possible. This is important to understand because you can’t use an SCP to give someone access. You can only use it to take away access that would otherwise be granted.
SCPs affect all principals in the account, including the root user. This makes them powerful guardrails. However, the management account is exempt from SCPs, which is why you should never run workloads in the management account.
SCPs are evaluated before identity and resource policies. If an SCP denies something, it doesn’t matter what your IAM policy says.
SCP Strategy: Deny List vs Allow List
Deny List approach (block specific actions):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": ["organizations:LeaveOrganization"],
"Resource": "*"
}
]
}This blocks leaving the organization but allows everything else.
Allow List approach (only permit specific actions):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ec2:*", "s3:*", "lambda:*", "rds:*"],
"Resource": "*"
}
]
}This allows only EC2, S3, Lambda, and RDS. Everything else is implicitly denied.
Most organizations use deny lists because they’re easier to manage and don’t break things unexpectedly.
Common SCP Patterns
Region restriction:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["us-east-1", "us-west-2", "eu-west-1"]
}
}
}
]
}Prevent disabling security services:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"guardduty:DeleteDetector",
"guardduty:DisassociateFromMasterAccount",
"securityhub:DisableSecurityHub",
"config:DeleteConfigurationRecorder",
"cloudtrail:DeleteTrail",
"cloudtrail:StopLogging"
],
"Resource": "*"
}
]
}Require IMDSv2 for EC2:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
}
]
}Prevent root user actions (except for legitimate uses):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::*:root"
}
}
}
]
}SCP Inheritance
SCPs flow down the organizational hierarchy:
Root → OU → Nested OU → AccountEffective permissions = intersection of all SCPs in the path. If any SCP in the chain denies something, it’s denied.
If you’re using allow-list SCPs, you need an explicit Allow at every level.
IRSA: IAM Roles for Service Accounts
IRSA gives Kubernetes pods AWS permissions without storing credentials. It’s the standard pattern for EKS workloads.
The Problem IRSA Solves
Before IRSA, giving pods AWS access meant either using the node’s IAM role (which gives every pod on that node the same permissions, way too broad) or injecting credentials into the pod (risky because credentials can leak and don’t rotate automatically).
IRSA provides per-pod IAM roles using OIDC federation. Each pod gets only the permissions it needs, credentials rotate automatically, and there’s nothing to leak.
How IRSA Works
Pod with ServiceAccount
→ Kubernetes injects OIDC token
→ Pod calls STS AssumeRoleWithWebIdentity
→ STS validates token with EKS OIDC provider
→ Temporary credentials returned
→ Pod accesses AWS servicesThis is AssumeRoleWithWebIdentity from Part 2, implemented specifically for Kubernetes.
Setting Up IRSA
1. Associate OIDC provider with EKS cluster:
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--approveOr with AWS CLI:
# Get OIDC issuer URL
OIDC_URL=$(aws eks describe-cluster --name my-cluster \
--query "cluster.identity.oidc.issuer" --output text)
# Create OIDC provider
aws iam create-open-id-connect-provider \
--url $OIDC_URL \
--client-id-list sts.amazonaws.com \
--thumbprint-list $(openssl s_client -connect ${OIDC_URL#https://}:443 \
2>/dev/null </dev/null | openssl x509 -fingerprint -noout | cut -d= -f2 | tr -d :)2. Create IAM role with trust policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:sub": "system:serviceaccount:default:my-service-account",
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3041E:aud": "sts.amazonaws.com"
}
}
}
]
}That condition matters. It restricts which ServiceAccount in which namespace can assume the role.
3. Create ServiceAccount with annotation:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-service-account
namespace: default
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/MyPodRole4. Use the ServiceAccount in your Pod:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
serviceAccountName: my-service-account
containers:
- name: my-container
image: my-imageIRSA vs EKS Pod Identity
AWS introduced Pod Identity as a simpler alternative to IRSA:
| Aspect | IRSA | Pod Identity |
|---|---|---|
| Setup | More complex | Simpler |
| OIDC provider | Required | Not required |
| Cross-account | Supported | Supported |
| Works outside EKS | Yes (any OIDC) | No |
For the exam, know both. IRSA is more established and works anywhere OIDC is supported. Pod Identity is simpler but EKS-specific.
Identity-Based vs Resource-Based Policies
This distinction is fundamental to IAM but often misunderstood. Understanding when to use each type and how they interact matters for both the exam and production environments.

Identity-Based Policies
Identity-based policies attach to IAM principals (users, groups, roles). They define what actions that principal can perform.
These policies don’t include a Principal element because the principal is whoever the policy is attached to. You specify Resource to define what can be accessed. The key characteristic is portability: when a user assumes a different role, the identity policies on their original identity don’t apply. Only the assumed role’s policies matter.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}]
}This policy attached to a role says “this role can read and write objects in my-bucket.”
Resource-Based Policies
Resource-based policies attach directly to AWS resources. They define who can access that resource and what they can do.
These policies attach to resources like S3 buckets, KMS keys, and Lambda functions. They must include a Principal element specifying who is allowed access, and the Resource is implicit since it’s the resource the policy is attached to. The big advantage: resource-based policies enable cross-account access without role assumption. The caller keeps their original identity and permissions.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/DataProcessor"
},
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}]
}This bucket policy says “the DataProcessor role from account 111111111111 can read objects from this bucket.”
Same-Account vs Cross-Account Access
This is where people get tripped up on the exam.
For same-account access, either an identity policy OR a resource policy can grant access. Either one alone is sufficient. If your identity policy allows S3 access and the bucket has no policy, you’re in.
Cross-account access is stricter. Both the identity policy AND the resource policy must allow the access. Both sides must explicitly permit it. Miss either one and you get AccessDenied.
Same Account:
Identity Policy ALLOWS → ACCESS GRANTED
Resource Policy ALLOWS → ACCESS GRANTED
Cross Account:
Identity Policy ALLOWS + Resource Policy ALLOWS → ACCESS GRANTED
Identity Policy ALLOWS + Resource Policy MISSING → ACCESS DENIED
Identity Policy MISSING + Resource Policy ALLOWS → ACCESS DENIEDServices Supporting Resource-Based Policies
Not all services support resource-based policies:
| Service | Resource Policy Name | Cross-Account? |
|---|---|---|
| S3 | Bucket policy | Yes |
| KMS | Key policy (required) | Yes |
| Lambda | Resource policy | Yes |
| SQS | Queue policy | Yes |
| SNS | Topic policy | Yes |
| API Gateway | Resource policy | Yes |
| Secrets Manager | Resource policy | Yes |
| ECR | Repository policy | Yes |
| EventBridge | Event bus policy | Yes |
The KMS Special Case
KMS is unique: key policies are REQUIRED. You cannot use identity-based policies alone for KMS access.
Even if your identity policy allows kms:Decrypt, the key policy must also allow it:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:root"
},
"Action": "kms:*",
"Resource": "*"
}
]
}This key policy enables IAM policies in the account to control access. Without this (or explicit principal permissions), identity policies are ignored.
When to Use Each
Identity-based policies work best for same-account access because they’re simpler to manage. Use them when permissions should follow the user or role, when you have complex permission logic spanning multiple resources, or when you want centralized permission management.
Resource-based policies shine for cross-account access without role assumption. They’re the right choice for service integrations like Lambda triggers, S3 events, and EventBridge rules. Use them when you have a centralized resource with many accessors from different accounts, or when you want to control access at the resource level rather than the identity level.
Decision Framework
| Scenario | Best Approach | Why |
|---|---|---|
| Developer needs S3 access in same account | Identity policy | Simpler, portable |
| Partner account needs S3 access | Resource policy | No role assumption needed |
| Lambda triggered by S3 | Resource policy | Service integration |
| EC2 accessing DynamoDB | Identity policy (via role) | Same account, portable |
| Central logging bucket | Resource policy | Many accounts writing |
| KMS key access | Both required | KMS requirement |
Cross-Account Resource Policy Example
S3 bucket in Account A, accessed from Account B:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT_B_ID:role/DataProcessor"
},
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}With this resource policy, the DataProcessor role in Account B can access the bucket without assuming a role in Account A.
Exam Notes
Know condition operators and when to use each. StringLike for wildcards, IpAddress for CIDR ranges, Null for checking key existence.
ABAC uses aws:PrincipalTag, aws:ResourceTag, and aws:RequestTag. The policy pattern uses ${aws:PrincipalTag/key} syntax.
Permission boundaries intersect with identity policies. Effective = Identity ∩ Boundary.
SCPs don’t grant permissions, only restrict. Management account is exempt. Evaluated before other policies.
KMS requires key policies. Identity-only doesn’t work.
IRSA uses AssumeRoleWithWebIdentity with EKS OIDC provider.
Part 3 of 4 in the AWS IAM Security Deep Dive series.