portrait

Tiexin Guo

Senior DevOps Consultant, Amazon Web Services
Author | 4th Coffee

In my previous article 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.

Side 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.

We'll see:

  • Why and how to delete the Root user access key.
  • How to create Admin Group/User
  • How to enforce MFA with Customer-Managed Policy & Policy Condition
  • How to customize password policy

Let'go!

1 Delete Access Keys of the Root User

OK, in fact, we'll start by breaking the rules. How? By using the console. Why? Because we need to 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.
Delete root user access key
Delete root user access key

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.

For most day-to-day operations, you don't need to use the root user: admin users are more than enough.  

That's why you should lock it away by deleting the Access key.  

We should try to avoid the usage of root users unless we absolutely have to. One such corner case would be if you had only one administrator user and that user accidentally removed admin permissions from themselves.  In that case, you'd have to log in with your root user and restore their permissions.

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):

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:

  • create a group intended for administrators
  • read the ARN of the "AdministratorAccess," which is an AWS-managed policy
  • attach the "AdministratorAccess" policy to the group
  • create an admin user
  • add that user to the admin group
  • enable console login for that admin user
  • add the initial password as a sensitive output

If we apply it:

terraform init
terraform apply
terraform output password

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

Do I Need to Use Groups?

Short answer: yes.

Well, technically, if you never need more than 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 non-admin users. With the same method, we can create job function/project/team/etc., dedicated groups that will share the same sets of permissions, which is more secure and easy than user-level permission management.

Sensitive Output

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

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.
  • Store the state remotely to avoid storing it as plain-text JSON files. 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 to know more about encryption at rest and encryption in transit.

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

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:

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.

Customer-Managed Policy

Contrary to the AdministratorAccess policy we used in the previous section (which is an AWS-managed policy), 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.

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 could also:

  • create a policy using JSON strings
  • convert a Terraform expression result to valid JSON syntax (see the example here)

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.

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 interesting advantage of using JSON strings: you can validate JSON policy in the AWS IAM console. See here for more information.

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:

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 strongerpassword_policy.tf:

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:

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:

  • How to centralize IAM to reduce operational overhead with cross-account assume role.
  • How to create an EC2 instance profile.
  • How to implement Just-In-Time access management with HashiCorp Vault AWS engine.
Tutorial: How to Use Terraform to Manage AWS IAM Policies (Part 2)
In this second part, you will learn how to centralize IAM for multiple AWS accounts, create and use EC2 instance profiles, and implement just-in-time access with Vault.

Bonus: 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 as well. Enjoy and see you next time!