マルチアカウント環境でのGuardDutyの一括設定

## 概要
GuardDutyはAWSの提供する不正検知・脅威検出に関するサービスで、Security Hub・InspectorとともにAWSで利用を強く推奨されるサービスの一つです。マルチアカウント環境では、Organizationsのサービス統合および管理アカウントのGuardDutyでアカウント追加時の自動有効化の設定により、Organizationsにアカウントが追加されたときにGuardDutyを自動で有効化することが出来ます。


一方で上記の設定を行う場合、GuardDutyで管理アカウント・メンバーアカウントの親子関係が構成されるため、メンバーアカウントからは一部の操作が制限されます。
管理アカウント・メンバーアカウントでの各アクションの実施可否については以下の公式Docsをご確認ください。

https://docs.aws.amazon.com/guardduty/latest/ug/administrator_member_relationships.html

そこで、本記事では[前回のCloudTrailの記事](https://qiita.com/naras/private/23949084c95afef47ed2)と同様に、Organizations統合・GuardDutyの新規アカウント自動有効化が設定出来ない環境(=管理アカウントとの親子関係を構築したくない環境)において、CloudFormationを用いてGuardDutyを自動設定する方法をご紹介します。

## Organizations統合・GuardDuty側での自動有効化を利用するパターン
一般的なパターンはこちらです。
Organizationsのサービス統合の設定からGuardDutyのサービス統合を有効化、GuardDutyの管理アカウントから新規アカウント追加時のGuardDuty自動有効化を設定しましょう。
![スクリーンショット 2024-03-06 185642.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662709/a07b8348-3134-fdcf-76da-94e3593100e5.png)
↑Organizationsのサービス統合
![スクリーンショット 2024-03-06 185713.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662709/06c01c07-a220-9db4-6dfa-5986b4c0ed46.png)
↑新規アカウントのGuardDuty自動有効化

この設定を行う場合、Organizationsの管理アカウントとGuardDutyの管理アカウントを分けることを推奨されています。Control Towerを利用している場合はAuditアカウントにGuardDutyの管理権限を委任することを検討しましょう。

https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/guardduty_organizations.html

メンバーアカウントで出来ない操作は以下のようにInformationが表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662709/94ca43c4-28cf-27a2-c6bd-a09e748b0b41.png)



## Organizations統合・GuardDuty側での自動有効化を利用しないパターン
ここではOrganizations統合・GuardDuty管理アカウントからの自動有効化を利用しない代わりに、CloudFormation StackSetsとLambdaを利用した設定方法をご紹介します。
具体的には以下の設定を行います。
| # | 設定内容 | 利用サービス |
| --- | --- | --- |
| ① | アカウントが作成(OUに追加)されたときに自動でGuardDutyを有効化する | CloudFormation StackSets |
| ② | GuardDutyの各種設定を行う | CloudFormation |
| ③ | 検出結果の出力先を設定する  | Lambda |
| ④ | メンバーアカウントからの一時停止・無効化を禁止する | SCP |
| ⑤ | CMKで出力結果を暗号化する | KMS |

