AWS CDK で Amazon GuardDuty を実装してみた

今回は、Amazon GuardDuty による脅威検出と脅威通知を AWS CDK で実装する方法をまとめました。

はじめに

今回は、AWS GuardDutyを使用して、VPCフローログ、CloudTrail、DNSログを機械学習で分析し、悪意のある活動や異常な行動パターンをリアルタイムで検出して通知するリソースをAWS CDKで実装していきます。Guard Dutyによる脅威検出とS3への長期保存、EventBridge経由での即座な通知を組み合わせて実装します。

今回作成するリソース

  • SNSトピック: GuardDuty脅威検出結果の通知
  • KMS暗号化キー: GuardDutyデータの暗号化
  • S3バケット: 検出結果の長期保存とアーカイブ
  • AWS GuardDuty: 機械学習ベースの脅威検出エンジン
  • EventBridge: 重要度別の自動通知ルール

アーキテクチャ概要

 

AWS CDK ソースコード

SNS通知設定

    const emailAddresses = [                                                             // SNS通知先メーリングリスト(通知先が複数ある場合はアドレスを追加)
      'xxxxxx@example.com',
      'xxxxxxx@example.com',
    ];

    // GuardDuty用トピック
    const guardDutyTopic = new sns.Topic(this, 'GuardDutyTopic', {                   
      topicName: 'guardduty-alertnotification',                                          // トピック名
      displayName: 'GuardDuty Alert Notifications'                                       // 表示名
    });

    // GuardDuty用サブスクリプション
    emailAddresses.forEach(email => {                                                    
        guardDutyTopic.addSubscription(
        new subscriptions.EmailSubscription(email)                                       // プロトコル:EMAIL
      );
    });

ポイント:

  • 複数の管理者への通知配信
  • アラーム発生時に通知するメールアドレスを指定

KMS暗号化キー設定

    const guardDutyKey = new kms.Key(this, 'GuardDutyKey', {
      alias: 'alias/guardduty-key',                                                      // エイリアス名
      description: 'KMS key for GuardDuty encryption',                                   // 説明
      enableKeyRotation: true,                                                           // ローテーションの有効化
      removalPolicy: cdk.RemovalPolicy.DESTROY                                           // スタック削除時にキーを削除する ※デプロイ時にRETAINに変更
    });
    cdk.Tags.of(guardDutyKey).add('Name', 'guardduty-key');                              // Nameタグ

ポイント:

  • セキュリティ強化: GuardDuty専用の暗号化キー
  • 自動ローテーション: セキュリティ基準に準拠した定期的なキー更新
  • アクセス制御: 後述のキーポリシーで細かいアクセス制御

S3バケット設定(検出結果エクスポート)

    // GuardDuty用S3バケット
    const guardDutyBucket = new s3.Bucket(this, 'GuardDutyBucket', {
      bucketName: 's3b-guardduty',                                           // バケット名
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,                                 // パブリックアクセスをすべてブロック
      encryption: s3.BucketEncryption.S3_MANAGED,                                        // 暗号化タイプ:SSE-S3
      enforceSSL: true,                                                                  // SSL通信を強制
      autoDeleteObjects: true,                                                           // スタック削除時にオブジェクトを自動的に削除 ※デプロイ時にコメントアウト
      removalPolicy: cdk.RemovalPolicy.DESTROY,                                          // スタック削除時にバケットも削除 ※デプロイ時にRETAINに修正
      lifecycleRules: [                                                                  // ライフサイクルルール作成
        {
          id: 'Expiration Rule 12 Months',                                               // ライフサイクルルール名
          expiration: cdk.Duration.days(366),                                            // オブジェクトの現行バージョンの有効期限:366日後にオブジェクトを削除
        }
      ]
    });

    guardDutyBucket.addToResourcePolicy(new iam.PolicyStatement({                        // ポリシー追加1
      sid: 'Deny incorrect encryption header',
      effect: iam.Effect.DENY,
      actions: ['s3:PutObject'],
      resources: [`${guardDutyBucket.bucketArn}/*`],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
      conditions: {
        StringNotLike: {
          's3:x-amz-server-side-encryption-aws-kms-key-id': guardDutyKey.keyArn
        }
      }
    }));

    guardDutyBucket.addToResourcePolicy(new iam.PolicyStatement({                        // ポリシー追加2
      sid: 'Deny unencrypted object uploads',
      effect: iam.Effect.DENY,
      actions: ['s3:PutObject'],
      resources: [`${guardDutyBucket.bucketArn}/*`],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
      conditions: {
        StringNotEquals: {
          's3:x-amz-server-side-encryption': 'aws:kms'
        }
      }
    }));

