AWS DevOps Blog

Secure AWS CodeCommit with Multi-Factor Authentication

This blog post shows you how to set up AWS CodeCommit if you want to enforce multi-factor authentication (MFA) for your repository users. One of the most common reasons for using MFA for your AWS CodeCommit repository is to secure sensitive data or prevent accidental pushes to the repository that could trigger a sensitive change process.

By using the MFA capabilities of AWS Identity and Access Management (IAM) you can add an extra layer of protection to sensitive code in your AWS CodeCommit repository. AWS Security Token Service (STS) and IAM allow you to stretch the period during which the authentication is valid from 15 minutes to 36 hours, depending on your needs. AWS CLI profile configuration and the AWS CodeCommit credential helper transparently use the MFA information as soon as it has been issued, so you can work with MFA with minimal impact to your daily development process.

Solution Overview

AWS CodeCommit currently provides two communication protocols and authentication methods:

  • SSH authentication uses keys configured in IAM user profiles.
  • HTTPS authentication uses IAM keys or temporary security credentials retrieved when assuming an IAM role.

It is possible to use SSH in a manner that incorporates multiple factors. An SSH private key can be considered something you have and its passphrase something you know. However, the passphrase cannot technically be enforced on the client side. Neither is it issued on an independent device.

That is why the solution described in this post uses the assumption of IAM roles to enforce MFA. STS can validate MFA information from devices that issue time-based one-time passwords (TOTPs).

A typical scenario involves the use of multiple AWS accounts (for example, Dev and Prod). One account is used for authentication and another contains the resource to be accessed (in this case, your AWS CodeCommit repository). You could also apply this solution to a single account.

This is what the workflow looks like:

Overview

  1. A user authenticates with IAM keys and a token from her MFA device and retrieves temporary credentials from STS. Temporary credentials consist of an access key ID, a secret access key, and a session token. The expiration of these keys can be configured with a duration of up to 36 hours.
  2. To access the resources in a different account, role delegation comes into play. The local Git repository is configured to use the temporary credentials to assume an IAM role that has access to the AWS CodeCommit repository. Here again, STS provides temporary credentials, but they are valid for a maximum of one hour.
  3. When Git is calling AWS CodeCommit, the credentials retrieved in step 2 are used to authenticate the requests. When the credentials expire, they are reissued with the credentials from step 1.

You could use permanent IAM keys to directly assume the role in step 2 without the temporary credentials from step 1. The process reduces the frequency with which a developer needs to use MFA by increasing the lifetime of the temporary credentials.

Account Setup Tasks

The tasks to set up the MFA scenario are as follows:

  1. Create a repository in AWS CodeCommit.
  2. Create a role that is used to access the repository.
  3. Create a group allowed to assume the role.
  4. Create a user with an MFA device who belongs to the group.

The following steps assume that you have set up the AWS CLI and configured it with the keys of users who have the required permissions to IAM and AWS CodeCommit in two accounts. Following the workflow, we will create the following admin users and AWS CLI profiles:

  • admin-account-a needs permissions to administer IAM (built-in policy IAMFullAccess)
  • admin-account-b needs permissions to administer IAM and AWS CodeCommit (built-in policies IAMFullAccess and AWSCodeCommitFullAccess)

At the time of this writing, AWS CodeCommit is available in us-east-1 only, so use that region for the region profile attribute for account B.

The following scripts work for Linux and Mac OS. For readability, line breaks are separated by back slashes. If you want to run these scripts on Microsoft Windows, you will need to adapt them or run them on an emulation layer (for example, Cygwin).

Replace placeholders like <XXXX> before issuing the commands.

Task 1: Create a repository in AWS CodeCommit

Create an AWS CodeCommit repository in Account B:

aws codecommit create-repository 
   --repository-name myRepository 
   --repository-description "My Repository" 
   --profile admin-account-b

