16th November 2022

AWS - MFA and CLI

IAM allows to associate a MFA device user to a user. That user can then be prompted to enter a temprary token provided by that device, in addition to the password, to access the AWS console.

Enabling MFA for a given user is done by going to the IAM center, selecting security credentials for a given user and enabling a MFA device. With a registered MFA device, MFA becomes enabled and the user needs to provide the MFA token to access the console. But these services can be still accessed using the access keys (CLI access), and enabling the MFA device has no impact on this access.

Before understanding how MFA can be enabled for programmatic access, it must be noted that AWS (https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#enable-mfa-for-privileged-users) recommends using IAM roles for human users and workloads that access your AWS resources so that they use temporary credentials. This, as opposed to the long term credentials that the user access keys represent.

The objective here is to ensure that the user enters the MFA token when it does programmatic access, using IAM roles or not.

Programmatic access and MFA without IAM roles

We create a user, and assign a MFA virtual device. We assign access keys and configure the CLI using aws configure.

For initial testing purposes, we assign a single policy to this user:

{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "s3:ListAllMyBuckets",
                    "s3:ListBucket",
                    "iam:ListMFADevices"
                ],
                "Resource": "*"
            }
        ]
    }

With this policy, we are able to execute aws s3 ls. The user has MFA enabled, but this does not impact the CLI access. There is AS blog post : How can I require MFA authentication for IAM users that use the AWS CLI (https://aws.amazon.com/premiumsupport/knowledge-center/mfa-iam-user-aws-cli/), which defines the policy that must be enabled for the user. Adapted to our existing policy, the result is:

{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "s3:ListAllMyBuckets",
                    "s3:ListBucket",
                    "iam:ListMFADevices"
                ],
                "Resource": "*"
            },
            {
                "Sid": "BlockMostAccessUnlessSignedInWithMFA",
                "Effect": "Deny",
                "NotAction": [
                    "iam:CreateVirtualMFADevice",
                    "iam:DeleteVirtualMFADevice",
                    "iam:ListVirtualMFADevices",
                    "iam:EnableMFADevice",
                    "iam:ResyncMFADevice",
                    "iam:ListAccountAliases",
                    "iam:ListUsers",
                    "iam:ListSSHPublicKeys",
                    "iam:ListAccessKeys",
                    "iam:ListServiceSpecificCredentials",
                    "iam:ListMFADevices",
                    "iam:GetAccountSummary",
                    "sts:GetSessionToken"
                ],
                "Resource": "*",
                "Condition": {
                    "BoolIfExists": {
                        "aws:MultiFactorAuthPresent": "false",
                        "aws:ViaAWSService": "false"
                    }
                }
            }
        ]
    }

