At Praetorian, one of our top priorities is looking over each client’s Identity and Access Management (IAM) structure. Several of our large clients use Google Cloud Platform (GCP), which is one of the top three cloud providers with about eight percent of the cloud services market share. During our work with GCP, we have noticed that as an organization grows, additional divisions and projects significantly increase the complexity of IAM permissions.

This blog post aims, at a high level, to provide tools and methodologies to help large organizations think about how best to organize their IAM models. We will introduce core IAM concepts and discuss ways to logically organize groups of resources within GCP’s hierarchy chain to take advantage of IAM policy inheritances from parent resources. Finally, we will discuss three scenarios within a sample organization structure in GCP and use our built-in hierarchy to create simple conditional statements for access that are easy to verify and validate.

Start with basics:

IAM policies delineate who can take what actions on which resources . It seems simple, but can become incredibly complex as organizations attempt to scale their policies to accompany growth. Understanding each of the three concepts and how they interplay is essential to effective IAM regardless of an organization’s size.

The first building block of IAM is principal , which describes the who in our definition. IAM policies can apply to multiple GCP principals, including service accounts, google accounts, google groups, and google workspace.

The second building block of IAM is what actions. Roles are a collection of permissions that determine what action a principal can conduct. The following are the three types of roles :

  • Basic Roles: These include owner, editor, and viewer roles. These roles have a broad level of permissions and neither Google nor Praetorian recommends using them.
  • Predefined Roles : These are roles Google created with fine-grained access control.
  • Custom Roles : These are roles organizations create. Custom roles can grant users a minimum set of permissions.

The third building block of IAM is which resource, which dictates where within the cloud hierarchy a principal can perform their allowed actions. IAM policies define which resources have which permissions and what organizations are responsible for, and bind one or more principals with one or more roles. Figure 1 outlines GCP’s resource hierarchy:

Figure 1: Resource hierarchy in GCP.

GCP users need to bear in mind the IAM inheritance policy as they combine the three IAM building blocks. An organization can set IAM policies at any level (organization, folder, project, and resource) and resources will inherit all policies from parent resources. The resulting permissions will be a union of all policies.

Deny policies and conditional statements

Users can create deny policies that prevent permissions. In an explicit deny , deny policies will always override allowed permissions. This means principals will not be able to conduct an action if they have a deny IAM policy on that associated permission even if they also have a role containing that permission. In contrast, an implicit deny occurs in the absence of any policy to deny or allow permission. When a principal does not have an IAM policy that either denies or allows a permission, GCP denies it by default.

Conditional statements , which cause the IAM policy to apply only if the condition is met (i.e., resource has a tag), are an important part of effective IAM policy. This allows for IAM policies that do not apply to child resources via GCP’s IAM policy inheritance. While we will use tags and conditional statements to create a secure IAM model in this post, we also know conditional statements can unintentionally introduce IAM flaws. We will try to pare down conditional statements in our examples to reduce complexity and make them easier to audit and extend.

Division of Organization Structure

Creating a well-thought-out organizational structure will help us place IAM policies at the right position and reduce complexity. During the course of Praetorian’s work in multiple GCP environments, we have seen the following divisions in order of largest to smallest. Note that not all divisions may be present or needed.

Division by business unit

Organizations often attempt to arrange the highest level of organizational structure by business unit. For a large company, business units can be divisions of resources like Gmail and Google search. For a small company, business units can be more specific, like application frontend and application backend.

Dividing by business unit makes billing easier for the company. Doing so also establishes a one-to-one relationship between the principal and their business unit, as generally employees will not belong to multiple business units. Within GCP, business units typically map directly to the organization or to a folder. Note that business units can also switch amongst the groupings below depending on the company’s needs.

Division by environment type

At a middle level, organizations tend to separate the GCP environment by environment types like production, testing, and quality assurance. This generally involves a one-to-many mapping, as each employee may need access to multiple environments for their job function. Separation of environment allows companies to create more stringent rules as they approach production (i.e., fewer people should have permissions to prod). Within GCP, environment types usually map to organizations or folders.

