Managing AWS IAM With Terraform: Part 1
Covering the basics of managing AWS IAM with Terraform and learn how to delete the Root/User Access key, enforce MFA, customize password policy, and more.
Join the DZone community and get the full member experience.
Join For FreeIn 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 article assumes 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 an Admin Group/User
- How to enforce MFA with Customer-Managed Policy and Policy Condition
- How to customize password policy
1. Delete Access Keys of the Root User
Let's 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, and API Access" section, find the access key, and then, under the "Actions" column, choose "Delete"
For most day-to-day operations, you don't need to use the root user. Admin users are more than enough, which is 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 from this tutorial).
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 block 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.
In the real world, you most likely would have a group of admins instead of 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.
3. Enforcing MFA: Customer-Managed Policy and 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. In most cases, you do not need the inline policy at all. If you are still interested, see this inline policy.
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
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 unbearable if you don't use it regularly.
Sometimes, writing a JSON string and using it to create a policy is easier. 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.
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 and 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
}
IAM Tutorial: Part 2
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 the HashiCorp Vault AWS engine
Published at DZone with permission of Tiexin Guo. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments