Secure development on AWS using role-based access and MFA

By combining aws-vault and a Yubikey we can set up multi-factor authentication (MFA) for secure developer access to AWS accounts that is more secure and easy to use.

It is difficult to overstate the importance of keeping your credentials safe as a cloud developer, as in a modern environment it is not unusual to be granted user credentials with a wide range of permissions. Such credentials are often static and stored in plaintext on workstations, making them a prime target for malicious actors. To minimize the attack surface security-minded developers will often store such credentials in an encrypted keystore, using them only to assume roles in accounts for any work. Roles, when used correctly, are temporary credentials that grant limited access. This is already a step up compared to using static plaintext credentials, yet we can and should do better.

Using multi-factor authentication has become the norm and is recommended by AWS when accessing their website through a browser. Sadly the same cannot be said for accessing AWS as a developer through the CLI. Here MFA is a rarer sight due to the fact that it is more complex to set up and even more cumbersome to use. But luckily we can do something about that.

A more secure and painless workflow

By combining aws-vault and a Yubikey we can achieve something that we cannot do any other way: a painless and more secure workflow for developers. To understand why we must first know the alternative MFA setup, namely using a one-time password (OTP) with an application such as Google Authenticator.

Since AWS does not support U2F we can only use OTP as a second factor. This simply means that every time we have to authenticate using our second factor we must open Google Authenticator and enter the 6-digit code that is shown on our screen. This is at best annoying and at worst a motivation to disable MFA, since it disrupts flow. No matter how good the pitch for better security is, if it results in a more complicated workflow the measures will be met with opposition and possibly even ignored.

Using a Yubikey with aws-vault allows us to authenticate with OTP as simply as pushing a button. Once in a while your terminal will ask you for a code, which can be entered by touching your Yubikey. Painless and secure.

What we are going to set up

We will be setting up an AWS role that can only be accessed by our development user if he or she authenticates themselves with a Yubikey.

aws-vault is a CLI tool that allows developers to store their user credentials in their operating system’s secure keystore. It is then possible to use these credentials to authenticate yourself for a session using aws-vault exec <profile-name> -- or log into your account using aws-vault login <profile-name>. Both commands use temporary credentials without exposing your valuable user credentials.

The MFA device we will use is a Yubikey. Instead of using the incompatible U2F approach, we will configure our Yubikey to generate OTP codes for us. For this we will have to register our yubikey to the AWS user we create.

All in all the elements we need are:

  • A role with a condition that only allows developers with MFA to assume it.
  • A user that can assume this role with a registered Yubikey.
  • aws-vault with a profile to configure our user credentials and role.

Tutorial time

We will implement the described solution from scratch, assuming that you have access to an AWS account and are allowed to create users and roles. If you do not, please follow a tutorial on how to create an AWS account and log in as a user.

The setup is split into two distinctive parts. First we will configure the AWS account with a user, a role and an MFA device. Second we will move onto the workstation and set-up the user and role configurations.

AWS

First we create both a user and a role. To speed up this process we will use CloudFormation for this. Deploy the following to your AWS account and wait for the stack creation to be completed.

AWSTemplateFormatVersion: '2010-09-09'

Resources:
  DevUser:
    Type: 'AWS::IAM::User'
    Properties:
      UserName: 'dev'
      Policies:
        - PolicyName: 'EditOwnUser'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action: 
                  - 'iam:GetAccountPasswordPolicy'
                  - 'iam:GetAccountSummary'       
                  - 'iam:ListVirtualMFADevices'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'iam:ChangePassword'
                  - 'iam:GetUser'
                Resource: 'arn:aws:iam::*:user/${aws:username}'
              - Effect: 'Allow'
                Action: 
                  - 'iam:CreateAccessKey'
                  - 'iam:DeleteAccessKey'
                  - 'iam:ListAccessKeys'
                  - 'iam:UpdateAccessKey'
                Resource: 'arn:aws:iam::*:user/${aws:username}'
              - Effect: 'Allow'
                Action:
                  - 'iam:CreateVirtualMFADevice'
                  - 'iam:DeleteVirtualMFADevice'
                Resource: 'arn:aws:iam::*:mfa/${aws:username}'
              - Effect: 'Allow'
                Action:
                  - 'iam:DeactivateMFADevice'
                  - 'iam:EnableMFADevice'
                  - 'iam:ListMFADevices'
                  - 'iam:ResyncMFADevice'
                Resource: 'arn:aws:iam::*:user/${aws:username}'
  
  DevRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'DevRole'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              'AWS': !GetAtt 'DevUser.Arn'
            Action: 'sts:AssumeRole'
            Condition:
              'Bool': {'aws:MultiFactorAuthPresent': 'true'}
              'NumericLessThan': {'aws: MultiFactorAuthAge': '300'}
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/IAMReadOnlyAccess'
        - 'arn:aws:iam::aws:policy/AmazonEC2FullAccess'
        - 'arn:aws:iam::aws:policy/AWSLambdaFullAccess'
        - 'arn:aws:iam::aws:policy/AmazonAPIGatewayAdministrator'

