DevTools Hub

Search tools

Search for a developer tool

IAM Policy Examples

Part of the AWS Toolkit

IAM Policy Basics covers the pieces a policy is built from. This is ten real, working shapes built from those pieces — the patterns that cover most of what an actual policy needs to do, each with the specific detail that's easy to get wrong. Every condition-key example here matches AWS's own documented patterns.

1. Read-only access to one S3 bucket

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::my-bucket"
    },
    {
      "Sid": "GetObjects",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

Two statements, two different resource shapes, and that's not optional: s3:ListBucket is a bucket-level operation and takes the bucket's own ARN (no trailing /*), while s3:GetObject operates on the objects inside it and needs the wildcarded object path. Give ListBucket the /* form by copy-paste habit and it silently never matches — a very common way a "read-only bucket access" policy quietly can't list what's in the bucket.

2. Full administrator access

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    }
  ]
}

This is the entire content of AWS's own AdministratorAccess managed policy. It's a legitimate starting point for a brand-new account or a genuinely break-glass role — AWS's own guidance is that starting broad and narrowing later is expected, not wrong. See Least Privilege Explained for what narrowing this down actually looks like once real usage tells you what's needed.

3. An explicit deny that overrides a broader allow

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAllS3",
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "*"
    },
    {
      "Sid": "DenyBucketDeletion",
      "Effect": "Deny",
      "Action": "s3:DeleteBucket",
      "Resource": "*"
    }
  ]
}

Statement order doesn't matter here — AWS evaluates every statement and looks for any applicable deny before anything else, so this identity gets every S3 action except deleting a bucket, regardless of which statement comes first in the array. See IAM Policy Simulator to test this exact pattern against a real action and resource.

4. Require MFA for a specific destructive action

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAllEC2",
      "Effect": "Allow",
      "Action": "ec2:*",
      "Resource": "*"
    },
    {
      "Sid": "DenyStopAndTerminateWithoutMFA",
      "Effect": "Deny",
      "Action": [
        "ec2:StopInstances",
        "ec2:TerminateInstances"
      ],
      "Resource": "*",
      "Condition": {
        "BoolIfExists": { "aws:MultiFactorAuthPresent": false }
      }
    }
  ]
}

The operator is BoolIfExists, not Bool — worth getting right on purpose. aws:MultiFactorAuthPresent doesn't exist in the request context at all for a session that never used MFA, and a plain Bool check can't evaluate a key that isn't there. BoolIfExists is built for exactly this: treat a missing key as if the check still applies, rather than silently skipping the condition because the key was never present to compare against.

5. Restrict access by source IP

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": "*",
      "Condition": {
        "IpAddress": { "aws:SourceIp": "203.0.113.0/24" }
      }
    }
  ]
}

aws:SourceIp takes a CIDR block, not a bare address — a single IP still needs the /32 suffix. Worth knowing before relying on this for anything strict: requests routed through a NAT gateway, VPN, or corporate proxy will show that infrastructure's IP, not the original client's.

6. Deny access outside a set of allowed Regions

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyOutsideAllowedRegions",
      "Effect": "Deny",
      "NotAction": [
        "iam:*",
        "route53:*",
        "cloudfront:*",
        "support:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["us-east-1", "us-west-2"]
        }
      }
    }
  ]
}

The NotAction list matters as much as the condition: IAM, Route 53, CloudFront, and Support are global services with no per-Region concept, so a strict region-lock without this exclusion list can lock an identity out of managing its own access. This policy only denies — pair it with a separate allow policy that actually grants the actions you want available inside the allowed Regions.

7. Attribute-based access control: match a resource's tag to the caller's own tag

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "dynamodb:PutItem",
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/project": "${aws:PrincipalTag/project}"
        }
      }
    }
  ]
}

The ${aws:PrincipalTag/project} syntax is a policy variable — it's replaced at request time with the value of the calling principal's own project tag, then compared against the resource's project tag. One policy statement now works correctly for every project team without editing it per-team, as long as both the identities and the resources are tagged consistently — the entire point of attribute-based access control over hardcoding a list of ARNs. Policy variables need the current 2012-10-17 version; they silently don't work under the legacy 2008-10-17 one. See Enforcing Least Privilege at Scale for why this pattern matters beyond convenience — it's what keeps a policy correct as new resources get created, instead of needing an edit every time one does.

8. Allow assuming a role — from both sides

Identity-based policy, attached to the user or role that needs to assume it:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "arn:aws:iam::123456789012:role/deployment-role"
    }
  ]
}

Trust policy, attached to the role itself — without this half, the identity-based half above does nothing:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:user/deployer" },
      "Action": "sts:AssumeRole"
    }
  ]
}

Role trust policies are one of the two documented exceptions to the usual "either side can grant" rule for resource-based policies — a trust policy has to explicitly name the principal, full stop, regardless of how permissive the identity-based side is. See How AWS Evaluates Multiple IAM Policies for the other exception (KMS key policies) and the rest of what happens once more than one policy is in play.

9. Deny deleting anything tagged as protected

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": [
        "ec2:TerminateInstances",
        "rds:DeleteDBInstance",
        "s3:DeleteBucket"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": { "aws:ResourceTag/protected": "true" }
      }
    }
  ]
}

Tag a handful of resources protected: true and this statement blocks deletion across all of them at once, regardless of what any other attached policy allows — an explicit deny always wins. The tradeoff: this only works for services and actions whose deletion API actually evaluates aws:ResourceTag, which isn't universal — check the specific service's condition-key reference before relying on it as the only safeguard.

10. Resource-based: an S3 bucket policy granting another account read access

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::999988887777:root" },
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::shared-reports",
        "arn:aws:s3:::shared-reports/*"
      ]
    }
  ]
}

Attached directly to the bucket, not to any identity — this is a resource-based policy, so it needs a Principal element the way the identity-based examples above never do. Even with this in place, the requesting account still needs its own identity-based policy allowing the same actions on the same ARN — a cross-account request only succeeds if both sides independently allow it. See How AWS Evaluates Multiple IAM Policies for exactly how that two-sided evaluation works.

Try it yourself

Paste any of these into IAM Policy Viewer for a plain-English breakdown, or IAM Policy Simulator to test a real action and resource against them — the deny-overrides-allow and MFA examples above are worth trying there directly. IAM Policy Generator builds a single statement like these from plain fields instead of hand-written JSON. All three run entirely in your browser.

Related tools