AWS: How to secure access keys with MFA

How to prevent account compromise caused by leaked access keys

Author's image
Tamás Sallai
6 mins

One of the most catastrophic of the AWS account security breaches is not sophisticated hacking involving 0-day vulnerabilities traded on the deep web by high-profile hackers. It is when you post your access and secret keys in plain text to the public. After all, it's so easy to test with some hard-coded keys and accidentally push it to the VCS.

There are numerous crawlers constantly searching GitHub and other public sources to catch these keys. Not just by the bad guys, AWS itself also runs these bots trawling for leaked credentials and I've heard about stories that they contacted account owners in case of a catch.

The problem with access keys is that they do not require anything else to be usable. If you have the key pair, you can use them to do anything the user has access to.

When you use the console, you have an option to require an MFA device to log in to the account along with the username/password pair. Securing console access is then only a matter of associating an MFA device with the user. With this in place, the account is secured.

I wondered, is there a way to do the same for access keys also?

Yes, there is.

MFA-protected access keys

Contrary to the console logins, adding an MFA device does not immediately affect access keys. But it can be checked in an IAM policy to achieve a similar effect.

Here is a policy for this, you can simply copy-paste it:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowIndividualUserToListOnlyTheirOwnMFA",
            "Effect": "Allow",
            "Action": [
                "iam:ListMFADevices"
            ],
            "Resource": [
                "arn:aws:iam::*:user/${aws:username}"
            ]
        },
        {
            "Sid": "BlockIndividualUserToListOtherMFAsWithoutMFA",
            "Effect": "Deny",
            "Action": [
                "iam:ListMFADevices"
            ],
            "NotResource": [
                "arn:aws:iam::*:user/${aws:username}"
            ],
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "false"
                }
            }
        },
        {
            "Sid": "BlockMostAccessUnlessSignedInWithMFA",
            "Effect": "Deny",
            "NotAction": [
                "iam:ListMFADevices",
                "sts:GetSessionToken",
                "sts:GetCallerIdentity"
            ],
            "Resource": "*",
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "false"
                }
            }
        }
    ]
}

It is loosely based on an example policy provided by AWS.

What does it do exactly?

The first part, AllowIndividualUserToListOnlyTheirOwnMFA allows the user to list their own MFA devices. This is to make scripting easier, as starting a session requires the serial number of the device. With this call it can be looked up dynamically.

The next part, BlockIndividualUserToListOtherMFAsWithoutMFA makes sure that no other MFA devices are accessible if the session is not itself authenticated with an MFA device. This sounds confusing, but this implements the least privilege. The non-MFA authenticated user should not have access to anything that is not strictly required for this one task: to authenticate with an MFA device. Everything else should be denied, including listing other MFA devices.

The last part, the BlockMostAccessUnlessSignedInWithMFA extends this concept to all other services.

Notice that the policy does not give any permissions apart from the bare minimum. In this sense, it is a blacklisting policy that's sole purpose is to limit other policies attached to the user. For example, you can use this along with the Administrator managed policy to have an MFA-protected admin.

Sessions

In AWS when you authenticate with an MFA device you need to start a session. This is a call to the sts:get-session-token endpoint and it returns an AWS_ACCESS_KEY_ID, an AWS_SECRET_ACCESS_KEY, and an AWS_SESSION_TOKEN.

Since the AWS CLI uses environment variables when they are present, you can set the session parameters in the environment. Subsequent calls to the APIs will use the temporary credentials of the session instead of permanent ones from the config.

How temporary?

By default, a session is valid for 12 hours, which should be plenty for a workday. And if it expires, you can get another set with a new MFA code.

Tooling

To make things easier, it is best to have a shell script ready to do the mundane work:

eval `aws sts get-session-token \
	--serial-number $( \
		aws iam list-mfa-devices \
			--user-name $( \
				aws sts get-caller-identity | \
					jq -r '.Arn | split("/")[1]' \
			) \
		| jq -r '.MFADevices[0].SerialNumber' \
	) \
	--token-code $1 \
| jq -r '"export AWS_ACCESS_KEY_ID=" + .Credentials.AccessKeyId, "export AWS_SECRET_ACCESS_KEY="+.Credentials.SecretAccessKey, "export AWS_SESSION_TOKEN="+.Credentials.SessionToken'`

Only jq is required apart from the AWS CLI which you can usually install with apt install jq or something similar.

To use it, provide the MFA token and source the result:

. ./setup.sh 123456

Unfortunately, it does not work with U2F MFA devices, only virtual ones. That means you need to use your phone and Google Authenticator, Authy (my preference), or a similar app.

To deauth the session and get back to the permanent credentials:

unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN

A curious case is that there are 3 calls in the script above, but the policy only allows the iam:ListMFADevices and neither of the sts:GetCallerIdentity nor the sts:GetCallerIdentity. As it turned out those calls are always allowed, even if you explicitly deny them.

This is one of those quirks that make working with IAM policies, well, interesting.

Monitoring leaked keys

The original problem was privileges from leaked keys which this solution effectively prevents. But the cherry on top is that you can also monitor lost keys.

CloudTrail logs all denied accesses to the AWS APIs and using that you can monitor keys. This can signal a lost key which you can then replace.

Security assessment

How secure is the above solution?

Using an MFA device immediately raises the question of what happens if the device is stolen/lost? As long as there is an admin above the user who could assign a new device all is well. And since these keys are usually used by developers, it does not pose a problem. If the device is lost, report it to management and they will replace it for you.

One downside is that you can't use U2F keys, only virtual ones and those are not guaranteed to be copy-proof. If someone gets the MFA secret code, he can recreate the device.

And lastly, security cannot be assessed without taking convenience into the equation. The above solution requires the developers to enter a code once every day then they can use their account just how they would without this protection. I believe this is a reasonable compromise, considering the enormous gains.

Conclusion

MFA is one of the best tools to prevent hacking into an account, and with some setup and tooling, you can secure developer accounts with minimal inconveniences.

February 26, 2019
In this article