AWS CDK で AWS Security Hub を実装してみた

今回は、AWS Config と AWS Security Hub を活用した統合的なセキュリティ監視を AWS CDK で実装する方法をまとめました。

はじめに

今回はをAWS CDKでAWS ConfigとSecurityHubを実装していきます。
また、EventBridgeでコンプライアンス違反を検知して入力トランスフォーマーでメール文を成型して通知します。

今回作成するリソース

  • SNSトピック: セキュリティアラートの通知
  • S3バケット: AWS Config設定履歴の保存
  • IAMロール: AWS ConfigとSecurityHub実行権限
  • AWS Config: 全リソースの構成変更記録
  • AWS SecurityHub: セキュリティ基準チェック
  • EventBridge: 脅威検知時の自動通知

 

アーキテクチャ概要

 

AWS CDK ソースコード

SNS通知設定

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

    // SecurityHub用トピック
    const securityHubTopic = new sns.Topic(this, 'SecurityHubTopic', {                   
      topicName: 'securityhub-alertnotification',                                        // トピック名
      displayName: 'SecurityHub Alert Notifications'                                     // 表示名
    });

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

ポイント:

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

S3バケット設定(Config履歴保存)

    const configBucket = new s3.Bucket(this, 'ConfigBucket', {
      bucketName: 's3b-config',                                                          // バケット名
      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日後にオブジェクトを削除
        }
      ]
    });

    configBucket.addToResourcePolicy(new iam.PolicyStatement({                           // バケットポリシー追加1
      effect: iam.Effect.ALLOW,
      actions: [
        's3:GetBucketAcl',
        's3:ListBucket'
      ],
      resources: [configBucket.bucketArn],
      principals: [new iam.ServicePrincipal('config.amazonaws.com')],
      conditions: {
        StringEquals: {
          'aws:SourceAccount': cdk.Stack.of(this).account               
        }
      }
    }));

    configBucket.addToResourcePolicy(new iam.PolicyStatement({                           // バケットポリシー追加2
      effect: iam.Effect.ALLOW,
      actions: [
        's3:PutObject'
      ],
      resources: [`${configBucket.bucketArn}/AWSLogs/${cdk.Stack.of(this).account}/Config/*`],
      principals: [new iam.ServicePrincipal('config.amazonaws.com')],
      conditions: {
        StringEquals: {
          's3:x-amz-acl': 'bucket-owner-full-control',                                  // バケット所有者にフルコントロールを付与
          'aws:SourceAccount': cdk.Stack.of(this).account            
        }
      }
    }));

ポイント:

  • セキュア設計: パブリックアクセス完全ブロック、SSL強制
  • 長期保存: コンプライアンス要件に応じた1年間保持
  • 適切な権限: AWS Configサービスのみにアクセス許可

AWS Config設定

    // サービスロール作成
    const configServiceRole = new iam.CfnServiceLinkedRole(this, 'ConfigServiceLinkedRole', {                     // 既存のAWS Configサービスにリンクされたロール(Config実行に必要な権限を自動付与)※すでに付与されている場合はコメントアウトしてデプロイ
      awsServiceName: 'config.amazonaws.com',                                                                     // サービス名
    });

    // レコーダーの作成
    const accountId = cdk.Stack.of(this).account;
    const configRecorder = new config.CfnConfigurationRecorder(this, 'Recorder', {
      roleArn: `arn:aws:iam::${accountId}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig`,    // ConfigのIAMロール(Configがリソースの設定変更を記録する権限)
      recordingGroup: {                                                                                           // 記録対象の設定
        allSupported: true,                                                                                       // サポートされている全リソースタイプを記録
        includeGlobalResourceTypes: true,                                                                         // グローバルリソースも記録対象に含める
      }
    });

    // 配信チャネルの作成
    const configDeliveryChannel = new config.CfnDeliveryChannel(this, 'DeliveryChannel', {                        // 
      s3BucketName: configBucket.bucketName,                                                                      // Config用のバケット
      configSnapshotDeliveryProperties: {                                                                         // 
        deliveryFrequency: 'TwentyFour_Hours'                                                                     // スナップショットを24時間(1日)ごとにS3バケットへ配信
      }
    });

