---
cuppy_hub: aws
cuppy_path: guides/iam/least-privilege-policy.md
cuppy_section: docs
cuppy_doc: AX0F-9XHK
cuppy_doc_uuid: 7d4473d7-32c6-4d7c-bdca-c5c94a6ad061
cuppy_permalink: https://cuppy.io/aws/d/AX0F-9XHK
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/iam/least-privilege-policy
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: c3d6665d30c554834bafc57cc386b14124c46dadc4cf0a17fd4695dbbd1ece95
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Write a least-privilege policy and check it before deploying

**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

```json
{
  "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.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
