Write a least-privilege policy and check it before deploying

Derive actions and resources from the exact task, write a scoped customer managed policy, validate it with IAM Access Analyzer, gate changes in CI with custom policy checks, simulate it, and tighten it later from real usage.

markdownguidev4AX0F-9XHKstage: buildtrust: officialservice: iamservice: access-analyzerinterface: cliinterface: iacleast privilegecustomer managed policyIAM Access Analyzervalidate-policycheck-no-new-accesscheck-access-not-grantedpolicy generationunused accessService Authorization Reference

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

Grant the actions the task performs, on the resources it touches, under the conditions it runs in, and let IAM Access Analyzer check the result before it ships. AWS's own guidance accepts starting broader while exploring, as long as you narrow the policy as the use case settles. Prefer a customer managed policy over an AWS managed one: AWS managed policies are written for every customer and are rarely least privilege for yours.

1. List what the task actually calls

Write down each API operation the code or deployment performs. Then map operations to IAM actions: they are not always one-to-one (an S3 GetObject operation can authorize several actions). The machine-readable service reference gives the mapping where the service supports it:

curl -s https://servicereference.us-east-1.amazonaws.com/v1/<service>/<service>.json \
  | jq '.Operations[] | select(.Name == "<Operation>") | .AuthorizedActions'

The same file gives each action's resource types, ARN formats and condition keys, and flags actions as IsWrite, IsList, IsPermissionManagement or IsTaggingOnly. Treat any IsPermissionManagement action as high risk and include it only when the task needs it.

2. Write the statement narrowly

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "ReadAppConfig",
    "Effect": "Allow",
    "Action": ["<service>:<ActionA>", "<service>:<ActionB>"],
    "Resource": "arn:aws:<service>:<region>:<account-id>:<resource-type>/<name>",
    "Condition": { "<operator>": { "<key>": "<value>" } }
  }]
}
  • Name actions explicitly; avoid <service>:* except while exploring.
  • Use full resource ARNs from the service reference's ARN formats. Bucket-level and object-level S3 ARNs are different resources.
  • Add conditions that hold for every legitimate request (organization, TLS, endpoint). Check the traps first: a condition on a key the request lacks makes the Allow silently not apply; ForAllValues passes when the key is missing (reference/iam/policy-grammar-and-conditions.md).
  • Each customer managed policy is limited to 6,144 characters, white space not counted. Split by responsibility if you approach it.

3. Validate the grammar and the risks

Access Analyzer policy validation checks a policy against the policy grammar and AWS best practices. Findings come in four types: ERROR (part of the policy does not work), SECURITY_WARNING (access AWS considers overly permissive), WARNING (not following best practice) and SUGGESTION (style only). Each finding has an issueCode, a message, a link and the locations in the policy.

aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY \
  --policy-document file://policy.json

(CLI: aws accessanalyzer validate-policy --policy-type IDENTITY_POLICY --policy-document file://policy.json; the operation is ValidatePolicy in the Access Analyzer API model.) Fix every ERROR and SECURITY_WARNING before deploying.

4. Gate changes in CI with custom policy checks

Two checks are worth running on every pull request that changes a policy. They are charged per request.

# Fails if the new version grants anything the deployed version did not
aws accessanalyzer check-no-new-access --policy-type IDENTITY_POLICY \
  --existing-policy-document file://deployed.json --new-policy-document file://policy.json

# Fails if the policy grants any action on your critical list
aws accessanalyzer check-access-not-granted --policy-type IDENTITY_POLICY \
  --policy-document file://policy.json --access actions=iam:PassRole,iam:CreateUser

A failing check returns result: FAIL with a message and reasons that point to the statement index. A third check, Check No Public Access, tests whether a resource policy grants public access.

5. Simulate against the real principal

aws iam simulate-principal-policy --policy-source-arn <role or user ARN> \
  --action-names <service>:<ActionA> --resource-arns <resource ARN>

Expect allowed for every action the task needs, and spot-check that a neighbouring action you did not grant comes back implicitDeny. Remember the simulator does not evaluate RCPs and does not fetch resource policies for you.

6. Tighten from real usage

After the workload has run for a representative period:

  • Policy generation: Access Analyzer can generate a policy from the CloudTrail activity of a role or user over a date range. Test the generated policy before replacing the current one.
  • Unused access findings: report unused roles, unused keys and passwords, and unused services and actions on active roles. Charged per role and user analyzed, per analyzer, per month; one analyzer covers all Regions for this finding type.
  • Last accessed information in IAM shows which permissions are no longer used.

Then remove what was never used and run steps 3–5 again.

Read this document as markdown — the same bytes an agent receives.