今回は、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で実装しました。 皆さんのお役に立てば幸いです。