Create an IAM Policy with Least Privilege

Create an IAM policy with least privilege — AWS Cloud & DevOps with Python tutorial, lesson 4.

Focus: create an iam policy with least privilege

Sponsored

You've built your first EC2 instance, pushed code to S3, and maybe even triggered a Lambda — but somewhere in the back of your mind, you know the real danger isn't what you deploy, it's who (or what) can touch it. Handing out broad IAM permissions is like giving every employee a master key to the building: convenient until someone walks out with the servers. In this lesson you'll learn how to create an IAM policy with least privilege — the single most important habit for securing any AWS environment. By the end, you'll be able to write policies that grant exactly the permissions needed, no more, no less, and verify them with Python.

The Problem This Lesson Solves

Picture this: you've just launched a Python web app that needs to read a CSV file from S3. You find a quick tutorial that says, "give the role AmazonS3FullAccess," and you paste it in. Done, right? Not quite. That policy grants every S3 action on every bucket in your account — delete, overwrite, and even public-read configuration. One bug or malicious actor later, your data is gone.

The principle of least privilege is AWS's security best practice that says: give an identity only the permissions it needs to perform its job, and nothing more. Broad policies are the #1 cause of security breaches in the cloud. According to AWS, most attacks exploit over-permissioned roles. The cost of a compromise far outweighs the minutes you save by copying a wildcard policy.

This lesson gives you a repeatable process to create an IAM policy with least privilege — from identifying the exact API calls your workload makes, to writing the policy JSON, to testing it in a sandbox. You'll also see how Python and the AWS SDK (boto3) fit into authoring, validating, and simulating policies.

Core Concept / Mental Model

Think of IAM policies as boarding passes. A boarding pass doesn't grant you access to every seat on every flight — it lists your flight, seat, and class. Similarly, an IAM policy grants access to specific actions on specific resources under specific conditions.

Definitions to anchor you:

  • IAM policy — a JSON document that defines permissions. It can be attached to a user, group, or role.
  • Action — the API call you're allowing or denying, like s3:GetObject.
  • Resource — the ARN (Amazon Resource Name) of the thing you're acting on, e.g., arn:aws:s3:::my-bucket/*.
  • EffectAllow or Deny (Deny always wins).
  • Condition — optional qualifiers, like IP address, MFA, or time-of-day.

Here's the mental model in a single line: least privilege = smallest set of actions + smallest set of resources + optional conditions that match exactly what the code does.

The Two Halves of Least Privilege

  • Service-level least privilege: don't grant s3:*; grant s3:GetObject and s3:ListBucket only.
  • Resource-level least privilege: don't use * as the resource; use the specific bucket ARN.

When you combine both, you have a truly least-privilege policy.

How It Works Step by Step

Follow this repeatable process every time you need to create an IAM policy with least privilege.

1. Identify the API Calls Your Workload Makes

The easiest way is to run your application in a sandbox with CloudTrail logging enabled. CloudTrail records every API call, including the action and resource ARN. Do this before you finalize the policy.

Alternatively, you can read your code. For a Python script using boto3, look for calls like s3.get_object() or ec2.describe_instances(). Each maps directly to an IAM action.

2. List the Required Actions and Resources

