From 80ada0e8ddfcad8425a138ae6a457b500873cfaf Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Tue, 8 Aug 2023 18:57:59 +0200 Subject: [PATCH] Refactor firewall policy module (#1576) * refactor module interface * hierarchical attachment and example * hierarchical rules and TODO * split rules resources * additional fields * keep using a single resource for rules * factory * factory test * boilerplate * Prefix ingress and egress rule ids * Tests for other firewall policy types * Fix rule id and names --------- Co-authored-by: Julio Castillo --- modules/net-vpc-firewall-policy/README.md | 202 ++++++++++++++++-- modules/net-vpc-firewall-policy/factory.tf | 119 +++++++++++ .../net-vpc-firewall-policy/hierarchical.tf | 102 +++++++++ modules/net-vpc-firewall-policy/main.tf | 134 +----------- modules/net-vpc-firewall-policy/net-global.tf | 118 ++++++++++ .../net-vpc-firewall-policy/net-regional.tf | 121 +++++++++++ modules/net-vpc-firewall-policy/outputs.tf | 10 +- modules/net-vpc-firewall-policy/variables.tf | 47 ++-- .../examples/factory.yaml | 98 +++++++++ .../examples/global-net.yaml | 137 ++++++++++++ .../examples/hierarchical.yaml | 125 +++++++++++ .../examples/regional-net.yaml | 86 ++++++++ 12 files changed, 1132 insertions(+), 167 deletions(-) create mode 100644 modules/net-vpc-firewall-policy/factory.tf create mode 100644 modules/net-vpc-firewall-policy/hierarchical.tf create mode 100644 modules/net-vpc-firewall-policy/net-global.tf create mode 100644 modules/net-vpc-firewall-policy/net-regional.tf create mode 100644 tests/modules/net_vpc_firewall_policy/examples/factory.yaml create mode 100644 tests/modules/net_vpc_firewall_policy/examples/global-net.yaml create mode 100644 tests/modules/net_vpc_firewall_policy/examples/hierarchical.yaml create mode 100644 tests/modules/net_vpc_firewall_policy/examples/regional-net.yaml diff --git a/modules/net-vpc-firewall-policy/README.md b/modules/net-vpc-firewall-policy/README.md index a24561386f..dd2b206418 100644 --- a/modules/net-vpc-firewall-policy/README.md +++ b/modules/net-vpc-firewall-policy/README.md @@ -1,14 +1,63 @@ -# Google Cloud Network Firewall Policies +# Firewall Policies -This module allows creation and management of a [global](https://cloud.google.com/vpc/docs/network-firewall-policies) or [regional](https://cloud.google.com/vpc/docs/regional-firewall-policies) network firewall policy, including its associations and rules. +This module allows creation and management of two different firewall policy types: -The module interface deviates slightly from the [`net-vpc-firewall`](../net-vpc-firewall/) module since the underlying resources and API objects are different. +- a [hierarchical policy](https://cloud.google.com/firewall/docs/firewall-policies) in a folder or organization, or +- a [global](https://cloud.google.com/vpc/docs/network-firewall-policies) or [regional](https://cloud.google.com/vpc/docs/regional-firewall-policies) network policy -It also makes fewer assumptions about implicit defaults, only using one to set `match.layer4_configs` to `[{ protocol = "all" }]` if no explicit set of protocols and ports has been specified. +The module also manages policy rules via code or a factory, and optional policy attachments. The interface deviates slightly from the [`net-vpc-firewall`](../net-vpc-firewall/) module since the underlying resources and API objects are different. -A factory implementation will be added in a subsequent release. +The module also makes fewer assumptions about implicit defaults, only using one to set `match.layer4_configs` to `[{ protocol = "all" }]` if no explicit set of protocols and ports has been specified. -## Example +## Examples + +### Hierarchical Policy + +```hcl +module "firewall-policy" { + source = "./fabric/modules/net-vpc-firewall-policy" + name = "test-1" + parent_id = "folders/1234567890" + attachments = { + test = "folders/4567890123" + } + egress_rules = { + smtp = { + priority = 900 + match = { + destination_ranges = ["0.0.0.0/0"] + layer4_configs = [{ protocol = "tcp", ports = ["25"] }] + } + } + } + ingress_rules = { + icmp = { + priority = 1000 + match = { + source_ranges = ["0.0.0.0/0"] + layer4_configs = [{ protocol = "icmp" }] + } + } + mgmt = { + priority = 1001 + match = { + source_ranges = ["10.1.1.0/24"] + } + } + ssh = { + priority = 1002 + match = { + source_ranges = ["10.0.0.0/8"] + # source_tags = ["tagValues/123456"] + layer4_configs = [{ protocol = "tcp", ports = ["22"] }] + } + } + } +} +# tftest modules=1 resources=6 inventory=hierarchical.yaml +``` + +### Global Network policy ```hcl module "vpc" { @@ -18,12 +67,10 @@ module "vpc" { } module "firewall-policy" { - source = "./fabric/modules/net-vpc-firewall-policy" - name = "test-1" - project_id = "my-project" - # specify a region to create and manage a regional policy - # region = "europe-west8" - target_vpcs = { + source = "./fabric/modules/net-vpc-firewall-policy" + name = "test-1" + parent_id = "my-project" + attachments = { my-vpc = module.vpc.self_link } egress_rules = { @@ -59,26 +106,137 @@ module "firewall-policy" { } } } -# tftest modules=2 resources=9 +# tftest modules=2 resources=9 inventory=global-net.yaml ``` - +### Regional Network policy + +```hcl +module "vpc" { + source = "./fabric/modules/net-vpc" + project_id = "my-project" + name = "my-network" +} + +module "firewall-policy" { + source = "./fabric/modules/net-vpc-firewall-policy" + name = "test-1" + parent_id = "my-project" + region = "europe-west8" + attachments = { + my-vpc = module.vpc.self_link + } + egress_rules = { + smtp = { + priority = 900 + match = { + destination_ranges = ["0.0.0.0/0"] + layer4_configs = [{ protocol = "tcp", ports = ["25"] }] + } + } + } + ingress_rules = { + icmp = { + priority = 1000 + match = { + source_ranges = ["0.0.0.0/0"] + layer4_configs = [{ protocol = "icmp" }] + } + } + } +} +# tftest modules=2 resources=7 inventory=regional-net.yaml +``` + +### Factory + +Similarly to other modules, a rules factory (see [Resource Factories](../../blueprints/factories/)) is also included here to allow route management via descriptive configuration files. + +Factory configuration is via three optional attributes in the `rules_factory_config` variable: + +- `cidr_file_path` specifying the path to a mapping of logical names to CIDR ranges, used for source and destination ranges in rules when available +- `egress_rules_file_path` specifying the path to the egress rules file +- `ingress_rules_file_path` specifying the path to the ingress rules file + +Factory rules are merged with rules declared in code, with the latter taking precedence where both use the same key. + +This is an example of a simple factory: + +```hcl +module "firewall-policy" { + source = "./fabric/modules/net-vpc-firewall-policy" + name = "test-1" + parent_id = "folders/1234567890" + attachments = { + test = "folders/4567890123" + } + ingress_rules = { + ssh = { + priority = 1002 + match = { + source_ranges = ["10.0.0.0/8"] + layer4_configs = [{ protocol = "tcp", ports = ["22"] }] + } + } + } + rules_factory_config = { + cidr_file_path = "configs/cidrs.yaml" + egress_rules_file_path = "configs/egress.yaml" + ingress_rules_file_path = "configs/ingress.yaml" + } +} +# tftest modules=1 resources=5 files=cidrs,egress,ingress inventory=factory.yaml +``` + +```yaml +# tftest-file id=cidrs path=configs/cidrs.yaml +rfc1918: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/24 +``` + +```yaml +# tftest-file id=egress path=configs/egress.yaml +smtp: + priority: 900 + match: + destination_ranges: + - rfc1918 + layer4_configs: + - protocol: tcp + ports: + - 25 +``` + +```yaml +# tftest-file id=ingress path=configs/ingress.yaml +icmp: + priority: 1000 + match: + source_ranges: + - 10.0.0.0/8 + layer4_configs: + - protocol: icmp +``` + + ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| -| [name](variables.tf#L98) | Policy name. | string | ✓ | | -| [project_id](variables.tf#L104) | Project id of the project that holds the network. | string | ✓ | | -| [description](variables.tf#L17) | Policy description. | string | | null | -| [egress_rules](variables.tf#L23) | List of egress rule definitions, action can be 'allow', 'deny', 'goto_next'. The match.layer4configs map is in protocol => optional [ports] format. | map(object({…})) | | {} | -| [ingress_rules](variables.tf#L60) | List of ingress rule definitions, action can be 'allow', 'deny', 'goto_next'. | map(object({…})) | | {} | -| [region](variables.tf#L110) | Policy region. Leave null for global policy. | string | | null | -| [target_vpcs](variables.tf#L116) | VPC ids to which this policy will be attached, in descriptive name => self link format. | map(string) | | {} | +| [name](variables.tf#L113) | Policy name. | string | ✓ | | +| [parent_id](variables.tf#L119) | Parent node where the policy will be created, `folders/nnn` or `organizations/nnn` for hierarchical policy, project id for a network policy. | string | ✓ | | +| [attachments](variables.tf#L17) | Ids of the resources to which this policy will be attached, in descriptive name => self link format. Specify folders or organization for hierarchical policy, VPCs for network policy. | map(string) | | {} | +| [description](variables.tf#L24) | Policy description. | string | | null | +| [egress_rules](variables.tf#L30) | List of egress rule definitions, action can be 'allow', 'deny', 'goto_next'. The match.layer4configs map is in protocol => optional [ports] format. | map(object({…})) | | {} | +| [ingress_rules](variables.tf#L71) | List of ingress rule definitions, action can be 'allow', 'deny', 'goto_next'. | map(object({…})) | | {} | +| [region](variables.tf#L125) | Policy region. Leave null for hierarchical policy, or global network policy. | string | | null | +| [rules_factory_config](variables.tf#L131) | Configuration for the optional rules factory. | object({…}) | | {} | ## Outputs | name | description | sensitive | |---|---|:---:| | [id](outputs.tf#L17) | Fully qualified firewall policy id. | | - diff --git a/modules/net-vpc-firewall-policy/factory.tf b/modules/net-vpc-firewall-policy/factory.tf new file mode 100644 index 0000000000..a0e655c75b --- /dev/null +++ b/modules/net-vpc-firewall-policy/factory.tf @@ -0,0 +1,119 @@ +/** + * 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 { + _factory_egress_rules = ( + var.rules_factory_config.egress_rules_file_path == null + ? {} + : yamldecode(file(var.rules_factory_config.egress_rules_file_path)) + ) + _factory_ingress_rules = ( + var.rules_factory_config.ingress_rules_file_path == null + ? {} + : yamldecode(file(var.rules_factory_config.ingress_rules_file_path)) + ) + factory_cidrs = ( + var.rules_factory_config.cidr_file_path == null + ? {} + : yamldecode(file(var.rules_factory_config.cidr_file_path)) + ) + factory_egress_rules = { + for k, v in local._factory_egress_rules : "ingress/${k}" => { + action = "deny" + direction = "EGRESS" + priority = v.priority + description = lookup(v, "description", null) + disabled = lookup(v, "disabled", false) + enable_logging = lookup(v, "enable_logging", null) + target_service_accounts = lookup(v, "target_service_accounts", null) + target_tags = lookup(v, "target_tags", null) + match = { + address_groups = lookup(v.match, "address_groups", null) + fqdns = lookup(v.match, "fqdns", null) + region_codes = lookup(v.match, "region_codes", null) + threat_intelligences = lookup(v.match, "threat_intelligences", null) + destination_ranges = ( + lookup(v.match, "destination_ranges", null) == null + ? null + : flatten([ + for r in v.match.destination_ranges : + try(local.factory_cidrs[r], r) + ]) + ) + source_ranges = ( + lookup(v.match, "source_ranges", null) == null + ? null + : flatten([ + for r in v.match.source_ranges : + try(local.factory_cidrs[r], r) + ]) + ) + source_tags = lookup(v.match, "source_tags", null) + layer4_configs = ( + lookup(v.match, "layer4_configs", null) == null + ? [{ protocol = "all", ports = null }] + : [ + for c in v.match.layer4_configs : + merge({ protocol = "all", ports = null }, c) + ] + ) + } + } + } + factory_ingress_rules = { + for k, v in local._factory_ingress_rules : "egress/${k}" => { + action = "allow" + direction = "INGRESS" + priority = v.priority + description = lookup(v, "description", null) + disabled = lookup(v, "disabled", false) + enable_logging = lookup(v, "enable_logging", null) + target_service_accounts = lookup(v, "target_service_accounts", null) + target_tags = lookup(v, "target_tags", null) + match = { + address_groups = lookup(v.match, "address_groups", null) + fqdns = lookup(v.match, "fqdns", null) + region_codes = lookup(v.match, "region_codes", null) + threat_intelligences = lookup(v.match, "threat_intelligences", null) + destination_ranges = ( + lookup(v.match, "destination_ranges", null) == null + ? null + : flatten([ + for r in v.match.destination_ranges : + try(local.factory_cidrs[r], r) + ]) + ) + source_ranges = ( + lookup(v.match, "source_ranges", null) == null + ? null + : flatten([ + for r in v.match.source_ranges : + try(local.factory_cidrs[r], r) + ]) + ) + source_tags = lookup(v.match, "source_tags", null) + layer4_configs = ( + lookup(v.match, "layer4_configs", null) == null + ? [{ protocol = "all", ports = null }] + : [ + for c in v.match.layer4_configs : + merge({ protocol = "all", ports = null }, c) + ] + ) + } + } + } +} diff --git a/modules/net-vpc-firewall-policy/hierarchical.tf b/modules/net-vpc-firewall-policy/hierarchical.tf new file mode 100644 index 0000000000..6c1decdcf5 --- /dev/null +++ b/modules/net-vpc-firewall-policy/hierarchical.tf @@ -0,0 +1,102 @@ +/** + * 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. + */ + +resource "google_compute_firewall_policy" "hierarchical" { + count = local.use_hierarchical ? 1 : 0 + parent = var.parent_id + short_name = var.name + description = var.description +} + +resource "google_compute_firewall_policy_association" "hierarchical" { + for_each = local.use_hierarchical ? var.attachments : {} + name = "${var.name}-${each.key}" + attachment_target = each.value + firewall_policy = google_compute_firewall_policy.hierarchical.0.name +} + +output "foo" { + value = { + rules = local.rules + cidrs = local.factory_cidrs + } +} + +resource "google_compute_firewall_policy_rule" "hierarchical" { + # Terraform's type system barfs in the condition if we use the locals map + for_each = toset( + local.use_hierarchical ? keys(local.rules) : [] + ) + firewall_policy = google_compute_firewall_policy.hierarchical.0.name + action = local.rules[each.key].action + description = local.rules[each.key].description + direction = local.rules[each.key].direction + disabled = local.rules[each.key].disabled + enable_logging = local.rules[each.key].enable_logging + priority = local.rules[each.key].priority + target_service_accounts = local.rules[each.key].target_service_accounts + match { + dest_ip_ranges = local.rules[each.key].match.destination_ranges + src_ip_ranges = local.rules[each.key].match.source_ranges + dest_address_groups = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.address_groups + : null + ) + dest_fqdns = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.fqdns + : null + ) + dest_region_codes = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.region_codes + : null + ) + dest_threat_intelligences = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.threat_intelligences + : null + ) + src_address_groups = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.address_groups + : null + ) + src_fqdns = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.fqdns + : null + ) + src_region_codes = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.region_codes + : null + ) + src_threat_intelligences = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.threat_intelligences + : null + ) + dynamic "layer4_configs" { + for_each = local.rules[each.key].match.layer4_configs + content { + ip_protocol = layer4_configs.value.protocol + ports = layer4_configs.value.ports + } + } + } +} diff --git a/modules/net-vpc-firewall-policy/main.tf b/modules/net-vpc-firewall-policy/main.tf index 190b209316..f253e2219b 100644 --- a/modules/net-vpc-firewall-policy/main.tf +++ b/modules/net-vpc-firewall-policy/main.tf @@ -15,136 +15,18 @@ */ locals { - rules = merge( - local._rules_egress, local._rules_ingress - ) _rules_egress = { for name, rule in merge(var.egress_rules) : - name => merge(rule, { direction = "EGRESS" }) + "egress/${name}" => merge(rule, { name = name, direction = "EGRESS" }) } _rules_ingress = { for name, rule in merge(var.ingress_rules) : - name => merge(rule, { direction = "INGRESS" }) - } -} - -############################################################################### -# global policy # -############################################################################### - -resource "google_compute_network_firewall_policy" "default" { - count = var.region == null ? 1 : 0 - project = var.project_id - name = var.name - description = var.description -} - -resource "google_compute_network_firewall_policy_association" "default" { - for_each = var.region == null ? var.target_vpcs : {} - project = var.project_id - name = "${var.name}-${each.key}" - attachment_target = each.value - firewall_policy = google_compute_network_firewall_policy.default.0.name -} - -resource "google_compute_network_firewall_policy_rule" "default" { - provider = google-beta - for_each = var.region == null ? local.rules : {} - project = var.project_id - firewall_policy = google_compute_network_firewall_policy.default.0.name - rule_name = each.key - action = each.value.action - description = each.value.description - direction = each.value.direction - disabled = each.value.disabled - enable_logging = each.value.enable_logging - priority = each.value.priority - target_service_accounts = each.value.target_service_accounts - match { - dest_ip_ranges = each.value.match.destination_ranges - src_ip_ranges = each.value.match.source_ranges - dynamic "layer4_configs" { - for_each = each.value.match.layer4_configs - content { - ip_protocol = layer4_configs.value.protocol - ports = layer4_configs.value.ports - } - } - dynamic "src_secure_tags" { - for_each = toset(coalesce(each.value.match.source_tags, [])) - content { - name = src_secure_tags.key - } - } - } - dynamic "target_secure_tags" { - for_each = toset( - each.value.target_tags == null ? [] : each.value.target_tags - ) - content { - name = target_secure_tags.value - } - } -} - -############################################################################### -# regional policy # -############################################################################### - -resource "google_compute_region_network_firewall_policy" "default" { - count = var.region != null ? 1 : 0 - project = var.project_id - name = var.name - description = var.description - region = var.region -} - -resource "google_compute_region_network_firewall_policy_association" "default" { - for_each = var.region != null ? var.target_vpcs : {} - project = var.project_id - region = var.region - name = "${var.name}-${each.key}" - attachment_target = each.value - firewall_policy = google_compute_region_network_firewall_policy.default.0.name -} - -resource "google_compute_region_network_firewall_policy_rule" "default" { - provider = google-beta - for_each = var.region != null ? local.rules : {} - project = var.project_id - region = var.region - firewall_policy = google_compute_region_network_firewall_policy.default.0.name - rule_name = each.key - action = each.value.action - description = each.value.description - direction = each.value.direction - disabled = each.value.disabled - enable_logging = each.value.enable_logging - priority = each.value.priority - target_service_accounts = each.value.target_service_accounts - match { - dest_ip_ranges = each.value.match.destination_ranges - src_ip_ranges = each.value.match.source_ranges - dynamic "layer4_configs" { - for_each = each.value.match.layer4_configs - content { - ip_protocol = layer4_configs.value.protocol - ports = layer4_configs.value.ports - } - } - dynamic "src_secure_tags" { - for_each = toset(coalesce(each.value.match.source_tags, [])) - content { - name = src_secure_tags.key - } - } - } - dynamic "target_secure_tags" { - for_each = toset( - each.value.target_tags == null ? [] : each.value.target_tags - ) - content { - name = target_secure_tags.value - } + "ingress/${name}" => merge(rule, { name = name, direction = "INGRESS" }) } + rules = merge( + local.factory_egress_rules, local.factory_ingress_rules, + local._rules_egress, local._rules_ingress + ) + use_hierarchical = strcontains(var.parent_id, "/") ? true : false + use_regional = !local.use_hierarchical && var.region != null } diff --git a/modules/net-vpc-firewall-policy/net-global.tf b/modules/net-vpc-firewall-policy/net-global.tf new file mode 100644 index 0000000000..685c860514 --- /dev/null +++ b/modules/net-vpc-firewall-policy/net-global.tf @@ -0,0 +1,118 @@ +/** + * 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. + */ + +resource "google_compute_network_firewall_policy" "net-global" { + count = !local.use_hierarchical && !local.use_regional ? 1 : 0 + project = var.parent_id + name = var.name + description = var.description +} + +resource "google_compute_network_firewall_policy_association" "net-global" { + for_each = ( + !local.use_hierarchical && !local.use_regional ? var.attachments : {} + ) + project = var.parent_id + name = "${var.name}-${each.key}" + attachment_target = each.value + firewall_policy = google_compute_network_firewall_policy.net-global.0.name +} + +resource "google_compute_network_firewall_policy_rule" "net-global" { + # Terraform's type system barfs in the condition if we use the locals map + for_each = toset( + !local.use_hierarchical && !local.use_regional + ? keys(local.rules) + : [] + ) + project = var.parent_id + firewall_policy = google_compute_network_firewall_policy.net-global.0.name + rule_name = local.rules[each.key].name + action = local.rules[each.key].action + description = local.rules[each.key].description + direction = local.rules[each.key].direction + disabled = local.rules[each.key].disabled + enable_logging = local.rules[each.key].enable_logging + priority = local.rules[each.key].priority + target_service_accounts = local.rules[each.key].target_service_accounts + match { + dest_ip_ranges = local.rules[each.key].match.destination_ranges + src_ip_ranges = local.rules[each.key].match.source_ranges + dest_address_groups = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.address_groups + : null + ) + dest_fqdns = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.fqdns + : null + ) + dest_region_codes = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.region_codes + : null + ) + dest_threat_intelligences = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.threat_intelligences + : null + ) + src_address_groups = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.address_groups + : null + ) + src_fqdns = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.fqdns + : null + ) + src_region_codes = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.region_codes + : null + ) + src_threat_intelligences = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.threat_intelligences + : null + ) + dynamic "layer4_configs" { + for_each = local.rules[each.key].match.layer4_configs + content { + ip_protocol = layer4_configs.value.protocol + ports = layer4_configs.value.ports + } + } + dynamic "src_secure_tags" { + for_each = toset(coalesce(local.rules[each.key].match.source_tags, [])) + content { + name = src_secure_tags.key + } + } + } + dynamic "target_secure_tags" { + for_each = toset( + local.rules[each.key].target_tags == null + ? [] + : local.rules[each.key].target_tags + ) + content { + name = target_secure_tags.value + } + } +} diff --git a/modules/net-vpc-firewall-policy/net-regional.tf b/modules/net-vpc-firewall-policy/net-regional.tf new file mode 100644 index 0000000000..a77b30f3e1 --- /dev/null +++ b/modules/net-vpc-firewall-policy/net-regional.tf @@ -0,0 +1,121 @@ +/** + * 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. + */ + +resource "google_compute_region_network_firewall_policy" "net-regional" { + count = !local.use_hierarchical && local.use_regional ? 1 : 0 + project = var.parent_id + name = var.name + description = var.description + region = var.region +} + +resource "google_compute_region_network_firewall_policy_association" "net-regional" { + for_each = ( + !local.use_hierarchical && local.use_regional ? var.attachments : {} + ) + project = var.parent_id + region = var.region + name = "${var.name}-${each.key}" + attachment_target = each.value + firewall_policy = google_compute_region_network_firewall_policy.net-regional.0.name +} + +resource "google_compute_region_network_firewall_policy_rule" "net-regional" { + # Terraform's type system barfs in the condition if we use the locals map + for_each = toset( + !local.use_hierarchical && local.use_regional + ? keys(local.rules) + : [] + ) + project = var.parent_id + region = var.region + firewall_policy = google_compute_region_network_firewall_policy.net-regional.0.name + rule_name = local.rules[each.key].name + action = local.rules[each.key].action + description = local.rules[each.key].description + direction = local.rules[each.key].direction + disabled = local.rules[each.key].disabled + enable_logging = local.rules[each.key].enable_logging + priority = local.rules[each.key].priority + target_service_accounts = local.rules[each.key].target_service_accounts + match { + dest_ip_ranges = local.rules[each.key].match.destination_ranges + src_ip_ranges = local.rules[each.key].match.source_ranges + dest_address_groups = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.address_groups + : null + ) + dest_fqdns = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.fqdns + : null + ) + dest_region_codes = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.region_codes + : null + ) + dest_threat_intelligences = ( + local.rules[each.key].direction == "EGRESS" + ? local.rules[each.key].match.threat_intelligences + : null + ) + src_address_groups = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.address_groups + : null + ) + src_fqdns = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.fqdns + : null + ) + src_region_codes = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.region_codes + : null + ) + src_threat_intelligences = ( + local.rules[each.key].direction == "INGRESS" + ? local.rules[each.key].match.threat_intelligences + : null + ) + dynamic "layer4_configs" { + for_each = local.rules[each.key].match.layer4_configs + content { + ip_protocol = layer4_configs.value.protocol + ports = layer4_configs.value.ports + } + } + dynamic "src_secure_tags" { + for_each = toset(coalesce(local.rules[each.key].match.source_tags, [])) + content { + name = src_secure_tags.key + } + } + } + dynamic "target_secure_tags" { + for_each = toset( + local.rules[each.key].target_tags == null + ? [] + : local.rules[each.key].target_tags + ) + content { + name = target_secure_tags.value + } + } +} diff --git a/modules/net-vpc-firewall-policy/outputs.tf b/modules/net-vpc-firewall-policy/outputs.tf index 284dc96314..44c0a7a6a0 100644 --- a/modules/net-vpc-firewall-policy/outputs.tf +++ b/modules/net-vpc-firewall-policy/outputs.tf @@ -16,9 +16,9 @@ output "id" { description = "Fully qualified firewall policy id." - value = ( - var.region == null - ? try(google_compute_network_firewall_policy.default.0.id, null) - : try(google_compute_region_network_firewall_policy.default.0.id, null) - ) + value = coalesce([ + try(google_compute_firewall_policy.hierarchical.0.id, null), + try(google_compute_network_firewall_policy.net-global.0.id, null), + try(google_compute_region_network_firewall_policy.net-regional.0.id, null) + ]) } diff --git a/modules/net-vpc-firewall-policy/variables.tf b/modules/net-vpc-firewall-policy/variables.tf index 8465f9444c..a1694d50c9 100644 --- a/modules/net-vpc-firewall-policy/variables.tf +++ b/modules/net-vpc-firewall-policy/variables.tf @@ -14,6 +14,13 @@ * limitations under the License. */ +variable "attachments" { + description = "Ids of the resources to which this policy will be attached, in descriptive name => self link format. Specify folders or organization for hierarchical policy, VPCs for network policy." + type = map(string) + default = {} + nullable = false +} + variable "description" { description = "Policy description." type = string @@ -31,9 +38,13 @@ variable "egress_rules" { target_service_accounts = optional(list(string)) target_tags = optional(list(string)) match = object({ - destination_ranges = optional(list(string)) - source_ranges = optional(list(string)) - source_tags = optional(list(string)) + address_groups = optional(list(string)) + fqdns = optional(list(string)) + region_codes = optional(list(string)) + threat_intelligences = optional(list(string)) + destination_ranges = optional(list(string)) + source_ranges = optional(list(string)) + source_tags = optional(list(string)) layer4_configs = optional(list(object({ protocol = optional(string, "all") ports = optional(list(string)) @@ -68,9 +79,13 @@ variable "ingress_rules" { target_service_accounts = optional(list(string)) target_tags = optional(list(string)) match = object({ - destination_ranges = optional(list(string)) - source_ranges = optional(list(string)) - source_tags = optional(list(string)) + address_groups = optional(list(string)) + fqdns = optional(list(string)) + region_codes = optional(list(string)) + threat_intelligences = optional(list(string)) + destination_ranges = optional(list(string)) + source_ranges = optional(list(string)) + source_tags = optional(list(string)) layer4_configs = optional(list(object({ protocol = optional(string, "all") ports = optional(list(string)) @@ -101,21 +116,25 @@ variable "name" { nullable = false } -variable "project_id" { - description = "Project id of the project that holds the network." +variable "parent_id" { + description = "Parent node where the policy will be created, `folders/nnn` or `organizations/nnn` for hierarchical policy, project id for a network policy." type = string nullable = false } variable "region" { - description = "Policy region. Leave null for global policy." + description = "Policy region. Leave null for hierarchical policy, or global network policy." type = string default = null } -variable "target_vpcs" { - description = "VPC ids to which this policy will be attached, in descriptive name => self link format." - type = map(string) - default = {} - nullable = false +variable "rules_factory_config" { + description = "Configuration for the optional rules factory." + type = object({ + cidr_file_path = optional(string) + egress_rules_file_path = optional(string) + ingress_rules_file_path = optional(string) + }) + nullable = false + default = {} } diff --git a/tests/modules/net_vpc_firewall_policy/examples/factory.yaml b/tests/modules/net_vpc_firewall_policy/examples/factory.yaml new file mode 100644 index 0000000000..b3709fe631 --- /dev/null +++ b/tests/modules/net_vpc_firewall_policy/examples/factory.yaml @@ -0,0 +1,98 @@ +# 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. +values: + module.firewall-policy.google_compute_firewall_policy.hierarchical[0]: + parent: folders/1234567890 + short_name: test-1 + module.firewall-policy.google_compute_firewall_policy_association.hierarchical["test"]: + attachment_target: folders/4567890123 + name: test-1-test + module.firewall-policy.google_compute_firewall_policy_rule.hierarchical["egress/icmp"]: + action: allow + direction: INGRESS + disabled: false + enable_logging: null + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: icmp + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.0.0.0/8 + src_region_codes: null + src_threat_intelligences: null + priority: 1000 + target_resources: null + target_service_accounts: null + module.firewall-policy.google_compute_firewall_policy_rule.hierarchical["ingress/smtp"]: + action: deny + direction: EGRESS + disabled: false + enable_logging: null + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: + - 10.0.0.0/8 + - 172.16.0.0/12 + - 192.168.0.0/24 + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '25' + src_address_groups: null + src_fqdns: null + src_ip_ranges: null + src_region_codes: null + src_threat_intelligences: null + priority: 900 + target_resources: null + target_service_accounts: null + module.firewall-policy.google_compute_firewall_policy_rule.hierarchical["ingress/ssh"]: + action: allow + direction: INGRESS + disabled: false + enable_logging: null + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '22' + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.0.0.0/8 + src_region_codes: null + src_threat_intelligences: null + priority: 1002 + target_resources: null + target_service_accounts: null + +counts: + google_compute_firewall_policy: 1 + google_compute_firewall_policy_association: 1 + google_compute_firewall_policy_rule: 3 diff --git a/tests/modules/net_vpc_firewall_policy/examples/global-net.yaml b/tests/modules/net_vpc_firewall_policy/examples/global-net.yaml new file mode 100644 index 0000000000..35b7e4eda5 --- /dev/null +++ b/tests/modules/net_vpc_firewall_policy/examples/global-net.yaml @@ -0,0 +1,137 @@ +# 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. + +values: + module.firewall-policy.google_compute_network_firewall_policy.net-global[0]: + name: test-1 + project: my-project + module.firewall-policy.google_compute_network_firewall_policy_association.net-global["my-vpc"]: + firewall_policy: test-1 + name: test-1-my-vpc + project: my-project + module.firewall-policy.google_compute_network_firewall_policy_rule.net-global["egress/smtp"]: + action: deny + direction: EGRESS + disabled: false + enable_logging: null + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: + - 0.0.0.0/0 + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '25' + src_address_groups: null + src_fqdns: null + src_ip_ranges: null + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 900 + project: my-project + rule_name: smtp + target_secure_tags: [] + target_service_accounts: null + module.firewall-policy.google_compute_network_firewall_policy_rule.net-global["ingress/icmp"]: + action: allow + direction: INGRESS + disabled: false + enable_logging: null + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: icmp + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 0.0.0.0/0 + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 1000 + project: my-project + rule_name: icmp + target_secure_tags: [] + target_service_accounts: null + module.firewall-policy.google_compute_network_firewall_policy_rule.net-global["ingress/mgmt"]: + action: allow + direction: INGRESS + disabled: false + enable_logging: null + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: all + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.1.1.0/24 + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 1001 + project: my-project + rule_name: mgmt + target_secure_tags: [] + target_service_accounts: null + module.firewall-policy.google_compute_network_firewall_policy_rule.net-global["ingress/ssh"]: + action: allow + direction: INGRESS + disabled: false + enable_logging: null + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '22' + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.0.0.0/8 + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 1002 + project: my-project + rule_name: ssh + target_secure_tags: [] + target_service_accounts: null + +counts: + google_compute_network_firewall_policy: 1 + google_compute_network_firewall_policy_association: 1 + google_compute_network_firewall_policy_rule: 4 diff --git a/tests/modules/net_vpc_firewall_policy/examples/hierarchical.yaml b/tests/modules/net_vpc_firewall_policy/examples/hierarchical.yaml new file mode 100644 index 0000000000..2b0c4e3146 --- /dev/null +++ b/tests/modules/net_vpc_firewall_policy/examples/hierarchical.yaml @@ -0,0 +1,125 @@ +# 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. + +values: + module.firewall-policy.google_compute_firewall_policy.hierarchical[0]: + description: null + parent: folders/1234567890 + short_name: test-1 + module.firewall-policy.google_compute_firewall_policy_association.hierarchical["test"]: + attachment_target: folders/4567890123 + name: test-1-test + module.firewall-policy.google_compute_firewall_policy_rule.hierarchical["egress/smtp"]: + action: deny + description: null + direction: EGRESS + disabled: false + enable_logging: null + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: + - 0.0.0.0/0 + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '25' + src_address_groups: null + src_fqdns: null + src_ip_ranges: null + src_region_codes: null + src_threat_intelligences: null + priority: 900 + target_resources: null + target_service_accounts: null + module.firewall-policy.google_compute_firewall_policy_rule.hierarchical["ingress/icmp"]: + action: allow + description: null + direction: INGRESS + disabled: false + enable_logging: null + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: icmp + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 0.0.0.0/0 + src_region_codes: null + src_threat_intelligences: null + priority: 1000 + target_resources: null + target_service_accounts: null + module.firewall-policy.google_compute_firewall_policy_rule.hierarchical["ingress/mgmt"]: + action: allow + description: null + direction: INGRESS + disabled: false + enable_logging: null + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: all + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.1.1.0/24 + src_region_codes: null + src_threat_intelligences: null + priority: 1001 + target_resources: null + target_service_accounts: null + module.firewall-policy.google_compute_firewall_policy_rule.hierarchical["ingress/ssh"]: + action: allow + description: null + direction: INGRESS + disabled: false + enable_logging: null + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '22' + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 10.0.0.0/8 + src_region_codes: null + src_threat_intelligences: null + priority: 1002 + target_resources: null + target_service_accounts: null + +counts: + google_compute_firewall_policy: 1 + google_compute_firewall_policy_association: 1 + google_compute_firewall_policy_rule: 4 diff --git a/tests/modules/net_vpc_firewall_policy/examples/regional-net.yaml b/tests/modules/net_vpc_firewall_policy/examples/regional-net.yaml new file mode 100644 index 0000000000..326973ba8c --- /dev/null +++ b/tests/modules/net_vpc_firewall_policy/examples/regional-net.yaml @@ -0,0 +1,86 @@ +# 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. + +values: + module.firewall-policy.google_compute_region_network_firewall_policy.net-regional[0]: + name: test-1 + project: my-project + region: europe-west8 + module.firewall-policy.google_compute_region_network_firewall_policy_association.net-regional["my-vpc"]: + firewall_policy: test-1 + name: test-1-my-vpc + project: my-project + region: europe-west8 + module.firewall-policy.google_compute_region_network_firewall_policy_rule.net-regional["egress/smtp"]: + action: deny + direction: EGRESS + disabled: false + enable_logging: null + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: + - 0.0.0.0/0 + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: tcp + ports: + - '25' + src_address_groups: null + src_fqdns: null + src_ip_ranges: null + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 900 + project: my-project + region: europe-west8 + rule_name: smtp + target_secure_tags: [] + target_service_accounts: null + module.firewall-policy.google_compute_region_network_firewall_policy_rule.net-regional["ingress/icmp"]: + action: allow + direction: INGRESS + disabled: false + enable_logging: null + firewall_policy: test-1 + match: + - dest_address_groups: null + dest_fqdns: null + dest_ip_ranges: null + dest_region_codes: null + dest_threat_intelligences: null + layer4_configs: + - ip_protocol: icmp + ports: null + src_address_groups: null + src_fqdns: null + src_ip_ranges: + - 0.0.0.0/0 + src_region_codes: null + src_secure_tags: [] + src_threat_intelligences: null + priority: 1000 + project: my-project + region: europe-west8 + rule_name: icmp + target_secure_tags: [] + target_service_accounts: null + +counts: + google_compute_region_network_firewall_policy: 1 + google_compute_region_network_firewall_policy_association: 1 + google_compute_region_network_firewall_policy_rule: 2