この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、CX事業本部の若槻です。
前回のエントリでは、AWS WAFv2のWebACLの作成とCloudFrontへの適用をAWS CDKで行いました。
このAWS WAFv2にはログ出力の設定がありますが、そのためには別途FirehoseでDeliveryStreamを作成して紐付ける必要があります。
今回は、AWS WAF v2のFirehoseを使用したS3へのログ出力をAWS CDK(一部AWS CLI)で設定してみました。
やってみた
前回エントリで作成したリソースに修正を加える形で手順をご紹介します。
CDKモジュール追加
% npm i @aws-cdk/aws-kinesisfirehose @aws-cdk/aws-logs
CDKスタックへのリソース追加
ハイライト箇所が追記部分です。
waf-firehose-stack.ts
import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as s3 from "@aws-cdk/aws-s3";
import * as s3deploy from "@aws-cdk/aws-s3-deployment";
import * as iam from "@aws-cdk/aws-iam";
import * as wafv2 from "@aws-cdk/aws-wafv2";
import * as firehose from "@aws-cdk/aws-kinesisfirehose";
import * as logs from "@aws-cdk/aws-logs";
export class AwsCdkDeployReactStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const websiteBucket = new s3.Bucket(this, "WebsiteBucket", {
websiteErrorDocument: "index.html",
websiteIndexDocument: "index.html",
});
const websiteIdentity = new cloudfront.OriginAccessIdentity(
this,
"WebsiteIdentity"
);
const webSiteBucketPolicyStatement = new iam.PolicyStatement({
actions: ["s3:GetObject"],
effect: iam.Effect.ALLOW,
principals: [websiteIdentity.grantPrincipal],
resources: [`${websiteBucket.bucketArn}/*`],
});
websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement);
//AWSマネージドルールを適用するWebACLの作成
const websiteWafV2WebAcl = new wafv2.CfnWebACL(this, "WafV2WebAcl", {
defaultAction: { allow: {} },
scope: "CLOUDFRONT",
visibilityConfig: {
cloudWatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "websiteWafV2WebAcl",
},
rules: [
{
name: "AWSManagedRulesCommonRuleSet",
priority: 1,
statement: {
managedRuleGroupStatement: {
vendorName: "AWS",
name: "AWSManagedRulesCommonRuleSet",
},
},
overrideAction: { none: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "AWSManagedRulesCommonRuleSet",
},
},
{
name: "AWSManagedRulesAdminProtectionRuleSet",
priority: 2,
statement: {
managedRuleGroupStatement: {
vendorName: "AWS",
name: "AWSManagedRulesAdminProtectionRuleSet",
},
},
overrideAction: { none: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "AWSManagedRulesAdminProtectionRuleSet",
},
},
{
name: "AWSManagedRulesKnownBadInputsRuleSet",
priority: 3,
statement: {
managedRuleGroupStatement: {
vendorName: "AWS",
name: "AWSManagedRulesKnownBadInputsRuleSet",
},
},
overrideAction: { none: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "AWSManagedRulesKnownBadInputsRuleSet",
},
},
{
name: "AWSManagedRulesAmazonIpReputationList",
priority: 4,
statement: {
managedRuleGroupStatement: {
vendorName: "AWS",
name: "AWSManagedRulesAmazonIpReputationList",
},
},
overrideAction: { none: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "AWSManagedRulesAmazonIpReputationList",
},
},
{
name: "AWSManagedRulesAnonymousIpList",
priority: 5,
statement: {
managedRuleGroupStatement: {
vendorName: "AWS",
name: "AWSManagedRulesAnonymousIpList",
},
},
overrideAction: { none: {} },
visibilityConfig: {
cloudWatchMetricsEnabled: true,
sampledRequestsEnabled: true,
metricName: "AWSManagedRulesAnonymousIpList",
},
},
],
});
const websiteDistribution = new cloudfront.CloudFrontWebDistribution(
this,
"WebsiteDistribution",
{
errorConfigurations: [
{
errorCachingMinTtl: 300,
errorCode: 403,
responseCode: 200,
responsePagePath: "/index.html",
},
{
errorCachingMinTtl: 300,
errorCode: 404,
responseCode: 200,
responsePagePath: "/index.html",
},
],
originConfigs: [
{
s3OriginSource: {
s3BucketSource: websiteBucket,
originAccessIdentity: websiteIdentity,
},
behaviors: [
{
isDefaultBehavior: true,
},
],
},
],
priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
webACLId: websiteWafV2WebAcl.attrArn, //作成したWebACLをCloudFrontに適用する
}
);
new s3deploy.BucketDeployment(this, "WebsiteDeploy", {
sources: [s3deploy.Source.asset("./web/build")],
destinationBucket: websiteBucket,
distribution: websiteDistribution,
distributionPaths: ["/*"],
});
const region = cdk.Stack.of(this).region;
const accountId = cdk.Stack.of(this).account;
const deliveryStreamName = "aws-waf-logs-demo";
const logStreamName = "S3Delivery";
//データ配信先S3バケット
const wafLogBucket = new s3.Bucket(this, "wafLogBucket", {
bucketName: `${deliveryStreamName}-${region}-${accountId}`,
});
//データ配信失敗時のイベントを記録するLogGroupとLogStream
const wafLogDeliveryStreamLogGroup = new logs.CfnLogGroup(
this,
"wafLogDeliveryStreamLogGroup",
{
logGroupName: `/aws/kinesisfirehose/${deliveryStreamName}`,
}
);
const wafLogDeliveryStreamLogStream = new logs.CfnLogStream(
this,
"wafLogDeliveryStreamLogStream",
{
logGroupName: wafLogDeliveryStreamLogGroup.logGroupName as string,
logStreamName: logStreamName,
}
);
//明示的に依存性を設定しなければLogStreamの作成が失敗する場合がある
wafLogDeliveryStreamLogStream.addDependsOn(wafLogDeliveryStreamLogGroup);
//配信ストリームに付与するロール
const wafLogDeliveryStreamRole = new iam.Role(
this,
"wafLogDeliveryStreamRole",
{
assumedBy: new iam.ServicePrincipal("firehose.amazonaws.com"),
}
);
wafLogDeliveryStreamRole.addToPolicy(
new iam.PolicyStatement({
actions: [
"kinesis:DescribeStream",
"kinesis:GetShardIterator",
"kinesis:GetRecords",
],
effect: iam.Effect.ALLOW,
resources: [`arn:aws:kinesis:${region}:${accountId}:stream/*`],
})
);
wafLogDeliveryStreamRole.addToPolicy(
new iam.PolicyStatement({
actions: [
"s3:AbortMultipartUpload",
"s3:GetBucketLocation",
"s3:GetObject",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:PutObject",
],
effect: iam.Effect.ALLOW,
resources: [wafLogBucket.bucketArn, `${wafLogBucket.bucketArn}/*`],
})
);
wafLogDeliveryStreamRole.addToPolicy(
new iam.PolicyStatement({
actions: ["logs:PutLogEvents"],
effect: iam.Effect.ALLOW,
resources: [
`arn:aws:logs:${region}:${accountId}:log-group:/aws/kinesisfirehose/*`,
],
})
);
//配信ストリーム
new firehose.CfnDeliveryStream(this, "wafLogDeliveryStream", {
deliveryStreamName: deliveryStreamName,
deliveryStreamType: "DirectPut",
s3DestinationConfiguration: {
bucketArn: wafLogBucket.bucketArn,
roleArn: wafLogDeliveryStreamRole.roleArn,
cloudWatchLoggingOptions: {
enabled: true,
logGroupName: wafLogDeliveryStreamLogGroup.logGroupName,
logStreamName: logStreamName,
},
compressionFormat: "GZIP",
prefix: "logs/",
errorOutputPrefix: "errors/",
},
});
}
}
このCDKスタック中ではWebACLとDeliveryStreamの紐付けは行いません。@aws-cdk/aws-wafv2
のドキュメントを見る限り、紐付けるためのプロパティがなく出来ないためです。
よって紐付けの設定のみAWS CLIで手動で行います。
CDKデプロイ
% cdk deploy
WebACLとDeliveryStreamを紐付ける
まずWebACLとDeliveryStreamのArnを確認します。(CDKスタック内でconsole.log
でも確認可能です)
REGION=us-east-1
ACCOUNT_ID=XXXXXXXXXXXX
% aws wafv2 list-web-acls \
--scope CLOUDFRONT \
--region ${REGION}
% aws firehose describe-delivery-stream \
--delivery-stream-name aws-waf-logs-demo \
--region ${REGION} \
--query DeliveryStreamDescription.DeliveryStreamARN
下記のコマンドで両者のArnを指定して紐付けます。
% aws wafv2 put-logging-configuration \
--logging-configuration \
ResourceArn=${WEB_ACL_ARN},LogDestinationConfigs=${FIREHOSE_DELIVERY_STREAM_ARN} \
--region ${REGION}
動作確認
WebACLが適用されているCloudFrontがホストしているWebサイトにアクセスします。
10分ほどしたらS3バケットのlogs
プレフィクス配下にログが作成されました。
% aws s3 ls s3://aws-waf-logs-demo-${REGION}-${ACCOUNT_ID} --recursive
2021-06-10 19:04:27 1559 logs/2021/06/10/09/aws-waf-logs-demo-1-2021-06-10-09-58-08-9864cb19-1d2e-4a57-9e30-0ddfdca6f5e3.gz
S3 SELECTを使用して下記のような指定でログの内容を確認できました。
- 入力設定
- 形式:JSON
- JSONコンテンツタイプ:行
- 圧縮:GZIP
- 出力設定
- 形式JSON
WebACLのログのフィールド仕様は下記ドキュメントから確認可能です。
Log Examples
{
"timestamp": 1576280412771,
"formatVersion": 1,
"webaclId": "arn:aws:wafv2:ap-southeast-2:EXAMPLE12345:regional/webacl/STMTest/1EXAMPLE-2ARN-3ARN-4ARN-123456EXAMPLE",
"terminatingRuleId": "STMTest_SQLi_XSS",
"terminatingRuleType": "REGULAR",
"action": "BLOCK",
"terminatingRuleMatchDetails": [
{
"conditionType": "SQL_INJECTION",
"location": "HEADER",
"matchedData": [
"10",
"AND",
"1"
]
}
],
"httpSourceName": "-",
"httpSourceId": "-",
"ruleGroupList": [],
"rateBasedRuleList": [],
"nonTerminatingMatchingRules": [],
"httpRequest": {
"clientIp": "1.1.1.1",
"country": "AU",
"headers": [
{
"name": "Host",
"value": "localhost:1989"
},
{
"name": "User-Agent",
"value": "curl/7.61.1"
},
{
"name": "Accept",
"value": "*/*"
},
{
"name": "x-stm-test",
"value": "10 AND 1=1"
}
],
"uri": "/foo",
"args": "",
"httpVersion": "HTTP/1.1",
"httpMethod": "GET",
"requestId": "rid"
},
"labels": [
{
"name": "value"
}
]
}
おわりに
AWS WAF v2のFirehoseを使用したS3へのログ出力をAWS CDK(一部AWS CLI)で設定してみました。
WebACLのログからアクセスのAllow/Block/Countの状況を確認して、ルールの最適化を行いたい場合にこの設定がされていると便利ですね。
参考
- Amazon S3 にログを保存するように AWS WAF の包括的なログ記録を設定する
- AWS CDKでAPI GatewayからKinesis Data Streamsにデータを流す設定を作成してみた | DevelopersIO
- AWS WAFのLogを保存するKinesis FirehoseをCloudFormationでさくっと作ってみた | DevelopersIO
以上