From 1cbebc79f481bb556619ebba6c8966c204a60e82 Mon Sep 17 00:00:00 2001 From: Francesco Medas Date: Thu, 26 Dec 2024 10:42:10 +0400 Subject: [PATCH] feat: DEVOPS-1640 Use Google Cloud Armor to protect the faucet --- infra/tf/apps.tf | 52 +- .../tf/modules/google-cloud-armor/.gitignore | 48 ++ infra/tf/modules/google-cloud-armor/README.md | 801 ++++++++++++++++++ infra/tf/modules/google-cloud-armor/main.tf | 671 +++++++++++++++ .../tf/modules/google-cloud-armor/outputs.tf | 20 + .../modules/google-cloud-armor/variables.tf | 345 ++++++++ .../tf/modules/google-cloud-armor/versions.tf | 35 + infra/tf/variables.tf | 11 +- 8 files changed, 1962 insertions(+), 21 deletions(-) create mode 100644 infra/tf/modules/google-cloud-armor/.gitignore create mode 100644 infra/tf/modules/google-cloud-armor/README.md create mode 100644 infra/tf/modules/google-cloud-armor/main.tf create mode 100644 infra/tf/modules/google-cloud-armor/outputs.tf create mode 100644 infra/tf/modules/google-cloud-armor/variables.tf create mode 100644 infra/tf/modules/google-cloud-armor/versions.tf diff --git a/infra/tf/apps.tf b/infra/tf/apps.tf index 604a7780e..f80a1e80a 100644 --- a/infra/tf/apps.tf +++ b/infra/tf/apps.tf @@ -7,7 +7,7 @@ resource "google_compute_firewall" "allow_apps_external_http" { network = local.network_name direction = "INGRESS" - source_ranges = ["0.0.0.0/0"] + source_ranges = local.google_load_balancer_ip_ranges target_tags = [format("%s-%s", var.chain_name, "apps")] @@ -17,21 +17,6 @@ resource "google_compute_firewall" "allow_apps_external_http" { } } -resource "google_compute_firewall" "allow_apps_external_https" { - name = "${var.chain_name}-apps-allow-external-https" - network = local.network_name - - direction = "INGRESS" - source_ranges = ["0.0.0.0/0"] - - target_tags = [format("%s-%s", var.chain_name, "apps")] - - allow { - protocol = "tcp" - ports = ["443"] - } -} - module "apps" { source = "./modules/node" @@ -124,6 +109,9 @@ resource "google_compute_backend_service" "spout" { capacity_scaler = 1.0 } } + + ## Attach Cloud Armor policy to the backend service + security_policy = module.spout_security_policies.policy.self_link } resource "google_compute_url_map" "apps" { @@ -213,3 +201,35 @@ resource "google_compute_global_forwarding_rule" "faucet_https" { target = google_compute_target_https_proxy.apps.id ip_address = data.google_compute_global_address.faucet.address } + +module "spout_security_policies" { + source = "./modules/google-cloud-armor" + + project_id = var.project_id + name = "${var.chain_name}-apps-spout" + description = "Cloud Armor security policy for the ${var.chain_name} faucet" + default_rule_action = "deny(403)" + type = "CLOUD_ARMOR" + + security_rules = { + allow_whitelisted_ip_ranges = { + action = "allow" + priority = 999 + description = "Allow whitelisted IP address ranges" + src_ip_ranges = ["*"] + } + throttle = { + action = "throttle" + priority = 990 + description = "Limit requests per IP" + src_ip_ranges = ["0.0.0.0/0"] + + rate_limit_options = { + enforce_on_key = "IP" + exceed_action = "deny(429)" + rate_limit_http_request_count = var.apps.faucet_max_hourly_requests + rate_limit_http_request_interval_sec = 3600 + } + } + } +} diff --git a/infra/tf/modules/google-cloud-armor/.gitignore b/infra/tf/modules/google-cloud-armor/.gitignore new file mode 100644 index 000000000..7fc273afb --- /dev/null +++ b/infra/tf/modules/google-cloud-armor/.gitignore @@ -0,0 +1,48 @@ +# OSX leaves these everywhere on SMB shares +._* + +# OSX trash +.DS_Store + +# Python +*.pyc + +# Emacs save files +*~ +\#*\# +.\#* + +# Vim-related files +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist + +### https://raw.github.com/github/gitignore/90f149de451a5433aebd94d02d11b0e28843a1af/Terraform.gitignore + +# Local .terraform directories +**/.terraform/* +.terraform.lock.hcl + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Kitchen files +**/inspec.lock +**/.kitchen +**/.kitchen.local.yml +**/Gemfile.lock + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars +test/fixtures/shared/terraform.tfvars + +credentials.json diff --git a/infra/tf/modules/google-cloud-armor/README.md b/infra/tf/modules/google-cloud-armor/README.md new file mode 100644 index 000000000..ad09480d5 --- /dev/null +++ b/infra/tf/modules/google-cloud-armor/README.md @@ -0,0 +1,801 @@ +# Cloud Armor Terraform Module +This module makes it easy to setup [Cloud Armor global Security Policy](https://cloud.google.com/armor/docs/cloud-armor-overview#security_policies) with Security rules. You can attach the global Security Policy policy to backend services exposed by the following load balancer types: +- Global external Application Load Balancer (HTTP/HTTPS) +- Classic Application Load Balancer (HTTP/HTTPS) +- Global external proxy Network Load Balancer (TCP/SSL) +- Classic proxy Network Load Balancer (TCP/SSL) + +There are `five` type of rules you can create in each policy: +1) [Pre-Configured Rules](#pre_configured_rules): These are based on [pre-configured waf rules](https://cloud.google.com/armor/docs/waf-rules). +2) [Security Rules](#security_rules): Allow or Deny traffic from list of IP addresses or IP adress ranges. +3) [Custom Rules](#custom_rules): You can create your own rules using [Common Expression Language (CEL)](https://cloud.google.com/armor/docs/rules-language-reference). +4) [Threat Intelligence Rules](#threat_intelligence_rules): Add Rules based on [threat intelligence](https://cloud.google.com/armor/docs/threat-intelligence). [Managed protection plus](https://cloud.google.com/armor/docs/managed-protection-overview) subscription is needed to use this feature. +5) [Automatically deploy Adaptive Protection Suggested Rules](#adaptive_protection_auto_deploy); When enable module will create a rule for automatically deploying the suggested rules that [Adaptive Protection generates](https://cloud.google.com/armor/docs/adaptive-protection-auto-deploy). + + +NOTE: For `external passthrough Network Load Balancers`, `protocol forwarding` and `VMs with public IP addresses` create [network Edge Security policy](https://cloud.google.com/armor/docs/security-policy-overview#network-edge-policies) using [advanced network DDoS protection](./modules/advanced-network-ddos-protection/) and [network edge security policy](./modules/network-edge-security-policy/) sub-modules. + + +## Compatibility + +This module is meant for use with Terraform 1.3+ and tested using Terraform 1.3+. If you find incompatibilities using Terraform >=1.3, please open an issue. + +## Version + +Current version is 2.X. Upgrade guides: + +- [0.X -> 1.0.](/docs/upgrading_to_v1.0.md) +- [1.X -> 2.0.](/docs/upgrading_to_v2.0.md) +- [2.X -> 2.1.](/docs/upgrading_to_v2.1.md) + +## Module Format + +``` +module security_policy { + source = "GoogleCloudPlatform/cloud-armor/google" + + project_id = "my-project-id" + name = "my-test-ca-policy" + description = "Test Cloud Armor security policy with preconfigured rules, security rules and custom rules" + default_rule_action = "deny(403)" + type = "CLOUD_ARMOR" + layer_7_ddos_defense_enable = true + layer_7_ddos_defense_rule_visibility = "STANDARD" + recaptcha_redirect_site_key = google_recaptcha_enterprise_key.primary.name + json_parsing = "STANDARD" + log_level = "VERBOSE" + + pre_configured_rules = {} + security_rules = {} + custom_rules = {} + threat_intelligence_rules = {} + adaptive_protection_auto_deploy = {} +} +``` + +Rule details and Sample Code for each type of rule is available [here](#Rules) + +## Usage +There are examples included in the [examples](https://github.com/GoogleCloudPlatform/terraform-google-cloud-armor/tree/main/examples) folder but simple usage is as follows: + +``` +module "security_policy" { + source = "GoogleCloudPlatform/cloud-armor/google" + version = "~> 2.2" + + project_id = var.project_id + name = "my-test-security-policy" + description = "Test Security Policy" + recaptcha_redirect_site_key = google_recaptcha_enterprise_key.primary.name + default_rule_action = "allow" + type = "CLOUD_ARMOR" + layer_7_ddos_defense_enable = true + layer_7_ddos_defense_rule_visibility = "STANDARD" + + # Pre-configured WAF Rules + + pre_configured_rules = { + + "sqli_sensitivity_level_4" = { + action = "deny(502)" + priority = 1 + target_rule_set = "sqli-v33-stable" + + sensitivity_level = 4 + description = "sqli-v33-stable Sensitivity Level 4 and 2 preconfigured_waf_config_exclusions" + } + + "xss-stable_level_2_with_exclude" = { + action = "deny(502)" + priority = 2 + description = "XSS Sensitivity Level 2 with excluded rules" + preview = true + target_rule_set = "xss-v33-stable" + sensitivity_level = 2 + exclude_target_rule_ids = ["owasp-crs-v030301-id941380-xss", "owasp-crs-v030301-id941280-xss"] + } + + "php-stable_level_0_with_include" = { + action = "deny(502)" + priority = 3 + description = "PHP Sensitivity Level 0 with included rules" + target_rule_set = "php-v33-stable" + include_target_rule_ids = ["owasp-crs-v030301-id933190-php", "owasp-crs-v030301-id933111-php"] + } + + } + + # Action against specific IP addresses or IP adress ranges + + security_rules = { + + "deny_project_bad_actor1" = { + action = "deny(502)" + priority = 11 + description = "Deny Malicious IP address from project bad_actor1" + src_ip_ranges = ["190.217.68.211/32", "45.116.227.68/32", "103.43.141.122", "123.11.215.36", "123.11.215.37", ] + preview = true + } + + "rate_ban_project_actor3" = { + action = "rate_based_ban" + priority = 14 + description = "Rate based ban for address from project actor3 only if they cross banned threshold" + src_ip_ranges = ["190.217.68.213", "45.116.227.70", ] + rate_limit_options = { + exceed_action = "deny(502)" + rate_limit_http_request_count = 10 + rate_limit_http_request_interval_sec = 60 + ban_duration_sec = 600 + ban_http_request_count = 1000 + ban_http_request_interval_sec = 300 + enforce_on_key = "ALL" + } + } + } + + # Custom Rules using CEL + + custom_rules = { + + deny_specific_regions = { + action = "deny(502)" + priority = 21 + description = "Deny specific Regions" + expression = <<-EOT + '[AU,BE]'.contains(origin.region_code) + EOT + } + + deny_specific_ip = { + action = "deny(502)" + priority = 22 + description = "Deny specific IP address in US Region" + expression = <<-EOT + origin.region_code == "US" && inIpRange(origin.ip, '47.185.201.159/32') + EOT + } + + allow_path_token_header = { + action = "allow" + priority = 25 + description = "Allow path and token match with addition of header" + + expression = <<-EOT + request.path.matches('/login.html') && token.recaptcha_session.score < 0.2 + EOT + + header_action = [ + { + header_name = "reCAPTCHA-Warning" + header_value = "high" + }, + { + header_name = "X-Resource" + header_value = "test" + } + ] + + } + } + + # Threat Intelligence Rules + + threat_intelligence_rules = { + + deny_malicious_ips = { + action = "deny(502)" + priority = 200 + description = "Deny IP addresses known to attack web applications" + preview = false + feed = "iplist-known-malicious-ips" + exclude_ip = "['47.100.100.100', '47.189.12.139']" + } + } + +} + +resource "google_compute_backend_service" "backend_service" { + provider = google-beta + + ## Attach Cloud Armor policy to the backend service + security_policy = module.cloud_armor.policy.self_link + + project = var.project_id + + name = "glb-ca-web-backend-svc-a" + port_name = "http" + protocol = "HTTP" + timeout_sec = 10 + + backend { + group = google_compute_instance_group.ca_vm_1_ig.self_link + max_utilization = 0.5 + } + + health_checks = [google_compute_http_health_check.default.id] + load_balancing_scheme = "EXTERNAL" +} + +``` + + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| adaptive\_protection\_auto\_deploy | Configuration for Automatically deploy Cloud Armor Adaptive Protection suggested rules. `priority` and `action` fields are required if `enable` is set to true. Requires `layer_7_ddos_defense_enable` set to `true`. |
object({
enable = bool
priority = optional(number, null)
action = optional(string, null)
preview = optional(bool, false)
description = optional(string, "Adaptive Protection auto-deploy")
load_threshold = optional(number)
confidence_threshold = optional(number)
impacted_baseline_threshold = optional(number)
expiration_sec = optional(number)
redirect_type = optional(string)
redirect_target = optional(string)

rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)

enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))

exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}), {})
})
|
{
"enable": false
}
| no | +| custom\_rules | Custome security rules |
map(object({
action = string
priority = number
description = optional(string)
preview = optional(bool, false)
expression = string
redirect_type = optional(string, null)
redirect_target = optional(string, null)
rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)
enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))
exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}),
{})
header_action = optional(list(object({
header_name = optional(string)
header_value = optional(string)
})), [])

preconfigured_waf_config_exclusion = optional(object({
target_rule_set = string
target_rule_ids = optional(list(string), [])
request_header = optional(list(object({
operator = string
value = optional(string)
})))
request_cookie = optional(list(object({
operator = string
value = optional(string)
})))
request_uri = optional(list(object({
operator = string
value = optional(string)
})))
request_query_param = optional(list(object({
operator = string
value = optional(string)
})))
}), { target_rule_set = null }) # Obsolete. Use preconfigured_waf_config_exclusions

preconfigured_waf_config_exclusions = optional(map(object({
target_rule_set = string
target_rule_ids = optional(list(string), [])
request_header = optional(list(object({
operator = string
value = optional(string)
})))
request_cookie = optional(list(object({
operator = string
value = optional(string)
})))
request_uri = optional(list(object({
operator = string
value = optional(string)
})))
request_query_param = optional(list(object({
operator = string
value = optional(string)
})))
})), null)

}))
| `{}` | no | +| default\_rule\_action | default rule that allows/denies all traffic with the lowest priority (2,147,483,647). | `string` | `"allow"` | no | +| description | An optional description of this security policy. Max size is 2048. | `string` | `null` | no | +| json\_custom\_config\_content\_types | A list of custom Content-Type header values to apply the JSON parsing. Only applicable when json\_parsing is set to STANDARD. Not supported for CLOUD\_ARMOR\_EDGE policy type. | `list(string)` | `[]` | no | +| json\_parsing | Whether or not to JSON parse the payload body. Possible values are DISABLED and STANDARD. Not supported for CLOUD\_ARMOR\_EDGE policy type. | `string` | `"DISABLED"` | no | +| layer\_7\_ddos\_defense\_enable | (Optional) If set to true, enables Cloud Armor Adaptive Protection for L7 DDoS detection. Cloud Armor Adaptive Protection is only supported in Global Security Policies of type CLOUD\_ARMOR. Set this variable `true` for Adaptive Protection Auto Deploy. | `bool` | `false` | no | +| layer\_7\_ddos\_defense\_rule\_visibility | (Optional) Rule visibility can be one of the following: STANDARD - opaque rules. PREMIUM - transparent rules. This field is only supported in Global Security Policies of type CLOUD\_ARMOR. | `string` | `"STANDARD"` | no | +| log\_level | Log level to use. Possible values are NORMAL and VERBOSE. Not supported for CLOUD\_ARMOR\_EDGE policy type. | `string` | `"NORMAL"` | no | +| name | Name of the security policy. | `string` | n/a | yes | +| pre\_configured\_rules | Map of pre-configured rules with Sensitivity levels. preconfigured\_waf\_config\_exclusion is obsolete and available for backward compatibility. Use preconfigured\_waf\_config\_exclusions which allows multiple exclusions |
map(object({
action = string
priority = number
description = optional(string)
preview = optional(bool, false)
redirect_type = optional(string, null)
redirect_target = optional(string, null)
target_rule_set = string
sensitivity_level = optional(number, 4)
include_target_rule_ids = optional(list(string), [])
exclude_target_rule_ids = optional(list(string), [])
rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)
enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))
exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}), {})

header_action = optional(list(object({
header_name = optional(string)
header_value = optional(string)
})), [])

preconfigured_waf_config_exclusion = optional(object({
target_rule_set = string
target_rule_ids = optional(list(string), [])
request_header = optional(list(object({
operator = string
value = optional(string)
})))
request_cookie = optional(list(object({
operator = string
value = optional(string)
})))
request_uri = optional(list(object({
operator = string
value = optional(string)
})))
request_query_param = optional(list(object({
operator = string
value = optional(string)
})))
}), { target_rule_set = null }) # Obsolete. Use preconfigured_waf_config_exclusions

