Automating AWS IAM User Management with Terraform

A guide to quickly create and manage IAM users and roles in AWS with Terraform

Last modified: 30 July 2023

This post is part of a series of Terraform posts related to making AWS deployment less cumbersome and more efficient. In this post, we’ll dive into the process of automating part of the IAM user management using Terraform.

AWS IAM (Identity and Access Management) is a service that enables you to manage access to AWS resources securely. Creating and managing IAM users and roles through the AWS management portal can be a time-consuming process, especially when you have a large number of users to manage. Terraform can help you automate this process with the “write-once-reuse-many” principle.

Infrastructure as code is becoming increasingly popular, and Terraform is a popular tool for implementing it. In this post, we’ll show you how to use Terraform to create IAM users, associate a user profile with a generated password, create access keys, and attach security policies to the user.

Before we get started, here’s what you’ll need:

  1. Terraform
  2. Keybase (for user password encryption)
  3. AWS account

The post will not go into details on what is what, so let’s dive into the practical part.

  1. Create new IAM users
  2. Associate a user profile with a generated password
  3. Create access keys
  4. Attach security policies to the user

Project structure

The folder structure will be as follow:

  • main.tf (main login)
  • policies.tf (define the user policies)
  • output.tf (define the outputs)
  • variables.tf (store the user variables)

Getting started with Terraform AWS Provider

To get started with Terraform AWS Provider, include the necessary module in the main.tf file as shown below:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

In the variables.tf file, firstly define the AWS provider with its zone, then define the variables. You can define a list of users to be created as follows:

provider "aws" {
  region = "eu-north-1"
}

variable "username_list" {
  description = "List of equal users to create"
  type        = map(any)
  default = {
    mark = {
      access_key_status     = "Active"
    },
    john = {
      access_key_status     = "Active"
    }
  }  
}

In this example, we’ve defined two dictionaries containing the user’s name and the access key status that will later define whether the key should be enabled or not.

Next, we’ll jump back to the main.tf file to define the three main resources needed to create IAM users: aws_iam_user, aws_iam_access_key, and aws_iam_user_login_profile. If you do not want a web console login, then skip the login profile section.

These can be defined as follow, note some string variables can be defined and added in the variables.tf

resource "aws_iam_user" "new_user" {
  for_each = var.username_list
  name = each.key
  path = "/${var.project_name}/"
  force_destroy = true
}

resource "aws_iam_access_key" "new_user_access_key" {
  for_each = var.username_list
  user = each.key
  pgp_key = "keybase:${var.deployer_keybase}"
  
  # change the variables file to disallow a user's access key
  status = each.value.access_key_status
}

resource "aws_iam_user_login_profile" "new_user_login_profile" {
  for_each = var.username_list
  user = each.key
  password_length = 40
  pgp_key = "keybase:${var.deployer_keybase}"  
}

This is all for the main.tf. Awesome isn’t it?

Now some explanation.

  • The for_each Terraform construct will create a loop and replicate the same instructions for each element contained in the value assigned. In this case, it will scroll through the username_list and replicate the same resource.
  • key and value are the two attribute of the for_each construct.
  • pgp_key contains a reference to a Keybase user in the form “keybase:username”. This will instruct Terraform to fetch the public PGP key of said user and use that key to encrypt the secret generated at runtime. This way, multiple users can be created and their secrets get automatically encrypted with their PGP public key.
  • status in the aws_iam_access_key define if the access key associated to the user is enabled or not.

How to attach policies to a user

In the policies.tf we can define IAM user policies and attach them to each user.

This policy example allows the user to perform S3 operations (ListBucket, GetObject, and PutObject) on the “example-bucket” S3 bucket and its objects, but denies all S3 operations on objects not located in the “logs” directory within the bucket.

resource "aws_iam_user_policy" "policy_s3_example" {
  for_each = var.username_list
  user = each.key
  name = "s3-example"
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::example-bucket",
                "arn:aws:s3:::example-bucket/*"
            ]
        },
        {
            "Effect": "Deny",
            "Action": "s3:*",
            "NotResource": [
                "arn:aws:s3:::example-bucket/logs/*"
            ]
        }
    ]
}
EOF
}

The Terraform output file

The output.tf defines the Terraform outputs. These can be either through console, or i.e. to file.

In this example we want to print out the generated users, and create a file for each user generated containing the credentials information.

To simplify this task, a Template is created. A template file will be fed into Terraform, which in turn will populate it with the variables defined and spit out the final file.

This is for example, a template file named user_creds.tmpl.

Username: ${username}
PGP Password: ${password}
AccessKeyID: ${access_key_id}
PGP SecretAccessKey: ${secret_access_key}
AWS Login URL: ${aws_login_url}

The ${username} format string in the template will be populated with the username variable.

This is the content of output.tf

output "users_created" {
  value = [
    for new_u in aws_iam_user.new_user : new_u.name
  ]
}

resource "local_file" "user_creds" {
    for_each = aws_iam_user.new_user
    filename = "output/user_creds_${each.value.name}.conf"

    content = templatefile("templates/user_creds.tmpl", {
        username = each.value.name,
        password = aws_iam_user_login_profile.new_user_login_profile[each.key].encrypted_password,
        access_key_id = aws_iam_access_key.new_user_access_key[each.key].id,
        secret_access_key = aws_iam_access_key.new_user_access_key[each.key].secret,
        aws_login_url = var.aws_login_url
    })
    file_permission = "0644"
}

This is all!

How to run the Terraform code

We are now ready to run the Terraform code.

  1. First, initialize the working directory by running terraform init in the same directory as the Terraform configuration file.
  2. Then, review the changes that Terraform will make by running terraform plan. This command will create an execution plan that shows the changes that Terraform will make to the infrastructure.
  3. If the execution plan looks correct, we can apply the changes by running terraform apply.

Note that some of the resources in our Terraform code will create IAM users, which may incur charges on your AWS account.

If a user’s access key needs to be disabled, just change its value in the variables.tf file and terraform apply to make it happen.

Conclusion

In this post, we have shown how to use Terraform to automate the creation and management of IAM users in AWS. By defining the user variables and their policies in the Terraform configuration files, we can quickly create and manage multiple IAM users with ease.

IAM user management can be quite time-consuming when done manually through the AWS management portal. By using Terraform, we can reduce the time and effort required to manage IAM users in AWS.

I hope you found this post helpful. If you have any questions or feedback, feel free to leave a comment below.