DEV Community

Cover image for Day 16 : Managing IAM Users, Groups, and MFA at Scale with Terraform
Zakariyau Mukhtar
Zakariyau Mukhtar

Posted on

Day 16 : Managing IAM Users, Groups, and MFA at Scale with Terraform

Day 16 of my #30DaysOfAWSTerraform challenge focused on one of the most sensitive parts of cloud infrastructure: identity and access management (IAM).Unlike EC2 or networking, IAM mistakes don’t usually “fail loudly” they create security risks. Today’s goal was to learn how to create, manage, and secure multiple IAM users at scale using Terraform.
This day was relatively straightforward conceptually, but the implementation forced me to think like a real administrator rather than a hobbyist.

What I Built

Using Terraform, I automated the creation of:

  • Multiple IAM users from a CSV file.
  • IAM login profiles with forced password reset.
  • Department-based IAM groups.
  • Conditional group memberships.
  • Custom IAM policies per department.
  • Mandatory MFA enforcement for all users.
  • Account-wide password policy.

All of this was done declaratively and reproducibly.

Creating Multiple IAM Users from CSV:

Instead of hardcoding users, I used a CSV file as the source of truth. Terraform’s csvdecode() function made this clean and scalable.

locals {
  users = csvdecode(file("users.csv"))
}
Enter fullscreen mode Exit fullscreen mode

Each user was created dynamically using for_each:

resource "aws_iam_user" "users" {
  for_each = { for user in local.users : user.first_name => user }
  name = lower("${substr(each.value.first_name, 0, 1)}${each.value.last_name}")
  path = "/users/"

  tags = {
    DisplayName = "${each.value.first_name} ${each.value.last_name}"
    Department  = each.value.department
    JobTitle    = each.value.job_title
    Email       = each.value.email
    Phone       = each.value.phone
  }
}
Enter fullscreen mode Exit fullscreen mode

This approach allowed me to scale user creation without touching Terraform code again adding a user only requires editing the CSV.

Enabling Console Access and Password Policies:

Each user received a login profile with a forced password reset on first login:

resource "aws_iam_user_login_profile" "users" {
  for_each = aws_iam_user.users

  user                    = each.value.name
  password_reset_required = true
}
Enter fullscreen mode Exit fullscreen mode

I also enforced a strong account-wide password policy:

resource "aws_iam_account_password_policy" "strong" {
  minimum_password_length        = 10
  require_lowercase_characters   = true
  require_uppercase_characters   = true
  allow_users_to_change_password = true
  max_password_age               = 90
}
Enter fullscreen mode Exit fullscreen mode

This ensures consistency and security across all users.

IAM Groups and Automatic Membership:

I created four IAM groups:

  • Management.
  • Sales.
  • Accounting.
  • HR. Membership was determined dynamically using tags instead of manual assignments.

Example Management group membership based on job title:

resource "aws_iam_group_membership" "management_members" {
  name  = "management-group-membership"
  group = aws_iam_group.management.name
  users = [
    for user in aws_iam_user.users :
    user.name
    if contains(keys(user.tags), "JobTitle") &&
       can(regex("Manager|CEO", user.tags.JobTitle))
  ]
}
Enter fullscreen mode Exit fullscreen mode

This pattern removes human error and keeps access aligned with organizational roles.

Enforcing MFA for Every User:

One of the most important parts of today was forcing MFA. I created a custom IAM policy that:

  • Allows users to set up their own MFA devices.
  • Denies all AWS actions if MFA is not enabled.
resource "aws_iam_policy" "force_mfa" {
  name = "Force-MFA-Policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "DenyAllExceptMFASetupIfNoMFA"
        Effect = "Deny"
        NotAction = [
          "iam:CreateVirtualMFADevice",
          "iam:EnableMFADevice",
          "iam:GetUser",
          "sts:GetSessionToken",
          "iam:ChangePassword"
        ]
        Resource = "*"
        Condition = {
          BoolIfExists = {
            "aws:MultifactorAuthPresent" = "false"
          }
        }
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

This policy was attached to every user automatically:

resource "aws_iam_user_policy_attachment" "enforce_mfa" {
  for_each   = aws_iam_user.users
  user       = each.value.name
  policy_arn = aws_iam_policy.force_mfa.arn
}
Enter fullscreen mode Exit fullscreen mode

Department-Specific Policies:

Each department received tailored permissions:

  • Management: Broad access, restricted IAM management.
  • Sales: Read-only access.
  • Accounting: Billing and cost management.
  • HR: IAM user and credential management only.

These policies were attached at the group level, not the user level a best practice.

Outputs and Visibility:

I exposed useful outputs for visibility and auditing:

output "group_members" {
  value = {
    Management = length(aws_iam_group_membership.management_members.users)
    Sales      = length(aws_iam_group_membership.sales_members.users)
    Accounting = length(aws_iam_group_membership.accounting_members.users)
    HR         = length(aws_iam_group_membership.hr_members.users)
  }
}
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

Day 16 showed me that IAM is not about permissions it’s about structure. Terraform forces discipline, consistency, and repeatability in access control, which is exactly what real organizations need.

No chaos today just clean, deliberate infrastructure.

On to Day 17.

Top comments (0)