From c9b0f19172aa544d7890a42b05c12ddc93baee09 Mon Sep 17 00:00:00 2001 From: DimitriKirchner Date: Mon, 1 Jun 2026 16:29:33 -0400 Subject: [PATCH 1/5] feat(google_fastly_waf): add ddos notification for slack --- google_fastly_waf/README.md | 2 ++ google_fastly_waf/alerts.tf | 44 ++++++++++++++++++++++++++++++++++ google_fastly_waf/variables.tf | 23 ++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 google_fastly_waf/alerts.tf diff --git a/google_fastly_waf/README.md b/google_fastly_waf/README.md index 9da31bb5..307d2ef6 100644 --- a/google_fastly_waf/README.md +++ b/google_fastly_waf/README.md @@ -148,8 +148,10 @@ module "fastly_stage" { | [cache\_settings](#input\_cache\_settings) | List of cache settings for the Fastly service. |
list(object({
name = string
action = optional(string)
cache_condition = optional(string)
stale_ttl = optional(number)
ttl = optional(number)
}))
| `[]` | no | | [conditions](#input\_conditions) | List of Fastly conditions to create (REQUEST, RESPONSE or CACHE). |
list(object({
name = string # required, unique
statement = string # VCL conditional expression
type = string # one of: REQUEST, RESPONSE, CACHE
priority = optional(number) # lower runs first, default 10
}))
| `[]` | no | | [ddos\_protection](#input\_ddos\_protection) | Optional DDoS Protection configuration for the Fastly service product enablement. |
object({
enabled = bool
mode = string
})
| `null` | no | +| [ddos\_protection\_alert](#input\_ddos\_protection\_alert) | Optional Slack alerting for Fastly DDoS Protection. When set, the module creates a Slack `fastly_integration` and a `fastly_alert` on the `ddos_detected_requests` stats metric that notifies the channel behind the webhook. Intended to be paired with `ddos_protection` being enabled. Set to `null` (the default) to create no alerting resources. |
object({
enabled = optional(bool, true)
slack_webhook = string
threshold = optional(number, 1)
period = optional(string, "5m")
})
| `null` | no | | [domains](#input\_domains) | A list of domains | `list(any)` | `[]` | no | | [https\_redirect\_enabled](#input\_https\_redirect\_enabled) | n/a | `bool` | `true` | no | +| [legacy\_edge\_deployment](#input\_legacy\_edge\_deployment) | If true (default), deploy NGWAF via the legacy sigsci EdgeDeployment APIs and Fastly dynamic snippets. If false, deploy via Fastly's product\_enablement ngwaf block. Default preserves behavior for services still on the legacy method. | `bool` | `true` | no | | [log\_sampling\_enabled](#input\_log\_sampling\_enabled) | n/a | `bool` | `false` | no | | [log\_sampling\_percent](#input\_log\_sampling\_percent) | n/a | `string` | `"10"` | no | | [ngwaf\_agent\_level](#input\_ngwaf\_agent\_level) | This is the site wide blocking level | `string` | `"log"` | no | diff --git a/google_fastly_waf/alerts.tf b/google_fastly_waf/alerts.tf new file mode 100644 index 00000000..8972d531 --- /dev/null +++ b/google_fastly_waf/alerts.tf @@ -0,0 +1,44 @@ +# DDoS Protection alerting +# +# When var.ddos_protection_alert is set, create a Slack integration and a +# Fastly stats alert that fires on the `ddos_detected_requests` metric. This is +# only meaningful when ddos_protection is enabled (mode "log" or "block"). +# +# count keys off the non-sensitive `enabled` field so it can be computed without +# tainting the value as sensitive; the webhook itself is wrapped in sensitive() +# below to keep it out of plan output. + +locals { + ddos_protection_alert_enabled = ( + var.ddos_protection_alert != null && var.ddos_protection_alert.enabled + ) +} + +resource "fastly_integration" "ddos_protection_slack" { + count = local.ddos_protection_alert_enabled ? 1 : 0 + + name = "${var.application}-${var.realm}-${var.environment} DDoS Protection Slack integration" + description = "Slack notifications for Fastly DDoS Protection detection events" + type = "slack" + + config = { + webhook = sensitive(var.ddos_protection_alert.slack_webhook) + } +} + +resource "fastly_alert" "ddos_protection" { + count = local.ddos_protection_alert_enabled ? 1 : 0 + + name = "${var.application}-${var.realm}-${var.environment} DDoS Protection events" + service_id = fastly_service_vcl.default.id + source = "stats" + metric = "ddos_detected_requests" + + evaluation_strategy { + type = "above_threshold" + period = var.ddos_protection_alert.period + threshold = var.ddos_protection_alert.threshold + } + + integration_ids = [fastly_integration.ddos_protection_slack[0].id] +} diff --git a/google_fastly_waf/variables.tf b/google_fastly_waf/variables.tf index 725d095e..16c00c7b 100644 --- a/google_fastly_waf/variables.tf +++ b/google_fastly_waf/variables.tf @@ -143,6 +143,29 @@ variable "ddos_protection" { } } +variable "ddos_protection_alert" { + description = <<-EOT + Optional Slack alerting for Fastly DDoS Protection. When set, the module + creates a Slack `fastly_integration` and a `fastly_alert` on the + `ddos_detected_requests` stats metric that notifies the channel behind the + webhook. Intended to be paired with `ddos_protection` being enabled. + Set to `null` (the default) to create no alerting resources. + EOT + type = object({ + enabled = optional(bool, true) # set false to keep config but tear the alert down + slack_webhook = string # Slack incoming-webhook URL (sensitive) + threshold = optional(number, 1) # ddos_detected_requests count that fires the alert + period = optional(string, "5m") # evaluation window: 2m, 3m, 5m, 15m, or 30m + }) + default = null + validation { + condition = var.ddos_protection_alert == null || contains( + ["2m", "3m", "5m", "15m", "30m"], var.ddos_protection_alert.period + ) + error_message = "ddos_protection_alert.period must be one of: 2m, 3m, 5m, 15m, or 30m." + } +} + ## NGWAF variable "legacy_edge_deployment" { type = bool From 64c8eb846ab17999ab84c5dcaaa3c394efd5b421 Mon Sep 17 00:00:00 2001 From: DimitriKirchner Date: Tue, 2 Jun 2026 10:03:55 -0400 Subject: [PATCH 2/5] Update to use secret --- google_fastly_waf/README.md | 2 +- google_fastly_waf/alerts.tf | 2 +- google_fastly_waf/variables.tf | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/google_fastly_waf/README.md b/google_fastly_waf/README.md index 307d2ef6..998fb588 100644 --- a/google_fastly_waf/README.md +++ b/google_fastly_waf/README.md @@ -148,7 +148,7 @@ module "fastly_stage" { | [cache\_settings](#input\_cache\_settings) | List of cache settings for the Fastly service. |
list(object({
name = string
action = optional(string)
cache_condition = optional(string)
stale_ttl = optional(number)
ttl = optional(number)
}))
| `[]` | no | | [conditions](#input\_conditions) | List of Fastly conditions to create (REQUEST, RESPONSE or CACHE). |
list(object({
name = string # required, unique
statement = string # VCL conditional expression
type = string # one of: REQUEST, RESPONSE, CACHE
priority = optional(number) # lower runs first, default 10
}))
| `[]` | no | | [ddos\_protection](#input\_ddos\_protection) | Optional DDoS Protection configuration for the Fastly service product enablement. |
object({
enabled = bool
mode = string
})
| `null` | no | -| [ddos\_protection\_alert](#input\_ddos\_protection\_alert) | Optional Slack alerting for Fastly DDoS Protection. When set, the module creates a Slack `fastly_integration` and a `fastly_alert` on the `ddos_detected_requests` stats metric that notifies the channel behind the webhook. Intended to be paired with `ddos_protection` being enabled. Set to `null` (the default) to create no alerting resources. |
object({
enabled = optional(bool, true)
slack_webhook = string
threshold = optional(number, 1)
period = optional(string, "5m")
})
| `null` | no | +| [ddos\_protection\_alert](#input\_ddos\_protection\_alert) | Optional Slack alerting for Fastly DDoS Protection. When set, the module creates a Slack `fastly_integration` and a `fastly_alert` on the `ddos_detected_requests` stats metric that notifies the channel behind the webhook. Intended to be paired with `ddos_protection` being enabled. Set to `null` (the default) to create no alerting resources. |
object({
enabled = optional(bool, true)
slack_webhook_secret = string
threshold = optional(number, 1)
period = optional(string, "5m")
})
| `null` | no | | [domains](#input\_domains) | A list of domains | `list(any)` | `[]` | no | | [https\_redirect\_enabled](#input\_https\_redirect\_enabled) | n/a | `bool` | `true` | no | | [legacy\_edge\_deployment](#input\_legacy\_edge\_deployment) | If true (default), deploy NGWAF via the legacy sigsci EdgeDeployment APIs and Fastly dynamic snippets. If false, deploy via Fastly's product\_enablement ngwaf block. Default preserves behavior for services still on the legacy method. | `bool` | `true` | no | diff --git a/google_fastly_waf/alerts.tf b/google_fastly_waf/alerts.tf index 8972d531..7b084be1 100644 --- a/google_fastly_waf/alerts.tf +++ b/google_fastly_waf/alerts.tf @@ -22,7 +22,7 @@ resource "fastly_integration" "ddos_protection_slack" { type = "slack" config = { - webhook = sensitive(var.ddos_protection_alert.slack_webhook) + webhook = sensitive(var.ddos_protection_alert.slack_webhook_secret) } } diff --git a/google_fastly_waf/variables.tf b/google_fastly_waf/variables.tf index 16c00c7b..96a7f810 100644 --- a/google_fastly_waf/variables.tf +++ b/google_fastly_waf/variables.tf @@ -152,10 +152,10 @@ variable "ddos_protection_alert" { Set to `null` (the default) to create no alerting resources. EOT type = object({ - enabled = optional(bool, true) # set false to keep config but tear the alert down - slack_webhook = string # Slack incoming-webhook URL (sensitive) - threshold = optional(number, 1) # ddos_detected_requests count that fires the alert - period = optional(string, "5m") # evaluation window: 2m, 3m, 5m, 15m, or 30m + enabled = optional(bool, true) # set false to keep config but tear the alert down + slack_webhook_secret = string # Slack incoming-webhook URL (sensitive) + threshold = optional(number, 1) # ddos_detected_requests count that fires the alert + period = optional(string, "5m") # evaluation window: 2m, 3m, 5m, 15m, or 30m }) default = null validation { From e50e47ff1deaabd729992fd78fc110dadfd39a0f Mon Sep 17 00:00:00 2001 From: DimitriKirchner Date: Tue, 2 Jun 2026 10:19:34 -0400 Subject: [PATCH 3/5] Couple slack alerting with ddos being enabled Slack alerting is now coupled to DDoS Protection being active --- google_fastly_waf/alerts.tf | 7 ++++--- google_fastly_waf/variables.tf | 18 +++++++++++++++--- google_fastly_waf/versions.tf | 4 ++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/google_fastly_waf/alerts.tf b/google_fastly_waf/alerts.tf index 7b084be1..ffe7b745 100644 --- a/google_fastly_waf/alerts.tf +++ b/google_fastly_waf/alerts.tf @@ -1,8 +1,9 @@ # DDoS Protection alerting # # When var.ddos_protection_alert is set, create a Slack integration and a -# Fastly stats alert that fires on the `ddos_detected_requests` metric. This is -# only meaningful when ddos_protection is enabled (mode "log" or "block"). +# Fastly stats alert that fires on the `ddos_protection_requests_detect_count` +# metric (requests classified as DDoS attacks). This is only meaningful when +# ddos_protection is enabled (mode "log" or "block"). # # count keys off the non-sensitive `enabled` field so it can be computed without # tainting the value as sensitive; the webhook itself is wrapped in sensitive() @@ -32,7 +33,7 @@ resource "fastly_alert" "ddos_protection" { name = "${var.application}-${var.realm}-${var.environment} DDoS Protection events" service_id = fastly_service_vcl.default.id source = "stats" - metric = "ddos_detected_requests" + metric = "ddos_protection_requests_detect_count" evaluation_strategy { type = "above_threshold" diff --git a/google_fastly_waf/variables.tf b/google_fastly_waf/variables.tf index 96a7f810..43290cf6 100644 --- a/google_fastly_waf/variables.tf +++ b/google_fastly_waf/variables.tf @@ -147,14 +147,15 @@ variable "ddos_protection_alert" { description = <<-EOT Optional Slack alerting for Fastly DDoS Protection. When set, the module creates a Slack `fastly_integration` and a `fastly_alert` on the - `ddos_detected_requests` stats metric that notifies the channel behind the - webhook. Intended to be paired with `ddos_protection` being enabled. + `ddos_protection_requests_detect_count` stats metric that notifies the + channel behind the webhook. Intended to be paired with `ddos_protection` + being enabled. Set to `null` (the default) to create no alerting resources. EOT type = object({ enabled = optional(bool, true) # set false to keep config but tear the alert down slack_webhook_secret = string # Slack incoming-webhook URL (sensitive) - threshold = optional(number, 1) # ddos_detected_requests count that fires the alert + threshold = optional(number, 1) # ddos_protection_requests_detect_count that fires the alert period = optional(string, "5m") # evaluation window: 2m, 3m, 5m, 15m, or 30m }) default = null @@ -164,6 +165,17 @@ variable "ddos_protection_alert" { ) error_message = "ddos_protection_alert.period must be one of: 2m, 3m, 5m, 15m, or 30m." } + validation { + # The detect-count metric is only populated when DDoS Protection is actively + # inspecting traffic, so alerting requires ddos_protection enabled in log or + # block mode. (Cross-variable validation requires Terraform >= 1.9.) + condition = var.ddos_protection_alert == null || ( + var.ddos_protection != null && + var.ddos_protection.enabled && + var.ddos_protection.mode != "off" + ) + error_message = "ddos_protection_alert requires ddos_protection to be enabled with mode \"log\" or \"block\"." + } } ## NGWAF diff --git a/google_fastly_waf/versions.tf b/google_fastly_waf/versions.tf index b7721a65..5c90a7fb 100644 --- a/google_fastly_waf/versions.tf +++ b/google_fastly_waf/versions.tf @@ -1,4 +1,8 @@ terraform { + # >= 1.9 is required for cross-variable references in validation blocks + # (see ddos_protection_alert in variables.tf). + required_version = ">= 1.9" + required_providers { fastly = { source = "fastly/fastly" From fd64f8edf128113b4d40599d915635c0e9b8bb6d Mon Sep 17 00:00:00 2001 From: DimitriKirchner Date: Tue, 2 Jun 2026 10:23:35 -0400 Subject: [PATCH 4/5] Update alerts.tf --- google_fastly_waf/alerts.tf | 5 ----- 1 file changed, 5 deletions(-) diff --git a/google_fastly_waf/alerts.tf b/google_fastly_waf/alerts.tf index ffe7b745..52e0a726 100644 --- a/google_fastly_waf/alerts.tf +++ b/google_fastly_waf/alerts.tf @@ -1,13 +1,8 @@ # DDoS Protection alerting -# # When var.ddos_protection_alert is set, create a Slack integration and a # Fastly stats alert that fires on the `ddos_protection_requests_detect_count` # metric (requests classified as DDoS attacks). This is only meaningful when # ddos_protection is enabled (mode "log" or "block"). -# -# count keys off the non-sensitive `enabled` field so it can be computed without -# tainting the value as sensitive; the webhook itself is wrapped in sensitive() -# below to keep it out of plan output. locals { ddos_protection_alert_enabled = ( From 2fcfa3fc4c27722faa694d48d2c11f9964902896 Mon Sep 17 00:00:00 2001 From: DimitriKirchner Date: Tue, 2 Jun 2026 10:24:58 -0400 Subject: [PATCH 5/5] Update README.md --- google_fastly_waf/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google_fastly_waf/README.md b/google_fastly_waf/README.md index 998fb588..acc93266 100644 --- a/google_fastly_waf/README.md +++ b/google_fastly_waf/README.md @@ -148,7 +148,7 @@ module "fastly_stage" { | [cache\_settings](#input\_cache\_settings) | List of cache settings for the Fastly service. |
list(object({
name = string
action = optional(string)
cache_condition = optional(string)
stale_ttl = optional(number)
ttl = optional(number)
}))
| `[]` | no | | [conditions](#input\_conditions) | List of Fastly conditions to create (REQUEST, RESPONSE or CACHE). |
list(object({
name = string # required, unique
statement = string # VCL conditional expression
type = string # one of: REQUEST, RESPONSE, CACHE
priority = optional(number) # lower runs first, default 10
}))
| `[]` | no | | [ddos\_protection](#input\_ddos\_protection) | Optional DDoS Protection configuration for the Fastly service product enablement. |
object({
enabled = bool
mode = string
})
| `null` | no | -| [ddos\_protection\_alert](#input\_ddos\_protection\_alert) | Optional Slack alerting for Fastly DDoS Protection. When set, the module creates a Slack `fastly_integration` and a `fastly_alert` on the `ddos_detected_requests` stats metric that notifies the channel behind the webhook. Intended to be paired with `ddos_protection` being enabled. Set to `null` (the default) to create no alerting resources. |
object({
enabled = optional(bool, true)
slack_webhook_secret = string
threshold = optional(number, 1)
period = optional(string, "5m")
})
| `null` | no | +| [ddos\_protection\_alert](#input\_ddos\_protection\_alert) | Optional Slack alerting for Fastly DDoS Protection. When set, the module creates a Slack `fastly_integration` and a `fastly_alert` on the `ddos_protection_requests_detect_count` stats metric that notifies the channel behind the webhook. Intended to be paired with `ddos_protection` being enabled. Set to `null` (the default) to create no alerting resources. |
object({
enabled = optional(bool, true)
slack_webhook_secret = string
threshold = optional(number, 1)
period = optional(string, "5m")
})
| `null` | no | | [domains](#input\_domains) | A list of domains | `list(any)` | `[]` | no | | [https\_redirect\_enabled](#input\_https\_redirect\_enabled) | n/a | `bool` | `true` | no | | [legacy\_edge\_deployment](#input\_legacy\_edge\_deployment) | If true (default), deploy NGWAF via the legacy sigsci EdgeDeployment APIs and Fastly dynamic snippets. If false, deploy via Fastly's product\_enablement ngwaf block. Default preserves behavior for services still on the legacy method. | `bool` | `true` | no |