AWS CDK Wafv2 enable logging the Custom Resource way

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