### ①アカウントが作成(OUに追加)されたときに自動でGuardDutyを有効化する
こちらは[前回のCloudTrailの記事](https://qiita.com/naras/private/23949084c95afef47ed2)と同様の設定になりますので、割愛します。

### ②GuardDutyの各種設定を行う
CloudFormation Stacksetsで展開を行うため、GuardDutyを設定するためのCloudFormation Templateを作成します。GuardDutyの有効化には`Type: AWS::GuardDuty::Detector`を利用し、各種サービスに対する監視設定等を行います。
詳細は以下の公式ドキュメントをご確認ください。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-guardduty-detector.html

ここで気をつけないといけないのが、検出結果のエクスポート設定はCloudFormationで行えないという点です。そのため、この設定を行うための方法を③で説明します。
```
  EnableGuradDuty:
    Type: AWS::GuardDuty::Detector
    Properties: 
      Enable: True
      DataSources:
        Kubernetes: 
          AuditLogs: 
            Enable: true
        S3Logs: 
          Enable: true
```

### ③検出結果の出力先を設定する
GuardDutyの検出結果エクスポート設定を行うため、同一CloudFormation Template内で設定を行うLambdaを作成し、実行まで行います。
以下がCFn Templateの作成例です。
*Lambda用のIAM Role, IAM Policyも同Template内で作成していますが、ここでは割愛しています
```
  CreateLambdaForSettingGuardDuty:
    Type: AWS::Lambda::Function
    Properties:
      Architectures:
        - x86_64
      PackageType: Zip
      Code:
        ZipFile: |
          import boto3
          import os
          import cfnresponse
          client = boto3.client("guardduty")
          def lambda_handler(event,context):
              if event['RequestType'] == "Create" or event['RequestType'] == "Update":
                  try:
                    response = client.create_publishing_destination(
                        DetectorId = os.environ['DetectorId'],
                        DestinationType = "S3",
                        DestinationProperties={
                            "DestinationArn": os.environ['DestinationS3Arn'],
                            "KmsKeyArn": os.environ['EncryptionKeyArn']
                            }
                            )
                    responseData = {}
                    responseData['Data'] = response
                    print(response)
                    cfnresponse.send(event,context,cfnresponse.SUCCESS,responseData)
                    return
                    
                  except:
                    cfnresponse.send(event,context,cfnresponse.FAILED,{})
                    return
              
              if event['RequestType'] == "Delete":
                  cfnresponse.send(event,context,cfnresponse.SUCCESS,{})
                  return
      Description: "Lambda function for setting GuardDuty"
      FunctionName: !Ref LambdaFunctionName
      Handler: index.lambda_handler
      Role: !GetAtt IAMRoleForLambda.Arn
      Environment:
        Variables:
          "DetectorId": !Ref EnableGuradDuty
          "DestinationS3Arn": !Ref DestinationS3Arn
          "EncryptionKeyArn": !Ref EncryptionKeyArn
      Runtime: python3.9
      Timeout: 60
      Tags: 
        - Key: "Key"
          Value: "Value"      
  
  SetFindingOutputDestination:
    Type: Custom::SetFindingOutputDestination
    Properties:
      ServiceToken: !GetAtt "CreateLambdaForSettingGuardDuty.Arn"
```
・ Lambdaの実行
同一Template内で作成したLambdaを実行するにはCloudFormationのカスタムリソースの仕組みを利用します。
作成例での該当箇所は以下の通りです。
`ServiceToken`に実行したいLambdaのArnを指定することで、CFn Stack作成時に指定したLambdaが実行されます。
```
  SetFindingOutputDestination:
    Type: Custom::SetFindingOutputDestination
    Properties:
      ServiceToken: !GetAtt "CreateLambdaForSettingGuardDuty.Arn"
```
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html


・cfn-responseモジュールの埋め込み
このような形でLambdaを実行する場合、実行完了後にCloudFormationに対してレスポンスを返す必要があります。
このモジュールを埋め込まないと、Lambdaが実行されたあともCloudFormation Stackの作成が完了せずタイムアウトまで待つことになります。
作成例での該当箇所は以下の通りです。Stack作成時のイベントである"Create"の他に、更新・削除を行うこともあるかと思いますので、"Update","Delete"の場合の設定も忘れずに行いましょう。
```
              if event['RequestType'] == "Create" or event['RequestType'] == "Update":
                  try:
                  ...
                    cfnresponse.send(event,context,cfnresponse.SUCCESS,responseData)
                    return
                    
                  except:
                    cfnresponse.send(event,context,cfnresponse.FAILED,{})
                    return
              
              if event['RequestType'] == "Delete":
                  cfnresponse.send(event,context,cfnresponse.SUCCESS,{})
                  return
```

### ④メンバーアカウントからの一時停止・無効化を禁止する
メンバーアカウントからの変更を禁止するため、SCPを利用して操作制限を行います。
GuardDutyに関連するアクションは内容によって対象となるリソースが異なるため、どの操作を制限したいかによって記載すべき対象リソースを確認する必要があります。
どのアクションの対象リソースがどれなのかは以下のドキュメントで確認することが出来ます。

https://docs.aws.amazon.com/ja_jp/service-authorization/latest/reference/list_amazonguardduty.html

また、以下の作成例ではGuardDutyの検出結果出力設定用Lambdaについてもタグベースでの操作制限を行っています。

```
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUpdateAndDisableGuardDuty",
      "Effect": "Deny",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalARN": [
            "arn:aws:iam::*:role/stacksets-exec-*",
            "arn:aws:iam::*:role/admin-role",
            "arn:aws:iam::*:role/GuardDutyLambdaRole"
        ]}},
      "Action": [
        "guardduty:AcceptAdministratorInvitation",
        "guardduty:AcceptInvitation",
        "guardduty:CreateDetector",
        "guardduty:CreateFilter",
        "guardduty:CreateMembers",
        "guardduty:CreatePublishingDestination",
        "guardduty:DeleteDetector",
        "guardduty:DeleteFilter",
        "guardduty:DeletePublishingDestination",
        "guardduty:DeleteThreatIntelSet",
        "guardduty:InviteMembers",
        "guardduty:UpdateDetector",
        "guardduty:UpdateFilter",
        "guardduty:UpdateFindingsFeedback",
        "guardduty:UpdateMalwareScanSettings",
        "guardduty:UpdatePublishingDestination"
      ],
      "Resource":[
        "arn:*:guardduty:*:*:detector/*",
        "arn:*:guardduty:*:*:detector/*/filter/*",
        "arn:*:guardduty:*:*:detector/*/ipset/*",
        "arn:*:guardduty:*:*:detector/*/threatintelset/*",
        "arn:*:guardduty:*:*:detector/*/publishingDestination/*"
      ]
    },
  {
    "Sid": "DenyUpdateAndDisableGuardDuty",
    "Effect": "Deny",
    "Condition": {
      "ArnNotLike": {
        "aws:PrincipalARN": [
          "arn:aws:iam::*:role/stacksets-exec-*",
          "arn:aws:iam::*:role/admin-role"
      ]},
      "StringEquals": { "aws:ResourceTag/Key": "Value"}
        },
    "Action": [
      "lambda:*"
        ],
    "Resource":"*"
  }]
}

```

### ⑤CMKで出力結果を暗号化する
GuardDutyの検出結果はCMKによる暗号化が必須となっています。
設定内容は[前回のCloudTrailの記事](https://qiita.com/naras/private/23949084c95afef47ed2)とほぼ同様のため割愛しますが、メンバーアカウントが新規追加されることを想定してキーポリシーを設定することをオススメします。

## まとめ
本記事ではOrganizationsによるサービス統合・GuardDutyの新規アカウント追加時の自動有効化機能を利用せずにマルチアカウント環境に対してGuardDutyの自動有効化設定を行う方法を紹介しました。
メンバーアカウント毎にGuardDutyの設定を変更したい場合・GuardDutyの設定で親子関係を構成することによるメンバーアカウントへの操作制限を許容出来ない場合などの対応方法の一つとしてご参考になれば幸いです。