preconfigured_waf_config_exclusions = optional(map(object({
target_rule_set = string
target_rule_ids = optional(list(string), [])
request_header = optional(list(object({
operator = string
value = optional(string)
})))
request_cookie = optional(list(object({
operator = string
value = optional(string)
})))
request_uri = optional(list(object({
operator = string
value = optional(string)
})))
request_query_param = optional(list(object({
operator = string
value = optional(string)
})))
})), null)

}))
| `{}` | no | +| project\_id | The project in which the resource belongs. | `string` | n/a | yes | +| recaptcha\_redirect\_site\_key | reCAPTCHA site key to be used for all the rules using the redirect action with the redirect type of GOOGLE\_RECAPTCHA. | `string` | `null` | no | +| security\_rules | Map of Security rules with list of IP addresses to block or unblock. |
map(object({
action = string
priority = number
description = optional(string)
preview = optional(bool, false)
redirect_type = optional(string, null)
redirect_target = optional(string, null)
src_ip_ranges = list(string)
rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)
enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))
exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}),
{})
header_action = optional(list(object({
header_name = optional(string)
header_value = optional(string)
})), [])
}))
| `{}` | no | +| threat\_intelligence\_rules | Map of Threat Intelligence Feed rules |
map(object({
action = string
priority = number
description = optional(string)
preview = optional(bool, false)
feed = string
exclude_ip = optional(string)
rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)
enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))
exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}),
{})
header_action = optional(list(object({
header_name = optional(string)
header_value = optional(string)
})), [])
}))
| `{}` | no | +| type | Type indicates the intended use of the security policy. Possible values are CLOUD\_ARMOR and CLOUD\_ARMOR\_EDGE. | `string` | `"CLOUD_ARMOR"` | no | +| user\_ip\_request\_headers | An optional list of case-insensitive request header names to use for resolving the callers client IP address. | `list(string)` | `[]` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| policy | Security policy created | + + + + + +## Rules + +[Pre-Configured Rules](#pre_configured_rules), [Security Rules](#security_rules), [Custom Rules](#custom_rules) and [Threat Intelligence Rules](#threat_intelligence_rules) are maps of rules. Each rule is a map which provides details about the rule. Here is an example of `pre_configured_rules`: + +``` + "my_rule" = { + action = "deny(502)" + priority = 1 + description = "SQL Sensitivity Level 4" + preview = false + redirect_type = null + redirect_target = null + target_rule_set = "sqli-v33-stable" + sensitivity_level = 4 + include_target_rule_ids = [] + exclude_target_rule_ids = [] + header_action = [] + rate_limit_options = {} + preconfigured_waf_config_exclusions = {} + } +``` + +`action, priority, description, preview, rate_limit_options, header_action, redirect_type and redirect_target` are common in all the rule types. Some of then are optional and some have default value see [Input](#Inputs). + +## Rate limit +`rate_limit_options` is needed for the rules where action is set to `throttle` or `rate_based_ban`. `rate_limit_options` is a map of strings with following key pairs. You can find more details about rate limit [here](https://cloud.google.com/armor/docs/rate-limiting-overview). + +``` +rate_limit_options = { + exceed_action = "deny(502)" + rate_limit_http_request_count = 10 + rate_limit_http_request_interval_sec = 60 # must be one of 60, 120, 180, 240, 300, 600, 900, 1200, 1800, 2700, 3600 seconds + ban_duration_sec = 600 # needed only if action is rate_based_ban + ban_http_request_count = 1000 # needed only if action is rate_based_ban + ban_http_request_interval_sec = 300 # must be one of 60, 120, 180, 240, 300, 600, 900, 1200, 1800, 2700, 3600 seconds. needed only if action is rate_based_ban + enforce_on_key = "ALL" # All is default value. If null is passed terraform will use ALL as the value. Will be set to "" when `enforce_on_key_configs` is not null + + enforce_on_key_configs = [ + { + enforce_on_key_type = "HTTP_PATH" + }, + { + enforce_on_key_type = "HTTP_COOKIE" + enforce_on_key_name = "site_id" + } + ] +} +``` + +## Preconfigured WAF Config +:bangbang: **NOTE:** `preconfigured_waf_config_exclusion` in `pre_configured_rules` and `custom_rules` is obsolete and available for backward compatibility only. Use `pre_configured_rules.preconfigured_waf_config_exclusions` which allows multiple exclusions. They are mutually exclusive. + +`preconfigured_waf_config_exclusions` is needed for custom application that might contain content in request fields (like headers, cookies, query parameters, or URIs) that matches signatures in preconfigured WAF rules, but which you know is legitimate. In this case, you can reduce false positives by excluding those request fields from inspection by associating a list of exclusions for request fields with the security policy rule. You can pass `request_header`, `request_uri`, `request_cookie` and `request_query_param`. It is available in [Pre-Configured Rules](#pre_configured_rules). You can find more details about `preconfigured_waf_config` [here](https://cloud.google.com/armor/docs/rule-tuning#exclude_request_fields_from_inspection) + +``` +preconfigured_waf_config_exclusions = { + + exclusion_1 = { + target_rule_set = "sqli-v33-stable" + target_rule_ids = ["owasp-crs-v030301-id942120-sqli", "owasp-crs-v030301-id942130-sqli"] + request_cookie = [ + { + operator = "STARTS_WITH" + value = "abc" + } + ] + request_header = [ + { + operator = "STARTS_WITH" + value = "xyz" + }, + { + operator = "STARTS_WITH" + value = "uvw" + } + ] + } + + exclusion_2 = { + target_rule_set = "sqli-v33-stable" + target_rule_ids = ["owasp-crs-v030301-id942150-sqli", "owasp-crs-v030301-id942180-sqli"] + request_header = [ + { + operator = "STARTS_WITH" + value = "lmn" + }, + { + operator = "ENDS_WITH" + value = "opq" + } + ] + request_uri = [ + { + operator = "CONTAINS" + value = "https://hashicorp.com" + }, + { + operator = "CONTAINS" + value = "https://xyz.com" + }, + ] + } + +} +``` + +## pre_configured_rules +List of preconfigured rules are available [here](https://cloud.google.com/armor/docs/waf-rules). Following is the key value pairs for setting up pre configured rules. `include_target_rule_ids` and `exclude_target_rule_ids` are mutually exclusive. If `include_target_rule_ids` is provided, sensitivity_level is automatically set to 0 by the module as it is a [requirement for opt in rule signature](https://cloud.google.com/armor/docs/rule-tuning#opt_in_rule_signatures). `exclude_target_rule_ids` is ignored when `include_target_rule_ids` is provided. + +### Format: + +``` + "sqli_sensitivity_level_4" = { + action = "deny(502)" + priority = 1 + description = "SQL Sensitivity Level 4" + preview = false + redirect_type = null + redirect_target = null + target_rule_set = "sqli-v33-stable" + sensitivity_level = 4 + include_target_rule_ids = [] + exclude_target_rule_ids = [] + rate_limit_options = {} + header_action = [] + preconfigured_waf_config_exclusions = {} + } +``` + + +### Sample: + +``` +pre_configured_rules = { + + "php-stable_level_1_with_include" = { + action = "deny(502)" + priority = 3 + description = "PHP Sensitivity Level 1 with included rules" + target_rule_set = "xss-v33-stable" + sensitivity_level = 0 + include_target_rule_ids = ["owasp-crs-v030301-id933190-php", "owasp-crs-v030301-id933111-php"] + } + + "sqli_sensitivity_level_4" = { + action = "deny(502)" + priority = 1 + target_rule_set = "sqli-v33-stable" + sensitivity_level = 4 + + preconfigured_waf_config_exclusions = { + + exclusion_1 = { + target_rule_set = "sqli-v33-stable" + target_rule_ids = ["owasp-crs-v030301-id942120-sqli", "owasp-crs-v030301-id942130-sqli"] + request_cookie = [ + { + operator = "STARTS_WITH" + value = "abc" + } + ] + request_header = [ + { + operator = "STARTS_WITH" + value = "xyz" + }, + { + operator = "STARTS_WITH" + value = "uvw" + } + ] + } + + exclusion_2 = { + target_rule_set = "sqli-v33-stable" + target_rule_ids = ["owasp-crs-v030301-id942150-sqli", "owasp-crs-v030301-id942180-sqli"] + request_header = [ + { + operator = "STARTS_WITH" + value = "lmn" + }, + { + operator = "ENDS_WITH" + value = "opq" + } + ] + request_uri = [ + { + operator = "CONTAINS" + value = "https://hashicorp.com" + }, + { + operator = "CONTAINS" + value = "https://xyz.com" + }, + ] + } + + } + + } + +} +``` + + +## security_rules: +Set of IP addresses or ranges (IPV4 or IPV6) in CIDR notation to match against inbound traffic. There is a limit of 10 IP ranges per rule. + +### Format: +Each rule is key value pair where key is a unique name of the rule and value is the action associated with it. + +``` +"block_bad_actor_ip" = { + action = "deny(502)" + priority = 11 + description = "Deny Malicious IP address" + src_ip_ranges = ["A..B.C.D", "W.X.Y.Z",] + preview = false + redirect_type = null + redirect_target = null + rate_limit_options = {} + header_action = [] +} +``` + +### Sample: + +``` +security_rules = { + + "deny_project_bad_actor" = { + action = "deny(502)" + priority = 11 + description = "Deny Malicious IP address from project bad_actor" + src_ip_ranges = ["190.217.68.211/32", "45.116.227.68/32", "103.43.141.122", "123.11.215.36", ] + } + + "throttle_project_droptwenty" = { + action = "throttle" + priority = 15 + description = "Throttle IP addresses from project droptwenty" + src_ip_ranges = ["190.217.68.214", "45.116.227.71", ] + + rate_limit_options = { + exceed_action = "deny(502)" + rate_limit_http_request_count = 10 + rate_limit_http_request_interval_sec = 60 + enforce_on_key_configs = [ + { + enforce_on_key_type = "HTTP_PATH" + }, + { + enforce_on_key_type = "HTTP_COOKIE" + enforce_on_key_name = "site_id" + } + ] + } + + } + +} +``` + +## custom_rules: +Add Custom Rules using [Common Expression Language (CEL)](https://cloud.google.com/armor/docs/rules-language-reference) + +### Format: +Each rule is key value pair where key is a unique name of the rule and value is the action associated with it. + +``` +allow_specific_regions = { + action = "allow" + priority = 21 + description = "Allow specific Regions" + preview = false + expression = <<-EOT + '[US,AU,BE]'.contains(origin.region_code) + EOT + redirect_type = null + redirect_target = null + rate_limit_options = {} + header_action = [] +} +``` + +### Sample: + +``` +custom_rules = { + + allow_specific_regions = { + action = "allow" + priority = 21 + description = "Allow specific Regions" + preview = true + expression = <<-EOT + '[US,AU,BE]'.contains(origin.region_code) + EOT + } + + allow_path_token_header = { + action = "allow" + priority = 25 + description = "Allow path and token match with addition of header" + + expression = <<-EOT + request.path.matches('/login.html') && token.recaptcha_session.score < 0.2 + EOT + + header_action = [ + { + header_name = "reCAPTCHA-Warning" + header_value = "high" + }, + { + header_name = "X-Resource" + header_value = "test" + } + ] + + } + +} +``` + +## threat_intelligence_rules: +Add Rules based on [threat intelligence](https://cloud.google.com/armor/docs/threat-intelligence). [Managed protection plus](https://cloud.google.com/armor/docs/managed-protection-overview) subscription is needed to use this feature. + +### Format: +Each rule is key value pair where key is a unique name of the rule and value is the action associated with it. NOTE: `exclude_ip` is a string with IP addresse(s) in single quotes and enclused within a sqare bracket (You can find detail [here](https://cloud.google.com/armor/docs/threat-intelligence#configure-nti)). + +``` +threat_intelligence_rules = { + deny_crawlers_ip = { + action = "deny(502)" + priority = 31 + description = "Deny IP addresses of search engine crawlers" + preview = false + feed = "iplist-search-engines-crawlers" + exclude_ip = null + rate_limit_options = {} + header_action = [] + } +} +``` + +### Sample: + +``` +threat_intelligence_rules = { + + deny_malicious_ips = { + action = "deny(502)" + priority = 31 + description = "Deny IP addresses known to attack web applications" + preview = true + feed = "iplist-known-malicious-ips" + exclude_ip = "['47.100.100.100', '47.189.12.139']" + } + + deny_tor_exit_ips = { + action = "deny(502)" + priority = 31 + description = "Deny Tor exit nodes IP addresses" + preview = true + feed = "iplist-tor-exit-nodes" + } + +} +``` + +## adaptive_protection_auto_deploy: +Add a rule to [Automatically deploy Adaptive Protection suggested rules](https://cloud.google.com/armor/docs/adaptive-protection-auto-deploy). [Managed protection plus](https://cloud.google.com/armor/docs/managed-protection-overview) subscription is needed to use this feature. By default this feature is disabled. If `enable` is set to true you need to provide `priority` and `action` for this module to deploy auto deploy rule. Module will create a rule with expression `evaluateAdaptiveProtectionAutoDeploy()`. + +### Format: +It is an object with key value pair. + +``` +adaptive_protection_auto_deploy = { + enable = true + action = "deny(502)" + priority = 31 + description = "Automatically deploy Adaptive Protection suggested rules" + preview = false + load_threshold = 0.1 + confidence_threshold = 0.5 + impacted_baseline_threshold = 0.01 + expiration_sec = 7200 + redirect_type = null + redirect_target = null + rate_limit_options = {} +} +``` + +### Sample 1 (Deny): + +``` +adaptive_protection_auto_deploy = { + enable = true + priority = 100000 + action = "deny(403)" +} +``` + +### Sample 2 (redirect): + +``` +adaptive_protection_auto_deploy = { + enable = true + priority = 100000 + action = "redirect" + redirect_type = "GOOGLE_RECAPTCHA" +} +``` + +### Sample 3 (throttle): + +``` +adaptive_protection_auto_deploy = { + enable = true + priority = 100000 + action = "throttle" + + rate_limit_options = { + exceed_action = "deny(502)" + rate_limit_http_request_count = 500 + rate_limit_http_request_interval_sec = 120 + enforce_on_key = "IP" + } +} +``` + +## Requirements + +These sections describe requirements for using this module. + +### Software + +The following dependencies must be available: + +- [Terraform][terraform] v1.3+ +- [Terraform Provider for GCP][terraform-provider-gcp] plugin v4.79+ + +### Service Account + +A service account with the following permission must be used to provision +the resources of this module: + +- compute.networkEdgeSecurityServices.create +- compute.networkEdgeSecurityServices.update +- compute.networkEdgeSecurityServices.get +- compute.networkEdgeSecurityServices.delete +- compute.networkEdgeSecurityServices.list +- compute.securityPolicies.create +- compute.securityPolicies.delete +- compute.securityPolicies.get +- compute.securityPolicies.list +- compute.securityPolicies.use +- compute.securityPolicies.update +- recaptchaenterprise.keys.list +- recaptchaenterprise.keys.get + +Following roles contain above mentioned permissions. You can either assing one of the following role or create custom roles with above permissions. + +- Compute Organization Security Policy Admin: `roles/compute.orgSecurityPolicyAdmin` +- Compute Security Admin: `roles/compute.securityAdmin` +- reCAPTCHA Enterprise Admin: `roles/recaptchaenterprise.admin` + +### Enable API's +In order to operate with the Service Account you must activate the following API on the project where the Service Account was created: + +- Compute Engine API - compute.googleapis.com + +## Contributing + +Refer to the [contribution guidelines](./CONTRIBUTING.md) for +information on contributing to this module. + + +Copyright 2023 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [google](#requirement\_google) | >= 4.79, < 6 | +| [google-beta](#requirement\_google-beta) | >= 4.79, < 6 | + +## Providers + +| Name | Version | +|------|---------| +| [google-beta](#provider\_google-beta) | >= 4.79, < 6 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [google-beta_google_compute_security_policy.policy](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_compute_security_policy) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [adaptive\_protection\_auto\_deploy](#input\_adaptive\_protection\_auto\_deploy) | Configuration for Automatically deploy Cloud Armor Adaptive Protection suggested rules. `priority` and `action` fields are required if `enable` is set to true. Requires `layer_7_ddos_defense_enable` set to `true`. |
object({
enable = bool
priority = optional(number, null)
action = optional(string, null)
preview = optional(bool, false)
description = optional(string, "Adaptive Protection auto-deploy")
load_threshold = optional(number)
confidence_threshold = optional(number)
impacted_baseline_threshold = optional(number)
expiration_sec = optional(number)
redirect_type = optional(string)
redirect_target = optional(string)

rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)

enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))

exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}), {})
})
|
{
"enable": false
}
| no | +| [custom\_rules](#input\_custom\_rules) | Custome security rules |
map(object({
action = string
priority = number
description = optional(string)
preview = optional(bool, false)
expression = string
redirect_type = optional(string, null)
redirect_target = optional(string, null)
rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)
enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))
exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}),
{})
header_action = optional(list(object({
header_name = optional(string)
header_value = optional(string)
})), [])

preconfigured_waf_config_exclusion = optional(object({
target_rule_set = string
target_rule_ids = optional(list(string), [])
request_header = optional(list(object({
operator = string
value = optional(string)
})))
request_cookie = optional(list(object({
operator = string
value = optional(string)
})))
request_uri = optional(list(object({
operator = string
value = optional(string)
})))
request_query_param = optional(list(object({
operator = string
value = optional(string)
})))
}), { target_rule_set = null }) # Obsolete. Use preconfigured_waf_config_exclusions

preconfigured_waf_config_exclusions = optional(map(object({
target_rule_set = string
target_rule_ids = optional(list(string), [])
request_header = optional(list(object({
operator = string
value = optional(string)
})))
request_cookie = optional(list(object({
operator = string
value = optional(string)
})))
request_uri = optional(list(object({
operator = string
value = optional(string)
})))
request_query_param = optional(list(object({
operator = string
value = optional(string)
})))
})), null)

}))
| `{}` | no | +| [default\_rule\_action](#input\_default\_rule\_action) | default rule that allows/denies all traffic with the lowest priority (2,147,483,647). | `string` | `"allow"` | no | +| [description](#input\_description) | An optional description of this security policy. Max size is 2048. | `string` | `null` | no | +| [json\_custom\_config\_content\_types](#input\_json\_custom\_config\_content\_types) | A list of custom Content-Type header values to apply the JSON parsing. Only applicable when json\_parsing is set to STANDARD. Not supported for CLOUD\_ARMOR\_EDGE policy type. | `list(string)` | `[]` | no | +| [json\_parsing](#input\_json\_parsing) | Whether or not to JSON parse the payload body. Possible values are DISABLED and STANDARD. Not supported for CLOUD\_ARMOR\_EDGE policy type. | `string` | `"DISABLED"` | no | +| [layer\_7\_ddos\_defense\_enable](#input\_layer\_7\_ddos\_defense\_enable) | (Optional) If set to true, enables Cloud Armor Adaptive Protection for L7 DDoS detection. Cloud Armor Adaptive Protection is only supported in Global Security Policies of type CLOUD\_ARMOR. Set this variable `true` for Adaptive Protection Auto Deploy. | `bool` | `false` | no | +| [layer\_7\_ddos\_defense\_rule\_visibility](#input\_layer\_7\_ddos\_defense\_rule\_visibility) | (Optional) Rule visibility can be one of the following: STANDARD - opaque rules. PREMIUM - transparent rules. This field is only supported in Global Security Policies of type CLOUD\_ARMOR. | `string` | `"STANDARD"` | no | +| [log\_level](#input\_log\_level) | Log level to use. Possible values are NORMAL and VERBOSE. Not supported for CLOUD\_ARMOR\_EDGE policy type. | `string` | `"NORMAL"` | no | +| [name](#input\_name) | Name of the security policy. | `string` | n/a | yes | +| [pre\_configured\_rules](#input\_pre\_configured\_rules) | Map of pre-configured rules with Sensitivity levels. preconfigured\_waf\_config\_exclusion is obsolete and available for backward compatibility. Use preconfigured\_waf\_config\_exclusions which allows multiple exclusions |
map(object({
action = string
priority = number
description = optional(string)
preview = optional(bool, false)
redirect_type = optional(string, null)
redirect_target = optional(string, null)
target_rule_set = string
sensitivity_level = optional(number, 4)
include_target_rule_ids = optional(list(string), [])
exclude_target_rule_ids = optional(list(string), [])
rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)
enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))
exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}), {})

header_action = optional(list(object({
header_name = optional(string)
header_value = optional(string)
})), [])

preconfigured_waf_config_exclusion = optional(object({
target_rule_set = string
target_rule_ids = optional(list(string), [])
request_header = optional(list(object({
operator = string
value = optional(string)
})))
request_cookie = optional(list(object({
operator = string
value = optional(string)
})))
request_uri = optional(list(object({
operator = string
value = optional(string)
})))
request_query_param = optional(list(object({
operator = string
value = optional(string)
})))
}), { target_rule_set = null }) # Obsolete. Use preconfigured_waf_config_exclusions

