Introduction
I recently set up AWS WAF v2 and then found it to be a very useful service. I introduce it in this blog!
So far, I have been using professional security vendor-managed rules, but this time I deployed it using the rulesets provided by AWS(AWS Managed Rules), which I found easy to use and very convenient. In this article, I will guide you through the process of building a WAF with AWS Managed Rules via Terraform.
Disclaimer
WAF is a significant service that governs security. What I will set up in this article is not always correct for every case and depends on the product you are setting up.
Setting up a WAF is your own responsibility, and if you are not confident in your ability to self-set up a WAF given the risks involved, I recommend an approach where you enlist the help of a vendor.
Reference
- What is AWS WAF?
- For those who don't be good at security knowledge, so you can use security vendor consults who AWS WAF security partners are.
How to Set up
Sample Codes
Here are sample codes. I also wrote an example in the blog, but it would be too long to write everything, so I have abbreviated some of the information. If you want to see the full version, please refer to this GitHub Gist.
1. Create a Web ACL
Web ACL is a central resource.
That provides the following features and so on.
- Dashboard
- Which rule sets to apply
- Which AWS resources to configure
- Log and Metrics settings
The setting is so simple, so you can use Management Console easily. As for me, I was set up by Terraform. Here is the document, aws_wafv2_web_acl
resource "aws_wafv2_web_acl" "this" {
name = "${var.prefix}-${var.name}"
scope = "REGIONAL"
default_action {
allow {}
}
tags = {
Name = "${var.prefix}-${var.name}"
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = var.metric_name
sampled_requests_enabled = true
}
}
The above sample is modularized and is used in the form of a call from the main file that serves as the entry point.
module "waf_alb" {
source = "../../modules/waf_alb"
prefix = local.prefix
name = "waf-alb"
metric_name = "waf-alb"
alb_ipsets_v4 = module.waf_ipsets.waf_ipsets_alb_ipv4_arn
alb_ipsets_v6 = module.waf_ipsets.waf_ipsets_alb_ipv6_arn
bucket = "aws-waf-logs-api-server-dev-blah-blah-blah
}
2. Create a Rulesets
I use two types of Rulesets, "AWS Managed Rules" and "my own rules."
As the Disclaimer described, the rulesets are so IMPORTANT.
Above all, you have to set appropriate rules, but I hope my setting rules help you something.
My own rules
I set three types of rules.
name | rule |
---|---|
AWSRateBasedRuleDomesticDOS | prevent DOS from domestic |
AWSRateBasedRuleGlobalDOS | prevent DOS from global |
AWSIPBlackList | prevent Attack based on IP based |
Here are Terraform codes; These attributes are also in aws_wafv2_web_acl.
rule {
name = "AWSRateBasedRuleDomesticDOS"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
scope_down_statement {
geo_match_statement {
country_codes = ["JP"]
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSRateBasedRuleDomesticDOS"
sampled_requests_enabled = true
}
}
rule {
name = "AWSRateBasedRuleGlobalDOS"
priority = 2
action {
block {}
}
statement {
rate_based_statement {
limit = 500
aggregate_key_type = "IP"
scope_down_statement {
not_statement {
statement {
geo_match_statement {
country_codes = ["JP"]
}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSRateBasedRuleGlobalDOS"
sampled_requests_enabled = true
}
}
These two rules can prevent DOS attacks based on rate_based_statement.limit
and country_codes
. The web service I have set up this time is basically intended only for use from my home country(Japan), so I have divided the range of the rate limit between Japan and overseas.
However, the country_codes
may be spoofed, and since the connection restriction by this rule is a time-limited block of 5 minutes, other countermeasures may be safer in case of targeted attacks.
As for the rate based-type detailed rule, please refer to the document.
That's why I set the third rule, AWSIPBlaskList
.
In this case, the rule "IP set match" is used to create a list of IPs and prohibit the request from the IP sets.
Here is document
rule {
name = "AWSIPBlackList"
priority = 3
action {
block {}
}
statement {
or_statement {
statement {
ip_set_reference_statement {
arn = var.alb_ipsets_v4
}
}
statement {
ip_set_reference_statement {
arn = var.alb_ipsets_v6
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSIPBlackList"
sampled_requests_enabled = true
}
}
Also, I prepare another module "waf_ipsets" so that I manage blacklist IP sets for utilizing the above rule.
provider "aws" {
alias = "us-east-1"
region = "us-east-1"
}
resource "aws_wafv2_ip_set" "blacklist_alb_ipv4" {
name = "${var.prefix}-alb-ipv4"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = ["127.0.0.1/32"]
tags = {
Name = "${var.prefix}-alb-ipv4"
}
}
resource "aws_wafv2_ip_set" "blacklist_cf_ipv4" {
name = "${var.prefix}-cf-ipv4"
scope = "CLOUDFRONT"
provider = aws.us-east-1
ip_address_version = "IPV4"
addresses = ["127.0.0.1/32"]
tags = {
Name = "${var.prefix}-cf-ipv4"
}
}
resource "aws_wafv2_ip_set" "blacklist_alb_ipv6" {
name = "${var.prefix}-alb-ipv6"
scope = "REGIONAL"
ip_address_version = "IPV6"
addresses = ["2001:0db8:0000:0000:0000:0000:0000:0001/128"]
tags = {
Name = "${var.prefix}-alb-ipv6"
}
}
resource "aws_wafv2_ip_set" "blacklist_cf_ipv6" {
name = "${var.prefix}-cf-ipv6"
scope = "CLOUDFRONT"
provider = aws.us-east-1
ip_address_version = "IPV6"
addresses = ["2001:0db8:0000:0000:0000:0000:0000:0001/128"]
tags = {
Name = "${var.prefix}-cf-ipv6"
}
}
// ARN
output "waf_ipsets_alb_ipv4_arn" {
value = aws_wafv2_ip_set.blacklist_alb_ipv4.arn
description = "IP sets arn"
}
output "waf_ipsets_cf_ipv4_arn" {
value = aws_wafv2_ip_set.blacklist_cf_ipv4.arn
description = "IP sets arn"
}
output "waf_ipsets_alb_ipv6_arn" {
value = aws_wafv2_ip_set.blacklist_alb_ipv6.arn
description = "IP sets arn"
}
output "waf_ipsets_cf_ipv6_arn" {
value = aws_wafv2_ip_set.blacklist_cf_ipv6.arn
description = "IP sets arn"
}
AWS ManagedRules
Here are the rules I use from ManagedRules provided by AWS.
name | rule |
---|---|
AWSManagedRulesCommonRuleSet | General rules, including those listed in OWASP, CVE, etc. |
AWSManagedRulesKnownBadInputsRuleSet | Detection of requests to discover/exploit vulnerabilities. |
AWSManagedRulesAmazonIpReputationList | Inspects IPs that have been identified as bots by Amazon threat intelligence. _ |
AWSManagedRulesAnonymousIpList | Inspects for a list of IPs known to anonymize client information, like TOR nodes, temporary proxies, and other masking services. |
AWSManagedRulesSQLiRuleSet | block request patterns associated with exploitation of SQL databases, like SQL injection. |
AWSManagedRulesLinuxRuleSet | block request patterns associated with the exploitation of vulnerabilities specific to Linux, including LFI attacks. |
AWSManagedRulesUnixRuleSet | block request patterns associated with the exploitation of vulnerabilities specific to POSIX and POSIX-like OS, including LFI attacks. |
The app doesn't use WordPress and also PHP, but if you so, you should use WordPress or PHP rulesets. If you are using Windows, I recommend you should choose a ruleset based on Windows OS, not Linux. Please refer to this reference for all of these rulesets. The rulesets also have capacity limits, so please refer to the doc to choose the rules you need.
For your reference, I have included the code I have set up in Terraform.
The AWSManagedRulesCommonRuleSet and AWSManagedRulesKnownBadInputsRuleSet are listed as representatives. Other rules are also set in the same way, specifying their names in the rule.name
and rule.priority
.
To prevent false positives, these rule sets are set to COUNT Action by overriding the rule setting that BLOCK them.
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 10
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSManagedRulesCommonRuleSetMetric"
sampled_requests_enabled = true
}
}
rule {
name = "AWSManagedRulesKnownBadInputsRuleSet"
priority = 20
override_action {
count {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesKnownBadInputsRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSManagedRulesKnownBadInputsRuleSetMetric"
sampled_requests_enabled = true
}
}
Attach to CloudFront and ALB & Setting monitoring option
Finally, I will associate WAF with AWS resources and configure logging.
In this case, I will show you an example of associating with ALB and CloudFront.
Also, I have chosen to output the logs to S3. Other than output to S3, you can also use Kinesis, and CloudWatch Logs. However, the resource name of the log output destination must always be set to aws-waf-logs-
.
Please be careful(document)
resource "aws_s3_bucket" "bucket" {
bucket = var.bucket
tags = {
Name = "${var.prefix}-${var.name}"
}
}
resource "aws_wafv2_web_acl_logging_configuration" "api_server_waf_log" {
log_destination_configs = [aws_s3_bucket.bucket.arn]
resource_arn = aws_wafv2_web_acl.this.arn
}
// ARN
output "waf_arn" {
value = aws_wafv2_web_acl.this.arn
description = "IP sets arn"
}
output "s3_bucket_arn" {
value = aws_s3_bucket.bucket.arn
description = "S3 bucket arn"
}
The main file calling the module is described below.
module "waf_alb" {
source = "../../modules/waf_alb"
prefix = local.prefix
name = "waf-alb"
metric_name = "waf-alb"
alb_ipsets_v4 = module.waf_ipsets.waf_ipsets_alb_ipv4_arn
alb_ipsets_v6 = module.waf_ipsets.waf_ipsets_alb_ipv6_arn
bucket = "aws-waf-logs-api-server-prd-blah-blah"
}
module "waf_ipsets" {
source = "../../modules/waf_ipsets"
prefix = local.prefix
name = "ip"
}
module "waf_cf" {
source = "../../modules/waf_cf"
prefix = local.prefix
name = "waf-teacher"
metric_name = "waf-teacher"
cf_ipsets_v4 = module.waf_ipsets.waf_ipsets_cf_ipv4_arn
cf_ipsets_v6 = module.waf_ipsets.waf_ipsets_cf_ipv6_arn
bucket = "aws-waf-logs-teacher-prd-blah-blah"
}
resource "aws_wafv2_web_acl_association" "api_server_waf" {
resource_arn = module.hoge_hoge_alb.alb_arn
web_acl_arn = module.waf_alb.waf_arn
}
# CloudFront associated with WAF(※Module is not listed here)
module "cloudfront_waf" {
source = "../../modules/cloudfront_spa_cdn"
prefix = local.prefix
name = "spa-frontend"
cloudfront_fqdn = local.hoge_teacher_web_frontend_fqdn
zone_id = module.route53_zone.zone_id
web_acl_id = module.waf_teacher.waf_arn
}
# ALB associated with WAF(※Module is not listed here)
module "hoge_hoge_alb" {
source = "../../modules/alb"
prefix = local.prefix
name = "api-alb"
vpc_id = module.main_vpc.vpc_id
zone_id = module.route53_zone.zone_id
alb_subnets = module.main_vpc.public_subnets
alb_fqdn = local.hoge_study_api_alb_fqdn
target_group_port = local.hoge_study_api_server_container_port
target_group_health_check_path = local.hoge_study_api_server_health_check_path
}
In Conclusion
Congrats🎉 Complete the set up💡
Again, if you want to see full sample, please look at the GitHub Gist.
Happy AWS WAF Life!
Top comments (0)