DEV Community

ontrack
ontrack

Posted on

Automating AWS IAM with Python

Hello There , here some docs about Automating AWS IAM with Python: Practical Patterns for Safer Access

Practical IAM automation patterns using Boto3, role design, and least-privilege principles.

Managing AWS IAM manually works when you have a few resources and a small team.

Then the environment grows.

A few roles become dozens. Temporary access becomes permanent. Someone creates a policy with * because "it was only for testing." Another person copies an existing role and changes two permissions. Eventually, nobody is completely sure which role exists for which workload.

That is where IAM automation becomes useful.

Python and Boto3 can turn IAM from something we click through in the AWS Console into something we can define, review, reproduce, and automate.

The goal isn't simply to automate IAM API calls.

The goal is to make access predictable and intentionally limited.


Why automate IAM?

IAM is configuration, but it is also security configuration.

That makes manual changes particularly uncomfortable.

Imagine deploying three environments:

development
staging
production
Enter fullscreen mode Exit fullscreen mode

Each environment has applications that need access to different AWS resources.

Without automation, you might end up manually creating:

app-dev-role
app-staging-role
app-production-role
Enter fullscreen mode Exit fullscreen mode

Then attaching policies to each one.

The problem isn't creating the roles.

The problem is keeping them consistent.

Automation gives us a repeatable process:

Python configuration
        ↓
Validate desired access
        ↓
Create/update IAM role
        ↓
Create/update policy
        ↓
Attach policy
        ↓
Verify configuration
Enter fullscreen mode Exit fullscreen mode

Now IAM changes can be reviewed like code.


The IAM model we actually need

Before writing Python, it helps to separate two concepts that are often confused.

Trust policy

The trust policy answers:

Who is allowed to assume this role?

For example, an EC2 instance, Lambda function, or another AWS account might be allowed to assume a role.

Permissions policy

The permissions policy answers:

What can the role do after it is assumed?

For example:

s3:GetObject
s3:ListBucket
Enter fullscreen mode Exit fullscreen mode

These are different responsibilities.

A role can have the correct permissions but an overly broad trust policy.

It can also have a tightly restricted trust policy but excessive permissions.

Good IAM design requires both sides to be intentional.


Setting up Boto3

Install Boto3:

pip install boto3
Enter fullscreen mode Exit fullscreen mode

Then create an IAM client:

import boto3

iam = boto3.client("iam")
Enter fullscreen mode Exit fullscreen mode

Boto3 uses the AWS credential/provider chain rather than requiring credentials to be hard-coded into the Python script.

For example, you can work with a configured AWS profile:

import boto3

session = boto3.Session(profile_name="dev")
iam = session.client("iam")
Enter fullscreen mode Exit fullscreen mode

Avoid putting access keys directly into source code.


Pattern 1: Define policies as Python data

Instead of scattering IAM JSON throughout the program, define policies as Python dictionaries.

For example:

backup_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": "arn:aws:s3:::company-backups"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::company-backups/*"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

This gives us something important:

IAM configuration becomes data.

Once permissions are represented as data, we can validate them, test them, generate them, and store them in Git.


Pattern 2: Create the role separately from its permissions

Let's create a role for our application.

First, define the trust relationship:

import json
import boto3

iam = boto3.client("iam")

trust_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

response = iam.create_role(
    RoleName="backup-reader",
    AssumeRolePolicyDocument=json.dumps(trust_policy),
    Description="Role used by the backup application"
)

print(response["Role"]["Arn"])
Enter fullscreen mode Exit fullscreen mode

The important part is:

"Principal": {
    "Service": "ec2.amazonaws.com"
}
Enter fullscreen mode Exit fullscreen mode

We are saying that EC2 is allowed to assume the role.

This does not mean the EC2 workload automatically has access to S3.

The role still needs permissions.


Pattern 3: Attach only the permissions the application needs

Now we can create a managed policy.

policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": "arn:aws:s3:::company-backups"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::company-backups/*"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Create the policy:

response = iam.create_policy(
    PolicyName="BackupReaderPolicy",
    PolicyDocument=json.dumps(policy),
    Description="Read-only access to the company backup bucket"
)

policy_arn = response["Policy"]["Arn"]
Enter fullscreen mode Exit fullscreen mode

Then attach it:

iam.attach_role_policy(
    RoleName="backup-reader",
    PolicyArn=policy_arn
)
Enter fullscreen mode Exit fullscreen mode

A complete small example

Putting the pieces together:

import json
import boto3

ROLE_NAME = "backup-reader"
POLICY_NAME = "BackupReaderPolicy"

iam = boto3.client("iam")

trust_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ec2.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

permissions_policy = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::company-backups"
        },
        {
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::company-backups/*"
        }
    ]
}

role = iam.create_role(
    RoleName=ROLE_NAME,
    AssumeRolePolicyDocument=json.dumps(trust_policy),
    Description="Read-only backup access"
)

policy = iam.create_policy(
    PolicyName=POLICY_NAME,
    PolicyDocument=json.dumps(permissions_policy),
    Description="Read-only access to backup objects"
)

iam.attach_role_policy(
    RoleName=ROLE_NAME,
    PolicyArn=policy["Policy"]["Arn"]
)

print(f"Created role: {role['Role']['Arn']}")
print(f"Created policy: {policy['Policy']['Arn']}")
Enter fullscreen mode Exit fullscreen mode

This works as a demonstration, but I would not stop here for production.

Why?

Because running it twice can fail when the role or policy already exists.

That brings us to a more interesting problem.


IAM automation should be idempotent

A good automation script should be safe to run repeatedly.

Consider:

python iam_setup.py
Enter fullscreen mode Exit fullscreen mode

The first run creates everything.

What should happen on the second run?

Ideally:

Role already exists
        ↓
Check its configuration
        ↓
Policy already exists
        ↓
Check/update its version
        ↓
Verify attachment
        ↓
Done
Enter fullscreen mode Exit fullscreen mode

Not:

EntityAlreadyExistsException
Enter fullscreen mode Exit fullscreen mode

This is one of the biggest differences between a quick automation script and a useful infrastructure tool.

A simple existence check can be implemented like this:

def role_exists(iam, role_name):
    try:
        iam.get_role(RoleName=role_name)
        return True
    except iam.exceptions.NoSuchEntityException:
        return False
Enter fullscreen mode Exit fullscreen mode

Then:

if role_exists(iam, ROLE_NAME):
    print(f"{ROLE_NAME} already exists")
else:
    iam.create_role(
        RoleName=ROLE_NAME,
        AssumeRolePolicyDocument=json.dumps(trust_policy)
    )
Enter fullscreen mode Exit fullscreen mode

This pattern can be extended to policies, attachments, tags, and trust relationships.


Role design: one role, one responsibility

A useful rule for IAM design is:

A role should represent a workload or responsibility, not a collection of unrelated permissions.

For example, avoid creating:

ApplicationEverythingRole
Enter fullscreen mode Exit fullscreen mode

with:

S3
DynamoDB
EC2
RDS
IAM
Lambda
CloudWatch
Secrets Manager
Enter fullscreen mode Exit fullscreen mode

Instead, consider roles based on actual workload responsibilities:

orders-api-role
backup-reader-role
monitoring-role
deployment-role
Enter fullscreen mode Exit fullscreen mode

Then permissions follow the workload.

For example:

orders-api-role
    ├── DynamoDB access
    ├── SQS access
    └── CloudWatch logging

backup-reader-role
    └── S3 read access

monitoring-role
    └── CloudWatch read access
Enter fullscreen mode Exit fullscreen mode

This makes access easier to understand and audit.


Don't confuse "working" with "least privilege"

This is probably the most important lesson in IAM automation.

Suppose your application receives:

AccessDenied
Enter fullscreen mode Exit fullscreen mode

The fastest solution is tempting:

"Action": "*",
"Resource": "*"
Enter fullscreen mode Exit fullscreen mode

The application works.

But the security model is now much worse.

Instead, identify the exact API call that failed.

If the application needs:

s3:GetObject
Enter fullscreen mode Exit fullscreen mode

give it:

"Action": "s3:GetObject"
Enter fullscreen mode Exit fullscreen mode

not:

"Action": "s3:*"
Enter fullscreen mode Exit fullscreen mode

And if possible, restrict the resource:

"Resource": "arn:aws:s3:::company-backups/*"
Enter fullscreen mode Exit fullscreen mode

rather than:

"Resource": "*"
Enter fullscreen mode Exit fullscreen mode

Least privilege is not about making policies tiny for the sake of being tiny.

It is about making the permissions match the actual job.


Use conditions when they make sense

Actions and resources are only part of the IAM policy.

Conditions can provide another layer of control.

For example:

"Condition": {
    "StringEquals": {
        "aws:RequestedRegion": "ap-northeast-2"
    }
}
Enter fullscreen mode Exit fullscreen mode

Now the permission can be constrained by context.

Conditions can be useful for things such as:

source VPC endpoints
requested regions
resource tags
principal tags
MFA requirements
request attributes
Enter fullscreen mode Exit fullscreen mode

The exact condition should depend on the threat model and workload.

The important idea is:

Don't automatically treat every permission as unconditional.


Tags make automated IAM easier to manage

If you create IAM resources programmatically, tag them.

For example:

tags = [
    {
        "Key": "ManagedBy",
        "Value": "Python"
    },
    {
        "Key": "Environment",
        "Value": "Production"
    },
    {
        "Key": "Application",
        "Value": "Backup"
    }
]
Enter fullscreen mode Exit fullscreen mode

Then:

iam.create_policy(
    PolicyName=POLICY_NAME,
    PolicyDocument=json.dumps(permissions_policy),
    Description="Read-only backup access",
    Tags=tags
)
Enter fullscreen mode Exit fullscreen mode

Now you can answer questions such as:

Which IAM resources are managed automatically?
Which application owns this policy?
Which environment is this role for?
Enter fullscreen mode Exit fullscreen mode

A better architecture: configuration first

For larger projects, I wouldn't hard-code every role inside Python.

Instead, imagine a configuration file:

roles:

  backup-reader:
    trusted_service: ec2.amazonaws.com

    permissions:
      - actions:
          - s3:GetObject
        resources:
          - arn:aws:s3:::company-backups/*

      - actions:
          - s3:ListBucket
        resources:
          - arn:aws:s3:::company-backups
Enter fullscreen mode Exit fullscreen mode

Python becomes the engine that translates this configuration into AWS resources.

The architecture becomes:

YAML / JSON
     ↓
Validation
     ↓
Python
     ↓
Boto3
     ↓
AWS IAM
Enter fullscreen mode Exit fullscreen mode

Now changing access doesn't necessarily mean changing application logic.

You change the desired configuration.


Add a "plan" mode before making changes

This is another pattern I strongly recommend.

Before changing IAM, show what the script intends to do.

For example:

IAM PLAN

Role:
  backup-reader

Trust:
  ec2.amazonaws.com

Permissions:
  s3:GetObject
  s3:ListBucket

Resources:
  arn:aws:s3:::company-backups/*
  arn:aws:s3:::company-backups
Enter fullscreen mode Exit fullscreen mode

Then require an explicit flag:

python iam.py --plan
Enter fullscreen mode Exit fullscreen mode

versus:

python iam.py --apply
Enter fullscreen mode Exit fullscreen mode

This gives you a chance to catch mistakes before they become AWS changes.

It also makes the tool much easier to integrate into CI/CD.


Treat IAM changes like code

Once IAM is automated, put the configuration in Git.

For example:

iam-automation/
├── policies/
│   ├── backup-reader.json
│   └── monitoring.json
├── roles/
│   ├── backup-reader.json
│   └── monitoring.json
├── iam.py
├── requirements.txt
└── README.md
Enter fullscreen mode Exit fullscreen mode

Now a permission change can go through:

Developer
   ↓
Git commit
   ↓
Pull request
   ↓
Review
   ↓
Validation
   ↓
CI/CD
   ↓
AWS
Enter fullscreen mode Exit fullscreen mode

That is a major improvement over:

Someone opens AWS Console
        ↓
Clicks around
        ↓
Changes a policy
        ↓
Nobody knows why
Enter fullscreen mode Exit fullscreen mode

Validate before deploying

Automation can make bad changes very quickly.

So automation needs guardrails.

AWS IAM Access Analyzer can validate IAM policies and help identify overly permissive access.

A useful pipeline could therefore look like:

Policy definition
       ↓
JSON/schema validation
       ↓
IAM policy validation
       ↓
Security review
       ↓
Plan
       ↓
Apply
       ↓
Verification
Enter fullscreen mode Exit fullscreen mode

The Python script shouldn't be the only safety mechanism.


Don't create long-lived credentials for automation

Another important distinction:

Automating IAM doesn't mean creating access keys everywhere.

For AWS workloads, prefer temporary credentials and IAM roles where possible.

For example:

EC2
  ↓
IAM Role
  ↓
Temporary credentials
  ↓
AWS API
Enter fullscreen mode Exit fullscreen mode

rather than:

EC2
  ↓
Hard-coded access key
  ↓
AWS API
Enter fullscreen mode Exit fullscreen mode

This reduces the number of secrets you have to manage.


What I would automate first

If you're building your first IAM automation project, don't try to automate the entire AWS account on day one.

Start small.

I'd automate these operations first:

1. Create role
2. Configure trust policy
3. Create customer-managed policy
4. Attach policy
5. Add tags
6. Verify configuration
7. Detect drift
8. Produce a plan before changes
Enter fullscreen mode Exit fullscreen mode

Then add:

9. Policy version management
10. Access review
11. Unused permission detection
12. CI/CD integration
13. Multi-account support
Enter fullscreen mode Exit fullscreen mode

This progression keeps the project understandable while giving you room to grow it.


A practical mental model

When designing IAM automation, I use five questions.

1. Who?

Who should be able to assume this role?

EC2?
Lambda?
Another AWS account?
Human identity?
CI/CD system?
Enter fullscreen mode Exit fullscreen mode

2. What?

What actions does the workload actually perform?

s3:GetObject?
dynamodb:GetItem?
logs:PutLogEvents?
Enter fullscreen mode Exit fullscreen mode

3. Where?

Which resources should those actions apply to?

Specific bucket?
Specific table?
Specific log group?
Enter fullscreen mode Exit fullscreen mode

4. Under what conditions?

Can the permission be restricted further?

Region?
Tags?
Network path?
MFA?
Principal attributes?
Enter fullscreen mode Exit fullscreen mode

5. How do we prove it?

Can we validate and monitor the access?

Policy validation
Access Analyzer
CloudTrail
Code review
Automated tests
Enter fullscreen mode Exit fullscreen mode

If you can answer those five questions, your IAM design is usually heading in the right direction.


Final thoughts

IAM automation isn't really about Python.

Python is simply the tool that lets us turn our access model into something repeatable.

The bigger idea is:

Manual IAM
    ↓
Defined IAM
    ↓
Automated IAM
    ↓
Validated IAM
    ↓
Auditable IAM
Enter fullscreen mode Exit fullscreen mode

Boto3 gives us the API layer. The important engineering work is deciding what access should exist and why.

Start with the workload.

Define the trust relationship.

Define the minimum permissions.

Restrict the resources.

Add conditions where useful.

Put the configuration in Git.

Validate before applying.

And make the automation safe to run more than once.

The best IAM automation isn't the script that creates the most roles.

It's the one that makes it difficult to accidentally create too much access.


Useful References

AWS IAM — Policies and permissions
AWS IAM — Least-privilege permissions
Boto3 IAM documentation
Boto3 IAM examples

Top comments (0)