preconfigured_waf_config_exclusions = optional(map(object({
target_rule_set = string
target_rule_ids = optional(list(string), [])
request_header = optional(list(object({
operator = string
value = optional(string)
})))
request_cookie = optional(list(object({
operator = string
value = optional(string)
})))
request_uri = optional(list(object({
operator = string
value = optional(string)
})))
request_query_param = optional(list(object({
operator = string
value = optional(string)
})))
})), null)

}))
| `{}` | no | +| [project\_id](#input\_project\_id) | The project in which the resource belongs. | `string` | n/a | yes | +| [recaptcha\_redirect\_site\_key](#input\_recaptcha\_redirect\_site\_key) | reCAPTCHA site key to be used for all the rules using the redirect action with the redirect type of GOOGLE\_RECAPTCHA. | `string` | `null` | no | +| [security\_rules](#input\_security\_rules) | Map of Security rules with list of IP addresses to block or unblock. |
map(object({
action = string
priority = number
description = optional(string)
preview = optional(bool, false)
redirect_type = optional(string, null)
redirect_target = optional(string, null)
src_ip_ranges = list(string)
rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)
enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))
exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}),
{})
header_action = optional(list(object({
header_name = optional(string)
header_value = optional(string)
})), [])
}))
| `{}` | no | +| [threat\_intelligence\_rules](#input\_threat\_intelligence\_rules) | Map of Threat Intelligence Feed rules |
map(object({
action = string
priority = number
description = optional(string)
preview = optional(bool, false)
feed = string
exclude_ip = optional(string)
rate_limit_options = optional(object({
enforce_on_key = optional(string)
enforce_on_key_name = optional(string)
enforce_on_key_configs = optional(list(object({
enforce_on_key_name = optional(string)
enforce_on_key_type = optional(string)
})))
exceed_action = optional(string)
rate_limit_http_request_count = optional(number)
rate_limit_http_request_interval_sec = optional(number)
ban_duration_sec = optional(number)
ban_http_request_count = optional(number)
ban_http_request_interval_sec = optional(number)
}),
{})
header_action = optional(list(object({
header_name = optional(string)
header_value = optional(string)
})), [])
}))
| `{}` | no | +| [type](#input\_type) | Type indicates the intended use of the security policy. Possible values are CLOUD\_ARMOR and CLOUD\_ARMOR\_EDGE. | `string` | `"CLOUD_ARMOR"` | no | +| [user\_ip\_request\_headers](#input\_user\_ip\_request\_headers) | An optional list of case-insensitive request header names to use for resolving the callers client IP address. | `list(string)` | `[]` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [policy](#output\_policy) | Security policy created | + diff --git a/infra/tf/modules/google-cloud-armor/main.tf b/infra/tf/modules/google-cloud-armor/main.tf new file mode 100644 index 000000000..8f6f25315 --- /dev/null +++ b/infra/tf/modules/google-cloud-armor/main.tf @@ -0,0 +1,671 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +locals { + ### find all the preconfigured rule with no include or exclude expression + pre_configured_rules_no_cond_expr = { for name, policy in var.pre_configured_rules : name => { + expression = "evaluatePreconfiguredWaf('${policy["target_rule_set"]}', {'sensitivity': ${policy["sensitivity_level"]}})" + } if length(policy["include_target_rule_ids"]) == 0 && length(policy["exclude_target_rule_ids"]) == 0 + } + + ### find all the preconfigured rule with include (Opt In rules) expression + pre_configured_rules_include = { for name, policy in var.pre_configured_rules : name => { + target_rule_set = policy.target_rule_set + include_target_rule_ids = replace(join(",", policy.include_target_rule_ids), ",", "','") + sensitivity_level = policy.sensitivity_level + action = policy.action + priority = 0 + description = policy.description + preview = policy.preview + redirect_type = policy.redirect_type + rate_limit_options = policy.rate_limit_options + } if length(policy["include_target_rule_ids"]) > 0 + } + + pre_configured_rules_include_expr = { for name, policy in local.pre_configured_rules_include : name => { + expression = "evaluatePreconfiguredWaf('${policy["target_rule_set"]}', {'sensitivity': 0, 'opt_in_rule_ids': ['${policy.include_target_rule_ids}']})" + } + } + + ### find all the preconfigured rule with Exclude (Opt out rules) expression + pre_configured_rules_exclude = { for name, policy in var.pre_configured_rules : name => { + target_rule_set = policy.target_rule_set + exclude_target_rule_ids = replace(join(",", policy.exclude_target_rule_ids), ",", "','") + sensitivity_level = policy.sensitivity_level + action = policy.action + priority = policy.priority + description = policy.description + preview = policy.preview + redirect_type = policy.redirect_type + rate_limit_options = policy.rate_limit_options + } if length(policy["include_target_rule_ids"]) == 0 && length(policy["exclude_target_rule_ids"]) > 0 + } + pre_configured_rules_exclude_expr = { for name, policy in local.pre_configured_rules_exclude : name => { + expression = "evaluatePreconfiguredWaf('${policy["target_rule_set"]}', {'sensitivity': ${policy.sensitivity_level}, 'opt_out_rule_ids': ['${policy.exclude_target_rule_ids}']})" + } + } + ## Combine all the preconfigured rules + pre_configured_rules_expr = merge(local.pre_configured_rules_no_cond_expr, local.pre_configured_rules_include_expr, local.pre_configured_rules_exclude_expr) +} + +resource "google_compute_security_policy" "policy" { + provider = google-beta + name = var.name + description = var.description + project = var.project_id + type = var.type + + dynamic "recaptcha_options_config" { + for_each = var.recaptcha_redirect_site_key == null ? [] : ["redirect_site_key"] + content { + redirect_site_key = var.recaptcha_redirect_site_key + } + } + + # Advanced options for Cloud Armor are currently only supported for security policies with CLOUD_ARMOR type + dynamic "advanced_options_config" { + for_each = var.type == "CLOUD_ARMOR" ? ["CLOUD_ARMOR"] : [] + content { + json_parsing = var.json_parsing + log_level = var.log_level + user_ip_request_headers = var.user_ip_request_headers + dynamic "json_custom_config" { + for_each = var.json_parsing == "STANDARD" && length(var.json_custom_config_content_types) > 0 ? ["json_custom_config"] : [] + content { + content_types = var.json_custom_config_content_types + } + } + } + } + + ##### Preconfigured WAF Rules + + dynamic "rule" { + for_each = var.pre_configured_rules + content { + action = rule.value["action"] + priority = rule.value["priority"] + preview = rule.value["preview"] + description = rule.value["description"] + + match { + expr { + expression = local.pre_configured_rules_expr[rule.key].expression + } + } + + # Header Action Block. Only if header_action is provided + dynamic "header_action" { + for_each = length(rule.value["header_action"]) == 0 ? [] : ["header_action"] + content { + dynamic "request_headers_to_adds" { + for_each = { for x in rule.value["header_action"] : x.header_name => x } + content { + header_name = request_headers_to_adds.value.header_name + header_value = request_headers_to_adds.value.header_value + } + } + } + } + + ### Redirect option + dynamic "redirect_options" { + for_each = rule.value["action"] == "redirect" ? ["redirect"] : [] + content { + type = rule.value["redirect_type"] + target = rule.value["redirect_type"] == "EXTERNAL_302" ? rule.value["redirect_target"] : null + } + } + + ### Rate limit. Execute only if Action is "rate_based_ban" or "throttle" + dynamic "rate_limit_options" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limits"] : [] + content { + conform_action = "allow" + ban_duration_sec = rule.value["action"] == "rate_based_ban" ? lookup(rule.value["rate_limit_options"], "ban_duration_sec") : null + exceed_action = lookup(rule.value["rate_limit_options"], "exceed_action") + enforce_on_key = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key", null) : "" + enforce_on_key_name = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key_name", null) : null + + dynamic "enforce_on_key_configs" { + for_each = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? {} : { for x in lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") : x.enforce_on_key_type => x } + content { + enforce_on_key_type = enforce_on_key_configs.value.enforce_on_key_type + enforce_on_key_name = enforce_on_key_configs.value.enforce_on_key_name + } + } + + ## Required for all rate limit options + dynamic "rate_limit_threshold" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limit_options"] : [] + content { + count = rule.value["rate_limit_options"].rate_limit_http_request_count + interval_sec = rule.value["rate_limit_options"].rate_limit_http_request_interval_sec + } + } + + ## Optional. Can be provided for for rate based ban. Not needed for throttle + dynamic "ban_threshold" { + for_each = rule.value["action"] == "rate_based_ban" && lookup(rule.value["rate_limit_options"], "ban_http_request_count", null) != null && lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec", null) != null ? ["ban_threshold"] : [] + content { + count = lookup(rule.value["rate_limit_options"], "ban_http_request_count") + interval_sec = lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec") + } + } + } + } + + # Optional preconfigured_waf_config Block if preconfigured_waf_config_exclusion is provided + dynamic "preconfigured_waf_config" { + for_each = rule.value.preconfigured_waf_config_exclusion.target_rule_set == null ? [] : ["preconfigured_waf_config_exclusion"] + content { + exclusion { + target_rule_set = rule.value.preconfigured_waf_config_exclusion.target_rule_set + target_rule_ids = rule.value.preconfigured_waf_config_exclusion.target_rule_ids + dynamic "request_header" { + for_each = rule.value.preconfigured_waf_config_exclusion.request_header == null ? {} : { for x in rule.value.preconfigured_waf_config_exclusion.request_header : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_header.value.operator + value = request_header.value.operator == "EQUALS_ANY" ? null : request_header.value.value + } + } + dynamic "request_cookie" { + for_each = rule.value.preconfigured_waf_config_exclusion.request_cookie == null ? {} : { for x in rule.value.preconfigured_waf_config_exclusion.request_cookie : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_cookie.value.operator + value = request_cookie.value.operator == "EQUALS_ANY" ? null : request_cookie.value.value + } + } + dynamic "request_uri" { + for_each = rule.value.preconfigured_waf_config_exclusion.request_uri == null ? {} : { for x in rule.value.preconfigured_waf_config_exclusion.request_uri : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_uri.value.operator + value = request_uri.value.operator == "EQUALS_ANY" ? null : request_uri.value.value + } + } + dynamic "request_query_param" { + for_each = rule.value.preconfigured_waf_config_exclusion.request_query_param == null ? {} : { for x in rule.value.preconfigured_waf_config_exclusion.request_query_param : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_query_param.value.operator + value = request_query_param.value.operator == "EQUALS_ANY" ? null : request_query_param.value.value + } + } + } + } + } + + # Optional preconfigured_waf_config Block if preconfigured_waf_config_exclusion is provided + dynamic "preconfigured_waf_config" { + for_each = rule.value.preconfigured_waf_config_exclusions == null ? [] : ["preconfigured_waf_config_exclusions"] #rule.value.preconfigured_waf_config_exclusions + content { + dynamic "exclusion" { + for_each = rule.value.preconfigured_waf_config_exclusions + content { + target_rule_set = exclusion.value.target_rule_set + target_rule_ids = exclusion.value.target_rule_ids + dynamic "request_header" { + for_each = exclusion.value.request_header == null ? {} : { for x in exclusion.value.request_header : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_header.value.operator + value = request_header.value.operator == "EQUALS_ANY" ? null : request_header.value.value + } + } + dynamic "request_cookie" { + for_each = exclusion.value.request_cookie == null ? {} : { for x in exclusion.value.request_cookie : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_cookie.value.operator + value = request_cookie.value.operator == "EQUALS_ANY" ? null : request_cookie.value.value + } + } + dynamic "request_uri" { + for_each = exclusion.value.request_uri == null ? {} : { for x in exclusion.value.request_uri : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_uri.value.operator + value = request_uri.value.operator == "EQUALS_ANY" ? null : request_uri.value.value + } + } + dynamic "request_query_param" { + for_each = exclusion.value.request_query_param == null ? {} : { for x in exclusion.value.request_query_param : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_query_param.value.operator + value = request_query_param.value.operator == "EQUALS_ANY" ? null : request_query_param.value.value + } + } + } + } + + } + } + + } + } + + + ##### Security Rules IP + + dynamic "rule" { + for_each = var.security_rules + content { + action = rule.value["action"] + priority = rule.value["priority"] + preview = rule.value["preview"] + description = rule.value["description"] + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = rule.value["src_ip_ranges"] + } + } + + # Header Action Block. Only if header_action is provided + dynamic "header_action" { + for_each = length(rule.value["header_action"]) == 0 ? [] : ["header_action"] + content { + dynamic "request_headers_to_adds" { + for_each = { for x in rule.value["header_action"] : x.header_name => x } + content { + header_name = request_headers_to_adds.value.header_name + header_value = request_headers_to_adds.value.header_value + } + } + } + } + + ### Redirect option. Execute only if Action is "redirect" + dynamic "redirect_options" { + for_each = rule.value["action"] == "redirect" ? ["redirect"] : [] + content { + type = rule.value["redirect_type"] + target = rule.value["redirect_type"] == "EXTERNAL_302" ? rule.value["redirect_target"] : null + } + } + + ### Rate limit. Execute only if Action is "rate_based_ban" or "throttle" + dynamic "rate_limit_options" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limits"] : [] + content { + conform_action = "allow" + ban_duration_sec = rule.value["action"] == "rate_based_ban" ? lookup(rule.value["rate_limit_options"], "ban_duration_sec") : null + exceed_action = lookup(rule.value["rate_limit_options"], "exceed_action") + enforce_on_key = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key", null) : "" + enforce_on_key_name = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key_name", null) : null + + dynamic "enforce_on_key_configs" { + for_each = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? {} : { for x in lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") : x.enforce_on_key_type => x } + content { + enforce_on_key_type = enforce_on_key_configs.value.enforce_on_key_type + enforce_on_key_name = enforce_on_key_configs.value.enforce_on_key_name + } + } + + ## Required for all rate limit options + dynamic "rate_limit_threshold" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limit_options"] : [] + content { + count = rule.value["rate_limit_options"].rate_limit_http_request_count + interval_sec = rule.value["rate_limit_options"].rate_limit_http_request_interval_sec + } + } + + ## Optional. Can be provided for for rate based ban. Not needed for throttle + dynamic "ban_threshold" { + for_each = rule.value["action"] == "rate_based_ban" && lookup(rule.value["rate_limit_options"], "ban_http_request_count", null) != null && lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec", null) != null ? ["ban_threshold"] : [] + content { + count = lookup(rule.value["rate_limit_options"], "ban_http_request_count") + interval_sec = lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec") + } + } + } + } + + } + } + + ##### Custom Rules + + dynamic "rule" { + for_each = var.custom_rules + content { + action = rule.value["action"] + priority = rule.value["priority"] + preview = rule.value["preview"] + description = rule.value["description"] + + match { + expr { + expression = rule.value["expression"] + } + } + + # Header Action Block. Only if header_action is provided + dynamic "header_action" { + for_each = length(rule.value["header_action"]) == 0 ? [] : ["header_action"] + content { + dynamic "request_headers_to_adds" { + for_each = { for x in rule.value["header_action"] : x.header_name => x } + content { + header_name = request_headers_to_adds.value.header_name + header_value = request_headers_to_adds.value.header_value + } + } + } + } + + # Redirect option block + dynamic "redirect_options" { + for_each = rule.value["action"] == "redirect" ? ["redirect"] : [] + content { + type = rule.value["redirect_type"] + target = rule.value["redirect_type"] == "EXTERNAL_302" ? rule.value["redirect_target"] : null + } + } + + ### Rate limit. Execute only if Action is "rate_based_ban" or "throttle" + dynamic "rate_limit_options" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limits"] : [] + content { + conform_action = "allow" + ban_duration_sec = rule.value["action"] == "rate_based_ban" ? lookup(rule.value["rate_limit_options"], "ban_duration_sec") : null + exceed_action = lookup(rule.value["rate_limit_options"], "exceed_action") + enforce_on_key = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key", null) : "" + enforce_on_key_name = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key_name", null) : null + + dynamic "enforce_on_key_configs" { + for_each = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? {} : { for x in lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") : x.enforce_on_key_type => x } + content { + enforce_on_key_type = enforce_on_key_configs.value.enforce_on_key_type + enforce_on_key_name = enforce_on_key_configs.value.enforce_on_key_name + } + } + + ## Required for all rate limit options + dynamic "rate_limit_threshold" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limit_options"] : [] + content { + count = rule.value["rate_limit_options"].rate_limit_http_request_count + interval_sec = rule.value["rate_limit_options"].rate_limit_http_request_interval_sec + } + } + + ## Optional. Can be provided for for rate based ban. Not needed for throttle + dynamic "ban_threshold" { + for_each = rule.value["action"] == "rate_based_ban" && lookup(rule.value["rate_limit_options"], "ban_http_request_count", null) != null && lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec", null) != null ? ["ban_threshold"] : [] + content { + count = lookup(rule.value["rate_limit_options"], "ban_http_request_count") + interval_sec = lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec") + } + } + } + } + + # Optional preconfigured_waf_config Block if preconfigured_waf_config_exclusion is provided + dynamic "preconfigured_waf_config" { + for_each = rule.value.preconfigured_waf_config_exclusion.target_rule_set == null ? [] : ["preconfigured_waf_config_exclusion"] + content { + exclusion { + target_rule_set = rule.value.preconfigured_waf_config_exclusion.target_rule_set + target_rule_ids = rule.value.preconfigured_waf_config_exclusion.target_rule_ids + dynamic "request_header" { + for_each = rule.value.preconfigured_waf_config_exclusion.request_header == null ? {} : { for x in rule.value.preconfigured_waf_config_exclusion.request_header : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_header.value.operator + value = request_header.value.operator == "EQUALS_ANY" ? null : request_header.value.value + } + } + dynamic "request_cookie" { + for_each = rule.value.preconfigured_waf_config_exclusion.request_cookie == null ? {} : { for x in rule.value.preconfigured_waf_config_exclusion.request_cookie : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_cookie.value.operator + value = request_cookie.value.operator == "EQUALS_ANY" ? null : request_cookie.value.value + } + } + dynamic "request_uri" { + for_each = rule.value.preconfigured_waf_config_exclusion.request_uri == null ? {} : { for x in rule.value.preconfigured_waf_config_exclusion.request_uri : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_uri.value.operator + value = request_uri.value.operator == "EQUALS_ANY" ? null : request_uri.value.value + } + } + dynamic "request_query_param" { + for_each = rule.value.preconfigured_waf_config_exclusion.request_query_param == null ? {} : { for x in rule.value.preconfigured_waf_config_exclusion.request_query_param : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_query_param.value.operator + value = request_query_param.value.operator == "EQUALS_ANY" ? null : request_query_param.value.value + } + } + } + } + } + + # Optional preconfigured_waf_config Block if preconfigured_waf_config_exclusion is provided + dynamic "preconfigured_waf_config" { + for_each = rule.value.preconfigured_waf_config_exclusions == null ? [] : ["preconfigured_waf_config_exclusions"] #rule.value.preconfigured_waf_config_exclusions + content { + dynamic "exclusion" { + for_each = rule.value.preconfigured_waf_config_exclusions + content { + target_rule_set = exclusion.value.target_rule_set + target_rule_ids = exclusion.value.target_rule_ids + dynamic "request_header" { + for_each = exclusion.value.request_header == null ? {} : { for x in exclusion.value.request_header : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_header.value.operator + value = request_header.value.operator == "EQUALS_ANY" ? null : request_header.value.value + } + } + dynamic "request_cookie" { + for_each = exclusion.value.request_cookie == null ? {} : { for x in exclusion.value.request_cookie : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_cookie.value.operator + value = request_cookie.value.operator == "EQUALS_ANY" ? null : request_cookie.value.value + } + } + dynamic "request_uri" { + for_each = exclusion.value.request_uri == null ? {} : { for x in exclusion.value.request_uri : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_uri.value.operator + value = request_uri.value.operator == "EQUALS_ANY" ? null : request_uri.value.value + } + } + dynamic "request_query_param" { + for_each = exclusion.value.request_query_param == null ? {} : { for x in exclusion.value.request_query_param : "${x.operator}-${base64encode(coalesce(x.value, "test"))}" => x } + content { + operator = request_query_param.value.operator + value = request_query_param.value.operator == "EQUALS_ANY" ? null : request_query_param.value.value + } + } + } + } + + } + } + + + } + } + + ##### Threat Intelligence Rules + + dynamic "rule" { + for_each = var.threat_intelligence_rules + content { + action = rule.value["action"] + priority = rule.value["priority"] + preview = rule.value["preview"] + description = rule.value["description"] + + match { + expr { + expression = lookup(rule.value, "exclude_ip", null) == null ? "evaluateThreatIntelligence('${rule.value["feed"]}')" : "evaluateThreatIntelligence('${rule.value["feed"]}', ${rule.value["exclude_ip"]})" + } + } + + # Header Action Block. Only if header_action is provided + dynamic "header_action" { + for_each = length(rule.value["header_action"]) == 0 ? [] : ["header_action"] + content { + dynamic "request_headers_to_adds" { + for_each = { for x in rule.value["header_action"] : x.header_name => x } + content { + header_name = request_headers_to_adds.value.header_name + header_value = request_headers_to_adds.value.header_value + } + } + } + } + + ### Rate limit. Execute only if Action is "rate_based_ban" or "throttle" + dynamic "rate_limit_options" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limits"] : [] + content { + conform_action = "allow" + ban_duration_sec = rule.value["action"] == "rate_based_ban" ? lookup(rule.value["rate_limit_options"], "ban_duration_sec") : null + exceed_action = lookup(rule.value["rate_limit_options"], "exceed_action") + enforce_on_key = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key", null) : null + enforce_on_key_name = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key_name", null) : null + + dynamic "enforce_on_key_configs" { + for_each = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? {} : { for x in lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") : x.enforce_on_key_type => x } + content { + enforce_on_key_type = enforce_on_key_configs.value.enforce_on_key_type + enforce_on_key_name = enforce_on_key_configs.value.enforce_on_key_name + } + } + + ## Required for all rate limit options + dynamic "rate_limit_threshold" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limit_options"] : [] + content { + count = rule.value["rate_limit_options"].rate_limit_http_request_count + interval_sec = rule.value["rate_limit_options"].rate_limit_http_request_interval_sec + } + } + + ## Optional. Can be provided for for rate based ban. Not needed for throttle + dynamic "ban_threshold" { + for_each = rule.value["action"] == "rate_based_ban" && lookup(rule.value["rate_limit_options"], "ban_http_request_count", null) != null && lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec", null) != null ? ["ban_threshold"] : [] + content { + count = lookup(rule.value["rate_limit_options"], "ban_http_request_count") + interval_sec = lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec") + } + } + } + } + } + } + + + ##### Default Rule + rule { + action = var.default_rule_action + priority = "2147483647" + description = "Default rule, higher priority overrides it" + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = ["*"] + } + } + } + + # Cloud Armor Adaptive Protection is currently not supported for edge or network policies + dynamic "adaptive_protection_config" { + for_each = var.layer_7_ddos_defense_enable && var.type != "CLOUD_ARMOR_EDGE" ? ["adaptive_protection_config"] : [] + content { + layer_7_ddos_defense_config { + enable = var.layer_7_ddos_defense_enable + rule_visibility = var.layer_7_ddos_defense_rule_visibility + } + dynamic "auto_deploy_config" { + for_each = var.adaptive_protection_auto_deploy.enable ? { auto_deploy = var.adaptive_protection_auto_deploy } : {} + content { + load_threshold = auto_deploy_config.value["load_threshold"] + confidence_threshold = auto_deploy_config.value["confidence_threshold"] + impacted_baseline_threshold = auto_deploy_config.value["impacted_baseline_threshold"] + expiration_sec = auto_deploy_config.value["expiration_sec"] + } + + } + } + } + + ##### Automatic Adaptive protection rule deploy + + dynamic "rule" { + for_each = var.layer_7_ddos_defense_enable && var.adaptive_protection_auto_deploy.enable && var.type != "CLOUD_ARMOR_EDGE" ? { auto_deploy = var.adaptive_protection_auto_deploy } : {} + content { + action = rule.value["action"] + priority = rule.value["priority"] + preview = rule.value["preview"] + description = rule.value["description"] + + match { + expr { + expression = "evaluateAdaptiveProtectionAutoDeploy()" + } + } + + # Redirect option block + dynamic "redirect_options" { + for_each = rule.value["action"] == "redirect" ? ["redirect"] : [] + content { + type = rule.value["redirect_type"] + target = rule.value["redirect_type"] == "EXTERNAL_302" ? rule.value["redirect_target"] : null + } + } + + ### Rate limit. Execute only if Action is "rate_based_ban" or "throttle" + dynamic "rate_limit_options" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limits"] : [] + content { + conform_action = "allow" + ban_duration_sec = rule.value["action"] == "rate_based_ban" ? lookup(rule.value["rate_limit_options"], "ban_duration_sec") : null + exceed_action = lookup(rule.value["rate_limit_options"], "exceed_action") + enforce_on_key = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key", null) : "" + enforce_on_key_name = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? lookup(rule.value["rate_limit_options"], "enforce_on_key_name", null) : null + + dynamic "enforce_on_key_configs" { + for_each = lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") == null ? {} : { for x in lookup(rule.value["rate_limit_options"], "enforce_on_key_configs") : x.enforce_on_key_type => x } + content { + enforce_on_key_type = enforce_on_key_configs.value.enforce_on_key_type + enforce_on_key_name = enforce_on_key_configs.value.enforce_on_key_name + } + } + + ## Required for all rate limit options + dynamic "rate_limit_threshold" { + for_each = rule.value["action"] == "rate_based_ban" || rule.value["action"] == "throttle" ? ["rate_limit_options"] : [] + content { + count = rule.value["rate_limit_options"].rate_limit_http_request_count + interval_sec = rule.value["rate_limit_options"].rate_limit_http_request_interval_sec + } + } + + ## Optional. Can be provided for for rate based ban. Not needed for throttle + dynamic "ban_threshold" { + for_each = rule.value["action"] == "rate_based_ban" && lookup(rule.value["rate_limit_options"], "ban_http_request_count", null) != null && lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec", null) != null ? ["ban_threshold"] : [] + content { + count = lookup(rule.value["rate_limit_options"], "ban_http_request_count") + interval_sec = lookup(rule.value["rate_limit_options"], "ban_http_request_interval_sec") + } + } + } + } + + } + } + +} diff --git a/infra/tf/modules/google-cloud-armor/outputs.tf b/infra/tf/modules/google-cloud-armor/outputs.tf new file mode 100644 index 000000000..5ecd7b237 --- /dev/null +++ b/infra/tf/modules/google-cloud-armor/outputs.tf @@ -0,0 +1,20 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "policy" { + description = "Security policy created" + value = google_compute_security_policy.policy +} diff --git a/infra/tf/modules/google-cloud-armor/variables.tf b/infra/tf/modules/google-cloud-armor/variables.tf new file mode 100644 index 000000000..55f7d216b --- /dev/null +++ b/infra/tf/modules/google-cloud-armor/variables.tf @@ -0,0 +1,345 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +variable "project_id" { + description = "The project in which the resource belongs." + type = string +} + +variable "name" { + description = "Name of the security policy." + type = string +} + +variable "description" { + description = "An optional description of this security policy. Max size is 2048." + type = string + default = null +} + +variable "default_rule_action" { + description = "default rule that allows/denies all traffic with the lowest priority (2,147,483,647)." + type = string + default = "allow" +} + +variable "recaptcha_redirect_site_key" { + description = "reCAPTCHA site key to be used for all the rules using the redirect action with the redirect type of GOOGLE_RECAPTCHA." + type = string + default = null +} + +variable "pre_configured_rules" { + description = "Map of pre-configured rules with Sensitivity levels. preconfigured_waf_config_exclusion is obsolete and available for backward compatibility. Use preconfigured_waf_config_exclusions which allows multiple exclusions" + type = map(object({ + action = string + priority = number + description = optional(string) + preview = optional(bool, false) + redirect_type = optional(string, null) + redirect_target = optional(string, null) + target_rule_set = string + sensitivity_level = optional(number, 4) + include_target_rule_ids = optional(list(string), []) + exclude_target_rule_ids = optional(list(string), []) + rate_limit_options = optional(object({ + enforce_on_key = optional(string) + enforce_on_key_name = optional(string) + enforce_on_key_configs = optional(list(object({ + enforce_on_key_name = optional(string) + enforce_on_key_type = optional(string) + }))) + exceed_action = optional(string) + rate_limit_http_request_count = optional(number) + rate_limit_http_request_interval_sec = optional(number) + ban_duration_sec = optional(number) + ban_http_request_count = optional(number) + ban_http_request_interval_sec = optional(number) + }), {}) + + header_action = optional(list(object({ + header_name = optional(string) + header_value = optional(string) + })), []) + + preconfigured_waf_config_exclusion = optional(object({ + target_rule_set = string + target_rule_ids = optional(list(string), []) + request_header = optional(list(object({ + operator = string + value = optional(string) + }))) + request_cookie = optional(list(object({ + operator = string + value = optional(string) + }))) + request_uri = optional(list(object({ + operator = string + value = optional(string) + }))) + request_query_param = optional(list(object({ + operator = string + value = optional(string) + }))) + }), { target_rule_set = null }) # Obsolete. Use preconfigured_waf_config_exclusions + + preconfigured_waf_config_exclusions = optional(map(object({ + target_rule_set = string + target_rule_ids = optional(list(string), []) + request_header = optional(list(object({ + operator = string + value = optional(string) + }))) + request_cookie = optional(list(object({ + operator = string + value = optional(string) + }))) + request_uri = optional(list(object({ + operator = string + value = optional(string) + }))) + request_query_param = optional(list(object({ + operator = string + value = optional(string) + }))) + })), null) + + })) + + default = {} +} + +variable "security_rules" { + description = "Map of Security rules with list of IP addresses to block or unblock." + type = map(object({ + action = string + priority = number + description = optional(string) + preview = optional(bool, false) + redirect_type = optional(string, null) + redirect_target = optional(string, null) + src_ip_ranges = list(string) + rate_limit_options = optional(object({ + enforce_on_key = optional(string) + enforce_on_key_name = optional(string) + enforce_on_key_configs = optional(list(object({ + enforce_on_key_name = optional(string) + enforce_on_key_type = optional(string) + }))) + exceed_action = optional(string) + rate_limit_http_request_count = optional(number) + rate_limit_http_request_interval_sec = optional(number) + ban_duration_sec = optional(number) + ban_http_request_count = optional(number) + ban_http_request_interval_sec = optional(number) + }), + {}) + header_action = optional(list(object({ + header_name = optional(string) + header_value = optional(string) + })), []) + })) + + default = {} +} + +variable "custom_rules" { + description = "Custome security rules" + type = map(object({ + action = string + priority = number + description = optional(string) + preview = optional(bool, false) + expression = string + redirect_type = optional(string, null) + redirect_target = optional(string, null) + rate_limit_options = optional(object({ + enforce_on_key = optional(string) + enforce_on_key_name = optional(string) + enforce_on_key_configs = optional(list(object({ + enforce_on_key_name = optional(string) + enforce_on_key_type = optional(string) + }))) + exceed_action = optional(string) + rate_limit_http_request_count = optional(number) + rate_limit_http_request_interval_sec = optional(number) + ban_duration_sec = optional(number) + ban_http_request_count = optional(number) + ban_http_request_interval_sec = optional(number) + }), + {}) + header_action = optional(list(object({ + header_name = optional(string) + header_value = optional(string) + })), []) + + preconfigured_waf_config_exclusion = optional(object({ + target_rule_set = string + target_rule_ids = optional(list(string), []) + request_header = optional(list(object({ + operator = string + value = optional(string) + }))) + request_cookie = optional(list(object({ + operator = string + value = optional(string) + }))) + request_uri = optional(list(object({ + operator = string + value = optional(string) + }))) + request_query_param = optional(list(object({ + operator = string + value = optional(string) + }))) + }), { target_rule_set = null }) # Obsolete. Use preconfigured_waf_config_exclusions + + preconfigured_waf_config_exclusions = optional(map(object({ + target_rule_set = string + target_rule_ids = optional(list(string), []) + request_header = optional(list(object({ + operator = string + value = optional(string) + }))) + request_cookie = optional(list(object({ + operator = string + value = optional(string) + }))) + request_uri = optional(list(object({ + operator = string + value = optional(string) + }))) + request_query_param = optional(list(object({ + operator = string + value = optional(string) + }))) + })), null) + + })) + default = {} +} + +variable "threat_intelligence_rules" { + description = "Map of Threat Intelligence Feed rules" + type = map(object({ + action = string + priority = number + description = optional(string) + preview = optional(bool, false) + feed = string + exclude_ip = optional(string) + rate_limit_options = optional(object({ + enforce_on_key = optional(string) + enforce_on_key_name = optional(string) + enforce_on_key_configs = optional(list(object({ + enforce_on_key_name = optional(string) + enforce_on_key_type = optional(string) + }))) + exceed_action = optional(string) + rate_limit_http_request_count = optional(number) + rate_limit_http_request_interval_sec = optional(number) + ban_duration_sec = optional(number) + ban_http_request_count = optional(number) + ban_http_request_interval_sec = optional(number) + }), + {}) + header_action = optional(list(object({ + header_name = optional(string) + header_value = optional(string) + })), []) + })) + default = {} +} + +variable "type" { + description = "Type indicates the intended use of the security policy. Possible values are CLOUD_ARMOR and CLOUD_ARMOR_EDGE." + type = string + default = "CLOUD_ARMOR" +} + +variable "layer_7_ddos_defense_enable" { + description = "(Optional) If set to true, enables Cloud Armor Adaptive Protection for L7 DDoS detection. Cloud Armor Adaptive Protection is only supported in Global Security Policies of type CLOUD_ARMOR. Set this variable `true` for Adaptive Protection Auto Deploy." + type = bool + default = false +} + +variable "layer_7_ddos_defense_rule_visibility" { + description = "(Optional) Rule visibility can be one of the following: STANDARD - opaque rules. PREMIUM - transparent rules. This field is only supported in Global Security Policies of type CLOUD_ARMOR." + type = string + default = "STANDARD" +} + +variable "adaptive_protection_auto_deploy" { + description = "Configuration for Automatically deploy Cloud Armor Adaptive Protection suggested rules. `priority` and `action` fields are required if `enable` is set to true. Requires `layer_7_ddos_defense_enable` set to `true`." + type = object({ + enable = bool + priority = optional(number, null) + action = optional(string, null) + preview = optional(bool, false) + description = optional(string, "Adaptive Protection auto-deploy") + load_threshold = optional(number) + confidence_threshold = optional(number) + impacted_baseline_threshold = optional(number) + expiration_sec = optional(number) + redirect_type = optional(string) + redirect_target = optional(string) + + rate_limit_options = optional(object({ + enforce_on_key = optional(string) + enforce_on_key_name = optional(string) + + enforce_on_key_configs = optional(list(object({ + enforce_on_key_name = optional(string) + enforce_on_key_type = optional(string) + }))) + + exceed_action = optional(string) + rate_limit_http_request_count = optional(number) + rate_limit_http_request_interval_sec = optional(number) + ban_duration_sec = optional(number) + ban_http_request_count = optional(number) + ban_http_request_interval_sec = optional(number) + }), {}) + }) + + default = { + enable = false + } +} + +variable "json_parsing" { + description = "Whether or not to JSON parse the payload body. Possible values are DISABLED and STANDARD. Not supported for CLOUD_ARMOR_EDGE policy type." + type = string + default = "DISABLED" +} + +variable "log_level" { + description = "Log level to use. Possible values are NORMAL and VERBOSE. Not supported for CLOUD_ARMOR_EDGE policy type." + type = string + default = "NORMAL" +} + +variable "json_custom_config_content_types" { + description = "A list of custom Content-Type header values to apply the JSON parsing. Only applicable when json_parsing is set to STANDARD. Not supported for CLOUD_ARMOR_EDGE policy type." + type = list(string) + default = [] +} + +variable "user_ip_request_headers" { + description = "An optional list of case-insensitive request header names to use for resolving the callers client IP address." + type = list(string) + default = [] +} diff --git a/infra/tf/modules/google-cloud-armor/versions.tf b/infra/tf/modules/google-cloud-armor/versions.tf new file mode 100644 index 000000000..296820ee1 --- /dev/null +++ b/infra/tf/modules/google-cloud-armor/versions.tf @@ -0,0 +1,35 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +terraform { + required_version = ">= 1.3.0" + required_providers { + google = { + source = "hashicorp/google" + version = ">= 4.79" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 4.79" + } + } + provider_meta "google" { + module_name = "blueprints/terraform/terraform-google-cloud-armor/v2.2.0" + } + provider_meta "google-beta" { + module_name = "blueprints/terraform/terraform-google-cloud-armor/v2.2.0" + } +} diff --git a/infra/tf/variables.tf b/infra/tf/variables.tf index 331faa89f..b7fe738db 100644 --- a/infra/tf/variables.tf +++ b/infra/tf/variables.tf @@ -17,11 +17,12 @@ variable "region" { variable "apps" { description = "(Optional) The configuration of the apps nodes" type = object({ - disk_size = optional(number, 256) - instance_type = optional(string, "e2-standard-2") - provisioning_model = optional(string, "STANDARD") - generate_external_ip = optional(bool, false) - detach_load_balancer = optional(bool, false) + disk_size = optional(number, 256) + instance_type = optional(string, "e2-standard-2") + provisioning_model = optional(string, "STANDARD") + generate_external_ip = optional(bool, false) + detach_load_balancer = optional(bool, false) + faucet_max_hourly_requests = optional(number, 1000000) nodes = list(object({ count = number region = optional(string)