ポイント:

  • セキュア設計: パブリックアクセス完全ブロック、SSL強制
  • 暗号化必須: KMS暗号化のみを許可するバケットポリシー
  • 長期保存: セキュリティ調査用の1年間保持
  • コンプライアンス: 暗号化されていないデータの拒否

AWS GuardDuty設定

    const guardDutyDetector = new guardduty.CfnDetector(this, 'GuardDuty', {
      enable: true,                                                                      // GuardDutyの有効化
      findingPublishingFrequency: 'FIFTEEN_MINUTES',                                     // 検出結果の更新頻度:15分
      dataSources: {                                                                     // データソースの設定
        s3Logs: {
          enable: true                                                                   // S3アクセスログの監視有効化
        },
        malwareProtection: {                                                             // マルウェア保護の設定
          scanEc2InstanceWithFindings: {
            ebsVolumes: true                                                             // EBSボリュームのスキャン有効化
          }
        }
      }
    });

    const s3Export = new guardduty.CfnPublishingDestination(this, 'S3Export', {
      detectorId: guardDutyDetector.ref,                                                 // GuardDuty Detectorの参照
      destinationType: 'S3',                                                             // 出力先のタイプ
      destinationProperties: {                                                           // 出力先のプロパティ
        destinationArn: guardDutyBucket.bucketArn,                                       // 出力先:S3バケット
        kmsKeyArn: guardDutyKey.keyArn                                                   // KMSキーのARN
      }
    });

ポイント:

  • 包括的監視: VPCフローログ、CloudTrail、DNSログ、S3アクセスログ
  • マルウェア保護: EBSボリュームの自動スキャン機能
  • リアルタイム更新: 15分間隔での検出結果更新
  • 暗号化エクスポート: KMS暗号化でのS3保存

権限設定(KMS・S3ポリシー)

    // KMSポリシー
    guardDutyKey.addToResourcePolicy(new iam.PolicyStatement({                           // ポリシー追加1
      sid: 'Allow GuardDuty to encrypt findings',
      effect: iam.Effect.ALLOW,
      actions: ['kms:GenerateDataKey*'],
      resources: ['*'],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
        conditions: {
          StringEquals: {
            'aws:SourceAccount': cdk.Stack.of(this).account
          },
          StringLike: {
            'aws:SourceArn': `arn:aws:guardduty:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:detector/${guardDutyDetector.attrId}`
          }
        }
    }));


    // GuardDutyポリシー
    guardDutyBucket.addToResourcePolicy(new iam.PolicyStatement({                        // ポリシー追加3
      sid: 'Allow PutObject',
      effect: iam.Effect.ALLOW,
      actions: ['s3:PutObject'],
      resources: [`${guardDutyBucket.bucketArn}/*`],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
      conditions: {
        StringEquals: {
          'aws:SourceAccount': cdk.Stack.of(this).account,
          'aws:SourceArn': `arn:aws:guardduty:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:detector/${guardDutyDetector.attrId}`
        }
      }
    }));

    // GuardDutyポリシー
    guardDutyBucket.addToResourcePolicy(new iam.PolicyStatement({                        // ポリシー追加4
      sid: 'Allow GetBucketLocation',
      effect: iam.Effect.ALLOW,
      actions: ['s3:GetBucketLocation'],
      resources: [`${guardDutyBucket.bucketArn}`],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
      conditions: {
        StringEquals: {
          'aws:SourceAccount': cdk.Stack.of(this).account,
          'aws:SourceArn': `arn:aws:guardduty:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:detector/${guardDutyDetector.attrId}`
        }
      }
    }));

ポイント:

  • 最小権限: GuardDutyサービスのみに必要な権限を付与
  • アカウント制限: SourceAccount条件でクロスアカウントアクセス防止
  • ソースARN制限: 特定のGuardDuty Detectorのみからのアクセス許可

EventBridge統合

    const guardDutyRule = new events.Rule(this, 'GuardDutyEventRule', {                  // GuardDuty用のEventBridge
      ruleName: 'eventbridge-rule-guardduty',                                            // ルール名
      eventPattern: {                                                                    // イベントパターンを指定
        source: ['aws.guardduty'],
        detailType: ['GuardDuty Finding'],                                               // GuardDutyによって検出された結果(Findings)がインポートされた際に発行されるイベント
        detail: {
          severity: [
            {
              numeric: [ '>=', 7 ]                                                       // 重要度高(7.0~8.9)を通知
            }
          ] 
        }
      },
      targets: [                                                                         // ターゲットを指定
        new targets.SnsTopic(guardDutyTopic)                                             // ターゲットタイプ: SNSトピック、トピック: GuardDuty用のトピック
      ]
    });

