Reading a policy precisely

Lesson 2 of 5 in Policy Evaluation.

SCS-C03 dropped "components and impact of a policy" from its explicit skill list, and it would be a mistake to read that as "policies matter less". The exam still puts JSON in the stem and asks what happens. What changed is the kind of question: not "name the five elements" but "read this and tell me what it permits". Treat the document as something you read for meaning, the way you read code in review.

A statement answers four questions, and a fifth optional one:

  • Effect — Allow or Deny. There is no third value.
  • Principal — who. Required in resource-based policies, forbidden in identity-based policies (the principal is whoever the policy is attached to).
  • Action — what, as service:Operation, with * wildcards permitted.
  • Resource — on what, as ARNs. Required in identity-based policies; in a resource-based policy it is implied by the attachment, though some services still require it explicitly.
  • Condition — under what circumstances. Optional, and where most of the security lives.

Plus Version (always "2012-10-17" — "2008-10-17" is legacy and does not support policy variables) and Sid, a label that does nothing functionally and everything for the person debugging at 3 a.m.

A five-layer stack describing how to read an IAM policy statement, from the outermost framing inward. Bottom: Version and Sid — the policy language version, always 2012-10-17 for anything using policy variables, and a statement label that aids debugging but changes nothing. Second: Effect — Allow or Deny, and the first thing to look at because a Deny short-circuits the entire evaluation chain. Third: Principal, Action, and Resource — who, what, and on what, with NotPrincipal, NotAction, and NotResource as inverted forms that are far broader than they appear. Fourth: wildcards and ARN shape — whether the asterisks and the ARN segments actually narrow anything. Top, emphasized: Condition — the operators and keys that decide whether the statement applies at all, including whether a missing key makes it silently not match.

One statement, read outside-in. The condition is the layer that most often decides the answer — and the one candidates skim. (illustrative — source: IAM User Guide — IAM JSON policy elements reference)

ARNs are where imprecision hides. The shape is arn:partition:service:region:account-id:resource, and three details matter on the exam. The partition is aws commercially, aws-us-gov in GovCloud, aws-cn in China — a policy hard-coding aws does not work in GovCloud. The region and account segments are empty for global resources: an S3 bucket ARN is arn:aws:s3:::bucket-name, with nothing between the colons, and an IAM role is arn:aws:iam::111122223333:role/name with no region. And the bucket and its objects are two different resources — arn:aws:s3:::reports is the bucket (for s3:ListBucket), arn:aws:s3:::reports/* is the objects (for s3:GetObject). A policy listing only the second and wondering why ListBucket fails is a classic.

Policy variables make one statement serve many principals: ${aws:username}, ${aws:userid}, ${aws:PrincipalTag/team}. They substitute from the request context at evaluation time, work in Resource and in condition values, and — importantly — a variable whose key is absent does not match, so the statement fails closed. Default values exist for that case: ${aws:PrincipalTag/team, "none"}. Note that ${aws:username} is empty for role sessions, which is one reason tag-based scoping outlives username-based scoping.

Two more reading habits. A missing Resource in an identity-based policy makes the policy invalid — it will not save. In a resource-based policy it is often implied by the attachment. And multiple statements never conflict in the way people expect: they are evaluated together, Deny beats Allow, and two Allows simply union.

Trap: NotAction + Allow

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "EverythingExceptDelete",
    "Effect": "Allow",
    "NotAction": "s3:DeleteObject",
    "Resource": "*"
  }]
}

Read it literally. This does not mean "all S3 actions except delete". NotAction matches every action in every AWS service that is not s3:DeleteObject — IAM, Organizations, KMS, EC2, everything, including services AWS has not launched yet. Paired with "Resource": "*", it is an administrator policy with one cosmetic hole.

The author almost certainly wanted:

{
  "Effect": "Allow",
  "Action": "s3:*",
  "Resource": "*"
},
{
  "Effect": "Deny",
  "Action": "s3:DeleteObject",
  "Resource": "*"
}

NotAction with Deny is the safe direction — "deny everything except this short list" is how MFA-enforcement and break-glass carve-outs are written. NotAction with Allow is a red flag in a stem.

Trap: NotPrincipal

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "DenyEveryoneButMe",
    "Effect": "Deny",
    "NotPrincipal": { "AWS": "arn:aws:iam::111122223333:role/admin" },
    "Action": "s3:*",
    "Resource": "arn:aws:s3:::secure-bucket/*"
  }]
}

The intent — "only the admin role may touch this bucket" — is reasonable. The mechanism is not. NotPrincipal with Deny is notoriously hard to reason about: the set it matches depends on how principals are compared, and the statement interacts badly with role sessions, service principals, and the account root. AWS recommends not using it.

The clear equivalent is a Deny with a condition on the principal ARN:

{
  "Effect": "Deny",
  "Principal": "*",
  "Action": "s3:*",
  "Resource": "arn:aws:s3:::secure-bucket/*",
  "Condition": {
    "ArnNotEquals": {
      "aws:PrincipalArn": "arn:aws:iam::111122223333:role/admin"
    }
  }
}

Same intent, readable semantics, and it composes with other conditions.

Reading a tight policy well

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListOwnPrefixOnly",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::team-data",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["${aws:PrincipalTag/team}/*"]
        }
      }
    },
    {
      "Sid": "ReadWriteOwnPrefix",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::team-data/${aws:PrincipalTag/team}/*"
    },
    {
      "Sid": "RequireTLS",
      "Effect": "Deny",
      "Action": "s3:*",
      "Resource": ["arn:aws:s3:::team-data", "arn:aws:s3:::team-data/*"],
      "Condition": { "Bool": { "aws:SecureTransport": "false" } }
    }
  ]
}

Four things to notice. Two resources, two statements: the bucket ARN for ListBucket, the object ARN for object actions — the distinction that trips people. s3:prefix scopes the listing so a user cannot enumerate other teams' keys, which object-level permissions alone would not prevent. The policy variable makes one document serve every team, and a principal with no team tag matches nothing — failing closed. And the TLS Deny covers both ARNs, because a statement that omits the bucket ARN leaves bucket-level calls unprotected.

Reading a dangerous policy well

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "LetTheAppWork",
      "Effect": "Allow",
      "Action": ["s3:*", "iam:PassRole", "lambda:*"],
      "Resource": "*"
    }
  ]
}

Every line is a finding.

  • s3:* on * includes s3:PutBucketPolicy and s3:PutBucketAcl — the application can make any bucket in the account public, and can rewrite the policy protecting its own data.
  • iam:PassRole on * is the privilege-escalation workhorse: combined with lambda:CreateFunction, the application passes an administrator role to a function it creates and runs. PassRole should always be scoped to specific role ARNs, ideally with an iam:PassedToService condition.
  • lambda:* includes lambda:AddPermission, so the function's own resource policy can be rewritten to allow anyone to invoke it.

The exam does not usually ask "is this bad". It asks which one action makes escalation possible, and the answer is almost always iam:PassRole — or iam:CreatePolicyVersion, iam:AttachRolePolicy, or iam:UpdateAssumeRolePolicy on an unscoped resource.

Interactive sorting exercise: Each policy fragment is either dangerously broad, correctly scoped, or simply ineffective (it does not do what it looks like). Sort them.

Key terms: NotAction, Policy variable, Condition key

Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.