AWSコンソールサインインとIAM操作の通知を実装する方法 [AWS CloudFormation テンプレート付き]

こんにちは、SCSKの石原です。

皆様はAWSコンソールサインインやIAM操作の通知が欲しくなったことありますでしょうか。

AWS OrganizationsやAWS Configを活用して、組織として中央統制の管理になっていれば問題はないですが、必ずしもそのような管理がされているわけではありません。代表的な例として、AWS認定試験の受験勉強のため利用した個人所有のAWSアカウントが挙げられます。

今回は「AWSコンソールサインイン」と「IAM操作」の通知機能をさくっと実装する例を紹介します。すぐにプロビジョニングできるように 可能な限り、AWS CloudFormation テンプレートを付けさせていただきたいと思います。

AWSコンソールサインイン通知

今回はCloudTrailの証跡をCloudWatchLogsの出力したのち、サブスクリプションフィルターを利用してAWSコンソールサインインのイベントを通知します。この実装では、CloudWatchLogsに集約したのち検知するため数分程度遅れて通知されることになります。

実装イメージは下記の通りです。

CloudTrailの設定

AWSコンソールサインインイベントはリージョン毎に異なりますので、まずCloudTrailにコンソールサインインの記録を集約することになります。CloudTrailでは全てのリージョンに適用される証跡が作成できますので、そちらをご利用になると良いかと思います。

AWS CloudTrail による IAM および AWS STS の API コールのログ記録 - AWS Identity and Access Management
AWS STS による IAM および AWS CloudTrail のログ記録について説明します。

続いて、証跡をCloudWatchLogsに出力する設定をします。

CloudWatch Logs へのイベントの送信 - AWS CloudTrail
証跡から CloudWatch Logs にイベントを送信するように設定して、CloudTrail ログイベントをモニタリングできるようにします。

Lambda・SNSの設定

SNSにパブリッシュするLambda関数を作成し、SNSからEmailに送信できるようにトピックとサブスクリプションを作成します。

SNSのサブスクリプションについては通知先を追加する可能性があるのでネスト構成としました。
テンプレートは以下の通りで、親スタック用と子スタック用の2つあります。

1名のみに通知するのであれば、親スタックに追記していただいても問題ありません。

親スタック
AWSTemplateFormatVersion: "2010-09-09"
Description: "aws-console-login-cloudtrail-event-notify"
Parameters:
  LowerStackSnsSubscriptionYamlUrl:
    Type: String
    Default: "Your LowerStackSnsSubscriptionYamlUrl"

  NotifyEmailAddress:
    Type: String

  EventType:
    Type: String
    Default: "AwsConsoleSignIn"

Resources:
# =====================================
# SNS
# =====================================
  ConsoleLoginNotifyTopic:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: "ConsoleLoginNotify"
      TopicName: !Sub "Topic-${AWS::AccountId}-${EventType}"
  
  LowerStackSubscription:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref LowerStackSnsSubscriptionYamlUrl 
      Parameters:
        NotifyEndpoint: !Ref NotifyEmailAddress
        TopicArn: !Ref ConsoleLoginNotifyTopic

# =====================================
# Lambda
# =====================================
  LambdaIAMRole:
    Type: "AWS::IAM::Role"
    Properties:
      Path: "/"
      RoleName: !Sub "Lambda2snsRole"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "lambda.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      MaxSessionDuration: 3600
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSNSFullAccess
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess
      Description: "Allows Lambda To SNS."
  
  AWSLambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      FunctionName: !Sub "func-${AWS::AccountId}-${EventType}"
      Handler: index.handler
      Role: !GetAtt LambdaIAMRole.Arn
      Timeout: 60
      Runtime: python3.8
      Environment:
        Variables:
          ALARM_SUBJECT: !Sub "${AWS::AccountId}-${EventType}"
          SNS_TOPIC_ARN: !Ref ConsoleLoginNotifyTopic
      Code:
        ZipFile: |
          import base64
          import json
          import zlib
          import datetime
          import os
          import boto3
          from botocore.exceptions import ClientError

          def handler(event, context):
              data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
              data_json = json.loads(data)
              log_entire_json = json.loads(json.dumps(data_json["logEvents"], ensure_ascii=False))
              log_entire_len = len(log_entire_json)
              for i in range(log_entire_len): 
                  log_json = json.loads(json.dumps(data_json["logEvents"][i], ensure_ascii=False))

                  try:
                      sns = boto3.client('sns')
                      publishResponse = sns.publish(
                          TopicArn = os.environ['SNS_TOPIC_ARN'],
                          Message = log_json['message'],
                          Subject = os.environ['ALARM_SUBJECT']
                      )
              
                  except Exception as e:
                      print(e)

Outputs:
  ConsoleLoginNotifyTopicARN:
    Description: "SNS Topic ARN"
    Value: !Ref ConsoleLoginNotifyTopic
子スタック

ネストするコード「LowerStackSnsSubscription」は下記の通りです。

AWSTemplateFormatVersion: "2010-09-09"
Description: "LowerStackSnsSubscriptionYaml"
Parameters:
  NotifyEndpoint:
    Type: String
  TopicArn:
    Type: String

Resources:
  Subscription:
    Type: AWS::SNS::Subscription
    Properties: 
      Endpoint: !Ref NotifyEndpoint
      Protocol: email
      TopicArn: !Ref TopicArn

ダウンロードはこちらからどうぞ

CloudwatchLogsのサブスクリプションフィルター設定

CloudwatchLogsのサブスクリプションフィルターを利用して、特定の出力があった場合に先ほどCFnテンプレートで作成したLambda関数へログを送信しましょう。

CloudWatch Logs サブスクリプションフィルターの使用 - Amazon CloudWatch Logs
AWS CloudTrail イベントを含むロググループにサブスクリプションフィルタを関連付けます。

CloudTrailの証跡を出力しているCloudWatchLogsのロググループを選択して、サブスクリプションフィルターを作成します。設定内容は下記の通りです

Lambda関数 func-[アカウントID]-AwsConsoleSignIn ※デフォルトの場合
ログの形式 Amazon CloudTrail
フィルターパターン { $.eventType = “AwsConsoleSignIn” }
サブスクリプションフィルター名 任意

以上でAWSコンソールサインインイベント通知の設定が完了です。この設定では下記の図に示すサインインイベントを検知します。IAMユーザにサインインだけでなく、ジャンプアカウントからのAssumeRoleやAWS SSO経由のアクセスにも対応しています。

IAM操作通知

続いてIAM操作の通知を設定します。

今回はCloudWatchのメトリクスフィルターとアラームを利用して通知を行います。この実装ではユーザが何の操作をしたかまでは通知されません。内容まで通知したい場合は、サブスクリプションフィルターが2つまで設定できますので、上述のサインインイベントのサブスクリプションフィルターを利用した実装をご検討いただければと思います。

対象とするIAM操作は以下の通りです。監視が必要なAPIがある場合は、CFnを修正してご使用ください。

AddUserToGroup
AttachUserPolicy
CreateAccessKey
CreateAccessKey
CreateLoginProfile
CreatePolicyVersion
CreateRole
CreateUser
PutUserPolicy
Actions - AWS Identity and Access Management
The following actions are supported:

CloudWatch・SNSの設定

CloudTrailからCloudWatchLogsへの出力は、「AWSコンソールサインイン」の設定で実施していますので、CloudWatchとSNSの設定をCFnテンプレートを利用して設定します。

AWSTemplateFormatVersion: "2010-09-09"
Description: "aws-cloudtrail-event-metricsfilter-iam"
Parameters:
  LowerStackSnsSubscriptionYamlUrl:
    Type: String
    Default:  "Your LowerStackSnsSubscriptionYamlUrl"

  NotifyEmailAddress:
    Type: String

  EventType:
    Type: String
    Default: "AwsIAMEvent"

  CloudWatchLogsGroup:
    Type: String

Resources:
# =====================================
# SNS
# =====================================
  EventTypeNotifyTopic:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: !Sub ${EventType}-${AWS::AccountId}
      TopicName: !Sub "Topic-${AWS::AccountId}-${EventType}"
  
  LowerStackSubscription:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Ref LowerStackSnsSubscriptionYamlUrl 
      Parameters:
        NotifyEndpoint: !Ref NotifyEmailAddress
        TopicArn: !Ref EventTypeNotifyTopic

# =====================================
# Cloudwatch
# =====================================
  MetricsFilterIAMWarn:
    Type: AWS::Logs::MetricFilter
    Properties: 
      FilterPattern: "{($.eventName=CreateAccessKey)||($.eventName=CreateUser)||($.eventName=CreateLoginProfile)||($.eventName=AddUserToGroup)||($.eventName=PutUserPolicy)||($.eventName=AttachUserPolicy)||($.eventName=AttachUserPolicy)||($.eventName=CreateAccessKey)||($.eventName=CreateRole)||($.eventName=CreatePolicyVersion)}"
      LogGroupName: !Ref CloudWatchLogsGroup
      MetricTransformations: 
        - 
          DefaultValue: 0
          MetricName: !Sub ${CloudWatchLogsGroup}/Warn
          MetricNamespace: "IAMEvent"
          MetricValue: "1"

  AlarmIAMWarn:
    Type: AWS::CloudWatch::Alarm
    Properties: 
      AlarmActions: 
        - !Ref EventTypeNotifyTopic
      AlarmDescription: "IAM Event"
      AlarmName: "AlarmIAMWarn"
      ComparisonOperator: GreaterThanOrEqualToThreshold
      DatapointsToAlarm: 1
      EvaluationPeriods: 1
      MetricName: !Sub ${CloudWatchLogsGroup}/Warn
      Namespace: "IAMEvent"
      Period: 300
      Statistic: Maximum
      Threshold: 1
      TreatMissingData: notBreaching

Outputs:
  EventTypeNotifyTopicARN:
    Description: "SNS Topic ARN"
    Value: !Ref EventTypeNotifyTopic

これで実装は完了です。IAM操作に気付けるようになりましたね。

終わりに

今回は実施しなかった別の実装方法もありますので、一言コメントとともに記載しておきます。

【AWSコンソールサインイン通知】

EventBridgeでの実装 コンソールサインインイベントが必ずしもus-east-1で発生するわけではない。すべてのリージョンに設定する必要があるので見送り

【IAM操作通知】

サブスクリプションフィルターでの実装 設定上限が2つなので、拡張性がないことから見送り
EventBridgeでの実装 公式の回答がありましたので見送り(https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-iam-event-monitoring/)
AWS Config 今回対象としていた、組織で管理されていないアカウントではAWS Configが有効になっていない可能性が高いので見送り

 

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