cotton
Cotton / ResourcesWORKED CASE · IAM LEAST PRIVILEGE

Test a least-privilege policy change against workload requirements

Follow a shared reporting role from broad S3 access to a narrower draft. Catch a missing export permission before treating the review as complete.

Reducing a policy to the permissions one job uses can break another job sharing the same role. Start with the complete set of declared workload requirements, then test both the allowed work and the access you want to remove.

Worked example available now. Copy the policies and download the tests below. Cotton’s hosted reviewer and ChatGPT plugin are not yet published; see reviewer availability.

Authored synthetic example. Every policy, resource name, request test, and expected result on this page is fictional. The displayed outcomes are not observed AWS results. No policy is deployed and no application is tested by reading or copying this example.

Two workloads, one permission set

A reporting job reads objects from inputs/ and writes generated files to reports/. An export job reads the reports and writes deliveries to exports/. Both jobs assume the same synthetic IAM role.

The role should retain the union of those requirements. It should no longer read unrelated objects or delete report objects. This example uses specific object requests; bucket listing, encryption-key access, retries, and other application needs would require additional requirements if the real workloads depend on them.

A shared role does not isolate the workloads

Both jobs receive every permission granted to the role. Preserving their combined requirements does not prevent the reporting job from using the export permissions. Workload isolation would require a separate design, such as distinct roles, and its own review.

The current policy permits more than the jobs need

The current identity policy grants s3:* on the synthetic reporting bucket and all its objects. Within this supplied policy, that covers the required reads and writes, but also the unrelated read and object deletion we want denied.

Current policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SharedReportingAccess",
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::cotton-synthetic-reporting",
        "arn:aws:s3:::cotton-synthetic-reporting/*"
      ]
    }
  ]
}

Label this document Current when comparing. Its broad grant is the baseline to narrow, not a recommended starting policy.

Write down the requests that define success

The tests below distinguish a policy that preserves the declared work from one that merely removes permissions. The reporting-role report-read test represents the export job’s read of a generated report. Labels describe the fixture; the IAM evaluator sees the action and resource, not separate workload identities.

Permission tests
{
  "schemaVersion": "1.0.0",
  "policyKind": "iam_identity",
  "permissionTests": [
    {
      "label": "Reporting reads inputs",
      "action": "s3:GetObject",
      "resource": "arn:aws:s3:::cotton-synthetic-reporting/inputs/source.csv",
      "expectedBehavior": "allow"
    },
    {
      "label": "Reporting writes reports",
      "action": "s3:PutObject",
      "resource": "arn:aws:s3:::cotton-synthetic-reporting/reports/daily.csv",
      "expectedBehavior": "allow"
    },
    {
      "label": "Reporting reads reports",
      "action": "s3:GetObject",
      "resource": "arn:aws:s3:::cotton-synthetic-reporting/reports/daily.csv",
      "expectedBehavior": "allow"
    },
    {
      "label": "Exporter writes exports",
      "action": "s3:PutObject",
      "resource": "arn:aws:s3:::cotton-synthetic-reporting/exports/delivery.csv",
      "expectedBehavior": "allow"
    },
    {
      "label": "Unrelated reads stay denied",
      "action": "s3:GetObject",
      "resource": "arn:aws:s3:::cotton-synthetic-reporting/unrelated/private.csv",
      "expectedBehavior": "deny"
    },
    {
      "label": "Object deletion stays denied",
      "action": "s3:DeleteObject",
      "resource": "arn:aws:s3:::cotton-synthetic-reporting/reports/daily.csv",
      "expectedBehavior": "deny"
    }
  ]
}

Download permission tests (JSON)

This file records the six example requests in Cotton AWS Policy Review’s requirements format, with schema version 1.0.0 and policy kind iam_identity. Read or adapt it for your review; it is not an IAM policy or a deployment template.

The first proposal forgets the export write

