← Back to DevBytes

IAM: Complete Setup and Configuration Guide

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:

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:

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:

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:

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:

Auditing and Troubleshooting IAM

When access is unexpectedly denied, use these tools systematically:

  1. IAM Policy Simulator: Paste the policy and test a specific action/resource combination. It tells you exactly which statement allowed or denied the request
  2. CloudTrail Event History: Search for the user's access denied events. The error message includes the exact policy and condition that caused the denial
  3. IAM Access Analyzer: Analyzes policies attached to your account and flags those that grant public access or cross-account access to external entities
  4. 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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles