diff --git a/.github/workflows/checkov.yml b/.github/workflows/checkov.yml index 33fc0b5..95ff7be 100644 --- a/.github/workflows/checkov.yml +++ b/.github/workflows/checkov.yml @@ -8,11 +8,11 @@ jobs: scan: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.9 - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.11 - name: Scan with Checkov id: checkov uses: bridgecrewio/checkov-action@v12 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e7348e..4580703 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,9 +7,9 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Terraform - uses: hashicorp/setup-terraform@v2 + uses: hashicorp/setup-terraform@v3 - name: Terraform Init id: init run: terraform init @@ -19,11 +19,11 @@ jobs: checkov: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.9 - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.11 - name: Scan with Checkov id: checkov uses: bridgecrewio/checkov-action@v12 @@ -39,8 +39,5 @@ jobs: runs-on: ubuntu-latest needs: [test, checkov] steps: - - uses: actions/checkout@v3 - - uses: "marvinpinto/action-automatic-releases@919008cf3f741b179569b7a6fb4d8860689ab7f0" # v1.2.1 - with: - repo_token: "${{ secrets.GITHUB_TOKEN }}" - prerelease: false + - uses: actions/checkout@v4 + - uses: softprops/action-gh-release@v2 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 3488f5e..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: tests -on: - push: - branches: - - main - pull_request: -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Terraform - uses: hashicorp/setup-terraform@v2 - - name: Terraform Init - id: init - run: terraform init - - name: Terraform Validate - id: validate - run: terraform validate diff --git a/.github/workflows/tf-plan.yml b/.github/workflows/tf-plan.yml new file mode 100644 index 0000000..97cbcde --- /dev/null +++ b/.github/workflows/tf-plan.yml @@ -0,0 +1,29 @@ +name: tf-plan +on: + push: + branches: + - main + pull_request: + +permissions: + id-token: write # This is required for requesting the JWT + contents: read # This is required for actions/checkout + +jobs: + plan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.TF_READ_ONLY_GITHUB_OIDC_ROLE_ARN }} + aws-region: eu-west-1 + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + - name: Terraform Init + id: init + run: terraform init + - name: Terraform Plan + id: plan + run: terraform plan -var 'name=baseline-waf-rule-group' -var 'scope=REGIONAL' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 41361c5..6603338 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,11 +10,11 @@ repos: - id: detect-private-key - id: no-commit-to-branch - repo: https://github.com/gitleaks/gitleaks - rev: v8.16.1 + rev: v8.18.2 hooks: - id: gitleaks - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.81.0 + rev: v1.89.1 hooks: - id: terraform_fmt - id: terraform_validate @@ -24,12 +24,12 @@ repos: args: - --args=--config=__GIT_WORKING_DIR__/.tflint.hcl --fix - repo: https://github.com/bridgecrewio/checkov.git - rev: "2.3.314" + rev: "3.2.74" hooks: - id: checkov args: ["--quiet", "--compact", "--framework", "terraform", "--download-external-modules", "false", "--skip-path", "examples"] - repo: https://github.com/terraform-docs/terraform-docs - rev: v0.16.0 + rev: v0.17.0 hooks: - id: terraform-docs-go args: ["."] diff --git a/.tf-header.md b/.tf-header.md index 2f55ff4..5e572bb 100644 --- a/.tf-header.md +++ b/.tf-header.md @@ -1,5 +1,6 @@ -# Title +# Create a baseline security rule group for WAF -![License](https://img.shields.io/github/license/terrablocks/REPO_NAME?style=for-the-badge) ![Tests](https://img.shields.io/github/actions/workflow/status/terrablocks/REPO_NAME/tests.yml?branch=main&label=Test&style=for-the-badge) ![Checkov](https://img.shields.io/github/actions/workflow/status/terrablocks/REPO_NAME/checkov.yml?branch=main&label=Checkov&style=for-the-badge) ![Commit](https://img.shields.io/github/last-commit/terrablocks/REPO_NAME?style=for-the-badge) ![Release](https://img.shields.io/github/v/release/terrablocks/REPO_NAME?style=for-the-badge) +![License](https://img.shields.io/github/license/terrablocks/aws-wafv2-baseline-rule-group?style=for-the-badge) ![Plan](https://img.shields.io/github/actions/workflow/status/terrablocks/aws-wafv2-baseline-rule-group/tf-plan.yml?branch=main&label=Plan&style=for-the-badge) ![Checkov](https://img.shields.io/github/actions/workflow/status/terrablocks/aws-wafv2-baseline-rule-group/checkov.yml?branch=main&label=Checkov&style=for-the-badge) ![Commit](https://img.shields.io/github/last-commit/terrablocks/aws-wafv2-baseline-rule-group?style=for-the-badge) ![Release](https://img.shields.io/github/v/release/terrablocks/aws-wafv2-baseline-rule-group?style=for-the-badge) This terraform module will deploy the following services: +- WAFv2 Rule Group diff --git a/.tflint.hcl b/.tflint.hcl index a2066b4..ab2cb55 100644 --- a/.tflint.hcl +++ b/.tflint.hcl @@ -1,6 +1,6 @@ plugin "aws" { enabled = true - version = "0.24.3" + version = "0.30.0" source = "github.com/terraform-linters/tflint-ruleset-aws" } diff --git a/LICENSE b/LICENSE index ea0682e..2b47a03 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 terrablocks (by SkildOps) +Copyright (c) 2024 terrablocks (by SkildOps) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0a99a78..7af88c1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,49 @@ -# terraform-base-template -This is a template repository that will serve as a starting point for all the new terraform modules + +# Create a baseline security rule group for WAF -## Important changes: -- Replace `REPO_NAME` with the actual repository name in examples and .tf-header.md -- Update module name in the examples -- Add title in the .tf-header.tf file +![License](https://img.shields.io/github/license/terrablocks/aws-wafv2-baseline-rule-group?style=for-the-badge) ![Plan](https://img.shields.io/github/actions/workflow/status/terrablocks/aws-wafv2-baseline-rule-group/tf-plan.yml?branch=main&label=Plan&style=for-the-badge) ![Checkov](https://img.shields.io/github/actions/workflow/status/terrablocks/aws-wafv2-baseline-rule-group/checkov.yml?branch=main&label=Checkov&style=for-the-badge) ![Commit](https://img.shields.io/github/last-commit/terrablocks/aws-wafv2-baseline-rule-group?style=for-the-badge) ![Release](https://img.shields.io/github/v/release/terrablocks/aws-wafv2-baseline-rule-group?style=for-the-badge) + +This terraform module will deploy the following services: +- WAFv2 Rule Group + +# Usage Instructions +## Example +```hcl +module "wafv2_rule_group" { + source = "github.com/terrablocks/aws-wafv2-baseline-rule-group.git?ref=" # Always use `ref` to point module to a specific version or hash + + name = "baseline-waf-rule-group" + scope = "REGIONAL" +} +``` + +## Requirements + +| Name | Version | +|------|---------| +| terraform | >= 1.8.0 | +| aws | >= 5.0.0 | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| block_cloudfront_default_domain | Block all incoming traffic if the request host header contains cloudfront domain. This rule prevents bad actors from bypassing the custom domain to which you have mapped cloudfront domain | ```object({ enabled = bool priority = optional(number) enable_cw_metrics = optional(bool) })``` | ```{ "enable_cw_metrics": true, "enabled": true, "priority": 1 }``` | no | +| block_load_balancer_default_domain | Block all incoming traffic if the request host header contains load balancer domain. This rule prevents bad actors from bypassing the custom domain to which you have mapped load balancer domain | ```object({ enabled = bool priority = optional(number) enable_cw_metrics = optional(bool) })``` | ```{ "enable_cw_metrics": true, "enabled": true, "priority": 2 }``` | no | +| block_sanctioned_countries | Blacklist all incoming traffic from the countries sanctioned by the US. Country codes must follow alpha-2 format as per [ISO](https://www.iso.org/obp/ui) 3166 standards | ```object({ enabled = bool priority = optional(number) countries_code = optional(list(string)) enable_cw_metrics = optional(bool) })``` | ```{ "countries_code": [ "CU", "IR", "KP", "RU", "SY" ], "enable_cw_metrics": true, "enabled": true, "priority": 0 }``` | no | +| description | Description for the rule group | `string` | `"Baseline security WAF rule group"` | no | +| enable_cw_metrics | Enable CloudWatch metrics for the rule group | `bool` | `true` | no | +| name | Name of the rule group | `string` | n/a | yes | +| scope | Scope of the rule group. **Note:** Valid value is either **REGIONAL** or **CLOUDFRONT** | `string` | n/a | yes | +| tags | Map of key value pair to associate with the rule group | `map(string)` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| arn | ARN of the WAF rule group | +| capacity | WCU (web ACL capacity units) required for the WAF rule group | +| id | ID of the WAF rule group | + + diff --git a/examples/default.tf b/examples/default.tf index 9f6d837..0b7e84e 100644 --- a/examples/default.tf +++ b/examples/default.tf @@ -1,3 +1,6 @@ -module "name" { - source = "github.com/terrablocks/REPO_NAME.git" # Always use `ref` to point module to a specific version or hash +module "wafv2_rule_group" { + source = "github.com/terrablocks/aws-wafv2-baseline-rule-group.git?ref=" # Always use `ref` to point module to a specific version or hash + + name = "baseline-waf-rule-group" + scope = "REGIONAL" } diff --git a/main.tf b/main.tf index e69de29..f2a7bd7 100644 --- a/main.tf +++ b/main.tf @@ -0,0 +1,137 @@ +locals { + # Following docs can be referred to calculate capacity required for each rule + # https://docs.aws.amazon.com/cli/latest/reference/wafv2/check-capacity.html + # https://docs.aws.amazon.com/waf/latest/APIReference/API_CheckCapacity.html#API_CheckCapacity_RequestParameters + rule_group_capacity = sum(flatten( + [ + # https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-geo-match.html + lookup(var.block_sanctioned_countries, "enabled", false) ? [1] : [0], + + # https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-string-match.html + # https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-transformation.html + # 2 for ENDS_WITH constraint and 10 for text transformations + lookup(var.block_cloudfront_default_domain, "enabled", false) ? [12] : [0], + + # https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-type-string-match.html + # https://docs.aws.amazon.com/waf/latest/developerguide/waf-rule-statement-transformation.html + # 2 for ENDS_WITH constraint and 10 for text transformations + lookup(var.block_load_balancer_default_domain, "enabled", false) ? [12] : [0] + ]) + ) +} + +resource "aws_wafv2_rule_group" "this" { + name = var.name + description = var.description + scope = var.scope + capacity = local.rule_group_capacity + + dynamic "rule" { + for_each = lookup(var.block_sanctioned_countries, "enabled", false) ? [0] : [] + content { + name = "block-sanctioned-countries" + priority = var.block_sanctioned_countries["priority"] + + action { + block {} + } + + statement { + geo_match_statement { + country_codes = var.block_sanctioned_countries["countries_code"] + } + } + + dynamic "visibility_config" { + for_each = var.block_sanctioned_countries["enable_cw_metrics"] ? [0] : [] + content { + cloudwatch_metrics_enabled = true + metric_name = "${var.name}-block-sanctioned-countries-rule-waf" + sampled_requests_enabled = true + } + } + } + } + + dynamic "rule" { + for_each = lookup(var.block_cloudfront_default_domain, "enabled", false) ? [0] : [] + content { + name = "block-cloudfront-default-domain" + priority = var.block_cloudfront_default_domain["priority"] + + action { + block {} + } + + statement { + byte_match_statement { + field_to_match { + single_header { + name = "host" + } + } + positional_constraint = "ENDS_WITH" + search_string = ".cloudfront.net" + text_transformation { + type = "LOWERCASE" + priority = 0 + } + } + } + + dynamic "visibility_config" { + for_each = var.block_cloudfront_default_domain["enable_cw_metrics"] ? [0] : [] + content { + cloudwatch_metrics_enabled = true + metric_name = "${var.name}-block-cloudfront-default-domain-rule-waf" + sampled_requests_enabled = true + } + } + } + } + + dynamic "rule" { + for_each = lookup(var.block_load_balancer_default_domain, "enabled", false) ? [0] : [] + content { + name = "block-load-balancer-default-domain" + priority = var.block_load_balancer_default_domain["priority"] + + action { + block {} + } + + statement { + byte_match_statement { + field_to_match { + single_header { + name = "host" + } + } + positional_constraint = "ENDS_WITH" + search_string = ".elb.amazonaws.com" + text_transformation { + type = "LOWERCASE" + priority = 0 + } + } + } + + dynamic "visibility_config" { + for_each = var.block_load_balancer_default_domain["enable_cw_metrics"] ? [0] : [] + content { + cloudwatch_metrics_enabled = true + metric_name = "${var.name}-block-load-balancer-default-domain-rule-waf" + sampled_requests_enabled = true + } + } + } + } + + visibility_config { + cloudwatch_metrics_enabled = var.enable_cw_metrics + metric_name = "${var.name}-waf-rule-group" + sampled_requests_enabled = var.enable_cw_metrics + } + + tags = var.tags +} diff --git a/outputs.tf b/outputs.tf index e69de29..aa57853 100644 --- a/outputs.tf +++ b/outputs.tf @@ -0,0 +1,14 @@ +output "arn" { + value = aws_wafv2_rule_group.this.arn + description = "ARN of the WAF rule group" +} + +output "id" { + value = aws_wafv2_rule_group.this.id + description = "ID of the WAF rule group" +} + +output "capacity" { + value = local.rule_group_capacity + description = "WCU (web ACL capacity units) required for the WAF rule group" +} diff --git a/requirements.tf b/requirements.tf index c62a830..b04fccc 100644 --- a/requirements.tf +++ b/requirements.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.3.0" + required_version = ">= 1.8.0" required_providers { aws = { source = "hashicorp/aws" diff --git a/variables.tf b/variables.tf index e69de29..63083c0 100644 --- a/variables.tf +++ b/variables.tf @@ -0,0 +1,85 @@ +variable "name" { + type = string + description = "Name of the rule group" +} + +variable "description" { + type = string + default = "Baseline security WAF rule group" + description = "Description for the rule group" +} + +variable "scope" { + type = string + description = "Scope of the rule group. **Note:** Valid value is either **REGIONAL** or **CLOUDFRONT**" + + validation { + condition = var.scope == "REGIONAL" || var.scope == "CLOUDFRONT" + error_message = "scope must be either REGIONAL or CLOUDFRONT" + } +} + +variable "enable_cw_metrics" { + type = bool + default = true + description = "Enable CloudWatch metrics for the rule group" +} + +variable "block_sanctioned_countries" { + type = object({ + enabled = bool + priority = optional(number) + countries_code = optional(list(string)) + enable_cw_metrics = optional(bool) + }) + + default = { + enabled = true + priority = 0 + countries_code = [ + "CU", # Cuba + "IR", # Iran + "KP", # N. Korea + "RU", # Russia + "SY" # Syria + ] + enable_cw_metrics = true + } + description = "Blacklist all incoming traffic from the countries sanctioned by the US. Country codes must follow alpha-2 format as per [ISO](https://www.iso.org/obp/ui) 3166 standards" +} + +variable "block_cloudfront_default_domain" { + type = object({ + enabled = bool + priority = optional(number) + enable_cw_metrics = optional(bool) + }) + + default = { + enabled = true + priority = 1 + enable_cw_metrics = true + } + description = "Block all incoming traffic if the request host header contains cloudfront domain. This rule prevents bad actors from bypassing the custom domain to which you have mapped cloudfront domain" +} + +variable "block_load_balancer_default_domain" { + type = object({ + enabled = bool + priority = optional(number) + enable_cw_metrics = optional(bool) + }) + + default = { + enabled = true + priority = 2 + enable_cw_metrics = true + } + description = "Block all incoming traffic if the request host header contains load balancer domain. This rule prevents bad actors from bypassing the custom domain to which you have mapped load balancer domain" +} + +variable "tags" { + type = map(string) + default = null + description = "Map of key value pair to associate with the rule group" +}