IAM Policies
IAM policies are JSON documents that grant or deny actions on AWS resources. Get this wrong and you grant the world write access to your data; get it right and even a leaked credential is contained to a narrow blast radius.
Identity vs resource, conditions, deny
EXAMPLE
// 1) Policy shape
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadOwnObjects",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::reports-bucket/users/${aws:userid}/*"
}
]
}
// Effect: Allow | Deny (an explicit Deny always wins)
// Action: service:Operation (\* wildcard within a service)
// Resource: an ARN or list of ARNs (also wildcards)
// Condition: extra constraints (IP, MFA, tag match, time of day)
// 2) Identity-based vs resource-based policies
// Identity policy — attached to a user, role, or group; says 'this principal can do X'
// Resource policy — attached to the resource (S3 bucket, KMS key, SQS queue);
// says 'these principals can do X to me'
// Both are evaluated; access requires at least one Allow AND no explicit Deny.
// 3) Least privilege — narrow Action AND Resource
// ✓ Good — read one specific bucket only
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::company-invoices",
"arn:aws:s3:::company-invoices/*"
]
}
// ❌ Bad — over-broad
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
// 4) Conditions — situational gates
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::sensitive-data/*",
"Condition": {
"Bool": { "aws:MultiFactorAuthPresent": "true" },
"NumericLessThan":{ "aws:MultiFactorAuthAge": "3600" },
"IpAddress": { "aws:SourceIp": ["203.0.113.0/24", "198.51.100.0/24"] },
"StringEquals": { "aws:RequestedRegion": "ap-southeast-2" }
}
}
// IP allowlist + MFA required + region locked.
// 5) Tag-based access — scales without rewriting policy per resource
// Resource has tag Team=billing
// Principal allowed when their tag PrincipalTeam == resource tag aws:ResourceTag/Team
{
"Effect": "Allow",
"Action": ["ec2:StartInstances", "ec2:StopInstances"],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": { "aws:ResourceTag/Team": "${aws:PrincipalTag/Team}" }
}
}
// 6) Explicit Deny for guardrails — wins over any Allow
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": { "aws:RequestedRegion": ["ap-southeast-2", "us-east-1"] }
}
}
// Locks all activity to two regions — even if someone attaches AdministratorAccess.
// 7) Bucket policy — limit S3 to TLS and a VPC endpoint
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"DenyInsecureTransport",
"Effect":"Deny",
"Principal":"*",
"Action":"s3:*",
"Resource":["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition":{ "Bool": { "aws:SecureTransport": "false" } }
},
{
"Sid":"OnlyFromVPC",
"Effect":"Deny",
"Principal":"*",
"Action":"s3:*",
"Resource":["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
"Condition":{ "StringNotEquals": { "aws:SourceVpce": "vpce-0a1b2c3d" } }
}
]
}
// 8) Trust policy — who can ASSUME a role
{
"Version":"2012-10-17",
"Statement":[
{
"Effect":"Allow",
"Principal":{ "Service":"ec2.amazonaws.com" },
"Action":"sts:AssumeRole"
}
]
}
// EC2 instances using this role's instance profile can call STS to assume it.
// For cross-account: Principal: { AWS: "arn:aws:iam::ACCOUNT_ID:root" } + ExternalId condition.
// 9) Permission boundary — cap on what a role can EVER grant or use
// Attach a boundary policy that says 'no action beyond this list'.
// Useful for letting devs create their own roles within guardrails.
// 10) Common patterns to avoid
// • iam:PassRole granted broadly → privilege escalation (pass an admin role to a service)
// • Resource: * with destructive actions like s3:DeleteBucket or ec2:TerminateInstances
// • Inline policies — hard to audit, prefer customer-managed
// • AdministratorAccess on dev users → fine for sandboxes, terrible for prod accounts
// 11) Tools to validate before you ship
// • IAM Access Analyzer — finds external access in policies, validates policies
// • aws iam simulate-principal-policy — replay an action against a principal
// • Policy linter (cfn-lint, parliament) in CI
// • CloudTrail + IAM Access Advisor — find unused permissions, then trim
awscli> aws iam simulate-principal-policy \\
--policy-source-arn arn:aws:iam::123:role/AppRole \\
--action-names s3:GetObject \\
--resource-arns arn:aws:s3:::my-bucket/secret.json
// 12) Checklist
// ✓ One role per workload, not shared
// ✓ Action and Resource narrowed; \* only after deliberate review
// ✓ Conditions for MFA, IP, region where it makes sense
// ✓ Resource policies + identity policies aligned (both required)
// ✓ Explicit Deny guardrails at the organization / SCP level
// ✓ Permission boundaries on roles devs can create
// ✓ Quarterly review: Access Advisor → remove unused permissions
Why it matters
Least privilege is hard, but you don’t need to ship it on day one — ship something workable, then use Access Advisor and CloudTrail to peel away unused permissions over the next few sprints. Lock the worst foot-guns up front: explicit Deny guardrails for regions you don’t use, MFA required for sensitive actions, and never Action: *, Resource: * for a real workload.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
{
"Version": "2012-10-17",
"Statement": [{ "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::bucket/*" }]
}
Try it Yourself »
Discussion
Loading…