<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Terraform on inetshell</title><link>https://inet.sh/en/tags/terraform/</link><description>Recent content in Terraform on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/terraform/index.xml" rel="self" type="application/rss+xml"/><item><title>GitHub Actions → Google Cloud without keys: Workload Identity Federation with gcloud and Terraform</title><link>https://inet.sh/en/posts/gcp/github-actions-workload-identity-federation/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/gcp/github-actions-workload-identity-federation/</guid><description>Authenticate a GitHub Actions workflow to Google Cloud without storing a service account JSON key: Workload Identity Federation pool and OIDC provider, an attribute condition that limits it to your org or repo, the workloadIdentityUser binding, and the google-github-actions/auth@v3 step with permissions id-token: write. The same setup in Terraform with the gh-oidc module, and the usual permission errors.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PROJECT_ID</span><span class="o">=</span>my-project
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PROJECT_NUMBER</span><span class="o">=</span><span class="k">$(</span>gcloud projects describe <span class="nv">$PROJECT_ID</span> --format<span class="o">=</span><span class="s1">&#39;value(projectNumber)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">GITHUB_ORG</span><span class="o">=</span>my-org
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">REPO</span><span class="o">=</span>my-org/my-repo
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud iam workload-identity-pools create github <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --project<span class="o">=</span><span class="nv">$PROJECT_ID</span> --location<span class="o">=</span>global --display-name<span class="o">=</span><span class="s2">&#34;GitHub Actions&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud iam workload-identity-pools providers create-oidc github-oidc <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --project<span class="o">=</span><span class="nv">$PROJECT_ID</span> --location<span class="o">=</span>global --workload-identity-pool<span class="o">=</span>github <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --display-name<span class="o">=</span><span class="s2">&#34;GitHub OIDC&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --issuer-uri<span class="o">=</span><span class="s2">&#34;https://token.actions.githubusercontent.com&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --attribute-mapping<span class="o">=</span><span class="s2">&#34;google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --attribute-condition<span class="o">=</span><span class="s2">&#34;assertion.repository_owner == &#39;</span><span class="nv">$GITHUB_ORG</span><span class="s2">&#39;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud iam service-accounts create github-actions --project<span class="o">=</span><span class="nv">$PROJECT_ID</span> --display-name<span class="o">=</span><span class="s2">&#34;GitHub Actions&#34;</span>
</span></span><span class="line"><span class="cl">gcloud iam service-accounts add-iam-policy-binding github-actions@<span class="nv">$PROJECT_ID</span>.iam.gserviceaccount.com <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --project<span class="o">=</span><span class="nv">$PROJECT_ID</span> --role<span class="o">=</span>roles/iam.workloadIdentityUser <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --member<span class="o">=</span><span class="s2">&#34;principalSet://iam.googleapis.com/projects/</span><span class="nv">$PROJECT_NUMBER</span><span class="s2">/locations/global/workloadIdentityPools/github/attribute.repository/</span><span class="nv">$REPO</span><span class="s2">&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And in the workflow:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">id-token</span><span class="p">:</span><span class="w"> </span><span class="l">write           </span><span class="w"> </span><span class="c"># without this GitHub does not issue the OIDC token</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">auth</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">google-github-actions/auth@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">workload_identity_provider</span><span class="p">:</span><span class="w"> </span><span class="l">projects/123456789012/locations/global/workloadIdentityPools/github/providers/github-oidc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">service_account</span><span class="p">:</span><span class="w"> </span><span class="l">github-actions@my-project.iam.gserviceaccount.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">google-github-actions/setup-gcloud@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">gcloud storage ls</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Whatever roles the pipeline needs (Artifact Registry, Cloud Run, GKE…) go to <code>github-actions@…</code> like to any service account.</p>
<h2 id="why">Why</h2>
<p>The classic alternative is exporting a service account JSON key and storing it as a GitHub secret. That key never expires, anyone with access to the repo or the logs can copy it, and rotating it is manual work nobody does. With <strong>Workload Identity Federation</strong> GitHub issues a short-lived OIDC token for each job, Google verifies it against GitHub&rsquo;s issuer, checks your <em>attribute condition</em>, and hands out temporary service account credentials. There is no secret to store or rotate, and you can restrict who gets in down to the repo or branch.</p>
<h2 id="the-pieces">The pieces</h2>
<table>
  <thead>
      <tr>
          <th>Piece</th>
          <th>What it is</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Pool</strong> (<code>github</code>)</td>
          <td>Container for external identities; one per provider is enough</td>
      </tr>
      <tr>
          <td><strong>OIDC provider</strong> (<code>github-oidc</code>)</td>
          <td>Trusts <code>https://token.actions.githubusercontent.com</code> and maps token claims (<code>repository</code>, <code>repository_owner</code>, <code>actor</code>, <code>ref</code>) to attributes</td>
      </tr>
      <tr>
          <td><strong>Attribute condition</strong></td>
          <td>CEL filter over those claims. Without it, <strong>any GitHub repo</strong> could try to enter the pool; the official README says to always set one</td>
      </tr>
      <tr>
          <td><strong><code>workloadIdentityUser</code> binding</strong></td>
          <td>Which pool identities may impersonate the service account: <code>attribute.repository/my-org/my-repo</code>, or <code>attribute.repository_owner/my-org</code> for the whole organization</td>
      </tr>
      <tr>
          <td><strong><code>auth@v3</code></strong></td>
          <td>Exchanges GitHub&rsquo;s token for Google credentials and leaves them configured for <code>gcloud</code>, the SDKs, and Terraform</td>
      </tr>
  </tbody>
</table>
<p>To restrict to a branch, map the claim and use it in both the condition and the binding:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">--attribute-mapping=&#34;…,attribute.ref=assertion.ref&#34;
</span></span><span class="line"><span class="cl">--attribute-condition=&#34;assertion.repository_owner == &#39;my-org&#39; &amp;&amp; assertion.ref == &#39;refs/heads/main&#39;&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="with-terraform">With Terraform</h2>
<p>The official module creates pool, provider, and bindings in one block:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">module</span> <span class="s2">&#34;github_actions_sa&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  source</span>     <span class="o">=</span> <span class="s2">&#34;terraform-google-modules/service-accounts/google&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  version</span>    <span class="o">=</span> <span class="s2">&#34;~&gt; 4.0&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  project_id</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  names</span>      <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;github-actions&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  project_roles</span> <span class="o">=</span><span class="n"> [&#34;${var.project_id}</span><span class="o">=</span><span class="err">&gt;</span><span class="k">roles</span><span class="err">/</span><span class="k">artifactregistry</span><span class="p">.</span><span class="k">writer</span><span class="err">&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="s2">&#34;gh_oidc&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  source</span>              <span class="o">=</span> <span class="s2">&#34;terraform-google-modules/github-actions-runners/google//modules/gh-oidc&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  project_id</span>          <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">project_id</span>
</span></span><span class="line"><span class="cl"><span class="n">  pool_id</span>             <span class="o">=</span> <span class="s2">&#34;github&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  provider_id</span>         <span class="o">=</span> <span class="s2">&#34;github-oidc&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  attribute_condition</span> <span class="o">=</span><span class="n"> &#34;assertion.repository_owner</span> <span class="o">==</span> <span class="err">&#39;</span><span class="k">my</span><span class="err">-</span><span class="k">org</span><span class="err">&#39;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  sa_mapping</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">    &#34;github-actions&#34;</span> <span class="o">=</span> {
</span></span><span class="line"><span class="cl"><span class="n">      sa_name</span>   <span class="o">=</span> <span class="s2">&#34;projects/${var.project_id}/serviceAccounts/${module.github_actions_sa.email}&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">      attribute</span> <span class="o">=</span> <span class="s2">&#34;attribute.repository/my-org/my-repo&#34;</span>
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">output &#34;workload_identity_provider&#34; { value</span> <span class="o">=</span> <span class="k">module</span><span class="p">.</span><span class="k">gh_oidc</span><span class="p">.</span><span class="k">provider_name</span> }
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <code>output</code> is exactly the value that goes into the workflow&rsquo;s <code>workload_identity_provider</code>.</p>
<h2 id="without-a-service-account">Without a service account</h2>
<p><code>auth@v3</code> also supports <strong>direct federation</strong>: the <code>principalSet</code> gets IAM roles without going through a service account (<code>workload_identity_provider</code> and <code>project_id</code>, no <code>service_account</code>). Fewer pieces, but not every service accepts federated principals; Artifact Registry, GCS, and most APIs do, and it is the path Google recommends for new setups.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>id-token: write</code></strong> in the job&rsquo;s <code>permissions</code>. Without it, <code>auth</code> fails with <em>&ldquo;Unable to get ACTIONS_ID_TOKEN_REQUEST_URL&rdquo;</em>.</li>
<li><strong>Project number, not ID</strong>, in the provider path and the <code>principalSet</code>: <code>projects/123456789012/…</code>.</li>
<li><strong><code>Permission 'iam.serviceAccounts.getAccessToken' denied</code></strong>: the <code>roles/iam.workloadIdentityUser</code> binding is missing or <code>attribute.repository</code> does not match (case and exact repo name).</li>
<li><strong>The condition must reference mapped claims</strong>: if you use <code>assertion.ref</code> in the condition without mapping it to an attribute, the provider gets created but never matches.</li>
<li><strong>Forks and external PRs</strong>: tokens from a fork&rsquo;s PR carry the fork&rsquo;s <code>repository</code>; a condition on <code>repository_owner</code> keeps them out, which is usually what you want.</li>
<li><strong>Rotation</strong>: nothing to rotate, but something to review. <code>gcloud iam workload-identity-pools providers describe</code> and the STS audit logs tell you who is coming in.</li>
</ul>
<p>Reference: <a href="https://github.com/google-github-actions/auth">google-github-actions/auth</a> and <a href="https://docs.cloud.google.com/iam/docs/workload-identity-federation">Workload Identity Federation</a> in the Google Cloud documentation.</p>
]]></content:encoded></item></channel></rss>