diff --git a/fast/stages/0-org-setup/schemas/project.schema.json b/fast/stages/0-org-setup/schemas/project.schema.json
index 89b0aeaba..4f3b15610 100644
--- a/fast/stages/0-org-setup/schemas/project.schema.json
+++ b/fast/stages/0-org-setup/schemas/project.schema.json
@@ -481,6 +481,36 @@
"descriptive_name": {
"type": "string"
},
+ "dns_threat_detector": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "excluded_networks": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "labels": {
+ "type": "object"
+ },
+ "location": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "threat_detector_provider": {
+ "type": "string",
+ "enum": [
+ "INFOBLOX"
+ ]
+ }
+ }
+ },
"org_policies": {
"type": "object",
"additionalProperties": false,
diff --git a/fast/stages/0-org-setup/schemas/project.schema.md b/fast/stages/0-org-setup/schemas/project.schema.md
index d2cceab50..eadce0d3c 100644
--- a/fast/stages/0-org-setup/schemas/project.schema.md
+++ b/fast/stages/0-org-setup/schemas/project.schema.md
@@ -149,6 +149,16 @@
- items: *string*
- **name**: *string*
- **descriptive_name**: *string*
+- **dns_threat_detector**: *object*
+
*additional properties: false*
+ - **enabled**: *boolean*
+ - **excluded_networks**: *array*
+ - items: *string*
+ - **labels**: *object*
+ - **location**: *string*
+ - **name**: *string*
+ - **threat_detector_provider**: *string*
+
*enum: ['INFOBLOX']*
- **org_policies**: *object*
*additional properties: false*
- **`^[a-z]+\.`**: *object*
diff --git a/fast/stages/2-networking/schemas/project.schema.json b/fast/stages/2-networking/schemas/project.schema.json
index 89b0aeaba..4f3b15610 100644
--- a/fast/stages/2-networking/schemas/project.schema.json
+++ b/fast/stages/2-networking/schemas/project.schema.json
@@ -481,6 +481,36 @@
"descriptive_name": {
"type": "string"
},
+ "dns_threat_detector": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "excluded_networks": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "labels": {
+ "type": "object"
+ },
+ "location": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "threat_detector_provider": {
+ "type": "string",
+ "enum": [
+ "INFOBLOX"
+ ]
+ }
+ }
+ },
"org_policies": {
"type": "object",
"additionalProperties": false,
diff --git a/fast/stages/2-networking/schemas/project.schema.md b/fast/stages/2-networking/schemas/project.schema.md
index d2cceab50..eadce0d3c 100644
--- a/fast/stages/2-networking/schemas/project.schema.md
+++ b/fast/stages/2-networking/schemas/project.schema.md
@@ -149,6 +149,16 @@
- items: *string*
- **name**: *string*
- **descriptive_name**: *string*
+- **dns_threat_detector**: *object*
+
*additional properties: false*
+ - **enabled**: *boolean*
+ - **excluded_networks**: *array*
+ - items: *string*
+ - **labels**: *object*
+ - **location**: *string*
+ - **name**: *string*
+ - **threat_detector_provider**: *string*
+
*enum: ['INFOBLOX']*
- **org_policies**: *object*
*additional properties: false*
- **`^[a-z]+\.`**: *object*
diff --git a/fast/stages/2-project-factory/schemas/project.schema.json b/fast/stages/2-project-factory/schemas/project.schema.json
index 89b0aeaba..4f3b15610 100644
--- a/fast/stages/2-project-factory/schemas/project.schema.json
+++ b/fast/stages/2-project-factory/schemas/project.schema.json
@@ -481,6 +481,36 @@
"descriptive_name": {
"type": "string"
},
+ "dns_threat_detector": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "excluded_networks": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "labels": {
+ "type": "object"
+ },
+ "location": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "threat_detector_provider": {
+ "type": "string",
+ "enum": [
+ "INFOBLOX"
+ ]
+ }
+ }
+ },
"org_policies": {
"type": "object",
"additionalProperties": false,
diff --git a/fast/stages/2-project-factory/schemas/project.schema.md b/fast/stages/2-project-factory/schemas/project.schema.md
index d2cceab50..eadce0d3c 100644
--- a/fast/stages/2-project-factory/schemas/project.schema.md
+++ b/fast/stages/2-project-factory/schemas/project.schema.md
@@ -149,6 +149,16 @@
- items: *string*
- **name**: *string*
- **descriptive_name**: *string*
+- **dns_threat_detector**: *object*
+
*additional properties: false*
+ - **enabled**: *boolean*
+ - **excluded_networks**: *array*
+ - items: *string*
+ - **labels**: *object*
+ - **location**: *string*
+ - **name**: *string*
+ - **threat_detector_provider**: *string*
+
*enum: ['INFOBLOX']*
- **org_policies**: *object*
*additional properties: false*
- **`^[a-z]+\.`**: *object*
diff --git a/fast/stages/2-security/schemas/project.schema.json b/fast/stages/2-security/schemas/project.schema.json
index 89b0aeaba..4f3b15610 100644
--- a/fast/stages/2-security/schemas/project.schema.json
+++ b/fast/stages/2-security/schemas/project.schema.json
@@ -481,6 +481,36 @@
"descriptive_name": {
"type": "string"
},
+ "dns_threat_detector": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "excluded_networks": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "labels": {
+ "type": "object"
+ },
+ "location": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "threat_detector_provider": {
+ "type": "string",
+ "enum": [
+ "INFOBLOX"
+ ]
+ }
+ }
+ },
"org_policies": {
"type": "object",
"additionalProperties": false,
diff --git a/fast/stages/2-security/schemas/project.schema.md b/fast/stages/2-security/schemas/project.schema.md
index d2cceab50..eadce0d3c 100644
--- a/fast/stages/2-security/schemas/project.schema.md
+++ b/fast/stages/2-security/schemas/project.schema.md
@@ -149,6 +149,16 @@
- items: *string*
- **name**: *string*
- **descriptive_name**: *string*
+- **dns_threat_detector**: *object*
+
*additional properties: false*
+ - **enabled**: *boolean*
+ - **excluded_networks**: *array*
+ - items: *string*
+ - **labels**: *object*
+ - **location**: *string*
+ - **name**: *string*
+ - **threat_detector_provider**: *string*
+
*enum: ['INFOBLOX']*
- **org_policies**: *object*
*additional properties: false*
- **`^[a-z]+\.`**: *object*
diff --git a/modules/project-factory/README.md b/modules/project-factory/README.md
index 94512fd73..da99713d1 100644
--- a/modules/project-factory/README.md
+++ b/modules/project-factory/README.md
@@ -622,6 +622,8 @@ labels:
app: app-0
team: team-a
parent: $folder_ids:team-a/app-0
+dns_threat_detector:
+ enabled: true
iam_by_principals:
$iam_principals:service_accounts/dev-ta-app0-be/app-0-be:
- roles/storage.objectViewer
@@ -864,6 +866,7 @@ compute.disableSerialPortAccess:
| [projects-bigquery.tf](./projects-bigquery.tf) | None | bigquery-dataset | |
| [projects-buckets.tf](./projects-buckets.tf) | None | gcs | |
| [projects-defaults.tf](./projects-defaults.tf) | None | | |
+| [projects-dns-armor.tf](./projects-dns-armor.tf) | None | | google_network_security_dns_threat_detector |
| [projects-kms.tf](./projects-kms.tf) | None | kms | |
| [projects-log-buckets.tf](./projects-log-buckets.tf) | None | logging-bucket | |
| [projects-pubsub.tf](./projects-pubsub.tf) | None | pubsub | |
@@ -878,11 +881,11 @@ compute.disableSerialPortAccess:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [factories_config](variables.tf#L165) | Path to folder with YAML resource description data files. | object({…}) | ✓ | |
+| [factories_config](variables.tf#L166) | Path to folder with YAML resource description data files. | object({…}) | ✓ | |
| [context](variables.tf#L17) | Context-specific interpolations. | object({…}) | | {} |
-| [data_defaults](variables.tf#L42) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} |
-| [data_merges](variables.tf#L107) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} |
-| [data_overrides](variables.tf#L126) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} |
+| [data_defaults](variables.tf#L43) | Optional default values used when corresponding project or folder data from files are missing. | object({…}) | | {} |
+| [data_merges](variables.tf#L108) | Optional values that will be merged with corresponding data from files. Combines with `data_defaults`, file data, and `data_overrides`. | object({…}) | | {} |
+| [data_overrides](variables.tf#L127) | Optional values that override corresponding data from files. Takes precedence over file data and `data_defaults`. | object({…}) | | {} |
| [folders](variables-folders.tf#L17) | Folders data merged with factory data. | map(object({…})) | | {} |
| [notification_channels](variables-billing.tf#L17) | Notification channels used by budget alerts. | map(object({…})) | | {} |
| [projects](variables-projects.tf#L17) | Projects data merged with factory data. | map(object({…})) | | {} |
diff --git a/modules/project-factory/projects-dns-armor.tf b/modules/project-factory/projects-dns-armor.tf
new file mode 100644
index 000000000..7646054f5
--- /dev/null
+++ b/modules/project-factory/projects-dns-armor.tf
@@ -0,0 +1,40 @@
+/**
+ * Copyright 2026 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.
+ */
+
+resource "google_network_security_dns_threat_detector" "dns_threat_detector" {
+ provider = google-beta
+ for_each = {
+ for k, v in local.projects_input : k => v
+ if try(v.dns_threat_detector.enabled, false)
+ }
+ project = module.projects[each.key].project_id
+ name = (
+ try(each.value.dns_threat_detector.name, null) != null
+ ? each.value.dns_threat_detector.name
+ : (
+ each.value.prefix == null
+ ? each.value.name
+ : "${each.value.prefix}-${each.value.name}"
+ )
+ )
+ excluded_networks = [
+ for s in try(each.value.dns_threat_detector.excluded_networks, []) :
+ lookup(local.ctx.networks, s, s)
+ ]
+ threat_detector_provider = try(each.value.dns_threat_detector.threat_detector_provider, null)
+ labels = try(each.value.dns_threat_detector.labels, {})
+ location = try(each.value.dns_threat_detector.location, null)
+}
diff --git a/modules/project-factory/schemas/project.schema.json b/modules/project-factory/schemas/project.schema.json
index 89b0aeaba..4f3b15610 100644
--- a/modules/project-factory/schemas/project.schema.json
+++ b/modules/project-factory/schemas/project.schema.json
@@ -481,6 +481,36 @@
"descriptive_name": {
"type": "string"
},
+ "dns_threat_detector": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "excluded_networks": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "labels": {
+ "type": "object"
+ },
+ "location": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "threat_detector_provider": {
+ "type": "string",
+ "enum": [
+ "INFOBLOX"
+ ]
+ }
+ }
+ },
"org_policies": {
"type": "object",
"additionalProperties": false,
diff --git a/modules/project-factory/schemas/project.schema.md b/modules/project-factory/schemas/project.schema.md
index d2cceab50..eadce0d3c 100644
--- a/modules/project-factory/schemas/project.schema.md
+++ b/modules/project-factory/schemas/project.schema.md
@@ -149,6 +149,16 @@
- items: *string*
- **name**: *string*
- **descriptive_name**: *string*
+- **dns_threat_detector**: *object*
+
*additional properties: false*
+ - **enabled**: *boolean*
+ - **excluded_networks**: *array*
+ - items: *string*
+ - **labels**: *object*
+ - **location**: *string*
+ - **name**: *string*
+ - **threat_detector_provider**: *string*
+
*enum: ['INFOBLOX']*
- **org_policies**: *object*
*additional properties: false*
- **`^[a-z]+\.`**: *object*
diff --git a/modules/project-factory/variables-projects.tf b/modules/project-factory/variables-projects.tf
index ee613f7a7..5e228fa5c 100644
--- a/modules/project-factory/variables-projects.tf
+++ b/modules/project-factory/variables-projects.tf
@@ -231,6 +231,14 @@ variable "projects" {
friendly_name = optional(string)
location = optional(string)
})), {})
+ dns_threat_detector = optional(object({
+ enabled = optional(bool, false)
+ excluded_networks = optional(list(string), [])
+ labels = optional(map(string), {})
+ location = optional(string)
+ name = optional(string)
+ threat_detector_provider = optional(string)
+ }), {})
factories_config = optional(object({
custom_roles = optional(string)
observability = optional(string)
diff --git a/modules/project-factory/variables.tf b/modules/project-factory/variables.tf
index 3c4aa3919..09badec0b 100644
--- a/modules/project-factory/variables.tf
+++ b/modules/project-factory/variables.tf
@@ -25,6 +25,7 @@ variable "context" {
kms_keys = optional(map(string), {})
locations = optional(map(string), {})
log_buckets = optional(map(string), {})
+ networks = optional(map(string), {})
notification_channels = optional(map(string), {})
project_ids = optional(map(string), {})
project_numbers = optional(map(string), {})
diff --git a/tests/modules/project_factory/examples/example.yaml b/tests/modules/project_factory/examples/example.yaml
index 67b46798f..d01b9e74a 100644
--- a/tests/modules/project_factory/examples/example.yaml
+++ b/tests/modules/project_factory/examples/example.yaml
@@ -972,6 +972,7 @@ counts:
google_kms_crypto_key_iam_member: 2
google_kms_key_ring: 1
google_monitoring_notification_channel: 1
+ google_network_security_dns_threat_detector: 1
google_org_policy_policy: 3
google_privileged_access_manager_entitlement: 2
google_project: 4
@@ -995,5 +996,5 @@ counts:
google_tags_tag_value: 2
google_tags_tag_value_iam_binding: 1
modules: 34
- resources: 118
+ resources: 119
terraform_data: 2