Josh von Schaumburg in security 8 minutes to read

Improving the AWS Force MFA Policy for IAM Users

At Trek10, whenever we take on a new customer, one of our very first security checks we perform is to ensure all users are leveraging multi-factor authentication (MFA). Customers often ask if there is a way to force all users to use MFA. While there is no simple checkbox for this, there is an IAM policy that can be applied to all users, which strips away all IAM permissions (except those needed to configure MFA) until a user logs in with an MFA code. This policy is one of our top recommendations to customers looking to secure access to the AWS management console and API.

Fortunately, you do not need to write this policy from scratch! And neither did we… AWS released a blog post last summer describing how to implement this IAM policy. This has become the standard policy to enforce MFA across an organization. This policy works great when applied to a user without any IAM permissions, but if you apply it to a user who already has AdministratorAccess or IAMFullAccess, there is a minor security risk. As an MSP and Consulting Partner of AWS, most of our new customers already have a number of users with full AdministratorAccess. Given this, we decided to expand upon the Force_MFA policy to mitigate the security risk that remains when applying it to users with AdministratorAccess permissions.

In this blog post, I will first breakdown the AWS Force_MFA policy by each statement ID (SID), then I will describe the changes we made to cover the use case of applying this policy to users who already have full IAM rights. Remember, the purpose of the Force_MFA policy is to strip the user of all permissions unless they log in with MFA, but it should still give the user enough permissions to log in to the console to be able to configure MFA (only for their account).

    {
        "Sid": "AllowAllUsersToListAccountsAndGetPasswordPolicyForReset",
        "Effect": "Allow",
        "Action": [
            "iam:ListAccountAliases",
            "iam:GetAccountPasswordPolicy",
            "iam:ListUsers"
        ],
        "Resource": [
            "*"
        ]
    }

This SID gives the user access to the IAM permissions to view the account alias when clicking on the IAM service, as well as the ability to list all users in the console. Without ListUsers, the user would not be able click into their ID to make changes such as reset the password or configure MFA. It also gives users access to the account’s password policy, which is required to reset the password.

    {
        "Sid": "AllowIndividualUserToSeeTheirAccountInformation",
        "Effect": "Allow",
        "Action": [
            "iam:ChangePassword",
            "iam:CreateLoginProfile",
            "iam:DeleteLoginProfile",
            "iam:GetAccountPasswordPolicy",
            "iam:GetAccountSummary",
            "iam:GetLoginProfile",
            "iam:UpdateLoginProfile"
        ],
        "Resource": [
            "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}"
        ]
    }

This SID gives the user a number of different permissions required to view all of their account information and change their password. Notice that the resource is different than in the above SID. Here, we can limit the resource to just one specific user – i.e., user/${aws:username}, whereas in the previous SID, the actions defined must apply across the account – i.e., “*”.

The ${aws:username} is what is known as a policy variable. When the IAM policy is evaluated, the IAM ID of the authenticated user replaces the policy variable.

    {
        "Sid": "AllowIndividualUserToListTheirMFA",
        "Effect": "Allow",
        "Action": [
            "iam:ListVirtualMFADevices",
            "iam:ListMFADevices"
        ],
        "Resource": [
            "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/*",
            "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}"
        ]
    }

This SID gives the user proper permissions to view their MFA device. You can think of this one as read only access on MFA for the authenticated user.

    {
        "Sid": "AllowIndividualUserToManageThierMFA",
        "Effect": "Allow",
        "Action": [
            "iam:CreateVirtualMFADevice",
            "iam:DeactivateMFADevice",
            "iam:DeleteVirtualMFADevice",
            "iam:EnableMFADevice",
            "iam:ResyncMFADevice"
        ],
        "Resource": [
            "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}",
            "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}"
        ]
    }

This SID allows the user to actually configure their (and only their) MFA device. This is write access to the authenticated user’s MFA configurations.

    {
        "Sid": "DoNotAllowAnythingOtherThanAboveUnlessMFAd",
        "Effect": "Deny",
        "NotAction": "iam:*",
        "Resource": "*",
        "Condition": {
            "Null": [
                "aws:MultiFactorAuthAge": "true"
            ]
        }
    }

