AWS IAM Users, Groups, Roles

Learn AWS IAM users, groups, and roles in this hands-on PythonSkillset tutorial. Understand core concepts, step-by-step usage, troubleshooting, and what to study next.

Focus: understand aws iam users, groups and roles

Sponsored

You've spun up an EC2 instance, deployed a Python app to Lambda, and maybe even pushed a few objects to S3. Then reality hits: that Admin user you created with your personal email is the only way into your account, or worse, you've been embedding aws_access_key_id and aws_secret_access_key directly into your Python scripts. It works — until the day a key leaks on GitHub or a contractor leaves with the keys to your entire cloud kingdom. That's the pain this lesson solves: understanding AWS IAM users, groups, and roles so you can grant the right permissions to the right principals, and stop living in fear of a security incident.

The problem this lesson solves

When you start with AWS, the default is the root user — full, unrestricted access to everything. Every tutorial tells you to create an IAM user, but few explain why the distinction between users, groups, and roles matters so much. The result? Developers embed long-lived credentials in code, share one set of keys across a team, and have no idea who did what in the console.

The core problem is privilege sprawl. You give everyone AdministratorAccess because it's easy, and then you can't answer simple audit questions: Which user launched that expensive instance? Who deleted that S3 bucket?

IAM solves this by giving you three distinct tools — users, groups, and roles — each designed for a different type of identity. Understanding the difference is the foundation of every secure AWS architecture, whether you're building a two-tier app or a multi-account enterprise.

By the end of this lesson, you'll be able to explain the difference in an interview, and you'll have hands-on experience creating a least-privilege setup using Python and the AWS SDK.

Core concept / mental model

Think of IAM as the security guard of your AWS account. Every request — from the console, CLI, or SDK — is checked against policies to decide "yes" or "no". But the guard needs to know who is asking. That's where identities come in.

  • IAM user — a permanent identity for a person or application. It has long-term credentials (password, access keys).
  • IAM group — a collection of users. Groups make it easy to assign permissions to many people at once. A group is not an identity you can log in as.
  • IAM role — a temporary identity that you assume. Roles have no long-term credentials; instead, AWS issues short-lived session credentials when you assume the role.

Here's a simple analogy:

Imagine a hotel. The users are the guests and staff — each has their own keycard. The groups are like departments (Housekeeping, Front Desk) — when someone joins housekeeping, they automatically get the floor-access permissions. The roles are like a temporary VIP pass — any employee can assume the VIP role for a special event, and the pass expires after a few hours.

Key differences at a glance:

Feature IAM User IAM Group IAM Role
Identity Permanent Not an identity Temporary
Credentials Long-term (password, keys) None Short-term (STS)
Who uses it People, some apps Collections of users Services, cross-account, apps
Login Yes (console/API) No No direct login — assume it
Typical use An individual developer Team access EC2 instance, Lambda, cross-account

This mental model is the key to knowing when to use each. If you remember that roles are for temporary access and users are for permanent identities, you'll make better architectural decisions.

How it works step by step

Let's trace what happens when you — or your Python script — make an AWS API call.

  1. You create an IAM user (e.g., deployer) and attach a policy that allows, say, s3:PutObject on a specific bucket.
  2. Your Python script uses boto3 with the user's access key to sign the request.
  3. AWS IAM receives the request and checks: - Authentication: Are the credentials valid? - Authorization: Does the identity have a policy allowing this action on this resource? - Permissions boundary (optional): Is there an additional limit?
  4. If all checks pass, AWS executes the action; otherwise, you get an AccessDenied error.

For a role, the flow is slightly different:

  1. You create a role with a trust policy that says who can assume it.
  2. You attach a permissions policy that defines what the role can do.
  3. When an EC2 instance starts, it assumes the role via the instance profile. boto3 automatically gets temporary credentials from the instance metadata service.
  4. Your code uses those temporary credentials — no hard-coded keys involved.

This process is the backbone of AWS security. Understanding it means you can reason about permissions instead of guessing.

Hands-on walkthrough

