Security is absolutely paramount to modern applications - especially cloud-based solutions. In fact, it's so critical that specifically within the AWS ecosystem, security is one of the 6 Pillars of the Well-Architected Framework meaning that it should be given an equal amount of thought & effort as everything else that is required for your deployed application.
With that in mind, AWS WAF (Web Application Firewall) is AWS' product that can secure your web applications from common exploits, bots and attacks. It can help mitigate things like DDoS attempts, brute-force attacks and just simply filter your traffic to target your specific audience if you don't necessarily need to have a wide-open public application.
So let's take a look at how we can provision a WAF and attach it to something like an API Gateway - all using AWS CDK.
✍️ Define some CDK
Now unfortunately, AWS CDK does not have any L2 constructs that nicely wrap the AWS WAF functionality, so we'll just create our own reusable components/constructs that wrap the CloudFormation L1 constructs themselves.
Provision the WAF itself
First of all, we want to actually provision the WAF instance itself. Now since I'm going to be attaching this to a REST API Gateway instance (which is a regional instance), this is going to be a regional WAF i.e. region-specific.
Take a look at the following CDK:
import * as waf from "aws-cdk-lib/aws-wafv2";
const defaultActionProperty: waf.CfnWebACL.DefaultActionProperty = {
block: {
customResponse: {
responseCode: 403,
},
},
};
const customWaf = new waf.CfnWebACL(this, "api-gateway-waf", {
defaultAction: defaultActionProperty,
scope: "REGIONAL",
name: "api-gateway-waf",
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "api-gateway-waf",
sampledRequestsEnabled: true,
},
});
There are a couple of interesting things here:
- We're using the
v2
implementation of the WAF service which is the latest & greatest. - Whenever you're provisioning the WAF, you need to specify a
defaultAction
- this essentially is what action you want to take if the request meets none of the rules. By default, I want to block the request and return a 403 response. -
scope
is what I briefly mentioned above. It's setting the WAF to eitherREGIONAL
orCLOUDFRONT
- pretty self-explanatory, but critical if you're going to attach the WAF to something like an API Gateway or a CloudFront distribution. - Finally, the
visibilityConfig
is enabling the metrics to be captured and pushed into CloudWatch so that you can analyse and report on the traffic
Create rules for the WAF
Next we want to create and define some rules that we wish to be enforced by the WAF. These are going to define the specifics of which actions you wish to allow/challenge/block etc. There are also managed rules by AWS and customers that are available to choose from - driven by things like OWASP top-10. A note though; depending on which option(s) you choose, it can have different pricing impacts, so always worth checking out the user guide and pricing for more information.
For us, we're just going to define a very basic one to showcase the functionality:
const wafRules: waf.CfnWebACL.RuleProperty[] = [
{
name: "GeoLocationRule",
priority: 0,
action: {
allow: {},
},
statement: {
geoMatchStatement: {
countryCodes: ["US"],
},
},
visibilityConfig: {
sampledRequestsEnabled: true,
cloudWatchMetricsEnabled: true,
metricName: "GeoLocationRule",
},
},
];
const customWaf = new waf.CfnWebACL(this, "api-gateway-waf", {
defaultAction: defaultActionProperty,
scope: "REGIONAL",
name: "api-gateway-waf",
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "api-gateway-waf",
sampledRequestsEnabled: true,
},
rules: wafRules,
});
In the above, you can see that we're just defining a normal array of objects that contain the specific rule configuration. I've defined a GeoLocation rule that allows any traffic origininating from the US. This effectively means that my WAF will only allow traffic from the US - because remember by default action is to block!
Finally, just update the WAF with the rules
property and reference our new array.
Attach the WAF to the API Gateway
To attach our WAF to any regional resource that is supported (in our case, an existing API gateway that I have), you need to create a CfnWebACLAssociation
construct.
This simply just links the resource and the WAF:
const wafApiGatewayAssociation: waf.CfnWebACLAssociation =
new waf.CfnWebACLAssociation(this, "waf-api-gateway-association", {
webAclArn: customWaf.attrArn,
resourceArn: `arn:aws:apigateway:${cdk.Aws.REGION}::/restapis/${restApiGateway.api.restApiId}/stages/${restApiGateway.api.deploymentStage.stageName}`,
});
Please note that this is again for the regional resources, for CloudFront distribution(s) associations, you need to configure it separately and won't be able to use this construct.
🚀 Deploy
Whenever you're ready, you can perform a diff & then deploy to check out the resources created and the attachment to your regional resource!
Our diff should look something similar to below:
Resources
[+] AWS::WAFv2::WebACL api-gateway-waf apigatewaywaf
[+] AWS::WAFv2::WebACLAssociation waf-api-gateway-association wafapigatewayassociation
And if you jump into the console, you should see your WAF along with the rule configured (make sure you're pointing at the correct region!):
And you should also see this WAF correctly associated with your resource (in my case, an existing API gateway):
Conclusion
Remember to cdk destroy once you are complete to destroy the CloudFormation stack in order to avoid any unnecessary costs if you're just testing this demo out!
- Flexible Security: Provisioning an AWS WAF using AWS CDK enhances web application security effectively.
- Custom Components: Utilise CloudFormation L1 constructs to create custom reusable components due to the lack of L2 constructs for AWS WAF in AWS CDK, but since the API is fairly straightforward this is thankfully quite easy!
- Traffic Filtering: Define specific rules to filter traffic, allowing you to mitigate threats and control the audience accessing your application.
- Regional Attachment: Attach the WAF to regional resources, such as an API Gateway, ensuring appropriate protection for your specific deployment.
- Monitoring: Enable metrics and integrate with CloudWatch for valuable insights into traffic patterns and security efficacy.
- Best Practices: Adhere to AWS's Well-Architected Framework, ensuring your infrastructure is resilient and secure.
- Scalable Security: Maintain robust security settings that can adapt as your application evolves and faces new threats.