In this SID, we thought it would be best to make an improvement based on our particular use case. The above SID uses the NotAction element to deny all possible API actions other than the the one listed (“iam:*”). There is also a condition to this SID. The condition says that the SID should only be in effect if aws:MultiFactorAuthAge = true is null – i.e., only evaluate the SID if the user has not configured MFA. (Note that the aws:MultiFactorAuthAge key is not present if MFA is not enabled; hence, the Null portion of the condition.)

This SID works perfectly fine if the policy is applied to a user who does not already have permissions to perform IAM actions. As a Consulting and MSP partner, when we support a new customer, we often apply this policy to user IDs who already have full AdministratorAccess rights attached. This means that denying “NotAction:” “iam:*” is not strict enough because the user would then have ALL iam:* permissions applied from the AdministratorAccess policy.

In summary, if this policy is applied to a user with admin rights, the user is really not forced to configure MFA if they do not want to. The user has the IAM rights to simply remove the Force_MFA policy from their ID rather than configuring MFA. The user could also just create a new, full admin user without having to configure MFA. To mitigate this risk, we decided to replace the last SID with the two below SIDs, which truly removes all permissions (regardless of whether or not the user already has full admin) until MFA is configured.

    {
        "Sid": "DenyEverythingExceptForBelowUnlessMFAd",
        "Effect": "Deny",
        "NotAction": [
            "iam:ListVirtualMFADevices",
            "iam:ListMFADevices",
            "iam:ListUsers",
            "iam:ListAccountAliases",
            "iam:CreateVirtualMFADevice",
            "iam:DeactivateMFADevice",
            "iam:DeleteVirtualMFADevice",
            "iam:EnableMFADevice",
            "iam:ResyncMFADevice",
            "iam:ChangePassword",
            "iam:CreateLoginProfile",
            "iam:DeleteLoginProfile",
            "iam:GetAccountPasswordPolicy",
            "iam:GetAccountSummary",
            "iam:GetLoginProfile",
            "iam:UpdateLoginProfile"
        ],
        "Resource": "*",
        "Condition": {
            "Null": [
                "aws:MultiFactorAuthAge": "true"
            ]
        }
    }

This SID is very similar to the original one it is replacing, except we are breaking down the iam:* action into only the specific actions that are required. This means that all other IAM actions will be denied.

    {
        "Sid": "DenyIamAccessToOtherAccountsUnlessMFAd",
        "Effect": "Deny",
        "Action": [
            "iam:CreateVirtualMFADevice",
            "iam:DeactivateMFADevice",
            "iam:DeleteVirtualMFADevice",
            "iam:EnableMFADevice",
            "iam:ResyncMFADevice",
            "iam:ChangePassword",
            "iam:CreateLoginProfile",
            "iam:DeleteLoginProfile",
            "iam:GetAccountSummary",
            "iam:GetLoginProfile",
            "iam:UpdateLoginProfile"
        ],
        "NotResource": [
            "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:mfa/${aws:username}",
            "arn:aws:iam::ACCOUNT-ID-WITHOUT-HYPHENS:user/${aws:username}"
        ]
    }    

This SID limits all actions that are user specific to only being allowed by the user who is authenticating by using NotResource. The NotResource element means that the SID will apply to all resources except for the one defined; in this case, the authenticated user. The SID explicitly denies any actions which may be granted from an AdministratorAccess policy. Remember that an explicit deny will always override any allow. Note that some may argue this last SID is really not necessary because once the admin user enables MFA, they will have access to these actions. If an attacker gains access to this account, the attacker can, of course, just configure MFA to gain access to these actions. I would argue that an attacker may not realize MFA is required to gain full admin access, so there is still value in this SID so that an attacker cannot log in and start performing these destructive actions on IAM accounts (like deleting access keys).

Here is link to the full policy for implementation into your environment.

Questions/comments on AWS security? Feel free to reach us at security@trek10.com.