ポイント:

  • 重要度フィルタリング: 7.0以上の高リスク脅威のみ通知

今回実装したコンストラクトファイルまとめ

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as guardduty from 'aws-cdk-lib/aws-guardduty';
import * as events from 'aws-cdk-lib/aws-events'; 
import * as targets from 'aws-cdk-lib/aws-events-targets';

export interface GuardDutyConstructProps {
  // 必要に応じて追加のプロパティを定義
}

export class GuardDutyConstruct extends Construct {     
  constructor(scope: Construct, id: string, props?: GuardDutyConstructProps) {
    super(scope, id);

    //===========================================
    // SNS
    //===========================================
    const emailAddresses = [                                                             // SNS通知先メーリングリスト(通知先が複数ある場合はアドレスを追加)
      'xxxxxx@example.com',
      'xxxxxxx@example.com',
    ];

    // GuardDuty用トピック
    const guardDutyTopic = new sns.Topic(this, 'GuardDutyTopic', {                   
      topicName: 'guardduty-alertnotification',                                          // トピック名
      displayName: 'GuardDuty Alert Notifications'                                       // 表示名
    });

    // GuardDuty用サブスクリプション
    emailAddresses.forEach(email => {                                                    
        guardDutyTopic.addSubscription(
        new subscriptions.EmailSubscription(email)                                       // プロトコル:EMAIL
      );
    });


    //===========================================
    // KMS
    //===========================================
    const guardDutyKey = new kms.Key(this, 'GuardDutyKey', {
      alias: 'alias/guardduty-key',                                                      // エイリアス名
      description: 'KMS key for GuardDuty encryption',                                   // 説明
      enableKeyRotation: true,                                                           // ローテーションの有効化
      removalPolicy: cdk.RemovalPolicy.DESTROY                                           // スタック削除時にキーを削除する ※デプロイ時にRETAINに変更
    });
    cdk.Tags.of(guardDutyKey).add('Name', 'guardduty-key');                              // Nameタグ



    //===========================================
    // S3
    //===========================================
    // GuardDuty用S3バケット
    const guardDutyBucket = new s3.Bucket(this, 'GuardDutyBucket', {
      bucketName: 's3b-guardduty',                                           // バケット名
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,                                 // パブリックアクセスをすべてブロック
      encryption: s3.BucketEncryption.S3_MANAGED,                                        // 暗号化タイプ:SSE-S3
      enforceSSL: true,                                                                  // SSL通信を強制
      autoDeleteObjects: true,                                                           // スタック削除時にオブジェクトを自動的に削除 ※デプロイ時にコメントアウト
      removalPolicy: cdk.RemovalPolicy.DESTROY,                                          // スタック削除時にバケットも削除 ※デプロイ時にRETAINに修正
      lifecycleRules: [                                                                  // ライフサイクルルール作成
        {
          id: 'Expiration Rule 12 Months',                                               // ライフサイクルルール名
          expiration: cdk.Duration.days(366),                                            // オブジェクトの現行バージョンの有効期限:366日後にオブジェクトを削除
        }
      ]
    });

    guardDutyBucket.addToResourcePolicy(new iam.PolicyStatement({                        // ポリシー追加1
      sid: 'Deny incorrect encryption header',
      effect: iam.Effect.DENY,
      actions: ['s3:PutObject'],
      resources: [`${guardDutyBucket.bucketArn}/*`],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
      conditions: {
        StringNotLike: {
          's3:x-amz-server-side-encryption-aws-kms-key-id': guardDutyKey.keyArn
        }
      }
    }));

    guardDutyBucket.addToResourcePolicy(new iam.PolicyStatement({                        // ポリシー追加2
      sid: 'Deny unencrypted object uploads',
      effect: iam.Effect.DENY,
      actions: ['s3:PutObject'],
      resources: [`${guardDutyBucket.bucketArn}/*`],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
      conditions: {
        StringNotEquals: {
          's3:x-amz-server-side-encryption': 'aws:kms'
        }
      }
    }));



    //===========================================
    // GuardDuty作成
    //===========================================
    const guardDutyDetector = new guardduty.CfnDetector(this, 'GuardDuty', {
      enable: true,                                                                      // GuardDutyの有効化
      findingPublishingFrequency: 'FIFTEEN_MINUTES',                                     // 検出結果の更新頻度:15分
      dataSources: {                                                                     // データソースの設定
        s3Logs: {
          enable: true                                                                   // S3アクセスログの監視有効化
        },
        malwareProtection: {                                                             // マルウェア保護の設定
          scanEc2InstanceWithFindings: {
            ebsVolumes: true                                                             // EBSボリュームのスキャン有効化
          }
        }
      }
    });

    const s3Export = new guardduty.CfnPublishingDestination(this, 'S3Export', {
      detectorId: guardDutyDetector.ref,                                                 // GuardDuty Detectorの参照
      destinationType: 'S3',                                                             // 出力先のタイプ
      destinationProperties: {                                                           // 出力先のプロパティ
        destinationArn: guardDutyBucket.bucketArn,                                       // 出力先:S3バケット
        kmsKeyArn: guardDutyKey.keyArn                                                   // KMSキーのARN
      }
    });

    // GuardDuty Detectorへの依存関係を追加
    s3Export.node.addDependency(guardDutyDetector);

    // S3バケットへの依存関係を追加(必要に応じて)
    s3Export.node.addDependency(guardDutyBucket);

    // KMSキーへの依存関係を追加(必要に応じて)
    s3Export.node.addDependency(guardDutyKey);


    //===========================================
    // KMS/S3 一部ポリシー追加
    //===========================================  
    // KMSポリシー
    guardDutyKey.addToResourcePolicy(new iam.PolicyStatement({                           // ポリシー追加1
      sid: 'Allow GuardDuty to encrypt findings',
      effect: iam.Effect.ALLOW,
      actions: ['kms:GenerateDataKey*'],
      resources: ['*'],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
        conditions: {
          StringEquals: {
            'aws:SourceAccount': cdk.Stack.of(this).account
          },
          StringLike: {
            'aws:SourceArn': `arn:aws:guardduty:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:detector/${guardDutyDetector.attrId}`
          }
        }
    }));


    // GuardDutyポリシー
    guardDutyBucket.addToResourcePolicy(new iam.PolicyStatement({                        // ポリシー追加3
      sid: 'Allow PutObject',
      effect: iam.Effect.ALLOW,
      actions: ['s3:PutObject'],
      resources: [`${guardDutyBucket.bucketArn}/*`],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
      conditions: {
        StringEquals: {
          'aws:SourceAccount': cdk.Stack.of(this).account,
          'aws:SourceArn': `arn:aws:guardduty:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:detector/${guardDutyDetector.attrId}`
        }
      }
    }));

    // GuardDutyポリシー
    guardDutyBucket.addToResourcePolicy(new iam.PolicyStatement({                        // ポリシー追加4
      sid: 'Allow GetBucketLocation',
      effect: iam.Effect.ALLOW,
      actions: ['s3:GetBucketLocation'],
      resources: [`${guardDutyBucket.bucketArn}`],
      principals: [new iam.ServicePrincipal('guardduty.amazonaws.com')],
      conditions: {
        StringEquals: {
          'aws:SourceAccount': cdk.Stack.of(this).account,
          'aws:SourceArn': `arn:aws:guardduty:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:detector/${guardDutyDetector.attrId}`
        }
      }
    }));



    //===========================================
    // EventBridge
    //===========================================    
    // GuardDuty用ルール
    const guardDutyRule = new events.Rule(this, 'GuardDutyEventRule', {                  // GuardDuty用のEventBridge
      ruleName: 'eventbridge-rule-guardduty',                                            // ルール名
      eventPattern: {                                                                    // イベントパターンを指定
        source: ['aws.guardduty'],
        detailType: ['GuardDuty Finding'],                                               // GuardDutyによって検出された結果(Findings)がインポートされた際に発行されるイベント
        detail: {
          severity: [
            {
              numeric: [ '>=', 7 ]                                                       // 重要度高(7.0~8.9)を通知
            }
          ] 
        }
      },
      targets: [                                                                         // ターゲットを指定
        new targets.SnsTopic(guardDutyTopic)                                             // ターゲットタイプ: SNSトピック、トピック: GuardDuty用のトピック
      ]
    });
  }    
}

まとめ

今回は、AWS GuardDutyを活用した機械学習ベースの脅威検出システムをAWS CDKで実装しました。

皆さんのお役に立てば幸いです。

タイトルとURLをコピーしました