DEV Community

Atsushi Suzuki
Atsushi Suzuki

Posted on

AWS WAF Implementation Guide: Setting Up with Terraform for Enhanced Security

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

./modules/waf/variables.tf

variable "log_group_arn" {
  description = "The ARN of the CloudWatch Logs group"
  type        = string
}
Enter fullscreen mode Exit fullscreen mode
  • The aws_wafv2_web_acl resource creates a Web ACL. Here, a Web ACL named api-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"
        }
Enter fullscreen mode Exit fullscreen mode

This is equivalent to updating the Rule action settings for each rule in the management console.

Image description

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
}
Enter fullscreen mode Exit fullscreen mode
  • The aws_iam_role resource creates an IAM role named waf_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"
}
Enter fullscreen mode Exit fullscreen mode

./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
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Once applied, the created WebACL will be displayed on the console screen.

Image description

Top comments (0)