Securing AWS API Calls & CLI Access with MFA (Two-Factor) Authentication

By: Morten Jensen, Feb 3 2016

One of the largest concerns of allowing AWS API calls to be made from the outside is issuing an API key and secret for developer and administrator PCs and laptops alike because they may be interceptable in one way or another. Some scenarios spring to mind:

Add to that the fact that policies (bundles of permissions) can be hard to formulate and get right; and therefore often do not reflect a least-privilege policy. Policies are therefore often construed as overly permissive as there simply isn’t the time in the day to run many trial-and-error simulations to get it right and get on with business although CloudTrail does often thankfully help to identify requisite privileges when things fail.

One way to at least mitigate the risk of AWS keys and secrets being usable if falling into the wrong hands is to make those sets of credentials with high-impacting permissions time-limited and to make policies dependent on multi-factor authentication. Time limitation can be achieved by issuing credentials via the Secure Token Service, which was introduced in 2015.

I have worked with STS combined with MFA over the last few weeks in an effort to increase security while still enabling people to get on with their work and I have come up with a self-service approach that involves the following example set-up:

  "Version": "2012-10-17",
  "Statement": [
      "Effect": "Allow",
      "Action": "ec2:DescribeRegions",
      "Resource": "*",
      "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
aws ec2 describe-regions --profile testuser --region eu-west-1
A client error (UnauthorizedOperation) occurred when calling the DescribeRegions operation: You are not authorized to perform this operation.
echo -n "Enter token followed by [ENTER]: "
read token
output=`aws sts get-session-token --duration-seconds 14400 --serial-number arn:aws:iam::012345678912:mfa/testuser --token-code $token --output text "$@"`
if [[ $rc -ne 0 ]] ; then
  return $rc
export AWS_ACCESS_KEY_ID="${oarr[1]}"
export AWS_SECRET_ACCESS_KEY="${oarr[3]}"
export AWS_SESSION_TOKEN="${oarr[4]}"
. --profile testuser
Enter token followed by [ENTER]: 123456

aws ec2 describe-regions --region eu-west-1
REGIONS	eu-west-1
REGIONS	ap-southeast-1
REGIONS	ap-southeast-2
REGIONS	eu-central-1
REGIONS	ap-northeast-2
REGIONS	ap-northeast-1
REGIONS	us-east-1
REGIONS	sa-east-1
REGIONS	us-west-1
REGIONS	us-west-2
aws ec2 describe-regions --region eu-west-1
Unable to locate credentials. You can configure credentials by running "aws configure".
aws ec2 describe-regions --region eu-west-1 --profile testuser
A client error (UnauthorizedOperation) occurred when calling the DescribeRegions operation: You are not authorized to perform this operation.

The reason this approach works is that the environment variables take precedence over the credentials configured in ~/.aws/credentials. Therefore, if the environment variables are not set one would default back to the non-MFA permissions currently none, although this need not necessarily be the case.

As for choosing a validity period of the token this would be a balance between risk and convenience. In my case I choose 4 hours as this allows me to work in the morning until lunch, then take out a new set of credentials in the afternoon. The chance of someone intercepting the temporary set of credentials and being able to use them before they expire is therefore reasonably low.

On the other hand the risk of 3rd party access to the permanent credentials stored in the .aws/credentials file is in this case mitigated by the fact that there are no inherent permissions assigned because the credentials are not MFA authenticated. While one should still rotate them regularly the possible impact of interception is considerably reduced.

Another advantage is that with MFA authentication in place one can unify the policies & permissions for users of both the API and the Console because two-factor authentication is enabled.

Finally, one point worth mentioning is that there are per-region soft limits to how many instances of services like RDS, EC2 etc. that can be spun up. To further reduce the likelihood of attackers managing to spin up a considerable amount of costly estate, e.g. for the purposes of bitcoin mining, one would want to remove the ability to issue STS tokens for regions that are not being used in the account. To do so, simply follow the STS blog previously mentioned and do the reverse (deactivate) regions from STS.