Skip to content

Commit

Permalink
feat!: add granular service accounts (#724)
Browse files Browse the repository at this point in the history
* add granular service accounts

* add test for granular roles for the SA

* add Viewer role to CI account because the account is not owner of the projects

* add roles/compute.xpnAdmin role at org level for 'compute.globalOperations.get' permission

* add roles for hub and spoke transitivity

* grant sensitive roles to networks service account only in hub and spoke projects

* add note regarding project creator role for the service accounts

* add sleep after bootstrap for API activation in seed project

* add parent iam member module

* update granular service account description

* update terraform service account email in documentation

* add output for service account id

* fix terraform lint error

* remove granular service account module

Co-authored-by: Andrew Peabody <andrewpeabody@google.com>
  • Loading branch information
daniel-cit and apeabody committed Jul 25, 2022
1 parent b2e8bfc commit 4c84d80
Show file tree
Hide file tree
Showing 21 changed files with 425 additions and 73 deletions.
4 changes: 4 additions & 0 deletions 0-bootstrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,12 @@ the following steps:
|------|-------------|
| cloudbuild\_project\_id | Project where CloudBuild configuration and terraform container image will reside. |
| csr\_repos | List of Cloud Source Repos created by the module, linked to Cloud Build triggers. |
| environment\_step\_terraform\_service\_account\_email | Environment Step Terraform Account |
| gcs\_bucket\_cloudbuild\_artifacts | Bucket used to store Cloud/Build artifacts in CloudBuild project. |
| gcs\_bucket\_tfstate | Bucket used for storing terraform state for foundations pipelines in seed project. |
| networks\_step\_terraform\_service\_account\_email | Networks Step Terraform Account |
| organization\_step\_terraform\_service\_account\_email | Organization Step Terraform Account |
| projects\_step\_terraform\_service\_account\_email | Projects Step Terraform Account |
| seed\_project\_id | Project where service accounts and core APIs will be enabled. |
| terraform\_sa\_name | Fully qualified name for privileged service account for Terraform. |
| terraform\_service\_account | Email for privileged service account for Terraform. |
Expand Down
57 changes: 13 additions & 44 deletions 0-bootstrap/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,17 @@
Bootstrap GCP Organization.
*************************************************/
locals {
parent = var.parent_folder != "" ? "folders/${var.parent_folder}" : "organizations/${var.org_id}"
// The bootstrap module will enforce that only identities
// in the list "org_project_creators" will have the Project Creator role,
// so the granular service accounts for each step need to be added to the list.
step_terraform_sa = [
"serviceAccount:${google_service_account.terraform-env-sa["org"].email}",
"serviceAccount:${google_service_account.terraform-env-sa["env"].email}",
"serviceAccount:${google_service_account.terraform-env-sa["net"].email}",
"serviceAccount:${google_service_account.terraform-env-sa["proj"].email}",
]
org_project_creators = distinct(concat(var.org_project_creators, local.step_terraform_sa))
parent = var.parent_folder != "" ? "folders/${var.parent_folder}" : "organizations/${var.org_id}"
org_admins_org_iam_permissions = var.org_policy_admin_role == true ? [
"roles/orgpolicy.policyAdmin", "roles/resourcemanager.organizationAdmin", "roles/billing.user"
] : ["roles/resourcemanager.organizationAdmin", "roles/billing.user"]
Expand All @@ -40,7 +50,7 @@ module "seed_bootstrap" {
group_org_admins = var.group_org_admins
group_billing_admins = var.group_billing_admins
default_region = var.default_region
org_project_creators = var.org_project_creators
org_project_creators = local.org_project_creators
sa_enable_impersonation = true
parent_folder = var.parent_folder == "" ? "" : local.parent
org_admins_org_iam_permissions = local.org_admins_org_iam_permissions
Expand Down Expand Up @@ -80,20 +90,7 @@ module "seed_bootstrap" {
"billingbudgets.googleapis.com"
]

sa_org_iam_permissions = [
"roles/accesscontextmanager.policyAdmin",
"roles/billing.user",
"roles/compute.networkAdmin",
"roles/compute.xpnAdmin",
"roles/iam.securityAdmin",
"roles/iam.serviceAccountAdmin",
"roles/logging.configWriter",
"roles/orgpolicy.policyAdmin",
"roles/resourcemanager.projectCreator",
"roles/resourcemanager.folderAdmin",
"roles/securitycenter.notificationConfigEditor",
"roles/resourcemanager.organizationViewer"
]
sa_org_iam_permissions = []
}

resource "google_billing_account_iam_member" "tf_billing_admin" {
Expand Down Expand Up @@ -202,34 +199,6 @@ resource "google_folder_iam_member" "folder_cb_sa_browser" {
member = "serviceAccount:${data.google_project.cloudbuild.number}@cloudbuild.gserviceaccount.com"
}

resource "google_organization_iam_member" "org_tf_compute_security_policy_admin" {
count = var.parent_folder == "" ? 1 : 0
org_id = var.org_id
role = "roles/compute.orgSecurityPolicyAdmin"
member = "serviceAccount:${module.seed_bootstrap.terraform_sa_email}"
}

resource "google_folder_iam_member" "folder_tf_compute_security_policy_admin" {
count = var.parent_folder != "" ? 1 : 0
folder = var.parent_folder
role = "roles/compute.orgSecurityPolicyAdmin"
member = "serviceAccount:${module.seed_bootstrap.terraform_sa_email}"
}

resource "google_organization_iam_member" "org_tf_compute_security_resource_admin" {
count = var.parent_folder == "" ? 1 : 0
org_id = var.org_id
role = "roles/compute.orgSecurityResourceAdmin"
member = "serviceAccount:${module.seed_bootstrap.terraform_sa_email}"
}

resource "google_folder_iam_member" "folder_tf_compute_security_resource_admin" {
count = var.parent_folder != "" ? 1 : 0
folder = var.parent_folder
role = "roles/compute.orgSecurityResourceAdmin"
member = "serviceAccount:${module.seed_bootstrap.terraform_sa_email}"
}

## Un-comment the jenkins_bootstrap module and its outputs if you want to use Jenkins instead of Cloud Build
# module "jenkins_bootstrap" {
# source = "./modules/jenkins-agent"
Expand Down
36 changes: 36 additions & 0 deletions 0-bootstrap/modules/parent-iam-member/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright 2022 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 {
org_id = var.parent_type == "organization" ? var.parent_id : ""
folder_id = var.parent_type == "folder" ? var.parent_id : ""
}

resource "google_organization_iam_member" "org_parent_iam" {
for_each = toset(local.org_id != "" ? var.roles : [])

org_id = local.org_id
role = each.key
member = var.member
}

resource "google_folder_iam_member" "folder_parent_iam" {
for_each = toset(local.folder_id != "" ? var.roles : [])

folder = local.folder_id
role = each.key
member = var.member
}
40 changes: 40 additions & 0 deletions 0-bootstrap/modules/parent-iam-member/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright 2022 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.
*/

variable "parent_type" {
description = "Type of the parent resource. valid values are `organization` and `folder`."
type = string

validation {
condition = var.parent_type == "organization" || var.parent_type == "folder"
error_message = "For parent_type only `organization` and `folder` are valid."
}
}

variable "parent_id" {
description = "ID of the parent resource."
type = string
}

variable "member" {
description = "Member to have the given roles in the parent resource. Prefix of `group:`, `user:` or `serviceAccount:` is required."
type = string
}

variable "roles" {
description = "Roles to grant to the member in the parent resource."
type = list(string)
}
32 changes: 32 additions & 0 deletions 0-bootstrap/modules/parent-iam-member/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright 2022 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.
*/

terraform {
required_version = ">= 0.13"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 3.77"
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 3.77"
}
random = {
source = "hashicorp/random"
}
}
}
20 changes: 20 additions & 0 deletions 0-bootstrap/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ output "terraform_service_account" {
value = module.seed_bootstrap.terraform_sa_email
}

output "projects_step_terraform_service_account_email" {
description = "Projects Step Terraform Account"
value = google_service_account.terraform-env-sa["proj"].email
}

output "networks_step_terraform_service_account_email" {
description = "Networks Step Terraform Account"
value = google_service_account.terraform-env-sa["net"].email
}

output "environment_step_terraform_service_account_email" {
description = "Environment Step Terraform Account"
value = google_service_account.terraform-env-sa["env"].email
}

output "organization_step_terraform_service_account_email" {
description = "Organization Step Terraform Account"
value = google_service_account.terraform-env-sa["org"].email
}

output "terraform_sa_name" {
description = "Fully qualified name for privileged service account for Terraform."
value = module.seed_bootstrap.terraform_sa_name
Expand Down
134 changes: 134 additions & 0 deletions 0-bootstrap/sa.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* Copyright 2022 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 {

parent_type = var.parent_folder == "" ? "organization" : "folder"
parent_id = var.parent_folder == "" ? var.org_id : var.parent_folder

granular_sa = {
"org" = "Foundation Organization SA. Managed by Terraform.",
"env" = "Foundation Environment SA. Managed by Terraform.",
"net" = "Foundation Network SA. Managed by Terraform.",
"proj" = "Foundation Projects SA. Managed by Terraform.",
}

granular_sa_org_level_roles = {
"org" = [
"roles/orgpolicy.policyAdmin",
"roles/logging.configWriter",
"roles/resourcemanager.organizationAdmin",
"roles/securitycenter.notificationConfigEditor",
"roles/resourcemanager.organizationViewer",
"roles/accesscontextmanager.policyAdmin",
],
"net" = [
"roles/accesscontextmanager.policyAdmin",
"roles/compute.xpnAdmin",
],
"proj" = [
"roles/accesscontextmanager.policyAdmin",
"roles/serviceusage.serviceUsageConsumer"
],
}

granular_sa_parent_level_roles = {
"org" = [
"roles/resourcemanager.folderAdmin",
],
"env" = [
"roles/resourcemanager.folderAdmin"
],
"net" = [
"roles/resourcemanager.folderViewer",
"roles/compute.networkAdmin",
"roles/compute.securityAdmin",
"roles/compute.orgSecurityPolicyAdmin",
"roles/compute.orgSecurityResourceAdmin",
"roles/dns.admin",
],
"proj" = [
"roles/resourcemanager.folderViewer",
"roles/resourcemanager.folderIamAdmin",
"roles/compute.networkAdmin",
"roles/compute.xpnAdmin",
],
}
}

resource "google_service_account" "terraform-env-sa" {
for_each = local.granular_sa

project = module.seed_bootstrap.seed_project_id
account_id = "terraform-${each.key}-sa"
display_name = each.value
}

module "org_iam_member" {
source = "./modules/parent-iam-member"
for_each = local.granular_sa_org_level_roles

member = "serviceAccount:${google_service_account.terraform-env-sa[each.key].email}"
parent_type = "organization"
parent_id = var.org_id
roles = each.value
}

module "parent_iam_member" {
source = "./modules/parent-iam-member"
for_each = local.granular_sa_parent_level_roles

member = "serviceAccount:${google_service_account.terraform-env-sa[each.key].email}"
parent_type = local.parent_type
parent_id = local.parent_id
roles = each.value
}

resource "google_billing_account_iam_member" "tf_billing_user" {
for_each = local.granular_sa

billing_account_id = var.billing_account
role = "roles/billing.user"
member = "serviceAccount:${google_service_account.terraform-env-sa[each.key].email}"

depends_on = [
google_service_account.terraform-env-sa
]
}

resource "google_billing_account_iam_member" "billing_admin_user" {
for_each = local.granular_sa

billing_account_id = var.billing_account
role = "roles/billing.admin"
member = "serviceAccount:${google_service_account.terraform-env-sa[each.key].email}"

depends_on = [
google_billing_account_iam_member.tf_billing_user
]
}

resource "google_service_account_iam_member" "cloudbuild_terraform_sa_impersonate_permissions" {
for_each = local.granular_sa

service_account_id = google_service_account.terraform-env-sa[each.key].id
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:${data.google_project.cloudbuild.number}@cloudbuild.gserviceaccount.com"

depends_on = [
google_service_account.terraform-env-sa
]
}
2 changes: 1 addition & 1 deletion 1-org/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Run `terraform output cloudbuild_project_id` in the `0-bootstrap` folder to see
**Troubleshooting:**
If you received a `PERMISSION_DENIED` error running the `gcloud access-context-manager` or the `gcloud scc notifications` commands you can append
```
--impersonate-service-account=org-terraform@<SEED_PROJECT_ID>.iam.gserviceaccount.com
--impersonate-service-account=terraform-org-sa@<SEED_PROJECT_ID>.iam.gserviceaccount.com
```
to run the command as the Terraform service account.

Expand Down
1 change: 1 addition & 0 deletions 1-org/envs/shared/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
| log\_export\_storage\_location | The location of the storage bucket used to export logs. | `string` | `"US"` | no |
| log\_export\_storage\_retention\_policy | Configuration of the bucket's data retention policy for how long objects in the bucket should be retained. | <pre>object({<br> is_locked = bool<br> retention_period_days = number<br> })</pre> | `null` | no |
| log\_export\_storage\_versioning | (Optional) Toggles bucket versioning, ability to retain a non-current object version when the live object version gets replaced or deleted. | `bool` | `false` | no |
| networks\_step\_terraform\_service\_account\_email | Service account email of the account to impersonate to run Terraform in the network step. | `string` | `""` | no |
| org\_audit\_logs\_project\_alert\_pubsub\_topic | The name of the Cloud Pub/Sub topic where budget related messages will be published, in the form of `projects/{project_id}/topics/{topic_id}` for the org audit logs project. | `string` | `null` | no |
| org\_audit\_logs\_project\_alert\_spent\_percents | A list of percentages of the budget to alert on when threshold is exceeded for the org audit logs project. | `list(number)` | <pre>[<br> 0.5,<br> 0.75,<br> 0.9,<br> 0.95<br>]</pre> | no |
| org\_audit\_logs\_project\_budget\_amount | The amount to use as the budget for the org audit logs project. | `number` | `1000` | no |
Expand Down

0 comments on commit 4c84d80

Please sign in to comment.