With this extended policy, executing aws s3 ls fails with access denied. The solution is in this another AWS post: How do I use an MFA token to authenticate access to my AWS resources through the AWS CLI? (https://aws.amazon.com/premiumsupport/knowledge-center/authenticate-mfa-cli/):

aws sts get-session-token --serial-number arn:aws:iam::012174738912:mfa/coderazzi@gmail.com --token-code 315715 

This returns temporal credentials valid for 12 hours, unless a duration is specified in the previous command. If the token is invalid, it fails with message:

    An error occurred (AccessDenied) when calling the GetSessionToken operation:
                MultiFactorAuthentication failed with invalid MFA one time pass code.

Otherwise, the output message includes the credentials information:

{
        "Credentials": {
            "AccessKeyId": "ASIAQPOK6K7QJ466NEUN",
            "SecretAccessKey": "wtGvlwnB1HJgeI3CBcQgOKMLFJsntYkDjupEfRIu",
            "SessionToken": "IQoJb3JpZ2luX2VjENz//////////wEaCW ...........",
            "Expiration": "2022-09-12T00:00:45+00:00"
        }
    }

Doing now:

export AWS_ACCESS_KEY_ID=ASIAQPOK6K7QJ466NEUN
    export AWS_SECRET_ACCESS_KEY=wtGvlwnB1HJgeI3CBcQgOKMLFJsntYkDjupEfRIu
    export AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjENz//////////wEaCWV1L................

Enables us to access S3 again.

Calling again aws sts get-session-token will fail if these environment variables are set, so it is first required to do:

unset AWS_ACCESS_KEY_ID
    unset AWS_SECRET_ACCESS_KEY
    unset AWS_SESSION_TOKEN

So this solution is mostly complete. The only drawback is that we are not prompted to enter a MFA token in response to our commands: it is out responsability to retrieve the session token and set the environment variables before issuing the AWS access commands.

With this solution, instead of setting/unsetting environment variables, an alternative is using temporary credentials with named profiles:

[mfa]
    aws_access_key_id = ASIAQPOK6K7QPZMOC7WT
    aws_secret_access_key = nY3JvrGO0LvGvL9o3nsoI06qEhsqSeXoAQyCAQBT
    aws_session_token = IQoJb3JpZ2luX2VjEGUaCWV1LXdlc...............

Then, it is posible to access the AWS service speicfying this profile:

aws s3 ls --profile mfa

Programmatic access and MFA with IAM roles

Using AWS roles for CLI access is described here: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html

Setup of IAM role and user

Let's first create a simple role in AWS giving access to S3. When creating a role, we choose custom trust policy:

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

And add permission with a policy such as:

{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "s3:ListAllMyBuckets",
                    "s3:ListBucket",
                    "iam:ListMFADevices"
                ],
                "Resource": "*"
            }
        ]
    }

Then, we give it a name, such as: Test_S3_Role_Remove

Now, we need to have a user, and give him permission to switch to this role, plus, as before, access to S3 buckets:

{
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": "sts:AssumeRole",
                "Resource": "arn:aws:iam::033174738912:role/Test_S3_Role_Remove"
            }
        ]
    }

We call this policy Test_IAM_Role_Policy_Remove.

The first setup is to use an IAM role without MFA access. In this case, the credentials file will contain an entry such as:

[coderazzi2]
    aws_access_key_id = AKIAQPOK6K7QEHQHIOXJ
    aws_secret_access_key = 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

    [iamrole]
    role_arn = arn:aws:iam::033174738912:role/Test_S3_Role_Remove
    source_profile = coderazzi2

So we can try:

aws s3 ls --profile coderazzi2

This will fail, access denied. But the following call:

aws s3 ls --profile iamrole

succeeds, listing the S3 buckets as expected.

Updating role ro require MFA

In this case, we update the trust policy we had already created:

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "Statement1",
                "Effect": "Allow",
                "Principal": {"AWS": "arn:aws:iam::033174738912:root"},
                "Action": "sts:AssumeRole",
               "Condition": { "Bool": { "aws:multifactorAuthPresent": true } }
            }
        ]
    }

We have just added one line, the condition, to require MFA. But this cannot be tested immediately, as when the user assumes a role, it is assumed by default for one hour. This can be checked in CloudTrail, Event History, filtering by AWS::STS::AssumedRole.

When the user needs to assume again the role, it will fail, not authorized. We need to allocate a MFA device to this user, as done before for the first user, and we get the identity: arn:aws:iam::033174738912:mfa/coderazzi2@gmail.com

We modify now the credential file to add this mfa serial number:

[iamrole]
    role_arn = arn:aws:iam::033174738912:role/Test_S3_Role_Remove
    source_profile = coderazzi2
    mfa_serial = arn:aws:iam::033174738912:mfa/coderazzi2@gmail.com

    [coderazzi2]
    aws_access_key_id = AKIAQPOK6K7QEHQHIOXJ
    aws_secret_access_key = 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

When we do now:

    aws s3 ls --profile iamrole

AWS will ask: Enter MFA code for arn:aws:iam::033174738912:mfa/coderazzi2@gmail.com: If the token matches, the user can assume the role and list the tokens.

As before, this role is now assumed for one hour, so next calls with this profile will not prompt the user to enter a new MFA code.