概要
AWSマネジメントコンソールにログインしたイベントを、メールで通知させる仕組みを構築します。知らないところで誰かに勝手にログインされても気がつけるようになります。CloudFormationテンプレートで、簡単に導入できるようにしました。
構成
今回構築するシステム構成です。
通知サンプル
以下のようなメールを受け取ることができます。
※ログ情報の一部をそのまま連携しただけなので、見た目が悪いです。本文の修正は、別の記事で紹介する予定です。
各コンポーネントの設定
それでは、各コンポーネントを説明してきます。
マネジメントコンソール
AWSをブラウザ経由で使用する時のログイン先です。時々、ログイン先のリージョンが変わります。そのためのすべてのリージョンのイベントを監視する必要があります。
CloudTrail
ログを記録するためのセキュリティ関連サービスです。ログ保存先としてS3バケットを指定します。また、すべてのリージョンのログを記録するように設定します。
マネジメントコンソールでログインした場合、イベント「ConsoleLogin」が記録されます。私の場合シドニー(ap-southeast-2)に記録されていました。
CloudTrailのログはS3バケット以外に、CloudWatch Logsの指定したロググループへ連携すること(オプション)ができます。
CloudWatch Logs
ロググループにサブスクリプションを設定し、イベント「ConsoleLogin」を受信した場合、通知と記録のためのLambda関数を起動します。
サブスリプションフィルターのパターンには、{ $.eventName = "ConsoleLogin" }と設定します。
Lambda関数
受け取った「ConsoleLogin」イベント情報をデータテーブルへ出力します。また、メール通知するための件名と本文を作成します。
他のリソースへのアクセスが必要ですので、Lambda実行ロールに権限を付与します。
ログインイベントデータテーブル
ログインイベントデータを整形し、DynamoDBテーブルに格納します。UserNameにはコンソールにログインしたIAMUser名(Rootユーザでログインした場合はRootと表示)やログイン日時、アクセス元のIPアドレス情報nが記録されます。
DynamoDBテーブルでは、データレコードに有効期限を設定できます。unixtime(TTL)が期限を示しており、eventTimeの90日後に設定しています。有効期限を超えた古いレコードはAWSが自動的に削除を行います。
SendEmailTopic
通知先のメールアドレスを登録します。
以下のようにメールアドレスを登録してください。
マネジメントコンソールより Amazon SNS > トピック > SendEmailTopic-xxxxxxxx へ移動し、「サブスクリプションの作成」
サブスクリプションの作成画面で、プロトコルに「Eメール」、エンドポイントに送信先のメールアドレスを入力します。
以下のようなメールが送信されてくるので、「Confirm subscription」を右クリックして、リンクのURLをコピー※
※そのままクリックすると、SNS通知メールに含まれるサブスクリプション解除リンクが有効となってしまうため、せっかく通知を受け取れる状態になっても誤ってメール配信を解除してしまう可能性があります。
SNSの画面に戻り、保留中の確認ステータスとなっているサブスクリプションを選択し、「サブスクリプションの確認」をクリック
サブスクリプションの確認に、先程コピーしたURLのリンクを貼り付けて、「サブスクリプションの確認」をクリック
以上で完成です。
CFnテンプレート
今回2つのテンプレートを作成しました。
CloudTrail設定テンプレート
本テンプレートでは、CloudTrailとログ保存先のS3バケット、ロググループ、それらに付随する関連リソースを作成します。CloudTrailは設定済みであり、CloudWatch Logsに連携するように設定しているのであれば、本テンプレートの展開は必要ありません。
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トピック、そららに付随する関連リソースを作成します。
テンプレート展開後は、SNSトピックへのメールアドレス登録は手動で行ってください。
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の操作を記録する基本的なサービスですので、セキュリティのために設定することをおすすめします。