Demo user with AWS-CLI and MFA.

Why?

I was preparing to a demo of AWS features and I was going to use CLI for most AWS interactions. Presented material was spanning across multiple services and involved creation and destruction of resources that incur cost. Some of the actions required a user that is empowered. The broad range of services required a versatile one. Who knows what you may need during a live demo?

For AWS-CLI interactions I used to create an IAM user (at least didn't use root) and invoke aws configure. It would bake that user's key into the filesystem in a plain form. Such set of credentials is long-lived and the permission-set would usually remain fairly static in my case. This method was convenient and authentication was done automatically in a transparent fashion. While I usually go for cattle approach when working with servers, I was definitely petting the IAM primitives.

I had no major reservations about this workflow on my main workstation, but when I started parading around with my laptop, alarm bells started ringing in my head. Even though I have limits set low for the count and size of resources, these credentials would potentially be an attractive target for attackers to intercept. Whoever was to gain access to that key would be able to do much damage to my services and to my wallet.

Multi-factor authentication

MFA is an authentication mechanism requiring the users to provide:

  • something that they know (e.g. password, pin, collection of personal details)
  • something that they have (e.g. physical key, bespoke hardware or software token generator)
  • something that they are (e.g. retina scan, fingerprint, DNA)

I hope you can see the benefit right away.

Simply speaking, MFA adds an extra layer of security. An attacker will not be able to gain illicit access to your account having your key (equivalent of credentials) alone. While compromise is still possible, it now becomes significantly harder for an attacker. It often would also require the attacker to be in proximity of the victim. I presume it is something that many "software" attackers would want to avoid.

To set it up in AWS use IAM service.

AWS documentation serves you all the information needed to create an appropriate policy enforcing MFA and provides guidance on how to attach it to a group. This policy can remain static and linked to multiple groups, even though each group or user within a group may have different effective permission.

Identity and Access Management (IAM) setup

For reference, here is an example of MFA policy fetched from AWS docs (page link above) at the time of writing:

 1{
 2    "Version": "2012-10-17",
 3    "Statement": [
 4        {
 5            "Sid": "AllowAllUsersToListAccounts",
 6            "Effect": "Allow",
 7            "Action": [
 8                "iam:ListAccountAliases",
 9                "iam:ListUsers",
10                "iam:ListVirtualMFADevices",
11                "iam:GetAccountPasswordPolicy",
12                "iam:GetAccountSummary"
13            ],
14            "Resource": "*"
15        },
16        {
17            "Sid": "AllowIndividualUserToSeeAndManageOnlyTheirOwnAccountInformation",
18            "Effect": "Allow",
19            "Action": [
20                "iam:ChangePassword",
21                "iam:CreateAccessKey",
22                "iam:CreateLoginProfile",
23                "iam:DeleteAccessKey",
24                "iam:DeleteLoginProfile",
25                "iam:GetLoginProfile",
26                "iam:ListAccessKeys",
27                "iam:UpdateAccessKey",
28                "iam:UpdateLoginProfile",
29                "iam:ListSigningCertificates",
30                "iam:DeleteSigningCertificate",
31                "iam:UpdateSigningCertificate",
32                "iam:UploadSigningCertificate",
33                "iam:ListSSHPublicKeys",
34                "iam:GetSSHPublicKey",
35                "iam:DeleteSSHPublicKey",
36                "iam:UpdateSSHPublicKey",
37                "iam:UploadSSHPublicKey"
38            ],
39            "Resource": "arn:aws:iam::*:user/${aws:username}"
40        },
41        {
42            "Sid": "AllowIndividualUserToViewAndManageTheirOwnMFA",
43            "Effect": "Allow",
44            "Action": [
45                "iam:CreateVirtualMFADevice",
46                "iam:DeleteVirtualMFADevice",
47                "iam:EnableMFADevice",
48                "iam:ListMFADevices",
49                "iam:ResyncMFADevice"
50            ],
51            "Resource": [
52                "arn:aws:iam::*:mfa/${aws:username}",
53                "arn:aws:iam::*:user/${aws:username}"
54            ]
55        },
56        {
57            "Sid": "AllowIndividualUserToDeactivateOnlyTheirOwnMFAOnlyWhenUsingMFA",
58            "Effect": "Allow",
59            "Action": [
60                "iam:DeactivateMFADevice"
61            ],
62            "Resource": [
63                "arn:aws:iam::*:mfa/${aws:username}",
64                "arn:aws:iam::*:user/${aws:username}"
65            ],
66            "Condition": {
67                "Bool": {
68                    "aws:MultiFactorAuthPresent": "true"
69                }
70            }
71        },
72        {
73            "Sid": "BlockMostAccessUnlessSignedInWithMFA",
74            "Effect": "Deny",
75            "NotAction": [
76                "iam:CreateVirtualMFADevice",
77                "iam:DeleteVirtualMFADevice",
78                "iam:ListVirtualMFADevices",
79                "iam:EnableMFADevice",
80                "iam:ResyncMFADevice",
81                "iam:ListAccountAliases",
82                "iam:ListUsers",
83                "iam:ListSSHPublicKeys",
84                "iam:ListAccessKeys",
85                "iam:ListServiceSpecificCredentials",
86                "iam:ListMFADevices",
87                "iam:GetAccountSummary",
88                "sts:GetSessionToken"
89            ],
90            "Resource": "*",
91            "Condition": {
92                "BoolIfExists": {
93                    "aws:MultiFactorAuthPresent": "false"
94                }
95            }
96        }
97    ]
98}

Elaboration of the statements can be found on the aforementioned site.

You can set it all up via AWS console (instruction on that same site) or AWS-CLI. For AWS-CLI ensure the user you are executing commands with has the following permissions:

  • iam:CreatePolicy
  • iam:CreateGroup
  • iam:CreateUser
  • iam:AddUserToGroup
  • iam:AttachGroupPolicy
  • iam:AttachUserPolicy

and for user cleanup:

  • iam:DetachUserPolicy - if you just want to do that, otherwise simply delete user
  • iam:RemoveUserFromGroup
  • iam:DeleteUser

Save the above json snippet of a policy as 'mfa.json' and run

1# once only
2POLICY_ARN=$(aws iam create-policy --policy-name force-mfa --policy-document file://mfa.json | jq '.Policy.Arn' -r)
3aws iam create-group --group-name disposables
4aws iam attach-group-policy --group-name disposables --policy-arn ${POLICY_ARN}
5
6# for each new user
7aws iam create-user --user-name disposable-demo
8aws iam add-user-to-group --group-name disposables --user-name disposable-demo

POLICY_ARN will contain string in format arn:aws:iam::\<your-acc-no\>:policy/force-mfa. You can also find it in AWS console, in IAM -> Policies -> filter by "Customer managed" -> select policy

Now you have a 'disposable-demo' user, added to a 'disposables' group with policy 'force-mfa', forcing MFA on majority of operations performed by that user. This user can't do anything exciting yet, apart from viewing some basic info and managing its own details (check the AWS site mentioned before).

Let's say your demo involves all things EC2 related. You can now attach AWS managed policy 'AmazonEC2FullAccess' to your user

1aws iam attach-user-policy --user-name disposable-demo --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess

And by doing that user will be able to use EC2 service but will be forced to use MFA.

Once you are done with the user (after the demo?), remove it

1aws iam remove-user-from-group --group-name disposables --user-name disposable-demo
2aws iam delete-user --user-name disposable-demo

In order to use that user from AWS-CLI we need to obtain access key id and secret key and configure our development environment.

That's when I became slightly disappointed with my new shiny toy.

Configure AWS-CLI to use MFA

I was spoiled by the workflow utilising long-lived credentials, which handles authentication for you. After initial and quick setup (e.g. aws configure), you just work with AWS API without a need to pay much attention to what happens behind the scenes.

You may be already somewhat familiar with the AWS'es config and credentials files:

1$ cat ~/.aws/credentials
2[demo-of-ec2-s3-iam]
3aws_access_key_id = <from AWS console>
4aws_secret_access_key = <from AWS console>
5
6[disposable-demo]
7aws_access_key_id = <from AWS console>
8aws_secret_access_key = <from AWS console>
1$ cat ~/.aws/config
2[default]
3region = eu-west-1

Once you start using MFA, you have to incorporate few extra hops into the interaction:

  • generate MFA token every so and often, frequency depending on token longevity (configurable)
  • call STS with the above token to obtain temporary credentials, these are consisting of access key id, secret access key and session token
  • parse the json output of above call and extract all three pieces of information
  • make the temporary credentials available to subsequent AWS invocations (e.g. export AWS_*)

So, quite few steps and had to be scripted. The following script will prompt for MFA, call STS, extract the key information and export it:

 1$ cat set_token.sh
 2# provide your account number here
 3ACC_NO=123456789012
 4
 5# and the token expiry, 1h in this example
 6TOKEN_EXPIRY_SECS=3600
 7
 8# default your user
 9USERNAME=${1:-disposable-demo}
10
11if [ "$0" = "$BASH_SOURCE" ]; then
12  echo "This script needs to be sourced!"
13  exit 1
14fi
15
16read -sp "Enter the MFA token for user '${USERNAME}': " TOKEN
17echo
18export AWS_SESSION_TOKEN_RESPONSE=$(aws sts get-session-token --serial-number "arn:aws:iam::${ACC_NO}:mfa/${USERNAME}" --duration-seconds ${TOKEN_EXPIRY_SECS} --profile ${USERNAME} --token-code ${TOKEN})
19$(echo ${AWS_SESSION_TOKEN_RESPONSE} | jq '.Credentials | "export AWS_ACCESS_KEY_ID=\(.AccessKeyId)\nexport AWS_SECRET_ACCESS_KEY=\(.SecretAccessKey)\nexport AWS_SESSION_TOKEN=\(.SessionToken)"' --raw-output)

Note: In the above snippets I am using the username as profile name as it simplifies the scripts and fits my use case.

and a script to clear the exported key information in one go:

1$ cat unset_token.sh
2if [ "$0" = "$BASH_SOURCE" ]; then
3  echo "This script needs to be sourced!"
4  exit 1
5fi
6
7unset AWS_ACCESS_KEY_ID
8unset AWS_SECRET_ACCESS_KEY
9unset AWS_SESSION_TOKEN

then, whenever token expires you just do:

 1# for default, to request temporary credentials for a default user  
 2. set_token.sh
 3> Enter the MFA token for user 'disposable-demo':
 4> ******
 5
 6# or for specific one
 7. set_token.sh demo-of-ec2-s3-iam
 8> Enter the MFA token for user 'demo-of-ec2-s3-iam':
 9> ******
10
11# just continue to use AWS-CLI
12
13# when done
14. unset_token.sh

and I've found it convenient enough.