Automating Dynamic AWS WAFv2 Rule Creation with Terraform

Fabricio
System Weakness
Published in
4 min readAug 1, 2023

--

IA generated picture

Introduction

AWS Web Application Firewall (WAF) is a powerful security service that protects web applications from common web exploits and malicious traffic. With the release of AWS WAF version 2, AWS has introduced several improvements and new features. In this article, we will explore how to automate the creation of AWS WAFv2 rules based on IPSet(IPs, IP ranges) using Terraform with a CSV previous fulfilled.

CSV File:

To create our rules, we need some input data such as the IPs we want to block or allow, priority, name and others. We will use a CSV file to provide this input data to our Terraform code.

CSV sample

Terraform Code

The Terraform code will create the IPset and the web acl. The data needed will be fetched from the CSV file all around the code, to name the rule, ta define the priority of the rule and the description. We’ll not approach authentication method in this code and it’s up to user to setup his own authentication method.

provider "aws" {
region = "us-west-2" # Change this to your desired region
}

resource "aws_wafv2_ip_set" "ipset" {
for_each = { for idx, rule in local.wafrule_details : rule.name => rule }

name = "ipset-${each.value.name}"
description = each.value.description

ip_address_version = "IPV4"
scope = var.ipset_scope

addresses = [each.value.ipaddr]
}

resource "aws_wafv2_web_acl" "waf_acl" {
name = "web-acl-${var.web_acl_name}"
description = "Web ACL for ${var.web_acl_name}"
scope = var.wafacl_scope

default_action {
dynamic "allow" {
for_each = var.default_action_type == "allow" ? [1] : []

content {}
}

dynamic "block" {
for_each = var.default_action_type == "block" ? [1] : []

content {}
}
}

visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "WebACL-${var.web_acl_metrics}"
sampled_requests_enabled = true
}

dynamic "rule" {
for_each = local.wafrule_details
content {
name = "rule-${rule.value.name}"
priority = rule.value.priority

action {
count {}
}

dynamic "statement" {
for_each = rule.value != {} ? [1] : []
content {
byte_match_statement {
field_to_match {
single_header {
name = rule.value != {} ? lower(rule.value.name) : "default-header"
}
}
positional_constraint = rule.value != {} ? "STARTS_WITH" : "CONTAINS"
search_string = rule.value != {} ? rule.value.ipaddr : "default-ipaddr"

text_transformation {
priority = 0
type = "NONE"
}
}
}
}

dynamic "visibility_config" {
for_each = rule.value != {} ? [1] : []
content {
cloudwatch_metrics_enabled = true
metric_name = "WebACL-${var.web_acl_metrics}-Rule-${rule.key}"
sampled_requests_enabled = true
}
}
}
}
}

Variables

Variables are an important part of our code and for that we’ll use 2 files, the vars.tfvars file to pass the values we want to be used by the code, the variables.tf file will setup the variables.

Variables.tf file:

variable "web_acl_name" {
type = string
description = "Name of the AWS WAF Web ACL."
}

variable "web_acl_metrics" {
type = string
description = "Metric name for the AWS WAF Web ACL."
}

variable "csv_file_path" {
type = string
description = "File path of the CSV containing the rules data."
}

variable "waf_rule_name" {
type = string
description = "Name of the AWS WAF rule."
}

variable "waf_rule_metrics" {
type = string
description = "Metric name for the AWS WAF rule."
}

variable "rule_priorities" {
type = list(number)
description = "List of rule priorities from CSV data."
default = []
}

variable "ipset_scope" {
type = string
description = "Default scope for ipset CLOUDFRONT / REGIONAL"
}

variable "wafacl_scope" {
type = string
description = "Default scope for WAF ACL CLOUDFRONT / REGIONAL"
}

locals {
csv_data = file(var.csv_file_path)
csv_ips = csvdecode(local.csv_data)
description = "Variable to extract data from CSV"

wafrule_details = [
for wrd in local.csv_ips : {
ipaddr = wrd.ip_address
type = wrd.type
name = wrd.name
priority = wrd.priority
description = wrd.description
}
]

# Rule names are case sensitive, this function will assure names in CSV field
# will be passed in lowercase to rules name.
lowercase_names = {
for rule in local.wafrule_details : rule.name => lower(rule.name)
}

}

variable "default_action_type" {
description = "Type of Default Action. Valid values are 'allow' or 'block'."
type = string
default = "allow" # Change this to "allow" if you want the default action to be "allow"
}

Vars.tfvars file:

web_acl_name        = "Webacl"
web_acl_metrics = "Webaclmetrics"
waf_rule_name = "Wafrulename"
waf_rule_metrics = "Wafrulemetrics"
csv_file_path = "ips.csv"
ipset_scope = "REGIONAL"
wafacl_scope = "REGIONAL"
default_action_type = "block"

Conclusion

Automating the creation of AWS WAFv2 rules using Terraform provides a scalable and efficient way to manage your web application security. With the power of Terraform’s dynamic blocks, you can easily generate rules based on input data, making it flexible for different use cases. The ability to version control your infrastructure as code allows you to track changes and easily replicate your security settings across different environments.

By following this guide, you can quickly set up AWS WAFv2 rules for your web applications and improve their security posture. Always ensure that you are using the latest version of Terraform and AWS WAF services to leverage the latest features and enhancements.

Happy automating!

GitHub Repository.

--

--