AWSマルチアカウント運用時の脅威検出の導入


こんにちは、Hamee株式会社でSREとして働いています。大嶋です。
普段は[NextEngine](https://next-engine.net/)のクラウド化案件を担当しています。

この記事は[Hamee Advent Calendar 2020](https://qiita.com/advent-calendar/2020/hamee)の9日目になります。
また、前職でテックブログ執筆のために準備していた記事を修正し公開しています。
（公開のタイミングを見失っていたためこのタイミングで公開させてください）
そのためやってみた記事と、今後弊社で個人的に取り入れていきたい仕組みの紹介（提案）となります。

それではAWSマルチアカウント運用時の脅威検出の導入の取り組みについて、紹介させて頂きます。

# 目次
- 概要
    - 課題
    - 利用AWSサービス説明
        - CloudFormationStackSets
        - GuardDuty
    - 全体イメージ
- CloudFormationStackSets
    - 必要なリソース、用途
    - 構築手順

- GuardDuty
    - 必要なリソース、用途
    - 構築手順
   
- Slack通知
    - 必要なリソース、用途
    - 構築手順
    - AWS Chatbotとの比較
- まとめ




# 概要
AWSアカウントを複数所有していて、プロダクト、組織、用途（Dev、Stg、Prod）などで分割され、AWS Organizationsを利用しマルチアカウントを管理しています。
以下の図はre:invent2018での資料（日本語版）ですが、用途ごとにアカウントを分割し管理することをベストプラクティスとしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/ca1faa6b-fd54-f93b-78b4-5df9acaefb8d.png)
https://d0.awsstatic.com/events/jp/2017/summit/slide/D4T2-2.pdf

今後更にアカウントが増加していく可能性がある中で、いくつか課題がありました。

## 課題
1.　すべてのアカウントの設定が必要な場合はそれぞれのアカウントに都度ログインする必要があり大変
2.　アカウント担当者が適切に外部からの脅威を防げているのか不明であり、セキュリティホールが存在or今後発生する可能性がある

## 使用AWSサービス概要
今回利用したサービスを紹介させていただきます
### CloudFormation StackSets
![AWS-CloudFormation@4x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/f60ef8e2-115e-7769-cd66-4bb089224519.png)

AWS CloudFormationは弊社でもデフォルトで使われるサービスですが、CloudFormation StackSetsは複数のアカウント及びリージョンに対してスタックを作成、更新、削除できるサービスです。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/what-is-cfnstacksets.html

今回は大規模マルチアカウントを運用することになり、一つ一つのアカウントに対してCloudFormationを実施する必要があることが今後現実的に厳しいことからCloudFormation StackSetsを採用することにしました。

### GuardDuty
![Amazon-GuardDuty@4x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/5359c580-0fe5-21b5-31de-de6ef67b046e.png)

悪意のある操作、不正な動作について脅威検出するサービスです。機械学習、異常検出等を利用し、潜在的な脅威なども識別することができます。AWSの複数のサービスを利用しイベントを分析することが可能です。
https://aws.amazon.com/jp/guardduty/

CloudTrail、CloudWatchなどを利用し独自カスタマイズし脅威検出を導入することもできるのですが、今回は以下2つの理由からAWSのマネージド脅威検出サービスであるGuardDutyを採用しました。

- 脅威検出の初手ということでなるべく早く導入したかった
- アカウント数が多くすべてのユースケースに対応したカスタマイズすることが困難であった

## 全体イメージ
上記サービスを利用し、今回どのような仕組みを構築したのか紹介させて頂きます。
全体イメージとしては以下のようになります
![全体イメージ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/cb199f29-97c5-ea2e-52d0-ca57514c915a.jpeg)
今回はAWSアカウントを3パターンに分けています
1. マスターアカウント
　　AWS Organizationsを管理しているマスターアカウントです。
2. メンバーアカウント（セキュリティ用）
　　マスターアカウントに管理されているメンバーアカウントの中でセキュリティに関することを取り扱うアカウントです。
　　今回はGuardDutyのログ集約やSlack通知を実行します。
3. メンバー（一般）
　　マスターアカウントに管理されているメンバーアカウントです。

また今回はマスターアカウントからCloudFormation StackSetsを実行できるようにするだけでなく、セキュリティに関するCloudFormation StackSetsはメンバーアカウント（セキュリティ用）から実行できる設計にしました。
そうすることでセキュリティ担当者がマスターアカウントにログインする権限が不要になり、マスターアカウントにIAMユーザーを作成する必要がなくなります。
特にマスターアカウントにはセキュリティ担当者など不要なIAMユーザーを作成することは避けたかったことが理由になります。

次からは構築手順を順を追って紹介させて頂きます。

#CloudFormationStackSets
## 必要なリソース、用途
- Administration IAM Role（管理用）
  - マスターアカウントからExecution IAM Role（管理用）を保持しているアカウントに対してCloudFormationStackSetsを実行できるようにするRole
- Execution IAM Role（管理用）
 - メンバーアカウント（セキュリティ用、一般）がCloudFormationStackSetsを実行できるようにするRole
- Administration IAM Role（セキュリティ用）
 - メンバーアカウント（セキュリティ用）からExecution IAM Role（セキュリティ用）を保持しているアカウントに対してCloudFormationStackSetsを実行できるようにするRole
- Execution IAM Role（セキュリティ用）
 - マスターアカウント、メンバーアカウント（一般）がCloudFormationStackSetsを実行できるようにするRole

先程の3パターンのアカウントに配置する各リソースの表はこちらです。

| |Administration IAM Role（管理用）|Execution IAM Role（管理用）|Administration IAM Role（セキュリティ用）|Execution IAM Role（セキュリティ用）|
|:--:|:--:|:--:|:--:|:--:|
|マスターアカウント|◯|||◯|
|メンバーアカウント（セキュリティ用）||◯|◯||
|メンバーアカウント（一般）||◯||◯|

このように配置することで、先程記述したマスターアカウントからもメンバーアカウント（セキュリティ用）どちらからもCloudFormationStackSetsが実行できるようになります。

- マスターアカウントから実行する場合
![master_account_stacksets.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/412c8cc8-ab49-7f32-f96a-6bc8380cac9a.jpeg)

- メンバーアカウント（セキュリティ用）から実行する場合![security_account_stacksets.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/e1de5dbb-5c56-2ebf-c3f0-4065647a2a41.jpeg)




それぞれリソースのコードは以下です。

### Administration IAM Role（管理用）

```yml:AWSCloudFormationStackSetAdministrationRole.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the AWSCloudFormationStackSetAdministrationRole to enable use of AWS CloudFormation StackSets.

Resources:
  AdministrationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AWSCloudFormationStackSetAdministrationRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: AssumeRole-AWSCloudFormationStackSetExecutionRole
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource:
                  - "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRole"

```


### Execution IAM Role（管理用）

```yml:AWSCloudFormationStackSetExecutionRole.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the AWSCloudFormationStackSetExecutionRole to enable use of your account as a target account in AWS CloudFormation StackSets.

Parameters:
  AdministratorAccountId:
    Type: String
    Description: AWS Account Id of the administrator account (the account in which StackSets will be created).
    MaxLength: 12
    MinLength: 12

Resources:
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: AWSCloudFormationStackSetExecutionRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Ref AdministratorAccountId
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess



```


### Administration IAM Role（セキュリティ用）

```yml:AWSCloudFormationStackSetAdministrationRoleForOtherAccount.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the AWSCloudFormationStackSetAdministrationRole to enable use of AWS CloudFormation StackSets.

Parameters:
  RoleSuffix:
    Type: String
    Description: IAM Role Suffix.CamelCase highly reccomend. ex) When RoleSuffix is SecurityAccount, AWSCloudFormationStackSetAdministrationRoleForSecurityAccount


Resources:
  AdministrationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub AWSCloudFormationStackSetAdministrationRoleFor${RoleSuffix}
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub AssumeRole-AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource:
                  - !Sub "arn:aws:iam::*:role/AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix}"

```


### Execution IAM Role（セキュリティ用）
```yml:AWSCloudFormationStackSetExecutionRoleForOtherAccount.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Configure the AWSCloudFormationStackSetExecutionRole to enable use of your account as a target account in AWS CloudFormation StackSets.

Parameters:
  AdministratorAccountId:
    Type: String
    Description: AWS Account Id of the administrator account (the account in which StackSets will be created).
    MaxLength: 12
    MinLength: 12
  RoleSuffix:
    Type: String
    Description: IAM Role Suffix.CamelCase highly reccomend. ex) When RoleSuffix is SecurityAccount, AWSCloudFormationStackSetExecutionRoleForSecurityAccount

Resources:
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub AWSCloudFormationStackSetExecutionRoleFor${RoleSuffix}
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Ref AdministratorAccountId
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess



```
　　
## 構築手順
マルチアカウントを運用している方に読んで頂いていると思うので、具体的なAWSの操作は省略させていただきます。
### 手順1
|概要|操作アカウント|
|:--:|:--:|
|Administration IAM Role（管理用）を作成|マスターアカウント|

以下リンクから作成
[![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?filter=active&templateURL=https://s3.amazonaws.com/cloudformation-stackset-sample-templates-us-east-1/AWSCloudFormationStackSetAdministrationRole.yml&stackName=stackset-administration-role)



### 手順2

|概要|操作アカウント|
|:--:|:--:|
|Exection IAM Role（管理用）を作成|メンバーアカウント（セキュリティ用、一般）|
　（今回のみすべてのアカウントにログインが必要になります）

以下リンクから作成
[![Launch Stack](https://cdn.rawgit.com/buildkite/cloudformation-launch-stack-button-svg/master/launch-stack.svg)](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?filter=active&templateURL=https://s3.amazonaws.com/cloudformation-stackset-sample-templates-us-east-1/AWSCloudFormationStackSetExecutionRole.yml&stackName=stackset-execution-role&param_AdministratorAccountId=XXXXXXXX)
※AdministratorAccountIdには親アカウントのアカウントIDを記入
（アカウントが多い場合はURLのXXX部分を修正すると楽になります）

### 手順3
|概要|操作アカウント|
|:--:|:--:|
|Administration IAM Role（セキュリティ用）を作成|メンバーアカウント（セキュリティ用）|

CloudFormationにて必要なリソースで紹介したAdministration IAM Role（セキュリティ用）の`AWSCloudFormationStackSetAdministrationRoleForOtherAccount.yml`を実行し、メンバーアカウント（セキュリティ）にIAM Roleを作成
※RoleSuffixには任意で記入　例）SecurityAccount

### 手順4
|概要|操作アカウント|
|:--:|:--:|
|Exection IAM Role（セキュリティ用）を作成|マスターアカウント|

CloudFormation StackSetsにて必要なリソースで紹介したExecution IAM Role（セキュリティ用）`AWSCloudFormationStackSetExecutionRoleForOtherAccount.yml`を実行し、マスターアカウントとメンバーアカウント（一般）にIAM Roleを作成
※ AdministratorAccountIdには子アカウント（セキュリティ）のアカウントIDを記入
※ RoleSuffixには任意で記入　例）SecurityAccount
※ StackSetsの対象はメンバーアカウント（セキュリティ用）を除くすべてアカウントを指定してください、任意のOUを作成しメンバーアカウント（セキュリティ用）以外を参加させるか、後述する`stacksets_accounts.csv`を利用




次にGuarDutyを設定していきます　

#GuardDuty
## 必要なリソース、用途
- マスターアカウント、メンバーアカウント（一般）のアカウントID一覧
    - CloudFormation StackSets実行時に対象を指定するために利用します
   
``` stacksets_accounts.csv
123456789012,123456789013,・・・・123456789099
 ```

- マスターアカウント、メンバーアカウント（一般）のアカウントID、メールアドレス一覧
    - GuardDutyのメンバー招待用のAWSアカウントIDとルートユーザのメールアドレスのcsvファイルです。

※GuardDutyのメンバーアカウントとStackSetsのメンバーアカウントが混同しやすいので注意してください

``` guardduty_member_accounts.csv
123456789012,aws+123456789012@example.com
123456789013,aws+123456789013@example.com
・・・・
123456789099,aws+123456789099@example.com
 ```


## 構築手順
こちらも同様に具体的な操作は省略させて頂きます。
### 手順1
|概要|操作アカウント|
|:--:|:--:|
|セキュリティアカウントでGuardDuty有効化|セキュリティアカウント|
コンソールにログインしサービスからGuardDutyを選択し、有効化します

### 手順2
|概要|操作アカウント|
|:--:|:--:|
|GuardDutyメンバー招待|セキュリティアカウント|
準備しておいた`guardduty_member_accounts.csv`を利用しメンバーに招待します。

### 手順3
|概要|操作アカウント|
|:--:|:--:|
|Stacksetsですべてアカウント、リージョンでGuardDuty有効化|セキュリティアカウント|

次にSlackに通知を設定していきます。

# Slack通知
## 必要なリソース、用途
- Slack通知用リソース
    - CloudWatchEventRuleにて制御を入れることで脅威レベルがミディアム、高いものに絞って通知するようにしました。現時点で脅威レベルが低いを含めてしまうと通知が多く重要な脅威検出ができない可能性があったためです。今後少しずつ脅威レベルが低いものを修正した段階で脅威レベルが低いものも通知しようと思ってます。
 
```notify-guardduty-alert.yml
AWSTemplateFormatVersion: 2010-09-09
Description:
  'enable guardduty and set alert'
Parameters:
  SlackWebhookUrl:
    Type: String
    Default: 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXX/XXXXXXXXXXXXXXXXXXXXX'
  SlackMentionId:
    Type: String
  SlackMentionName:
    Type: String

Resources:
  LambdaFunctionNotifyAlertFromGuardDuty:
    Type: 'AWS::Lambda::Function'
    Properties:
      Handler: 'index.lambda_handler'
      Runtime: 'python3.7'
      Code: 
        ZipFile: |
          import json
          import os
          import urllib.request

          def get_severity_level(severity, sre_mention):
              # ref: http://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity
              if severity == 0.0:
                  level = {'label': 'Information', 'color': 'good', 'mention': ''}
              elif 0.1 <= severity <= 3.9:
                  level = {'label': 'Low', 'color': 'warning', 'mention': ''}
              elif 4.0 <= severity <= 6.9:
                  level = {'label': 'Medium', 'color': 'warning', 'mention': sre_mention}
              elif 7.0 <= severity <= 8.9:
                  level = {'label': 'High', 'color': 'danger', 'mention': sre_mention}
              elif 9.0 <= severity <= 10.0:
                  level = {'label': 'Critical', 'color': 'danger', 'mention': sre_mention}
              else:
                  level = {'label': 'Unknown', 'color': '#666666', 'mention': ''}
              return level

          def format_message(data, sre_mention):
              account_id     = data['detail']['accountId']
              region         = data['detail']['region']
              severity       = data['detail']['severity']
              title          = data['detail']['title']
              description    = data['detail']['description']
              type           = data['detail']['type']
              severity_level = get_severity_level(severity, sre_mention)

              payload = {
                  'username': 'GuardDuty',
                  'text': '{}  GuardDuty Finding in {}'.format(severity_level['mention'], region),
                  'icon_emoji': ':aws:',
                  'attachments': [
                      {
                          'fallback': 'Detailed information on GuardDuty Finding.',
                          'color': severity_level['color'],
                          'title': title,
                          'text': description,
                          'fields': [
                              {
                                  'title': 'Account ID',
                                  'value': account_id,
                                  'short': True
                              },
                              {
                                  'title': 'Severity',
                                  'value': severity_level['label'],
                                  'short': True
                              },
                              {
                                  'title': 'Type',
                                  'value': type,
                                  'short': False
                              }
                          ]
                      }
                  ]
              }
              return payload
                    
          def notify_slack(url, payload):
              data = json.dumps(payload).encode('utf-8')
              method = 'POST'
              headers = {'Content-Type': 'application/json'}
              request = urllib.request.Request(url, data = data, method = method, headers = headers)
              with urllib.request.urlopen(request) as response:
                  return response.read().decode('utf-8')
          
          def lambda_handler(event, context):
              slack_webhook_url   = os.getenv('SLACK_WEBHOOK_URL')
              slack_mention_id    = os.getenv('SLACK_MENTION_ID')
              slack_mention_name  = os.getenv('SLACK_MENTION_NAME')
              sre_mention         = '<!subteam^%s|%s>' % (slack_mention_id, slack_mention_name)
              payload             = format_message(event, sre_mention)
              response            = notify_slack(slack_webhook_url, payload)
              return response
      MemorySize: 128
      Timeout: 60
      Environment:
        Variables:
          SLACK_WEBHOOK_URL: !Ref SlackWebhookUrl
          SLACK_MENTION_ID: !Ref SlackMentionId
          SLACK_MENTION_NAME: !Ref SlackMentionName
      Role: !GetAtt IAMRoleNotifyAlertFromGuardDuty.Arn

  IAMRoleNotifyAlertFromGuardDuty:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2008-10-17'
        Statement:
        - Effect: 'Allow'
          Principal:
            Service: 'lambda.amazonaws.com'
          Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy

  LambdaPermissionNotifyAlertFromGuardDuty: 
    Type: 'AWS::Lambda::Permission'
    Properties: 
      FunctionName: !Ref LambdaFunctionNotifyAlertFromGuardDuty
      Action: 'lambda:InvokeFunction'
      Principal: 'events.amazonaws.com'
      SourceArn: !GetAtt EventsRuleNotifyAlertFromGuardDutySchedule.Arn

  EventsRuleNotifyAlertFromGuardDutySchedule:
    Type: 'AWS::Events::Rule'
    Properties:
      Description: 'Alert to slack when find threats by GuardDuty'
      EventPattern: |
        {
          'source': [
            'aws.guardduty'
          ],
          'detail-type': [
            'GuardDuty Finding'
          ],
          'detail': {
            'severity': [4.0,4.1,4.2,4.3,4.4,4.5,4.6,4.7,4.8,4.9,5.0,5.1,5.2,5.3,5.4,5.5,5.6,5.7,5.8,5.9,6.0,6.1,6.2,6.3,6.4,6.5,6.6,6.7,6.8,6.9,7.0,7.1,7.2,7.3,7.4,7.5,7.6,7.7,7.8,7.9,8.0,8.1,8.2,8.3,8.4,8.5,8.6,8.7,8.8,8.9,9.0,9.1,9.2,9.3,9.4,9.5,9.6,9.7,9.8,9.9,10.0,4,5,6,7,8,9,10]

          }
        }
      Targets: 
        - Arn: !GetAtt LambdaFunctionNotifyAlertFromGuardDuty.Arn
          Id: 'Slackbot'

  IAMRoleLambdaExecutionNotifyAlertFromGuardDuty:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: 'Allow'
            Principal:
              Service: 'events.amazonaws.com'
            Action: 'sts:AssumeRole'  

```



## 構築手順
こちらも同様に具体的な操作は省略させて頂きます。
### 手順1
|概要|操作アカウント|
|:--:|:--:|
|SlackにてWebフックURLの取得|-|

### 手順2
|概要|操作アカウント|
|:--:|:--:|
|Slack通知を設定|セキュリティアカウント|
CloudFormationにて必要なリソースで紹介した`notify-guardduty-alert.yml`を実行し、Slack通知を設定

##  AWS Chatbotとの比較
今回はLambdaにてSlack通知を実施しましたが、AWS Chatbotも利用を検討しました。
簡単にLambdaを選択した理由をまとめておきます。

||Lambda|AWS Chatbot|
|:--:|:--:|:--:|
|Slack対応|可能|可能|
|GuarDuty対応|可能|可能|
|脅威レベル絞り込み|可能|可能|
|メンション|可能|不可能|
|表示アカウントID|問題のアカウント|セキュリティ用アカウント（セキュリティアカウントにログインして詳細を確認することで問題のアカウントを特定する）|
|通知例|![モザイク_スクリーンショット 2019-11-15 17.01.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/e16e2e1a-4abf-8ca6-9b42-6a5510b2732a.png)|![モザイク_スクリーンショット 2019-11-15 17.00.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261535/83a7ed4e-4727-3b2c-a177-b67a3a13659b.png)|

以上より、メンション可能であること、Slack通知を見た時にどのアカウントで脅威が検出されたのかわかるの2点から今回はLambdaにてSlack通知することとしました。

# まとめ
今回は以上の手順にて、AWSマルチアカウント運用時の脅威検出の導入の取り組みについて紹介させて頂きました。
大量のAWSアカウントを運用している場合に、一つ一つのアカウントにログインして設定することは想像以上に大変のため、今回のCloudFormationStackSetsを利用することにより運用の負荷を下げることが可能です。
またGuardDutyを利用し脅威検出を可能にしたため、アカウント運用チームの知らないところでセキュリティインシデント発生も抑制することが可能になるのではと思っております。


# 参考
https://dev.classmethod.jp/cloud/aws/create-stacksets-iam-role/
https://dev.classmethod.jp/cloud/aws/set-guardduty-all-region/
https://dev.classmethod.jp/cloud/aws/introducing-cloudformation-stacksets/