Task 2: Create a role that is used to access the repository

  1. Create an IAM policy that grants access to the repository in Account B. Name it MyRepositoryContributorPolicy.

    Here is the MyRepositoryContributorPolicy.json policy document:

    {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "codecommit:CreateBranch",
                "codecommit:GetBlob",
                "codecommit:GetBranch",
                "codecommit:GetObjectIdentifier",
                "codecommit:GetRepository",
                "codecommit:GetTree",
                "codecommit:GitPull",
                "codecommit:GitPush",
                "codecommit:ListBranches"
            ],
            "Resource": [
                "arn:aws:codecommit:<ACCOUNT_B_REGION>:<ACCOUNT_B_ID>:myRepository"
            ]
        }
    ]
    }

    Create the policy:

    aws iam create-policy 
        --policy-name MyRepositoryContributorPolicy 
        --policy-document file://./MyRepositoryContributorPolicy.json 
        --profile admin-account-b
  2. Create a MyRepositoryContributorRole role that has the MyRepositoryContributorPolicy attached in Account B.

    Here is the MyRepositoryContributorTrustPolicy.json trust policy document:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "AWS": "arn:aws:iam::<ACCOUNT_A_ID>:root"
          },
          "Action": "sts:AssumeRole"
        }
      ]
    }

    Create the role:

    aws iam create-role 
    --role-name MyRepositoryContributorRole 
    --assume-role-policy-document file://./MyRepositoryContributorTrustPolicy.json 
    --profile admin-account-b

    Attach the MyRepositoryContributorPolicy:

    aws iam attach-role-policy 
    --role-name MyRepositoryContributorRole 
    --policy-arn arn:aws:iam::<ACCOUNT_B_ID>:policy/MyRepositoryContributorPolicy 
    --profile admin-account-b

Task 3: Create a group allowed to assume the role

  1. Create a MyRepositoryContributorAssumePolicy policy for users who are allowed to assume the role in Account A.

    Here is the MyRepositoryContributorAssumePolicy.json policy document:

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "sts:AssumeRole"
                ],
                "Resource": [
                    "arn:aws:iam::<ACCOUNT_B_ID>:role/MyRepositoryContributorRole"
                ],
                "Condition": {
                    "NumericLessThan": {
                        "aws:MultiFactorAuthAge": "86400"
                    }
                }
            }
        ]
    }

    The aws:MultiFactorAuthAge attribute is used to specify the validity, in seconds, after the temporary credentials with MFA information have been issued. After this period, the user can’t issue new temporary credentials by assuming a role. However, the old credentials retrieved by role assumption may still be valid for one hour to make calls to the repository.

    For this example, we set the value to 24 hours (86400 seconds).

    Create the policy:

    aws iam create-policy 
        --policy-name MyRepositoryContributorAssumePolicy 
        --policy-document file://./MyRepositoryContributorAssumePolicy.json 
        --profile admin-account-a
  2. Create the group for all users who need access to the repository:
    aws iam create-group 
        --group-name MyRepositoryContributorGroup 
        --profile admin-account-a
  3. Attach the policy to the group:
    aws iam attach-group-policy 
        --group-name MyRepositoryContributorGroup 
        --policy-arn arn:aws:iam::<ACCOUNT_A_ID>:policy/MyRepositoryContributorAssumePolicy 
        --profile admin-account-a

Task 4: Create a user with an MFA device who belongs to the group

  1. Create an IAM user in Account A:
    aws iam create-user 
        --user-name MyRepositoryUser 
        --profile admin-account-a
  2. Add the user to the IAM group:
    aws iam add-user-to-group 
        --group-name MyRepositoryContributorGroup 
        --user-name MyRepositoryUser 
        --profile admin-account-a
  3. Create a virtual MFA device for the user. You can use the AWS CLI, but in this case it is easier to create one in the AWS Management Console.
  4. Create IAM access keys for the user. Make note of the output of AccessKeyId and SecretAccessKey. They will be referenced as <ACCESS_KEY_ID> and <SECRET_ACCESS> later in this post.
    aws iam create-access-key 
       --user-name MyRepositoryUser 
       --profile admin-account-a

You’ve now completed the account setup. To create more users, repeat task 4. Now we can continue to the local setup of the contributor’s environment.

Initialize the Contributor’s Environment

Each contributor must perform the setup in order to have access to the repository.

Setup Tasks:

  1. Create a profile for the IAM user who fetches temporary credentials.
  2. Create a profile that is used to access the repository.
  3. Populate the role assuming profile with temporary credentials.

Task 1: Create a profile for the IAM user who fetches temporary credentials

By default, the AWS CLI maintains two files in ~/.aws/ that contain per-profile settings. One is credentials, which stores sensitive information for means of authentication (for example, the secret access keys). The other is config, which defines all other settings, such as the region or the MFA device to use.

Add the IAM keys for MyRepositoryUser that you created in Account Setup task 4 to ~/.aws/credentials:

[FetchMfaCredentials]
aws_access_key_id=<ACCESS_KEY_ID>
aws_secret_access_key=<SECRET_ACCESS>

Add the following lines to ~/.aws/config:

[profile FetchMfaCredentials]
mfa_serial=arn:aws:iam::<ACCOUNT_A_ID>:mfa/MyRepositoryUser
get_session_token_duration_seconds=86400

