Skip to content

Instantly share code, notes, and snippets.

@zoonderkins
Last active January 5, 2022 22:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zoonderkins/6e3beddb3933b8255104e60151c3e460 to your computer and use it in GitHub Desktop.
Save zoonderkins/6e3beddb3933b8255104e60151c3e460 to your computer and use it in GitHub Desktop.
AWS CDK enable WafV2 logging with Firehose via Custom Resource
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 lambda = require('@aws-cdk/aws-lambda');
import * as wafv2 from '@aws-cdk/aws-wafv2';
import * as ssm from '@aws-cdk/aws-ssm';
import * as cr from '@aws-cdk/custom-resources';
import { EmptyBucketOnDelete } from './empty-bucket';
export interface FirehoseProps {
bucket: s3.Bucket;
// inputStream: kds.Stream;
emptyBucket: EmptyBucketOnDelete;
}
export class FirehoseInfrastructure extends cdk.Construct {
public fireHoseArn: 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);
// props.lambda.grantInvoke(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.fireHoseArn = 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 emptyBucket = new EmptyBucketOnDelete(this, 'EmptyBucket', {
bucket: bucket,
});
const firehose = new FirehoseInfrastructure(this, 'FirehoseInfrastructure', {
bucket: bucket,
emptyBucket: emptyBucket,
});
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(firehose);
// const lambdaSource = fs.readFileSync('lambda/waf-reader-stream.py').toString();
// const wafReaderStreamFn = new lambda.Function(this, `wafReaderStreamFn`, {
// runtime: lambda.Runtime.PYTHON_3_7,
// code: lambda.Code.fromAsset('./lambda/gate-waf-logging/'),
// handler: 'waf_reader_stream.lambda_handler',
// });
/*
TODO: Minimum Lambda role
iam:CreateServiceLinkedRole
firehose:ListDeliveryStreams
wafv2:PutLoggingConfiguration
*/
// if (wafReaderStreamFn.role) {
// wafReaderStreamFn.role.addToPrincipalPolicy(
// new iam.PolicyStatement({
// effect: iam.Effect.ALLOW,
// actions: ['*'],
// resources: ['*'],
// }),
// );
// }
/* AWS Waf Logging
1. https://docs.aws.amazon.com/waf/latest/developerguide/logging.html
2. https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/waf.html#WAF.Client.put_logging_configuration
*/
// Provider that invokes the lambda function
// const wafReaderStreamProvider = new cr.Provider(this, 'wafReaderStreamCustomResourceProvider', {
// onEventHandler: wafReaderStreamFn,
// });
const WafProviderToken = new cdk.CfnParameter(this, 'WafProviderToken', {
type: 'String',
description: 'The name of the WafProviderToken',
});
// const wafReaderStreamProvder = cdk.Fn.importValue('WafReaderStreamProvider');
const wafCfnResource = new cdk.CustomResource(this, 'wafReaderStreamResource', {
serviceToken: WafProviderToken.valueAsString,
properties: {
WafArn: waf.attrArn,
FireHose: firehose.fireHoseArn,
},
});
wafCfnResource.node.addDependency(waf);
wafCfnResource.node.addDependency(firehose);
// const ssmAwsSdkCall: cr.AwsSdkCall = {
// service: 'WAFV2',
// action: 'putLoggingConfiguration',
// parameters: {
// LoggingConfiguration: {
// LogDestinationConfigs: [firehose.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,
// }),
// });
const wafSSMArn = new ssm.StringParameter(this, 'WafArnSSMExport', {
parameterName: '/gate/waf-test-arn',
stringValue: waf.attrArn,
description: 'waf arn',
type: ssm.ParameterType.STRING,
tier: ssm.ParameterTier.STANDARD,
allowedPattern: '.*',
});
const wafSSMAcl = new ssm.StringParameter(this, 'WafAclSSMExport', {
parameterName: '/gate/waf-test-acl',
stringValue: waf.attrId,
description: 'waf acl',
type: ssm.ParameterType.STRING,
tier: ssm.ParameterTier.STANDARD,
allowedPattern: '.*',
});
const fireHoseSSM = new ssm.StringParameter(this, 'FireHoseSSMExport', {
parameterName: '/gate/firehorse-arn',
stringValue: firehose.fireHoseArn,
description: 'waf arn',
type: ssm.ParameterType.STRING,
tier: ssm.ParameterTier.STANDARD,
allowedPattern: '.*',
});
wafSSMArn.node.addDependency(waf);
wafSSMAcl.node.addDependency(waf);
fireHoseSSM.node.addDependency(firehose);
wafCfnResource.node.addDependency(wafSSMArn);
wafCfnResource.node.addDependency(fireHoseSSM);
new cdk.CfnOutput(this, 'S3Bucket', { value: bucket.bucketName });
}
}
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';
export interface FirehoseProps {
bucket: s3.IBucket;
}
export class FirehoseInfrastructure extends cdk.Construct {
public fireHoseArn: 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,
},
});
this.fireHoseArn = firehose.attrArn;
}
}
export class WafLogging extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const bucket = s3.Bucket.fromBucketName(
this,
'WafBucket',
'aws-waf-logs-gate-api-v1-dev-ap-northeast-1',
);
const firehose = new FirehoseInfrastructure(this, 'FirehoseInfrastructure', {
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(firehose);
// const lambdaSource = fs.readFileSync('lambda/waf-reader-stream.py').toString();
// const wafReaderStreamFn = new lambda.Function(this, `wafReaderStreamFn`, {
// runtime: lambda.Runtime.PYTHON_3_7,
// code: lambda.Code.fromAsset('./lambda/gate-waf-logging/'),
// handler: 'waf_reader_stream.lambda_handler',
// });
/*
TODO: Minimum Lambda role
iam:CreateServiceLinkedRole
firehose:ListDeliveryStreams
wafv2:PutLoggingConfiguration
*/
// if (wafReaderStreamFn.role) {
// wafReaderStreamFn.role.addToPrincipalPolicy(
// new iam.PolicyStatement({
// effect: iam.Effect.ALLOW,
// actions: ['*'],
// resources: ['*'],
// }),
// );
// }
/* AWS Waf Logging
1. https://docs.aws.amazon.com/waf/latest/developerguide/logging.html
2. https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/waf.html#WAF.Client.put_logging_configuration
*/
// Provider that invokes the lambda function
// const wafReaderStreamProvider = new cr.Provider(this, 'wafReaderStreamCustomResourceProvider', {
// onEventHandler: wafReaderStreamFn,
// });
const WafProviderToken = new cdk.CfnParameter(this, 'WafProviderToken', {
type: 'String',
description: 'The name of the WafProviderToken',
});
// const wafReaderStreamProvder = cdk.Fn.importValue('WafReaderStreamProvider');
const wafCfnResource = new cdk.CustomResource(this, 'wafReaderStreamResource', {
serviceToken: WafProviderToken.valueAsString,
properties: {
WafArn: waf.attrArn,
FireHose: firehose.fireHoseArn,
},
});
wafCfnResource.node.addDependency(waf);
wafCfnResource.node.addDependency(firehose);
// const ssmAwsSdkCall: cr.AwsSdkCall = {
// service: 'WAFV2',
// action: 'putLoggingConfiguration',
// parameters: {
// LoggingConfiguration: {
// LogDestinationConfigs: [firehose.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,
// }),
// });
const wafSSMArn = new ssm.StringParameter(this, 'WafArnSSMExport', {
parameterName: '/gate/waf-test-arn',
stringValue: waf.attrArn,
description: 'waf arn',
type: ssm.ParameterType.STRING,
tier: ssm.ParameterTier.STANDARD,
allowedPattern: '.*',
});
const wafSSMAcl = new ssm.StringParameter(this, 'WafAclSSMExport', {
parameterName: '/gate/waf-test-acl',
stringValue: waf.attrId,
description: 'waf acl',
type: ssm.ParameterType.STRING,
tier: ssm.ParameterTier.STANDARD,
allowedPattern: '.*',
});
const fireHoseSSM = new ssm.StringParameter(this, 'FireHoseSSMExport', {
parameterName: '/gate/firehorse-arn',
stringValue: firehose.fireHoseArn,
description: 'waf arn',
type: ssm.ParameterType.STRING,
tier: ssm.ParameterTier.STANDARD,
allowedPattern: '.*',
});
wafSSMArn.node.addDependency(waf);
wafSSMAcl.node.addDependency(waf);
fireHoseSSM.node.addDependency(firehose);
wafCfnResource.node.addDependency(wafSSMArn);
wafCfnResource.node.addDependency(fireHoseSSM);
new cdk.CfnOutput(this, 'S3Bucket', { value: bucket.bucketName });
}
}
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);
}
}
@benm5678
Copy link

benm5678 commented Jan 5, 2022

We get an error as the AwsCustomResource deploys "Response is not valid JSON" -- if I change the service/action params to some bogus value the error changes... so pretty sure we are talking to it & getting some bad response. Have you seen that before or know of some way to find logs for it? My only other thought is to try the custom lambda approach so can do more logging. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment