AWS IAM User Management | Terraform & Boto3 Scripts

Photo by mauro mora on Unsplash

AWS IAM User Management | Terraform & Boto3 Scripts

Overview

Finding the balance between ensuring the security of user identities while providing a self-service user experience takes continuous effort. With the constantly evolving cloud landscape and the security around it, IAM user administration can become a burdensome task. In this post, I'll provide the steps to an approach that attempts to ensure the security of AWS IAM users while streamlining administrative overhead and maintaining a self-service user experience by using a combination of Terraform code and Python Boto3 scripts.

Code Functionality

The solution below is comprised of both Terraform code and Python boto3 scripts creating an efficient method for managing IAM users and their associated login profiles, MFA devices and access keys.

Terraform Code

  • Manages IAM user identities, email address tags and optional custom tags (i.e. Access Key descriptions)

  • aws_iam_user resource block is used with a for_each meta-argument which loops through the map of users assigned to the user_info variable

  • local-exec provisioner is used to call external executable scripts which are uniquely defined based on the creation and destruction of an IAM user

  • create-time provisioner creates the login profile and generates a temporary login password during user creation

  • destroy-time provisioner destroys the login profile, MFA devices and access keys during user destruction

Python Boto3 Scripts

  • Automatically invoked by the local-exec provisioner upon terraform apply and are unique based on the definition set in the when meta-argument

  • These scripts allow users to manage their MFA devices and access keys rather than having these resources checked into the terraform state and managed by an IAM admin

  • Allows IAM admins to run a single command to create or destroy a user rather than running additional commands to invoke scripts to create or destroy user resources such as the login profile, MFA devices and access keys

Code Resources

Link to the Terraform code: https://github.com/jksprattler/aws-security/blob/main/terraform/iam/users.tf

Link to the local-exec provisioner boto3 script that creates the login profile and generates the temp password for the new user: https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_user_password_reset.py

Link to the local-exec provisioner boto3 script that destroys the login profile, MFA devices and access keys for a removed user: https://github.com/jksprattler/aws-security/blob/main/scripts/aws_iam_user_cleanup.py

Link to my blog post on IAM User Password Expiry Notice which is why I have the email tags required in this post on IAM user management: https://blog.jennasrunbooks.com/aws-lambda-function-iam-user-password-expiry-notice-ses-boto3-terraform

Usage

Create IAM User

Creating a new AWS IAM user with the provided terraform configuration requires updating the user_info variable definition by adding a new map object assignment containing the new username and the required corresponding email address key-value tag. The optional user_tags can be disregarded for now - these are only needed if the user decides to apply a description to their access key ID. The need to update these optional tags would be indicated by a future terraform plan output where the key-value tags are missing from the terraform state.

Below is some sample terraform output from creating a new IAM user called "userB":

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_user.this["userB"] will be created
  + resource "aws_iam_user" "this" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "userB"
      + path          = "/"
      + tags          = {
          + "email"            = "userB@jennasrunbooks.com"
          + "environment-type" = "lab"
          + "provisioner"      = "terraform"
          + "repo"             = "aws-security"
          + "resource-owner"   = "aws-landing-zone@jennasrunbooks.com"
        }
      + tags_all      = {
          + "email"            = "userB@jennasrunbooks.com"
          + "environment-type" = "lab"
          + "provisioner"      = "terraform"
          + "repo"             = "aws-security"
          + "resource-owner"   = "aws-landing-zone@jennasrunbooks.com"
        }
      + unique_id     = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_iam_user.this["userB"]: Creating...
aws_iam_user.this["userB"]: Provisioning with 'local-exec'...
aws_iam_user.this["userB"] (local-exec): Executing: ["/bin/sh" "-c" "python ../../scripts/aws/aws_iam_user_password_reset.py profile -u userB"]
aws_iam_user.this["userB"] (local-exec): New login profile has been created for: userB
aws_iam_user.this["userB"] (local-exec): Login with temp password:
aws_iam_user.this["userB"] (local-exec): ,)y3}"fXafTHj=Acei';
aws_iam_user.this["userB"] (local-exec): Password reset will be enforced upon initial login
aws_iam_user.this["userB"]: Creation complete after 1s [id=userB]
Releasing state lock. This may take a few moments...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