get_session_token_duration_seconds is a custom attribute that is used later by a script. It must not exceed the value of aws:MultiFactorAuthAge that we used in the assume policy.

Task 2: Create a profile that is used to access the repository

Add the following lines to ~/.aws/config:

[profile MyRepositoryContributor]
region=<ACCOUNT_B_REGION>
role_arn=arn:aws:iam::<ACCOUNT_B_ID>:role/MyRepositoryContributorRole
source_profile=MyRepositoryAssumer

When the MyRepositoryContributor profile is used, the MyRepositoryContributorRole is assumed with credentials of the MyRepositoryAssumer profile. You may have noticed that we have not put MyRepositoryAssumer in the credentials file yet. The following task shows how the file is populated.

Task 3: Populate the role assuming profile with temporary credentials

  1. Create the populateSessionTokenProfile.sh script in your home directory or any other location:
    #!/bin/bash
    
    # Parameter 1 is the name of the profile that is populated
    # with keys and tokens.
    KEY_PROFILE="$1"
    
    # Parameter 2 is the name of the profile that calls the
    # session token service.
    # It must contain IAM keys and mfa_serial configuration
    
    # The STS response contains an expiration date/ time.
    # This is checked to only set the keys if they are expired.
    EXPIRATION=$(aws configure get expiration --profile "$1")
    
    RELOAD="true"
    if [ -n "$EXPIRATION" ];
    then
            # get current time and expiry time in seconds since 1-1-1970
            NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    
            # if tokens are set and have not expired yet
            if [[ "$EXPIRATION" > "$NOW" ]];
            then
                    echo "Will not fetch new credentials. They expire at (UTC) $EXPIRATION"
                    RELOAD="false"
            fi
    fi
    
    if [ "$RELOAD" = "true" ];
    then
            echo "Need to fetch new STS credentials"
            MFA_SERIAL=$(aws configure get mfa_serial --profile "$2")
            DURATION=$(aws configure get get_session_token_duration_seconds --profile "$2")
            read -p "Token for MFA Device ($MFA_SERIAL): " TOKEN_CODE
            read -r AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN EXPIRATION AWS_ACCESS_KEY_ID < <(aws sts get-session-token \
                    --profile "$2" \
                    --output text \
                    --query '[Credentials.SecretAccessKey, Credentials.SessionToken, Credentials.Expiration, Credentials.AccessKeyId]' \
                    --serial-number $MFA_SERIAL \
                    --duration-seconds $DURATION \
                    --token-code $TOKEN_CODE)
    
            aws configure set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" --profile "$KEY_PROFILE"
            aws configure set aws_session_token "$AWS_SESSION_TOKEN" --profile "$KEY_PROFILE"
            aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID" --profile "$KEY_PROFILE"
            aws configure set expiration "$EXPIRATION" --profile "$1"
    fi

    This script takes the credentials from the profile from the second parameter to request temporary credentials. These will be written to the profile specified in the first parameter.

  2. Run the script once. You might need to set execution permission (for example, chmod 755) before you run it.
    ~/populateSessionTokenProfile.sh MyRepositoryAssumer FetchMfaCredentials
    Need to fetch new STS credentials
    Token for MFA Device (arn:aws:iam::<ACCOUNT_A_ID>:mfa/MyRepositoryUser): XXXXXX

    This populates information retrieved from STS to the ~/.aws/config and ~/.aws/credentials file.

  3. Clone the repository, configure Git to use temporary credentials, and create an alias to renew MFA credentials:
    git clone --config 'credential.helper=!aws codecommit 
        --profile MyRepositoryContributor 
        credential-helper $@' 
        --config 'credential.UseHttpPath=true' 
        --config 'alias.mfa=!~/populateSessionTokenProfile.sh 
        MyRepositoryAssumer FetchMfaCredentials' 
        $(aws codecommit get-repository 
        --repository-name myRepository 
        --profile MyRepositoryContributor 
        --output text 
        --query repositoryMetadata.cloneUrlHttp)

    This clones the repository from AWS CodeCommit. You can issue subsequent calls as long as the temporary credentials retrieved in step 2 have not expired. As soon as they have expired, the credential helper will return an error with prompts for username and password:

    A client error (ExpiredToken) occurred when calling the AssumeRole operation:
    The security token included in the request is expired

    In this case, you should cancel the Git command (Ctrl-C) and trigger the renewal of the token by calling the alias in your repository:

    git mfa

We hope you find the steps for enforcing MFA for your repository users helpful. Feel free to leave your feedback in the comments.