Let's get practical. You'll need:

  • An AWS account (if you're using the root user, create an IAM admin user first)
  • boto3 installed (pip install boto3)
  • AWS CLI configured (aws configure with an access key and secret)

Step 1: Create an IAM user using boto3

First, let's create a user and attach a managed policy. We'll use AmazonS3ReadOnlyAccess to keep it minimal.

import boto3

iam = boto3.client('iam')

# Create the user
response = iam.create_user(UserName='s3-reader')
print(f"Created user: {response['User']['Arn']}")

# Attach a managed policy (read-only on S3)
iam.attach_user_policy(
    UserName='s3-reader',
    PolicyArn='arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess'
)
print("Attached AmazonS3ReadOnlyAccess")

# Create an access key for programmatic access
keys = iam.create_access_key(UserName='s3-reader')
print(f"Access key: {keys['AccessKey']['AccessKeyId']}")
print(f"Secret key: {keys['AccessKey']['SecretAccessKey']}")

Expected output:

Created user: arn:aws:iam::123456789012:user/s3-reader
Attached AmazonS3ReadOnlyAccess
Access key: AKIAXXXXXXXXXXXXXXXX
Secret key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

⚠️ Security note: Never print secrets to logs in real life. This is for demonstration only. Store secrets in AWS Secrets Manager or use roles instead.

Step 2: Test the user's permissions

Now let's use that access key to test what the user can do.

import boto3

# Assume we have the keys from step 1
iam_user_session = boto3.Session(
    aws_access_key_id='AKIAXXXXXXXXXXXXXXXX',
    aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
)

s3 = iam_user_session.client('s3')

# This should work
buckets = s3.list_buckets()
print("Buckets:", [b['Name'] for b in buckets['Buckets']])

# This should fail with AccessDenied
iam = iam_user_session.client('iam')
try:
    iam.list_users()
    print("This should not print")
except Exception as e:
    print(f"Expected error: {e}")

Expected output:

Buckets: ['my-bucket']
Expected error: An error occurred (AccessDenied) when calling the ListUsers operation: User: arn:aws:iam::123456789012:user/s3-reader is not authorized to perform: iam:ListUsers

Step 3: Create a group and add the user

Groups are the best way to manage permissions for multiple users. Let's create a group and add our user.

import boto3

iam = boto3.client('iam')

# Create group
iam.create_group(GroupName='S3Support')

# Attach same policy to the group
iam.attach_group_policy(
    GroupName='S3Support',
    PolicyArn='arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess'
)

# Add user to group
iam.add_user_to_group(GroupName='S3Support', UserName='s3-reader')

# Verify user's effective permissions
response = iam.get_account_authorization_details(Filter=['User'])
print("User's group memberships:", response['UserDetailList'][0]['GroupList'])

Expected output:

User's group memberships: ['S3Support']

Step 4: Create an IAM role for EC2 (no hard-coded keys!)

This is the most powerful pattern — assign a role to an EC2 instance and let boto3 automatically get temporary credentials.

import boto3
import json

iam = boto3.client('iam')

# Trust policy — allows EC2 to assume the role
assume_role_policy = {
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Principal": {"Service": "ec2.amazonaws.com"},
        "Action": "sts:AssumeRole"
    }]
}

# Create role
role = iam.create_role(
    RoleName='EC2-S3ReadOnly',
    AssumeRolePolicyDocument=json.dumps(assume_role_policy)
)
print(f"Create role: {role['Role']['Arn']}")

# Attach S3 read-only policy
iam.attach_role_policy(
    RoleName='EC2-S3ReadOnly',
    PolicyArn='arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess'
)

# Create instance profile and add role
iam.create_instance_profile(InstanceProfileName='EC2-S3ReadOnly-Profile')
iam.add_role_to_instance_profile(
    InstanceProfileName='EC2-S3ReadOnly-Profile',
    RoleName='EC2-S3ReadOnly'
)
print("Role and instance profile ready.")

Now, on an EC2 instance with that role, you can simply do:

import boto3

# No credentials provided — boto3 picks up the role automatically
s3 = boto3.client('s3')
buckets = s3.list_buckets()
print(buckets)

This is the gold standard for cloud security: your code never contains secrets.

Compare options / when to choose what

Now that you've seen all three in action, here's how to decide:

  • IAM users — use for human developers and for long-lived applications that can't use roles (rare). If you have multiple people, put them in groups.
  • IAM groups — always use groups for managing people. Assign permissions to the group, not the user. You avoid the "user-specific drift" that happens when you attach policies individually.
  • IAM roles — use for AWS services (EC2, Lambda, ECS), cross-account access, and any scenario where you want temporary credentials. Roles are also great for federated identity (like SSO).
Scenario Best choice Why
A developer needs console access IAM user Long-term password + MFA
A team of 10 devs needs S3 access IAM group Manage once, apply to many
An EC2 instance needs to read DynamoDB IAM role No hard-coded keys, auto-rotation
A third-party service needs access IAM role (cross-account) Temporary, revocable
A Lambda function needs to write logs IAM role Managed by Lambda service
A CI/CD pipeline (GitHub Actions) IAM role (OIDC) Short-lived credentials, no secrets