ポイント:

  • 包括的記録: 全サポートリソースの構成変更を記録
  • グローバルリソース対応: IAM、CloudFrontなども監視対象
  • 定期スナップショット: 24時間ごとの設定状況保存
  • サービスリンクロール: AWS Config専用の権限で実行

AWS SecurityHub設定

    // AWS基礎セキュリティのベストプラクティスv1.0.0 / CISAWSFoundationsBenchmarkv1.2.0 有効化
    const securityHub = new securityhub.CfnHub(this, 'SecurityHub', { 
      enableDefaultStandards: true,                                                      // デフォルトのセキュリティ基準を有効化
      controlFindingGenerator: 'SECURITY_CONTROL',                                       // セキュリティ管理ベースの検出
    });

ポイント:

  • AWS Foundational Security Standard: AWSベストプラクティス基準
  • CIS AWS Foundations Benchmark v1.2.0: 業界標準セキュリティ基準
  • 自動検出: セキュリティコントロール違反の自動検知

EventBridge設定

    // SecurityHub用ルール
    const securityHubRule = new events.Rule(this, 'SecurityHubEventRule', {
      ruleName: 'eventbridge-rule-securityhub',                                          // ルール名
      eventPattern: {                                                                    // イベントパターンを指定
        source: ['aws.securityhub'],
        detailType: ['Security Hub Findings - Imported'],                                // SecurityHubによって検出された結果(Findings)がインポートされた際に発行されるイベント
        detail: {
          findings: {
            Compliance: {                                                                // コンプライアンスステータスがFAILEDかどうか
              Status: ['FAILED']                                                         // FAILED:セキュリティポリシーやコンプライアンス要件を満たさない検出結果
            },
            Severity: {
              Label: ['HIGH', 'CRITICAL']                                                // 重要度がHIGH,CRITICALかどうか
            },
            Workflow: {                                                                  // ワークフローステータスがNEWかどうか
              Status: ['NEW']                                                            // NEW:まだ調査や対応が行われていない新しい検出結果
            }
          }
        }
      },
    });

    // 入力パスマップ(InputPathsMap)
    const inputPathsMap: { [key: string]: string } = {
      accountId: '$.detail.findings[0].AwsAccountId',
      description: '$.detail.findings[0].Description',
      resourceId: '$.detail.findings[0].Resources[0].Id',
      securityControlId: '$.detail.findings[0].Compliance.SecurityControlId',
      severity: '$.detail.findings[0].Severity.Label',
      title: '$.detail.findings[0].Title',
    };

    const inputTemplate = "\"アカウントID: の SecurityHub でイベント検知がありました。\"\n\"検知内容を確認し、対応をお願いします。\"\n\n\"タイトル: []

    const cfnRule = securityHubRule.node.defaultChild as events.CfnRule;
    cfnRule.addPropertyOverride('Targets', [
      {
        Arn: securityHubTopic.topicArn,                                                  // SNSトピックARN
        Id: 'SecurityHubTopicTarget',                                                    // ターゲットID
        InputTransformer: {                                                              // 入力トランスフォーマー
          InputPathsMap: inputPathsMap,
          InputTemplate: inputTemplate,
        },
      },
    ]);

    // SNS Topic ポリシー(EventBridge からの Publish を許可)
    securityHubTopic.addToResourcePolicy(new iam.PolicyStatement({
      sid: 'AllowEventBridgePublish',                                                    // ステートメントID
      effect: iam.Effect.ALLOW,                                                          // 許可
      principals: [new iam.ServicePrincipal('events.amazonaws.com')],                    // EventBridgeサービスプリンシパル
      actions: ['sns:Publish'],                                                          // Publish権限
      resources: [securityHubTopic.topicArn],                                            // 対象トピック
      conditions: {                                                                      // ルールARNに限定
        ArnEquals: { 'aws:SourceArn': securityHubRule.ruleArn }
      }
    }));

ポイント:

  • フィルタリング: HIGH/CRITICAL重要度の新規違反のみ通知
  • Input Transformer: Lambdaなしで通知内容を整形
  • 詳細情報: アカウントID、リソースID、違反内容を自動抽出
  • セキュア通知: 特定のEventBridgeルールからのみPublish許可

