AWS Configのカスタムルールと自動修復でMFAを全IAMユーザーに強制する

AWS Configのカスタムルールによる自動検出と自動修復の仕組みを使って、「全IAMユーザーに自動でMFAを強制する」仕組みを試してみました。


目次

目次


やりたいこと

  • AWS Configカスタムルールで「MFAを通さないと何もできない様なIAMポリシー」が全IAMユーザーにアタッチされているかチェックする
    • チェックの除外対象のIAMユーザも設定できるようにする
  • 上記IAMポリシーがアタッチされていないIAMユーザーがいたら、自動でアタッチする(=自動修復


AWS Configとは

AWSリソースの構成管理サービスで、様々なリソースの設定変更の履歴を追ったり、ルールに逸脱するリソースを自動でチェックして通知したりできるサービスです。

docs.aws.amazon.com


AWS Config カスタムルールとは

AWSリソースが、「独自のルール」に準拠しているかどうかを評価できる機能です。


実はConfigにはマネージドルールという、AWSがあらかじめ様々な評価ルールを提供してくれておりそれを設定のみで使用できる便利な機能もあるのですが、必ずしもマネージドルールでは賄えないルールで評価・チェックしたい場合に使用するのがこのカスタムルールになります。

docs.aws.amazon.com


また、カスタムルールの実装には、AWS Lambdaを使用する必要があります。「カスタム」=「自前実装」ということになります。


自動修復とは

上記「マネージドルール」や「カスタムルール」は、ルールに基づいてリソースを「評価」する機能でしたが、準拠していないリソースに対して強制的に準拠させる仕組みが「自動修復」という仕組みになります。


こちらの「自動修復」も実はAWS ConfigではAWS Systems Manager」「Automation」という、AWSがある程度用意してくれているテンプレート化されたドキュメントをもとに手順を自動実行するサービスを使用することができるため、ノンコーディングで自動修復を行うことができるのです。


しかし、これもマネージドルールと同様、あらかじめ提供されている修復方法で実現できるのであれば良いですが、そうではない独自の修復を行いたい場合、またはAWS Systems ManagerのAutomationでは表しきれない様な修復を行いたい場合は、カスタムルールと同様AWS Lambdaで自前で実装することで独自の修復が行えるようになります。


前提

  • デプロイしたいリージョンでAWS Configが有効化されていること
    • リージョンはどこでもOKです
    • そのためAWS Configまわりでは設定レコーダーなどのお話はせずConfig Ruleのみに絞ります


使用技術

  • AWS Config
    • Config Rule
  • CloudWatch Events
  • IAM Role/Policy
  • Lambda
  • AWS SAM
    • デプロイに使用します


方法

やりたいこと(再掲)

  • 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


ソースコードGitHubにアップしています。

github.com


実装

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と統合・一元管理できる


最後に

今回もかなり長くなってしまいました。。。

このような仕組みをしている会社さんはかなりあると思いますが、中々ここまで詳しく書いてあるのも少なかったので、今回記載するに至りました。