Create a table. For each API call, note the service, action, and resource ARN. For S3, a get_object call requires s3:GetObject on the bucket ARN with /*. A list_objects_v2 call requires s3:ListBucket on the bucket ARN itself.

API Call (boto3) IAM Action Resource ARN
s3.get_object s3:GetObject arn:aws:s3:::my-bucket/*
s3.list_objects_v2 s3:ListBucket arn:aws:s3:::my-bucket
dynamodb.get_item dynamodb:GetItem table ARN

3. Write the Policy JSON

Start with the Version and Statement keys. Each statement has Effect, Action, Resource, and optionally Condition. Keep it minimal.

4. Validate with the IAM Policy Simulator or Python

Before attaching, test. IAM has a policy simulator in the console, but you can also use boto3's simulate_principal_policy to script it.

5. Attach, Monitor, and Iterate

Attach the policy to the role or user, then monitor CloudTrail for AccessDenied errors. Those errors tell you what you missed — add only the missing action, and repeat.

Hands-On Walkthrough

Let's build a policy for a Python Lambda that reads a CSV from S3 and writes results to DynamoDB. We'll use the CLI and boto3.

Prerequisites

  • AWS CLI configured (aws configure)
  • boto3 installed (pip install boto3)
  • An S3 bucket named my-data-bucket and a DynamoDB table my-results

Step 1: Write the Least-Privilege Policy as JSON

Create a file least-privilege-policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-data-bucket",
        "arn:aws:s3:::my-data-bucket/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "dynamodb:PutItem",
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/my-results"
    }
  ]
}

Step 2: Create the Policy and Attach to a Role

aws iam create-policy --policy-name LeastPrivilegeS3DynamoDB \
  --policy-document file://least-privilege-policy.json

Then attach it to your role:

aws iam attach-role-policy --role-name my-lambda-role \
  --policy-arn arn:aws:iam::123456789012:policy/LeastPrivilegeS3DynamoDB

Step 3: Validate with boto3 (the Python Way)

Here's a Python script that simulates the policy against a list of actions:

import boto3

iam = boto3.client('iam')

# ARN of the role you want to test
role_arn = 'arn:aws:iam::123456789012:role/my-lambda-role'

actions_to_test = [
    's3:GetObject',
    's3:ListBucket',
    's3:PutObject',   # we did NOT grant this on purpose
    'dynamodb:PutItem',
    'dynamodb:DeleteItem'  # not allowed
]

response = iam.simulate_principal_policy(
    PolicySourceArn=role_arn,
    ActionNames=actions_to_test
)

for result in response['EvaluationResults']:
    print(f"{result['EvalActionName']}: {result['EvalDecision']}")

Expected output:

s3:GetObject: allowed
s3:ListBucket: allowed
s3:PutObject: implicitDeny
s3:PutItem: allowed
dynamodb:PutItem: allowed
dynamodb:DeleteItem: implicitDeny

This confirms the policy is correctly scoped.

Step 4: Write a Reusable Policy Generator in Python

To make least privilege a habit, automate it:

import json

def build_least_privilege_policy(actions_resources: dict, conditions: dict = None):
    """Takes {action: [resource_arns]} and returns policy dict."""
    statements = []
    for action, resources in actions_resources.items():
        statement = {
            "Effect": "Allow",
            "Action": action,
            "Resource": resources
        }
        if conditions:
            statement["Condition"] = conditions
        statements.append(statement)
    return {
        "Version": "2012-10-17",
        "Statement": statements
    }

# Example usage
policy = build_least_privilege_policy({
    "s3:GetObject": ["arn:aws:s3:::my-data-bucket/*"],
    "dynamodb:GetItem": ["arn:aws:dynamodb:us-east-1:123456789012:table/my-results"]
})
print(json.dumps(policy, indent=2))

Run it to see the JSON — you can pipe it directly into aws iam create-policy.

Compare Options / When to Choose What

When you need to grant permissions, you have several flavors. Here's how they compare:

Approach Pros Cons Best for
Inline policy Simple, lives with the user/role Hard to reuse, harder to audit One-off test roles
Managed policy (AWS) Prebuilt, easy to attach Often too broad Fast prototyping (then least privilege)
Customer-managed policy Reusable, auditable, can be least privilege You maintain it Production workloads
Policy with conditions Adds MFA/IP/time restrictions More complex High-security environments

When to choose what: - Use customer-managed policies for anything that stays longer than a week. They let you enforce least privilege consistently. - Use inline policies only for quick experiments, and convert them later. - Never use Action: "*" or Resource: "*" in production unless you truly need it (like an admin role).

A common pattern is to start with AWS managed, run in production with CloudTrail, then replace with a custom least-privilege policy after a week of logging.

Troubleshooting & Edge Cases

You get AccessDenied even though the policy looks right

  • Check resource ARN format. For S3, the bucket itself (arn:aws:s3:::bucket) and objects (arn:aws:s3:::bucket/*) are different. ListBucket needs the bucket ARN; GetObject needs the object ARN.
  • Check for explicit deny. Add "Effect": "Deny" to block dangerous actions — it overrides any allow.
  • Check the role's trust policy. The role can't assume if the trust policy is missing.
  • Check for service-level restrictions such as SCPs or permission boundaries.

You allowed s3:GetObject but still get 403

  • If the bucket is encrypted with a customer-managed KMS key, you also need kms:Decrypt on that key. This is a classic miss.

You added a wildcard to save time

  • s3:Get* is not least privilege. It grants GetBucketLocation, GetLifecycleConfiguration, etc. Always list the exact action.

The policy simulator says implicitDeny but you need it

  • Run your app and check CloudTrail for AccessDenied. Add only the missing action, not a wildcard.

Python boto3 gives ClientError: AccessDenied on list_objects_v2

  • You likely forgot s3:ListBucket on the bucket ARN. GetObject doesn't imply ListBucket.

What You Learned & What's Next

You now know how to create an IAM policy with least privilege. Specifically, you can:

  • Explain why broad policies are dangerous.
  • Identify the exact API calls your workload makes.
  • Write a minimal JSON policy with correct resource ARNs.
  • Validate it with the AWS CLI and with Python's boto3 simulator.
  • Troubleshoot common AccessDenied causes.

You've completed the core of IAM. Next, you'll dive deeper into IAM roles for EC2 — how to give EC2 instances temporary credentials without embedding keys. You'll learn about instance profiles and how to use boto3 to assume roles for cross-account access. That's the next step in securing your AWS Python applications.

Pro tip: Make least privilege a habit. Every time you're about to paste *:*, pause and ask, "What's the smallest thing that works?" Your future self — and your security team — will thank you.

Practice recap

Create a least-privilege policy for your own project: write down three API calls your Python app makes, map them to IAM actions, and generate a JSON policy using the build_least_privilege_policy function from this lesson. Then use the AWS CLI to create the policy and attach it to a test role, and run the simulate_principal_policy script to confirm only the intended actions are allowed.

Common mistakes

  • Using Action: "s3:*" instead of listing exact actions like s3:GetObject — this grants unintended permissions like s3:DeleteBucket.
  • Using Resource: "*" when you know the specific bucket ARN — this allows access to all buckets in the account.
  • Forgetting s3:ListBucket for operations like list_objects_v2 — you get a confusing 403.
  • Not adding kms:Decrypt when using S3 with a customer-managed KMS key — your GetObject fails even with S3 allowed.
  • Skipping CloudTrail and guessing permissions — you either over-grant or under-grant; testing in a sandbox is faster in the long run.

Variations

  1. Use AWS managed policies as a starting point, then trim them down to custom least-privilege policies after observing usage.
  2. Use IAM permission boundaries to cap the maximum permissions a role can have, even if a broader policy is attached.
  3. Use the IAM Access Analyzer to generate policies based on CloudTrail activity — it can automatically produce a least-privilege policy for you.

Real-world use cases

  • Give a Lambda function read-only access to a single S3 bucket to process data files.
  • Grant a CI/CD pipeline permission to deploy to only one specific CloudFormation stack, not the whole account.
  • Allow a data scientist to query only a specific DynamoDB table without access to any other table or service.

Key takeaways

  • Least privilege means granting the minimum actions on the minimum resources required for a task.
  • Identify required API calls from CloudTrail logs or your code before writing the policy.
  • Resource ARNs are critical — S3 bucket vs object ARNs are different and both may be needed.
  • Validate policies with the IAM Policy Simulator or boto3.simulate_principal_policy before deployment.
  • Start with AWS managed policies for prototyping, then replace with custom least-privilege policies.
  • Always test in a sandbox and monitor CloudTrail for AccessDenied errors to fine-tune permissions.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.