As seen in the above output, the local-exec provisioner provides the temporary password to the login profile for the new user based on this line:

aws_iam_user.this["userB"] (local-exec): ,)y3}"fXafTHj=Acei';

This can now be directly provided to the new user so they can log into the AWS Console UI where they'll be forced to reset their password and then proceed to set up their MFA devices and Access keys on their own.

Destroy IAM User

Destroying an AWS IAM user is as simple as deleting their map object assignment values from the user_info variable. Additionally, you'd want to remove the user from any IAM groups however, this post is focused on IAM user management using the aws_iam_user terraform resource block.

Below is some sample terraform output from destroying the "userB" IAM user created in the previous section:

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_iam_user.this["userB"] will be destroyed
  # (because key ["userB"] is not in for_each map)
  - resource "aws_iam_user" "this" {
      - arn           = "arn:aws:iam::<accountID>:user/userB" -> null
      - force_destroy = false -> null
      - id            = "userB" -> null
      - name          = "userB" -> null
      - path          = "/" -> null
      - tags          = {
          - "AKIASRJ6UGTM2MP47QO6" = "testkey2"
          - "AKIASRJ6UGTMV3JU6CU2" = "testkey1"
          - "email"                = "userB@jennasrunbooks.com"
          - "environment-type"     = "lab"
          - "provisioner"          = "terraform"
          - "repo"                 = "aws-security"
          - "resource-owner"       = "aws-landing-zone@jennasrunbooks.com"
        } -> null
      - tags_all      = {
          - "AKIASRJ6UGTM2MP47QO6" = "testkey2"
          - "AKIASRJ6UGTMV3JU6CU2" = "testkey1"
          - "email"                = "userB@jennasrunbooks.com"
          - "environment-type"     = "lab"
          - "provisioner"          = "terraform"
          - "repo"                 = "aws-security"
          - "resource-owner"       = "aws-landing-zone@jennasrunbooks.com"
        } -> null
      - unique_id     = "AIDASRJ6UGTMQUHFQ77G4" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_iam_user.this["userB"]: Destroying... [id=userB]
aws_iam_user.this["userB"]: Provisioning with 'local-exec'...
aws_iam_user.this["userB"] (local-exec): Executing: ["/bin/sh" "-c" "python ../../scripts/aws/aws_iam_user_cleanup.py userB"]
aws_iam_user.this["userB"] (local-exec): Deleting login profile for userB
aws_iam_user.this["userB"] (local-exec): Deleting MFA device for userB: arn:aws:iam::<accountID>:u2f/user/testuser/testfido-I5GXZJQHZL3FXDIXX4
aws_iam_user.this["userB"] (local-exec): Deleted access key: AKIASRJ6UGTMV3JU6CU2 for user: userB
aws_iam_user.this["userB"] (local-exec): Deleted access key: AKIASRJ6UGTM2MP47QO6 for user: userB
aws_iam_user.this["userB"]: Destruction complete after 2s
Releasing state lock. This may take a few moments...

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
💡
The force_destroy option when set to true in the aws_iam_user resource block states: When destroying this user, destroy even if it has non-Terraform-managed IAM access keys, login profile or MFA devices. Without force_destroy a user with non-Terraform-managed access keys and login profile will fail to be destroyed. I tried testing this option by destroying a user with an associated login profile, MFA device and access key while running AWS provider version 5.16.2 and terraform version 1.5.7 and it failed with the following error:

 Error: deleting IAM User (blahblah): DeleteConflict: Cannot delete entity, must delete login profile first.
       status code: 409, request id: ca2c4e6e-620a-4082-9077-04af49b29bcb

Conclusion

By integrating Terraform and Python Boto3 scripts, the above approach attempts to provide an efficient solution for managing AWS IAM users while maintaining a secure yet self-service environment for the end user. Administrative overhead should be reduced since MFA device and access key management remain in the hands of the user and outside of the terraform state. As long as you have good documentation and your users are savvy enough managing MFA devices and access keys should not be an issue for them. The Terraform code also follows the DRY principle to reduce repetitive lines of code with a single aws_iam_user resource block used.