In this post, I will guide you on how to add an extra layer of security to Terraform when you’re using it on hobby projects with AWS. This is particularly helpful when your AWS API keys are stored in an unencrypted form on your local machine.

My suggested strategy involves creating a specific user with limited access. This user’s only permission will be to assume a role that has more privileges. We will then add a policy to this privileged role, allowing only those users who have successfully authenticated via Multi-Factor Authentication (MFA) to assume it. This approach might sound straightforward, but it’s effective. Here’s how to do it:

Create A User

  1. Create a user

    1. IAM -> Users -> Create user
    2. Add a name, don’t check allow access to aws console
    3. Don’t add any permissions, just click through
  2. Edit the newly created user and add a virtual MFA device:

  3. Click the security credentials tab

  4. Click Assign MFA device, name the device, and choose Virtual (this is important), use your phone to set it up. Remember the ARN, it will look like arn:aws:iam::123:mfa/USER

  5. Click the Access Keys tab, add an API key for Command Line Interface and save it in a secure location.

Create A Role

  1. Create a role that is used to do administrative/power user things on AWS.
    1. Navigate to IAM -> Roles -> Create Role
    2. Add the permissions policies that you want
    3. Add a name and description for the role
    4. Select Custom Trust Policy for the trusted entity type, and choose the json editor
    5. Paste in the following policy, update the user field to match the user that you just created, and add a random secret for the ExternalId:
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "AssumeAdminRolePolicy",
			"Effect": "Allow",
			"Principal": {
				"AWS": "arn:aws:iam::123:user/USER"
			},
			"Action": "sts:AssumeRole",
            "Condition": {
                "StringEquals": {"sts:ExternalId": "a-random-string"},
                "Bool": { "aws:multifactorAuthPresent": true} 
		    }
		}
	]
}

Note: Remember the role ARN, it will look something like arn:aws:iam::123:role/ROLE

Install and configure AWS cli

  1. Install the AWS cli.
  2. Create the ~/.aws directory if it doesn’t exist:
mkdir ~/.aws
  1. Create an empty configuration file:
touch ~/.aws/config
  1. Create an empty credentials file:
touch ~/.aws/credentials
  1. In the config file put the following, with the correct region
[default]
region = us-east-1
output = json
  1. In the credentials file, put the following, with the correct values:
[default]
aws_access_key_id = <YOUR_KEY_ID>
aws_secret_access_key = <YOUR_ACCESS_KEY>

Create A Policy For Your User

  1. Navigate to IAM > Users > (User you created).
  2. Go to the permissions section, and create an inline permissions policy.
  3. Click the “JSON” button and paste the following policy JSON and save:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::123:role/ROLE"
        }
    ]
}

Authenticate Terraform

  1. For this section, we are going to use the AWS Security Token Service (STS) to get some AWS session credentials that have the privileged role attached!
  2. Before running Terraform, obtain session credentials for duration seconds, the serial-number and token-code parameters are important!
aws --profile default sts assume-role \
  --role-arn arn:aws:iam::<account id>:role/terraform-admin \
  --role-session-name TFsession \
  --output text \
  --query "Credentials.[AccessKeyId,SecretAccessKey]" \
  --external-id a-random-string \
  --duration 900 \
  --serial-number arn:aws:iam::<account id>:mfa/terraform-mfa \
  --token-code 12345

This command will output the temporary AccessKeyId, SecretAccessKey, and SessionToken, you can now place these into the shell environment – and run terraform:

export AWS_ACCESS_KEY_ID="new_access_key_id"
export AWS_SECRET_ACCESS_KEY="new_secret_access_key"
export AWS_SESSION_TOKEN="new_session_token"

Make sure that the terraform provider has minimal configuration, you’ll especially need to make sure that it doesn’t specify a profile, otherwise it will interfere with how the credentials are read. This is mine:

provider "aws" {
	region = "us-east-1"
}

I have created a simple script that makes it easier to obtain a session when you want to run terraform:

Important: Please ensure to ‘source’ the script instead of executing it in a regular manner. ‘Sourcing’ the script ensures that the environment variables, which are set by the script, persist even after the script finishes its execution. This is crucial because the Terraform client relies on these environment variables. If you don’t ‘source’ the script, these variables will not be available to the Terraform client once the script execution is completed.

source get_tf_session.sh --aid 123 --eid a-random-string --role terraform-admin --mfa terraform-mfa --code 12345
...
...
terraform plan

Thats it! You should now be able to run terraform commands with the correct permissions from the privileged role, but only if you provide the correct MFA code.