Outputs:
  DevRoleArn:
    Value: !GetAtt 'DevRole.Arn'

The gist of our configuration here is that we apply a so called Condition to our role. By enforcing ‘aws:MultiFactorAuthPresent’ to be ‘true’ we will only allow MFA authenticated users to assume the role.

Please note the ARN of the role we have created. You can find this if you browse to the role in IAM, or you can copy the value from the Outputs tab of your CloudFormation stack.

Now we need to configure our user. Go to IAM -> User -> Security credentials. Click on Create access key and store the credentials on your laptop. Do not forget to delete this file once you are finished with the setup. Also note the User ARN on top.

We should now register our Yubikey to our user. Find Assigned MFA device under Security credentials and add an MFA device by clicking Manage. Select the first option Virtual MFA device.

Now instead of using a QR code, we will click on Show secret key and copy the encoded key. The form expects us to input two codes from our MFA device, so let’s configure it to output them.

Workstation

On your workstation install yubikey-manager, a CLI tool for managing your, well, yubikey. On Ubuntu this is as simple as sudo apt install yubikey-manager. MacOS has brew install yakman and for windows please check this page.

Now open a terminal and register a new OTP profile, note that the name must correspond with the ARN of the user we have created:

You will be requested to paste the key you copied just recently. Once you have created this profile you can generate OTP codes using:

Generate two codes and paste them into the form in your browser. Note that you have to wait a period of time before the code changes.

Note the arn that now appears where “Not assigned” was before. This is your MFA device arn, and it will be in the format of arn:aws:iam::<account-id>:mfa/<user-name>.

We are practically done, what is left is to configure aws-vault. Install aws-vault from the github page. We will use AWS vault to both store our user access keys in a secure manner and log into our account with the configured role. Input the following command and paste the access key id and the secret access key when requested:

The profile name can be anything you want, but note that this is not the profile you will be using when executing commands! For that we need to configure a role in ~/.aws/config. Open this file now.

You will notice that a profile already exists, the one you just added with aws-vault. We now need to configure a second profile with the role that we want to assume. Modify the following snippet and paste it into the file:

An example of how you file can look like after this step:

We have configured a new profile, connected to both your role and your Yubikey, which you can use for development work. To test whether everything works input the following command:

You will be requested to touch your Yubikey and will then see the identity with which you have logged in. Note that the identity will be a role, not a user. If everything works, congratulations!

To avoid having to enter --prompt=ykman all the time you can add AWS_VAULT_PROMPT=ykman to your shell script (e.g. ~/.zshrc or ~/.bashrc). That is also the location where you would add an alias for this command, such as alias awse='aws-vault exec' or alias deve='aws-vault exec dev-role --'.

On the use of OTP versus U2F

U2F is rapidly becoming an industry standard for MFA due to the extra level of security it provides. U2F is an open authentication standard created by Yubico and Google to counteract phishing, session-hijacking and man-in-the-middle attacks. The secret key never leaves the MFA device and the domain of the website you registered with is included in the validation so that phishing will not work. When possible, using a hardware token with U2F will be one the most secure second factors you can apply.

One-time passwords, compared to U2F are inherently less secure. This is because a shared secret is used to generate and validate codes. This shared secret must be present on both the server and your device and can be phished. The generated codes themselves are vulnerable to this as well. OTP is however very well supported and cheap to implement, making it a popular method. As a second factor it will serve well, just keep in mind that you need to be watchful for phishing attempts.

Ilia Awakimjan

Ilia Awakimjan is na het behalen van zijn Master-titel sinds 2017 Software Engineer met specialisatie AWS in dienst bij Profit4Cloud. Ilia is AWS Certified DevOps Professional, AWS Certified Security Specialty en AWS Certified Networking Specialty gecertificeerd.