AWS: How to secure access keys with MFA
How to prevent account compromise caused by leaked access keys
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.