From 1907c38e2257508cb527fa3fdbeac83da1ff1ed2 Mon Sep 17 00:00:00 2001 From: kovagoadam Date: Thu, 21 May 2026 02:38:06 +0000 Subject: [PATCH] Add IAM deny policies support (#3970) * Added IAM denial policies * Moved default to empty, removed trys, added condition vars to expression * remove redundant null checks * reduce line length * boilerplate and principal context expansion * update readmes * add explicit validation against null values * add context tests * Add missing license headers to examples --------- Co-authored-by: Julio Castillo --- fast/stages/0-org-setup/organization.tf | 3 + .../0-org-setup/schemas/folder.schema.json | 79 +++++++++++++++++++ .../0-org-setup/schemas/folder.schema.md | 23 ++++++ .../0-org-setup/schemas/project.schema.json | 79 +++++++++++++++++++ .../0-org-setup/schemas/project.schema.md | 23 ++++++ .../2-networking/schemas/folder.schema.json | 79 +++++++++++++++++++ .../2-networking/schemas/folder.schema.md | 23 ++++++ .../2-networking/schemas/project.schema.json | 79 +++++++++++++++++++ .../2-networking/schemas/project.schema.md | 23 ++++++ .../schemas/folder.schema.json | 79 +++++++++++++++++++ .../schemas/folder.schema.md | 23 ++++++ .../schemas/project.schema.json | 79 +++++++++++++++++++ .../schemas/project.schema.md | 23 ++++++ .../2-security/schemas/folder.schema.json | 79 +++++++++++++++++++ .../2-security/schemas/folder.schema.md | 23 ++++++ .../2-security/schemas/project.schema.json | 79 +++++++++++++++++++ .../2-security/schemas/project.schema.md | 23 ++++++ modules/folder/README.md | 51 ++++++++++++ modules/folder/deny-policies.tf | 52 ++++++++++++ modules/folder/variables-iam.tf | 36 +++++++++ modules/organization/README.md | 50 ++++++++++++ modules/organization/deny-policies.tf | 52 ++++++++++++ modules/organization/variables-iam.tf | 36 +++++++++ modules/project-factory/folders.tf | 4 + modules/project-factory/projects.tf | 3 +- .../schemas/folder.schema.json | 79 +++++++++++++++++++ .../project-factory/schemas/folder.schema.md | 23 ++++++ .../schemas/project.schema.json | 79 +++++++++++++++++++ .../project-factory/schemas/project.schema.md | 23 ++++++ modules/project/README.md | 52 ++++++++++++ modules/project/deny-policies.tf | 52 ++++++++++++ modules/project/variables-iam.tf | 36 +++++++++ tests/modules/folder/context.tfvars | 18 +++++ tests/modules/folder/context.yaml | 35 +++++++- .../folder/examples/iam-deny-policies.yaml | 63 +++++++++++++++ tests/modules/organization/context.tfvars | 18 +++++ tests/modules/organization/context.yaml | 50 +++++++++++- .../examples/iam-deny-policies.yaml | 58 ++++++++++++++ tests/modules/project/context.tfvars | 18 +++++ tests/modules/project/context.yaml | 59 +++++++++++++- .../project/examples/iam-deny-policies.yaml | 73 +++++++++++++++++ 41 files changed, 1829 insertions(+), 10 deletions(-) create mode 100644 modules/folder/deny-policies.tf create mode 100644 modules/organization/deny-policies.tf create mode 100644 modules/project/deny-policies.tf create mode 100644 tests/modules/folder/examples/iam-deny-policies.yaml create mode 100644 tests/modules/organization/examples/iam-deny-policies.yaml create mode 100644 tests/modules/project/examples/iam-deny-policies.yaml diff --git a/fast/stages/0-org-setup/organization.tf b/fast/stages/0-org-setup/organization.tf index c1da161dc..b0e05b4f0 100644 --- a/fast/stages/0-org-setup/organization.tf +++ b/fast/stages/0-org-setup/organization.tf @@ -173,4 +173,7 @@ module "organization-iam" { tags_config = { force_context_ids = true } + iam_deny_policies = lookup( + local.organization, "iam_deny_policies", {} + ) } diff --git a/fast/stages/0-org-setup/schemas/folder.schema.json b/fast/stages/0-org-setup/schemas/folder.schema.json index 166cb1e3f..84a7071c5 100644 --- a/fast/stages/0-org-setup/schemas/folder.schema.json +++ b/fast/stages/0-org-setup/schemas/folder.schema.json @@ -357,6 +357,85 @@ "iam_by_principals_conditional": { "$ref": "#/$defs/iam_by_principals_conditional" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "name": { "type": "string" }, diff --git a/fast/stages/0-org-setup/schemas/folder.schema.md b/fast/stages/0-org-setup/schemas/folder.schema.md index 2a89323f7..996034e8b 100644 --- a/fast/stages/0-org-setup/schemas/folder.schema.md +++ b/fast/stages/0-org-setup/schemas/folder.schema.md @@ -112,6 +112,29 @@ - **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **name**: *string* - **org_policies**: *object*
*additional properties: false* diff --git a/fast/stages/0-org-setup/schemas/project.schema.json b/fast/stages/0-org-setup/schemas/project.schema.json index 94c224062..3632daa1f 100644 --- a/fast/stages/0-org-setup/schemas/project.schema.json +++ b/fast/stages/0-org-setup/schemas/project.schema.json @@ -344,6 +344,85 @@ "iam_by_principals_additive": { "$ref": "#/$defs/iam_by_principals" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "kms": { "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 31998ca8a..3def29e9c 100644 --- a/fast/stages/0-org-setup/schemas/project.schema.md +++ b/fast/stages/0-org-setup/schemas/project.schema.md @@ -110,6 +110,29 @@ - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* - **iam_by_principals_additive**: *reference([iam_by_principals](#refs-iam_by_principals))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **kms**: *object*
*additional properties: false* - **autokeys**: *object* diff --git a/fast/stages/2-networking/schemas/folder.schema.json b/fast/stages/2-networking/schemas/folder.schema.json index 166cb1e3f..84a7071c5 100644 --- a/fast/stages/2-networking/schemas/folder.schema.json +++ b/fast/stages/2-networking/schemas/folder.schema.json @@ -357,6 +357,85 @@ "iam_by_principals_conditional": { "$ref": "#/$defs/iam_by_principals_conditional" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "name": { "type": "string" }, diff --git a/fast/stages/2-networking/schemas/folder.schema.md b/fast/stages/2-networking/schemas/folder.schema.md index 2a89323f7..996034e8b 100644 --- a/fast/stages/2-networking/schemas/folder.schema.md +++ b/fast/stages/2-networking/schemas/folder.schema.md @@ -112,6 +112,29 @@ - **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **name**: *string* - **org_policies**: *object*
*additional properties: false* diff --git a/fast/stages/2-networking/schemas/project.schema.json b/fast/stages/2-networking/schemas/project.schema.json index 94c224062..3632daa1f 100644 --- a/fast/stages/2-networking/schemas/project.schema.json +++ b/fast/stages/2-networking/schemas/project.schema.json @@ -344,6 +344,85 @@ "iam_by_principals_additive": { "$ref": "#/$defs/iam_by_principals" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "kms": { "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 31998ca8a..3def29e9c 100644 --- a/fast/stages/2-networking/schemas/project.schema.md +++ b/fast/stages/2-networking/schemas/project.schema.md @@ -110,6 +110,29 @@ - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* - **iam_by_principals_additive**: *reference([iam_by_principals](#refs-iam_by_principals))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **kms**: *object*
*additional properties: false* - **autokeys**: *object* diff --git a/fast/stages/2-project-factory/schemas/folder.schema.json b/fast/stages/2-project-factory/schemas/folder.schema.json index 166cb1e3f..84a7071c5 100644 --- a/fast/stages/2-project-factory/schemas/folder.schema.json +++ b/fast/stages/2-project-factory/schemas/folder.schema.json @@ -357,6 +357,85 @@ "iam_by_principals_conditional": { "$ref": "#/$defs/iam_by_principals_conditional" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "name": { "type": "string" }, diff --git a/fast/stages/2-project-factory/schemas/folder.schema.md b/fast/stages/2-project-factory/schemas/folder.schema.md index 2a89323f7..996034e8b 100644 --- a/fast/stages/2-project-factory/schemas/folder.schema.md +++ b/fast/stages/2-project-factory/schemas/folder.schema.md @@ -112,6 +112,29 @@ - **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **name**: *string* - **org_policies**: *object*
*additional properties: false* diff --git a/fast/stages/2-project-factory/schemas/project.schema.json b/fast/stages/2-project-factory/schemas/project.schema.json index 94c224062..3632daa1f 100644 --- a/fast/stages/2-project-factory/schemas/project.schema.json +++ b/fast/stages/2-project-factory/schemas/project.schema.json @@ -344,6 +344,85 @@ "iam_by_principals_additive": { "$ref": "#/$defs/iam_by_principals" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "kms": { "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 31998ca8a..3def29e9c 100644 --- a/fast/stages/2-project-factory/schemas/project.schema.md +++ b/fast/stages/2-project-factory/schemas/project.schema.md @@ -110,6 +110,29 @@ - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* - **iam_by_principals_additive**: *reference([iam_by_principals](#refs-iam_by_principals))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **kms**: *object*
*additional properties: false* - **autokeys**: *object* diff --git a/fast/stages/2-security/schemas/folder.schema.json b/fast/stages/2-security/schemas/folder.schema.json index 166cb1e3f..84a7071c5 100644 --- a/fast/stages/2-security/schemas/folder.schema.json +++ b/fast/stages/2-security/schemas/folder.schema.json @@ -357,6 +357,85 @@ "iam_by_principals_conditional": { "$ref": "#/$defs/iam_by_principals_conditional" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "name": { "type": "string" }, diff --git a/fast/stages/2-security/schemas/folder.schema.md b/fast/stages/2-security/schemas/folder.schema.md index 2a89323f7..996034e8b 100644 --- a/fast/stages/2-security/schemas/folder.schema.md +++ b/fast/stages/2-security/schemas/folder.schema.md @@ -112,6 +112,29 @@ - **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **name**: *string* - **org_policies**: *object*
*additional properties: false* diff --git a/fast/stages/2-security/schemas/project.schema.json b/fast/stages/2-security/schemas/project.schema.json index 94c224062..3632daa1f 100644 --- a/fast/stages/2-security/schemas/project.schema.json +++ b/fast/stages/2-security/schemas/project.schema.json @@ -344,6 +344,85 @@ "iam_by_principals_additive": { "$ref": "#/$defs/iam_by_principals" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "kms": { "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 31998ca8a..3def29e9c 100644 --- a/fast/stages/2-security/schemas/project.schema.md +++ b/fast/stages/2-security/schemas/project.schema.md @@ -110,6 +110,29 @@ - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* - **iam_by_principals_additive**: *reference([iam_by_principals](#refs-iam_by_principals))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **kms**: *object*
*additional properties: false* - **autokeys**: *object* diff --git a/modules/folder/README.md b/modules/folder/README.md index 2a56ae60a..779b32f62 100644 --- a/modules/folder/README.md +++ b/modules/folder/README.md @@ -23,6 +23,7 @@ This module allows the creation and management of folders, including support for - [Cloud Asset Search](#cloud-asset-search) - [Cloud Asset Inventory Feeds](#cloud-asset-inventory-feeds) - [Tags](#tags) +- [IAM Deny Policies](#iam-deny-policies) - [Files](#files) - [Variables](#variables) - [Outputs](#outputs) @@ -726,6 +727,54 @@ module "folder" { # tftest modules=2 resources=5 inventory=tags.yaml e2e serial ``` +## IAM Deny Policies + +[IAM Deny policies](https://cloud.google.com/iam/docs/deny-overview) allow you to set centralized guardrails that prevent principals from using specific permissions within the folder and all of its descendants, regardless of the roles they have been granted. + +You can define Deny policies using the `iam_deny_policies` variable. Each policy requires you to specify the principals and permissions to deny. You can optionally define exception principals, exception permissions, and conditions to tailor the restriction. + +Note that IAM Deny policies require a specific prefix for principal definitions (e.g., `principalSet://goog/public:all` or `principalSet://goog/group/group-email@example.com`), and permissions must be prefixed with the service fully qualified domain name (e.g., `iam.googleapis.com/serviceAccountKeys.create`). + +```hcl +module "folder" { + source = "./fabric/modules/folder" + parent = var.folder_id + name = "Folder name" + + iam_deny_policies = { + "prevent-key-creation" = { + display_name = "Prevent SA key creation" + rules = [ + { + description = "Deny service account key creation to all except the folder admin group." + denied_principals = ["principalSet://goog/public:all"] + denied_permissions = ["iam.googleapis.com/serviceAccountKeys.create"] + exception_principals = [ + "principalSet://goog/group/gcp-folder-admins@example.com" + ] + } + ] + } + "conditional-delete-deny" = { + display_name = "Conditional instance deletion deny" + rules = [ + { + description = "Deny deletion of compute instances based on resource tags." + denied_principals = ["principalSet://goog/public:all"] + denied_permissions = ["compute.googleapis.com/instances.delete"] + denial_condition = { + title = "prevent_prod_deletion" + description = "Prevent deletion of instances tagged as production." + expression = "resource.matchTag('123456789012/environment', 'prod')" + } + } + ] + } + } +} +# tftest modules=1 resources=3 inventory=iam-deny-policies.yaml +``` + ## Files @@ -733,6 +782,7 @@ module "folder" { | name | description | resources | |---|---|---| | [assets.tf](./assets.tf) | None | google_cloud_asset_folder_feed | +| [deny-policies.tf](./deny-policies.tf) | IAM Deny policies. | google_iam_deny_policy | | [iam.tf](./iam.tf) | IAM bindings. | google_folder_iam_binding · google_folder_iam_member | | [logging.tf](./logging.tf) | Log sinks and supporting resources. | google_bigquery_dataset_iam_member · google_folder_iam_audit_config · google_logging_folder_exclusion · google_logging_folder_settings · google_logging_folder_sink · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_assured_workloads_workload · google_compute_firewall_policy_association · google_essential_contacts_contact · google_folder · google_kms_autokey_config | @@ -770,6 +820,7 @@ module "folder" { | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | +| [iam_deny_policies](variables-iam.tf#L98) | IAM Deny policies to be applied to the folder. | map(object({…})) | | {} | | [id](variables.tf#L236) | Folder ID in case you use folder_create=false. | string | | null | | [logging_data_access](variables-logging.tf#L17) | Control activation of data access logs. The special 'allServices' key denotes configuration for all services. | map(object({…})) | | {} | | [logging_exclusions](variables-logging.tf#L28) | Logging exclusions for this folder in the form {NAME -> FILTER}. | map(string) | | {} | diff --git a/modules/folder/deny-policies.tf b/modules/folder/deny-policies.tf new file mode 100644 index 000000000..d5de9337e --- /dev/null +++ b/modules/folder/deny-policies.tf @@ -0,0 +1,52 @@ +/** + * 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. + */ + +# tfdoc:file:description IAM Deny policies. + +resource "google_iam_deny_policy" "default" { + for_each = var.iam_deny_policies + parent = urlencode("cloudresourcemanager.googleapis.com/folders/${local.folder_number}") + name = each.key + display_name = each.value.display_name + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + description = rule.value.description + deny_rule { + denied_principals = [ + for p in rule.value.denied_principals : lookup(local.ctx.iam_principals, p, p) + ] + dynamic "denial_condition" { + for_each = rule.value.denial_condition == null ? [] : [""] + content { + title = rule.value.denial_condition.title + expression = templatestring( + rule.value.denial_condition.expression, var.context.condition_vars + ) + description = rule.value.denial_condition.description + location = rule.value.denial_condition.location + } + } + denied_permissions = rule.value.denied_permissions + exception_principals = [ + for p in rule.value.exception_principals : lookup(local.ctx.iam_principals, p, p) + ] + exception_permissions = rule.value.exception_permissions + } + } + } +} diff --git a/modules/folder/variables-iam.tf b/modules/folder/variables-iam.tf index 3e8eca896..1c12b45b8 100644 --- a/modules/folder/variables-iam.tf +++ b/modules/folder/variables-iam.tf @@ -94,3 +94,39 @@ variable "iam_by_principals_conditional" { error_message = "IAM bindings with the same condition title must have identical expressions and descriptions." } } + +variable "iam_deny_policies" { + description = "IAM Deny policies to be applied to the folder." + type = map(object({ + display_name = optional(string) + rules = list(object({ + description = optional(string) + denied_principals = list(string) + denied_permissions = list(string) + denial_condition = optional(object({ + expression = string + title = optional(string) + description = optional(string) + location = optional(string) + })) + exception_principals = optional(list(string), []) + exception_permissions = optional(list(string), []) + })) + })) + default = {} + nullable = false + validation { + # Ensure denied_principals and denied_permissions are explicitly not null + # (to prevent HCL evaluation errors in loops) and contain at least one + # element (required by the GCP API). + condition = alltrue(flatten([ + for k, v in var.iam_deny_policies : [ + for r in v.rules : ( + try(length(r.denied_principals) > 0, false) && + try(length(r.denied_permissions) > 0, false) + ) + ] + ])) + error_message = "Each rule in iam_deny_policies must have at least one denied principal and one denied permission." + } +} diff --git a/modules/organization/README.md b/modules/organization/README.md index fc61694f2..c792a612b 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -40,6 +40,7 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b - [Tags](#tags) - [Tags Factory](#tags-factory) - [Workforce Identity](#workforce-identity) +- [IAM Deny Policies](#iam-deny-policies) - [Files](#files) - [Variables](#variables) - [Outputs](#outputs) @@ -1010,6 +1011,53 @@ module "org" { # tftest inventory=wfif.yaml ``` +## IAM Deny Policies + +[IAM Deny policies](https://cloud.google.com/iam/docs/deny-overview) allow you to set centralized guardrails that prevent principals from using specific permissions, regardless of the roles they have been granted. + +You can define Deny policies using the `iam_deny_policies` variable. Each policy requires you to specify the principals and permissions to deny, and optionally allows you to define exception principals, exception permissions, and conditions. + +Note that IAM Deny policies require a specific prefix for principal definitions (e.g., `principalSet://goog/public:all` or `principalSet://goog/group/group-email@example.com`). + +```hcl +module "organization" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + + iam_deny_policies = { + "prevent-sa-token-creation" = { + display_name = "Prevent SA token creation" + rules = [ + { + description = "Deny service account token creation to all except the central admin group." + denied_principals = ["principalSet://goog/public:all"] + denied_permissions = ["iam.serviceAccounts.getAccessToken"] + exception_principals = [ + "principalSet://goog/group/gcp-admins@example.com" + ] + } + ] + } + "conditional-key-deny" = { + display_name = "Conditional SA Key Deny" + rules = [ + { + description = "Deny key creation outside of authorized IPs using a condition." + denied_principals = ["principalSet://goog/public:all"] + denied_permissions = ["iam.serviceAccountKeys.create"] + denial_condition = { + title = "ip-restriction" + description = "Restrict access to specific IP ranges" + expression = "!inIpRange(request.auth.access_levels, 'accessPolicies/123456789/accessLevels/trusted_ips')" + } + } + ] + } + } +} +# tftest modules=1 resources=2 inventory=iam-deny-policies.yaml +``` + ## Files @@ -1017,6 +1065,7 @@ module "org" { | name | description | resources | |---|---|---| | [assets.tf](./assets.tf) | None | google_cloud_asset_organization_feed | +| [deny-policies.tf](./deny-policies.tf) | IAM Deny policies. | google_iam_deny_policy | | [iam.tf](./iam.tf) | IAM bindings. | google_organization_iam_binding · google_organization_iam_custom_role · google_organization_iam_member | | [identity-providers.tf](./identity-providers.tf) | Workforce Identity Federation provider definitions. | google_iam_workforce_pool · google_iam_workforce_pool_provider · google_iam_workforce_pool_provider_scim_tenant | | [logging.tf](./logging.tf) | Log sinks and data access logs. | google_bigquery_dataset_iam_member · google_logging_organization_exclusion · google_logging_organization_settings · google_logging_organization_sink · google_organization_iam_audit_config · google_project_iam_member · google_pubsub_topic_iam_member · google_storage_bucket_iam_member | @@ -1056,6 +1105,7 @@ module "org" { | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | +| [iam_deny_policies](variables-iam.tf#L98) | IAM Deny policies to be applied to the organization. | map(object({…})) | | {} | | [logging_data_access](variables-logging.tf#L17) | Control activation of data access logs. The special 'allServices' key denotes configuration for all services. | map(object({…})) | | {} | | [logging_exclusions](variables-logging.tf#L28) | Logging exclusions for this organization in the form {NAME -> FILTER}. | map(string) | | {} | | [logging_settings](variables-logging.tf#L35) | Default settings for logging resources. | object({…}) | | null | diff --git a/modules/organization/deny-policies.tf b/modules/organization/deny-policies.tf new file mode 100644 index 000000000..860a842e4 --- /dev/null +++ b/modules/organization/deny-policies.tf @@ -0,0 +1,52 @@ +/** + * 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. + */ + +# tfdoc:file:description IAM Deny policies. + +resource "google_iam_deny_policy" "default" { + for_each = var.iam_deny_policies + parent = urlencode("cloudresourcemanager.googleapis.com/organizations/${local.organization_id_numeric}") + name = each.key + display_name = each.value.display_name + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + description = rule.value.description + deny_rule { + denied_principals = [ + for p in rule.value.denied_principals : lookup(local.ctx.iam_principals, p, p) + ] + dynamic "denial_condition" { + for_each = rule.value.denial_condition == null ? [] : [""] + content { + title = rule.value.denial_condition.title + expression = templatestring( + rule.value.denial_condition.expression, var.context.condition_vars + ) + description = rule.value.denial_condition.description + location = rule.value.denial_condition.location + } + } + denied_permissions = rule.value.denied_permissions + exception_principals = [ + for p in rule.value.exception_principals : lookup(local.ctx.iam_principals, p, p) + ] + exception_permissions = rule.value.exception_permissions + } + } + } +} diff --git a/modules/organization/variables-iam.tf b/modules/organization/variables-iam.tf index f89ec3040..cea7b16fa 100644 --- a/modules/organization/variables-iam.tf +++ b/modules/organization/variables-iam.tf @@ -94,3 +94,39 @@ variable "iam_by_principals_conditional" { error_message = "IAM bindings with the same condition title must have identical expressions and descriptions." } } + +variable "iam_deny_policies" { + description = "IAM Deny policies to be applied to the organization." + type = map(object({ + display_name = optional(string) + rules = list(object({ + description = optional(string) + denied_principals = list(string) + denied_permissions = list(string) + denial_condition = optional(object({ + expression = string + title = optional(string) + description = optional(string) + location = optional(string) + })) + exception_principals = optional(list(string), []) + exception_permissions = optional(list(string), []) + })) + })) + default = {} + nullable = false + validation { + # Ensure denied_principals and denied_permissions are explicitly not null + # (to prevent HCL evaluation errors in loops) and contain at least one + # element (required by the GCP API). + condition = alltrue(flatten([ + for k, v in var.iam_deny_policies : [ + for r in v.rules : ( + try(length(r.denied_principals) > 0, false) && + try(length(r.denied_permissions) > 0, false) + ) + ] + ])) + error_message = "Each rule in iam_deny_policies must have at least one denied principal and one denied permission." + } +} diff --git a/modules/project-factory/folders.tf b/modules/project-factory/folders.tf index 403a213f1..846c3627c 100644 --- a/modules/project-factory/folders.tf +++ b/modules/project-factory/folders.tf @@ -104,6 +104,7 @@ module "folder-1-iam" { iam_by_principals = lookup(each.value, "iam_by_principals", {}) iam_by_principals_additive = lookup(each.value, "iam_by_principals_additive", {}) iam_by_principals_conditional = lookup(each.value, "iam_by_principals_conditional", {}) + iam_deny_policies = lookup(each.value, "iam_deny_policies", {}) logging_data_access = lookup(each.value, "data_access_logs", {}) logging_sinks = try(each.value.logging.sinks, {}) tag_bindings = lookup(each.value, "tag_bindings", {}) @@ -181,6 +182,7 @@ module "folder-2-iam" { iam_by_principals = lookup(each.value, "iam_by_principals", {}) iam_by_principals_additive = lookup(each.value, "iam_by_principals_additive", {}) iam_by_principals_conditional = lookup(each.value, "iam_by_principals_conditional", {}) + iam_deny_policies = lookup(each.value, "iam_deny_policies", {}) logging_data_access = lookup(each.value, "data_access_logs", {}) logging_sinks = try(each.value.logging.sinks, {}) tag_bindings = lookup(each.value, "tag_bindings", {}) @@ -261,6 +263,7 @@ module "folder-3-iam" { iam_by_principals = lookup(each.value, "iam_by_principals", {}) iam_by_principals_additive = lookup(each.value, "iam_by_principals_additive", {}) iam_by_principals_conditional = lookup(each.value, "iam_by_principals_conditional", {}) + iam_deny_policies = lookup(each.value, "iam_deny_policies", {}) logging_data_access = lookup(each.value, "data_access_logs", {}) logging_sinks = try(each.value.logging.sinks, {}) tag_bindings = lookup(each.value, "tag_bindings", {}) @@ -344,6 +347,7 @@ module "folder-4-iam" { logging_data_access = lookup(each.value, "data_access_logs", {}) logging_sinks = try(each.value.logging.sinks, {}) tag_bindings = lookup(each.value, "tag_bindings", {}) + iam_deny_policies = lookup(each.value, "iam_deny_policies", {}) context = merge(local.ctx, { folder_ids = merge(local.ctx.folder_ids, { for k, v in module.folder-3 : k => v.id diff --git a/modules/project-factory/projects.tf b/modules/project-factory/projects.tf index 7106d00e3..62a22b119 100644 --- a/modules/project-factory/projects.tf +++ b/modules/project-factory/projects.tf @@ -260,7 +260,8 @@ module "projects-iam" { tags_config = { force_context_ids = true } - universe = each.value.universe + iam_deny_policies = lookup(each.value, "iam_deny_policies", {}) + universe = each.value.universe # we use explicit depends_on as this allows us passing name and prefix depends_on = [ module.projects diff --git a/modules/project-factory/schemas/folder.schema.json b/modules/project-factory/schemas/folder.schema.json index 166cb1e3f..84a7071c5 100644 --- a/modules/project-factory/schemas/folder.schema.json +++ b/modules/project-factory/schemas/folder.schema.json @@ -357,6 +357,85 @@ "iam_by_principals_conditional": { "$ref": "#/$defs/iam_by_principals_conditional" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "name": { "type": "string" }, diff --git a/modules/project-factory/schemas/folder.schema.md b/modules/project-factory/schemas/folder.schema.md index 2a89323f7..996034e8b 100644 --- a/modules/project-factory/schemas/folder.schema.md +++ b/modules/project-factory/schemas/folder.schema.md @@ -112,6 +112,29 @@ - **iam_bindings_additive**: *reference([iam_bindings_additive](#refs-iam_bindings_additive))* - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **name**: *string* - **org_policies**: *object*
*additional properties: false* diff --git a/modules/project-factory/schemas/project.schema.json b/modules/project-factory/schemas/project.schema.json index 94c224062..3632daa1f 100644 --- a/modules/project-factory/schemas/project.schema.json +++ b/modules/project-factory/schemas/project.schema.json @@ -344,6 +344,85 @@ "iam_by_principals_additive": { "$ref": "#/$defs/iam_by_principals" }, + "iam_deny_policies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^[a-z0-9-]+$": { + "type": "object", + "additionalProperties": false, + "required": [ + "rules" + ], + "properties": { + "display_name": { + "type": "string" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "denied_permissions", + "denied_principals" + ], + "properties": { + "description": { + "type": "string" + }, + "denied_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "denied_principals": { + "type": "array", + "items": { + "type": "string" + } + }, + "denial_condition": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression" + ], + "properties": { + "expression": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "exception_permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "exception_principals": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + }, "kms": { "type": "object", "additionalProperties": false, diff --git a/modules/project-factory/schemas/project.schema.md b/modules/project-factory/schemas/project.schema.md index 31998ca8a..3def29e9c 100644 --- a/modules/project-factory/schemas/project.schema.md +++ b/modules/project-factory/schemas/project.schema.md @@ -110,6 +110,29 @@ - **iam_by_principals**: *reference([iam_by_principals](#refs-iam_by_principals))* - **iam_by_principals_conditional**: *reference([iam_by_principals_conditional](#refs-iam_by_principals_conditional))* - **iam_by_principals_additive**: *reference([iam_by_principals](#refs-iam_by_principals))* +- **iam_deny_policies**: *object* +
*additional properties: false* + - **`^[a-z0-9-]+$`**: *object* +
*additional properties: false* + - **display_name**: *string* + - ⁺**rules**: *array* + - items: *object* +
*additional properties: false* + - **description**: *string* + - ⁺**denied_permissions**: *array* + - items: *string* + - ⁺**denied_principals**: *array* + - items: *string* + - **denial_condition**: *object* +
*additional properties: false* + - ⁺**expression**: *string* + - **title**: *string* + - **description**: *string* + - **location**: *string* + - **exception_permissions**: *array* + - items: *string* + - **exception_principals**: *array* + - items: *string* - **kms**: *object*
*additional properties: false* - **autokeys**: *object* diff --git a/modules/project/README.md b/modules/project/README.md index d5572c214..3e4317f99 100644 --- a/modules/project/README.md +++ b/modules/project/README.md @@ -47,6 +47,7 @@ This module implements the creation and management of one GCP project including - [Observability](#observability) - [Observability factory](#observability-factory) - [Workload Identity Federation](#workload-identity-federation) +- [IAM Deny Policies](#iam-deny-policies) - [Files](#files) - [Variables](#variables) - [Outputs](#outputs) @@ -2245,6 +2246,55 @@ module "project" { # tftest modules=1 resources=7 inventory=wif.yaml ``` +## IAM Deny Policies + +[IAM Deny policies](https://cloud.google.com/iam/docs/deny-overview) allow you to set centralized guardrails that prevent principals from using specific permissions within the project, regardless of the roles they have been granted. + +You can define Deny policies using the `iam_deny_policies` variable. Each policy requires you to specify the principals and permissions to deny. You can optionally define exception principals, exception permissions, and conditions to tailor the restriction. + +Note that IAM Deny policies require a specific prefix for principal definitions (e.g., `principalSet://goog/public:all` or `principalSet://goog/group/group-email@example.com`), and permissions must be prefixed with the service fully qualified domain name (e.g., `iam.googleapis.com/serviceAccountKeys.create`). The module automatically leverages context interpolation for principal formatting if they are defined in your `var.context.iam_principals` mapping. + +```hcl +module "project" { + source = "./fabric/modules/project" + name = "my-project" + parent = var.folder_id + billing_account = var.billing_account_id + + iam_deny_policies = { + "prevent-kms-destruction" = { + display_name = "Prevent KMS Key destruction" + rules = [ + { + description = "Deny destroying KMS key versions to all except the key admins group." + denied_principals = ["principalSet://goog/public:all"] + denied_permissions = ["cloudkms.googleapis.com/cryptoKeyVersions.destroy"] + exception_principals = [ + "principalSet://goog/group/gcp-kms-admins@example.com" + ] + } + ] + } + "prevent-core-bucket-deletion" = { + display_name = "Prevent core bucket deletion" + rules = [ + { + description = "Deny deletion of any Cloud Storage bucket with the 'core-' prefix." + denied_principals = ["principalSet://goog/public:all"] + denied_permissions = ["storage.googleapis.com/buckets.delete"] + denial_condition = { + title = "core_buckets_only" + description = "Applies only to buckets starting with 'core-'." + expression = "resource.name.startsWith(\"projects/-/buckets/core-\")" + } + } + ] + } + } +} +# tftest modules=1 resources=3 inventory=iam-deny-policies.yaml +``` + ## Files @@ -2255,6 +2305,7 @@ module "project" { | [assets.tf](./assets.tf) | None | google_cloud_asset_project_feed | | [bigquery-reservation.tf](./bigquery-reservation.tf) | None | google_bigquery_reservation · google_bigquery_reservation_assignment | | [cmek.tf](./cmek.tf) | Service Agent IAM Bindings for CMEK | google_kms_crypto_key_iam_member | +| [deny-policies.tf](./deny-policies.tf) | IAM Deny policies. | google_iam_deny_policy | | [iam.tf](./iam.tf) | IAM bindings. | google_project_iam_binding · google_project_iam_custom_role · google_project_iam_member | | [identity-providers-defs.tf](./identity-providers-defs.tf) | Workload Identity provider definitions. | | | [identity-providers.tf](./identity-providers.tf) | None | google_iam_workload_identity_pool · google_iam_workload_identity_pool_provider | @@ -2308,6 +2359,7 @@ module "project" { | [iam_by_principals](variables-iam.tf#L61) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | | [iam_by_principals_additive](variables-iam.tf#L54) | Additive IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid errors. Merged internally with the `iam_bindings_additive` variable. | map(list(string)) | | {} | | [iam_by_principals_conditional](variables-iam.tf#L68) | Authoritative IAM binding in {PRINCIPAL => {roles = [roles], condition = {cond}}} format. Principals need to be statically defined to avoid errors. Condition is required. | map(object({…})) | | {} | +| [iam_deny_policies](variables-iam.tf#L98) | IAM Deny policies to be applied to the project. | map(object({…})) | | {} | | [kms_autokeys](variables.tf#L221) | KMS Autokey key handles. | map(object({…})) | | {} | | [labels](variables.tf#L239) | Resource labels. | map(string) | | {} | | [lien_reason](variables.tf#L246) | If non-empty, creates a project lien with this description. | string | | null | diff --git a/modules/project/deny-policies.tf b/modules/project/deny-policies.tf new file mode 100644 index 000000000..f5ba47866 --- /dev/null +++ b/modules/project/deny-policies.tf @@ -0,0 +1,52 @@ +/** + * 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. + */ + +# tfdoc:file:description IAM Deny policies. + +resource "google_iam_deny_policy" "default" { + for_each = var.iam_deny_policies + parent = urlencode("cloudresourcemanager.googleapis.com/projects/${local.project.project_id}") + name = each.key + display_name = each.value.display_name + dynamic "rules" { + for_each = each.value.rules + iterator = rule + content { + description = rule.value.description + deny_rule { + denied_principals = [ + for p in rule.value.denied_principals : lookup(local.ctx.iam_principals, p, p) + ] + dynamic "denial_condition" { + for_each = rule.value.denial_condition == null ? [] : [""] + content { + title = rule.value.denial_condition.title + expression = templatestring( + rule.value.denial_condition.expression, var.context.condition_vars + ) + description = rule.value.denial_condition.description + location = rule.value.denial_condition.location + } + } + denied_permissions = rule.value.denied_permissions + exception_principals = [ + for p in rule.value.exception_principals : lookup(local.ctx.iam_principals, p, p) + ] + exception_permissions = rule.value.exception_permissions + } + } + } +} diff --git a/modules/project/variables-iam.tf b/modules/project/variables-iam.tf index f89ec3040..6368ce054 100644 --- a/modules/project/variables-iam.tf +++ b/modules/project/variables-iam.tf @@ -94,3 +94,39 @@ variable "iam_by_principals_conditional" { error_message = "IAM bindings with the same condition title must have identical expressions and descriptions." } } + +variable "iam_deny_policies" { + description = "IAM Deny policies to be applied to the project." + type = map(object({ + display_name = optional(string) + rules = list(object({ + description = optional(string) + denied_principals = list(string) + denied_permissions = list(string) + denial_condition = optional(object({ + expression = string + title = optional(string) + description = optional(string) + location = optional(string) + })) + exception_principals = optional(list(string), []) + exception_permissions = optional(list(string), []) + })) + })) + default = {} + nullable = false + validation { + # Ensure denied_principals and denied_permissions are explicitly not null + # (to prevent HCL evaluation errors in loops) and contain at least one + # element (required by the GCP API). + condition = alltrue(flatten([ + for k, v in var.iam_deny_policies : [ + for r in v.rules : ( + try(length(r.denied_principals) > 0, false) && + try(length(r.denied_permissions) > 0, false) + ) + ] + ])) + error_message = "Each rule in iam_deny_policies must have at least one denied principal and one denied permission." + } +} diff --git a/tests/modules/folder/context.tfvars b/tests/modules/folder/context.tfvars index 82d94481b..9536e2887 100644 --- a/tests/modules/folder/context.tfvars +++ b/tests/modules/folder/context.tfvars @@ -137,3 +137,21 @@ tag_bindings = { baz = "$tag_values:test/one" foo = "$${projects[\"test-00\"].test}/cc-123" } + +iam_deny_policies = { + test-policy = { + display_name = "Test Deny Policy" + rules = [ + { + description = "Test Rule" + denied_principals = ["$iam_principals:myuser"] + denied_permissions = ["compute.googleapis.com/instances.create"] + exception_principals = ["$iam_principals:mygroup"] + denial_condition = { + title = "Test Condition" + expression = "resource.matchTag('$${organization.id}/environment', 'development')" + } + } + ] + } +} diff --git a/tests/modules/folder/context.yaml b/tests/modules/folder/context.yaml index ee98034e7..13857748b 100644 --- a/tests/modules/folder/context.yaml +++ b/tests/modules/folder/context.yaml @@ -4,7 +4,7 @@ # 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 +# https://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, @@ -96,6 +96,25 @@ values: condition: [] member: user:test-user@example.com role: organizations/366118655033/roles/myRoleTwo + google_iam_deny_policy.default["test-policy"]: + display_name: Test Deny Policy + name: test-policy + rules: + - deny_rule: + - denial_condition: + - description: null + expression: resource.matchTag('1234567890/environment', 'development') + location: null + title: Test Condition + denied_permissions: + - compute.googleapis.com/instances.create + denied_principals: + - user:test-user@example.com + exception_permissions: [] + exception_principals: + - group:test-group@example.com + description: Test Rule + timeouts: null google_logging_folder_settings.default[0]: kms_key_name: projects/test-kms-0/locations/europe-west8/keyRings/test/cryptoKeys/test timeouts: null @@ -162,10 +181,22 @@ counts: google_folder_iam_audit_config: 1 google_folder_iam_binding: 7 google_folder_iam_member: 1 + google_iam_deny_policy: 1 google_logging_folder_settings: 1 google_logging_folder_sink: 1 google_privileged_access_manager_entitlement: 1 google_pubsub_topic_iam_member: 1 google_tags_tag_binding: 3 modules: 0 - resources: 19 + resources: 20 + +outputs: + asset_search_results: {} + assured_workload: null + folder: __missing__ + id: __missing__ + name: Test Context + organization_policies_ids: {} + scc_custom_sha_modules_ids: {} + service_agents: {} + sink_writer_identities: __missing__ diff --git a/tests/modules/folder/examples/iam-deny-policies.yaml b/tests/modules/folder/examples/iam-deny-policies.yaml new file mode 100644 index 000000000..cc25e843f --- /dev/null +++ b/tests/modules/folder/examples/iam-deny-policies.yaml @@ -0,0 +1,63 @@ +# 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. + +values: + module.folder.google_folder.folder[0]: + deletion_protection: false + display_name: Folder name + parent: folders/1122334455 + tags: null + timeouts: null + module.folder.google_iam_deny_policy.default["conditional-delete-deny"]: + display_name: Conditional instance deletion deny + name: conditional-delete-deny + rules: + - deny_rule: + - denial_condition: + - description: Prevent deletion of instances tagged as production. + expression: resource.matchTag('123456789012/environment', 'prod') + location: null + title: prevent_prod_deletion + denied_permissions: + - compute.googleapis.com/instances.delete + denied_principals: + - principalSet://goog/public:all + exception_permissions: [] + exception_principals: [] + description: Deny deletion of compute instances based on resource tags. + timeouts: null + module.folder.google_iam_deny_policy.default["prevent-key-creation"]: + display_name: Prevent SA key creation + name: prevent-key-creation + rules: + - deny_rule: + - denial_condition: [] + denied_permissions: + - iam.googleapis.com/serviceAccountKeys.create + denied_principals: + - principalSet://goog/public:all + exception_permissions: [] + exception_principals: + - principalSet://goog/group/gcp-folder-admins@example.com + description: Deny service account key creation to all except the folder admin + group. + timeouts: null + +counts: + google_folder: 1 + google_iam_deny_policy: 2 + modules: 1 + resources: 3 + +outputs: {} diff --git a/tests/modules/organization/context.tfvars b/tests/modules/organization/context.tfvars index 13cea5e91..973d4e803 100644 --- a/tests/modules/organization/context.tfvars +++ b/tests/modules/organization/context.tfvars @@ -208,3 +208,21 @@ tags = { } } } + +iam_deny_policies = { + test-policy = { + display_name = "Test Deny Policy" + rules = [ + { + description = "Test Rule" + denied_principals = ["$iam_principals:myuser"] + denied_permissions = ["compute.googleapis.com/instances.create"] + exception_principals = ["$iam_principals:mygroup"] + denial_condition = { + title = "Test Condition" + expression = "resource.matchTag('$${organization.id}/environment', 'development')" + } + } + ] + } +} diff --git a/tests/modules/organization/context.yaml b/tests/modules/organization/context.yaml index cc259a189..777270272 100644 --- a/tests/modules/organization/context.yaml +++ b/tests/modules/organization/context.yaml @@ -4,7 +4,7 @@ # 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 +# https://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, @@ -28,7 +28,7 @@ values: feed_output_config: - pubsub_destination: - topic: projects/test-prod-audit-logs-0/topics/audit-logs - org_id: "1234567890" + org_id: '1234567890' timeouts: null google_essential_contacts_contact.contact["$email_addresses:default"]: email: foo@example.com @@ -37,6 +37,26 @@ values: - ALL parent: organizations/1234567890 timeouts: null + google_iam_deny_policy.default["test-policy"]: + display_name: Test Deny Policy + name: test-policy + parent: cloudresourcemanager.googleapis.com%2Forganizations%2F1234567890 + rules: + - deny_rule: + - denial_condition: + - description: null + expression: resource.matchTag('1234567890/environment', 'development') + location: null + title: Test Condition + denied_permissions: + - compute.googleapis.com/instances.create + denied_principals: + - user:test-user@example.com + exception_permissions: [] + exception_principals: + - group:test-group@example.com + description: Test Rule + timeouts: null google_logging_organization_settings.default[0]: kms_key_name: projects/test-kms-0/locations/europe-west8/keyRings/test/cryptoKeys/test organization: '1234567890' @@ -266,6 +286,7 @@ counts: google_bigquery_dataset_iam_member: 1 google_cloud_asset_organization_feed: 1 google_essential_contacts_contact: 1 + google_iam_deny_policy: 1 google_logging_organization_settings: 1 google_logging_organization_sink: 5 google_organization_iam_audit_config: 1 @@ -281,4 +302,27 @@ counts: google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 0 - resources: 30 + resources: 31 + +outputs: + asset_search_results: {} + custom_constraint_ids: {} + custom_role_id: {} + custom_roles: {} + id: organizations/1234567890 + logging_identities: __missing__ + logging_sinks: __missing__ + network_tag_keys: {} + network_tag_values: {} + organization_id: organizations/1234567890 + organization_policies_ids: {} + scc_custom_sha_modules_ids: {} + scc_mute_configs: {} + scim_tenants: {} + service_agents: {} + sink_writer_identities: __missing__ + tag_keys: {} + tag_values: {} + workforce_identity_pool_ids: {} + workforce_identity_provider_names: {} + workforce_identity_providers: {} diff --git a/tests/modules/organization/examples/iam-deny-policies.yaml b/tests/modules/organization/examples/iam-deny-policies.yaml new file mode 100644 index 000000000..8ae6ff0d5 --- /dev/null +++ b/tests/modules/organization/examples/iam-deny-policies.yaml @@ -0,0 +1,58 @@ +# 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. + +values: + module.organization.google_iam_deny_policy.default["conditional-key-deny"]: + display_name: Conditional SA Key Deny + name: conditional-key-deny + parent: cloudresourcemanager.googleapis.com%2Forganizations%2F1122334455 + rules: + - deny_rule: + - denial_condition: + - description: Restrict access to specific IP ranges + expression: '!inIpRange(request.auth.access_levels, ''accessPolicies/123456789/accessLevels/trusted_ips'')' + location: null + title: ip-restriction + denied_permissions: + - iam.serviceAccountKeys.create + denied_principals: + - principalSet://goog/public:all + exception_permissions: [] + exception_principals: [] + description: Deny key creation outside of authorized IPs using a condition. + timeouts: null + module.organization.google_iam_deny_policy.default["prevent-sa-token-creation"]: + display_name: Prevent SA token creation + name: prevent-sa-token-creation + parent: cloudresourcemanager.googleapis.com%2Forganizations%2F1122334455 + rules: + - deny_rule: + - denial_condition: [] + denied_permissions: + - iam.serviceAccounts.getAccessToken + denied_principals: + - principalSet://goog/public:all + exception_permissions: [] + exception_principals: + - principalSet://goog/group/gcp-admins@example.com + description: Deny service account token creation to all except the central admin + group. + timeouts: null + +counts: + google_iam_deny_policy: 2 + modules: 1 + resources: 2 + +outputs: {} diff --git a/tests/modules/project/context.tfvars b/tests/modules/project/context.tfvars index 61d2258f7..78d901113 100644 --- a/tests/modules/project/context.tfvars +++ b/tests/modules/project/context.tfvars @@ -254,3 +254,21 @@ tags = { vpc_sc = { perimeter_name = "$vpc_sc_perimeters:default" } + +iam_deny_policies = { + test-policy = { + display_name = "Test Deny Policy" + rules = [ + { + description = "Test Rule" + denied_principals = ["$iam_principals:myuser"] + denied_permissions = ["compute.googleapis.com/instances.create"] + exception_principals = ["$iam_principals:mygroup"] + denial_condition = { + title = "Test Condition" + expression = "resource.matchTag('$${organization.id}/environment', 'development')" + } + } + ] + } +} diff --git a/tests/modules/project/context.yaml b/tests/modules/project/context.yaml index b6cce50a4..6b03099c2 100644 --- a/tests/modules/project/context.yaml +++ b/tests/modules/project/context.yaml @@ -4,7 +4,7 @@ # 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 +# https://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, @@ -29,7 +29,6 @@ values: project: my-project timeouts: null google_compute_shared_vpc_service_project.shared_vpc_service[0]: - deletion_policy: null host_project: test-vpc-host service_project: my-project timeouts: null @@ -40,6 +39,26 @@ values: - ALL parent: projects/my-project timeouts: null + google_iam_deny_policy.default["test-policy"]: + display_name: Test Deny Policy + name: test-policy + parent: cloudresourcemanager.googleapis.com%2Fprojects%2Fmy-project + rules: + - deny_rule: + - denial_condition: + - description: null + expression: resource.matchTag('1234567890/environment', 'development') + location: null + title: Test Condition + denied_permissions: + - compute.googleapis.com/instances.create + denied_principals: + - user:test-user@example.com + exception_permissions: [] + exception_principals: + - group:test-group@example.com + description: Test Rule + timeouts: null google_kms_crypto_key_iam_member.service_agent_cmek["key-0.compute-system"]: condition: [] crypto_key_id: projects/kms-central-prj/locations/europe-west1/keyRings/my-keyring/cryptoKeys/ew1-compute @@ -156,7 +175,6 @@ values: google_project.project[0]: auto_create_network: false billing_account: null - deletion_policy: DELETE effective_labels: goog-terraform-provisioned: 'true' folder_id: '6789012345' @@ -323,6 +341,7 @@ counts: google_cloud_asset_project_feed: 1 google_compute_shared_vpc_service_project: 1 google_essential_contacts_contact: 1 + google_iam_deny_policy: 1 google_kms_crypto_key_iam_member: 1 google_logging_metric: 1 google_logging_project_sink: 1 @@ -341,4 +360,36 @@ counts: google_tags_tag_value_iam_binding: 2 google_tags_tag_value_iam_member: 1 modules: 0 - resources: 38 + resources: 39 + +outputs: + alert_ids: __missing__ + asset_search_results: {} + bigquery_reservations: + assignments: {} + reservations: {} + custom_role_id: {} + custom_roles: {} + default_service_accounts: __missing__ + id: my-project + kms_autokeys: {} + name: my-project + network_tag_keys: {} + network_tag_values: {} + notification_channel_names: __missing__ + notification_channels: __missing__ + number: __missing__ + organization_policies_ids: {} + project_id: my-project + quota_configs: {} + quotas: {} + scc_custom_sha_modules_ids: {} + service_agents: __missing__ + services: + - compute.googleapis.com + sink_writer_identities: __missing__ + tag_keys: {} + tag_values: {} + workload_identity_pool_ids: {} + workload_identity_provider_ids: {} + workload_identity_providers: {} diff --git a/tests/modules/project/examples/iam-deny-policies.yaml b/tests/modules/project/examples/iam-deny-policies.yaml new file mode 100644 index 000000000..9633c1ffc --- /dev/null +++ b/tests/modules/project/examples/iam-deny-policies.yaml @@ -0,0 +1,73 @@ +# 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. + +values: + module.project.google_iam_deny_policy.default["prevent-core-bucket-deletion"]: + display_name: Prevent core bucket deletion + name: prevent-core-bucket-deletion + parent: cloudresourcemanager.googleapis.com%2Fprojects%2Fmy-project + rules: + - deny_rule: + - denial_condition: + - description: Applies only to buckets starting with 'core-'. + expression: resource.name.startsWith("projects/-/buckets/core-") + location: null + title: core_buckets_only + denied_permissions: + - storage.googleapis.com/buckets.delete + denied_principals: + - principalSet://goog/public:all + exception_permissions: [] + exception_principals: [] + description: Deny deletion of any Cloud Storage bucket with the 'core-' prefix. + timeouts: null + module.project.google_iam_deny_policy.default["prevent-kms-destruction"]: + display_name: Prevent KMS Key destruction + name: prevent-kms-destruction + parent: cloudresourcemanager.googleapis.com%2Fprojects%2Fmy-project + rules: + - deny_rule: + - denial_condition: [] + denied_permissions: + - cloudkms.googleapis.com/cryptoKeyVersions.destroy + denied_principals: + - principalSet://goog/public:all + exception_permissions: [] + exception_principals: + - principalSet://goog/group/gcp-kms-admins@example.com + description: Deny destroying KMS key versions to all except the key admins group. + timeouts: null + module.project.google_project.project[0]: + auto_create_network: false + billing_account: 123456-123456-123456 + deletion_policy: DELETE + effective_labels: + goog-terraform-provisioned: 'true' + folder_id: '1122334455' + labels: null + name: my-project + org_id: null + project_id: my-project + tags: null + terraform_labels: + goog-terraform-provisioned: 'true' + timeouts: null + +counts: + google_iam_deny_policy: 2 + google_project: 1 + modules: 1 + resources: 3 + +outputs: {}