使用 Google Cloud Platform 時的身份和訪問管理最佳做法
從基礎開始
IAM 策略描述了誰可以對哪些資源采取什么行動。這看起來很簡單,但隨著組織試圖擴展其政策以伴隨增長,它可能會變得異常復雜。無論組織規模如何,了解這三個概念中的每一個以及它們如何相互作用對于有效的 IAM 都是必不可少的。
IAM 的第一個構建塊是principal,它描述了我們定義中的who。IAM 策略可以應用于多個 GCP 主體,包括服務帳戶、google 帳戶、google 組和 google 工作區。
IAM 的第二個構建塊是什么操作。角色是確定委托人可以執行什么操作的權限的集合。以下是三種類型的角色:
- 基本角色:這些角色包括所有者、編輯者和查看者角色。這些角色具有廣泛的權限,Google 和 Praetorian 都不建議使用它們。
- 預定義角色:這些是 Google 創建的具有細粒度訪問控制的角色。
- 自定義角色:這些是組織創建的角色。自定義角色可以授予用戶一組最低權限。
IAM 的第三個構建塊是哪個資源,它規定了委托人可以在云層次結構中的哪個位置執行其允許的操作。IAM 策略定義了哪些資源具有哪些權限以及哪些組織負責,并將一個或多個委托人與一個或多個角色綁定。圖 1 概述了 GCP 的資源層次結構:

圖 1:GCP 中的資源層次結構。
GCP 用戶在組合三個 IAM 構建塊時需要牢記 IAM 繼承策略。組織可以在任何級別(組織、文件夾、項目和資源)設置 IAM 策略,并且資源將從父資源繼承所有策略。生成的權限將是所有策略的聯合。
拒絕策略和條件語句
用戶可以創建阻止權限的拒絕策略。在顯式拒絕中,拒絕策略將始終覆蓋允許的權限。這意味著,如果委托人對該關聯權限具有拒絕 IAM 策略,即使他們也具有包含該權限的角色,他們也將無法執行操作。相反,隱式拒絕發生在沒有任何拒絕或允許許可的策略的情況下。當委托人沒有拒絕或允許權限的 IAM 策略時,GCP 默認拒絕它。
條件語句是有效 IAM 策略的重要組成部分,它僅在滿足條件時才應用 IAM 策略(即資源具有標簽)。這允許通過 GCP 的 IAM 策略繼承不適用于子資源的 IAM 策略。雖然我們將在本文中使用標簽和條件語句來創建安全的 IAM 模型,但我們也知道條件語句可能會無意中引入 IAM 缺陷。我們將嘗試減少示例中的條件語句,以降低復雜性并使它們更易于審計和擴展。
組織架構劃分
創建一個經過深思熟慮的組織結構將有助于我們將 IAM 策略置于正確的位置并降低復雜性。在 Praetorian 在多個 GCP 環境中的工作過程中,我們看到了以下按從大到小的劃分。請注意,并非所有部門都可能存在或需要。
按業務單位劃分
組織經常試圖按業務單位安排最高級別的組織結構。對于大型公司,業務部門可以是 Gmail 和 Google 搜索等資源的部門。對于小公司,業務部門可以更具體,如應用程序前端和應用程序后端。
按業務部門劃分使公司的計費更加容易。這樣做還會在委托人與其業務部門之間建立一對一的關系,因為通常員工不會屬于多個業務部門。在 GCP 中,業務部門通常直接映射到組織或文件夾。請注意,業務部門也可以根據公司的需要在以下分組之間切換。
按環境類型劃分
在中層,組織傾向于按照生產、測試和質量保證等環境類型來區分 GCP 環境。這通常涉及一對多映射,因為每個員工可能需要訪問多個環境來完成其工作職能。環境分離允許公司在接近生產時制定更嚴格的規則(即,更少的人應該有權進行生產)。在 GCP 中,環境類型通常映射到組織或文件夾。
按項目名稱/團隊名稱劃分
較低級別的組織結構通常將 GCP 環境與當前正在開發的項目分開。這通常是一對多的映射,因為員工可以是多個團隊和項目的一部分。GCP 環境還允許包含跨所有項目或項目集合的共享資源的共享項目。
圖 2 顯示了我們的工程師經常遇到的組織結構的示例圖片,我們將使用它作為基點來創建一些易于審計和擴展的示例 IAM 策略。
圖 2:我們將從中創建 IAM 策略場景的通用組織結構。
請注意,這只是 GCP 中一種潛在的組織結構。組織會發現,每種結構化方法都有其自身的成本和收益。
場景
我們現在將討論公司在設置 GCP 環境時可能經常遇到的三種情況。我們將遍歷每個場景并嘗試將 IAM 策略置于適當的級別,以使其更具可擴展性和可驗證性。
要求一:一個用戶跨多個業務部門的一個預定義角色
第一個場景的要求是創建一個 IAM 策略,為我們的 CFO 閱讀器提供適用于不同業務部門的Billing Account Costs Manager預定義角色 (roles/billing.costsManager)。這將使首席財務官對公司將錢花在哪里有一個很好的了解。規則應該去哪里?
解決方案:盡可能高的組織級別
滿足此要求的一種簡單方法是將 IAM 策略附加到不同的業務部門,如附錄的文件 1 所示。然而,這是低效的,因為新創建的業務單位也必須具有此 IAM 策略。此外,此解決方案在審核 IAM 策略時引入了冗余,因為這些策略是相同的。
更好的方法是利用 IAM 策略從父資源繼承的事實。我們可以將此 IAM 策略置于組織級別。然后,每個業務部門將從組織繼承 IAM 策略,如附錄的文件 2 所示。
從這個場景中,我們學會了在最高級別插入 IAM 策略,以利用 GCP 的 IAM 策略繼承。
要求二:一個用戶跨多種環境類型的多個預定義角色
第二種情況的要求是授予我們的 Praetorian Cloud Manager 以下內容:
- 開發中的Compute Admin預定義角色 (roles/compute.admin)
- 暫存中的計算網絡用戶預定義角色 (roles/compute.networkUser)
- 生產中的計算網絡查看器預定義角色 (roles/compute.networkViewer)
我們的目標是遵循最小權限原則,即隨著項目接近生產,用戶的訪問權限更少。IAM 政策應該去哪里?
解決方案:條件語句
創建 IAM 策略的最簡潔方法是按策略共有的最高資源對其進行拆分,然后引入條件語句。從第一個場景中學習,我們知道 GCP IAM 繼承會導致不同的環境類型從業務單元繼承權限。將 IAM 策略置于業務單元級別具有將所有策略保存在一個位置的好處,但隨著公司規模的擴大,可能會增加復雜性。
對于我們的場景,我們可以將策略放在 Praetorian 云業務單元中,并使用條件語句為我們的 Praetorian Cloud 管理器在開發中提供Compute Admin ,在 staging 中提供Compute Network User ,在生產中提供**Compute Network Viewer。在環境類型文件夾級別使用 env:cloud-dev 或 env:cloud-staging 等標簽標記資源允許我們使用條件表達式,如
resource.matchTag('{projectid}/env','cloud-dev')
使權限僅適用于環境類型級別。附錄的文件 3 顯示了完成上述操作的示例 Terraform 文件。
現在想象一個擴展,其中負責在生產中測試應用程序的額外 Praetorian 安全測試人員需要在Praetorian 云環境的生產環境中預定義安全審查員(roles/iam.securityReviewer) 角色以進行測試。我們需要向 IAM 策略添加更多條件語句,以便僅在生產環境中授予 Praetorian Security Tester 權限。這將開始使條件語句膨脹,并可能導致不必要的復雜性,從而導致 IAM 漏洞。
經驗教訓:最高層政策的工作有共同點。
在我們的場景及其擴展中,環境類型資源是 IAM 策略共有的最高級別。我們可以輕松地在此級別創建 IAM 策略,以在適當的環境類型中為 Praetorian Cloud Manager 和 Praetorian Security Tester 提供適當的權限,如附錄的文件 4 所示。
從這個場景中,我們了解到,雖然 IAM 策略應置于可能的最高級別以利用 GCP 的 IAM 策略繼承,但 IAM 策略應僅設置在它們共有的最高級別,以降低條件語句形式的復雜性. 將策略置于更高級別會使從場景一中吸取的教訓太過分,并且會導致難以驗證或擴展的極其復雜的策略。
要求三:一個用戶在特定標記資源上的一個預定義角色
最后一個場景的要求是給我們的 Praetorian Cloud Developer Compute Network Viewer預定義角色,只允許在云暫存環境的 Athena 項目中標記為“designation:not-sensitive”的那些資源。我們將如何制定此 IAM 政策?
解決方案:拒絕政策?
從上述兩個練習中,我們知道我們希望將 IAM 策略放在項目級別。即使存在允許 IAM 策略,拒絕策略也可以幫助我們阻止訪問。但是,我們必須特別注意任何顯式拒絕策略如何跨資源交互。
對于實際的政策,我們知道我們想要一個有條件的政策,比如
resource.matchTag('{projectid}/designation','不敏感')
在 staging/athena 項目中將Compute Network Viewer提供給我們的 Praetorian Cloud 開發人員。現在,雖然 GCP 的隱式拒絕會阻止 Praetorian Cloud Developer 擁有Compute Network Viewer,但我們希望確保我們滿足要求的唯一方面。為此,我們可以使用拒絕 IAM 策略,該策略將具有如下條件語句
!resource.matchTag('{projectid}/designation','not-sensitive')
這將拒絕訪問那些缺少不敏感標簽的人。附錄的文件 5 包括演示這一點的 Terraform 文件。
經驗教訓:隱式拒絕有助于避免意外拒絕策略組合。
雖然上面是滿足我們最后一個要求的有效方式,但它的可擴展性不是很好。考慮一個額外的請求,我們希望僅授予Compute Network Viewer*訪問我們的 Praetorian Cloud 開發人員對標記為“designation:isfine”的資源的訪問權限。我們還知道,Praetorian Cloud 暫存環境中的所有項目都將具有標記為“designation:isfine”的資源。只有這些項目中的資源才會具有該名稱。使用與上述相同的邏輯,我們可以有一個帶有條件語句的 IAM 策略
resource.matchTag('{projectid}/designation','isfine')
以及帶有條件語句的拒絕 IAM 政策
!resource.matchTag('{projectid}/designation','isfine')。
在查看 staging/athena 項目時會出現問題。由于 GCP 中的 IAM 策略繼承,staging/athena 項目將繼承其父級的拒絕語句。該項目將有兩個拒絕 IAM 策略,拒絕任何不具有“ designation:not-sensitive”和“designation:isfine”標簽的資源。這是不可能的,因為“designation”鍵不能同時具有“not-sensitive”和“isfine”值。這不是任何一條規則的創建者對我們的開發人員的意義。
這種情況及其擴展證明了在使用拒絕語句時要小心的重要性。在大多數情況下,公司應該允許 GCP 的隱式拒絕來限制權限。拒絕語句在特定情況下確實有其用途(即,在組織級別拒絕以執行公司政策,因為只有財務團隊才能在組織中擁有計費權限)。這些場景更加細微,需要審計和成熟的安全程序,以確保多個拒絕 IAM 策略的繼承不會結合起來阻止超出策略預期的權限。
最佳實踐
我們對 GCP IAM 的高級探索的一些主要收獲如下:
- 公司通常按業務單位、環境類型和項目/團隊劃分其 GCP 組織結構。
- 公司應致力于將條件 IAM 策略置于可能利用繼承的最高通用級別,但又不夠高,以至于對某些資源來說是不必要的并產生不必要的復雜性。
- 公司將希望專注于僅提供委托人進行日常運營所需的最低權限。拒絕 IAM 策略可能會增加擴展 IAM 模型以包含新要求的難度。
附錄:上述場景使用的 Terraform 文件
(文件 1)三個業務單位文件夾的計費成本管理器:
resource "google_folder" "PraetorianCloud" {
display_name = "PraetorianCloud"
parent = "organizations/<Praetorian Org ID>"
}
resource "google_folder_iam_member" "CFO_Cloud_Access" {
folder = google_folder.PraetorianCloud.id
role = "roles/billing.costManager"
member = "user:cfo@praetorian.com"
}
*repeat for Praetorian Webapp and Praetorian redteam folder
(文件 2)組織級別的計費成本管理器
resource "google_organization_iam_member" "CFO_Org_Access" {
org_id = "<Praetorian Org ID>"
role = "roles/billing.costManager"
member = "user:cfo@praetorian.com"
}
(文件 3)業務單元級別的角色分配
resource "google_folder_iam_member" "Cloud_Manager_Dev_Access" {
folder = google_folder.PraetorianCloud.id
role = "roles/compute.admin"
member = "user:cloudmanager@praetorian.com"
condition {
title = "Admin for development only"
description = "Admin for development only"
expression = "resource.matchTag('{projectid}/env', 'cloud-dev')"
}
}
resource "google_folder_iam_member" "Cloud_Manager_Stage_Access" {
folder = google_folder.PraetorianCloud.id
role = "roles/compute.networkUser"
member = "user:cloudmanager@praetorian.com"
condition {
title = "network user for staging only"
description = "network user for staging only"
expression = "resource.matchTag('{projectid}/env', 'cloud-stage')"
}
}
*repeat for Cloud production
(文件 4)環境級別的角色分配
resource "google_folder" "CloudProduction" {
display_name = "Production"
parent = google_folder.PraetorianCloud.id
}
resource "google_folder_iam_member" "Viewer_for_Cloud_manager_in_production" {
folder = google_folder.CloudProduction.id
role = "roles/compute.networkViewer"
member = "user:cloudmanager@praetorian.com"
}
resource "google_folder_iam_member" "Security_Reviewer_for_Security_Tester_in_production" {
folder = google_folder.CloudProduction.id
role = "roles/iam.securityReviewer"
member = "user:praetoriansecuritytester@praetorian.com"
}
*repeat for Cloud Staging and Cloud Development environments
(文件 5)項目級別的拒絕政策
resource "google_folder" "AthenaProject" {
display_name = "Athena"
parent = google_folder.CloudStaging.id
}
resource "google_folder_iam_member" "NetWork_Viewer_For Resources_with_the_not-sensitive_designation_in_the_Athena_project" {
folder = google_folder.AthenaProject.id
role = "compute.networkViewer"
member = "user:clouddeveloper@praetorian.com"
condition {
title = "Only allow not sensitive designation"
description = "Only allow not sensitive designation"
expression = “resource.matchTag('{projectid}/designation', 'not-sensitive')”
}
}
resource "google_iam_deny_policy" "Deny_if_is_sensitive" {
provider = google-beta
parent = urlencode("cloudresourcemanager.googleapis.com/${google_folder.AthenaProject.name}")
name = "my-deny-policy"
display_name = "A deny rule"
rules {
description = "Deny Developer"
deny_rule {
denied_principals = ["principal://goog/subject/clouddeveloper@praetorian.com"]
denial_condition {
title = "Conditional"
expression = "!resource.matchTag( '{projectid}/designation','not-sensitive') "
}
denied_permissions = [<perms for compute.networkviewer>]
}
}
}