Division by project name/team name

The lower level of organizational structure often separates the GCP environment by projects currently in development. This will generally map one-to-many, as employees can be a part of multiple teams and projects. GCP environments also allow for a shared project that contains shared resources across all projects or a collection of projects.

Figure 2 shows a sample picture of an organizational structure our engineers regularly encounter, which we will use as a basepoint to create some sample IAM policies that are easy to audit and extend.

Figure 2: Common organizational structure from which we will create our IAM policy scenarios.

Note that this is just one potential organizational structure in GCP. Organizations will find that each approach to structuring has its own set of costs and benefits.

Scenarios

We will now go over three scenarios that companies may commonly encounter while setting up their GCP environment. We will go through each scenario and try to place IAM policies at the appropriate level to make them more extensible and verifiable.

Requirement One: One predefined role for one user across multiple business units

The first scenario’s requirement is to create an IAM policy that gives our CFO reader the Billing Account Costs Manager predefined role (roles/billing.costsManager) applicable to the different business units. This will give the CFO a good idea of where the company is spending its money. Where should the rules go?

Solution: Highest possible organization level

A simple way to fulfill this requirement is to attach the IAM policy to the different business units, as shown in File 1 of the Appendix. This is inefficient, however, as newly created business units must also have this IAM policy. Additionally, this solution introduces redundancy into auditing the IAM policies, as the policies are identical.

The better method is to take advantage of the fact that IAM policies inherit from parent resources. We can put this IAM policy at the organizational level. Each business unit would then inherit the IAM policy from the organization, as shown in File 2 of the appendix.

From this scenario, we learned to insert IAM policies at the highest level possible to take advantage of GCP’s IAM policy inheritance.

Requirement Two: Multiple predefined roles for one user across multiple environment types

The second scenario’s requirement is to grant our Praetorian Cloud Manager the following:

  • The Compute Admin predefined role (roles/compute.admin) in development
  • The Compute Network User predefined role (roles/compute.networkUser) in staging
  • The Compute Network Viewer predefined role (roles/compute.networkViewer) in production

Our goal here is to follow the principle of least privilege where the user has less access as the project approaches production. Where should the IAM policies go?

Solution: Conditional statements

The cleanest way to create the IAM policies is to split them by the highest resource the policies have in common, then introduce conditional statements. Learning from the first scenario, we know GCP IAM inheritance will cause the different environment types to inherit permissions from the business unit. Putting the IAM policies at the business unit level has the benefit of keeping all the policies in one place, but can increase complexity as companies grow larger.

For our scenario, we can put the policy in the Praetorian cloud business unit and use conditional statements to give our Praetorian Cloud manager Compute Admin in development, Compute Network User in staging, and Compute Network Viewer in production. Tagging resources at the environment type folder level with tags like env:cloud-dev or env:cloud-staging allows us to then use conditional expressions like

resource.matchTag('{projectid}/env','cloud-dev')

to have permissions apply only at the environment type level. File 3 of the Appendix shows a sample Terraform file to accomplish the above.

Now imagine an extension where an additional Praetorian Security Tester responsible for testing applications in production would need the Security Reviewer ( roles/iam.securityReviewer ) predefined role in the Production Environment of the Praetorian Cloud Environment for testing purposes. We would need to add more conditional statements to the IAM policy to give permissions to the Praetorian Security Tester on only the Production environment. This would start to balloon the conditional statements and may cause unneeded complexity that could lead to IAM vulnerabilities.

Lesson Learned: Work at the highest level policies have in common.

In our scenario and its extension, the environment type resource is the highest level the IAM policies have in common. We can easily create IAM policies at this level to give both the Praetorian Cloud Manager and the Praetorian Security Tester the appropriate permissions within the appropriate environment type, as shown in File 4 of the Appendix.

