Section 2 · Identity and Security
Reading and Writing IAM Policies
20 min3 min readSpot the bug
Lesson at a glance
Read any IAM policy statement by statement, write least-privilege policies with exact ARNs, and add conditions such as MFA and TLS that tighten when an allow applies.
- 1Anatomy of a statement
- 2Bucket ARN or object ARN?
- 3Least privilege, in practice
- 4Conditions: when an allow applies
- 5Validate before you attach
No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.
In the last lesson you learned how IAM decides. Now you write what it decides from. Lantern needs a policy for the function that receives poster uploads, a policy for the developers, and a bucket policy that protects the posters. All three are the same small JSON language.
Anatomy of a statement
Here is the policy for Lantern’s upload function, which saves posters under uploads/ and occasionally lists what is there:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "WriteUploads",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::lantern-posters/uploads/*"
},
{
"Sid": "ListUploadsOnly",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::lantern-posters",
"Condition": {
"StringLike": { "s3:prefix": "uploads/*" }
}
}
]
}Read it top to bottom:
Versionis always"2012-10-17", the current policy language version. It is a fixed string, not today’s date.Statementis a list. Each statement is evaluated on its own.Sidis an optional label. Name what the statement is for; future you will thank you.EffectisAlloworDeny.Actionis one action or a list, writtenservice:Operation. Wildcards work (s3:Get*) but each one widens the door.Resourceis one ARN or a list. Wildcards work here too.Conditionis optional and narrows when the statement applies.
Resource-based policies, such as bucket policies, add a Principal element naming who the statement is about. Identity-based policies don’t have one, because the identity they are attached to is the principal.
Bucket ARN or object ARN?
The policy above has two different resources on purpose. s3:ListBucket is an operation on the bucket, so its resource is arn:aws:s3:::lantern-posters. s3:PutObject is an operation on objects, so its resource is the bucket ARN followed by a path: lantern-posters/uploads/*. Swap them and neither statement ever matches, and you get AccessDenied with a policy that looks right.
The same distinction exists elsewhere. DynamoDB’s dynamodb:Query on a table index needs the index ARN (table/lantern-rsvps/index/by-event), not only the table ARN. When a policy “obviously” allows something and AWS says no, compare the request’s ARN with the policy’s character by character.
Least privilege, in practice
Least privilege means granting only the actions and resources a job needs. It sounds slow, but the workflow is quick:
- Write down what the job does in plain words: “write posters to
uploads/, list that prefix”. - Translate each verb into an action, and each noun into an exact ARN.
- Deploy and test. An AccessDenied message names the missing action and resource; add exactly that.
For existing roles, IAM shows last accessed information per service, and IAM Access Analyzer can generate a policy from the actions a role actually used according to AWS CloudTrail. Both are good ways to shrink a policy that grew too wide.
Conditions: when an allow applies
A condition block maps operators to keys and values:
"Condition": {
"StringEquals": { "aws:RequestedRegion": ["eu-west-1", "eu-south-2"] },
"Bool": { "aws:MultiFactorAuthPresent": "true" }
}Two rules make conditions predictable. Separate keys are ANDed: the request must be in one of those Regions and made with MFA. Multiple values for one key are ORed: either Region will do.
Useful keys for Lantern:
| Key | Use it to |
|---|---|
aws:SecureTransport |
refuse plain-HTTP requests |
aws:MultiFactorAuthPresent |
require MFA for risky actions such as deletes |
aws:SourceIp |
limit a human to the office or VPN range |
aws:RequestedRegion |
keep work inside Lantern’s Regions |
s3:prefix |
limit ListBucket to a folder |
Conditions also make great deny guardrails. Lantern’s bucket policy refuses any request without TLS, from anyone:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::lantern-posters",
"arn:aws:s3:::lantern-posters/*"
],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
}
]
}Because an explicit deny always wins, no allow anywhere can undo it.
Validate before you attach
The IAM console’s policy editor runs IAM Access Analyzer checks as you type: syntax errors, invalid actions, and security warnings such as a pass-role wildcard. From the terminal:
aws accessanalyzer validate-policy \
--policy-type IDENTITY_POLICY \
--policy-document file://lantern-upload-policy.json \
--query "findings[].[findingType,issueCode]" --output tableAn empty result means no findings. Fix every ERROR and SECURITY_WARNING, and read each WARNING and SUGGESTION.
You can now write tight permissions. Next: how people get them without IAM users, and how code gets them without access keys.
Key takeaways
- A statement is Effect plus Action plus Resource, optionally narrowed by a Condition; resource-based policies also name a Principal.
- Bucket-level actions such as s3:ListBucket take the bucket ARN, while object-level actions such as s3:GetObject need the bucket ARN followed by /* or a narrower path.
- Least privilege means listing the specific actions and resources a job needs, then widening only when a real AccessDenied tells you to.
- Condition keys are ANDed with each other, and multiple values for one key are ORed.
- Validate every policy before attaching it with IAM Access Analyzer, in the console editor or with aws accessanalyzer validate-policy.