<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>GCP Security Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/gcp-security/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/gcp-security/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Sat, 09 May 2026 18:38:09 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>GCP Security Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/gcp-security/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access</title>
		<link>https://linuxcent.com/gcp-iam-deep-dive/</link>
					<comments>https://linuxcent.com/gcp-iam-deep-dive/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Wed, 15 Apr 2026 18:06:02 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[GCP IAM]]></category>
		<category><![CDATA[GCP Security]]></category>
		<category><![CDATA[Google Cloud IAM]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Service Accounts]]></category>
		<category><![CDATA[Workload Identity]]></category>
		<guid isPermaLink="false">https://linuxcent.com/gcp-iam-deep-dive/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span>GCP IAM explained: resource hierarchy, predefined vs custom roles, service accounts, workload identity federation, and why the inheritance model changes access control.</p>
<p>The post <a href="https://linuxcent.com/gcp-iam-deep-dive/">GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <strong>GCP Resource Hierarchy IAM</strong> → <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>GCP IAM bindings inherit downward — a binding at Organization or Folder level applies to every project and resource beneath it</li>
<li>Basic roles (<code class="" data-line="">viewer</code>/<code class="" data-line="">editor</code>/<code class="" data-line="">owner</code>) are legacy constructs; use predefined or custom roles in production</li>
<li>Service account keys are a long-lived credential antipattern — use ADC, impersonation, or Workload Identity Federation instead</li>
<li><code class="" data-line="">allAuthenticatedUsers</code> bindings expose resources to any of 3 billion Google accounts — audit for these in every environment</li>
<li><code class="" data-line="">iam.serviceAccounts.actAs</code> is the GCP equivalent of AWS <code class="" data-line="">iam:PassRole</code> — a direct privilege escalation vector</li>
<li>Conditional bindings with time-bound expiry eliminate &#8220;I&#8217;ll remember to remove this&#8221; as an operational pattern</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">                GCP IAM Inheritance Model
─────────────────────────────────────────────────────────
Organization (company.com)
│
├─ IAM binding at Org level ──────────────────┐
│                                             │ inherits down
├── Folder: Production                        ▼
│   │                                 ALL nodes below
│   ├── Folder: Shared-Services
│   │       └── Project: infra-core
│   │               ├── GCS: config-bucket  ← affected
│   │               └── Secret Manager      ← affected
│   │
│   └── Project: prod-web-app
│           ├── GCS: prod-assets             ← affected
│           ├── Cloud SQL: prod-db           ← affected
│           └── BigQuery: analytics          ← affected
│
└── Folder: Development                      ← NOT affected by
        └── Project: dev-app                    Production binding
</code></pre>
<p>GCP resource hierarchy IAM inheritance is the mechanism that makes a single binding cascade through an entire estate. It&#8217;s also the reason high-level bindings carry far more blast radius than they appear to.</p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>GCP resource hierarchy IAM operates on one rule: bindings propagate downward. Grant access at the Organization level and it applies to every Folder, every Project, and every resource in your GCP estate. Grant it at a Folder and it applies to every Project below. This is by design — and it&#8217;s the reason IAM misconfigurations in GCP can have a blast radius that teams migrating from AWS don&#8217;t anticipate.</p>
<p>I once inherited a GCP environment where the previous team had taken what they thought was a shortcut. They had a folder called <code class="" data-line="">Production</code> with twelve projects in it. Rather than grant developers access to each project individually, they bound <code class="" data-line="">roles/editor</code> at the folder level. One binding, twelve projects, all covered. Fast.</p>
<p>When I audited what <code class="" data-line="">roles/editor</code> on that folder actually meant, I found it gave every developer in that binding write access to Cloud SQL databases they&#8217;d never heard of, BigQuery datasets from other teams, Pub/Sub topics in shared services, and Cloud Storage buckets that held data exports. Not because anyone intended that. Because permissions in GCP flow downward through the hierarchy, and a broad role at a high level means a broad role everywhere below it.</p>
<p>The developer who made that binding understood &#8220;Editor means edit access.&#8221; They didn&#8217;t think through what &#8220;edit access at the folder level&#8221; means across twelve projects. This is the GCP IAM trap that catches teams coming from AWS: the hierarchy feels like an organizational convenience feature, not an access control mechanism. It&#8217;s both.</p>
<hr />
<h2 id="the-resource-hierarchy-not-just-org-structure">The Resource Hierarchy — Not Just Org Structure</h2>
<p>GCP&#8217;s resource hierarchy is the backbone of its IAM model:</p>
<pre><code class="" data-line="">Organization  (e.g., company.com)
  └── Folder  (e.g., Production, Development, Shared-Services)
        └── Folder  (nested, optional — up to 10 levels)
              └── Project  (unit of resource ownership and billing)
                    └── Resource  (GCE instance, GCS bucket, Cloud SQL, BigQuery, etc.)
