Write IAM Policies with Conditions
Learn how to write IAM policies with conditions to reduce blast radius. Step-by-step guide with hands-on exercises, troubleshooting, and next steps.
Focus: write iam policies with conditions
We’ve all been there: you lock down a bucket with a strict IAM policy, only to discover later that a contractor’s leaked credentials could still read every object in it. Why? Because who you are matters, but when, where, and how you act matter just as much. In this lesson, you’ll learn to write IAM policies with conditions — the secret ingredient that turns a blunt “allowed” into a precise, context-aware authorization that shrinks your blast radius.
The problem this lesson solves
Standard IAM policies answer a single question: Can this principal perform this action on this resource? If yes, the API call proceeds — regardless of whether the caller is in your office, on a compromised laptop, or using a script that hasn’t been updated in years.
The result is a world of over-permissioned identities. An admin key meant for automated backups can also be used to delete databases at 3 AM from a coffee shop in another country. A read-only role can be invoked with --source-ip spoofed to anywhere in the world. This is exactly where real-world breaches happen — not because the attacker cracked your password, but because a policy allowed the action with no additional checks.
Imagine a bank that lets anyone with a valid ID walk into the vault — no time-of-day check, no CCTV verification, no tamper-evident seal. That’s a policy without conditions. Conditions add the extra layers of verification that make a single leaked credential far less dangerous.
Core concept / mental model
Think of an IAM policy as a set of permission gates. The first gate checks the who (principal), the what (action), and the where (resource). A condition closes a second gate: it requires the request to also match additional context like the caller’s IP, the time of day, or whether MFA was used.
The easiest mental model? A nightclub’s door policy. The door list tells you who can enter (the principal). But the bouncer also checks your ID (MFA), your ticket’s validity window (time), and whether you’re at the correct entrance (IP range). If any check fails, you don’t get in — even if your name is on the list.
In IAM, conditions are expressed as a Condition block in the policy. They use operators like IpAddress, Bool, NumericLessThanEquals, or StringEquals. Here’s a simple skeleton:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-private-bucket/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "203.0.113.0/24"
}
}
}
]
}
This policy says: only allow s3:GetObject on that bucket if the request comes from the specified IP range. If the caller is outside that range, the action is denied — even if the principal is an admin.
How it works step by step
Building a condition-based policy is a methodical process. Let’s break it down into five steps you can apply to any scenario:
Step 1: Identify the context you care about
Ask yourself: What extra signal would make this permission safe? Common ones are:
- Caller IP (
aws:SourceIp) for office-only access - Time (
aws:CurrentTime) for scheduled jobs - MFA presence (
aws:MultiFactorAuthPresent) for sensitive destructive actions - Tag on resource (
aws:ResourceTag) to enforce naming or environment separation - Requested region (
aws:RequestedRegion) to keep actions inside your compliance boundary
Step 2: Choose the right operator
Different checks need different operators. For IP conditions use IpAddress or NotIpAddress. For true/false like MFA, use Bool. For strings, StringEquals or StringLike. For numbers, NumericGreaterThan etc. Each operator has a strict and relaxed variant (StringEquals vs StringEqualsIgnoreCase, IpAddress vs NotIpAddress).
Step 3: Write the Condition block
Place the condition inside the statement. You can have multiple keys, and they are combined with AND logic by default. Use Condition with a JSON object that maps operators to their key-value pairs.
Step 4: Test with the IAM Policy Simulator
Always simulate your policy with the AWS Console’s IAM Policy Simulator before applying it. This tool checks whether a given principal, action, resource, and context would be allowed or denied — and shows you which conditions failed.
Step 5: Apply and monitor
Attach the policy to a group or role. Then watch CloudTrail logs to see if any legitimate requests are being denied unexpectedly. If so, adjust the condition or the test context — don’t simply remove the condition.
Hands-on walkthrough
Let’s create a real policy that only allows read access to a specific bucket from a trusted IP, and requires MFA for delete operations. We’ll use the AWS CLI to create and test it.
Setup
Make sure you have the AWS CLI configured with appropriate permissions:
aws sts get-caller-identity
Write the policy JSON
Create a file named condition-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadFromOffice",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-private-bucket",
"arn:aws:s3:::my-private-bucket/*"
],
"Condition": {
"IpAddress": {
"aws:SourceIp": "203.0.113.0/24"
}
}
},
{
"Sid": "DenyDeleteWithoutMFA",
"Effect": "Deny",
"Action": "s3:DeleteObject",
"Resource": "arn:aws:s3:::my-private-bucket/*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Notice the second statement is a Deny with a BoolIfExists condition. BoolIfExists treats a missing key (like when MFA isn’t used) as false, so it effectively denies deletes when MFA is absent.
Attach the policy to a role
aws iam create-policy --policy-name ConditionDemo --policy-document file://condition-policy.json
aws iam attach-role-policy --role-name my-app-role --policy-arn arn:aws:iam::123456789012:policy/ConditionDemo
Test the policy
Use the IAM Policy Simulator in the console (or the aws iam simulate-principal-policy CLI command) to check:
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::123456789012:role/my-app-role --action-names s3:GetObject --resource-arns arn:aws:s3:::my-private-bucket/secret.txt
If you pass --context-entries with an IP outside the allowed range, the simulator returns explicitDeny. Expect output like:
{
"EvaluationResults": [
{
"EvalActionName": "s3:GetObject",
"EvalDecision": "explicitDeny",
"MatchedStatements": []
}
]
}
Compare options / when to choose what
Conditions aren’t one-size-fits-all. Here’s a comparison of the most common condition keys and when to use them:
| Condition key | Use case | Example operator | When to choose it |
|---|---|---|---|
aws:SourceIp |
Restrict to IP ranges | IpAddress |
When your team works from static IPs (office, VPN) |
aws:CurrentTime |
Allow only during business hours or a maintenance window | DateGreaterThan, DateLessThan |
For scheduled jobs or to reduce attack surface at night |
aws:MultiFactorAuthPresent |
Force MFA for critical actions | Bool, BoolIfExists |
For any destructive or sensitive API call |
aws:RequestedRegion |
Keep actions inside specific regions | StringEquals |
For compliance (e.g., data residency) |
aws:ResourceTag |
Require certain tags on the resource | StringEquals |
To enforce environment separation (prod vs dev) |
General rule: start with aws:SourceIp for user-facing roles, add aws:MultiFactorAuthPresent for high-risk actions, and use aws:RequestedRegion when you need geographic compliance.
Troubleshooting & edge cases
“It’s not working – my IP condition is ignored”
- Verify the IP format: Use CIDR notation (e.g.,
203.0.113.0/24), not a bare IP like203.0.113.5. If you want a single IP, use/32. - Check whether the condition key exists: Some services don’t propagate
aws:SourceIpfor instance roles via EC2. For EC2, useaws:VpcSourceIpif the request comes from within a VPC.
“The MFA condition denies everything”
BoolvsBoolIfExists:Boolfails when the key is missing;BoolIfExiststreats a missing key asfalse. If your CLI isn’t MFA-authenticated, useBoolIfExistsfor a deny that triggers appropriately.- Test with the simulator: Temporarily add a
ConditionwithBoolIfExists: trueto see if the rest of the policy works.
“I used StringEquals but I need a wildcard”
Use StringLike instead. For example, to match any s3 prefix:
"Condition": {
"StringLike": {
"s3:prefix": "logs/*"
}
}
“My policy allows, but the API call still fails”
- Check for an explicit Deny elsewhere. An explicit deny always overrides an allow.
- Check service-level conditions: S3 has
s3:prefix,s3:max-keys; EC2 hasec2:Region— always look up the service’s condition keys in the documentation.
What you learned & what's next
You now know how to write IAM policies with conditions — restricting permissions by IP, time, MFA, region, and more. You can build policies that reduce blast radius and keep your cloud environment secure even if a credential leaks.
Next up in the Cloud Security Essentials path is KMS usage stories — we’ll apply the same conditional thinking to encryption keys, and learn how to protect data at rest. You’re one step closer to full-stack cloud security mastery.
Practice recap
Create a policy that requires MFA for any s3:DeleteBucket action but allows reads from any IP between 9 AM and 6 PM UTC. Attach it to a test role and use the IAM Policy Simulator to verify both allow and deny scenarios.
Common mistakes
- Using
Boolinstead ofBoolIfExistsfor MFA checks — this denies requests when the key is absent, which might be your intent, but if you want to allow non-MFA requests for some actions, useBoolIfExistsor separate statements. - Forgetting to use CIDR notation for
aws:SourceIp— a single IP like203.0.113.5is invalid; use203.0.113.5/32. - Assuming
aws:SourceIpworks for EC2 instance roles — for VPC traffic, useaws:VpcSourceIpor check if the service supports it. - Placing conditions on the wrong statement or mixing AND/OR logic without parentheses — conditions combine with AND by default, so use multiple statements for OR logic.
- Not testing with the IAM Policy Simulator before production — you might miss unexpected denies due to missing context keys.
Variations
- Use
StringLikeinstead ofStringEqualsfor pattern matching on resource tags or prefixes. - Leverage IAM Roles Anywhere to get temporary credentials with conditions outside AWS, reducing the need for long-lived access keys.
- Combine conditions with
aws:PrincipalTagto enforce that only principals with a specific tag can perform actions, ideal for cross-account access.
Real-world use cases
- Restrict S3 bucket access to office IP ranges so that a leaked developer key is useless outside the corporate network.
- Require MFA for any data deletion (e.g., DynamoDB table delete) to prevent accidental or malicious irreversible actions.
- Keep EC2 instances in a single region by enforcing
aws:RequestedRegionso misconfigured scripts can’t spin up resources in non-compliant regions.
Key takeaways
- IAM conditions gate actions on context, not just identity — they dramatically reduce blast radius.
- Choose the right condition key and operator for your use case: IP, time, MFA, region, or tags.
- Use
BoolIfExistsinstead ofBoolwhen you want a deny to trigger when the key is missing. - Always test policies with the IAM Policy Simulator before applying to production.
- Remember: an explicit deny with a condition overrides an allow, so design with deny-first for risky actions.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.