<?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/es/tags/terraform/</link><description>Recent content in Terraform on inetshell</description><generator>Hugo</generator><language>es</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/es/tags/terraform/index.xml" rel="self" type="application/rss+xml"/><item><title>GitHub Actions → Google Cloud sin llaves: Workload Identity Federation con gcloud y Terraform</title><link>https://inet.sh/es/posts/gcp/github-actions-workload-identity-federation/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/gcp/github-actions-workload-identity-federation/</guid><description>Autenticar un workflow de GitHub Actions en Google Cloud sin guardar una llave JSON de service account: pool y proveedor OIDC de Workload Identity Federation, attribute condition que limita a tu organización o repo, binding workloadIdentityUser, y el paso google-github-actions/auth@v3 con permissions id-token: write. La misma configuración en Terraform con el módulo gh-oidc, y los errores típicos de permisos.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</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>Y en el 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"># sin esto GitHub no emite el token OIDC</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>Los roles que necesite el pipeline (Artifact Registry, Cloud Run, GKE…) se los das a <code>github-actions@…</code> como a cualquier service account.</p>
<h2 id="por-qué">Por qué</h2>
<p>La alternativa clásica es exportar una llave JSON del service account y guardarla como secreto en GitHub. Esa llave no caduca, cualquiera con acceso al repo o a los logs puede copiarla, y rotarla es un trabajo manual que nadie hace. Con <strong>Workload Identity Federation</strong> GitHub emite un token OIDC de corta vida para cada job, Google lo verifica contra el issuer de GitHub, comprueba tu <em>attribute condition</em> y entrega credenciales temporales del service account. No hay secreto que guardar ni rotar, y puedes limitar quién entra hasta el nivel de repo o rama.</p>
<h2 id="las-piezas">Las piezas</h2>
<table>
  <thead>
      <tr>
          <th>Pieza</th>
          <th>Qué es</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Pool</strong> (<code>github</code>)</td>
          <td>Contenedor de identidades externas; uno por proveedor basta</td>
      </tr>
      <tr>
          <td><strong>Proveedor OIDC</strong> (<code>github-oidc</code>)</td>
          <td>Confía en <code>https://token.actions.githubusercontent.com</code> y mapea claims del token (<code>repository</code>, <code>repository_owner</code>, <code>actor</code>, <code>ref</code>) a atributos</td>
      </tr>
      <tr>
          <td><strong>Attribute condition</strong></td>
          <td>Filtro CEL sobre esos claims. Sin ella, <strong>cualquier repo de GitHub</strong> podría intentar entrar al pool; el README oficial pide ponerla siempre</td>
      </tr>
      <tr>
          <td><strong>Binding <code>workloadIdentityUser</code></strong></td>
          <td>Qué identidades del pool pueden hacerse pasar por el service account: <code>attribute.repository/my-org/my-repo</code>, o <code>attribute.repository_owner/my-org</code> para toda la organización</td>
      </tr>
      <tr>
          <td><strong><code>auth@v3</code></strong></td>
          <td>Cambia el token de GitHub por credenciales de Google y las deja configuradas para <code>gcloud</code>, los SDK y Terraform</td>
      </tr>
  </tbody>
</table>
<p>Para restringir a una rama, mapea el claim y úsalo en la condición y en el 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="con-terraform">Con Terraform</h2>
<p>El módulo oficial hace pool, proveedor y bindings en un bloque:</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>El <code>output</code> es exactamente el valor que va en <code>workload_identity_provider</code> del workflow.</p>
<h2 id="sin-service-account">Sin service account</h2>
<p><code>auth@v3</code> también admite <strong>federación directa</strong>: el <code>principalSet</code> recibe roles IAM sin pasar por un service account (<code>workload_identity_provider</code> y <code>project_id</code>, sin <code>service_account</code>). Menos piezas, pero no todos los servicios aceptan principales federados; para Artifact Registry, GCS y la mayoría de APIs sí, y es el camino que Google recomienda para casos nuevos.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>id-token: write</code></strong> en <code>permissions</code> del job. Sin él, <code>auth</code> falla con <em>&ldquo;Unable to get ACTIONS_ID_TOKEN_REQUEST_URL&rdquo;</em>.</li>
<li><strong>Número de proyecto, no ID</strong>, en la ruta del proveedor y del <code>principalSet</code>: <code>projects/123456789012/…</code>.</li>
<li><strong><code>Permission 'iam.serviceAccounts.getAccessToken' denied</code></strong>: falta el binding <code>roles/iam.workloadIdentityUser</code> o el <code>attribute.repository</code> no coincide (mayúsculas y nombre exacto del repo).</li>
<li><strong>La condición debe referirse a claims mapeados</strong>: si usas <code>assertion.ref</code> en la condición sin haberlo mapeado a un atributo, el proveedor se crea pero nunca casa.</li>
<li><strong>Forks y PRs externos</strong>: los tokens de un PR desde un fork llevan <code>repository</code> del fork; una condición por <code>repository_owner</code> los deja fuera, que suele ser lo que quieres.</li>
<li><strong>Rotación</strong>: no hay nada que rotar, pero sí que revisar. <code>gcloud iam workload-identity-pools providers describe</code> y los logs de auditoría de STS dicen quién está entrando.</li>
</ul>
<p>Referencia: <a href="https://github.com/google-github-actions/auth">google-github-actions/auth</a> y <a href="https://docs.cloud.google.com/iam/docs/workload-identity-federation">Workload Identity Federation</a> en la documentación de Google Cloud.</p>
]]></content:encoded></item></channel></rss>