Secure an AWS account by requiring MFA on all console and API calls
This post shows how to configure and deploy an IAM policy that will require MFA on all console logins and API calls.
Prerequisites
- Terraform installed on your workstation..
- AWS CLI installed on your workstation and familiarity with how to setup named profiles.
Procedure
-
Create an AWS profile on your workstation named
something-long-term
with your IAM user access and secret keys.Your
.aws/credentials
file should look like this:> cat ~/.aws/credentials
[something-long-term] aws_access_key_id = AKIAXXXXXXXXXXXX aws_secret_access_key = XXXXXXXXXXXXXXXXXXXXX
-
Test the credentials by attempting to list IAM users with the AWS CLI. You should find the cloud_user:
> aws iam list-users --profile something-long-term
{ "Users": [ { "UserName": "cloud_user", "PasswordLastUsed": "2019-04-14T18:13:04Z", "CreateDate": "2019-04-14T16:00:21Z", "UserId": "XXXXXXXXXXXXXXXXXXXXX", "Path": "/", "Arn": "arn:aws:iam::123456789000:user/cloud_user" } ] }
-
Create a directory for your Terraform project.
-
Create a file named
terraform.tf
and configure the AWS provider to use your profile credentials and regionus-east-1
.terraform.tf:
provider "aws" { profile = "something-long-term" region = "us-east-1" }
-
Execute
terraform init
in your directory. This will download the AWS provider plugin in your project. -
You should now be able to execute
terraform plan
and get a response that says:No changes. Infrastructure is up-to-date.
-
Create
iam-groups.tf
and add one group named require-mfa:iam-groups.tf:
resource "aws_iam_group" "require_mfa" { name = "require-mfa" }
-
Create
iam-policy-mfa.tf
.This policy allows IAM users to create an MFA device on their first login in the console. Once the MFA is configured, they can then self-manage their credentials: console password, access keys, MFAs.
Note that some statements target different types of resources:
arn:aws:iam::*:mfa/$${aws:username}
: for the MFA devicearn:aws:iam::*:user/$${aws:username}
: for the user
Terraform interpolation are wrapped in
${}
. This is why a double dollar sign is required to escape one of them and render${aws:username}
in the actual IAM policy.The last statement is a little bit confusing. It will allow a user to perform a set of actions only if
aws:MultiFactorAuthPresent
is set tofalse
. Theaws:MultiFactorAuthPresent
key is never present when an API or CLI command is called with long-term credentials, such as standard access key pairs. This means that it would only be possible for a user to perform those allowed actions if they logged in on the console and don’t already have an MFA device configured. As soon as there’s an MFA device configured, the console login will request a valid MFA authentication before providing access.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
data "aws_iam_policy_document" "iam_selfservice" { statement { sid = "AllowViewAccountInfo" effect = "Allow" actions = [ "iam:GetAccountPasswordPolicy", "iam:GetAccountSummary", "iam:ListVirtualMFADevices", ] resources = [ "*", ] } statement { sid = "AllowManageOwnPasswords" effect = "Allow" actions = [ "iam:ChangePassword", "iam:GetUser", ] resources = [ "arn:aws:iam::*:mfa/$${aws:username}", ] } statement { sid = "AllowManageOwnAccessKeys" effect = "Allow" actions = [ "iam:CreateAccessKey", "iam:DeleteAccessKey", "iam:ListAccessKeys", "iam:UpdateAccessKey", ] resources = [ "arn:aws:iam::*:user/$${aws:username}", ] } statement { sid = "AllowManageOwnVirtualMFADevice" effect = "Allow" actions = [ "iam:CreateVirtualMFADevice", "iam:DeleteVirtualMFADevice", ] resources = [ "arn:aws:iam::*:mfa/$${aws:username}", ] } statement { sid = "AllowManageOwnUserMFA" effect = "Allow" actions = [ "iam:DeactivateMFADevice", "iam:EnableMFADevice", "iam:ListMFADevices", "iam:ResyncMFADevice", ] resources = [ "arn:aws:iam::*:user/$${aws:username}", ] } 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" "iam_selfservice" { name = "selfservice" policy = "${data.aws_iam_policy_document.iam_selfservice.json}" } resource "aws_iam_group_policy_attachment" "iam_selfservice" { group = "${aws_iam_group.require_mfa.name}" policy_arn = "${aws_iam_policy.iam_selfservice.arn}" }
-
Apply the changes by executing
terraform apply
. It should create the group, the policy and attach the policy to the group. -
Create an MFA device on your IAM user
cloud-user
. -
Logout the AWS console and login again. It should prompt you for your MFA code.
-
Go back under your
cloud-user
IAM user configuration and add yourself to therequire-mfa
group. Note that this will require MFA on all console and API requests made thereafter. -
Attempt to list users again using long-term credentials. It should be unauthorized given the new policy requires MFA.
> aws iam list-users --profile something-long-term
-
Read the section below on MFA usage in AWS. Configure and install
aws-mfa
. Use it to generate temporary credentials. You should then be able to list users using thesomething
profile that contains temporary credentials with MFA authentication (as opposed to thesomething-long-term
profile that uses long-term credentials):> aws iam list-users --profile something
From now on if you want to use Terraform with those temporary credentials, you’ll need to adjust
terraform.tf
to use thesomething
profile.
MFA usage in AWS
Console
You will automatically be prompted for your MFA after entering your username and password when logging in the AWS console.
AWS CLI and API calls
Once you configure a policy that requires MFA on your IAM users, any AWS CLI or API call made with their long-term access key will be unauthorized.
Your users need to request temporary credentials by calling STS GetSessionToken.
You can do it manually with the AWS CLI like this. The serial number parameter is the ARN of the MFA device configured on your IAM user. The token code is the value provided by the MFA device.
aws sts get-session-token
--serial-number arn:aws:iam::123456789000:mfa/cloud_user
--token-code 983106
--profile something-long-term
{
"Credentials": {
"SecretAccessKey": "XXXXXXXXXXXXXXXXXXXXX",
"SessionToken": "XXXXXXXXXXXXXXXXXXXXX",
"Expiration": "2019-04-15T06:54:09Z",
"AccessKeyId": "XXXXXXXXXXXXXXXXXXXXX"
}
}
You can use the supplied temporary credentials until their expiration.
This is a very cumbersome process when you need to frequently interact with the AWS APIs using either the AWS CLI or direct API requests.
While you could build your own scripts to populate your .aws/credentials
file with the temporary credentials automatically, there’s this open source project called aws-mfa
that provides this functionality. Please note that you should always carefully review third-party code before using it. This project is fortunately quite small and easy to understand if you know Python.
Here’s an example of .aws/credentials
ready to be used with AWS-MFA. You should append -long-term
to the name of your AWS profiles that contain long-term credentials so that the tool detects them automatically. You should also add the MFA serial number (ARN) as a paramter called aws_mfa_device
:
|
|
Once this is in place, you can run:
> aws-mfa --profile something
|
|
The tool will use the long-term credentials present in your .aws/credentials
along with the MFA device serial number and your MFA code to craft the GetSessionToken call to AWS, grab the response, and configure it under the base name of your profile. So in my case my long-term profile was something-long-term
so it populated temporary credentials in .aws/credentials
:
|
|
At this point I can now use the something
profile, which has temporary credentials that were MFA authenticated (aws:MultiFactorAuthPresent
set to true
).