Identity and access management · Intermediate

Least-privilege policies you can test

4 min read

Why it matters

Identity and access management appeared in a fifth of the DevOps job descriptions we read. Broad permissions such as s3:* on * are convenient on day one and dangerous forever after: a leaked key or a bug can then touch everything. Least privilege means granting only the actions, on only the resources, that a job needs.

The concept

An AWS IAM policy is a list of statements. Each statement has:

  • Effect: Allow or Deny.

  • Action: API operations such as s3:GetObject.

  • Resource: ARNs the actions apply to, such as arn:aws:s3:::acme-reports/monthly/*.

  • Optionally Condition: extra requirements, such as a source IP range, MFA or a tag.

Evaluation rules to remember: everything is denied by default; an Allow grants access; an explicit Deny always wins over any Allow.

Note that some actions apply to the bucket (s3:ListBucket on arn:aws:s3:::bucket) and some to objects (s3:GetObject on arn:aws:s3:::bucket/key). Getting this wrong is the most common reason a "correct" policy does not work.

Worked example: a report reader

least_privilege.py
import json

import boto3
from botocore.exceptions import ClientError
from moto import mock_aws
from moto.core import set_initial_no_auth_action_count

REGION = "ap-south-1"

READ_REPORTS_POLICY = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ListReportsBucket",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::acme-reports",
        },
        {
            "Sid": "ReadMonthlyReports",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::acme-reports/monthly/*",
        },
    ],
}


@mock_aws
@set_initial_no_auth_action_count(6)  # the 6 admin setup calls run unchecked; every later call is enforced
def main():
    iam = boto3.client("iam", region_name=REGION)
    s3 = boto3.client("s3", region_name=REGION)
    for bucket in ("acme-reports", "acme-payroll"):
        s3.create_bucket(Bucket=bucket, CreateBucketConfiguration={"LocationConstraint": REGION})
    s3.put_object(Bucket="acme-reports", Key="monthly/2026-08.csv", Body=b"region,revenue\nwest,1200\n")
    iam.create_user(UserName="report-reader")
    iam.put_user_policy(UserName="report-reader", PolicyName="read-reports", PolicyDocument=json.dumps(READ_REPORTS_POLICY))
    key = iam.create_access_key(UserName="report-reader")["AccessKey"]

    reader = boto3.client("s3", region_name=REGION, aws_access_key_id=key["AccessKeyId"], aws_secret_access_key=key["SecretAccessKey"])
    attempts = [
        ("list reports", lambda: reader.list_objects_v2(Bucket="acme-reports")),
        ("read monthly report", lambda: reader.get_object(Bucket="acme-reports", Key="monthly/2026-08.csv")),
        ("overwrite monthly report", lambda: reader.put_object(Bucket="acme-reports", Key="monthly/2026-08.csv", Body=b"x")),
        ("read payroll", lambda: reader.list_objects_v2(Bucket="acme-payroll")),
    ]
    for label, call in attempts:
        try:
            call()
            print(f"{label:26} allowed")
        except ClientError as error:
            print(f"{label:26} denied ({error.response['Error']['Code']})")


main()
shellsession
$ pip install "moto[all]" boto3
$ python3 least_privilege.py
list reports               allowed
read monthly report        allowed
overwrite monthly report   denied (AccessDenied)
read payroll               denied (AccessDenied)

The reader can list and read monthly reports and nothing else: it cannot overwrite a report, and it cannot see the payroll bucket at all. As a check that the policy is really being evaluated, changing s3:GetObject to s3:PutObject in the policy flips the result: reading is then denied and overwriting is allowed.

Common mistakes

  • Wildcards by default. "Action": "*" or "Resource": "*" to get something working, never tightened afterwards.

  • Long-lived access keys for people. Use single sign-on and roles instead; the access key here is only for the demo.

  • Policies attached to individual users instead of groups or roles, which makes reviews impossible.

  • Testing by trying things in production. Test policies in code, as here, or with the provider's policy simulator.

Interview angle

"What is least privilege and how do you apply it?", "why can a user with s3:GetObject not list the bucket?" (listing is a bucket-level action), and "how do you find out which permissions an application really uses?" (access logs such as CloudTrail, or tools that generate policies from observed activity, then tighten).

Practice task

  1. Add a condition so the reader can only read objects when the request comes from your office IP range, and test both cases.

  2. Write a policy for a job that writes to acme-reports/incoming/ but can never read from monthly/, and prove it.

Further reading