Tiexin Guo
Posted on July 8, 2022
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.
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.
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:
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):
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:
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
:
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 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
:
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.
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)
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 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:
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
:
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:
- 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 as well.
Posted on July 8, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.