Blog

Demonstrating Google Principal Access Boundaries with terraform

Mark van Holsteijn

November 30, 2025
5 minutes

In this blog we present you with a terraform template and demo to help you understand how to configure Principal Access Boundaries on Google Cloud.

Principal Access Boundaries are organization-level policies that restrict what resources a principal (user, service account, or group) can access, regardless of the IAM permissions granted to them. This makes them a perfect tool to enforce data sharing agreements within a Google Cloud organization.

The Challenge

Imagine you have multiple business units in your organization. Each has business unit is supported by a data engineering team, which use data pipelines to manage their resources on Google Cloud. Their data pipeline service accounts have full IAM administrator privileges on their project, allowing them to create any role binding. However, from an organizational standpoint, it is not allowed to share data with business units without a data sharing contract. This is where Principal Access Boundaries come in.

The organization

The following diagram illustrates the folder hierarchy and resources of our fictive organization:

Organization
└── business-units (folder)
    ├── BU-001 (folder)
    │   └── com-xebia-bu-001 (project)
    │       └── data-pipeline (service-account)
    │       └── com-xebia-bu-001-data (bucket)
    ├── BU-002 (folder)
    │   └── com-xebia-bu-002 (project)
    │       └── data-pipeline (service-account)
    │       └── com-xebia-bu-002-data (bucket)
    └── BU-003 (folder)
        └── com-xebia-bu-003 (project)
            └── data-pipeline (service-account)
            └── com-xebia-bu-003-data (bucket)          

Each business unit has an individual folder (BU-001, BU-002, BU-003), under which you will find all projects for that unit. In this example we created three projects:com-xebia-bu-001,com-xebia-bu-002, com-xebia-bu-003, one for each respective business unit.

over-permissive storage role bindings

To demonstrate the principal access boundaries, we have granted the role roles/storage.objectAdmin to all service accounts on each bucket in each business unit.

bindings:
  - role: 'roles/storage.objectAdmin'
    members:
      -'data-pipeline@com-xebia-bu-001'
      -'data-pipeline@com-xebia-bu-002'
      -'data-pipeline@com-xebia-bu-003'

This clearly violates our policy on data sharing, as this allows each service account to read the index.html document in each bucket.

Data sharing contracts

As described earlier, business units need to have a data sharing contract in place to access data from other business units. These contracts are shown below:

data_sharing_contracts = {
  "BU-001" : ["BU-001"], # only access resources
  "BU-002" : ["BU-001", "BU-002"], # own and BU-001 resources
  "BU-003" : ["BU-001", "BU-002", "BU-003"]   # all BU resources
}

These contracts can be translated into principal access boundaries.

Principal Access Boundaries

The data sharing contracts from the previous paragraph can be enforced by Principal Access Boundary policies, as shown below:

policies:
  - policy_id: bu-001
    display_name: data sharing contract enforcement for BU-001
    details:
      rules:
        - effect: ALLOW
          resources:
            - //cloudresourcemanager.googleapis.com/folders/<folder BU-001>

  - policy_id: bu-002
    display_name: data sharing contract enforcement for BU-002
    details:
      rules:
        - effect: ALLOW
          resources:
            - //cloudresourcemanager.googleapis.com/folders/<folder BU-001>
            - //cloudresourcemanager.googleapis.com/folders/<folder BU-002>

  - policy_id: bu-003
    display_name: data sharing contract enforcement for BU-003
    details:
      rules:
        - effect: ALLOW
          resources:
            - //cloudresourcemanager.googleapis.com/folders/<folder BU-001>
            - //cloudresourcemanager.googleapis.com/folders/<folder BU-002>
            - //cloudresourcemanager.googleapis.com/folders/<folder BU-003>

As you can see, each policy lists all the resources that are included in the boundary. We have removed some properties and folder ids for clarity.

Enforcing Principal Access Boundaries

To enforce the principal access boundary, we have to bind them to a principal set. In our case, the policies are bound to the pincipal set of the corresponding business unit folder, as shown below:

bindings:
  - policy_binding_id: bu-001
    policy_kind: PRINCIPAL_ACCESS_BOUNDARY
    display_name: data sharing contract enforcement for BU-001
    policy: .../principalAccessBoundaryPolicies/bu-001
    folder: <folder BU-002>
    target:
      - principal_set: //cloudresourcemanager.googleapis.com/folders/<folder BU-001>

  - policy_binding_id: bu-002
    policy_kind: PRINCIPAL_ACCESS_BOUNDARY
    display_name: data sharing contract enforcement for BU-002
    policy: .../principalAccessBoundaryPolicies/bu-002
    folder: <folder BU-002>
    target:
      - principal_set: //cloudresourcemanager.googleapis.com/folders/<folder BU-002>

  - policy_binding_id: bu-003
    policy_kind: PRINCIPAL_ACCESS_BOUNDARY
    display_name: data sharing contract enforcement for BU-003
    policy: .../principalAccessBoundaryPolicies/bu-003
    folder: <folder BU-003>
    target:
      - principal_set: //cloudresourcemanager.googleapis.com/folders/<folder BU-003>  

Binding the policy to the folder, enforces the boundary to all security principals defined in the projects under the business unit folders: whether it is a service account, workload identity subjector workforce identity subject.

Switching service accounts

To show the principal access boundaries in action, the terraform template also creates a gcloud configuration script (config-gloud.sh), which allows for easy switching of service account. It creates three gcloud configurations:

  • bu-001- Impersonates BU-001 service account
  • bu-002- Impersonates BU-002 service account
  • bu-003- Impersonates BU-003 service account

The script configures the configuration as follows:

gcloud config set account $(gcloud config get account)
gcloud config set core/project com-xebia-bu-001
gcloud config set auth/impersonate_service_account data-pipeline@...

To start the demo, read and apply the terraform template principal-access-boundaries.tf type:

./config-gcloud.sh

Principal Access Boundaries in action

So, let try to access our resources from our own bucket in bu-001:

gcloud config configuration activate bu-001
gcloud storage cat gs://com-xebia-bu-001-data/index.html
<HTML>....</HTML>

It shows the content as expected. Now lets try to access one of the other BU buckets:

gcloud storage cat gs://com-xebia-bu-002-data/index.html
**ERROR:** (gcloud.storage.cat) HTTPError 403: data-pipeline@com-xebia-bu-001.iam.gserviceaccount.com does not have storage.objects.get access to the Google Cloud Storage object. Operations on resource are denied due to an IAM Principal Access Boundary Policy.

As you can see the access was denied and it also states that it was caused by the principal access boundary. This IAM policy troubleshooter, visualizes it as shown in the following image:

policy troubleshooter pab denied

It clearly indicates that it is prohibited by the principal access boundary even though the principal does have the IAM permission!

Conclusion

When combining Principal Access Boundaries with a proper folder hierarchy, you can use the policies to enforce defined data sharing agreements between business units, even when an IAM binding are in place.


Image by Ramon Perucho from Pixabay

Written by

Mark van Holsteijn

Mark van Holsteijn is a senior software systems architect at Xebia Cloud-native solutions. He is passionate about removing waste in the software delivery process and keeping things clear and simple.

Contact

Let’s discuss how we can support your journey.