Enforce MFA to AWS IAM users
Ensure secure authentication by attaching a policy to enforce MFA to IAM users

Hrushikesh specializes in Application Security, Cloud and Cloud-Native security.
Search for a command to run...
Ensure secure authentication by attaching a policy to enforce MFA to IAM users

Hrushikesh specializes in Application Security, Cloud and Cloud-Native security.
No comments yet. Be the first to comment.
Implement a secure way to share sensitive data within the Organization

Whenever an IAM user is created, it's best if it is tagged. The reason is simple... It gives more information about the user. One of the use-cases for user tagging could be to distinguish the user while running automation scripts. Various types of us...

Maintain least privilege access every day!

Get real-time alerts whenever an important AWS service is changed

Welcome to yet another blog post, where we learn how to enforce Multi-factor authentication to IAM users and ensure secure authentication.
This solution will block access to all AWS resources until the user enables MFA on AWS. We will implement this at scale (assuming you have multiple accounts under AWS Organizations)
We will add a feature where we can whitelist some users using tags. The reason is, sometimes an IAM user is created and used as a service account using access keys. (Which is a bad practice. Instead, IAM Roles should be used)
To achieve our goal, and make sure that our solution is scalable, we will use the following AWS resources:

AWS Stack and StackSet will help us deploy our solution to all the accounts that are a part of the Organization. (Assuming AWS Organization is enabled).
If AWS organization is not enabled then the manual overhead of deploying the Stack to every account increases. (Feel free to correct me in the comments)
import json
import boto3
import os, math
import requests
import datetime, time
from botocore.exceptions import ClientError
policyJson = {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowViewAccountInfo",
"Effect": "Allow",
"Action": [
"iam:GetAccountPasswordPolicy",
"iam:GetAccountSummary",
"iam:ListVirtualMFADevices"
],
"Resource": "*"
},
{
"Sid": "AllowManageOwnPasswords",
"Effect": "Allow",
"Action": [
"iam:ChangePassword",
"iam:GetUser"
],
"Resource": "arn:aws:iam::*:user/${aws:username}"
},
{
"Sid": "AllowManageOwnAccessKeys",
"Effect": "Allow",
"Action": [
"iam:CreateAccessKey",
"iam:DeleteAccessKey",
"iam:ListAccessKeys",
"iam:UpdateAccessKey"
],
"Resource": "arn:aws:iam::*:user/${aws:username}"
},
{
"Sid": "AllowManageOwnSigningCertificates",
"Effect": "Allow",
"Action": [
"iam:DeleteSigningCertificate",
"iam:ListSigningCertificates",
"iam:UpdateSigningCertificate",
"iam:UploadSigningCertificate"
],
"Resource": "arn:aws:iam::*:user/${aws:username}"
},
{
"Sid": "AllowManageOwnSSHPublicKeys",
"Effect": "Allow",
"Action": [
"iam:DeleteSSHPublicKey",
"iam:GetSSHPublicKey",
"iam:ListSSHPublicKeys",
"iam:UpdateSSHPublicKey",
"iam:UploadSSHPublicKey"
],
"Resource": "arn:aws:iam::*:user/${aws:username}"
},
{
"Sid": "AllowManageOwnGitCredentials",
"Effect": "Allow",
"Action": [
"iam:CreateServiceSpecificCredential",
"iam:DeleteServiceSpecificCredential",
"iam:ListServiceSpecificCredentials",
"iam:ResetServiceSpecificCredential",
"iam:UpdateServiceSpecificCredential"
],
"Resource": "arn:aws:iam::*:user/${aws:username}"
},
{
"Sid": "AllowManageOwnVirtualMFADevice",
"Effect": "Allow",
"Action": [
"iam:CreateVirtualMFADevice",
"iam:DeleteVirtualMFADevice"
],
"Resource": "arn:aws:iam::*:mfa/${aws:username}"
},
{
"Sid": "AllowManageOwnUserMFA",
"Effect": "Allow",
"Action": [
"iam:DeactivateMFADevice",
"iam:EnableMFADevice",
"iam:ListMFADevices",
"iam:ResyncMFADevice"
],
"Resource": "arn:aws:iam::*:user/${aws:username}"
},
{
"Sid": "DenyAllExceptListedIfNoMFA",
"Effect": "Deny",
"NotAction": [
"iam:CreateVirtualMFADevice",
"iam:EnableMFADevice",
"iam:GetUser",
"iam:ListUsers",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:ResyncMFADevice",
"iam:DeleteVirtualMFADevice",
"iam:ChangePassword",
"iam:CreateLoginProfile",
"sts:GetSessionToken"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
client = boto3.client('iam')
sns = boto3.client('sns')
sts = boto3.client('sts')
iam_resource = boto3.resource('iam')
paginator = client.get_paginator('list_account_aliases')
whitelist_tags = os.environ['WHITELIST_TAG']
response = client.list_users()
url = os.environ['WEBHOOK_URL']
MFA_POLICY_NAME = "ForceMFA"
slack_emoji = ":aws-iam:"
def is_policy_exist(policy_name):
policy_exist = True
account_id = sts.get_caller_identity()['Account']
policy_arn = f'arn:aws:iam::{account_id}:policy/{policy_name}'
try:
# Check if policy exist Fast and direct
_ = client.get_policy(PolicyArn=policy_arn)['Policy']
except client.exceptions.NoSuchEntityException as error:
print("Creating a new ForceMFA Policy")
policy_exist = False
return policy_exist
<key>:<value>. userType: Servicedef is_user_whitelisted(username):
iam_user = iam_resource.User(username)
# key = 'userType'
# value = 'Service'
# If user has no tags, return False
print("is_user_whitelisted",iam_user.tags)
if iam_user.tags == None:
return False
whitelist_tag_list = whitelist_tags.split(',')
for tag_pair in whitelist_tag_list:
key = tag_pair.split(':')[0].strip()
value = tag_pair.split(':')[1].strip()
print(key, value)
for tag in iam_user.tags:
if tag["Key"] == key and tag["Value"].lower() == value.lower():
print("Ignoring user {}. Whitelisted using Tag".format(username))
return True
return False
def is_mfa_enabled(username):
userMfa = client.list_mfa_devices(UserName=username)
print("UserName " + username, userMfa)
if len(userMfa['MFADevices']) == 0:
return False
else:
print("Ignoring user {}. MFA is already enabled".format(username))
return True
def is_policy_attached(user,userPolicyList):
polList = []
userPolicies = userPolicyList['AttachedPolicies']
print("is Policy Attached for + " + user, userPolicies)
for policy in userPolicies:
if policy['PolicyName'] == MFA_POLICY_NAME:
print("Ignoring user {}. MFA Policy already exist".format(user))
return True
return False
# Get number of managed Policies attached to the user
def get_attached_policy_count(username):
# iam_client = get_iam_client()
managed_user_policies = client.list_attached_user_policies(UserName=username)
deny_policy_name = 'ForceMFA'
attached_policies = managed_user_policies['AttachedPolicies']
policy_count = len(attached_policies)
for policy in attached_policies:
# This is to make sure we don't count our very own attached policy. Because that can be deleted and attached again after updating
if policy['PolicyName'] == deny_policy_name:
policy_count = policy_count - 1
return policy_count
def send_slack_notification(status_code,user,account_id):
account_alias = get_account_alias()
payload = ""
if status_code == 1:
payload = """{
\n\t\"channel\": \"aws-custom-alerts\",
\n\t\"username\": \"Enforce MFA\",
\n\t\"icon_emoji\": \"""" + slack_emoji + """\",
\n\t\"attachments\":[\n
{\n
\"fallback\":\"MFA Enabled\",\n
\"pretext\":\"MFA Enabled\",\n
\"color\":\"#34bb13\",\n
\"fields\":[\n
{\n
\"value\":\"*User:* """ + user + """\n*AccountId:* """ + account_id + """\n*Account Alias:* """ + account_alias + """ \"\n
}\n
]\n
}\n
]\n
}"""
elif status_code == 2:
payload = """{
\n\t\"channel\": \"aws-custom-alerts\",
\n\t\"username\": \"Enforce MFA\",
\n\t\"icon_emoji\": \"""" + slack_emoji + """\",
\n\t\"attachments\":[\n
{\n
\"fallback\":\"MFA Enabled\",\n
\"pretext\":\"MFA Enabled\",\n
\"color\":\"#34bb13\",\n
\"fields\":[\n
{\n
\"value\":\":x: Could not attach ForceMFA Policy to the user :x:\n*Reason*: Cannot exceed quota for PoliciesPerUser: 10\n*Account:* """ + account_alias + """\n*User:* """ + user + """\n*AccountId:* """ + account_id + """\",\n
}\n
]\n
}\n
]\n
}"""
time.sleep(3) # To avoid slack api rate limit.
return payload
def lambda_handler(event,context):
# Check if the policy exist in this account. If not create one.
if not is_policy_exist(MFA_POLICY_NAME):
policyStr = json.dumps(policyJson)
client.create_policy(PolicyName=MFA_POLICY_NAME,PolicyDocument=policyStr,Description="Policy Creation from Lambda function - Enforcing MFA")
for user in response['Users']:
username = user['UserName']
userPolicyList = client.list_attached_user_policies(UserName=username)
account_id = sts.get_caller_identity()['Account']
if get_attached_policy_count(username) == 10:
slack_response = requests.request("POST", url, data=send_slack_notification(2,username,account_id), headers=headers)
elif not is_user_whitelisted(username) and not is_policy_attached(username,userPolicyList) and not is_mfa_enabled(username):
policy_arn = f'arn:aws:iam::{account_id}:policy/{MFA_POLICY_NAME}'
response2 = client.attach_user_policy(PolicyArn=policy_arn,UserName=username)
print("Attaching ForceMFA policy to the user {}".format(username))
slack_response = requests.request("POST", url, data=send_slack_notification(1,username,account_id), headers=headers)
.zip format and copy S3 Bucket Name and S3 Key. Enter this data as parameter values in the CloudFormation template.{
"AWSTemplateFormatVersion": "2010-09-09",
"Description" : "Deploy Lambda Function to attach Force MFA policy to the user who had not enabled Physical/Virtual MFA.",
"Parameters" : {
"SlackWebhookParameter" : {
"Type" : "String",
"Default" : "",
"Description" : "Webhook for Slack Channel"
},
"SlackChannelName" : {
"Type" : "String",
"Default" : "",
"Description" : "Name of the slack channel where you want alerts"
},
"WhitelistTag" : {
"Type" : "String",
"Default" : "userType:Service",
"Description" : "List of tags that will be whitelisted."
},
"S3Bucket" : {
"Type" : "String",
"Default" : "",
"Description" : "Name of the S3 bucket where the lambda is stored"
},
"S3Key" : {
"Type" : "String",
"Default" : "",
"Description" : "Key name of the S3 object"
},
"LambdaHandler" : {
"Type" : "String",
"Default" : "",
"Description" : "Lambda Handler name E.g: <file_name>.lambda_handler"
}
},
"Resources": {
"EnforceMFALambda": {
"Type": "AWS::Lambda::Function",
"Properties": {
"FunctionName": "enforceMFA",
"Tags": [
{
"Key": "CreatedBy",
"Value": "Security Team"
}
],
"Handler": { "Ref": "LambdaHandler" },
"Environment" : {
"Variables": {
"WEBHOOK_URL": { "Ref": "SlackWebhookParameter" },
"SLACK_CHANNEL_NAME": { "Ref": "SlackChannelName" },
"WHITELIST_TAG": { "Ref": "WhitelistTag" }
}
},
"Role": {
"Fn::GetAtt": [
"mfaEnforceLambdaRole",
"Arn"
]
},
"Code": {
"S3Bucket": { "Ref": "S3Bucket" },
"S3Key": { "Ref": "S3Key" }
},
"Runtime": "python3.7",
"Timeout": 900
}
},
"mfaEnforceLambdaRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"RoleName": "mfaEnforceLambdaRole",
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": [ "lambda.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}]
},
"Path": "/",
"Policies": [{
"PolicyName": "EnforceMFALambdaPolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:CreatePolicy",
"iam:ListPolicies",
"iam:ListAttachedUserPolicies",
"iam:AttachUserPolicy",
"iam:ListAccountAliases",
"iam:ListUsers",
"iam:ListUserPolicies",
"iam:ListMFADevices",
"iam:ListVirtualMFADevices",
"iam:GetLoginProfile",
"iam:ListUserTags",
"iam:GetAccountSummary",
"iam:GetPolicy",
"iam:GetUser",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:CreateLogGroup"
],
"Resource": "*"
}
]
}
}]
}
},
"ScheduledRule": {
"Type": "AWS::Events::Rule",
"Properties": {
"Description": "Rule to trigger EnforceMFA Lambda",
"Name" : "enforeMFALambdaRule",
"ScheduleExpression": "cron(0 12 * * ? *)",
"State": "ENABLED",
"Targets": [{
"Arn": { "Fn::GetAtt": ["EnforceMFALambda", "Arn"] },
"Id": "TargetFunctionV1"
}]
}
},
"PermissionForEventsToInvokeLambda": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"FunctionName": { "Ref": "EnforceMFALambda" },
"Action": "lambda:InvokeFunction",
"Principal": "events.amazonaws.com",
"SourceArn": { "Fn::GetAtt": ["ScheduledRule", "Arn"] }
}
}
}
}

We have successfully deployed our solution to all the cloud accounts using Cloudformation Stacks and StackSets. To verify that we have deployed it correctly, you can visit any account and check for the Lambda that is created, the Cloudwatch event-pattern rule, and the IAM Role created for the Lambda.
We used Cloudformation StackSets to implement AWS security measures that are scalable. In the future, if there is any new account added to the Organization, the same stack will be created for that account automatically.