Organization module refactor, in-module firewall policy factory for organization and folder (#385)

* move iam and logging to separate files, minimal refactoring

* update README

* fix example

* factory

* tfdoc

* boilerplate

* remove data_folder variable

* tfdoc

* fix default factory name

* add firewall policy to folder module

* add factory example
This commit is contained in:
Ludovico Magnocavallo 2021-12-13 08:41:02 +01:00 committed by GitHub
parent 026d634812
commit 174de3a087
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 734 additions and 400 deletions

View File

@ -15,7 +15,8 @@
*/
locals {
cidrs = try({ for name, cidrs in yamldecode(file("${var.templates_folder}/cidrs.yaml")) :
cidrs = try({
for name, cidrs in yamldecode(file("${var.templates_folder}/cidrs.yaml")) :
name => cidrs
}, {})

View File

@ -170,8 +170,9 @@ module "folder2" {
| name | description | type | required | default |
|---|---|:---: |:---:|:---:|
| *contacts* | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">{}</code> |
| *firewall_policies* | Hierarchical firewall policies to *create* in this folder. | <code title="map&#40;map&#40;object&#40;&#123;&#10;description &#61; string&#10;direction &#61; string&#10;action &#61; string&#10;priority &#61; number&#10;ranges &#61; list&#40;string&#41;&#10;ports &#61; map&#40;list&#40;string&#41;&#41;&#10;target_service_accounts &#61; list&#40;string&#41;&#10;target_resources &#61; list&#40;string&#41;&#10;logging &#61; bool&#10;&#125;&#41;&#41;&#41;">map(map(object({...})))</code> | | <code title="">{}</code> |
| *firewall_policy_attachments* | List of hierarchical firewall policy IDs to *attach* to this folder. | <code title="map&#40;string&#41;">map(string)</code> | | <code title="">{}</code> |
| *firewall_policies* | Hierarchical firewall policies created in this folder. | <code title="map&#40;map&#40;object&#40;&#123;&#10;action &#61; string&#10;description &#61; string&#10;direction &#61; string&#10;logging &#61; bool&#10;ports &#61; map&#40;list&#40;string&#41;&#41;&#10;priority &#61; number&#10;ranges &#61; list&#40;string&#41;&#10;target_resources &#61; list&#40;string&#41;&#10;target_service_accounts &#61; list&#40;string&#41;&#10;&#125;&#41;&#41;&#41;">map(map(object({...})))</code> | | <code title="">{}</code> |
| *firewall_policy_attachments* | List of hierarchical firewall policy IDs to attached to this folder. | <code title="map&#40;string&#41;">map(string)</code> | | <code title="">{}</code> |
| *firewall_policy_factory* | Configuration for the firewall policy factory. | <code title="object&#40;&#123;&#10;cidr_file &#61; string&#10;policy_name &#61; string&#10;rules_file &#61; string&#10;&#125;&#41;">object({...})</code> | | <code title="">null</code> |
| *folder_create* | Create folder. When set to false, uses id to reference an existing folder. | <code title="">bool</code> | | <code title="">true</code> |
| *group_iam* | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">{}</code> |
| *iam* | IAM bindings in {ROLE => [MEMBERS]} format. | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">{}</code> |

View File

@ -0,0 +1,97 @@
/**
* Copyright 2021 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_cidrs = try(
yamldecode(file(var.firewall_policy_factory.cidr_file)), {}
)
_factory_name = (
try(var.firewall_policy_factory.policy_name, null) == null
? "factory"
: var.firewall_policy_factory.policy_name
)
_factory_rules = try(
yamldecode(file(var.firewall_policy_factory.rules_file)), {}
)
_factory_rules_parsed = {
for name, rule in local._factory_rules : name => merge(rule, {
ranges = flatten([
for r in(rule.ranges == null ? [] : rule.ranges) :
lookup(local._factory_cidrs, trimprefix(r, "$"), r)
])
})
}
_merged_rules = flatten([
for policy, rules in local.firewall_policies : [
for name, rule in rules : merge(rule, {
policy = policy
name = name
})
]
])
firewall_policies = merge(var.firewall_policies, (
length(local._factory_rules) == 0
? {}
: { (local._factory_name) = local._factory_rules_parsed }
))
firewall_rules = {
for r in local._merged_rules : "${r.policy}-${r.name}" => r
}
}
resource "google_compute_organization_security_policy" "policy" {
provider = google-beta
for_each = local.firewall_policies
display_name = each.key
parent = local.folder.id
}
resource "google_compute_organization_security_policy_rule" "rule" {
provider = google-beta
for_each = local.firewall_rules
policy_id = google_compute_organization_security_policy.policy[each.value.policy].id
action = each.value.action
direction = each.value.direction
priority = try(each.value.priority, null)
target_resources = try(each.value.target_resources, null)
target_service_accounts = try(each.value.target_service_accounts, null)
enable_logging = try(each.value.logging, null)
# preview = each.value.preview
match {
description = each.value.description
config {
src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null
dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null
dynamic "layer4_config" {
for_each = each.value.ports
iterator = port
content {
ip_protocol = port.key
ports = port.value
}
}
}
}
}
resource "google_compute_organization_security_policy_association" "attachment" {
provider = google-beta
for_each = var.firewall_policy_attachments
name = "${local.folder.id}-${each.key}"
attachment_id = local.folder.id
policy_id = each.value
}

View File

@ -15,12 +15,6 @@
*/
locals {
extended_rules = flatten([
for policy, rules in var.firewall_policies : [
for rule_name, rule in rules :
merge(rule, { policy = policy, name = rule_name })
]
])
group_iam_roles = distinct(flatten(values(var.group_iam)))
group_iam = {
for r in local.group_iam_roles : r => [
@ -34,10 +28,6 @@ locals {
try(local.group_iam[role], [])
)
}
rules_map = {
for rule in local.extended_rules :
"${rule.policy}-${rule.name}" => rule
}
logging_sinks = coalesce(var.logging_sinks, {})
sink_type_destination = {
gcs = "storage.googleapis.com"
@ -151,51 +141,6 @@ resource "google_folder_organization_policy" "list" {
}
}
resource "google_compute_organization_security_policy" "policy" {
provider = google-beta
for_each = var.firewall_policies
display_name = each.key
parent = local.folder.id
}
resource "google_compute_organization_security_policy_rule" "rule" {
provider = google-beta
for_each = local.rules_map
policy_id = google_compute_organization_security_policy.policy[each.value.policy].id
action = each.value.action
direction = each.value.direction
priority = each.value.priority
target_resources = each.value.target_resources
target_service_accounts = each.value.target_service_accounts
enable_logging = each.value.logging
# preview = each.value.preview
match {
description = each.value.description
config {
src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null
dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null
dynamic "layer4_config" {
for_each = each.value.ports
iterator = port
content {
ip_protocol = port.key
ports = port.value
}
}
}
}
}
resource "google_compute_organization_security_policy_association" "attachment" {
provider = google-beta
for_each = var.firewall_policy_attachments
name = "${local.folder.id}-${each.key}"
attachment_id = local.folder.id
policy_id = each.value
}
resource "google_logging_folder_sink" "sink" {
for_each = local.logging_sinks
name = each.key

View File

@ -21,27 +21,37 @@ variable "contacts" {
}
variable "firewall_policies" {
description = "Hierarchical firewall policies to *create* in this folder."
description = "Hierarchical firewall policies created in this folder."
type = map(map(object({
action = string
description = string
direction = string
action = string
logging = bool
ports = map(list(string))
priority = number
ranges = list(string)
ports = map(list(string))
target_service_accounts = list(string)
target_resources = list(string)
logging = bool
target_service_accounts = list(string)
})))
default = {}
}
variable "firewall_policy_attachments" {
description = "List of hierarchical firewall policy IDs to *attach* to this folder."
description = "List of hierarchical firewall policy IDs to attached to this folder."
type = map(string)
default = {}
}
variable "firewall_policy_factory" {
description = "Configuration for the firewall policy factory."
type = object({
cidr_file = string
policy_name = string
rules_file = string
})
default = null
}
variable "folder_create" {
description = "Create folder. When set to false, uses id to reference an existing folder."
type = bool

View File

@ -28,7 +28,8 @@ locals {
})
}
cidrs = try({ for name, cidrs in yamldecode(file("${var.cidr_template_file}")) :
cidrs = try({
for name, cidrs in yamldecode(file(var.cidr_template_file)) :
name => cidrs
}, {})

View File

@ -45,7 +45,16 @@ There are several mutually exclusive ways of managing IAM in this module
Some care must be takend with the `groups_iam` variable (and in some situations with the additive variables) to ensure that variable keys are static values, so that Terraform is able to compute the dependency graph.
## Hierarchical firewall rules
## Hierarchical firewall policies
Hirerarchical firewall policies can be managed in two ways:
- via the `firewall_policies` variable, to directly define policies and rules in Terraform
- via the `firewall_policy_factory` variable, to leverage external YaML files via a simple "factory" embedded in the module ([see here](../../factories) for more context on factories)
Once you have policies (either created via the module or externally), you can attach them using the `firewall_policy_attachments` variable.
### Directly defined firewall policies
```hcl
module "org" {
@ -75,6 +84,60 @@ module "org" {
# tftest:modules=1:resources=3
```
### Firewall policy factory
The in-built factory allows you to define a single policy, using one file for rules, and an optional file for CIDR range substitution variables. Remember that non-absolute paths are relative to the root module (the folder where you run `terraform`).
```hcl
module "org" {
source = "./modules/organization"
organization_id = var.organization_id
firewall_policy_factory = {
cidr_file = "data/cidrs.yaml
policy_name = null
rules_file = "data/rules.yaml"
}
}
# tftest:skip
```
```yaml
# cidrs.yaml
rfc1918:
- 10.0.0.0/8
- 172.168.0.0/12
- 192.168.0.0/16
```
```yaml
# rules.yaml
allow-admins:
description: Access from the admin subnet to all subnets
direction: INGRESS
action: allow
priority: 1000
ranges:
- $rfc1918
ports:
all: []
target_resources: null
enable_logging: false
allow-ssh-from-iap:
description: Enable SSH from IAP
direction: INGRESS
action: allow
priority: 1002
ranges:
- 35.235.240.0/20
ports:
tcp: ["22"]
target_resources: null
enable_logging: false
```
## Logging Sinks
```hcl
@ -110,7 +173,7 @@ module "org" {
logging_sinks = {
warnings = {
type = "gcs"
type = "storage"
destination = module.gcs.name
filter = "severity=WARNING"
iam = false
@ -180,8 +243,9 @@ module "org" {
| organization_id | Organization id in organizations/nnnnnn format. | <code title="string&#10;validation &#123;&#10;condition &#61; can&#40;regex&#40;&#34;&#94;organizations&#47;&#91;0-9&#93;&#43;&#34;, var.organization_id&#41;&#41;&#10;error_message &#61; &#34;The organization_id must in the form organizations&#47;nnn.&#34;&#10;&#125;">string</code> | ✓ | |
| *contacts* | List of essential contacts for this resource. Must be in the form EMAIL -> [NOTIFICATION_TYPES]. Valid notification types are ALL, SUSPENSION, SECURITY, TECHNICAL, BILLING, LEGAL, PRODUCT_UPDATES | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">{}</code> |
| *custom_roles* | Map of role name => list of permissions to create in this project. | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">{}</code> |
| *firewall_policies* | Hierarchical firewall policies to *create* in the organization. | <code title="map&#40;map&#40;object&#40;&#123;&#10;description &#61; string&#10;direction &#61; string&#10;action &#61; string&#10;priority &#61; number&#10;ranges &#61; list&#40;string&#41;&#10;ports &#61; map&#40;list&#40;string&#41;&#41;&#10;target_service_accounts &#61; list&#40;string&#41;&#10;target_resources &#61; list&#40;string&#41;&#10;logging &#61; bool&#10;&#125;&#41;&#41;&#41;">map(map(object({...})))</code> | | <code title="">{}</code> |
| *firewall_policy_attachments* | List of hierarchical firewall policy IDs to *attach* to the organization | <code title="map&#40;string&#41;">map(string)</code> | | <code title="">{}</code> |
| *firewall_policies* | Hierarchical firewall policy rules created in the organization. | <code title="map&#40;map&#40;object&#40;&#123;&#10;action &#61; string&#10;description &#61; string&#10;direction &#61; string&#10;logging &#61; bool&#10;ports &#61; map&#40;list&#40;string&#41;&#41;&#10;priority &#61; number&#10;ranges &#61; list&#40;string&#41;&#10;target_resources &#61; list&#40;string&#41;&#10;target_service_accounts &#61; list&#40;string&#41;&#10;&#125;&#41;&#41;&#41;">map(map(object({...})))</code> | | <code title="">{}</code> |
| *firewall_policy_attachments* | List of hierarchical firewall policy IDs attached to the organization. | <code title="map&#40;string&#41;">map(string)</code> | | <code title="">{}</code> |
| *firewall_policy_factory* | Configuration for the firewall policy factory. | <code title="object&#40;&#123;&#10;cidr_file &#61; string&#10;policy_name &#61; string&#10;rules_file &#61; string&#10;&#125;&#41;">object({...})</code> | | <code title="">null</code> |
| *group_iam* | Authoritative IAM binding for organization groups, in {GROUP_EMAIL => [ROLES]} format. Group emails need to be static. Can be used in combination with the `iam` variable. | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">{}</code> |
| *iam* | IAM bindings, in {ROLE => [MEMBERS]} format. | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">{}</code> |
| *iam_additive* | Non authoritative IAM bindings, in {ROLE => [MEMBERS]} format. | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">{}</code> |
@ -190,7 +254,7 @@ module "org" {
| *iam_audit_config_authoritative* | IAM Authoritative service audit logging configuration. Service as key, map of log permission (eg DATA_READ) and excluded members as value for each service. Audit config should also be authoritative when using authoritative bindings. Use with caution. | <code title="map&#40;map&#40;list&#40;string&#41;&#41;&#41;">map(map(list(string)))</code> | | <code title="">null</code> |
| *iam_bindings_authoritative* | IAM authoritative bindings, in {ROLE => [MEMBERS]} format. Roles and members not explicitly listed will be cleared. Bindings should also be authoritative when using authoritative audit config. Use with caution. | <code title="map&#40;list&#40;string&#41;&#41;">map(list(string))</code> | | <code title="">null</code> |
| *logging_exclusions* | Logging exclusions for this organization in the form {NAME -> FILTER}. | <code title="map&#40;string&#41;">map(string)</code> | | <code title="">{}</code> |
| *logging_sinks* | Logging sinks to create for this organization. | <code title="map&#40;object&#40;&#123;&#10;destination &#61; string&#10;type &#61; string&#10;filter &#61; string&#10;iam &#61; bool&#10;include_children &#61; bool&#10;bq_partitioned_table &#61; bool&#10;exclusions &#61; map&#40;string&#41;&#10;&#125;&#41;&#41;">map(object({...}))</code> | | <code title="">{}</code> |
| *logging_sinks* | Logging sinks to create for this organization. | <code title="map&#40;object&#40;&#123;&#10;destination &#61; string&#10;type &#61; string&#10;filter &#61; string&#10;iam &#61; bool&#10;include_children &#61; bool&#10;bq_partitioned_table &#61; bool&#10;exclusions &#61; map&#40;string&#41;&#10;&#125;&#41;&#41;&#10;validation &#123;&#10;condition &#61; alltrue&#40;&#91;&#10;for k, v in&#40;var.logging_sinks &#61;&#61; null &#63; &#123;&#125; : var.logging_sinks&#41; :&#10;contains&#40;&#91;&#34;bigquery&#34;, &#34;logging&#34;, &#34;pubsub&#34;, &#34;storage&#34;&#93;, v.type&#41;&#10;&#93;&#41;&#10;error_message &#61; &#34;Type must be one of &#39;bigquery&#39;, &#39;logging&#39;, &#39;pubsub&#39;, &#39;storage&#39;.&#34;&#10;&#125;">map(object({...}))</code> | | <code title="">{}</code> |
| *policy_boolean* | Map of boolean org policies and enforcement value, set value to null for policy restore. | <code title="map&#40;bool&#41;">map(bool)</code> | | <code title="">{}</code> |
| *policy_list* | Map of list org policies, status is true for allow, false for deny, null for restore. Values can only be used for allow or deny. | <code title="map&#40;object&#40;&#123;&#10;inherit_from_parent &#61; bool&#10;suggested_value &#61; string&#10;status &#61; bool&#10;values &#61; list&#40;string&#41;&#10;&#125;&#41;&#41;">map(object({...}))</code> | | <code title="">{}</code> |

View File

@ -0,0 +1,104 @@
/**
* Copyright 2021 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_cidrs = try(
yamldecode(file(var.firewall_policy_factory.cidr_file)), {}
)
_factory_name = (
try(var.firewall_policy_factory.policy_name, null) == null
? "factory"
: var.firewall_policy_factory.policy_name
)
_factory_rules = try(
yamldecode(file(var.firewall_policy_factory.rules_file)), {}
)
_factory_rules_parsed = {
for name, rule in local._factory_rules : name => merge(rule, {
ranges = flatten([
for r in(rule.ranges == null ? [] : rule.ranges) :
lookup(local._factory_cidrs, trimprefix(r, "$"), r)
])
})
}
_merged_rules = flatten([
for policy, rules in local.firewall_policies : [
for name, rule in rules : merge(rule, {
policy = policy
name = name
})
]
])
firewall_policies = merge(var.firewall_policies, (
length(local._factory_rules) == 0
? {}
: { (local._factory_name) = local._factory_rules_parsed }
))
firewall_rules = {
for r in local._merged_rules : "${r.policy}-${r.name}" => r
}
}
resource "google_compute_organization_security_policy" "policy" {
provider = google-beta
for_each = local.firewall_policies
display_name = each.key
parent = var.organization_id
depends_on = [
google_organization_iam_audit_config.config,
google_organization_iam_binding.authoritative,
google_organization_iam_custom_role.roles,
google_organization_iam_member.additive,
google_organization_iam_policy.authoritative,
]
}
resource "google_compute_organization_security_policy_rule" "rule" {
provider = google-beta
for_each = local.firewall_rules
policy_id = google_compute_organization_security_policy.policy[each.value.policy].id
action = each.value.action
direction = each.value.direction
priority = try(each.value.priority, null)
target_resources = try(each.value.target_resources, null)
target_service_accounts = try(each.value.target_service_accounts, null)
enable_logging = try(each.value.logging, null)
# preview = each.value.preview
match {
description = each.value.description
config {
src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null
dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null
dynamic "layer4_config" {
for_each = each.value.ports
iterator = port
content {
ip_protocol = port.key
ports = port.value
}
}
}
}
}
resource "google_compute_organization_security_policy_association" "attachment" {
provider = google-beta
for_each = var.firewall_policy_attachments
name = "${var.organization_id}-${each.key}"
attachment_id = var.organization_id
policy_id = each.value
}

117
modules/organization/iam.tf Normal file
View File

@ -0,0 +1,117 @@
/**
* Copyright 2021 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 {
_group_iam_roles = distinct(flatten(values(var.group_iam)))
_group_iam = {
for r in local._group_iam_roles : r => [
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
_iam_additive_pairs = flatten([
for role, members in var.iam_additive : [
for member in members : { role = role, member = member }
]
])
_iam_additive_member_pairs = flatten([
for member, roles in var.iam_additive_members : [
for role in roles : { role = role, member = member }
]
])
iam = {
for role in distinct(concat(keys(var.iam), keys(local._group_iam))) :
role => concat(
try(var.iam[role], []),
try(local._group_iam[role], [])
)
}
iam_additive = {
for pair in concat(local._iam_additive_pairs, local._iam_additive_member_pairs) :
"${pair.role}-${pair.member}" => pair
}
}
resource "google_organization_iam_custom_role" "roles" {
for_each = var.custom_roles
org_id = local.organization_id_numeric
role_id = each.key
title = "Custom role ${each.key}"
description = "Terraform-managed"
permissions = each.value
}
resource "google_organization_iam_binding" "authoritative" {
for_each = local.iam
org_id = local.organization_id_numeric
role = each.key
members = each.value
}
resource "google_organization_iam_member" "additive" {
for_each = (
length(var.iam_additive) + length(var.iam_additive_members) > 0
? local.iam_additive
: {}
)
org_id = local.organization_id_numeric
role = each.value.role
member = each.value.member
}
resource "google_organization_iam_policy" "authoritative" {
count = var.iam_bindings_authoritative != null || var.iam_audit_config_authoritative != null ? 1 : 0
org_id = local.organization_id_numeric
policy_data = data.google_iam_policy.authoritative.policy_data
}
data "google_iam_policy" "authoritative" {
dynamic "binding" {
for_each = var.iam_bindings_authoritative != null ? var.iam_bindings_authoritative : {}
content {
role = binding.key
members = binding.value
}
}
dynamic "audit_config" {
for_each = var.iam_audit_config_authoritative != null ? var.iam_audit_config_authoritative : {}
content {
service = audit_config.key
dynamic "audit_log_configs" {
for_each = audit_config.value
iterator = config
content {
log_type = config.key
exempted_members = config.value
}
}
}
}
}
resource "google_organization_iam_audit_config" "config" {
for_each = var.iam_audit_config
org_id = local.organization_id_numeric
service = each.key
dynamic "audit_log_config" {
for_each = each.value
iterator = config
content {
log_type = config.key
exempted_members = config.value
}
}
}

View File

@ -0,0 +1,95 @@
/**
* Copyright 2021 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 {
logging_sinks = coalesce(var.logging_sinks, {})
sink_bindings = {
for type in ["bigquery", "logging", "pubsub", "storage"] :
type => {
for name, sink in local.logging_sinks :
name => sink if sink.iam && sink.type == type
}
}
}
resource "google_logging_organization_sink" "sink" {
for_each = local.logging_sinks
name = each.key
org_id = local.organization_id_numeric
destination = "${each.value.type}.googleapis.com/${each.value.destination}"
filter = each.value.filter
include_children = each.value.include_children
dynamic "bigquery_options" {
for_each = each.value.bq_partitioned_table == true ? [""] : []
content {
use_partitioned_tables = each.value.bq_partitioned_table
}
}
dynamic "exclusions" {
for_each = each.value.exclusions
iterator = exclusion
content {
name = exclusion.key
filter = exclusion.value
}
}
depends_on = [
google_organization_iam_binding.authoritative,
google_organization_iam_member.additive,
google_organization_iam_policy.authoritative,
]
}
resource "google_storage_bucket_iam_member" "storage-sinks-binding" {
for_each = local.sink_bindings["storage"]
bucket = each.value.destination
role = "roles/storage.objectCreator"
member = google_logging_organization_sink.sink[each.key].writer_identity
}
resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" {
for_each = local.sink_bindings["bigquery"]
project = split("/", each.value.destination)[1]
dataset_id = split("/", each.value.destination)[3]
role = "roles/bigquery.dataEditor"
member = google_logging_organization_sink.sink[each.key].writer_identity
}
resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" {
for_each = local.sink_bindings["pubsub"]
project = split("/", each.value.destination)[1]
topic = split("/", each.value.destination)[3]
role = "roles/pubsub.publisher"
member = google_logging_organization_sink.sink[each.key].writer_identity
}
resource "google_project_iam_member" "bucket-sinks-binding" {
for_each = local.sink_bindings["logging"]
project = split("/", each.value.destination)[1]
role = "roles/logging.bucketWriter"
member = google_logging_organization_sink.sink[each.key].writer_identity
# TODO(jccb): use a condition to limit writer-identity only to this bucket
}
resource "google_logging_organization_exclusion" "logging-exclusion" {
for_each = coalesce(var.logging_exclusions, {})
name = each.key
org_id = local.organization_id_numeric
description = "${each.key} (Terraform-managed)"
filter = each.value
}

View File

@ -16,130 +16,6 @@
locals {
organization_id_numeric = split("/", var.organization_id)[1]
group_iam_roles = distinct(flatten(values(var.group_iam)))
group_iam = {
for r in local.group_iam_roles : r => [
for k, v in var.group_iam : "group:${k}" if try(index(v, r), null) != null
]
}
iam = {
for role in distinct(concat(keys(var.iam), keys(local.group_iam))) :
role => concat(
try(var.iam[role], []),
try(local.group_iam[role], [])
)
}
iam_additive_pairs = flatten([
for role, members in var.iam_additive : [
for member in members : { role = role, member = member }
]
])
iam_additive_member_pairs = flatten([
for member, roles in var.iam_additive_members : [
for role in roles : { role = role, member = member }
]
])
iam_additive = {
for pair in concat(local.iam_additive_pairs, local.iam_additive_member_pairs) :
"${pair.role}-${pair.member}" => pair
}
extended_rules = flatten([
for policy, rules in var.firewall_policies : [
for rule_name, rule in rules :
merge(rule, { policy = policy, name = rule_name })
]
])
rules_map = {
for rule in local.extended_rules :
"${rule.policy}-${rule.name}" => rule
}
logging_sinks = coalesce(var.logging_sinks, {})
sink_type_destination = {
gcs = "storage.googleapis.com"
bigquery = "bigquery.googleapis.com"
pubsub = "pubsub.googleapis.com"
logging = "logging.googleapis.com"
}
sink_bindings = {
for type in ["gcs", "bigquery", "pubsub", "logging"] :
type => {
for name, sink in local.logging_sinks :
name => sink
if sink.iam && sink.type == type
}
}
}
resource "google_organization_iam_custom_role" "roles" {
for_each = var.custom_roles
org_id = local.organization_id_numeric
role_id = each.key
title = "Custom role ${each.key}"
description = "Terraform-managed"
permissions = each.value
}
resource "google_organization_iam_binding" "authoritative" {
for_each = local.iam
org_id = local.organization_id_numeric
role = each.key
members = each.value
}
resource "google_organization_iam_member" "additive" {
for_each = (
length(var.iam_additive) + length(var.iam_additive_members) > 0
? local.iam_additive
: {}
)
org_id = local.organization_id_numeric
role = each.value.role
member = each.value.member
}
resource "google_organization_iam_policy" "authoritative" {
count = var.iam_bindings_authoritative != null || var.iam_audit_config_authoritative != null ? 1 : 0
org_id = local.organization_id_numeric
policy_data = data.google_iam_policy.authoritative.policy_data
}
data "google_iam_policy" "authoritative" {
dynamic "binding" {
for_each = var.iam_bindings_authoritative != null ? var.iam_bindings_authoritative : {}
content {
role = binding.key
members = binding.value
}
}
dynamic "audit_config" {
for_each = var.iam_audit_config_authoritative != null ? var.iam_audit_config_authoritative : {}
content {
service = audit_config.key
dynamic "audit_log_configs" {
for_each = audit_config.value
iterator = config
content {
log_type = config.key
exempted_members = config.value
}
}
}
}
}
resource "google_organization_iam_audit_config" "config" {
for_each = var.iam_audit_config
org_id = local.organization_id_numeric
service = each.key
dynamic "audit_log_config" {
for_each = each.value
iterator = config
content {
log_type = config.key
exempted_members = config.value
}
}
}
resource "google_organization_policy" "boolean" {
@ -231,126 +107,6 @@ resource "google_organization_policy" "list" {
]
}
resource "google_compute_organization_security_policy" "policy" {
provider = google-beta
for_each = var.firewall_policies
display_name = each.key
parent = var.organization_id
depends_on = [
google_organization_iam_audit_config.config,
google_organization_iam_binding.authoritative,
google_organization_iam_custom_role.roles,
google_organization_iam_member.additive,
google_organization_iam_policy.authoritative,
]
}
resource "google_compute_organization_security_policy_rule" "rule" {
provider = google-beta
for_each = local.rules_map
policy_id = google_compute_organization_security_policy.policy[each.value.policy].id
action = each.value.action
direction = each.value.direction
priority = each.value.priority
target_resources = each.value.target_resources
target_service_accounts = each.value.target_service_accounts
enable_logging = each.value.logging
# preview = each.value.preview
match {
description = each.value.description
config {
src_ip_ranges = each.value.direction == "INGRESS" ? each.value.ranges : null
dest_ip_ranges = each.value.direction == "EGRESS" ? each.value.ranges : null
dynamic "layer4_config" {
for_each = each.value.ports
iterator = port
content {
ip_protocol = port.key
ports = port.value
}
}
}
}
}
resource "google_compute_organization_security_policy_association" "attachment" {
provider = google-beta
for_each = var.firewall_policy_attachments
name = "${var.organization_id}-${each.key}"
attachment_id = var.organization_id
policy_id = each.value
}
resource "google_logging_organization_sink" "sink" {
for_each = local.logging_sinks
name = each.key
org_id = local.organization_id_numeric
destination = "${local.sink_type_destination[each.value.type]}/${each.value.destination}"
filter = each.value.filter
include_children = each.value.include_children
dynamic "bigquery_options" {
for_each = each.value.bq_partitioned_table == true ? [""] : []
content {
use_partitioned_tables = each.value.bq_partitioned_table
}
}
dynamic "exclusions" {
for_each = each.value.exclusions
iterator = exclusion
content {
name = exclusion.key
filter = exclusion.value
}
}
depends_on = [
google_organization_iam_binding.authoritative,
google_organization_iam_member.additive,
google_organization_iam_policy.authoritative,
]
}
resource "google_storage_bucket_iam_member" "gcs-sinks-binding" {
for_each = local.sink_bindings["gcs"]
bucket = each.value.destination
role = "roles/storage.objectCreator"
member = google_logging_organization_sink.sink[each.key].writer_identity
}
resource "google_bigquery_dataset_iam_member" "bq-sinks-binding" {
for_each = local.sink_bindings["bigquery"]
project = split("/", each.value.destination)[1]
dataset_id = split("/", each.value.destination)[3]
role = "roles/bigquery.dataEditor"
member = google_logging_organization_sink.sink[each.key].writer_identity
}
resource "google_pubsub_topic_iam_member" "pubsub-sinks-binding" {
for_each = local.sink_bindings["pubsub"]
project = split("/", each.value.destination)[1]
topic = split("/", each.value.destination)[3]
role = "roles/pubsub.publisher"
member = google_logging_organization_sink.sink[each.key].writer_identity
}
resource "google_project_iam_member" "bucket-sinks-binding" {
for_each = local.sink_bindings["logging"]
project = split("/", each.value.destination)[1]
role = "roles/logging.bucketWriter"
member = google_logging_organization_sink.sink[each.key].writer_identity
# TODO(jccb): use a condition to limit writer-identity only to this
# bucket
}
resource "google_logging_organization_exclusion" "logging-exclusion" {
for_each = coalesce(var.logging_exclusions, {})
name = each.key
org_id = local.organization_id_numeric
description = "${each.key} (Terraform-managed)"
filter = each.value
}
resource "google_essential_contacts_contact" "contact" {
provider = google-beta
for_each = var.contacts

View File

@ -27,27 +27,36 @@ variable "custom_roles" {
}
variable "firewall_policies" {
description = "Hierarchical firewall policies to *create* in the organization."
description = "Hierarchical firewall policy rules created in the organization."
type = map(map(object({
action = string
description = string
direction = string
action = string
logging = bool
ports = map(list(string))
priority = number
ranges = list(string)
ports = map(list(string))
target_service_accounts = list(string)
target_resources = list(string)
logging = bool
#preview = bool
target_service_accounts = list(string)
# preview = bool
})))
default = {}
}
variable "firewall_policy_attachments" {
description = "List of hierarchical firewall policy IDs to *attach* to the organization"
# set to avoid manual casting with toset()
type = map(string)
default = {}
description = "List of hierarchical firewall policy IDs attached to the organization."
type = map(string)
default = {}
}
variable "firewall_policy_factory" {
description = "Configuration for the firewall policy factory."
type = object({
cidr_file = string
policy_name = string
rules_file = string
})
default = null
}
variable "group_iam" {
@ -120,6 +129,13 @@ variable "logging_sinks" {
# TODO exclusions also support description and disabled
exclusions = map(string)
}))
validation {
condition = alltrue([
for k, v in(var.logging_sinks == null ? {} : var.logging_sinks) :
contains(["bigquery", "logging", "pubsub", "storage"], v.type)
])
error_message = "Type must be one of 'bigquery', 'logging', 'pubsub', 'storage'."
}
default = {}
}

View File

@ -0,0 +1,18 @@
# Copyright 2021 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.
rfc1918:
- 10.0.0.0/8
- 172.168.0.0/12
- 192.168.0.0/16

View File

@ -0,0 +1,37 @@
# Copyright 2021 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.
allow-admins:
description: Access from the admin subnet to all subnets
direction: INGRESS
action: allow
priority: 1000
ranges:
- $rfc1918
ports:
all: []
target_resources: null
enable_logging: false
allow-ssh-from-iap:
description: Enable SSH from IAP
direction: INGRESS
action: allow
priority: 1002
ranges:
- 35.235.240.0/20
ports:
tcp: ["22"]
target_resources: null
enable_logging: false

View File

@ -27,6 +27,7 @@ module "test" {
policy_list = var.policy_list
firewall_policies = var.firewall_policies
firewall_policy_attachments = var.firewall_policy_attachments
firewall_policy_factory = var.firewall_policy_factory
logging_sinks = var.logging_sinks
logging_exclusions = var.logging_exclusions
}

View File

@ -79,6 +79,15 @@ variable "firewall_policy_attachments" {
default = {}
}
variable "firewall_policy_factory" {
type = object({
cidr_file = string
policy_name = string
rules_file = string
})
default = null
}
variable "logging_sinks" {
type = map(object({
destination = string

View File

@ -110,78 +110,3 @@ def test_policy_list(plan_runner):
assert values[1]['list_policy'][0]['deny'] == [
{'all': False, 'values': ["bar"]}]
assert values[2]['restore_policy'] == [{'default': True}]
def test_firweall_policy(plan_runner):
"Test boolean folder policy."
policy = """
{
policy1 = {
allow-ingress = {
description = ""
direction = "INGRESS"
action = "allow"
priority = 100
ranges = ["10.0.0.0/8"]
ports = {
tcp = ["22"]
}
target_service_accounts = null
target_resources = null
logging = false
}
deny-egress = {
description = ""
direction = "EGRESS"
action = "deny"
priority = 200
ranges = ["192.168.0.0/24"]
ports = {
tcp = ["443"]
}
target_service_accounts = null
target_resources = null
logging = false
}
}
}
"""
attachment = '{ iap_policy = "policy1" }'
_, resources = plan_runner(FIXTURES_DIR, firewall_policies=policy,
firewall_policy_attachments=attachment)
assert len(resources) == 4
policies = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy']
assert len(policies) == 1
rules = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy_rule']
assert len(rules) == 2
rule_values = []
for rule in rules:
name = rule['name']
index = rule['index']
action = rule['values']['action']
direction = rule['values']['direction']
priority = rule['values']['priority']
config = rule['values']['match']
assert len(config) == 1
config = config[0]['config']
rule_values.append((name, index, action, direction, priority, config))
assert sorted(rule_values) == sorted([
('rule', 'policy1-allow-ingress', 'allow', 'INGRESS', 100, [
{
'dest_ip_ranges': None,
'layer4_config': [{'ip_protocol': 'tcp', 'ports': ['22']}],
'src_ip_ranges': ['10.0.0.0/8']
}]),
('rule', 'policy1-deny-egress', 'deny', 'EGRESS', 200, [
{
'dest_ip_ranges': ['192.168.0.0/24'],
'layer4_config': [{'ip_protocol': 'tcp', 'ports': ['443']}],
'src_ip_ranges': None
}])
])

View File

@ -0,0 +1,137 @@
# Copyright 2021 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.
import os
import pytest
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixture')
_FACTORY = '''
{
cidr_file = "data/firewall-cidrs.yaml"
policy_name = "factory-1"
rules_file = "data/firewall-rules.yaml"
}
'''
_POLICIES = '''
{
policy1 = {
allow-ingress = {
description = ""
direction = "INGRESS"
action = "allow"
priority = 100
ranges = ["10.0.0.0/8"]
ports = {
tcp = ["22"]
}
target_service_accounts = null
target_resources = null
logging = false
}
deny-egress = {
description = ""
direction = "EGRESS"
action = "deny"
priority = 200
ranges = ["192.168.0.0/24"]
ports = {
tcp = ["443"]
}
target_service_accounts = null
target_resources = null
logging = false
}
}
policy2 = {
allow-ingress = {
description = ""
direction = "INGRESS"
action = "allow"
priority = 100
ranges = ["10.0.0.0/8"]
ports = {
tcp = ["22"]
}
target_service_accounts = null
target_resources = null
logging = false
}
}
}
'''
def test_custom(plan_runner):
'Test custom firewall policies.'
_, resources = plan_runner(FIXTURES_DIR, firewall_policies=_POLICIES)
assert len(resources) == 5
policies = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy']
rules = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy_rule']
assert set(r['index'] for r in policies) == set([
'policy1', 'policy2'
])
assert set(r['index'] for r in rules) == set([
'policy1-deny-egress', 'policy2-allow-ingress', 'policy1-allow-ingress'
])
def test_factory(plan_runner):
'Test firewall policy factory.'
_, resources = plan_runner(FIXTURES_DIR, firewall_policy_factory=_FACTORY)
assert len(resources) == 3
policies = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy']
rules = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy_rule']
assert set(r['index'] for r in policies) == set([
'factory-1'
])
assert set(r['index'] for r in rules) == set([
'factory-1-allow-admins', 'factory-1-allow-ssh-from-iap'
])
def test_factory_name(plan_runner):
'Test firewall policy factory default name.'
factory = _FACTORY.replace('"factory-1"', 'null')
_, resources = plan_runner(FIXTURES_DIR, firewall_policy_factory=factory)
assert len(resources) == 3
policies = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy']
assert set(r['index'] for r in policies) == set([
'factory'
])
def test_combined(plan_runner):
'Test combined rules.'
_, resources = plan_runner(FIXTURES_DIR, firewall_policies=_POLICIES,
firewall_policy_factory=_FACTORY)
assert len(resources) == 8
policies = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy']
rules = [r for r in resources
if r['type'] == 'google_compute_organization_security_policy_rule']
assert set(r['index'] for r in policies) == set([
'factory-1', 'policy1', 'policy2'
])
assert set(r['index'] for r in rules) == set([
'factory-1-allow-admins', 'factory-1-allow-ssh-from-iap',
'policy1-deny-egress', 'policy2-allow-ingress', 'policy1-allow-ingress'
])

View File

@ -25,7 +25,7 @@ def test_sinks(plan_runner):
"Test folder-level sinks."
logging_sinks = """ {
warning = {
type = "gcs"
type = "storage"
destination = "mybucket"
filter = "severity=WARNING"
iam = true