発見的ガードレールの効率的な通知の構成方法

目次

はじめに

前回の記事で、AWS Control Towerを利用することでAWSマルチアカウント管理の利便性を高めることができることを紹介しました。本記事では、AWS Control Towerが既定でセットアップしてくれる機能の一つとして、AWS環境の設定値が発見的ガードレールに非準拠の場合に利用者に通知してくれる機能の使い方と課題、その課題をどのように回避できるかを説明します。

発見的ガードレールとは

発見的ガードレールとは、AWS Configルールを利用したAWS環境の設定値が望ましい状態になっているかを管理する仕組みを指します。「AWS 基礎セキュリティのベストプラクティス」等のAWS Security Hubのセキュリティ標準を有効化した際に、特定のコントロールに設定値が非準拠になってスコアが変動するのも同じ仕組みで実現しています。

AWS Security Hubの場合、運用者が月次などの頻度で能動的にマネージメントコンソールを確認することで最新のスコアを把握し、設定値の是正対応を行うといった運用を想定することができますが、場合によってはAmazon RDSインスタンスがパブリックアクセス可能の設定になっている場合など、あるべき形から逸れた設定値がAWS環境で設定された際に、即座に通知してほしいという要望もあるかもしれません。

そうした要望に応える形で、AWS Control TowerによってAWSマルチアカウント構成によるランディングゾーンを作成すると、発見的ガードレールの非準拠を任意の宛先に通知してくれるSNSトピックが作成されます。このSNSトピックにメールアドレスなどをサブスクライブすれば、設定の非準拠を検知した際に運用者まで任意の方法で通知が行われます。

AWS Control Towerによって作成されるSNSトピック

AWSマルチアカウント構成において発見的ガードレール非準拠の通知はAuditアカウントに集約されます。AuditアカウントにはAWS Control Towerによって用途に応じて3つのSNSトピックが作成されます。

SNSトピック名 説明
aws-controltower-AllConfigNotifications AWS Configの準拠・非準拠の状態変更時・リソースの変更時と、CloudTrailのログデリバリー時に通知が行われる
aws-controltower-SecurityNotifications Control Towerがサポートするリージョンに作成され、リージョン内のAWS Configの準拠・非準拠の状態変更時・リソースの変更時に通知される。このトピックへの通知は、aws-controltower-NotificationForwarderというLambda関数により、aws-controltower-AggregateSecurityNotificationsトピックにフォワードされる
aws-controltower-AggregateSecurityNotifications 各リージョンのaws-controltower-SecurityNotificationsに通知された内容が、aws-controltower-NotificationForwarderのLambda関数によってAWS Control Towerのホームリージョンのaws-controltower-AggregateSecurityNotificationsに集約して通知される

Compliance notifications by SNS in the audit account - AWS Control Tower

Guidance on subscribing to SNS Topics - AWS Control Tower

SNSトピックによる通知はリージョンを跨げないため、間に通知をフォワードするLambda関数を挟んで、一つのSNSトピックに通知を集約する構成となっています。

公式ドキュメントによると、AWS Configの準拠状況を通知する用途なら各リージョンの通知が集約されるaws-controltower-AggregateSecurityNotificationsトピックにサブスクライブすることが推奨されており、より詳細な情報も通知したければaws-controltower-AllConfigNotificationsにサブスクライブするよう記載されています。

しかし、いずれのトピックにサブスクライブした場合も、実際にはかなり大量の通知が飛んでくることになり、運用者がそれらの通知を適切に捌く事は現実的でないのが実情です。というのも、aws-controltower-AggregateSecurityNotificationsトピックにサブスクライブした場合、AWS Security Hubの実態はAWS Configルールであるため、AWS Security Hubのコントロールで非準拠になった設定が全て通知され、単にAWS Control Tower標準の発見的ガードレールの非準拠状況だけでなく、AWS Security Hubの状況まで全て通知されるからです。この状況を避けるために、冒頭ではAWS Security Hubのスコアは運用者が月次などの任意の頻度で能動的に確認する例を記載しました。

一方、aws-controltower-AllConfigNotificationsにサブスクライブすると、それ以上に大量の通知が飛んでくるため、もっと運用が回らなくなります。

具体的には、AWS Configに関する以下の全ての通知が行われます。

  • リソース設定の変更
  • リソース設定履歴のアカウントへの配信
  • 記録対象のリソースの設定スナップショットがアカウントで起動および配信
  • リソースの準拠状態
  • リソースに対してルールの評価が開始
  • AWS Config からアカウントへの通知の配信失敗

Notifications that AWS Config Sends to an Amazon SNS topic - AWS Config

大量の通知が飛んでくることは公式ドキュメントでも触れられており、AWS Control Towerが作成してくれるSNSトピックにサブスクライブして運用者への通知を行うというのは、そのままでは現実的には難しいです。

SNS topics in AWS Control Tower are extremely noisy, by design.

発見的ガードレールの通知運用をどのように行うか

AWS Configルールの通知構成

上記の図は各AWSアカウントで発生した発見的ガードレールを含むAWS Configルールの通知をAWS Control Towerがどのように構成するかを表したものです。

赤枠で囲った範囲がAWS Control Towerが作成する通知の仕組みであり、これまで説明した通知を実現する構成です。各アカウントのAWS Configルールに対してEventBridgeルールが設定され、リージョン内でSNSトピックに通知されます。SNSトピックにはLambda関数がサブスクライブされており、AuditアカウントのControl TowerホームリージョンのSNSトピックに通知が集約されます。

この仕組みにそのまま乗ると、先述の通り運用者に通知が大量に来ることになるため、対応策を考える必要があります。

AWSからは二つの方法が提示されており、一つは追加のLambda関数を作成してaws-controltower-AggregateSecurityNotificationsトピックにサブスクライブすることで、特定の通知内容をフィルターすることです。もう一つはAWS Control Towerが作成するのと同等の通知構成を自身で行い、EventBridgeルールで通知対象をフィルターする方法です。

Administrators who wish to filter out specific types of notifications from an SNS topic can create an AWS Lambda function and subscribe it to the SNS topic. Alternatively, you can set up an EventBridge rule to filter notifications, as described in this support article, How can I be notified when an AWS resource is non-compliant using AWS Config?

Compliance notifications by SNS in the audit account - AWS Control Tower

EventBridgeルールで通知対象をフィルターするなら、AWS Control Towerが作成するaws-controltower-ConfigComplianceChangeEventRuleを編集すれば早いということになりますが、残念ながらその操作はAWSからサポートされません。AWS Control Towerが作成したリソースを利用者が編集することはAWSのサポート外となり、もし編集した場合はControl Towerのバージョンアップや廃止の際に問題が発生する可能性があります。

本記事では、そうした理由からAWS Control Towerが作成するのと同等の構成を自身で作成して、aws-controltower-ConfigComplianceChangeEventRuleに相当するEventBridgeルールにて通知対象をフィルターする方法を紹介します。

AWS Control Towerの発見的ガードレールの準拠状況のみを通知する構成方法

先述の構成図の青枠で囲った範囲のリソースを個別に作成することで、特定の対象のみ通知する構成を行うことができます。本記事では、Control Towerが標準で管理するガードレールの非準拠のみを通知するようフィルターする構成とします。

作成するAWSリソースの設定値と説明を文章で全て網羅することはできないので、必要なリソースを一発でデプロイするCloudFormationテンプレートを記載します。

①Auditアカウントで通知用のSNSトピックを作成する

AWSTemplateFormatVersion: "2010-09-09"
Description: Create Notification SNS Topic for Non Compliant Guardrails

Parameters:
  EmailAddress:
    Description: Email Address for recieving Non Compliant Notification.
    Type: String

  OrgID:
    Description: Organization ID for recieving Non Compliant Notification. e.g. o-xxxxxxxxxx
    Type: String

Resources:
## SNS Topic
  NonCompliantNotificationSNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: NonCompliantNotificationTopic

## SNS Topic Policy
  SNSNotificationPolicy:
    Type: AWS::SNS::TopicPolicy
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F18
            reason: "Condition restricts permissions to current account."
    Properties:
      Topics:
        - !Ref NonCompliantNotificationSNSTopic
      PolicyDocument:
        Statement:
          - Sid: __default_statement_ID
            Effect: Allow
            Principal:
              AWS: "*"
            Action:
              - SNS:GetTopicAttributes
              - SNS:SetTopicAttributes
              - SNS:AddPermission
              - SNS:RemovePermission
              - SNS:DeleteTopic
              - SNS:Subscribe
              - SNS:ListSubscriptionsByTopic
              - SNS:Publish
              - SNS:Receive
            Resource: !Ref NonCompliantNotificationSNSTopic
            Condition:
              StringEquals:
                AWS:SourceOwner: !Sub ${AWS::AccountId}
          - Sid: AcceptNotificationsfromwithinOrganization
            Effect: Allow
            Principal:
              "AWS": "*"
            Action: sns:Publish
            Resource: !Ref NonCompliantNotificationSNSTopic
            Condition:
              StringEquals:
                aws:PrincipalOrgID: !Ref OrgID

## SNS Subscription
  NonCompliantEmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: email
      Endpoint: !Ref EmailAddress
      TopicArn: !Ref NonCompliantNotificationSNSTopic

②ガードレールの非準拠を監視したいアカウントで、非準拠通知に必要なIAMロールを作成する

AWSTemplateFormatVersion: "2010-09-09"
Description: Configure SNS Notification Forward IAM Roles
Parameters:
  SecurityTopicName:
    Type: String
    Default: NonCompliantNotificationTopic
    Description: Security Notification SNS Topic Name.
  AuditAccountId:
    Type: String
    MaxLength: 12
    MinLength: 12
    Description: AWS Account Id of the Audit account.
  RoleName:
    Type: String
    Default: SnsNotificationForwardRole
    Description: SNS Notification Forward IAM Roles.

Resources:
  SnsNotificationForwardLambdaRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub ${RoleName}
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: "/"
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Policies:
        - PolicyName: sns
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - "sns:publish"
                Resource: !Sub arn:aws:sns:*:${AuditAccountId}:${SecurityTopicName}

Outputs:
  SnsNotificationForwardRoleName:
    Description: SnsNotificationForwardRoleName
    Value: !Ref RoleName
    Export:
      Name: SnsNotificationForwardRoleName

③ガードレールの非準拠を監視したいアカウントで、非準拠通知に必要なリソースを作成する

AWSTemplateFormatVersion: "2010-09-09"
Description: Configure Eventbridge Rule, local SNS Topic, notifications forwarding Lambda and CloudWatch Logs events to forward messages from local SNS Topic to Security Topic
Parameters:
  RoleName:
    Type: String
    Default: SnsNotificationForwardRole
    Description: SNS Notification Forward IAM Roles.
  SecurityTopicName:
    Type: String
    Default: NonCompliantNotificationTopic
    Description: Security Notification SNS Topic Name.
  SecurityAccountId:
    Type: "String"
    MaxLength: 12
    MinLength: 12
    Description: AWS Account Id of the Audit account.
  SecurityAccountRegion:
    Type: "String"
    Default: ap-northeast-1
    Description: AWS Control Tower Home Region of the Audit account.
  LogsRetentionInDays:
    Description: "Specifies the number of days you want to retain notification forwarding log events in the Lambda log group."
    Type: Number
    Default: 14
    AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]
  EnableConfigRuleComplianceChangeAlarm:
    Type: String
    Description: "Enable notifications for AWS Config rule compliance status changes"
    Default: true
    AllowedValues:
      - true
      - false

Conditions:
  EnableConfigRuleChangeNotification: !Equals
    - !Ref EnableConfigRuleComplianceChangeAlarm
    - "true"

Resources:
  ForwardSnsNotificationGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: /aws/lambda/LambdaNotificationForwarder
      RetentionInDays: !Ref LogsRetentionInDays

  ForwardSnsNotification:
    Type: "AWS::Lambda::Function"
    DependsOn: ForwardSnsNotificationGroup
    Properties:
      FunctionName: LambdaNotificationForwarder
      Description: SNS message forwarding function for aggregating account notifications.
      Code:
        ZipFile: !Sub |
          from __future__ import print_function
          import boto3
          import json
          import os
          def lambda_handler(event, context):
              #print("Received event: " + json.dumps(event, indent=2))
              region = os.environ.get('region')
              sns = boto3.client('sns', region_name=region)
              subject=event['Records'][0]['Sns']['Subject']
              if subject is None:
                  subject = 'None'
              message = event['Records'][0]['Sns']['Message']
              try:
                  msg = json.loads(message)
                  message = json.dumps(msg, indent=4)
                  if 'detail-type' in msg:
                    subject = msg['detail-type']
              except:
                  print('Not json')
              response = sns.publish(
                  TopicArn=os.environ.get('sns_arn'),
                  Subject='Config非準拠を検知しました',
                  Message=message
              )
              print(response)
              return response
      Handler: "index.lambda_handler"
      MemorySize: 128
      Role: !Sub arn:aws:iam::${AWS::AccountId}:role/${RoleName}
      Runtime: "python3.11"
      Timeout: 60
      Environment:
        Variables:
          # region: !Sub ${AWS::Region}
          # sns_arn: !Sub arn:aws:sns:${AWS::Region}:${SecurityAccountId}:${SecurityTopicName}
          region: !Sub ${SecurityAccountRegion}
          sns_arn: !Sub arn:aws:sns:${SecurityAccountRegion}:${SecurityAccountId}:${SecurityTopicName}

  LocalSecurityTopic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: LocalSecurityNotificationTopic
      TopicName: LocalSecurityNotificationTopic

  SNSNotificationPolicy:
    Type: AWS::SNS::TopicPolicy
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: F18
            reason: "Condition restricts permissions to current account."
    Properties:
      Topics:
        - !Ref LocalSecurityTopic
      PolicyDocument:
        Statement:
          - Sid: __default_statement_ID
            Effect: Allow
            Principal:
              AWS: "*"
            Action:
              - SNS:GetTopicAttributes
              - SNS:SetTopicAttributes
              - SNS:AddPermission
              - SNS:RemovePermission
              - SNS:DeleteTopic
              - SNS:Subscribe
              - SNS:ListSubscriptionsByTopic
              - SNS:Publish
              - SNS:Receive
            Resource: !Ref LocalSecurityTopic
            Condition:
              StringEquals:
                AWS:SourceOwner: !Sub ${AWS::AccountId}
          - Sid: TrustCWEToPublishEventsToMyTopic
            Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sns:Publish
            Resource: !Ref LocalSecurityTopic

  SNSNotificationSubscription:
    Type: "AWS::SNS::Subscription"
    Properties:
      Endpoint: !GetAtt ForwardSnsNotification.Arn
      Protocol: lambda
      TopicArn: !Ref LocalSecurityTopic

  SNSInvokeLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      Principal: sns.amazonaws.com
      SourceArn: !Ref LocalSecurityTopic
      FunctionName: !GetAtt ForwardSnsNotification.Arn

  # Enable notifications for AWS Config Rule compliance changes for NON_COMPLIANT
  CWEventRuleComplianceChangeEventForNONCOMPLIANT:
    Type: AWS::Events::Rule
    Condition: EnableConfigRuleChangeNotification
    Properties:
      Name: GRConfigNonCompliant
      Description: "Event Rule to send notification on NON_COMPLIANT Config Rule compliance changes."
      EventPattern:
        {
          "source": ["aws.config"],
          "detail-type": ["Config Rules Compliance Change"],
          "detail":
            {
              "messageType": ["ComplianceChangeNotification"],
              "configRuleName": [{ "prefix": "AWSControlTower_" }],
              "newEvaluationResult": { "complianceType": ["NON_COMPLIANT"] },
            },
        }
      State: ENABLED
      Targets:
        - Id: !Sub "Compliance-Change-Topic"
          Arn: !Ref LocalSecurityTopic
          InputTransformer:
            InputPathsMap:
              "Account": "$.account"
              "resourceId": "$.detail.resourceId"
              "Region": "$.region"
              "Time": "$.time"
              "complianceType": "$.detail.newEvaluationResult.complianceType"
              "configRuleName": "$.detail.configRuleName"
            InputTemplate: |
              "次のリソースで、Configルール(ガードレール)の非準拠を検知しました。"

                     "- Region : <Region> "
                     "- Account : <Account>"
                     "- Time : <Time>"
                     "- resourceId : <resourceId>"
                     "- configRuleName : <configRuleName>"
                     "- complianceType : <complianceType>"
Outputs:
  LocalSecurityTopic:
    Description: Local Security Notification SNS Topic ARN
    Value: !Ref LocalSecurityTopic
  LocalSecurityTopicName:
    Description: Local Security Notification SNS Topic Name
    Value: !GetAtt LocalSecurityTopic.TopicName

※1 通知先をControl Towerのホームリージョンに集約せず、リージョンごとにSNSトピックを作成して通知する場合、①のSNSトピックを通知対象リージョンごとに作成し、③のLambdaNotificationForwarderのLambda関数の環境変数regionとsns_arnをリージョンごとに設定ください。

※2 通知対象を変更する場合は、③のEventBridgeルールのEventPatternを変更ください。

まとめ

本記事では、AWS Control Towerによって設定される発見的ガードレール等の非準拠を通知する仕組みについて解説しました。そして、既定で作成される通知の対象が広範であるため、実運用での利用は厳しいことも説明しました。その課題に対してCloudFormationテンプレートを利用して個別の通知構成を行うことで、運用に耐えうる程度に通知対象を絞る方法について紹介しました。
AWS Control Towerが既定で作成する発見的ガードレールの通知の仕組みをよく理解できていなかった方や、既定の構成とは異なるワークアラウンドをお探しの方のお役に立てば幸いです。