Writing IAM policies

Amazon Web Services · Essentials · 14 min

Least privilege is only as good as the policies you write, and most access bugs are policy bugs.

A policy is a JSON document with a Version (always "2012-10-17") and a list of statements. Each statement has:

  • Effect: Allow or Deny.
  • Action: API actions like s3:GetObject; wildcards such as s3:Get* work.
  • Resource: the ARNs the actions apply to.
  • Condition (optional): extra tests on request context keys, such as aws:SourceIp, aws:RequestedRegion, aws:PrincipalTag/team or service keys like s3:prefix.

Identity-based policies attach to users, groups and roles; they have no Principal. Resource-based policies (S3 bucket policies, SQS queue policies, KMS key policies, role trust policies) attach to the resource and must name a Principal.

Policies come as AWS managed, customer managed (reusable, versioned) or inline (embedded in one identity). Prefer customer managed for anything you write yourself.

Evaluation basics: everything starts denied; an applicable Allow permits the action; an explicit Deny anywhere overrides every allow. That makes Deny statements good guardrails.

A classic gotcha is resource granularity. In S3, s3:ListBucket applies to the bucket ARN arn:aws:s3:::bucket, while s3:GetObject applies to objects, arn:aws:s3:::bucket/*. Put the wrong ARN in and you get AccessDenied despite the right action.

Write policies narrow, then use IAM Access Analyzer to validate them and to generate a least-privilege policy from the actions a role actually used in CloudTrail.

Example

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListReportsPrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::acme-data",
      "Condition": {"StringLike": {"s3:prefix": ["reports/*"]}}
    },
    {
      "Sid": "ReadWriteReports",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::acme-data/reports/*"
    },
    {
      "Sid": "NeverDelete",
      "Effect": "Deny",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::acme-data/*"
    }
  ]
}

Remember

  • Statement = Effect + Action + Resource (+ Condition)
  • Resource policies name a Principal; identity policies don't
  • Default deny, allow grants, explicit deny always wins
  • Bucket-level and object-level S3 actions need different ARNs

Watch out for

  • Granting s3:GetObject on arn:aws:s3:::bucket (no /*), which matches no objects.

Check yourself

  1. A role has an Allow for s3:* on a bucket and another attached policy with a Deny for s3:DeleteObject on it. Can it delete objects?
  2. Which policy type must contain a Principal element?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html