This draft replaces the broad grant with input reads and report reads and writes. It leaves out s3:PutObject on exports/*. A narrower action list alone would miss the service requirement.

Proposed policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadInputs",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::cotton-synthetic-reporting/inputs/*"
    },
    {
      "Sid": "ReportsOutput",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::cotton-synthetic-reporting/reports/*"
    }
  ]
}

Compare this draft with the current policy and all six request tests. The authored expectation is that the export write becomes implicitly denied within the supplied policy. The actual application effect still requires a live service check; the example does not claim an observed outage.

The corrected draft preserves the declared union

Add a bounded export-write statement. The corrected draft keeps input reads, report reads and writes, and export writes. It does not restore the broad s3:* permission.

Corrected policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadInputs",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::cotton-synthetic-reporting/inputs/*"
    },
    {
      "Sid": "ReportsOutput",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::cotton-synthetic-reporting/reports/*"
    },
    {
      "Sid": "ExportOutput",
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::cotton-synthetic-reporting/exports/*"
    }
  ]
}

The unrelated-read and deletion requests have no matching Allow in this draft. They are expected to be implicitly denied when evaluating this supplied policy alone; no explicit Deny statement has been added. Another applicable grant in a real environment could change effective access.

Compare expectations before running the checks

Scroll across to compare all columns.

Authored expectations for the supplied policies only. AWS execution: not run.
RequestRequiredCurrentFirst proposalCorrected draft
Read input objectAllowAllowAllowAllow
Write report objectAllowAllowAllowAllow
Read report objectAllowAllowAllowAllow
Write export objectAllowAllowImplicit deny: requirement missedAllow
Read unrelated objectDenyAllow: excessive accessImplicit denyImplicit deny
Delete report objectDenyAllow: excessive accessImplicit denyImplicit deny

The corrected draft satisfies the six authored expectations in this example. That does not prove a globally minimal policy, complete application coverage, workload isolation, or effective access in an AWS account.

Use Cotton’s IAM policy review workflow to keep validation findings, comparisons, request tests, and unresolved checks together. When running the checks with AWS, preserve the actual returned evidence and any incomplete checks.

Turn the example into an accountable change review

  1. Confirm the workload inventory. Find every job sharing the role, including infrequent export, recovery, and maintenance work.
  2. Expand the requirements. Add the actual actions, resources, condition values, and negative tests the service owners need. Observed activity can help; an observation window is not a complete specification.
  3. Review the full access context. Account for other applicable identity and resource policies, boundaries, organization and session controls, and encryption permissions. A supplied-policy simulation is a bounded check.
  4. Approve and deploy the exact change. Use your authorized change process, confirm current state, and prepare a separately governed recovery decision.
  5. Verify security and service outcomes. Confirm the unwanted access is denied and both workloads complete their required tasks. Missing evidence or a failed job keeps the remediation open.

If a job returns AccessDenied after deployment, retain the caller, action, resource, error context, and policy revisions. Compare the change with the requirement, then investigate the applicable controls. Restoring broad permissions without review can restore the original exposure.

The AccessDenied review questions and remediation evaluation checklist help carry the policy review into the wider workflow.

Sources and scope

Reviewed 7 October 2026. Cotton authored the example and expected outcomes. The AWS sources below explain policy evaluation, simulator limitations, and how observed activity can inform policy generation. They do not report a test of these fixtures.

DESIGN PARTNER PROGRAM

Bring one permission-change review to Cotton

We’re building the workflow from a finding to a reviewed change and verified outcome. Share the decision your team needs to make, the workload access it must preserve, and the evidence your reviewers require.

Explore a design partnership
Analytics privacy details

Google Analytics receives limited page and consent measurements without analytics cookies before you choose and after you reject optional cookies. Accepting enables analytics cookies plus scrolling, selected link and download clicks, demo interactions and confirmed inquiry submissions. Clicks do not establish that a download completed or a demo action succeeded. Google receives visit, browser, device and network information; cookieless does not mean no data is sent. The website works if you reject optional cookies.

We do not send inquiry contents or account identifiers through our analytics code. Advertising consent stays off. Page addresses omit arbitrary query parameters and fragments; campaign labels may be included to understand traffic sources.

Your choice is saved in this browser for 180 days. Analytics cookies expire after 180 days and can be renewed by later visits. Rejecting optional cookies in Cookie settings clears analytics cookies and stops our detailed events; limited cookieless measurement continues. Changing this choice does not delete information already sent to Google. This browser choice is separate from consent to contact you about an inquiry.

Read how Google uses information from sites that use its services and Google’s privacy policy.