Skip to content
ExponaBLOG
AWS FOR DEVELOPERS · DAY 006 / 100

Reading IAM policies: what every AWS developer should know

Every policy has the same four parts.

Expona Admin6 min read

Reading IAM policies

If you've spent any time on AWS, you've probably stared at an IAM policy and felt lost. They look cryptic at first—just nested JSON with unfamiliar keys—but once you understand their structure, they become straightforward. Every policy has the same four parts, and learning to read them fluently will save you hours of debugging permissions issues and prevent security gaps that could cost your organization real money.

The Four Parts: Effect, Action, Resource, and Condition

Every IAM policy statement boils down to four core components. Think of them as answering four simple questions: what decision are we making, which API calls does it apply to, on which AWS resources, and when does it apply. Let me show you a real example:

bash
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::uploads/*",
  "Condition": {
    "Bool": {
      "aws:SecureTransport": "true"
    }
  }
}

Break this down piece by piece.

Effect: Allow or Deny

Effect is the decision gate. It's either Allow or Deny. In the example above, we're saying "yes, permit this." In the real world, I've seen teams get confused thinking that Deny is for blocking bad actors—it's not quite that simple, which I'll explain in the next section.

Action: Which API Calls

Action specifies the AWS API operations you're authorizing. In the example, we've listed two S3 actions: s3:GetObject (read) and s3:PutObject (write). The format is always service:ActionName. You can use wildcards here—s3:* allows all S3 actions, which is fine for admin roles but dangerous for least-privilege access.

A common pattern I see in production is teams starting with wildcards during development, then forgetting to lock them down. That's a security smell.

Resource: Which AWS Resources

Resource specifies the ARN (Amazon Resource Name) of the resources the action applies to. In the example, arn:aws:s3:::uploads/* means "any object inside the uploads bucket." Without scoping this correctly, you risk giving access way too broadly.

The ARN syntax varies by service. For S3, it's arn:aws:s3:::bucket-name/key-path. For EC2 instances, it's arn:aws:ec2:region:account-id:instance/instance-id. The key insight is that every resource has an ARN, and you should almost always use specific ARNs rather than wildcards.

Condition: When Does It Apply

Condition is optional but powerful. It adds time-based, context-based, or identity-based constraints. In the example, "aws:SecureTransport": "true" means the policy only applies if the request comes over HTTPS. I've used conditions to restrict API access to specific IP ranges, specific times of day, or only when MFA is enabled. Conditions are where you move from basic permissions to sophisticated security controls.

Explicit Deny Wins

This is the rule that catches most people off guard: a single Deny statement overrides every Allow statement across all your policies. AWS evaluates permissions in this order:

  1. Explicit Deny — instant rejection
  2. Explicit Allow — grant access
  3. Implicit deny (no policy) — reject by default

Let me show you a real scenario. You're granting a developer broad S3 access with an Allow statement covering the entire bucket. But then your security team adds a Deny policy on objects tagged as sensitive: true. Even though the developer has explicit allow, the deny wins. Their requests to those objects get rejected.

bash
{
  "Effect": "Deny",
  "Action": ["s3:GetObject", "s3:DeleteObject"],
  "Resource": "arn:aws:s3:::data-lake/*",
  "Condition": {
    "StringEquals": {
      "s3:object-tag/sensitive": "true"
    }
  }
}

This is actually powerful for security. A deny policy acts like a handbrake—it's easier to add a deny than to audit every allow statement. I've built systems where the default is broad permission grants, then deny policies carve out sensitive areas. It scales better than the alternative of trying to micro-manage hundreds of allow statements.

The flip side: if your application is mysteriously getting permission errors and you've triple-checked your allow policies, start looking for deny statements. They're usually in an organization-level SCP (Service Control Policy) or a resource-based policy you didn't write.

Scope Resources: Avoid Resource: *

I can't stress this enough. Resource: "*" is a common anti-pattern that will come back to haunt you.

There are two categories of AWS actions: management actions and data actions. Management actions describe infrastructure—creating instances, managing security groups, deploying functions. Data actions read or modify your actual data—reading S3 objects, writing to DynamoDB tables, sending messages to SQS.

For management actions, using Resource: * is sometimes acceptable because you're not directly accessing customer data. For data actions, it's inexcusable. If you give someone s3:GetObject with Resource: "*", they can read every object in every S3 bucket in your account. That's not a permission; that's a security incident waiting to happen.

Here's what I do in production:

bash
{
  "Effect": "Allow",
  "Action": [
    "s3:GetObject",
    "s3:PutObject"
  ],
  "Resource": [
    "arn:aws:s3:::customer-uploads-prod/*",
    "arn:aws:s3:::logs-prod/*"
  ]
}

I list exactly which buckets and paths are needed. It takes five minutes longer to write, but it saves fifteen hours of incident response when credentials leak.

For resources you can't enumerate ahead of time, use ARN wildcards sparingly and combine them with conditions. For example, allow access to all objects in a bucket but only if they're tagged with the requester's department:

bash
{
  "Effect": "Allow",
  "Action": ["s3:GetObject"],
  "Resource": "arn:aws:s3:::shared-data/*",
  "Condition": {
    "StringEquals": {
      "s3:ExistingObjectTag/department": "${aws:PrincipalTag/department}"
    }
  }
}

This uses principal tags and resource tags to enforce data isolation without needing to hardcode ARNs.

Simulate Before Deploying

Before you attach a policy to a user, role, or resource, test it. AWS gives you the IAM Policy Simulator—a free tool that shows exactly what a policy allows and denies.

To use it:

  1. Go to the IAM console → Policies → Policy Simulator
  2. Select the user or role
  3. Choose a service (e.g., S3)
  4. Enter the action and resource ARN
  5. Click "Simulate Custom Policy" or "Simulate Principal Policy"

The simulator tells you: allowed, implicitly denied, or explicitly denied. It also shows you which policy granted or blocked the permission. This is a lifesaver when you're troubleshooting permission chains.

I always simulate three scenarios before going live:

  • Happy path: the intended user performing the intended action
  • Lateral movement: a different user trying to access resources they shouldn't
  • Privilege escalation: an existing user trying to elevate their own permissions

Running this takes maybe ten minutes and catches 90% of permission bugs before they hit production.

Common Mistakes

Using wildcards everywhere. I see developers who write "Action": "*" and "Resource": "*" to "just get things working" and promise to lock it down later. That "later" never comes. Use specific actions and resources from day one.

Forgetting that conditions are AND-ed. If you specify multiple conditions, all must be true. I once wrote a policy with IP restrictions and MFA requirements, thinking either one would suffice. Nope—both had to be true simultaneously. Read the condition block carefully.

Not checking for conflicting deny policies. You've written a perfect allow policy, attached it, and things still don't work. Before you bang your head against the wall, search for deny policies. Use the IAM Access Analyzer or grep through your SCPs and resource policies.

Assuming all services support resource-level permissions. Some AWS services don't support fine-grained resource ARNs. For those, you're stuck with Resource: "*" or you need to rely on conditions. Check the AWS documentation for your specific service.

Key Takeaways

  • Every IAM policy has four parts: Effect (Allow/Deny), Action (which API calls), Resource (which ARNs), and Condition (when it applies).
  • A single explicit Deny overrides all Allows—use this defensively to protect sensitive resources.
  • Always scope data action resources to specific ARNs. Avoid Resource: "*" for anything involving customer or sensitive data.
  • Use the IAM Policy Simulator to test before deploying policies to production.
  • Combine wildcards with conditions to enforce security without hardcoding every ARN.
  • Conditions are AND-ed together—all must be true for the policy to apply.
  • Search for deny policies if your allow policy isn't working as expected.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles