0
0

AWSマネジメントコンソールログイン通知をメールで受け取る

Posted at

概要

AWSマネジメントコンソールにログインしたイベントを、メールで通知させる仕組みを構築します。知らないところで誰かに勝手にログインされても気がつけるようになります。CloudFormationテンプレートで、簡単に導入できるようにしました。

構成

今回構築するシステム構成です。

image.png

通知サンプル

以下のようなメールを受け取ることができます。

image.png

※ログ情報の一部をそのまま連携しただけなので、見た目が悪いです。本文の修正は、別の記事で紹介する予定です。

各コンポーネントの設定

それでは、各コンポーネントを説明してきます。

マネジメントコンソール

AWSをブラウザ経由で使用する時のログイン先です。時々、ログイン先のリージョンが変わります。そのためのすべてのリージョンのイベントを監視する必要があります。

CloudTrail

ログを記録するためのセキュリティ関連サービスです。ログ保存先としてS3バケットを指定します。また、すべてのリージョンのログを記録するように設定します。

マネジメントコンソールでログインした場合、イベント「ConsoleLogin」が記録されます。私の場合シドニー(ap-southeast-2)に記録されていました。
image.png

image.png

CloudTrailのログはS3バケット以外に、CloudWatch Logsの指定したロググループへ連携すること(オプション)ができます。
image.png

CloudWatch Logs

ロググループにサブスクリプションを設定し、イベント「ConsoleLogin」を受信した場合、通知と記録のためのLambda関数を起動します。
image.png

サブスリプションフィルターのパターンには、{ $.eventName = "ConsoleLogin" }と設定します。
image.png

Lambda関数

受け取った「ConsoleLogin」イベント情報をデータテーブルへ出力します。また、メール通知するための件名と本文を作成します。

他のリソースへのアクセスが必要ですので、Lambda実行ロールに権限を付与します。
image.png
image.png

ログインイベントデータテーブル

ログインイベントデータを整形し、DynamoDBテーブルに格納します。UserNameにはコンソールにログインしたIAMUser名(Rootユーザでログインした場合はRootと表示)やログイン日時、アクセス元のIPアドレス情報nが記録されます。
image.png
DynamoDBテーブルでは、データレコードに有効期限を設定できます。unixtime(TTL)が期限を示しており、eventTimeの90日後に設定しています。有効期限を超えた古いレコードはAWSが自動的に削除を行います。

SendEmailTopic

通知先のメールアドレスを登録します。

以下のようにメールアドレスを登録してください。
マネジメントコンソールより Amazon SNS > トピック > SendEmailTopic-xxxxxxxx へ移動し、「サブスクリプションの作成」
image.png

サブスクリプションの作成画面で、プロトコルに「Eメール」、エンドポイントに送信先のメールアドレスを入力します。
image.png

ステータスが 保留中の確認 となります。
image.png

以下のようなメールが送信されてくるので、「Confirm subscription」を右クリックして、リンクのURLをコピー※
※そのままクリックすると、SNS通知メールに含まれるサブスクリプション解除リンクが有効となってしまうため、せっかく通知を受け取れる状態になっても誤ってメール配信を解除してしまう可能性があります。
image.png

SNSの画面に戻り、保留中の確認ステータスとなっているサブスクリプションを選択し、「サブスクリプションの確認」をクリック
image.png

サブスクリプションの確認に、先程コピーしたURLのリンクを貼り付けて、「サブスクリプションの確認」をクリック
image.png

ステータスが「確認済み」に変わります。
image.png

以上で完成です。

CFnテンプレート

今回2つのテンプレートを作成しました。

CloudTrail設定テンプレート

本テンプレートでは、CloudTrailとログ保存先のS3バケット、ロググループ、それらに付随する関連リソースを作成します。CloudTrailは設定済みであり、CloudWatch Logsに連携するように設定しているのであれば、本テンプレートの展開は必要ありません。

CloudTrail-template.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Create CloudTrail trail template

Parameters:
  ResourceSuffixName:
    Type: "String"
    Default: "ab0925cd"
    Description: "This suffix name will be added to newly created resource's name."
  
Resources:
  CloudTrailLogGroup:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::Logs::LogGroup"
    DeletionPolicy: "Delete"
    Properties:
      LogGroupClass: "STANDARD"
      RetentionInDays: 30
      LogGroupName: !Sub "aws-cloudtrail-loggroup-${AWS::AccountId}-${ResourceSuffixName}"
      DataProtectionPolicy: {}
  CloudTrailRoleForCloudWatchLogs:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::IAM::Role"
    DeletionPolicy: "Delete"
    Properties:
      Path: "/service-role/"
      ManagedPolicyArns:
      - !Ref CloudTrailRolePolicyForCloudWatchLogs
      MaxSessionDuration: 3600
      RoleName: "CloudTrailRoleForCloudWatchLogs"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Action: "sts:AssumeRole"
          Effect: "Allow"
          Principal:
            Service: "cloudtrail.amazonaws.com"
  CloudTrailTrail:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::CloudTrail::Trail"
    DeletionPolicy: "Delete"
    DependsOn: S3BucketPolicy
    Properties:
      IncludeGlobalServiceEvents: true
      EventSelectors: []
      CloudWatchLogsRoleArn:
        Fn::GetAtt:
        - "CloudTrailRoleForCloudWatchLogs"
        - "Arn"
      AdvancedEventSelectors:
      - FieldSelectors:
        - Field: "readOnly"
          Equals:
          - "false"
        - Field: "eventCategory"
          Equals:
          - "Management"
        Name: "Management events selector"
      TrailName: !Sub "Management-events-${ResourceSuffixName}"
      InsightSelectors: []
      CloudWatchLogsLogGroupArn:
        Fn::GetAtt:
        - "CloudTrailLogGroup"
        - "Arn"
      IsMultiRegionTrail: true
      S3BucketName:
        Ref: "S3BucketForCloudtraillogs"
      EnableLogFileValidation: true
      Tags: []
      IsLogging: true
  CloudTrailRolePolicyForCloudWatchLogs:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::IAM::ManagedPolicy"
    DeletionPolicy: "Delete"
    Properties:
      ManagedPolicyName: "CloudTrailRolePolicyForCloudWatchLogs"
      Path: "/service-role/"
      Description: "CloudTrail role to send logs to CloudWatch Logs"
      Groups: []
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Resource:
          - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-cloudtrail-loggroup-${AWS::AccountId}-${ResourceSuffixName}:*"
          Action:
          - "logs:CreateLogStream"
          Effect: "Allow"
          Sid: "AWSCloudTrailCreateLogStream2014110"
        - Resource:
          - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-cloudtrail-loggroup-${AWS::AccountId}-${ResourceSuffixName}:*"
          Action:
          - "logs:PutLogEvents"
          Effect: "Allow"
          Sid: "AWSCloudTrailPutLogEvents20141101"
  S3BucketPolicy:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::S3::BucketPolicy"
    DeletionPolicy: "Delete"
    Properties:
      Bucket:
        Ref: "S3BucketForCloudtraillogs"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Condition:
            StringEquals:
              AWS:SourceArn: !Sub "arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/Management-events-${ResourceSuffixName}"
          Resource: !Sub "arn:aws:s3:::aws-cloudtrail-logs-${AWS::AccountId}-${ResourceSuffixName}"
          Action: "s3:GetBucketAcl"
          Effect: "Allow"
          Principal:
            Service: "cloudtrail.amazonaws.com"
          Sid: "AWSCloudTrailAclCheck20150319"
        - Condition:
            StringEquals:
              AWS:SourceArn: !Sub "arn:aws:cloudtrail:${AWS::Region}:${AWS::AccountId}:trail/Management-events-${ResourceSuffixName}"
              s3:x-amz-acl: "bucket-owner-full-control"
          Resource: !Sub "arn:aws:s3:::aws-cloudtrail-logs-${AWS::AccountId}-${ResourceSuffixName}/AWSLogs/${AWS::AccountId}/*"
          Action: "s3:PutObject"
          Effect: "Allow"
          Principal:
            Service: "cloudtrail.amazonaws.com"
          Sid: "AWSCloudTrailWrite20150319"
  S3BucketForCloudtraillogs:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::S3::Bucket"
    DeletionPolicy: "Delete"
    Properties:
      PublicAccessBlockConfiguration:
        RestrictPublicBuckets: true
        IgnorePublicAcls: true
        BlockPublicPolicy: true
        BlockPublicAcls: true
      LifecycleConfiguration:
        Rules:
        - Status: "Enabled"
          ExpirationInDays: 60
          Id: "60 days retention"
      BucketName: !Sub "aws-cloudtrail-logs-${AWS::AccountId}-${ResourceSuffixName}"
      OwnershipControls:
        Rules:
        - ObjectOwnership: "BucketOwnerEnforced"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - BucketKeyEnabled: false
          ServerSideEncryptionByDefault:
            SSEAlgorithm: "AES256"

ログイン通知処理テンプレート

CloudTrailのログ情報が連携されているロググループを指定して、ログループのLambdaサブスクリプションやDynamoDBテーブル、SNSトピック、そららに付随する関連リソースを作成します。
image.png
テンプレート展開後は、SNSトピックへのメールアドレス登録は手動で行ってください。

ConsoleLoginNotification-template.yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Console Login event notification template

Parameters:
  ResourceSuffixName:
    Type: "String"
    Default: "ab0925cd"
    Description: "This suffix name will be added to newly created resource's name."
  
  ExistingCloudTrailLogGroupName:
    Type: "String"
    Description: "The name of an existing CloudTrail log group."
  
Resources:
  LogGroupSubscriptionFilter:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::Logs::SubscriptionFilter"
    DeletionPolicy: "Delete"
    DependsOn: ConsoleLoginNotificationLambdaPermission
    Properties:
      FilterPattern: "{ $.eventName = \"ConsoleLogin\" }"
      LogGroupName: !Sub ${ExistingCloudTrailLogGroupName}
      FilterName: "ConsoleLogin"
      DestinationArn:
        Fn::GetAtt:
        - "ConsoleLoginNotificationLambda"
        - "Arn"
      Distribution: "ByLogStream"
  ConsoleLoginEventDataTable:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::DynamoDB::Table"
    DeletionPolicy: "Delete"
    Properties:
      OnDemandThroughput:
        MaxReadRequestUnits: 1
        MaxWriteRequestUnits: 1
      SSESpecification:
        SSEEnabled: false
      TableName: "ConsoleLoginEventDataTable"
      AttributeDefinitions:
      - AttributeType: "S"
        AttributeName: "eventID"
      ContributorInsightsSpecification:
        Enabled: false
      BillingMode: "PAY_PER_REQUEST"
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: false
      KeySchema:
      - KeyType: "HASH"
        AttributeName: "eventID"
      DeletionProtectionEnabled: false
      TableClass: "STANDARD"
      Tags: []
      TimeToLiveSpecification:
        Enabled: true
        AttributeName: "unixtime"
  ConsoleLoginNotificationLambdaRolePolicy:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::IAM::ManagedPolicy"
    DeletionPolicy: "Delete"
    Properties:
      ManagedPolicyName: "ConsoleLoginNotificationLambdaRolePolicy"
      Path: "/service-role/"
      Description: ""
      Groups: []
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
          Action: "logs:CreateLogGroup"
          Effect: "Allow"
        - Resource:
          - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/ConsoleLoginNotificationLambda:*"
          Action:
          - "logs:CreateLogStream"
          - "logs:PutLogEvents"
          Effect: "Allow"
        - Resource:
          - !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/ConsoleLoginEventDataTable"
          Action:
          - "dynamodb:PutItem"
          - "dynamodb:Scan"
          - "dynamodb:GetItem"
          Effect: "Allow"
          Sid: "AllowDynamoDB"
        - Resource:
          - !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:SendEmailTopic-${ResourceSuffixName}"
          Action:
          - "sns:Publish"
          Effect: "Allow"
          Sid: "AllowSNS"
  SendEmailTopic:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::SNS::Topic"
    DeletionPolicy: "Delete"
    Properties:
      FifoTopic: false
      TracingConfig: "PassThrough"
      TopicName: !Sub "SendEmailTopic-${ResourceSuffixName}"
  ConsoleLoginNotificationLambdaLogGroup:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::Logs::LogGroup"
    DeletionPolicy: "Delete"
    Properties:
      LogGroupClass: "STANDARD"
      RetentionInDays: 30
      LogGroupName: "/aws/lambda/ConsoleLoginNotificationLambda"
      DataProtectionPolicy: {}
  ConsoleLoginNotificationLambdaPermission:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::Lambda::Permission"
    DeletionPolicy: "Delete"
    Properties:
      FunctionName:
        Fn::GetAtt:
        - "ConsoleLoginNotificationLambda"
        - "Arn"
      Action: "lambda:InvokeFunction"
      SourceArn: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${ExistingCloudTrailLogGroupName}:*"
      Principal: "logs.amazonaws.com"
      SourceAccount: !Sub ${AWS::AccountId}
  ConsoleLoginNotificationLambdaRole:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::IAM::Role"
    DeletionPolicy: "Delete"
    Properties:
      Path: "/service-role/"
      ManagedPolicyArns:
      - !Ref ConsoleLoginNotificationLambdaRolePolicy
      MaxSessionDuration: 3600
      RoleName: "ConsoleLoginNotificationLambdaRole"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Action: "sts:AssumeRole"
          Effect: "Allow"
          Principal:
            Service: "lambda.amazonaws.com"
  ConsoleLoginNotificationLambda:
    UpdateReplacePolicy: "Retain"
    Type: "AWS::Lambda::Function"
    DeletionPolicy: "Delete"
    Properties:
      MemorySize: 128
      Description: ""
      TracingConfig:
        Mode: "PassThrough"
      Timeout: 10
      RuntimeManagementConfig:
        UpdateRuntimeOn: "Auto"
      Handler: "index.lambda_handler"
      Role:
        Fn::GetAtt:
        - "ConsoleLoginNotificationLambdaRole"
        - "Arn"
      FileSystemConfigs: []
      FunctionName: "ConsoleLoginNotificationLambda"
      LoggingConfig:
        LogGroup: !Ref ConsoleLoginNotificationLambdaLogGroup
      Runtime: "python3.11"
      PackageType: "Zip"
      Environment:
        Variables:
          TOPIC_ARN: !Sub "arn:aws:sns:${AWS::Region}:${AWS::AccountId}:SendEmailTopic-${ResourceSuffixName}"
      EphemeralStorage:
        Size: 512
      Architectures:
        - "x86_64"
      Code:
          ZipFile: |
            # coding: utf-8
            import json
            import base64
            import gzip
            import os
            import boto3
            import datetime
            from dateutil import tz

            def lambda_handler(event, context):
                
                print(event)
                
                decoded_data = Decode_event(event)
                logEvents = decoded_data['logEvents']
                for logEvent in logEvents:
                    
                    inputData = create_inputData(logEvent['message'])
                    
                    response = Store_Data(inputData)
                    
                    # proccess email
                    if response['statusCode'] == 200:
                        subject = 'AWSコンソールログイン'
                        message = json.dumps(inputData)
                        
                        try:
                            response = Publish_sns(subject, message)
                        except Exception as e:
                            print(e)
                            print(response)
                            return{
                                'statusCode': 500,
                                'body': 'Error Publishing event!'
                                
                            }

                return {
                        'statusCode': 200,
                        'body': 'Console Login Event Published successfully.'
                }
                

            def create_inputData(strevent):
                
                print(strevent)
                event = json.loads(strevent)
                
                utcTime = datetime.datetime.strptime(event['eventTime'], '%Y-%m-%dT%H:%M:%SZ')
                ttlTime = utcTime + datetime.timedelta(days=180)
                japanTime = utcTime.astimezone(tz.gettz('Asia/Tokyo'))
                
                inputData = {}
                inputData['eventID'] = event['eventID']
                inputData['accountId'] = event['userIdentity']['accountId']
                match event['userIdentity']['type']:
                    case 'IAMUser':
                        inputData['userName'] = event['userIdentity']['userName']
                    case 'Root':
                        inputData['userName'] = 'Root'
                inputData['eventTime'] = japanTime.strftime('%Y/%m/%d %H:%M:%S')
                inputData['eventSource'] = event['eventSource']
                inputData['eventName'] = event['eventName']
                inputData['awsRegion'] = event['awsRegion']
                inputData['sourceIPAddress'] = event['sourceIPAddress']
                inputData['userAgent'] = event['userAgent']
                inputData['unixtime'] = int(datetime.datetime.timestamp(ttlTime))
                
                return inputData
                
                
            def Store_Data(inputData):
                
                dynamodb = boto3.resource('dynamodb')
                ConsoleLoginEventDataTable = dynamodb.Table('ConsoleLoginEventDataTable')

                try:
                    response = ConsoleLoginEventDataTable.put_item(Item=inputData)
                    #
                    # response = ConsoleLoginEventDataTable.put_item(
                    #     Item=inputData,
                    #     conditionExpression=Attr('eventID').not_exists()
                    # )
                    return {
                        'statusCode': 200,
                        'body': 'Console Login Event stored successfully.'
                    }
                except Exception as e:
                    print(e)
                    print(inputData)
                    return{
                        'statusCode': 500,
                        'body': 'Error storing event!'
                        
                    }


            def Publish_sns(subject, message):
                
                sns = boto3.client('sns')
                topicArn = os.environ['TOPIC_ARN']
                
                response = sns.publish(
                    TopicArn=topicArn,
                    Subject=subject,
                    Message=message
                )
                
                return response
                

            def Decode_event(event):
                
                decode_data = base64.b64decode(event['awslogs']['data'])
                
                json_data = json.loads(gzip.decompress(decode_data))
                
                return json_data

まとめ

今回、CloudTrailのログイベントとLambda関数を使用して、AWSマネジメントコンソールへのログイン情報をメールで通知する仕組みを作成しました。CloudTrailはAWSの操作を記録する基本的なサービスですので、セキュリティのために設定することをおすすめします。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0