Expectation
We need to collect all WAF logs to S3 bucket. But Cloud Formation or AWS CDK cannot easily configure it. Due to Cloud Formation still not support Logging + Firehose binding.
How
I will prepare a demo to show how I use CDK Custom Resource to trick Cloud Formation.
We need to have a S3 bucket, WAFv2 ACL, Firehose stream and A Custom Resource
Sample code:
https://gist.github.com/6e3beddb3933b8255104e60151c3e460#file-waf-logging-ts
import cdk = require('@aws-cdk/core');
import s3 = require('@aws-cdk/aws-s3');
import iam = require('@aws-cdk/aws-iam');
import kdf = require('@aws-cdk/aws-kinesisfirehose');
import * as wafv2 from '@aws-cdk/aws-wafv2';
import * as ssm from '@aws-cdk/aws-ssm';
import * as cr from '@aws-cdk/custom-resources';
export interface FirehoseProps {
bucket: s3.Bucket;
}
export class FirehoseInfrastructure extends cdk.Construct {
public fireHorseArn: string;
constructor(scope: cdk.Construct, id: string, props: FirehoseProps) {
super(scope, id);
const firehoseRole = new iam.Role(this, 'FirehoseRole', {
roleName: 'KinesisFirehoseServiceRole-aws-waf-gate-api-jp',
assumedBy: new iam.ServicePrincipal('firehose.amazonaws.com'),
});
props.bucket.grantReadWrite(firehoseRole);
const firehose = new kdf.CfnDeliveryStream(this, 'FirehoseDeliveryStream', {
deliveryStreamName: 'aws-waf-logs-gate-api-v1',
deliveryStreamType: 'DirectPut',
s3DestinationConfiguration: {
bucketArn: props.bucket.bucketArn,
bufferingHints: {
intervalInSeconds: 900,
sizeInMBs: 5,
},
roleArn: firehoseRole.roleArn,
},
});
firehose.node.addDependency(props.emptyBucket);
this.fireHorseArn = firehose.attrArn;
}
}
export class WafLogging extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const bucket = new s3.Bucket(this, 'GateApiWafStreamBucket', {
bucketName: `aws-waf-logs-gate-api-v1-dev-ap-northeast-1`,
versioned: false,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const firehorse = new FirehoseInfrastructure(this, 'FirehoseInfrastructure', {
bucket: bucket,
});
const waf = new wafv2.CfnWebACL(this, 'GateWafV2', {
description: 'ACL for Gate',
scope: 'REGIONAL',
defaultAction: { allow: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'gate-firewall',
},
rules: [
{
name: 'GeoMatch',
priority: 0,
action: {
count: {}, // Change to block to make active
},
statement: {
notStatement: {
statement: {
geoMatchStatement: {
countryCodes: ['TW'],
},
},
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'GeoMatch',
},
},
{
name: 'AWS-AWSManagedRulesCommonRuleSet',
priority: 1,
statement: {
managedRuleGroupStatement: {
vendorName: 'AWS',
name: 'AWSManagedRulesCommonRuleSet',
excludedRules: [
{
name: 'NoUserAgent_HEADER',
},
],
},
},
overrideAction: { none: {} },
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'AWS-AWSManagedRulesCommonRuleSet',
},
},
{
name: 'LimitRequests100',
priority: 2,
action: {
block: {},
},
statement: {
rateBasedStatement: {
limit: 100,
aggregateKeyType: 'IP',
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: 'LimitRequests100',
},
},
],
});
waf.node.addDependency(firehorse);
// Custom resource
// CDK will generate a Lambda while deploying CDK stack
const ssmAwsSdkCall: cr.AwsSdkCall = {
service: 'WAFV2',
action: 'putLoggingConfiguration',
parameters: {
LoggingConfiguration: {
LogDestinationConfigs: [firehrose.fireHorseArn],
ResourceArn: waf.attrArn,
},
},
physicalResourceId: cr.PhysicalResourceId.of('id'),
};
const wafCustomResource = new cr.AwsCustomResource(this, 'WafCustomResource', {
onUpdate: ssmAwsSdkCall,
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});
// We add Custom resource depend on firehose and Waf to make sure both run before custom resource.
wafCustomResource.node.addDependency(waf);
wafCustomResource.node.addDependency(firehorse);
}
}
Photo by Peter Okwara on Unsplash