概要
CloudTrailを用いて特定のAPIコールを監視する方法として下記の2パターンがあります。
今回は「1. EventBridgeのイベントソースとしてAPIを指定」のような「CloudTrailを用いて管理されたAPIコールをEventBridgeでフィルタリングし、特定のAPIが呼び出された時にSNS通知を行う」という構成をCDKで検証してまとめてみました。基本的にはBLEAテンプレートをベースにしております。
構成図
以下に示したようなAPIコールを監視する設計としています。
- IAM Policyの作成・削除、アタッチ・デタッチ
- Rootユーザのコンソールサインイン
- Access Keyの新規作成
- SGルールの作成・削除
- NACLの作成・削除、NACLルールの作成・削除、NACLのサブネットへの関連付け
- CloudTrailのロギング停止、証跡削除、証跡の設定変更
IAMリソースの操作ログはus-east-1に記録されるため、IAMの操作ログをSNSと連携するためのEventBridgeをus-east-1に、それ以外の操作ログをSNSと連携するためのEventBridgeをap-northeast-1に構築する必要があることに注意が必要です。
コンソール画面
ap-northeast-1
us-east-1
ディレクトリ構造
本記事ではbin, lib配下のファイルを紹介します。
├── bin
│ └── aws_cloud_trail_sample.ts
├── lib
│ └── Stack
│ ├── tokyo-stack.ts
│ └── us-stack.ts
│ └── Construct
│ ├── cloudtrail.ts
│ ├── eventbridge.ts
│ └── sns.ts
├── node_modules
├── test
├── cdk.json
├── jest.config.js
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
CDK実装コード
bin
aws_cloud_trail_sample.ts
Stackのenv.region
プロパティを指定することにより、Stack毎にDeploy先のリージョンを指定する必要があります。指定しないとAWSの認証情報で設定したリージョンにデプロイされてしまいます。(aws configureで確認可能)
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { AwsTokyoStack } from '../lib/Stack/tokyo-stack';
import { AwsUsStack } from '../lib/Stack/us-stack';
const app = new cdk.App();
new AwsTokyoStack(app, 'AwsTokyoStack', {
env: {region: "ap-northeast-1"},
});
new AwsUsStack(app, 'AwsUsStack', {
env: {region: "us-east-1"},
});
lib/Stack
- tokyo-stack.ts : ap-northeast-1に構築するCloudTrail証跡、EventBridgeルール、SNSトピックを定義
- us-stack.ts : us-east-1に構築するEventBridgeルール、SNSトピックを定義
今回の構成では証跡のホームリージョンはap-northeast-1に設定し、マルチリージョン証跡を有効化しています。そのため、全てのリージョンの証跡ログを格納するS3バケットはap-northeast-1に配置されます。
tokyo-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { SnsConstruct } from '../Construct/sns';
import { TrailConstruct } from '../Construct/cloudtrail';
import { EventsConstruct } from '../Construct/eventbridge';
export class AwsTokyoStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// ----- SNS Topic -----
const Topic_for_Alarm = new SnsConstruct(this, "Topic1", {
notifyEmail: "xxxxxxx@gmail.com"
}).createTopic();
// ----- CloudTrail Trail -----
new TrailConstruct(this, "Test", {})
// -----EventBridge Rules -----
new EventsConstruct(this, "Rules", {
region: "ap-northeast-1",
topic: Topic_for_Alarm,
})
}
}
us-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { SnsConstruct } from '../Construct/sns';
import { EventsConstruct } from '../Construct/eventbridge';
export class AwsUsStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// ----- SNS Topic -----
const Topic_for_Alarm = new SnsConstruct(this, "Topic1", {
notifyEmail: "xxxxxxx@gmail.com"
}).createTopic();
// -----EventBridge Rules -----
new EventsConstruct(this, "Rules", {
region: "us-east-1",
topic: Topic_for_Alarm,
})
}
}
lib/Construct
cloudtrail.ts
証跡ログを格納するS3バケットとCloudTrail証跡を定義しています。今回はCloudWatch Logsに配信はしていません。(Logsは高額になりかねませんからね...)
import { Construct } from "constructs";
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as trail from 'aws-cdk-lib/aws-cloudtrail';
export interface TrailProps{}
export class TrailConstruct extends Construct{
constructor(scope: Construct, id: string, props: TrailProps) {
super(scope, id);
// https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_s3.Bucket.html
const TrailBucket = new s3.Bucket(this, "TrailBucket", {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
});
// https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudtrail.Trail.html#sendtocloudwatchlogs
new trail.Trail(this, "TestTrail", {
bucket: TrailBucket,
enableFileValidation: true,
isMultiRegionTrail: true,
sendToCloudWatchLogs: false,
});
}
}
eventbridge.ts
env.region
プロパティの値がap-northeast-1なのかus-east-1なのかに応じて呼び出す関数を変えることで、リージョンごとに異なるEventBridgeルールを一つのConstructでデプロイすることを実現しています。
import { Construct } from "constructs";
import * as events from 'aws-cdk-lib/aws-events'
import * as sns from "aws-cdk-lib/aws-sns"
import * as eventsTarget from 'aws-cdk-lib/aws-events-targets';
export interface EventsProps {
region: string,
topic: sns.ITopic,
}
export class EventsConstruct extends Construct {
constructor(scope: Construct, id: string, props: EventsProps) {
super(scope, id);
if (props.region === "ap-northeast-1") {
this.createRulesInTokyo(props);
} else if (props.region === "us-east-1") {
this.createRuleInUs(props);
}
}
// https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_events-readme.html
// https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_events_targets-readme.html
private createRulesInTokyo(props: EventsProps): void{
new events.Rule(this, "SgChangeEventRule", {
description: 'Notify to create, update or delete a Security Group.',
enabled: true,
eventPattern: {
source: ['aws.ec2'],
detailType: ['AWS API Call via CloudTrail'],
detail: {
eventSource: ['ec2.amazonaws.com'],
eventName: [
'AuthorizeSecurityGroupIngress',
'AuthorizeSecurityGroupEgress',
'RevokeSecurityGroupIngress',
'RevokeSecurityGroupEgress',
],
},
},
targets: [new eventsTarget.SnsTopic(props.topic)],
});
new events.Rule(this, "NACLChangeEventRule", {
description: 'Notify to create, update or delete a Network ACL.',
enabled: true,
eventPattern: {
source: ['aws.ec2'],
detailType: ['AWS API Call via CloudTrail'],
detail: {
eventSource: ['ec2.amazonaws.com'],
eventName: [
'CreateNetworkAcl',
'CreateNetworkAclEntry',
'DeleteNetworkAcl',
'DeleteNetworkAclEntry',
'ReplaceNetworkAclEntry',
'ReplaceNetworkAclAssociation',
],
},
},
targets: [new eventsTarget.SnsTopic(props.topic)],
});
new events.Rule(this, "CloudTrailChangeEventRule", {
description: 'Notify to change on CloudTrail log configuration',
enabled: true,
eventPattern: {
detailType: ['AWS API Call via CloudTrail'],
detail: {
eventSource: ['cloudtrail.amazonaws.com'],
eventName: ['StopLogging', 'DeleteTrail', 'UpdateTrail'],
},
},
targets: [new eventsTarget.SnsTopic(props.topic)],
});
}
private createRuleInUs(props: EventsProps): void{
new events.Rule(this, "IAMPolicyChangeEventRule", {
description: 'Notify to change IAM Configuration.',
enabled: true,
eventPattern: {
source: ['aws.iam'],
detailType: ['AWS API Call via CloudTrail'],
detail: {
eventSource: ['iam.amazonaws.com'],
eventName: [
'DeleteGroupPolicy',
'DeleteRolePolicy',
'DeleteUserPolicy',
'PutGroupPolicy',
'PutRolePolicy',
'PutUserPolicy',
'CreatePolicy',
'DeletePolicy',
'CreatePolicyVersion',
'DeletePolicyVersion',
'AttachRolePolicy',
'DetachRolePolicy',
'AttachUserPolicy',
'DetachUserPolicy',
'AttachGroupPolicy',
'DetachGroupPolicy'
]
}
},
targets: [new eventsTarget.SnsTopic(props.topic)],
});
new events.Rule(this, "RootSigninRule", {
description: "Events rule for monitoring root AWS Console Sign In activity",
enabled: true,
eventPattern: {
source: ["aws.signin"],
detailType: ["AWS Console Sign In via CloudTrail"],
detail: {
userIdentity: {
type: ["Root"],
},
responseElements: {
ConsoleLogin: ["Success"],
}
}
},
targets: [new eventsTarget.SnsTopic(props.topic)],
});
new events.Rule(this, "NewAccessKeyCreatedRule", {
description: "Events rule for monitoring access key creation",
enabled: true,
eventPattern: {
source: ["aws.iam"],
detailType: ["AWS Console Sign In via CloudTrail"],
detail: {
eventSource: ["iam.amazonaws.com"],
eventName: ["CreateAccessKey"]
}
},
targets: [new eventsTarget.SnsTopic(props.topic)],
});
}
}
sns.ts
import { Construct } from "constructs";
import * as sns from "aws-cdk-lib/aws-sns"
export interface SnsProps {
notifyEmail: string;
}
export class SnsConstruct extends Construct {
private readonly topic: sns.ITopic
constructor(scope: Construct, id: string, props: SnsProps) {
super(scope, id);
// https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sns-readme.html
const topic = new sns.Topic(this, "Topic for Alarm",{});
new sns.Subscription(this, "MonitorAlarmEmail", {
endpoint: props.notifyEmail,
protocol: sns.SubscriptionProtocol.EMAIL,
topic: topic,
});
this.topic = topic;
}
// not SnsConstruct but sns.ITopic
public createTopic(): sns.ITopic {
return this.topic;
}
}
最後に
この構成にすると複数リージョンにEventBridgeやSNSをデプロイする必要があるので、少し管理も面倒ですね。CloudTrail→CloudWatch Logs→メトリクスフィルタ→Alarm→SNSの構成の方が柔軟かつシンプルに管理できそうです。