Managing AWS IAM with Terraform - Part 1
Tags: devops, aws, cloud, security
Published: 2022-07-08 06:59:16

In my previous article [AWS IAM Security Best Practices](https://blog.gitguardian.com/aws-iam-security-best-practices/), we covered a bunch of theoretical best practices on AWS IAM. In this tutorial, we will cover the basics of managing AWS IAM using Terraform.

> Author's note: this blog post assumes that you already understand what Terraform is and know the basics of it. If not, start with a [Terraform official tutorial](https://learn.hashicorp.com/collections/terraform/aws-get-started).

---

## 1 Delete Access Keys of the Root User

The access key for your AWS account root user gives full access to all your resources for all AWS services. You cannot reduce the permissions associated with your AWS account root user access key.

We can use the AWS Management Console to delete access keys for the root user:

- Use your AWS account email address and password to sign in to the AWS Management Console as the AWS account root user.
- Choose your account name in the navigation bar, and then choose "Security credentials".
- Under the "Access keys for CLI, SDK, & API access" section, find the access key, and then, under the "Actions" column, choose Delete.

See the following screenshot:

![delete root user access key](./delete_access_key.png)

There are only a few tasks you would use the root user for; in most cases, you don't need it. One of the times you might need it is if you have only one administrator user and that user accidentally removed admin permissions from themselves, you'd have to log in with your root user and restore their permissions.

For most day-to-day operations, lock your root user away, because admin users are more than enough. We should try to avoid the usage of root users unless we absolutely have to. This best practice follows the "least privilege" principle.

Now, let's see how to create admin users using Terraform:

---

## 2 Create Admin Group/User

Prepare a file  `admin_user_group.tf` with the following content (you can get all the code of this tutorial from [this repo here](https://github.com/IronCore864/terraform-iam-tutorial/tree/part-1)):

```hcl
resource "aws_iam_group" "administrators" {
  name = "Administrators"
  path = "/"
}

data "aws_iam_policy" "administrator_access" {
  name = "AdministratorAccess"
}

resource "aws_iam_group_policy_attachment" "administrators" {
  group      = aws_iam_group.administrators.name
  policy_arn = data.aws_iam_policy.administrator_access.arn
}

resource "aws_iam_user" "administrator" {
  name = "Administrator"
}

resource "aws_iam_user_group_membership" "devstream" {
  user   = aws_iam_user.administrator.name
  groups = [aws_iam_group.administrators.name]
}

resource "aws_iam_user_login_profile" "administrator" {
  user                    = aws_iam_user.administrator.name
  password_reset_required = true
}

output "password" {
  value     = aws_iam_user_login_profile.administrator.password
  sensitive = true
}
```

In the code snippet above, we:

- created a group intended for administrators
- read the ARN of the "AdministratorAccess," which is an AWS-managed policy
- attached the "AdministratorAccess" policy to the group
- created an admin user
- added that user to the admin group
- enabled console login for that admin user
- added the initial password as a sensitive output

If we apply it:

```shell
terraform init
terraform apply
terraform output password
```

We will create all those resources and print out the initial password.

### 2.1 Do I Need to Use Groups

Short answer, yes.

Well, technically, if you only need to create one admin user across your AWS account, you don't have to create an admin group and then put a single user into that group. I mean, you can do it, but maybe it doesn't make much sense to you in the first place.

In the real world, though, you probably would have a group of admins instead of only one, so, the easier way to manage access for all admins is to create groups. Even if you have only one admin user at the moment, you need to bear in mind that your company, team, and project are subject to growth (maybe quicker than you'd imagine,) and although using a group to manage merely one user at the moment can seem redundant, it's a small price to pay to be a bit more future-proof.

The same principle applies to managing other non-admin users. With the same method, we can create a group dedicated to a job function/project/team/etc., and the same group of people will have the same permissions, which is more secure and manageable, compared to managing permission at the user level.

### 2.2 Sensitive Output

In the example above, we have an output marked as `sensitive = true`:

```hcl
output "password" {
  value     = aws_iam_user_login_profile.administrator.password
  sensitive = true
}
```

In Terraform, an output can be marked as containing sensitive material using the optional sensitive argument. Terraform will hide values marked as sensitive in the messages from `terraform plan` and `terraform apply`.

In the above example, our admin user has an output which is their password. By declaring it as sensitive, we won't see the value when we execute `terraform output`. We'd have to specifically ask Terraform to output that variable to see the content (or use the `-json` or `-raw` command-line flag.)

Here are two _best practices_ for managing sensitive data with Terraform:

- If you manage any sensitive data with Terraform (like database passwords, user passwords, or private keys), treat the state itself as sensitive data because they are stored in the state. For resources such as databases, this may contain initial passwords.
- Storing state remotely can provide better security. Because when using local state, the state is stored in plain-text JSON files. However, if we use a remote state, Terraform does not persist state to the local disk. We can even use some backends that can be configured to encrypt the state data at rest. Read [this blog](https://blog.gitguardian.com/talking-about-data-security-an-introduction-to-aws-kms-and-hashicorp-vault/#encryption-at-rest) to know more about encryption at rest and encryption in transit.

---

## 3 Customer-Managed Policy & Policy Condition Use Case: Enforcing MFA

The way to enforce MFA in AWS isn't as straightforward as it can be, but it can be achieved with a single policy:

`enforce_mfa.tf`:

```hcl
data "aws_iam_policy_document" "enforce_mfa" {
  statement {
    sid    = "DenyAllExceptListedIfNoMFA"
    effect = "Deny"
    not_actions = [
      "iam:CreateVirtualMFADevice",
      "iam:EnableMFADevice",
      "iam:GetUser",
      "iam:ListMFADevices",
      "iam:ListVirtualMFADevices",
      "iam:ResyncMFADevice",
      "sts:GetSessionToken"
    ]
    resources = ["*"]
    condition {
      test     = "BoolIfExists"
      variable = "aws:MultiFactorAuthPresent"
      values   = ["false", ]
    }
  }
}

resource "aws_iam_policy" "enforce_mfa" {
  name        = "enforce-to-use-mfa"
  path        = "/"
  description = "Policy to allow MFA management"
  policy      = data.aws_iam_policy_document.enforce_mfa.json
}
```

There are a couple of important things to notice here.

### 3.1 Customer-Managed Policy

Contrary to the `AdministratorAccess` policy (which is an AWS-managed policy) we used in the previous section, here we have defined a "customer-managed policy".

_Note: we should try to use customer-managed policies over inline policies For most cases, you do not need the inline policy at all. If you are still interested, see an [example of an inline policy here](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role#inline_policy)._

### 3.2 Multiple Ways to Create a Policy

There are multiple ways to create a policy, and the example above is only one of them: in this example, we created it using Terraform `data`.

We can also:

- create a policy using JSON strings
- convert a Terraform expression result to valid JSON syntax (see [the example here](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy))

The benefit of using "aws_iam_policy_document" `data` is that the code looks nice and clean because they are Terraform/HashiCorp's HCL syntax. However, it isn't always as straightforward as it seems, and debugging it would be especially painful if you don't use it regularly.

Sometimes, it's easier to write a JSON string and use that to create a policy. However, JSON strings in a Terraform source code file can look a bit weird and not clean (after all, they are multiline strings,) especially when they are of great length.

So, there isn't a one-size-fits-all choice here; you'd have to decide on your own which is best for your use case.

There is, however, an advantage of using JSON strings, because you can validate JSON policy in the AWS IAM console. See [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-policy-validation.html) for more information.

### 3.3 Policy Condition

In this example above, we used a _"policy condition,"_ which only makes the policy effective when there isn't a multi-factor authentication.

This policy loosely translates to: "deny any operation that isn't MFA device-related if you don't have multi-factor authentication."

We can use `aws_iam_group_policy_attachment` to attach it to a group, then all the users in that group are affected. For example:

```hcl
resource "aws_iam_group_policy_attachment" "enforce_mfa" {
  group      = aws_iam_group.administrators.name
  policy_arn = aws_iam_policy.enforce_mfa.arn
}
```

This makes sure the administrator group must enable MFA.

---

## 4 Strong Password Policy & Password Rotation

The AWS default password policy enforces the following:

- Minimum password length of 8 characters and a maximum length of 128 characters
- Minimum of three of the following mix of character types: uppercase, lowercase, numbers, and ! @ # $ % ^ & * ( ) _ + - = [ ] { } | ' symbols
- Not be identical to your AWS account name or email address

We can, however, use a customized and stronger password policy:

`password_policy.tf`:

```hcl
resource "aws_iam_account_password_policy" "strict" {
  minimum_password_length        = 10
  require_uppercase_characters   = true
  require_lowercase_characters   = true
  require_numbers                = true
  require_symbols                = true
  allow_users_to_change_password = true
}
```

By using `aws_iam_account_password_policy`, we can also specify how often the users should change their password, and whether they could reuse their old passwords or not:

```hcl
resource "aws_iam_account_password_policy" "strict" {
  # omitted
  max_password_age               = 90
  password_reuse_prevention      = 3
}
```

---

## Summary

In the second part of the IAM tutorial, we will cover:
- centralized IAM to reduce operational overhead with cross-account assume role
- EC2 instance profile
- Just-In-Time access management with HashiCorp Vault AWS engine

In the meantime, if you are interested in Terraform and want to know more about it, please read my other article [9 Extraordinary Terraform Best Practices That Will Change Your Infra World](https://blog.gitguardian.com/9-extraordinary-terraform-best-practices/) as well.