</code></pre>
<p>The critical rule: <strong>IAM bindings at any level inherit downward to every node below</strong>.</p>
<pre><code class="" data-line="">Org IAM binding:
  alice@company.com → roles/viewer (org-level)
    ↓ inherited by
  Folder: Production
    ↓ inherited by
  Project: prod-web-app
    ↓ inherited by
  GCS bucket &quot;prod-assets&quot;

Result: alice can list and read resources across the ENTIRE org,
        across every folder, every project, every resource.
        Even if none of those resources have a direct binding for alice.
</code></pre>
<p><code class="" data-line="">roles/viewer</code> at the org level sounds benign — it&#8217;s just read access. But read access to everything in the organization, including infrastructure configurations, customer data exports in GCS, BigQuery analytics, Cloud SQL connection details, and Kubernetes cluster configs. Not benign.</p>
<p>Before making any binding above the project level, trace it down. Ask: what does this role grant, and at every project and resource below this folder, am I comfortable with that?</p>
<pre><code class="" data-line=""># Understand your org structure before making changes
gcloud organizations list

gcloud resource-manager folders list --organization=ORG_ID

gcloud projects list --filter=&quot;parent.id=FOLDER_ID&quot;

# See all existing bindings at the org level — do this regularly
gcloud organizations get-iam-policy ORG_ID --format=json | jq &#039;.bindings[]&#039;
</code></pre>
<hr />
<h2 id="member-types-who-can-hold-a-binding">Member Types — Who Can Hold a Binding</h2>
<p>GCP uses the term <strong>member</strong> (being renamed to <strong>principal</strong>) for the identity in a binding:</p>
<table>
<thead>
<tr>
<th>Member Type</th>
<th>Format</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Google Account</td>
<td><code class="" data-line="">user:alice@company.com</code></td>
<td>Individual Google/Workspace account</td>
</tr>
<tr>
<td>Service Account</td>
<td><code class="" data-line="">serviceAccount:sa@project.iam.gserviceaccount.com</code></td>
<td>Machine identity</td>
</tr>
<tr>
<td>Google Group</td>
<td><code class="" data-line="">group:team@company.com</code></td>
<td>Workspace group</td>
</tr>
<tr>
<td>Workspace Domain</td>
<td><code class="" data-line="">domain:company.com</code></td>
<td>All users in a Workspace domain</td>
</tr>
<tr>
<td>All Authenticated</td>
<td><code class="" data-line="">allAuthenticatedUsers</code></td>
<td>Any authenticated Google identity — extremely broad</td>
</tr>
<tr>
<td>All Users</td>
<td><code class="" data-line="">allUsers</code></td>
<td>Anonymous + authenticated — public access</td>
</tr>
<tr>
<td>Workload Identity</td>
<td><code class="" data-line="">principal://iam.googleapis.com/...</code></td>
<td>External workloads via WIF</td>
</tr>
</tbody>
</table>
<p>The ones that have caused data exposure incidents: <code class="" data-line="">allAuthenticatedUsers</code> and <code class="" data-line="">allUsers</code>. Any GCS bucket or GCP resource bound to <code class="" data-line="">allAuthenticatedUsers</code> is accessible to any of the ~3 billion Google accounts in existence. I have seen production customer data exposed this way. A developer testing a public CDN pattern applied the binding to the wrong bucket.</p>
<p>Audit for these regularly:</p>
<pre><code class="" data-line=""># Find any project-level binding with allUsers or allAuthenticatedUsers
gcloud projects get-iam-policy my-project --format=json \
  | jq &#039;.bindings[] | select(.members[] | contains(&quot;allUsers&quot;) or contains(&quot;allAuthenticatedUsers&quot;))&#039;