Common variations: Some teams use inline policies instead of managed policies, but that makes audit harder. Permissions boundaries can add an extra layer of protection — useful for developers who create other IAM users. Also, IAM Identity Center (successor to AWS SSO) is becoming the default for human access — it creates temporary credentials under the hood.

Troubleshooting & edge cases

You'll hit these issues sooner or later — here's how to fix them.

"An error occurred (AccessDenied) when calling the ListBuckets operation"

Problem: Your role/user doesn't have s3:ListAllMyBuckets permission. Fix: Attach a policy that allows s3:ListAllMyBuckets (included in AmazonS3ReadOnlyAccess). If you're using a custom policy, make sure you've included the right actions.

"The role defined for the function cannot be assumed by Lambda"

Problem: The trust policy doesn't allow the Lambda service principal. Fix: Ensure your role's trust policy has "Principal": {"Service": "lambda.amazonaws.com"}. If you copied a policy, you may have left ec2.amazonaws.com.

"An error occurred (InvalidClientTokenId) when calling the GetUser operation"

Problem: Your access key is wrong or expired. Fix: Double-check the key pair. If you're using a role, make sure your session isn't stale — call sts:AssumeRole again or check your environment variables.

Edge case: Users leaving the group still have permissions

If you attach a policy directly to a user and to a group, the user retains access even after leaving the group. That's why you should always assign permissions at the group level, not to individual users.

Edge case: Roles and permissions boundaries

If a user has a permissions boundary, they can't exceed it, even if they have an admin policy. This is a great guard rail for power users.

Edge case: Root user best practice

Never use the root user for daily tasks. Create an IAM admin user and use roles/SSO for everything else. If you lose the root MFA, you have a serious problem.

What you learned & what's next

You now understand the core of AWS IAM: users are permanent identities for people, groups organize users, and roles provide temporary credentials for services and apps. You can create all three with Python and boto3, and you know the security best practices: least privilege, no hard-coded keys, and groups over individual attachments.

You've met the learning objectives: explaining the core idea of IAM users, groups, and roles, and completing a practical exercise to create them programmatically.

Next up in the track: Create and manage EC2 instances with Python — you'll build on this IAM knowledge to securely launch instances with roles, key pairs, and security groups.

Try this mini-exercise: Create a role for a Lambda function that allows it to read a specific DynamoDB table. Then attach it to a Lambda and verify the function can access the table using only temporary credentials.

Practice recap

Open your Python environment and create a new IAM role for EC2 that allows read-only access to S3. Attach it to an instance profile, launch a simple EC2 instance (or use boto3 to create a placeholder), and run a script that lists S3 buckets using only the instance's temporary credentials. Then try to list IAM users — expect an AccessDenied error. This exercise locks in the difference between roles and users.

Common mistakes

  • Embedding access keys directly in Python code or environment variables that get committed to git — use IAM roles or AWS Secrets Manager instead.
  • Attaching policies directly to individual users instead of using groups, causing permission drift and management chaos when team members change.
  • Setting the trust policy wrong on a role — for example, using ec2.amazonaws.com when you need lambda.amazonaws.com, leading to cryptic 'cannot be assumed' errors.
  • Forgetting that roles are temporary — if you assume a role and keep using the session too long, it expires; always refresh via sts:AssumeRole or boto3 automatic refresh.

Variations

  1. Use AWS IAM Identity Center (SSO) instead of IAM users for human access — it gives short-lived credentials and central control.
  2. Use inline policies instead of managed policies when you need a one-off custom policy, but be aware they're harder to audit and reuse.
  3. Add permissions boundaries to users or roles as an extra safety net, especially if you manage junior developers or third-party integrators.

Real-world use cases

  • A CI/CD pipeline (e.g., GitHub Actions) uses an OIDC role to deploy to AWS without storing any long-lived secrets in the repo.
  • A Python microservice running on AWS Lambda assumes a role that grants access to a specific DynamoDB table, keeping the code credential-free.
  • A startup decentralizes access by organizing employees into groups (Dev, Ops, QA) so new hires automatically get the right S3 and EC2 permissions.

Key takeaways

  • IAM users are permanent identities for people or long-lived apps; IAM groups are containers that simplify permission management.
  • IAM roles provide temporary credentials via STS and are the secure way to grant permissions to AWS services like EC2 and Lambda.
  • Always follow least privilege — start with a minimal policy and expand only when needed.
  • Never hard-code credentials in Python code; use roles, instance profiles, or AWS Secrets Manager.
  • Use groups for human teams to avoid permission drift and make onboarding/offboarding easy.
  • Test permissions with the AWS policy simulator or by running your code with a non-admin user to catch missing actions early.

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.