From this scenario, we learned that while IAM policies should be put at the highest level possible to take advantage of GCP’s IAM policy inheritance, IAM policies should only be set at the highest level they have in common to reduce complexity in the form of conditional statements. Placing the policies at a higher level would take the lesson learned from Scenario One too far, and would result in exceedingly complex policies that are hard to validate or extend.

Requirement Three: One predefined role for one user on specifically tagged resources

The last scenario’s requirement is to give our Praetorian Cloud Developer the Compute Network Viewer predefined role to only those resources tagged with “designation:not-sensitive” in the Athena project of the cloud staging environment. How would we craft this IAM policy?

Solution: A deny policy?

From the above two exercises, we know we would like to put our IAM policies at the project level. Deny policies can then help us prevent access even when an allow IAM policy is present. However, we must pay particular attention to how any explicit deny policies interact across resources.

For the actual policy, we know we would like a conditional policy like

resource.matchTag('{projectid}/designation','not-sensitive')

to give Compute Network Viewer to our Praetorian Cloud Developer on the staging/athena project. Now, while GCP’s implicit deny will prevent the Praetorian Cloud Developer from having the Compute Network Viewer , we want to ensure we satisfy the only aspect of the requirement. For this, we can use a deny IAM policy, which would have a conditional statement like

!resource.matchTag('{projectid}/designation','not-sensitive')

This would deny access to those lacking the not-sensitive tag. File 5 of the Appendix includes Terraform files demonstrating this.

Lesson Learned: The implicit deny helps avoid unintended deny policy combinations.

While the above is a valid way of satisfying our last requirement, it is not very extensible. Consider an additional request where we would like to give Compute Network Viewer access to our Praetorian Cloud Developer to only those resources tagged with “designation:isfine”. We also know that all projects in the Praetorian Cloud staging environment will have resources tagged with “designation:isfine”. Only resources in those projects will have that designation. Using the same logic as above, we can have an IAM policy with the conditional statement

resource.matchTag('{projectid}/designation','isfine')

and a deny IAM policy with the conditional statement

!resource.matchTag('{projectid}/designation','isfine').

The problem will arise when looking at the staging/athena project. Because of IAM policy inheritance in GCP, the staging/athena project will inherit its parent’s deny statement. The project will have two deny IAM policies that deny any resources that do not have both the ‘designation:not-sensitive’ and ‘designation:isfine’ tag. This would be impossible as the ‘designation’ key cannot have both the `not-sensitive` and `isfine` value. This is not what the creator of either rule meant for our developer.

This scenario and its extension demonstrated the importance of being careful when using deny statements. Companies should, in most cases, allow GCP’s implicit deny to restrict permissions. Deny statements do have their uses in specific cases (i.e., deny at an organization level to enforce company policy, as when only the finance team can have billing permissions in the org). Those scenarios are more nuanced and require auditing and a mature security program to ensure that the inheritance of multiple deny IAM policies does not combine to prevent permissions beyond what the policies intended.

Best Practices 

Some of the key takeaways from our high level exploration of GCP IAM are as follows:

  • Companies commonly divide their GCP organizational structure by business units, environment types, and then projects/teams.
  • Companies should aim to put conditional IAM policies at the highest common level possible to take advantage of inheritance, but not high enough that it would be unnecessary to some resources and create unneeded complexity.
  • Companies will want to focus on only providing the minimum permissions necessary for principals to conduct their day-to-day operations. Deny IAM policies may increase the difficulty of extending an IAM model to include new requirements.

=======================================================================

Appendix: Terraform files used in the scenarios above

(File 1) Billing Cost Manager for three business unit folders:

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

(File 2) Billing Cost Manager at the organization level

resource "google_organization_iam_member" "CFO_Org_Access" { 

  org_id = "<Praetorian Org ID>" 

  role = "roles/billing.costManager" 

  member = "user:cfo@praetorian.com" 

}

(File 3) Role allocation at the business unit level

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

(File 4) Role allocation at the environment level

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

(File 5) Deny policy at the project level

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>] 

    }

  }

}