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
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/*. - Effect —
AlloworDeny(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:*; grants3:GetObjectands3:ListBucketonly. - 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) boto3installed (pip install boto3)- An S3 bucket named
my-data-bucketand a DynamoDB tablemy-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.ListBucketneeds the bucket ARN;GetObjectneeds 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:Decrypton that key. This is a classic miss.
You added a wildcard to save time
s3:Get*is not least privilege. It grantsGetBucketLocation,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:ListBucketon the bucket ARN.GetObjectdoesn't implyListBucket.
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
boto3simulator. - Troubleshoot common
AccessDeniedcauses.
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 likes3:GetObject— this grants unintended permissions likes3:DeleteBucket. - Using
Resource: "*"when you know the specific bucket ARN — this allows access to all buckets in the account. - Forgetting
s3:ListBucketfor operations likelist_objects_v2— you get a confusing 403. - Not adding
kms:Decryptwhen 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
- Use AWS managed policies as a starting point, then trim them down to custom least-privilege policies after observing usage.
- Use IAM permission boundaries to cap the maximum permissions a role can have, even if a broader policy is attached.
- 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_policybefore 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.