Identity and Access Management (IAM): Complete Setup and Configuration Guide
Identity and Access Management (IAM) is the security backbone of modern cloud infrastructure. It governs who can access what resources and how they can interact with them. Whether you're managing an AWS environment, a Google Cloud Platform project, or an internal enterprise system, IAM principles remain consistent: authenticate identities and authorize actions. This guide walks you through everything you need to know to design, implement, and maintain a robust IAM configuration from the ground up.
What Exactly Is IAM?
IAM is a framework of policies, roles, users, and groups that collectively define the access control system for your organization's resources. At its core, IAM separates two critical functions:
- Authentication (AuthN): Verifying that an entity is who it claims to be — this is the "identity" part
- Authorization (AuthZ): Determining what an authenticated entity is allowed to do — this is the "access management" part
In cloud platforms like AWS, IAM is a global service that operates at the account level. It is not tied to a specific region. Every action you take — launching an EC2 instance, reading from an S3 bucket, querying a DynamoDB table — flows through an IAM authorization check first.
Why IAM Matters Deeply
IAM is not merely an operational detail; it is the primary line of defense against data breaches, accidental misconfigurations, and insider threats. Here is why investing time in IAM mastery pays dividends:
- Least Privilege Enforcement: IAM allows you to grant exactly the permissions needed — nothing more. This minimizes blast radius if credentials are compromised
- Audit Trail Integrity: Every IAM action is logged (via AWS CloudTrail or equivalent), creating a tamper-proof record of who did what and when
- Compliance Mandates: Regulations like SOC 2, HIPAA, GDPR, and PCI DSS require demonstrable access controls — IAM provides the mechanism to prove compliance
- Automation Safety: CI/CD pipelines and infrastructure-as-code tools need scoped permissions; IAM enables secure automation without handing out root credentials
- Multi-Account Governance: IAM integrates with organization-wide structures to delegate access across development, staging, and production accounts seamlessly
Core IAM Concepts and Entities
Before diving into configuration, you must understand the fundamental building blocks. These entities form the vocabulary of IAM:
Users
A user is an identity representing a person or service that needs long-term access. Users have unique credentials: a password for console access and optionally access keys (access key ID + secret access key) for programmatic CLI/SDK access. In AWS, users are created within an account and can be assigned directly to groups or have policies attached inline.
Groups
Groups are collections of users. You attach policies to a group, and every user in that group inherits those permissions. This eliminates the tedious and error-prone practice of attaching policies to individual users. A well-designed IAM structure typically has zero inline user policies — everything flows through group membership.
Roles
A role is an identity that does not have permanent credentials. Instead, it is assumed by a trusted entity — a user, an AWS service like EC2 or Lambda, or an external SAML/OIDC identity provider. When assumed, the role issues temporary credentials (access key, secret key, session token) that expire automatically. Roles are the cornerstone of secure, temporary access.
Policies
A policy is a JSON document that defines permissions. It states which actions are allowed or denied on which resources, optionally with conditions. Policies come in several flavors:
- Managed Policies: Reusable policies created by AWS (AWS managed) or by you (customer managed). They appear in the Policies list and can be attached to multiple entities
- Inline Policies: Policies embedded directly into a single user, group, or role. They have a 1:1 relationship and are deleted when the entity is deleted
- Session Policies: Passed programmatically when a role is assumed to further restrict permissions for that session only
- Resource-based Policies: Attached to resources (like S3 buckets or SNS topics) to grant cross-account access without creating roles
Understanding Policy Structure
Every IAM policy document follows a consistent JSON structure. Here is the anatomy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"IpAddress": {
"aws:SourceIp": "192.168.1.0/24"
}
}
}
]
}
Breakdown of each element:
- Version: Always "2012-10-17" (or "2008-10-17" for older features). This is the policy language version, not a date you control
- Statement: An array of permission declarations. Each statement is evaluated independently
- Effect: Either "Allow" or "Deny". Explicit Deny always overrides any Allow
- Action: The API operation(s) to allow or deny. Can use wildcards like
s3:*or list specific actions - Resource: The ARN(s) of the resource(s) the action applies to. Wildcards are common here (e.g.,
arn:aws:s3:::my-bucket/*for all objects) - Condition: Optional constraints that must be true for the statement to apply. Conditions can check source IP, time, MFA status, tags, and more
Step-by-Step IAM Configuration
Now let's build a real-world IAM configuration from scratch. We will cover console operations, CLI commands, and infrastructure-as-code approaches so you can choose what fits your workflow.
Step 1: Create a Developer Group with Power User Access
Power users can do everything except manage IAM resources and billing. This is a safe starting point for developers who need broad access but should not modify the account's security posture.
First, create the group using the AWS CLI:
aws iam create-group --group-name Developers
Now attach the AWS-managed policy PowerUserAccess. This policy allows all AWS actions except IAM management and Organizations actions:
aws iam attach-group-policy \
--group-name Developers \
--policy-arn arn:aws:iam::aws:policy/PowerUserAccess
Verify the attachment:
aws iam list-attached-group-policies --group-name Developers
Output should confirm the policy ARN is attached.
Step 2: Create Users and Add Them to the Group
Create individual developer users. Each gets a unique name and, if needed, programmatic access keys:
# Create the user
aws iam create-user --user-name alice.dev
# Add user to the Developers group
aws iam add-user-to-group \
--group-name Developers \
--user-name alice.dev
# Generate access keys for programmatic access
aws iam create-access-key --user-name alice.dev
The output of create-access-key includes the secret access key. This is the only time it is shown. Store it securely — ideally in a secrets manager like AWS Secrets Manager or HashiCorp Vault, never in source code.
For console access, you must also set a password:
aws iam create-login-profile \
--user-name alice.dev \
--password 'TempP@ssw0rd2025!' \
--password-reset-required
The --password-reset-required flag forces the user to change their password on first login, which is a security best practice.
Step 3: Create a Service Role for EC2 Instances
When an EC2 instance needs to interact with other AWS services (read from S3, write to CloudWatch Logs, pull from ECR), it should assume a role — never have credentials baked into the instance. Here is how to create a role that EC2 can assume:
First, define the trust policy — this tells IAM which service is allowed to assume the role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Save this as ec2-trust-policy.json and create the role:
aws iam create-role \
--role-name MyApp-EC2-Role \
--assume-role-policy-document file://ec2-trust-policy.json
Now attach a permissions policy that grants the specific access needed. For example, allowing read access to a specific S3 bucket and write access to CloudWatch Logs:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::app-assets-prod",
"arn:aws:s3:::app-assets-prod/*"
]
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
Save as ec2-permissions.json and attach:
aws iam put-role-policy \
--role-name MyApp-EC2-Role \
--policy-name S3-Logs-Access \
--policy-document file://ec2-permissions.json
Now when launching an EC2 instance, specify this instance profile:
aws ec2 run-instances \
--image-id ami-0abcdef1234567890 \
--instance-type t3.medium \
--iam-instance-profile Name=MyApp-EC2-Role \
--key-name my-key-pair \
--security-group-ids sg-12345678 \
--subnet-id subnet-12345678
The instance will automatically receive temporary credentials via the instance metadata service (IMDS). Applications running on the instance can retrieve them without any manual configuration.
Step 4: Cross-Account Role for Delegated Access
A common enterprise pattern: a central logging account needs to read from S3 buckets in multiple workload accounts. Instead of duplicating users, you create a role in the workload account that the logging account can assume.
In the workload account (account ID 111122223333), create a trust policy that trusts the logging account:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::444455556666:root"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "unique-external-id-here"
}
}
}
]
}
The ExternalId condition adds a layer of security — the assuming account must pass this exact value, preventing the confused deputy problem. Create the role:
aws iam create-role \
--role-name CrossAccountLogger \
--assume-role-policy-document file://trust-logging-account.json
Attach the necessary S3 read policy:
aws iam attach-role-policy \
--role-name CrossAccountLogger \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
In the logging account (444455556666), create a policy that allows assuming this specific role:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::111122223333:role/CrossAccountLogger"
}
]
}
Attach this policy to the user or role in the logging account that needs to assume the cross-account role. Then, from the logging account, assume it:
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/CrossAccountLogger \
--role-session-name LogCollection \
--external-id unique-external-id-here
The output provides temporary credentials (AccessKeyId, SecretAccessKey, SessionToken) that can be used to make subsequent calls against the workload account's S3 buckets.
Step 5: Implementing Permission Boundaries
Permission boundaries are an advanced safeguard. They define the maximum permissions an entity can have, even if an attached policy tries to grant more. This is essential when you delegate IAM management to others but want to prevent privilege escalation.
Example: You want developers to create their own IAM roles for Lambda functions, but you must ensure they cannot create a role with admin access. Create a permission boundary policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*",
"dynamodb:*",
"sqs:*",
"lambda:*",
"logs:*"
],
"Resource": "*"
},
{
"Effect": "Deny",
"Action": [
"iam:*",
"organizations:*",
"billing:*"
],
"Resource": "*"
}
]
}
Save as developer-boundary.json and create the policy:
aws iam create-policy \
--policy-name DeveloperBoundary \
--policy-document file://developer-boundary.json
Now set this as the permission boundary on the developer's IAM role (the role they use to create resources):
aws iam put-role-permissions-boundary \
--role-name DeveloperRole \
--permissions-boundary-arn arn:aws:iam::123456789012:policy/DeveloperBoundary
Even if someone attaches the AdministratorAccess policy to a role with this boundary, the effective permissions will be the intersection — only the actions allowed by both the boundary and the attached policies. The explicit denies in the boundary prevent IAM modifications.
Step 6: Infrastructure as Code with Terraform
Managing IAM manually via CLI or console does not scale. Infrastructure as Code (IaC) is the professional approach. Here is a Terraform configuration that creates the entire setup described above:
# variables.tf
variable "account_id" {
type = string
default = "123456789012"
}
variable "logging_account_id" {
type = string
default = "444455556666"
}
# iam-groups.tf
resource "aws_iam_group" "developers" {
name = "Developers"
}
resource "aws_iam_group_policy_attachment" "developers_poweruser" {
group = aws_iam_group.developers.name
policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess"
}
# iam-users.tf
resource "aws_iam_user" "alice" {
name = "alice.dev"
tags = {
department = "engineering"
team = "platform"
}
}
resource "aws_iam_user_group_membership" "alice_dev_group" {
user = aws_iam_user.alice.name
groups = [aws_iam_group.developers.name]
}
resource "aws_iam_access_key" "alice_key" {
user = aws_iam_user.alice.name
}
# Output the secret (sensitive — use a secrets backend in production)
output "alice_access_key_id" {
value = aws_iam_access_key.alice_key.id
}
output "alice_secret_access_key" {
value = aws_iam_access_key.alice_key.secret
sensitive = true
}
# iam-roles.tf
resource "aws_iam_role" "ec2_app_role" {
name = "MyApp-EC2-Role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "ec2_app_permissions" {
name = "S3-Logs-Access"
role = aws_iam_role.ec2_app_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::app-assets-prod",
"arn:aws:s3:::app-assets-prod/*"
]
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:*:*"
}
]
})
}
resource "aws_iam_instance_profile" "ec2_app_profile" {
name = "MyApp-EC2-InstanceProfile"
role = aws_iam_role.ec2_app_role.name
}
# Cross-account role
resource "aws_iam_role" "cross_account_logger" {
name = "CrossAccountLogger"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${var.logging_account_id}:root"
}
Action = "sts:AssumeRole"
Condition = {
StringEquals = {
"sts:ExternalId" = "unique-external-id-here"
}
}
}]
})
}
resource "aws_iam_role_policy_attachment" "logger_s3_read" {
role = aws_iam_role.cross_account_logger.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
# Permission boundary
resource "aws_iam_policy" "developer_boundary" {
name = "DeveloperBoundary"
description = "Limits developers from IAM and billing actions"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = ["s3:*", "dynamodb:*", "sqs:*", "lambda:*", "logs:*"]
Resource = "*"
},
{
Effect = "Deny"
Action = ["iam:*", "organizations:*", "billing:*"]
Resource = "*"
}
]
})
}
Run terraform plan and terraform apply to provision everything. The state file should be stored remotely (S3 backend with DynamoDB locking) for team collaboration.
IAM Best Practices
Configuration alone is not enough. Long-term security requires adherence to proven practices:
1. Never Use Root Account for Day-to-Day Operations
The root user has unrestricted access. Create an administrator IAM user with MFA for yourself, lock the root user MFA in a secure physical location (or a secrets manager with strict access), and only use root for the few tasks that require it (like changing account email or enabling certain AWS features). Set up CloudWatch alarms that trigger on root login attempts.
2. Enforce Multi-Factor Authentication Everywhere
MFA should be non-negotiable for all human users. Use IAM policy conditions to enforce it:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
Attach this as a managed policy to all user groups. It denies every action unless MFA is present in the session context. Exceptions can be carved out for service accounts that use access keys (which cannot use MFA) by adding a condition on aws:ViaAWSService.
3. Rotate Credentials Regularly
Access keys should be rotated on a schedule. Automate this with Lambda functions that check key age via iam:ListAccessKeys and iam:GetAccessKeyLastUsed, then notify or force rotation. For roles, temporary credentials expire automatically — this is why roles are preferred over long-lived access keys.
4. Use Roles for Everything Programmatic
EC2 instances, Lambda functions, ECS tasks, CodeBuild projects — every compute service should use IAM roles, never hardcoded credentials. The same applies to CI/CD pipelines. If a third-party service needs access, use sts:AssumeRoleWithWebIdentity with OIDC or create a dedicated IAM user with tightly scoped permissions and rotate its keys aggressively.
5. Implement the Principle of Least Privilege Relentlessly
Start with nothing. Grant access incrementally based on observed needs. Use IAM Access Analyzer to identify policies that grant overly broad permissions. Use the IAM Policy Simulator to test whether a specific action would be allowed before deploying. Regularly audit with iam:GetAccountAuthorizationDetails or the IAM Access Advisor to see which services users and roles are actually using, then trim unused permissions.
6. Tag Everything and Use Attribute-Based Access Control (ABAC)
Instead of hardcoding resource ARNs in policies, use conditions based on tags. This scales beautifully. Example policy that allows a user to access only resources tagged with their department:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*",
"Condition": {
"StringEquals": {
"s3:ResourceTag/department": "${aws:PrincipalTag/department}"
}
}
}
]
}
The user's principal tag department (set via SAML assertion or directly on the user) is matched against the resource tag. No policy updates needed when new resources are created — just ensure they have the correct tags.
7. Monitor and Alert on IAM Changes
Set up CloudTrail with a trail that logs all management events. Create EventBridge rules that trigger on sensitive IAM API calls:
{
"source": ["aws.iam"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": [
"CreateUser",
"DeleteUser",
"CreateAccessKey",
"DeleteAccessKey",
"PutRolePolicy",
"AttachRolePolicy",
"CreatePolicy",
"DeletePolicy",
"UpdateAssumeRolePolicy"
]
}
}
Route these events to an SNS topic that pages your security team. Consider using AWS Config rules to detect policy drift and non-compliant IAM configurations.
8. Use Multiple Accounts from Day One
A single AWS account is a single blast radius. Use AWS Organizations to create separate accounts for production, staging, development, and sandbox environments. Apply Service Control Policies (SCPs) at the organization level to enforce guardrails — like denying the creation of public S3 buckets or requiring encryption at rest — across all accounts. Use IAM roles for cross-account access rather than creating duplicate users everywhere.
Common IAM Pitfalls and How to Avoid Them
Even experienced teams stumble. Here are patterns to watch for:
- Wildcard resources in sensitive actions:
"Resource": "*"combined with"Action": "iam:*"is essentially admin access. Always scope resources to specific ARNs or use conditions - Not setting permission boundaries on developer roles: If developers can create IAM roles (common for Lambda or ECS), they can escalate privileges unless a boundary caps their maximum permissions
- Leaking credentials in code repositories: Use
git-secretsortrufflehogpre-commit hooks. Better yet, never generate access keys for human users — use SAML/OIDC federation with temporary credentials - Confusing resource-based policies with identity-based policies: An S3 bucket policy controls access to the bucket itself. An IAM user policy controls what that user can do. Both must allow for access to be granted. The effective permissions are the union of both, minus any explicit denies
- Over-trusting the
Principalfield in trust policies: When creating roles assumable by other accounts, always addExternalIdorSourceArnconditions to prevent cross-account confused deputy attacks
Auditing and Troubleshooting IAM
When access is unexpectedly denied, use these tools systematically:
- IAM Policy Simulator: Paste the policy and test a specific action/resource combination. It tells you exactly which statement allowed or denied the request
- CloudTrail Event History: Search for the user's access denied events. The error message includes the exact policy and condition that caused the denial
- IAM Access Analyzer: Analyzes policies attached to your account and flags those that grant public access or cross-account access to external entities
- Access Advisor: Shows the last time a user or role used each service. Use this data to right-size permissions
For programmatic debugging, use the iam:SimulatePrincipalPolicy API:
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:user/alice.dev \
--action-names s3:PutObject \
--resource-arns arn:aws:s3:::my-bucket/example.txt
The response includes the evaluation decision and the specific statement that caused it.
Conclusion
IAM is not a one-time setup task — it is a living discipline that evolves with your infrastructure. A well-configured IAM system is the difference between a contained incident and a catastrophic breach. Start with the fundamentals: separate human users from service identities, enforce MFA universally, adopt roles as the default credential mechanism, and implement permission boundaries as safety nets. Build your IAM configuration as code so it is versioned, reviewed, and auditable. Regularly audit with IAM Access Analyzer and Access Advisor to prune unnecessary permissions. The investment you make today in understanding and hardening IAM will protect every resource you deploy tomorrow, next month, and years into the future. Security is not a product — it is a process, and IAM is its foundation.