# Check all GCS buckets in a project for public access
gsutil iam get gs://BUCKET_NAME \
  | grep -E &quot;(allUsers|allAuthenticatedUsers)&quot;
</code></pre>
<hr />
<h2 id="role-types-choose-the-right-granularity">Role Types — Choose the Right Granularity</h2>
<h3 id="basic-primitive-roles-dont-use-in-production">Basic (Primitive) Roles — Don&#8217;t Use in Production</h3>
<pre><code class="" data-line="">roles/viewer   → read access to most resources across the entire project
roles/editor   → read + write to most resources
roles/owner    → full access including IAM management
</code></pre>
<p>These are legacy roles from before GCP had service-specific roles. <code class="" data-line="">roles/editor</code> is particularly dangerous because it grants write access across almost every GCP service in the project. Use it in production and you have no meaningful separation of duties between your services.</p>
<p>I&#8217;ve seen <code class="" data-line="">roles/editor</code> granted to a data pipeline service account because &#8220;it needed access to BigQuery, Cloud Storage, and Pub/Sub.&#8221; All three of those have predefined roles. Three specific bindings. Instead: one broad role that also grants access to Cloud SQL, Kubernetes, Secret Manager, and Compute Engine — none of which the pipeline needed.</p>
<h3 id="predefined-roles-the-default-correct-choice">Predefined Roles — The Default Correct Choice</h3>
<p>Service-specific roles managed and updated by Google. For most use cases, these are the right choice:</p>
<pre><code class="" data-line=""># Find predefined roles for Cloud Storage
gcloud iam roles list --filter=&quot;name:roles/storage&quot; --format=&quot;table(name,title)&quot;
# roles/storage.objectViewer   — read objects (not list buckets)
# roles/storage.objectCreator  — create objects, cannot read or delete
# roles/storage.objectAdmin    — full object control
# roles/storage.admin          — full bucket + object control (much broader)

# See exactly what permissions a predefined role includes
gcloud iam roles describe roles/storage.objectViewer
</code></pre>
<p>The distinction between <code class="" data-line="">roles/storage.objectViewer</code> and <code class="" data-line="">roles/storage.admin</code> is the difference between &#8220;can read objects&#8221; and &#8220;can read objects, create objects, delete objects, and modify bucket IAM policies.&#8221; Use the narrowest role that covers the actual need.</p>
<h3 id="custom-roles-when-predefined-is-still-too-broad">Custom Roles — When Predefined Is Still Too Broad</h3>
<p>When you need finer control than any predefined role offers, create a custom role:</p>
<pre><code class="" data-line="">cat &gt; custom-log-reader.yaml &lt;&lt; &#039;EOF&#039;
title: &quot;Log Reader&quot;
description: &quot;Read application logs from Cloud Logging — nothing else&quot;
stage: &quot;GA&quot;
includedPermissions:
  - logging.logEntries.list
  - logging.logs.list
  - logging.logMetrics.get
  - logging.logMetrics.list
EOF

# Create at project level (available within one project)
gcloud iam roles create LogReader \
  --project=my-project \
  --file=custom-log-reader.yaml

# Or at org level (reusable across projects in the org)
gcloud iam roles create LogReader \
  --organization=ORG_ID \
  --file=custom-log-reader.yaml

# Grant the custom role
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;serviceAccount:log-reader@my-project.iam.gserviceaccount.com&quot; \
  --role=&quot;projects/my-project/roles/LogReader&quot;
</code></pre>
<p>Custom roles have an operational overhead: when Google adds new permissions to a service, predefined roles are updated automatically. Custom roles are not — you have to update them manually. For roles like &#8220;Log Reader&#8221; that are unlikely to need new permissions, this isn&#8217;t a concern. For roles like &#8220;App Admin&#8221; that span many services, it becomes a maintenance burden.</p>
<hr />
<h2 id="iam-policy-bindings-how-access-is-actually-granted">IAM Policy Bindings — How Access Is Actually Granted</h2>
<p>The mechanism for granting access in GCP is adding a binding to a resource&#8217;s IAM policy. A binding is: <code class="" data-line="">member + role + (optional condition)</code>.</p>
<pre><code class="" data-line=""># Grant a role on a project (all resources in the project inherit this)
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/storage.objectViewer&quot;

# Grant on a specific GCS bucket (narrower — only this bucket)
gcloud storage buckets add-iam-policy-binding gs://prod-assets \
  --member=&quot;serviceAccount:app@my-project.iam.gserviceaccount.com&quot; \
  --role=&quot;roles/storage.objectViewer&quot;

# Grant on a specific BigQuery dataset
bq update --add_iam_policy_binding \
  --member=&quot;group:analysts@company.com&quot; \
  --role=&quot;roles/bigquery.dataViewer&quot; \
  my-project:analytics_dataset

# View the current IAM policy on a project
gcloud projects get-iam-policy my-project --format=json

# View a specific resource&#039;s policy
gcloud storage buckets get-iam-policy gs://prod-assets
</code></pre>
<p>The choice between project-level and resource-level binding has real consequences. A binding on the GCS bucket affects only that bucket. A binding at the project level affects the bucket AND every other resource in the project. In practice, default to the most specific scope available. Only move up the hierarchy when the alternative is an unmanageable number of bindings.</p>
<h3 id="conditional-bindings-time-limited-and-context-scoped-access">Conditional Bindings — Time-Limited and Context-Scoped Access</h3>
<p>Conditions scope when a binding applies. They use CEL (Common Expression Language):</p>
<pre><code class="" data-line=""># Temporary access for a contractor — automatically expires
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;user:contractor@external.com&quot; \
  --role=&quot;roles/storage.objectViewer&quot; \
  --condition=&quot;expression=request.time &lt; timestamp(&#039;2026-06-30T00:00:00Z&#039;),title=Contractor access Q2 2026&quot;

# Access only from corporate network
gcloud projects add-iam-policy-binding my-project \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/bigquery.admin&quot; \
  --condition=&quot;expression=request.origin.ip.startsWith(&#039;10.0.&#039;),title=Corp network only&quot;
</code></pre>
<p>Temporary access that automatically expires is one of the most practical applications of conditional bindings. Instead of &#8220;I&#8217;ll grant access and remember to remove it,&#8221; you set an expiry and it removes itself. The cognitive overhead of tracking temporary grants doesn&#8217;t disappear — you still need to know the grant exists — but the risk of it outliving its purpose drops significantly.</p>
<hr />
<h2 id="service-accounts-gcps-machine-identity">Service Accounts — GCP&#8217;s Machine Identity</h2>
<p>Service accounts are the machine identity in GCP. They should be used for every workload that needs to call GCP APIs — GCE instances, GKE pods, Cloud Functions, Cloud Run services.</p>
<pre><code class="" data-line=""># Create a service account
gcloud iam service-accounts create app-backend \
  --display-name=&quot;App Backend Service Account&quot; \
  --project=my-project

SA_EMAIL=&quot;app-backend@my-project.iam.gserviceaccount.com&quot;

# Grant it the specific role it needs — on the specific resource it needs
gcloud storage buckets add-iam-policy-binding gs://app-assets \
  --member=&quot;serviceAccount:${SA_EMAIL}&quot; \
  --role=&quot;roles/storage.objectViewer&quot;

# Attach to a GCE instance
gcloud compute instances create my-vm \
  --service-account=&quot;${SA_EMAIL}&quot; \
  --scopes=&quot;cloud-platform&quot; \
  --zone=us-central1-a
</code></pre>
<p>From inside the VM, Application Default Credentials (ADC) handles authentication automatically:</p>
<pre><code class="" data-line=""># From the VM — ADC uses the attached SA without any credential configuration
gcloud auth application-default print-access-token

# Or via the metadata server directly
curl -H &quot;Metadata-Flavor: Google&quot; \
  &quot;http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token&quot;
</code></pre>
<h3 id="service-account-keys-the-antipattern-to-avoid">Service Account Keys — The Antipattern to Avoid</h3>
<p>A service account key is a JSON file containing a private key. It&#8217;s long-lived, it doesn&#8217;t expire automatically, and if it leaks it gives an attacker persistent access as that service account until someone discovers and revokes it.</p>
<pre><code class="" data-line=""># Creating a key — only if there is genuinely no alternative
gcloud iam service-accounts keys create key.json --iam-account=&quot;${SA_EMAIL}&quot;
# This generates a long-lived credential. It will exist until explicitly deleted.

# List all active keys — do this in every audit
gcloud iam service-accounts keys list --iam-account=&quot;${SA_EMAIL}&quot;

# Delete a key
gcloud iam service-accounts keys delete KEY_ID --iam-account=&quot;${SA_EMAIL}&quot;
</code></pre>
<p>In the GCP environment I mentioned earlier — the one with <code class="" data-line="">roles/editor</code> at the folder level — I also found 23 service account key files downloaded across the team&#8217;s laptops over 18 months. Nobody had a complete list of which keys were still valid and where they were stored. Several were for accounts that no longer existed. That&#8217;s not a hypothetical attack surface. It&#8217;s a breach waiting for a laptop to be stolen.</p>
<p><strong>Never create service account keys when:</strong><br />
&#8211; Code runs on GCE/GKE/Cloud Run/Cloud Functions — use the attached service account and ADC<br />
&#8211; Code runs in GitHub Actions — use Workload Identity Federation<br />
&#8211; Code runs on-premises with Kubernetes — use Workload Identity Federation with OIDC</p>
<h3 id="service-account-impersonation-the-right-alternative-to-keys">Service Account Impersonation — The Right Alternative to Keys</h3>
<p>Instead of downloading a key, grant a user or service account permission to impersonate the service account. They generate a short-lived token, not a permanent credential:</p>
<pre><code class="" data-line=""># Allow alice to impersonate the service account
gcloud iam service-accounts add-iam-policy-binding &quot;${SA_EMAIL}&quot; \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/iam.serviceAccountTokenCreator&quot;

# Alice generates a token for the SA — no key file, short-lived
gcloud auth print-access-token --impersonate-service-account=&quot;${SA_EMAIL}&quot;

# Or configure ADC to use impersonation
export GOOGLE_IMPERSONATE_SERVICE_ACCOUNT=&quot;${SA_EMAIL}&quot;
gcloud storage ls gs://app-assets  # runs as the SA
</code></pre>
<p>This is the right model for humans who need to act as service accounts for debugging or deployment: impersonate, use, done. The token expires. No file to manage.</p>
<hr />
<h2 id="workload-identity-federation-credentials-eliminated">Workload Identity Federation — Credentials Eliminated</h2>
<p>The cleanest solution for any workload running outside GCP that needs to call GCP APIs: Workload Identity Federation. The external workload authenticates with its native identity (a GitHub Actions OIDC JWT, an AWS IAM role, a Kubernetes service account token), exchanges it for a short-lived GCP access token, and never handles a service account key.</p>
<pre><code class="" data-line=""># Create a Workload Identity Pool
gcloud iam workload-identity-pools create &quot;github-actions-pool&quot; \
  --project=my-project \
  --location=global \
  --display-name=&quot;GitHub Actions WIF Pool&quot;

# Create a provider (GitHub OIDC)
gcloud iam workload-identity-pools providers create-oidc &quot;github-provider&quot; \
  --project=my-project \
  --location=global \
  --workload-identity-pool=&quot;github-actions-pool&quot; \
  --issuer-uri=&quot;https://token.actions.githubusercontent.com&quot; \
  --attribute-mapping=&quot;google.subject=assertion.sub,attribute.repository=assertion.repository&quot; \
  --attribute-condition=&quot;assertion.repository_owner == &#039;my-org&#039;&quot;

# Allow a specific GitHub repo to impersonate the SA
gcloud iam service-accounts add-iam-policy-binding &quot;${SA_EMAIL}&quot; \
  --role=&quot;roles/iam.workloadIdentityUser&quot; \
  --member=&quot;principalSet://iam.googleapis.com/projects/PROJECT_NUM/locations/global/workloadIdentityPools/github-actions-pool/attribute.repository/my-org/my-repo&quot;
</code></pre>
<p>GitHub Actions workflow — no key files, no secrets stored in GitHub:</p>
<pre><code class="" data-line="">jobs:
  deploy:
    permissions:
      id-token: write   # required for OIDC token request
      contents: read
    steps:
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: &quot;projects/PROJECT_NUM/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider&quot;
          service_account: &quot;app-backend@my-project.iam.gserviceaccount.com&quot;

      - run: gcloud storage cp dist/ gs://app-assets/ --recursive
</code></pre>
<p>The OIDC JWT from GitHub is presented to GCP, which verifies it against GitHub&#8217;s public keys, checks the attribute mapping and condition (only the specified repo can use this), and issues a short-lived GCP access token. The credential exists for the duration of the job and is then gone.</p>
<hr />
<h2 id="iam-deny-policies-org-wide-guardrails">IAM Deny Policies — Org-Wide Guardrails</h2>
<p>GCP added standalone deny policies separate from bindings. They override grants:</p>
<pre><code class="" data-line="">cat &gt; deny-iam-escalation.json &lt;&lt; &#039;EOF&#039;
{
  &quot;displayName&quot;: &quot;Deny IAM escalation permissions to non-admins&quot;,
  &quot;rules&quot;: [{
    &quot;denyRule&quot;: {
      &quot;deniedPrincipals&quot;: [&quot;principalSet://goog/group/developers@company.com&quot;],
      &quot;deniedPermissions&quot;: [
        &quot;iam.googleapis.com/roles.create&quot;,
        &quot;iam.googleapis.com/roles.update&quot;,
        &quot;iam.googleapis.com/serviceAccounts.actAs&quot;
      ]
    }
  }]
}
EOF

gcloud iam policies create deny-iam-escalation-policy \
  --attachment-point=&quot;cloudresourcemanager.googleapis.com/projects/my-project&quot; \
  --policy-file=deny-iam-escalation.json
</code></pre>
<p><code class="" data-line="">iam.serviceAccounts.actAs</code> is worth calling out specifically. It&#8217;s the GCP equivalent of AWS&#8217;s <code class="" data-line="">iam:PassRole</code> — it allows an identity to make a service act as a specified service account. If a developer can call <code class="" data-line="">actAs</code> on a high-privileged service account, they can launch a GCE instance using that service account and then operate with its permissions. Same privilege escalation pattern as <code class="" data-line="">iam:PassRole</code>, different name. Deny it for anyone who doesn&#8217;t explicitly need it.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong><code class="" data-line="">roles/editor</code> at folder level is a blast radius waiting to happen</strong><br />
The role sounds like &#8220;edit access.&#8221; At folder level it means edit access to every service in every project under that folder — including services nobody thought to trace. Always scope to the specific project or resource, never a folder unless the use case explicitly requires it.</p>
<p><strong><code class="" data-line="">allAuthenticatedUsers</code> on a GCS bucket is public to 3 billion accounts</strong><br />
Any Google account — personal Gmail included — qualifies as &#8220;authenticated.&#8221; I&#8217;ve seen production customer data exposed this way while a developer tested a CDN pattern on the wrong bucket. Audit for these bindings before they become a breach notification.</p>
<p><strong>Service account keys accumulate and nobody tracks them</strong><br />
In every GCP environment I&#8217;ve audited that allowed SA key creation, there were active keys for accounts that no longer existed, stored on laptops with no central inventory. Keys don&#8217;t expire. Audit with <code class="" data-line="">gcloud iam service-accounts keys list</code> across every SA in every project.</p>
<p><strong><code class="" data-line="">iam.serviceAccounts.actAs</code> is a privilege escalation path</strong><br />
If a principal can call <code class="" data-line="">actAs</code> on a high-privileged SA, they can launch a GCE instance with that SA and operate with its full permissions — without ever being directly granted those permissions. Block this with a deny policy for everyone who doesn&#8217;t explicitly need it.</p>
<p><strong>Org-level <code class="" data-line="">roles/viewer</code> is not a safe broad grant</strong><br />
Read access to every project config, every service configuration, every infrastructure metadata object across your entire GCP estate is not a benign grant. Treat any binding above the project level as high-blast-radius, regardless of the role.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># Audit org and folder structure before any high-level change
gcloud organizations list
gcloud resource-manager folders list --organization=ORG_ID
gcloud projects list --filter=&quot;parent.id=FOLDER_ID&quot;

# Inspect all bindings at org level
gcloud organizations get-iam-policy ORG_ID --format=json | jq &#039;.bindings[]&#039;

# Find allUsers / allAuthenticatedUsers in a project
gcloud projects get-iam-policy PROJECT_ID --format=json \
  | jq &#039;.bindings[] | select(.members[] | contains(&quot;allUsers&quot;) or contains(&quot;allAuthenticatedUsers&quot;))&#039;

# Check a GCS bucket for public access
gsutil iam get gs://BUCKET_NAME | grep -E &quot;(allUsers|allAuthenticatedUsers)&quot;

# Audit all user-managed SA keys across a project
gcloud iam service-accounts list --project=PROJECT_ID --format=&quot;value(email)&quot; \
  | xargs -I{} gcloud iam service-accounts keys list --iam-account={} --managed-by=user

# List predefined roles for a service
gcloud iam roles list --filter=&quot;name:roles/storage&quot; --format=&quot;table(name,title)&quot;

# Inspect what permissions a role actually includes
gcloud iam roles describe roles/storage.objectViewer

# Grant time-limited access with conditional binding
gcloud projects add-iam-policy-binding PROJECT_ID \
  --member=&quot;user:contractor@external.com&quot; \
  --role=&quot;roles/storage.objectViewer&quot; \
  --condition=&quot;expression=request.time &lt; timestamp(&#039;2026-06-30T00:00:00Z&#039;),title=Contractor Q2 2026&quot;

# Enable SA impersonation (avoids key creation)
gcloud iam service-accounts add-iam-policy-binding SA_EMAIL \
  --member=&quot;user:alice@company.com&quot; \
  --role=&quot;roles/iam.serviceAccountTokenCreator&quot;
</code></pre>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>GCP&#8217;s hierarchical model and service account patterns are the primary IAM constructs for GCP environments</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3 — Security Architecture</td>
<td>Resource hierarchy design determines access inheritance — architectural decisions with direct security implications</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>GCP IAM bindings are the technical implementation of access control policy in GCP environments</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Service account provisioning, conditional bindings with expiry, and workload identity federation</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.2 Privileged access rights</td>
<td>Folder/org-level bindings and basic roles represent the highest-risk privilege grants in GCP</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>IAM bindings and Workload Identity Federation address machine identity controls for CC6.1</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>Conditional bindings with time-bound expiry directly satisfy access removal requirements</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>GCP IAM is hierarchical — bindings inherit downward; a binding at org or folder level has much larger scope than it appears</li>
<li>Basic roles (viewer/editor/owner) are too coarse for production; use predefined or custom roles and grant at the narrowest scope</li>
<li>Service account keys are a long-lived credential antipattern; use ADC on GCP infrastructure, impersonation for humans, and Workload Identity Federation for external workloads</li>
<li><code class="" data-line="">allAuthenticatedUsers</code> and <code class="" data-line="">allUsers</code> bindings expose resources to the internet — audit for these in every environment</li>
<li><code class="" data-line="">iam.serviceAccounts.actAs</code> is a privilege escalation vector — treat it like <code class="" data-line="">iam:PassRole</code></li>
<li>Conditional bindings with expiry dates are better than &#8220;I&#8217;ll remember to remove this later&#8221;</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP06 covers Azure RBAC and Entra ID — the most directory-centric of the three models, where Active Directory&#8217;s 25 years of enterprise history shapes both the strengths and the complexity of Azure&#8217;s access control.</p>
<p><em>Next: <a href="/azure-rbac-entra-id-guide/">Azure RBAC Scopes</a> — Management Groups, Subscriptions, and how role inheritance works across the Microsoft estate.</em></p>
<p>Get EP06 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe/">subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&amp;linkname=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fgcp-iam-deep-dive%2F&#038;title=GCP%20IAM%20Policy%20Inheritance%3A%20How%20the%20Resource%20Hierarchy%20Controls%20Access" data-a2a-url="https://linuxcent.com/gcp-iam-deep-dive/" data-a2a-title="GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access"></a></p><p>The post <a href="https://linuxcent.com/gcp-iam-deep-dive/">GCP IAM Policy Inheritance: How the Resource Hierarchy Controls Access</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/gcp-iam-deep-dive/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1477</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-08-22 12:19:35 by W3 Total Cache
-->