AWS Configのカスタムルールによる自動検出と自動修復の仕組みを使って、「全IAMユーザーに自動でMFAを強制する」仕組みを試してみました。
目次
やりたいこと
- AWS Configのカスタムルールで「MFAを通さないと何もできない様なIAMポリシー」が全IAMユーザーにアタッチされているかチェックする
- チェックの除外対象のIAMユーザも設定できるようにする
- 上記IAMポリシーがアタッチされていないIAMユーザーがいたら、自動でアタッチする(=自動修復)
AWS Configとは
AWSリソースの構成管理サービスで、様々なリソースの設定変更の履歴を追ったり、ルールに逸脱するリソースを自動でチェックして通知したりできるサービスです。
AWS Config カスタムルールとは
AWSリソースが、「独自のルール」に準拠しているかどうかを評価できる機能です。
実はConfigにはマネージドルールという、AWSがあらかじめ様々な評価ルールを提供してくれておりそれを設定のみで使用できる便利な機能もあるのですが、必ずしもマネージドルールでは賄えないルールで評価・チェックしたい場合に使用するのがこのカスタムルールになります。
また、カスタムルールの実装には、AWS Lambdaを使用する必要があります。「カスタム」=「自前実装」ということになります。
自動修復とは
上記「マネージドルール」や「カスタムルール」は、ルールに基づいてリソースを「評価」する機能でしたが、準拠していないリソースに対して強制的に準拠させる仕組みが「自動修復」という仕組みになります。
こちらの「自動修復」も実はAWS Configでは「AWS Systems Manager」の「Automation」という、AWSがある程度用意してくれているテンプレート化されたドキュメントをもとに手順を自動実行するサービスを使用することができるため、ノンコーディングで自動修復を行うことができるのです。
しかし、これもマネージドルールと同様、あらかじめ提供されている修復方法で実現できるのであれば良いですが、そうではない独自の修復を行いたい場合、またはAWS Systems ManagerのAutomationでは表しきれない様な修復を行いたい場合は、カスタムルールと同様AWS Lambdaで自前で実装することで独自の修復が行えるようになります。
前提
- デプロイしたいリージョンでAWS Configが有効化されていること
- リージョンはどこでもOKです
- そのためAWS Configまわりでは設定レコーダーなどのお話はせずConfig Ruleのみに絞ります
使用技術
方法
やりたいこと(再掲)
- AWS Configのカスタムルールで「MFAを通さないと何もできない様なIAMポリシー」が全IAMユーザーにアタッチされているかチェックする
- チェックの除外対象のIAMユーザも設定できるようにする
- 上記IAMポリシーがアタッチされていないIAMユーザーがいたら、自動でアタッチする(=自動修復)
作るもの
- SAM(CloudFormation)用template.yml
- Config Rule(カスタムルール)
- CloudWatch Events(カスタムルールに準拠しない際の自動修復)
- カスタムルール用Lambda
- 自動修復用Lambda
- Lambda用のIAM Role, Permission, LogGroupなど諸々
- SSM Parameter Store
- テキストファイルから読み込んだホワイトリストユーザを格納します
- MFAをしないと何も操作できなくするIAMポリシー
- 実は一番大事な部分です
- ソースコード(Python)
- カスタムルール用Lambda
- 自動修復用Lambda
- ホワイトリストユーザ用テキストファイル
- カスタムルールでの評価対象から外すユーザを指定する
- デプロイシェル
ディレクトリ構成
. ├── src │ ├── check_mfa_policy_attached_for_iam_users.py │ ├── remediation_mfa_policy_attach_for_iam_users.py │ └── requirements.txt ├── whitelist_users │ └── sample.txt ├── deploy.sh └── template.yml
実装
SAM(CloudFormation)用template.yml
- template.yml
AWSTemplateFormatVersion: 2010-09-09 Transform: AWS::Serverless-2016-10-31 Description: Config Rule For Check MFAPolicy Attached For IAMUsers Parameters: WhitelistUsers: Type: CommaDelimitedList Default: "" DenyActionWithoutMFAPolicyName: Type: String Default: "DenyActionWithoutMFAPolicy" Resources: # ----------------------------------------------------------# # Config Role for Lambda # ----------------------------------------------------------# LambdaForConfigRole: Type: AWS::IAM::Role Properties: RoleName: "LambdaForConfigRole" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWS_ConfigRole - arn:aws:iam::aws:policy/SecurityAudit - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess Path: / AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" LambdaForConfigPolicy: Type: "AWS::IAM::ManagedPolicy" Properties: ManagedPolicyName: "LambdaForConfigManagedPolicy" Roles: - !Ref LambdaForConfigRole PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - "config:PutEvaluations" Resource: "*" - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "arn:aws:logs:*:*:*" - Effect: Allow Action: - "iam:AttachUserPolicy" Resource: "*" - Effect: Allow Action: - "ssm:DescribeParameters" Resource: "*" - Effect: Allow Action: - "ssm:GetParameters" Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/mfa/*" # ----------------------------------------------------------# # Lambda Functions # ----------------------------------------------------------# CheckMFAPolicyAttachForIAMUsers: Type: AWS::Serverless::Function Properties: CodeUri: src Handler: check_mfa_policy_attached_for_iam_users.lambda_handler Runtime: python3.8 Timeout: 180 Role: !GetAtt LambdaForConfigRole.Arn Environment: Variables: TARGET_POLICY_NAME: !Ref DenyActionWithoutMFAPolicyName SSM_PARAM_KEY: !Ref MFAWhiteUsersParameter Tracing: Active RemediationMFAPolicyAttachForIAMUsers: Type: AWS::Serverless::Function Properties: CodeUri: src Handler: RemediationMFAPolicyAttachForIAMUsers.lambda_handler Runtime: python3.8 Timeout: 180 Role: !GetAtt LambdaForConfigRole.Arn Environment: Variables: CONFIG_RULE_NAME: !Ref MFAPolicyAttachConfigRule TARGET_POLICY_ARN: !Ref DenyActionWithoutMFAPolicy SSM_PARAM_KEY: !Ref MFAWhiteUsersParameter Tracing: Active # ----------------------------------------------------------# # Lambda Log Groups # ----------------------------------------------------------# CheckMFAPolicyAttachForIAMUsersLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${CheckMFAPolicyAttachForIAMUsers} RetentionInDays: 90 RemediationMFAPolicyAttachForIAMUsersLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub /aws/lambda/${RemediationMFAPolicyAttachForIAMUsers} RetentionInDays: 90 # ----------------------------------------------------------# # Lambda Permissions # ----------------------------------------------------------# CheckMFAPolicyAttachForIAMUsersPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt CheckMFAPolicyAttachForIAMUsers.Arn Principal: config.amazonaws.com RemediationMFAPolicyAttachForIAMUsersPermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt RemediationMFAPolicyAttachForIAMUsers.Arn Principal: events.amazonaws.com # ----------------------------------------------------------# # Config Custom Rules # ----------------------------------------------------------# MFAPolicyAttachConfigRule: Type: AWS::Config::ConfigRule DependsOn: - CheckMFAPolicyAttachForIAMUsersPermission Properties: ConfigRuleName: mfa-policy-attach Scope: ComplianceResourceTypes: - "AWS::IAM::User" - "AWS::IAM::Policy" Source: Owner: CUSTOM_LAMBDA SourceIdentifier: !GetAtt CheckMFAPolicyAttachForIAMUsers.Arn SourceDetails: - EventSource: "aws.config" MessageType: "ConfigurationItemChangeNotification" - EventSource: "aws.config" MessageType: "OversizedConfigurationItemChangeNotification" - EventSource: "aws.config" MessageType: "ScheduledNotification" MaximumExecutionFrequency: One_Hour # ----------------------------------------------------------# # Remediation Events Rules # ----------------------------------------------------------# RemediationMFAPolicyAttachForIAMUsersEvents: Type: "AWS::Events::Rule" DependsOn: - RemediationMFAPolicyAttachForIAMUsersPermission Properties: Description: CloudWatch Events about a Config Rule for IAM Users not Attached the MFA Policy. EventPattern: source: - aws.config detail-type: - Config Rules Compliance Change detail: messageType: - ComplianceChangeNotification configRuleName: - !Ref MFAPolicyAttachConfigRule resourceType: - "AWS::IAM::User" newEvaluationResult: complianceType: - NON_COMPLIANT Name: remediation-mfa-policy-attach State: ENABLED Targets: - Arn: !GetAtt RemediationMFAPolicyAttachForIAMUsers.Arn Id: lambda # ----------------------------------------------------------# # SSM Parameter for MFA White Users # ----------------------------------------------------------# MFAWhiteUsersParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub "/mfa/WHITELIST_USERS" Type: StringList Value: !Join - "," - !Ref WhitelistUsers Description: SSM Parameter for MFA White Users StringList. # ----------------------------------------------------------# # MFA Policy # ----------------------------------------------------------# DenyActionWithoutMFAPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Ref DenyActionWithoutMFAPolicyName PolicyDocument: Version: "2012-10-17" Statement: - Sid: DenyDetachOthersPolicy Effect: Deny Action: - iam:DetachUserPolicy - iam:DetachGroupPolicy NotResource: - "arn:aws:iam::*:user/${aws:username}" Condition: ArnEquals: iam:PolicyArn: - "arn:aws:iam::*:policy/DenyActionWithoutMFAPolicy" - Sid: DenyDeleteAndChangePolicy Effect: Deny Action: - iam:CreatePolicyVersion - iam:DeletePolicyVersion - iam:SetDefaultPolicyVersion - iam:DeletePolicy Resource: - "arn:aws:iam::*:policy/DenyActionWithoutMFAPolicy" - Sid: AllowViewAccountInfo Effect: Allow Action: - iam:GetAccountPasswordPolicy - iam:GetAccountSummary - iam:ListVirtualMFADevices - iam:ListAccountAliases - iam:ListUsers Resource: "*" - Sid: AllowManageOwnPasswords Effect: Allow Action: - iam:ChangePassword - iam:GetUser - iam:CreateLoginProfile - iam:DeleteLoginProfile - iam:GetLoginProfile - iam:UpdateLoginProfile 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: DenyAllExceptListIfNoMFAOrGreaterThanTime Effect: Deny NotAction: - iam:CreateVirtualMFADevice - iam:EnableMFADevice - iam:ChangePassword - iam:GetAccountPasswordPolicy - iam:CreateLoginProfile - iam:DeleteLoginProfile - iam:GetLoginProfile - iam:UpdateLoginProfile - iam:GetUser - iam:ListMFADevices - iam:ListVirtualMFADevices - iam:ResyncMFADevice - sts:GetSessionToken Resource: "*" Condition: NumericGreaterThanIfExists: aws:MultiFactorAuthAge: 3600 # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: LambdaForConfigRoleArn: Value: !GetAtt LambdaForConfigRole.Arn Export: Name: !Sub "LambdaForConfigRoleArn"
ソースコード(Python)
- requirements.txt
boto3
- check_mfa_policy_attached_for_iam_users.py(カスタムルール)
import boto3 import logging import os from datetime import datetime session = boto3.Session() iam_client = session.client('iam') config_client = session.client('config') ssm_client = session.client('ssm') target_policy_name = os.environ['TARGET_POLICY_NAME'] ssm_param_key = os.environ['SSM_PARAM_KEY'] logger = logging.getLogger() logger.setLevel(logging.INFO) def get_parameters(): response = ssm_client.get_parameters( Names=[ ssm_param_key, ], WithDecryption=True ) for parameter in response['Parameters']: return parameter['Value'] def lambda_handler(event, _context): logger.info(f'event: {str(event)}') result_token = event['resultToken'] try: ssm_value = get_parameters() whitelist_users = [val.strip() for val in ssm_value.split(',')] iam_users = iam_client.list_users() if not iam_users: logger.info('IAM User does not exist') return for user in iam_users['Users']: user_name = user['UserName'] if user_name not in whitelist_users: evaluate_compliance( user_name, user['UserId'], result_token) else: logger.info(f'{user_name} is in whitelists') except Exception as e: logger.error(f'error: {str(e)}') def evaluate_compliance(user_name, user_id, result_token): try: user_policies = iam_client.list_attached_user_policies( UserName=user_name) compliance_type = 'NON_COMPLIANT' for policy_name in user_policies['AttachedPolicies']: if target_policy_name in policy_name['PolicyName']: compliance_type = 'COMPLIANT' logger.info(f'{user_name} is {compliance_type}') config_client.put_evaluations( Evaluations=[ { 'ComplianceResourceType': 'AWS::IAM::User', 'ComplianceResourceId': user_id, 'ComplianceType': compliance_type, 'OrderingTimestamp': datetime.today() } ], ResultToken=result_token ) except Exception as e: return e
- remediation_mfa_policy_attach_for_iam_users.py(自動修復)
import boto3 import logging import os session = boto3.Session() iam_client = session.client('iam') ssm_client = session.client('ssm') config_rule_name = os.environ['CONFIG_RULE_NAME'] target_policy_arn = os.environ['TARGET_POLICY_ARN'] ssm_param_key = os.environ['SSM_PARAM_KEY'] logger = logging.getLogger() logger.setLevel(logging.INFO) def get_parameters(): response = ssm_client.get_parameters( Names=[ ssm_param_key, ], WithDecryption=True ) for parameter in response['Parameters']: return parameter['Value'] def lambda_handler(event, _context): logger.info(f"event: {str(event)}") if 'detail' in event: detail = event['detail'] if 'configRuleName' in detail and detail['configRuleName'] == config_rule_name: try: ssm_value = get_parameters() whitelist_users = [val.strip() for val in ssm_value.split(',')] users = iam_client.list_users() for user in users['Users']: if detail['resourceId'] == user['UserId'] and user['UserName'] not in whitelist_users: user_policies = iam_client.list_attached_user_policies( UserName=user['UserName']) for policy in user_policies['AttachedPolicies']: if policy['PolicyArn'] == target_policy_arn: return iam_client.attach_user_policy( UserName=user['UserName'], PolicyArn=target_policy_arn, ) logger.info(f"user: {user['UserName']}") except Exception as e: logger.error(f"error: {str(e)}")
ホワイトリストユーザ用テキストファイル
- whitelist_users/sample.txt
Test-Back-Deploy-User Test-Back-Monitor-User
デプロイシェル
- deploy.sh
#!/bin/bash set -eu cd $(dirname $0) TEMPLATE_FILE="template.yml" STACK_BUCKET="config-custom-rules" WHITELIST_USER_FILE="./whitelist_users/sample.txt" REGION="ap-northeast-1" STACK_NAME="ConfigCustomRules" WHITELIST_USERS="," if [ -f ${WHITELIST_USER_FILE} ]; then for user in $(cat ${WHITELIST_USER_FILE} | tr -d " \t" | grep -v "^#" | sed -e "s/\([^#]*\)#.*$/\1/g"); do if [ -n "${user}" ]; then WHITELIST_USERS="${user},${WHITELIST_USERS}" fi done fi BUCKET_PREFIX=$(aws sts get-caller-identity \ --query "Account" \ --output text \ --region ${REGION}) BUCKET_PATH="${BUCKET_PREFIX}-${STACK_BUCKET}" if [ -z "$(aws s3 ls | grep ${BUCKET_PATH})" ]; then aws s3 mb "s3://${BUCKET_PATH}" fi sam build --template-file ${TEMPLATE_FILE} sam deploy \ --region ${REGION} \ --stack-name ${STACK_NAME} \ --s3-bucket ${BUCKET_PATH} \ --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ --parameter-overrides \ WhitelistUsers=${WHITELIST_USERS} \ --no-fail-on-empty-changeset
解説
SAM(CloudFormation)用template.yml
- Config Rule(カスタムルール)
カスタムルールの評価タイミングは「IAMユーザ・IAMポリシーに変更(作成・削除も)があった時」「1時間に1回」というような指定が可能です。
また、ここでカスタムルール実装Lambda ARNを指定します。
# ----------------------------------------------------------# # Config Custom Rules # ----------------------------------------------------------# MFAPolicyAttachConfigRule: Type: AWS::Config::ConfigRule DependsOn: - CheckMFAPolicyAttachForIAMUsersPermission Properties: ConfigRuleName: mfa-policy-attach Scope: ComplianceResourceTypes: - "AWS::IAM::User" - "AWS::IAM::Policy" Source: Owner: CUSTOM_LAMBDA SourceIdentifier: !GetAtt CheckMFAPolicyAttachForIAMUsers.Arn SourceDetails: - EventSource: "aws.config" MessageType: "ConfigurationItemChangeNotification" - EventSource: "aws.config" MessageType: "OversizedConfigurationItemChangeNotification" - EventSource: "aws.config" MessageType: "ScheduledNotification" MaximumExecutionFrequency: One_Hour
- CloudWatch Events(カスタムルールに準拠しない際の自動修復)
こちらも同じ様に発火のトリガーを指定します。
これは上記Configカスタムルールで"AWS::IAM::User"に対してNON_COMPLIANT
信号を受けた時に発火する、CloudWatch Eventsのイベントルールになります。
# ----------------------------------------------------------# # Remediation Events Rules # ----------------------------------------------------------# RemediationMFAPolicyAttachForIAMUsersEvents: Type: "AWS::Events::Rule" DependsOn: - RemediationMFAPolicyAttachForIAMUsersPermission Properties: Description: CloudWatch Events about a Config Rule for IAM Users not Attached the MFA Policy. EventPattern: source: - aws.config detail-type: - Config Rules Compliance Change detail: messageType: - ComplianceChangeNotification configRuleName: - !Ref MFAPolicyAttachConfigRule resourceType: - "AWS::IAM::User" newEvaluationResult: complianceType: - NON_COMPLIANT Name: remediation-mfa-policy-attach State: ENABLED Targets: - Arn: !GetAtt RemediationMFAPolicyAttachForIAMUsers.Arn Id: lambda
- SSM Parameter Store
テキストファイルから読み込んだホワイトリストユーザを格納します。
Value:
に!Join
で","
をつけているのは、!Ref WhitelistUsers
が空だったときのために頭にカンマをつけています。
# ----------------------------------------------------------# # SSM Parameter for MFA White Users # ----------------------------------------------------------# MFAWhiteUsersParameter: Type: AWS::SSM::Parameter Properties: Name: !Sub "/mfa/WHITELIST_USERS" Type: StringList Value: !Join - "," - !Ref WhitelistUsers Description: SSM Parameter for MFA White Users StringList.
- MFA強制IAMポリシー
- 自分以外のこのポリシーのデタッチは禁止
Sid: DenyDetachOthersPolicy
- このポリシー自体の編集や削除は禁止
Sid: DenyDeleteAndChangePolicy
- その他アクション
- MFAを通さないと基本操作は全て拒否
- MFAを通さなくてもできないといけないものは許可
NumericGreaterThanIfExists: aws:MultiFactorAuthAge: 3600
- MFAをしていても1時間以上経過していたらDenyする
- 自分以外のこのポリシーのデタッチは禁止
このIAMポリシーが実は一番大事な部分で、Configカスタムルール・自動修復でチェックし・各IAMユーザにアタッチするポリシーになります。
このIAMポリシーがアタッチされているIAMユーザは、MFAをしないと基本的な操作は何もできなくなります(「MFA設定」自体など、一部除外アクションあり)。
# ----------------------------------------------------------# # MFA Policy # ----------------------------------------------------------# DenyActionWithoutMFAPolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Ref DenyActionWithoutMFAPolicyName PolicyDocument: Version: "2012-10-17" Statement: - Sid: DenyDetachOthersPolicy Effect: Deny Action: - iam:DetachUserPolicy - iam:DetachGroupPolicy NotResource: - "arn:aws:iam::*:user/${aws:username}" Condition: ArnEquals: iam:PolicyArn: - "arn:aws:iam::*:policy/DenyActionWithoutMFAPolicy" - Sid: DenyDeleteAndChangePolicy Effect: Deny Action: - iam:CreatePolicyVersion - iam:DeletePolicyVersion - iam:SetDefaultPolicyVersion - iam:DeletePolicy Resource: - "arn:aws:iam::*:policy/DenyActionWithoutMFAPolicy" - Sid: AllowViewAccountInfo Effect: Allow Action: - iam:GetAccountPasswordPolicy - iam:GetAccountSummary - iam:ListVirtualMFADevices - iam:ListAccountAliases - iam:ListUsers Resource: "*" - Sid: AllowManageOwnPasswords Effect: Allow Action: - iam:ChangePassword - iam:GetUser - iam:CreateLoginProfile - iam:DeleteLoginProfile - iam:GetLoginProfile - iam:UpdateLoginProfile 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: DenyAllExceptListIfNoMFAOrGreaterThanTime Effect: Deny NotAction: - iam:CreateVirtualMFADevice - iam:EnableMFADevice - iam:ChangePassword - iam:GetAccountPasswordPolicy - iam:CreateLoginProfile - iam:DeleteLoginProfile - iam:GetLoginProfile - iam:UpdateLoginProfile - iam:GetUser - iam:ListMFADevices - iam:ListVirtualMFADevices - iam:ResyncMFADevice - sts:GetSessionToken Resource: "*" Condition: NumericGreaterThanIfExists: aws:MultiFactorAuthAge: 3600
上記MFA強制ポリシーの補足
上記ポリシーでなぜ、「自分以外のこのポリシーのデタッチは禁止」「このポリシー自体の編集や削除は禁止」を設定する必要があったかと言うと、下記の課題・問題がありました。
- ①「デタッチ」か「ポリシー編集・削除」を許可しないと、いざというときのデタッチ方法がなくなる
- ②間違えてこのポリシーの削除をしようとしてしまったとき、削除自体は禁止されるが、その際にこのポリシーが付いている全IAMユーザからデタッチされてしまう
①「デタッチ」か「ポリシー編集・削除」を許可しないと、いざというときのデタッチ方法がなくなる
①に関しては、全IAMユーザにMFA強制ポリシーがアタッチされる組織環境においては、このポリシーがついたユーザがいざと言うときにデタッチできる必要があります。そこで、「デタッチ」か「ポリシー編集・削除」のどちらかを許可できるようにする必要がありました。
しかし「デタッチ禁止」にすると、いざデタッチするときにポリシー自体の一時的な編集か削除が必要になり、その際にデタッチ対象以外のIAMユーザにも影響してしまう問題があります。
そのため、「デタッチ禁止」する代わりに「ポリシー編集・削除」を禁止して、デタッチを許可する方向にしました。
(ここでは「許可」と言っていますが、「デタッチのAllow」をこちらのポリシーで書いているわけではないので、もともとIAMポリシーの操作権限を持っていないIAMユーザであればデタッチ自体もできません。)
これにより、もともとIAMポリシーの操作権限を持つIAMユーザであれば自前で誰でもデタッチできてしまうようになりますが、その点はConfigのカスタムルール・自動修復ですぐ再アタッチされるため良しとしました。
また、デタッチして自動修復が走る前にすぐMFA強制ポリシーの削除もできてしまいますが、それは「デタッチ禁止にしてポリシー修正OK」の場合でもポリシー自体を修正してしまえば削除できるため、比較対象・要因には含んでいません。
②間違えてこのポリシーの削除をしようとしてしまったとき、削除自体は禁止されるが、その際にこのポリシーが付いている全IAMユーザからデタッチされてしまう
②に関して、AWSのIAM削除の挙動の問題になります。
①によって「ポリシーの削除は禁止されているが、デタッチは許可されている」場合、IAMのDeletePolicyアクションでこのポリシーを削除しようとすると、削除自体は禁止されるが、その際にこのMFA強制ポリシーが付いている全IAMユーザからポリシーがデタッチされてしまうという挙動になってしまいます。
これはIAMポリシー削除の際に、IAMサービス内部で「全IAMユーザからポリシーをデタッチ → ポリシー自体を削除」という処理が走るためです。
このケースだとまず「デタッチ」が権限的に許可されているため全IAMユーザからのデタッチが成功し、次の「削除」処理はMFA強制ポリシーによって禁止されているので、「全IAMユーザからポリシーがデタッチされた状態でIAMポリシーは削除されずに残る」というような挙動になります。
たとえ間違えて削除しようとしてしまったとき、削除は禁止されて安心ですが、全IAMユーザからMFA強制ポリシーが外れてしまい中々大変なことになるので、「自分以外のこのポリシーのデタッチは禁止」というステートメントを追加するに至りました。
これによって間違えて削除しようとしたときも、他のIAMユーザのデタッチは走らずに済みます。
ソースコード(Python)
- check_mfa_policy_attached_for_iam_users.py(カスタムルール)
ホワイトリスト用ユーザはSSMパラメータストアから取得します。
カンマで区切られているため、バラして配列に格納します。
def get_parameters(): response = ssm_client.get_parameters( Names=[ ssm_param_key, ], WithDecryption=True ) for parameter in response['Parameters']: return parameter['Value'] ... ... ... ssm_value = get_parameters() whitelist_users = [val.strip() for val in ssm_value.split(',')]
SDKで取得したIAMユーザをループして、ホワイトリストユーザでなかったらevaluate_compliance
を呼びます。
for user in iam_users['Users']: user_name = user['UserName'] if user_name not in whitelist_users: evaluate_compliance( user_name, user['UserId'], result_token) else: logger.info(f'{user_name} is in whitelists')
evaluate_compliance
では、IAMユーザのIAMポリシーを取得して、アタッチ対象のMFA強制ポリシーがすでについていればCOMPLIANT
(準拠)、ついていなければNON_COMPLIANT
(非準拠)をput_evaluations
によってConfigに送信します。
def evaluate_compliance(user_name, user_id, result_token): try: user_policies = iam_client.list_attached_user_policies( UserName=user_name) compliance_type = 'NON_COMPLIANT' for policy_name in user_policies['AttachedPolicies']: if target_policy_name in policy_name['PolicyName']: compliance_type = 'COMPLIANT' logger.info(f'{user_name} is {compliance_type}') config_client.put_evaluations( Evaluations=[ { 'ComplianceResourceType': 'AWS::IAM::User', 'ComplianceResourceId': user_id, 'ComplianceType': compliance_type, 'OrderingTimestamp': datetime.today() } ], ResultToken=result_token ) except Exception as e: return e
- remediation_mfa_policy_attach_for_iam_users.py(自動修復)
カスタムルールでNON_COMPLIANT
と評価されたIAMユーザに対して、再度ホワイトリストユーザではないか・アタッチ対象のMFA強制ポリシーがついていないかをチェックし、やはりついていない場合はattach_user_policy
というboto3のiamクライアントのメソッドでMFA強制ポリシーをアタッチします。
for user in users['Users']: if detail['resourceId'] == user['UserId'] and user['UserName'] not in whitelist_users: user_policies = iam_client.list_attached_user_policies( UserName=user['UserName']) for policy in user_policies['AttachedPolicies']: if policy['PolicyArn'] == target_policy_arn: return iam_client.attach_user_policy( UserName=user['UserName'], PolicyArn=target_policy_arn, ) logger.info(f"user: {user['UserName']}")
ホワイトリストユーザ用テキストファイル
- whitelist_users/sample.txt
こちらに記載したIAMユーザには、MFA強制IAMポリシーがアタッチされていなくても検知対象にはなりません。
MFAを実行するのが難しいIAMユーザ(CI/CDユーザ・外部SaaS用モニタリングユーザなど)を指定します。
Test-Back-Deploy-User Test-Back-Monitor-User
デプロイシェル
- deploy.sh
ホワイトリストユーザ用ファイルを読み込んでカンマ区切りにします。
WHITELIST_USERS="," if [ -f ${WHITELIST_USER_FILE} ]; then for user in $(cat ${WHITELIST_USER_FILE} | tr -d " \t" | grep -v "^#" | sed -e "s/\([^#]*\)#.*$/\1/g"); do if [ -n "${user}" ]; then WHITELIST_USERS="${user},${WHITELIST_USERS}" fi done fi
デプロイ方法
- AWS SAMが走ります。
sh deploy.sh
補足
注意点
AWS Configで非準拠検知→自動修復には最大で数分のラグが発生するので、完全にリアルタイムで対応したい場合はConfigを通さず、CloudWatch EventsでIAM操作を直接トリガーして Lambdaで対応という手法も可能です。
Configのメリット
それでもConfigでやるメリットは、以下が思い浮かびます。
- リソースの準拠・非準拠情報が履歴として追える
- Security Hubと統合・一元管理できる
最後に
今回もかなり長くなってしまいました。。。
このような仕組みをしている会社さんはかなりあると思いますが、中々ここまで詳しく書いてあるのも少なかったので、今回記載するに至りました。