To enhance the security of our application, we have implemented AWS WAF in front of API Gateway. During the implementation, we configured WAF, IAM roles, and CloudWatch Logs using Terraform, and this article serves as a memorandum of the process.
Here are the benefits of implementing AWS WAF:
- It can protect web applications from common web attacks.
- It allows for the efficient application of security rules using AWS-managed rule sets.
- It offers flexible security measures by allowing specific rules to be overridden.
https://aws.amazon.com/jp/waf/
Directory Structure
The directory structure of Terraform is modularized, with configurations separated for each environment (dev, stg, prod). The modules
directory contains Terraform modules (waf, iam_roles, cloudwatch_logs, etc.) used in the project.
-- terraform-project/
-- environments/
-- dev/
-- backend.tf
-- main.tf
-- stg/
-- backend.tf
-- main.tf
-- prod/
-- backend.tf
-- main.tf
-- modules/
-- waf/
-- main.tf
-- variables.tf
-- outputs.tf
-- provider.tf
-- README.md
-- iam_roles/
-- main.tf
-- variables.tf
-- outputs.tf
-- provider.tf
-- README.md
-- cloudwatch_logs/
-- main.tf
-- variables.tf
-- outputs.tf
-- provider.tf
-- README.md
-- ...other…
-- docs/
-- architecrture.drowio
-- architecrture.png
Implementation
WAF
The following Terraform code applies WAF to API Gateway and sets various security rules.
./modules/waf/main.tf
resource "aws_wafv2_web_acl" "api_gateway_waf" {
name = "api-gateway-waf"
description = "Managed rule WAF"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "AWS-AWSManagedRulesCommonRuleSet"
priority = 0
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
rule_action_override {
action_to_use {
allow {}
}
name = "SizeRestrictions_BODY"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWS-AWSManagedRulesCommonRuleSet"
sampled_requests_enabled = true
}
}
rule {
name = "AWS-AWSManagedRulesAmazonIpReputationList"
priority = 1
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesAmazonIpReputationList"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWS-AWSManagedRulesAmazonIpReputationList"
sampled_requests_enabled = true
}
}
rule {
name = "AWS-AWSManagedRulesSQLiRuleSet"
priority = 2
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWS-AWSManagedRulesSQLiRuleSet"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "apiGatewayWafMetrics"
sampled_requests_enabled = true
}
}
resource "aws_wafv2_web_acl_logging_configuration" "waf_logging_config" {
log_destination_configs = [var.log_group_arn]
resource_arn = aws_wafv2_web_acl.api_gateway_waf.arn
depends_on = [aws_wafv2_web_acl.api_gateway_waf]
redacted_fields {
single_header {
name = "user-agent"
}
}
}
// attach API Gateway dev
data "aws_ssm_parameter" "api_gateway_api_dev_arn" {
name = "/api/gateway/arn/api/dev"
}
resource "aws_wafv2_web_acl_association" "api_dev" {
resource_arn = data.aws_ssm_parameter.api_gateway_api_dev_arn.value
web_acl_arn = aws_wafv2_web_acl.api_gateway_waf.arn
depends_on = [aws_wafv2_web_acl.api_gateway_waf]
}
// attach API Gateway stg
data "aws_ssm_parameter" "api_gateway_api_stg_arn" {
name = "/api/gateway/arn/api/stg"
}
resource "aws_wafv2_web_acl_association" "api_stg" {
resource_arn = data.aws_ssm_parameter.api_gateway_api_stg_arn.value
web_acl_arn = aws_wafv2_web_acl.api_gateway_waf.arn
}
// attach API Gateway prod
data "aws_ssm_parameter" "api_gateway_api_prod_arn" {
name = "/api/gateway/arn/api/prod"
}
resource "aws_wafv2_web_acl_association" "api_prod" {
resource_arn = data.aws_ssm_parameter.api_gateway_api_prod_arn.value
web_acl_arn = aws_wafv2_web_acl.api_gateway_waf.arn
}
./modules/waf/variables.tf
variable "log_group_arn" {
description = "The ARN of the CloudWatch Logs group"
type = string
}
- The
aws_wafv2_web_acl
resource creates a Web ACL. Here, a Web ACL namedapi-gateway-waf
with a REGIONAL scope is created. - The
default_action
block sets the default action for requests that do not match any specific rule. Here, the allow action is set. If a request matches a configured rule, the action defined in that rule (e.g., block) is executed. - The
rule
block sets each rule. This code uses three rule sets managed by AWS.-
AWSManagedRulesCommonRuleSet
: A rule set to prevent common vulnerabilities and exploits (e.g., XSS, size restrictions). -
AWSManagedRulesAmazonIpReputationList
: A rule set that uses Amazon's IP reputation list (IP addresses of spammers or malware distributors) to filter malicious traffic. -
AWSManagedRulesSQLiRuleSet
: A rule set to detect and prevent SQL injection attacks.
-
- The
visibility_config
block configures the sending of metrics to CloudWatch. -
log_group_arn
defines the ARN of the CloudWatch Logs group as a variable. This ARN is used when sending logs from WAF to CloudWatch Logs.
Overriding Specific Rules in AWS-Managed Rule Sets
Using AWS-managed rule sets is convenient, but there may be times when you want to allow (not block) specific rules. In such cases, you override the settings with rule_action_override
. Below, requests that match the SizeRestrictions_BODY
rule (a rule related to the size restrictions of API requests) from AWSManagedRulesCommonRuleSet
are allowed.
rule_action_override {
action_to_use {
allow {}
}
name = "SizeRestrictions_BODY"
}
This is equivalent to updating the Rule action settings for each rule in the management console.
IAM Roles
The following Terraform code creates an IAM role for sending WAF logs to CloudWatch Logs.
./modules/iam_roles/main.tf
resource "aws_iam_role" "waf_logging" {
name = "waf_logging"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "waf.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_iam_role_policy" "waf_logging" {
name = "waf_logging"
role = aws_iam_role.waf_logging.id
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": "*"
}
]
}
EOF
}
- The
aws_iam_role
resource creates an IAM role namedwaf_logging
. - The
aws_iam_role_policy
resource attaches a policy to this role. This policy allows WAF to send logs to CloudWatch Logs.
CloudWatch Logs
The following Terraform code creates a CloudWatch Logs group to store WAF logs.
./modules/cloudwatch_logs/main.tf
resource "aws_cloudwatch_log_group" "waf_logs" {
name = "aws-waf-logs-api-gateway"
}
./modules/cloudwatch_logs/outputs.tf
output "log_group_arn" {
description = "The ARN of the CloudWatch Logs group"
value = aws_cloudwatch_log_group.waf_logs.arn
}
The aws_cloudwatch_log_group
resource creates a log group named aws-waf-logs-api-gateway
.
Applying the Code
Once each module is created, load the modules in ./environments/{environment}/main.tf
and apply them with terraform apply
.
./environments/{environment}/main.tf
module "waf" {
log_group_arn = module.cloudwatch_logs.log_group_arn
source = "../../modules/waf"
}
module "cloudwatch_logs" {
source = "../../modules/cloudwatch_logs"
}
module "iam_roles" {
source = "../../modules/iam_roles"
}
Once applied, the created WebACL will be displayed on the console screen.
Top comments (0)