IAM Policy Structure and Least-Privilege Design
IAM Policy Structure and Least-Privilege Design
IAM is the control plane for every action taken in a cloud account. Every API call — reading a bucket, launching an instance, modifying a database — is an IAM authorization decision. If IAM is misconfigured, every other security control becomes irrelevant because the attacker can simply use legitimate credentials with excessive permissions.
The anatomy of an IAM policy
An IAM policy is a JSON document with a specific structure. Understanding this structure is the foundation of everything else in IAM:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadSpecificBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-app-data",
"arn:aws:s3:::my-app-data/*"
],
"Condition": {
"IpAddress": {"aws:SourceIp": "203.0.113.0/24"}
}
}
]
}
The key fields:
- Effect:
AlloworDeny. An explicitDenyalways overrides anyAllow— this is the most important rule in IAM. - Action: The specific API operations being permitted. Use wildcards (
s3:*) sparingly — they're the most common source of over-permission. - Resource: The specific ARNs the policy applies to.
*means all resources — another common source of excessive privilege. - Condition: Additional constraints (IP address, MFA presence, time of day, etc.) that must be met for the policy to apply.
The principle of least privilege
Least privilege means granting only the permissions required to perform a specific task — nothing more. In practice, this means:
- Start with no permissions and add only what's needed
- Scope to specific resources, not
* - Use specific actions, not wildcard actions
- Add conditions to constrain when and where the permission applies
// BAD: Over-permissive
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
// GOOD: Least privilege
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-app-data/uploads/*",
"Condition": {
"Bool": {"aws:MultiFactorAuthPresent": "true"}
}
}
Policy evaluation logic
When a principal attempts an action, the cloud provider evaluates all applicable policies in a specific order:
- Explicit deny — if any policy explicitly denies the action, the request is denied immediately
- Allow — if any policy allows the action (and no deny exists), the request is allowed
- Default deny — if no policy explicitly allows the action, it's denied by default
This means IAM is deny-by-default: you must explicitly grant every permission. The danger is granting too much, not too little.
Identity-based vs. resource-based policies
There are two ways to attach permissions:
- Identity-based policies are attached to IAM users, groups, or roles. They define what that identity can do.
- Resource-based policies are attached to the resource itself (e.g., an S3 bucket policy). They define who can access that resource and what they can do.
When both exist, the evaluation is: (identity-based allow OR resource-based allow) AND NOT (explicit deny in either).
Roles and Temporary Credentials
Roles and Temporary Credentials
Long-lived access keys are the most commonly leaked credential in cloud environments. They get committed to GitHub, stored in environment variables, logged in debug output, and embedded in CI/CD configurations. Once leaked, they provide persistent access until manually rotated — which often never happens.
Roles solve this problem by providing temporary, automatically-rotating credentials that expire after a short duration.
How roles work
A role is an identity with no long-lived credentials. Instead, you assume the role to receive temporary credentials:
# Assuming a role and receiving temporary credentials
aws sts assume-role \
--role-arn arn:aws:iam::123456789012:role/AppReadOnly \
--role-session-name my-session
# Response contains temporary credentials
{
"Credentials": {
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"SessionToken": "...",
"Expiration": "2026-09-30T18:00:00Z"
}
}
The temporary credentials:
- Expire after a configurable duration (15 minutes to 12 hours, default 1 hour)
- Are automatically rotated by the cloud provider
- Cannot be leaked in a way that provides persistent access
- Are tied to the session that requested them
Common role patterns
1. Service roles for compute:
Instead of putting access keys on an EC2 instance, attach an IAM role. The instance metadata service provides temporary credentials automatically:
# On an EC2 instance with an attached role — no keys needed
aws s3 ls s3://my-app-data/
# The instance automatically retrieves temporary credentials from
# http://169.254.169.254/latest/meta-data/iam/security-credentials/
2. Cross-account access:
Roles enable secure cross-account access without sharing credentials:
// Trust policy on the role in Account B
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::111111111111:root"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"sts:ExternalId": "unique-external-id-123"}
}
}]
}
The ExternalId condition prevents the confused deputy problem — it ensures that Account B can only be assumed by someone who knows the external ID.
3. CI/CD pipeline roles:
Build systems assume roles to deploy, rather than using stored access keys:
# GitHub Actions example
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeploy
aws-region: us-east-1
Permission boundaries
A permission boundary is an advanced feature that sets the maximum permissions an identity-based policy can grant. Even if a policy allows s3:*, a permission boundary that only allows s3:GetObject will cap the effective permissions:
// Permission boundary policy
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": "*"
}]
}
Permission boundaries are useful for delegating policy creation — you can let team members create IAM policies, but the boundary ensures they can't grant more privilege than intended.
Service control policies (SCPs)
In AWS Organizations, SCPs act as permission boundaries at the organization level. They define what actions are allowed across all accounts in the organization:
// SCP that denies all actions outside us-east-1
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "DenyOutsideUSEast1",
"Effect": "Deny",
"NotAction": ["iam:*", "organizations:*", "route53:*"],
"Resource": "*",
"Condition": {
"StringNotEquals": {"aws:RequestedRegion": "us-east-1"}
}
}]
}
SCPs are the most powerful guardrail in AWS — they can deny actions even if an account's IAM policies allow them.
Practice Problems
A Python application running on EC2 needs to read objects from a specific S3 bucket (arn:aws:s3:::app-data) and write logs to a specific prefix (arn:aws:s3:::app-data/logs/*). Write an IAM policy that grants exactly these permissions and nothing more.
Solution
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadAppData",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::app-data",
"arn:aws:s3:::app-data/*"
]
},
{
"Sid": "WriteLogs",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::app-data/logs/*"
}
]
}
```
Note: `s3:ListBucket` must be granted on the bucket ARN (not the object ARN), while `s3:GetObject` is granted on the object ARN. This is a common IAM mistake. An IAM user has two policies attached. Policy A allows `s3:*` on `*`. Policy B denies `s3:DeleteBucket` on `arn:aws:s3:::production-data`. Can the user delete the production-data bucket? Explain why.
Solution
No. The explicit deny in Policy B overrides the allow in Policy A. IAM evaluation logic is: explicit deny > allow > default deny. Even though Policy A allows all S3 actions on all resources, the explicit deny for `s3:DeleteBucket` on the specific bucket ARN takes precedence. This is why explicit deny statements are so powerful — they create guardrails that cannot be accidentally overridden by permissive allow policies. Quiz
1. An IAM policy allows `s3:*` on `*`. A second policy attached to the same identity denies `s3:DeleteObject` on `arn:aws:s3:::critical-data/*`. What is the effective permission for deleting objects in the critical-data bucket?
2. Why are IAM roles with temporary credentials more secure than long-lived access keys?
3. What is the purpose of a permission boundary in IAM?
Flashcards
Question
What is the IAM policy evaluation order?
Click to reveal answer
Answer
1) Explicit deny (overrides everything), 2) Allow (if no deny exists), 3) Default deny (if no allow exists). Deny-by-default is the foundation.
Question
What is the difference between identity-based and resource-based policies?
Click to reveal answer
Answer
Identity-based policies are attached to users/groups/roles and define what that identity can do. Resource-based policies are attached to the resource (e.g., S3 bucket policy) and define who can access it.
Question
Why are temporary role credentials more secure than access keys?
Click to reveal answer
Answer
They expire automatically (15 min to 12 hours) and are rotated by the provider. Leaked temporary credentials provide access only until expiration; leaked access keys persist indefinitely.
Question
What does a permission boundary do?
Click to reveal answer
Answer
It sets the maximum permissions an identity-based policy can grant. Even if a policy allows more, the boundary caps effective permissions. Used to delegate policy creation safely.
Revision Notes
Key Takeaways
- 1. IAM is deny-by-default — you must explicitly grant every permission
- 2. Explicit deny always overrides any allow, making it the most powerful guardrail
- 3. Roles with temporary credentials eliminate the risk of leaked long-lived access keys
- 4. Least privilege means specific actions on specific resources with conditions, not wildcards
Interview Tips
- • Walk through a real example: 'I replaced EC2 access keys with an instance role, which eliminated our leaked-key risk.'
- • Explain the evaluation order: 'Explicit deny > allow > default deny — this is why SCPs are so effective as guardrails.'
- • Discuss the trade-off: 'Permission boundaries add complexity but let us delegate IAM policy creation to dev teams safely.'
Cheat Sheet
Cloud IAM, Roles, and Policies Cheat Sheet
Phase: Cloud Security Foundations | Level: intermediate
Policy evaluation order:
- Explicit deny — always wins
- Allow — if no deny exists
- Default deny — if no allow exists
Least privilege checklist:
- Start with no permissions, add only what's needed
- Scope to specific resource ARNs, not
* - Use specific actions, not
s3:* - Add conditions (IP, MFA, time) to constrain when/where
Roles vs. access keys:
- Roles provide temporary credentials (15 min – 12 hours)
- Credentials auto-expire and auto-rotate
- No long-lived keys to leak or rotate manually
- Common patterns: EC2 instance roles, cross-account roles, CI/CD pipeline roles
Advanced controls:
- Permission boundaries: cap max permissions for an identity
- SCPs: organization-level permission boundaries (AWS Organizations)
- ExternalId: prevents confused deputy in cross-account roles
Common mistakes:
- Using
Action: "*"andResource: "*" - Putting access keys on EC2 instances instead of using roles
- Forgetting that
s3:ListBucketapplies to the bucket ARN, not object ARNs
Continue Learning
Before You Start
- No prerequisites listed.