脅威検知フローと通知内容

検知フロー

  1. リソース変更検知: AWS Configがリソース構成変更を記録
  2. セキュリティチェック: SecurityHubが設定基準に照合
  3. 違反検出: コンプライアンス違反やセキュリティ脅威を特定
  4. フィルタリング: EventBridgeが重要度・ステータスでフィルタ
  5. 通知送信: 入力トランスフォーマーで整形してSNS経由でメール通知

通知メール例

アカウントID:123456789012 の SecurityHub でイベント検知がありました。
検知内容を確認し、対応をお願いします。

タイトル: [EC2.2] VPC default security group should not allow inbound and outbound traffic
重大度: HIGH
対象リソース: arn:aws:ec2:ap-northeast-1:123456789012:security-group/sg-12345678
説明: This AWS control checks whether the default security group of any VPC restricts all traffic.

 

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

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 s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as config from 'aws-cdk-lib/aws-config';
import * as securityhub from 'aws-cdk-lib/aws-securityhub';
import * as events from 'aws-cdk-lib/aws-events'; 
import * as targets from 'aws-cdk-lib/aws-events-targets';

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

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

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

    // SecurityHub用トピック
    const securityHubTopic = new sns.Topic(this, 'SecurityHubTopic', {                   
      topicName: 'securityhub-alertnotification',                                        // トピック名
      displayName: 'SecurityHub Alert Notifications'                                     // 表示名
    });

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

    //===========================================
    // S3
    //===========================================
    // Config用S3バケット
    const configBucket = new s3.Bucket(this, 'ConfigBucket', {
      bucketName: 's3b-config',                                                          // バケット名
      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日後にオブジェクトを削除
        }
      ]
    });

    configBucket.addToResourcePolicy(new iam.PolicyStatement({                           // バケットポリシー追加1
      effect: iam.Effect.ALLOW,
      actions: [
        's3:GetBucketAcl',
        's3:ListBucket'
      ],
      resources: [configBucket.bucketArn],
      principals: [new iam.ServicePrincipal('config.amazonaws.com')],
      conditions: {
        StringEquals: {
          'aws:SourceAccount': cdk.Stack.of(this).account               
        }
      }
    }));

    configBucket.addToResourcePolicy(new iam.PolicyStatement({                           // バケットポリシー追加2
      effect: iam.Effect.ALLOW,
      actions: [
        's3:PutObject'
      ],
      resources: [`${configBucket.bucketArn}/AWSLogs/${cdk.Stack.of(this).account}/Config/*`],
      principals: [new iam.ServicePrincipal('config.amazonaws.com')],
      conditions: {
        StringEquals: {
          's3:x-amz-acl': 'bucket-owner-full-control',                                            // バケット所有者にフルコントロールを付与
          'aws:SourceAccount': cdk.Stack.of(this).account            
        }
      }
    }));



    //===========================================
    // Config
    //===========================================
    // サービスロール作成
    const configServiceRole = new iam.CfnServiceLinkedRole(this, 'ConfigServiceLinkedRole', {                     // 既存のAWS Configサービスにリンクされたロール(Config実行に必要な権限を自動付与)※すでに付与されている場合はコメントアウトしてデプロイ
     awsServiceName: 'config.amazonaws.com',                                                                     // サービス名
    });
    

    // レコーダーの作成
    const accountId = cdk.Stack.of(this).account;
    const configRecorder = new config.CfnConfigurationRecorder(this, 'Recorder', {
      roleArn: `arn:aws:iam::${accountId}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig`,    // ConfigのIAMロール(Configがリソースの設定変更を記録する権限)
      recordingGroup: {                                                                                           // 記録対象の設定
        allSupported: true,                                                                                       // サポートされている全リソースタイプを記録
        includeGlobalResourceTypes: true,                                                                         // グローバルリソースも記録対象に含める
      }
    });


    // 配信チャネルの作成
    const configDeliveryChannel = new config.CfnDeliveryChannel(this, 'DeliveryChannel', {                        // 
      s3BucketName: configBucket.bucketName,                                                                      // Config用のバケット
      configSnapshotDeliveryProperties: {                                                                         // 
        deliveryFrequency: 'TwentyFour_Hours'                                                                     // スナップショットを24時間(1日)ごとにS3バケットへ配信
      }
    });
    

    
    //===========================================
    // SecurityHub
    //===========================================
    // AWS基礎セキュリティのベストプラクティスv1.0.0 / CISAWSFoundationsBenchmarkv1.2.0 有効化
    const securityHub = new securityhub.CfnHub(this, 'SecurityHub', { 
      enableDefaultStandards: true,                                                      // デフォルトのセキュリティ基準を有効化
      controlFindingGenerator: 'SECURITY_CONTROL',                                       // セキュリティ管理ベースの検出
    });



    //===========================================
    // EventBridge
    //===========================================

    // SecurityHub用ルール
    const securityHubRule = new events.Rule(this, 'SecurityHubEventRule', {
      ruleName: 'eventbridge-rule-securityhub',                                          // ルール名
      eventPattern: {                                                                    // イベントパターンを指定
        source: ['aws.securityhub'],
        detailType: ['Security Hub Findings - Imported'],                                // SecurityHubによって検出された結果(Findings)がインポートされた際に発行されるイベント
        detail: {
          findings: {
            Compliance: {                                                                // コンプライアンスステータスがFAILEDかどうか
              Status: ['FAILED']                                                         // FAILED:セキュリティポリシーやコンプライアンス要件を満たさない検出結果
            },
            Severity: {
              Label: ['HIGH', 'CRITICAL']                                                // 重要度がHIGH,CRITICALかどうか
            },
            Workflow: {                                                                  // ワークフローステータスがNEWかどうか
              Status: ['NEW']                                                            // NEW:まだ調査や対応が行われていない新しい検出結果
            }
          }
        }
      },
    });

    // 入力パスマップ(InputPathsMap)
    const inputPathsMap: { [key: string]: string } = {
      accountId: '$.detail.findings[0].AwsAccountId',
      description: '$.detail.findings[0].Description',
      resourceId: '$.detail.findings[0].Resources[0].Id',
      securityControlId: '$.detail.findings[0].Compliance.SecurityControlId',
      severity: '$.detail.findings[0].Severity.Label',
      title: '$.detail.findings[0].Title',
    };

    const inputTemplate = "\"アカウントID: の SecurityHub でイベント検知がありました。\"\n\"検知内容を確認し、対応をお願いします。\"\n\n\"タイトル: []

    const cfnRule = securityHubRule.node.defaultChild as events.CfnRule;
    cfnRule.addPropertyOverride('Targets', [
      {
        Arn: securityHubTopic.topicArn,                                                  // SNSトピックARN
        Id: 'SecurityHubTopicTarget',                                                    // ターゲットID
        InputTransformer: {                                                              // 入力トランスフォーマー
          InputPathsMap: inputPathsMap,
          InputTemplate: inputTemplate,
        },
      },
    ]);

    // SNS Topic ポリシー(EventBridge からの Publish を許可)
    securityHubTopic.addToResourcePolicy(new iam.PolicyStatement({
      sid: 'AllowEventBridgePublish',                                                    // ステートメントID
      effect: iam.Effect.ALLOW,                                                          // 許可
      principals: [new iam.ServicePrincipal('events.amazonaws.com')],                    // EventBridgeサービスプリンシパル
      actions: ['sns:Publish'],                                                          // Publish権限
      resources: [securityHubTopic.topicArn],                                            // 対象トピック
      conditions: {                                                                      // ルールARNに限定
        ArnEquals: { 'aws:SourceArn': securityHubRule.ruleArn }
      }
    }));
  }
}

まとめ

今回は、AWS ConfigとSecurityHubを活用したセキュリティ監視をAWS CDKで実装しました。

IaCとして管理することで、環境間での一貫したセキュリティポリシーの展開や、セキュリティ設定の変更履歴管理も可能になります。また、継続的なコンプライアンス監視により、セキュリティガバナンスの向上と監査対応の効率化を実現できます。

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

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