<?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>Kubernetes on inetshell</title><link>https://inet.sh/es/posts/kubernetes/</link><description>Recent content in Kubernetes 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/posts/kubernetes/index.xml" rel="self" type="application/rss+xml"/><item><title>helmfile en producción: diff a tres bandas, 'already set in map', OCI 403, dependencias duplicadas y adoptar recursos que ya estaban desplegados</title><link>https://inet.sh/es/posts/kubernetes/helmfile-en-produccion-trucos/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/kubernetes/helmfile-en-produccion-trucos/</guid><description>Lo que se aprende de helmfile después de meses de usarlo en CI: HELM_DIFF_THREE_WAY_MERGE para ver la deriva real del clúster, HELMFILE_GOCCY_GOYAML para el error &amp;#39;key already set in map&amp;#39;, --skip-diff-on-install para instalaciones enormes, el 403 de registros OCI que arregla un helm más nuevo, &amp;#39;more than one dependency with name or alias&amp;#39; por desajuste de versiones, filtrar por -l name=, y cómo hacer que Helm adopte recursos que se aplicaron con kubectl. Con los cambios de helmfile 1.x y Helm 4.</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></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">HELM_DIFF_THREE_WAY_MERGE</span><span class="o">=</span><span class="nb">true</span>        <span class="c1"># diff contra lo que hay en el clúster, no contra el último release</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HELM_DIFF_NORMALIZE_MANIFESTS</span><span class="o">=</span><span class="nb">true</span>    <span class="c1"># quita ruido de formato del diff</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HELMFILE_GOCCY_GOYAML</span><span class="o">=</span><span class="nb">true</span>            <span class="c1"># helmfile 0.x: evita &#34;key already set in map&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span>rabbitmq diff        <span class="c1"># una sola release</span>
</span></span><span class="line"><span class="cl">helmfile -e prod apply --skip-diff-on-install <span class="c1"># primera instalación gigante sin diff</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Y en CI, versiones fijas de las tres piezas (<code>helm</code>, <code>helmfile</code>, <code>helm-diff</code>): la mitad de los errores raros de helmfile son combinaciones que no se han probado juntas.</p>
<h2 id="los-errores-y-qué-los-arregla">Los errores y qué los arregla</h2>
<table>
  <thead>
      <tr>
          <th>Mensaje</th>
          <th>Causa</th>
          <th>Arreglo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>helmfile diff</code> dice que no hay cambios, pero el clúster no coincide con el chart</td>
          <td>helm-diff compara el manifiesto del último release con el nuevo, no con el estado vivo; un <code>kubectl edit</code> a mano no aparece</td>
          <td><code>HELM_DIFF_THREE_WAY_MERGE=true</code> (o <code>--three-way-merge</code> en helm-diff)</td>
      </tr>
      <tr>
          <td>El diff está lleno de cambios de comillas, orden de claves e indentación</td>
          <td>Distintas versiones de Helm o del chart serializan distinto</td>
          <td><code>HELM_DIFF_NORMALIZE_MANIFESTS=true</code></td>
      </tr>
      <tr>
          <td><code>key &quot;image&quot; already set in map</code> al renderizar valores</td>
          <td>La librería YAML antigua no admite claves duplicadas ni ciertos anclajes en <code>values</code></td>
          <td><code>HELMFILE_GOCCY_GOYAML=true</code> en helmfile 0.x; helmfile 1.x ya usa esa librería</td>
      </tr>
      <tr>
          <td><code>Body is too long (maximum is 65536 characters)</code> o el job se eterniza en la primera instalación</td>
          <td>El diff de una instalación nueva es el chart entero, y el comentario en la PR o el log no lo aguantan</td>
          <td><code>--skip-diff-on-install</code></td>
      </tr>
      <tr>
          <td><code>Error: failed to authorize: … 403 Forbidden</code> al bajar un chart de <code>oci://ghcr.io/…</code></td>
          <td>Helm anterior a 3.8 con soporte OCI experimental, o sin <code>helm registry login</code></td>
          <td>Actualizar helm; <code>helm registry login ghcr.io</code> si el registro es privado</td>
      </tr>
      <tr>
          <td><code>Error: validation: more than one dependency with name or alias</code></td>
          <td>Desajuste entre helm y helmfile (helm 3.15 necesitó helmfile ≥ 0.164; helm 3.13 iba con 0.158)</td>
          <td>Actualizar helmfile a la par que helm; en 1.x ya no ocurre</td>
      </tr>
      <tr>
          <td>El diff sale sin color en CI</td>
          <td>helm-diff detecta que no hay TTY</td>
          <td><code>HELM_DIFF_COLOR=true</code></td>
      </tr>
  </tbody>
</table>
<h2 id="adoptar-recursos-que-ya-existen">Adoptar recursos que ya existen</h2>
<p>El caso típico: alguien instaló ArgoCD (o lo que sea) con <code>kubectl apply -f</code>, y ahora quieres que lo gestione una release de helmfile. Helm se niega a instalar sobre recursos que no son suyos (<code>rendered manifests contain a resource that already exists … invalid ownership metadata</code>). Desde Helm 3.2 basta con <strong>marcarlos como suyos</strong> antes de instalar:</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></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="nv">NS</span><span class="o">=</span>argocd<span class="p">;</span> <span class="nv">REL</span><span class="o">=</span>argocd
</span></span><span class="line"><span class="cl"><span class="k">for</span> kind in deploy sts svc cm secret sa ingress role rolebinding networkpolicy<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  kubectl -n <span class="nv">$NS</span> get <span class="nv">$kind</span> -o name 2&gt;/dev/null <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r r<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    kubectl -n <span class="nv">$NS</span> label <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> app.kubernetes.io/managed-by<span class="o">=</span>Helm --overwrite
</span></span><span class="line"><span class="cl">    kubectl -n <span class="nv">$NS</span> annotate <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> meta.helm.sh/release-name<span class="o">=</span><span class="nv">$REL</span> meta.helm.sh/release-namespace<span class="o">=</span><span class="nv">$NS</span> --overwrite
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="c1"># recursos de clúster: mismas etiquetas, sin -n</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> r in <span class="k">$(</span>kubectl get crd,clusterrole,clusterrolebinding -o name <span class="p">|</span> grep -i argo<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  kubectl label <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> app.kubernetes.io/managed-by<span class="o">=</span>Helm --overwrite
</span></span><span class="line"><span class="cl">  kubectl annotate <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> meta.helm.sh/release-name<span class="o">=</span><span class="nv">$REL</span> meta.helm.sh/release-namespace<span class="o">=</span><span class="nv">$NS</span> --overwrite
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span><span class="nv">$REL</span> diff     <span class="c1"># debería ser casi vacío</span>
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span><span class="nv">$REL</span> apply
</span></span></code></pre></td></tr></table>
</div>
</div><p>Solo marca lo que el chart va a renderizar: un recurso etiquetado que el chart no incluye no pasa nada, pero uno que el chart renderiza con otro nombre acaba duplicado.</p>
<h2 id="helmfile-1x-y-helm-4">helmfile 1.x y Helm 4</h2>
<p>Si vienes de 0.x, esto es lo que cambia al actualizar:</p>
<ul>
<li>Un <code>helmfile.yaml</code> con plantillas Go tiene que llamarse <strong><code>helmfile.yaml.gotmpl</code></strong>; el <code>.yaml</code> a secas ya no se renderiza.</li>
<li><code>environments:</code> y <code>releases:</code> van en <strong>partes separadas por <code>---</code></strong> dentro del mismo archivo.</li>
<li><code>helmfile delete</code> desaparece (es <code>destroy</code>), <code>helmfile charts</code> también, y <code>--args</code> se elimina.</li>
<li><code>HELMFILE_SKIP_INSECURE_TEMPLATE_FUNCTIONS</code> pasa a ser <code>HELMFILE_DISABLE_INSECURE_FEATURES</code>.</li>
<li>helmfile 1.x funciona con <strong>Helm 3 y Helm 4</strong>. Con Helm 4, instala helm-diff con <code>helm plugin install https://github.com/databus23/helm-diff --verify=false</code> (Helm 4 verifica la procedencia de los plugins y helm-diff no la publica).</li>
</ul>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>apply</code> sin diff previo en CI</strong>: <code>helmfile apply</code> ya hace el diff y solo aplica si hay cambios; <code>sync</code> aplica siempre. En pipelines, <code>diff</code> en la PR y <code>apply</code> en main.</li>
<li><strong><code>installed: false</code></strong> desinstala la release en el siguiente <code>apply</code>; útil para retirar cosas por Git, peligroso si lo copias de otro entorno.</li>
<li><strong>Secretos en <code>values</code></strong>: helmfile los deja en el manifiesto del release (Secret de Helm en el namespace); usa <code>helm-secrets</code> con SOPS o referencias externas.</li>
<li><strong><code>--skip-diff-on-install</code> no es <code>--skip-diff</code></strong>: solo se salta el diff en releases que aún no existen. Las actualizaciones siguen mostrando su diff.</li>
<li><strong>Tres binarios, tres versiones</strong>: <code>helm version</code>, <code>helmfile version</code> y <code>helm plugin list</code> en el primer paso del pipeline, y en la imagen de CI fijadas por tag.</li>
</ul>
<p>Referencias: <a href="https://github.com/databus23/helm-diff">helm-diff</a>, <a href="https://helmfile.readthedocs.io/">helmfile</a> y <a href="https://github.com/helmfile/helmfile/blob/main/docs/proposals/towards-1.0.md">la propuesta de cambios para 1.0</a>.</p>
<h2 id="ver-también">Ver también</h2>
<ul>
<li><a href="/es/posts/k3s/importar-imagenes-sin-registry/">k3s: importar imágenes sin registro</a> — cuando la release depende de una imagen que no está en ningún registro.</li>
</ul>
]]></content:encoded></item><item><title>Quitar Rancher de un clúster: cattle-system en Terminating, clústeres que no se dejan borrar, el webhook que bloquea todo y recuperar el admin</title><link>https://inet.sh/es/posts/kubernetes/rancher-limpiar-cattle-system-namespaces-terminating/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/kubernetes/rancher-limpiar-cattle-system-namespaces-terminating/</guid><description>Después de desinstalar Rancher (o de que se rompa), cattle-system y cattle-fleet-system se quedan en Terminating por el finalizer controller.cattle.io/namespace-auth, los objetos Cluster no se dejan borrar, y los webhooks rancher.cattle.io rechazan cualquier cambio porque el pod que los atendía ya no existe. El job oficial rancher-cleanup para limpiar todo de una vez, la cirugía manual cuando solo quieres arreglar una cosa, y reset-password / ensure-default-admin cuando el problema es que no puedes entrar.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</h2>
<p>Para <strong>quitar Rancher del todo</strong> de un clúster donde estuvo instalado (irreversible, con backup hecho):</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl create -f https://raw.githubusercontent.com/rancher/rancher-cleanup/main/deploy/rancher-cleanup.yaml
</span></span><span class="line"><span class="cl">kubectl -n kube-system logs -l job-name<span class="o">=</span>cleanup-job -f
</span></span><span class="line"><span class="cl">kubectl create -f https://raw.githubusercontent.com/rancher/rancher-cleanup/main/deploy/verify.yaml
</span></span><span class="line"><span class="cl">kubectl -n kube-system logs -l job-name<span class="o">=</span>verify-job -f      <span class="c1"># debe salir vacío</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Para <strong>arreglar una sola cosa</strong> sin arrasar, lo que sigue.</p>
<h2 id="namespace-en-terminating">Namespace en Terminating</h2>
<p><code>cattle-system</code>, <code>cattle-fleet-system</code>, <code>cattle-impersonation-system</code>, <code>local</code>, los <code>cluster-fleet-*</code>, o cualquier namespace que Rancher creó, se quedan en <code>Terminating</code> para siempre:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get ns cattle-system -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.metadata.finalizers}{&#34;\n&#34;}{.spec.finalizers}{&#34;\n&#34;}{.status.conditions}{&#34;\n&#34;}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Dos casos distintos:</p>
<ol>
<li>
<p><strong><code>metadata.finalizers</code> trae <code>controller.cattle.io/namespace-auth</code></strong> (o cualquier <code>*.cattle.io</code>). Lo puso Rancher y solo Rancher lo quita; si Rancher ya no está, nadie lo hará. Quítalo tú:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl patch ns cattle-system --type<span class="o">=</span>merge -p <span class="s1">&#39;{&#34;metadata&#34;:{&#34;finalizers&#34;:[]}}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong><code>spec.finalizers</code> trae <code>kubernetes</code> y <code>status.conditions</code> dice <code>SomeResourcesRemain</code> o <code>DiscoveryFailed</code></strong>. El API server no borra el namespace porque dentro quedan recursos, casi siempre CRs de un API group cuyo servicio desapareció (un <code>apiservice</code> de Rancher). Antes de forzar, mira qué es:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl api-resources --verbs<span class="o">=</span>list --namespaced -o name <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> xargs -n1 kubectl -n cattle-system get --ignore-not-found --show-kind 2&gt;/dev/null
</span></span><span class="line"><span class="cl">kubectl get apiservice <span class="p">|</span> grep -v True        <span class="c1"># APIs muertas que rompen el discovery</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Borra el <code>apiservice</code> muerto o los CRs con finalizers, y el namespace cae solo. Forzar <code>spec.finalizers</code> a vacío es el último recurso y deja huérfanos:</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-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get ns cattle-system -o json <span class="p">|</span> jq <span class="s1">&#39;.spec.finalizers=[]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> kubectl replace --raw /api/v1/namespaces/cattle-system/finalize -f -
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ol>
<h2 id="clúster-que-no-se-puede-borrar">Clúster que no se puede borrar</h2>
<p>Un clúster importado que ya no existe se queda en <em>Removing</em> en la UI, o <code>kubectl delete</code> no termina. Son dos objetos con finalizers, uno por API:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl -n fleet-default get clusters.provisioning.cattle.io
</span></span><span class="line"><span class="cl">kubectl -n fleet-default patch clusters.provisioning.cattle.io my-cluster --type<span class="o">=</span>merge -p <span class="s1">&#39;{&#34;metadata&#34;:{&#34;finalizers&#34;:[]}}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get clusters.management.cattle.io          <span class="c1"># c-m-xxxxxxxx (v2.6+) o c-xxxxx</span>
</span></span><span class="line"><span class="cl">kubectl patch clusters.management.cattle.io c-m-xxxxxxxx --type<span class="o">=</span>merge -p <span class="s1">&#39;{&#34;metadata&#34;:{&#34;finalizers&#34;:[]}}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Después limpia el namespace <code>cluster-fleet-default-&lt;nombre&gt;-&lt;hash&gt;</code> y los <code>clusters.fleet.cattle.io</code> que queden en <code>fleet-default</code>.</p>
<h2 id="el-webhook-que-rechaza-todo">El webhook que rechaza todo</h2>
<p><code>rancher-webhook</code> valida y muta recursos de <code>*.cattle.io</code> (y algunos de core, como namespaces y ServiceAccounts en clústeres gestionados). Si el pod no existe o está roto, cada cambio falla con:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error from server (InternalError): Internal error occurred: failed calling webhook &#34;rancher.cattle.io&#34;: … connect: connection refused
</span></span></code></pre></td></tr></table>
</div>
</div><p>Exporta las configuraciones, bórralas, deja que el pod vuelva sano, y restáuralas:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get validatingwebhookconfiguration rancher.cattle.io -o yaml &gt; vwc-rancher.yaml
</span></span><span class="line"><span class="cl">kubectl get mutatingwebhookconfiguration rancher.cattle.io -o yaml &gt; mwc-rancher.yaml
</span></span><span class="line"><span class="cl">kubectl delete validatingwebhookconfiguration rancher.cattle.io
</span></span><span class="line"><span class="cl">kubectl delete mutatingwebhookconfiguration rancher.cattle.io
</span></span><span class="line"><span class="cl"><span class="c1"># … arreglar rancher-webhook (kubectl -n cattle-system rollout restart deploy/rancher-webhook) …</span>
</span></span><span class="line"><span class="cl">kubectl apply -f vwc-rancher.yaml -f mwc-rancher.yaml
</span></span></code></pre></td></tr></table>
</div>
</div><p>Si estás desinstalando, no las restaures: son lo primero que hay que quitar para que el resto del borrado avance.</p>
<h2 id="recuperar-el-acceso">Recuperar el acceso</h2>
<p>Contraseña del admin perdida, o el usuario admin desactivado por error:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl -n cattle-system <span class="nb">exec</span> -it deploy/rancher -- reset-password
</span></span><span class="line"><span class="cl"><span class="c1"># New password for default administrator (user-xxxxx): …</span>
</span></span><span class="line"><span class="cl">kubectl -n cattle-system <span class="nb">exec</span> -it deploy/rancher -- ensure-default-admin
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>ensure-default-admin</code> reactiva el usuario y le devuelve el rol de administrador; <code>reset-password</code> solo cambia la contraseña.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>rancher-cleanup</code> borra los objetos de los clústeres aguas abajo</strong> (los clústeres siguen funcionando, pero dejan de estar registrados) y todo lo que Rancher instaló: monitoring, logging, Gatekeeper. Longhorn se queda. No lo lances en el clúster de Rancher que quieres conservar.</li>
<li><strong>Backup antes</strong>: <code>kubectl get -A -o yaml</code> de los <code>*.cattle.io</code> que te importen, o el backup-operator de Rancher. Los finalizers se quitan en un segundo; lo que había detrás no vuelve.</li>
<li><strong><code>kubectl delete ns --force --grace-period=0</code> no hace nada</strong> con un namespace en Terminating: el bloqueo está en los finalizers, no en el grace period.</li>
<li><strong>Rancher vuelve a poner los finalizers</strong> mientras esté vivo: si estás limpiando un clúster que todavía está conectado a un Rancher, desconéctalo primero (borra el <code>cattle-cluster-agent</code>) o los patches se deshacen solos.</li>
<li><strong>Los CRDs <code>*.cattle.io</code> sobreviven</strong> a una desinstalación por Helm; <code>rancher-cleanup</code> los quita, la desinstalación manual no.</li>
</ul>
<h2 id="ver-también">Ver también</h2>
<ul>
<li><a href="/es/posts/k3s/cluster-reset-perdida-de-quorum/">k3s: reset del clúster tras perder el quórum</a> — la otra cirugía de clúster que conviene tener a mano.</li>
<li><a href="https://github.com/rancher/rancher-cleanup">rancher-cleanup</a> y <a href="https://ranchermanager.docs.rancher.com/faq/technical-items">Technical FAQ de Rancher</a>.</li>
</ul>
]]></content:encoded></item><item><title>Vault Agent Injector en GKE privado: el sidecar no aparece y el pod tarda 30 s (el control plane no llega al webhook en el 8080)</title><link>https://inet.sh/es/posts/kubernetes/vault-agent-injector-gke-private-cluster-sin-sidecar/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/kubernetes/vault-agent-injector-gke-private-cluster-sin-sidecar/</guid><description>En un clúster GKE privado, los pods con vault.hashicorp.com/agent-inject arrancan sin los contenedores vault-agent-init y vault-agent, tardan medio minuto más en crearse y el injector no registra ninguna petición. El control plane solo puede abrir conexiones a los nodos en los puertos 443 y 10250; el injector escucha en el 8080 y el webhook falla en silencio porque failurePolicy es Ignore. Regla de firewall desde el rango del control plane, cómo validar en los logs del injector, y el mismo problema en cualquier otro webhook de admisión.</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></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="nv">CLUSTER</span><span class="o">=</span>my-cluster<span class="p">;</span> <span class="nv">REGION</span><span class="o">=</span>us-central1<span class="p">;</span> <span class="nv">NETWORK</span><span class="o">=</span>my-vpc
</span></span><span class="line"><span class="cl"><span class="nv">MASTER_CIDR</span><span class="o">=</span><span class="k">$(</span>gcloud container clusters describe <span class="nv">$CLUSTER</span> --region <span class="nv">$REGION</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --format<span class="o">=</span><span class="s1">&#39;value(privateClusterConfig.masterIpv4CidrBlock)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">NODE_TAG</span><span class="o">=</span><span class="k">$(</span>gcloud compute firewall-rules list --filter<span class="o">=</span><span class="s2">&#34;name~^gke-</span><span class="nv">$CLUSTER</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --format<span class="o">=</span><span class="s1">&#39;value(targetTags.list())&#39;</span> <span class="p">|</span> head -1<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud compute firewall-rules create gke-<span class="nv">$CLUSTER</span>-master-to-vault-injector <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --network<span class="o">=</span><span class="nv">$NETWORK</span> --direction<span class="o">=</span>INGRESS --action<span class="o">=</span>ALLOW <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --source-ranges<span class="o">=</span><span class="nv">$MASTER_CIDR</span> --target-tags<span class="o">=</span><span class="nv">$NODE_TAG</span> --rules<span class="o">=</span>tcp:8080
</span></span></code></pre></td></tr></table>
</div>
</div><p>Borra un pod anotado y comprueba que el injector recibe la llamada:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl -n app delete pod -l <span class="nv">app</span><span class="o">=</span>api
</span></span><span class="line"><span class="cl">kubectl -n vault logs deploy/vault-agent-injector <span class="p">|</span> grep mutate
</span></span><span class="line"><span class="cl"><span class="c1"># [INFO]  handler: Request received: Method=POST URL=/mutate?timeout=30s</span>
</span></span><span class="line"><span class="cl">kubectl -n app get pod -l <span class="nv">app</span><span class="o">=</span>api -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.items[0].spec.initContainers[*].name} {.items[0].spec.containers[*].name}{&#34;\n&#34;}&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># vault-agent-init api vault-agent</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="el-síntoma">El síntoma</h2>
<ul>
<li>El pod lleva <code>vault.hashicorp.com/agent-inject: &quot;true&quot;</code> y el resto de anotaciones, pero arranca <strong>solo con el contenedor de la aplicación</strong>: ni <code>vault-agent-init</code> ni <code>vault-agent</code>.</li>
<li>Crear el pod <strong>tarda unos 30 segundos más</strong> de lo normal.</li>
<li><code>kubectl get events</code> no dice nada del webhook. En los logs del injector no hay ni una línea de <code>Request received</code>.</li>
<li>En otros clústeres (zonales públicos, kind, k3s) el mismo chart y las mismas anotaciones funcionan.</li>
</ul>
<h2 id="por-qué">Por qué</h2>
<p>En un clúster <strong>privado</strong> de GKE el control plane vive en una VPC de Google y llega a tus nodos a través de la regla de firewall que GKE crea sola (<code>gke-&lt;cluster&gt;-&lt;hash&gt;-master</code>). Esa regla solo permite <strong>TCP 443 y 10250</strong> desde el rango del control plane hacia los nodos. Todo lo demás está cerrado.</p>
<p>Un webhook de admisión no pasa por el Service como lo haría un cliente normal: el API server resuelve el Service a sus endpoints y <strong>conecta directamente con la IP del pod en el <code>targetPort</code></strong>. El Service del injector (<code>vault-agent-injector-svc</code>) publica el 443, pero el pod escucha en el <strong>8080</strong> (<code>injector.port</code> del chart), así que la conexión real es al 8080 y el firewall la descarta.</p>
<p>Lo que pasa después lo explican dos valores por defecto del chart de Vault:</p>
<table>
  <thead>
      <tr>
          <th>Valor</th>
          <th>Por defecto</th>
          <th>Efecto</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>injector.webhook.timeoutSeconds</code></td>
          <td><code>30</code></td>
          <td>El API server espera 30 s la respuesta del webhook</td>
      </tr>
      <tr>
          <td><code>injector.webhook.failurePolicy</code></td>
          <td><code>Ignore</code></td>
          <td>Si el webhook no contesta, el pod se crea <strong>sin mutar</strong>, sin error</td>
      </tr>
  </tbody>
</table>
<p>De ahí los 30 segundos de retraso y el pod sin sidecar y sin ningún mensaje. Con <code>failurePolicy: Fail</code> habrías visto <code>failed calling webhook &quot;vault.hashicorp.com&quot;: … context deadline exceeded</code> en los eventos del ReplicaSet, que al menos apunta al sitio correcto.</p>
<h2 id="no-es-solo-vault">No es solo Vault</h2>
<p>Cualquier webhook de admisión, conversión de CRDs o API agregada que escuche en un puerto que no sea 443 o 10250 falla igual en GKE privado: el operador de OpenTelemetry (9443), Kyverno (9443), Gatekeeper (8443)… cert-manager escucha en el <strong>10250</strong> precisamente para no necesitar esta regla. La documentación de GKE lo lista como requisito de &ldquo;admission webhooks, aggregated API servers, webhook conversion&rdquo;: una regla por puerto, desde <code>masterIpv4CidrBlock</code> hacia los tags de los nodos.</p>
<p>Si el clúster está en Terraform, deja la regla junto al clúster para que no se pierda al recrearlo:</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></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">resource</span> <span class="s2">&#34;google_compute_firewall&#34; &#34;master_to_vault_injector&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span>          <span class="o">=</span> <span class="s2">&#34;gke-${google_container_cluster.main.name}-master-to-vault-injector&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  network</span>       <span class="o">=</span> <span class="k">google_compute_network</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">name</span>
</span></span><span class="line"><span class="cl"><span class="n">  direction</span>     <span class="o">=</span> <span class="s2">&#34;INGRESS&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  source_ranges</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_container_cluster</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">private_cluster_config</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="k">master_ipv4_cidr_block</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  target_tags</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;gke-${google_container_cluster.main.name}&#34;</span><span class="p">]</span><span class="c1">   # el tag que usan tus node pools
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">allow</span> {
</span></span><span class="line"><span class="cl"><span class="n">    protocol</span> <span class="o">=</span> <span class="s2">&#34;tcp&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    ports</span>    <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;8080&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="trampas">Trampas</h2>
<ul>
<li><strong>El tag equivocado</strong>: los nodos llevan <code>gke-&lt;cluster&gt;-&lt;hash&gt;-node</code>; cópialo de las reglas <code>gke-&lt;cluster&gt;-*</code> existentes en vez de escribirlo a mano. Con varios node pools, comprueba que todos tengan el tag.</li>
<li><strong>El rango del control plane cambia si recreas el clúster</strong>; una regla escrita a mano con el CIDR viejo deja de servir sin avisar.</li>
<li><strong><code>failurePolicy: Fail</code></strong> hace visible el fallo, pero mientras el webhook esté caído <strong>no se crea ningún pod</strong> en los namespaces que casen con el <code>namespaceSelector</code>; el chart excluye al propio injector, pero no a Vault si comparten namespace y etiquetas. Úsalo con <code>replicas: 2</code>.</li>
<li><strong>El injector sí recibe la petición y sigue sin sidecar</strong>: entonces ya no es red. Mira los logs de <code>vault-agent-init</code>: <code>permission denied</code> es el rol de Kubernetes auth (<code>bound_service_account_names</code>, <code>bound_service_account_namespaces</code>) o la política; <code>connection refused</code> es <code>vault.hashicorp.com/service</code> o la dirección de Vault.</li>
<li><strong>Un solo puerto por regla no escala</strong>: si acumulas webhooks, una regla con <code>--rules=tcp:8080,8443,9443</code> desde el control plane es más fácil de mantener que cinco.</li>
</ul>
<p>Referencias: <a href="https://github.com/hashicorp/vault-k8s/issues/46">vault-k8s #46</a>, <a href="https://github.com/hashicorp/vault-k8s/issues/32">vault-k8s #32</a> y <a href="https://docs.cloud.google.com/kubernetes-engine/docs/how-to/private-clusters">reglas de firewall en clústeres privados</a> en la documentación de GKE.</p>
<h2 id="ver-también">Ver también</h2>
<ul>
<li><a href="/es/posts/kubernetes/cert-manager-http01-308-ingressclassname/">cert-manager: el reto HTTP-01 devuelve 308</a> — otro componente que &ldquo;no hace nada&rdquo; hasta que miras el sitio correcto.</li>
<li><a href="/es/posts/gcp/github-actions-workload-identity-federation/">GitHub Actions → Google Cloud sin llaves</a> — para desplegar en ese GKE sin llaves JSON.</li>
</ul>
]]></content:encoded></item><item><title>cert-manager: el reto HTTP-01 devuelve 308 (falta ingressClassName en el solver) y otros fallos del self-check</title><link>https://inet.sh/es/posts/kubernetes/cert-manager-http01-308-ingressclassname/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/kubernetes/cert-manager-http01-308-ingressclassname/</guid><description>El Challenge de cert-manager se queda en &amp;#39;wrong status code 308, expected 200&amp;#39;: el Ingress temporal del solver no lo atiende tu ingress controller y la petición cae en el Ingress de la app con ssl-redirect. Arreglo con ingressClassName en el solver http01 (cert-manager 1.12+), cómo diagnosticar con kubectl describe challenge, las otras causas (404, hairpin NAT, TLS) y DNS-01 con Cloudflare como alternativa.</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Waiting for HTTP-01 challenge propagation: wrong status code &#39;308&#39;, expected &#39;200&#39;
</span></span></code></pre></td></tr></table>
</div>
</div><p>cert-manager crea un Ingress temporal (<code>cm-acme-http-solver-…</code>) para responder al reto, pero sin clase tu ingress controller no lo adopta; la petición a <code>/.well-known/acme-challenge/…</code> la contesta el Ingress de la aplicación, que redirige a HTTPS con <strong>308</strong>. Pon la clase en el solver:</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></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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">acme</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l">https://acme-v02.api.letsencrypt.org/directory</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">email</span><span class="p">:</span><span class="w"> </span><span class="l">admin@example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">privateKeySecretRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod-account-key</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">solvers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">http01</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">ingress</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">ingressClassName: nginx        # cert-manager &gt;= 1.12; antes: class</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Aplica y borra el reto atascado para que lo reintente:</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl delete challenge -n &lt;ns&gt; --all
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="diagnóstico">Diagnóstico</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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get certificate,certificaterequest,order,challenge -A
</span></span><span class="line"><span class="cl">kubectl describe challenge -n &lt;ns&gt; &lt;nombre&gt;            <span class="c1"># Status → Reason es la frase que buscas</span>
</span></span><span class="line"><span class="cl">kubectl get ingress -n &lt;ns&gt;                            <span class="c1"># el cm-acme-http-solver-xxxx tiene CLASS &lt;none&gt;?</span>
</span></span><span class="line"><span class="cl">curl -i http://app.example.com/.well-known/acme-challenge/prueba   <span class="c1"># desde fuera y desde un pod</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Antes de pedirle a Let&rsquo;s Encrypt que valide, cert-manager hace un <strong>self-check</strong>: intenta él mismo la URL del reto. El <code>Reason</code> del Challenge es el resultado de ese self-check y casi siempre apunta a la causa.</p>
<table>
  <thead>
      <tr>
          <th><code>Reason</code></th>
          <th>Causa</th>
          <th>Arreglo</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>wrong status code '308'</code> (o 301/302)</td>
          <td>El Ingress del solver no tiene clase; responde el de la app con <code>ssl-redirect</code></td>
          <td><code>ingressClassName</code> en el solver</td>
      </tr>
      <tr>
          <td><code>wrong status code '404'</code></td>
          <td>El solver no está corriendo, o el DNS del host apunta a otra IP</td>
          <td><code>kubectl get pods -n &lt;ns&gt; | grep acme-solver</code>; <code>dig app.example.com</code></td>
      </tr>
      <tr>
          <td><code>dial tcp … i/o timeout</code> / <code>connection refused</code></td>
          <td>Desde dentro del clúster no se llega a la IP pública: NAT sin hairpin, DNS distinto dentro y fuera</td>
          <td>NAT reflection o split DNS en el firewall (<a href="/es/posts/pfsense/nat-reflection-port-forward-desde-lan/">pfSense</a>)</td>
      </tr>
      <tr>
          <td><code>tls: handshake failure</code></td>
          <td>El Ingress de la app ya tiene TLS con un secreto vacío</td>
          <td>Anotación <code>cert-manager.io/issue-temporary-certificate: &quot;true&quot;</code></td>
      </tr>
      <tr>
          <td><code>wrong status code '503'</code></td>
          <td>Varios Ingress con el mismo host y el controller no los combina</td>
          <td><code>acme.cert-manager.io/http01-edit-in-place: &quot;true&quot;</code> en el Ingress</td>
      </tr>
  </tbody>
</table>
<h2 id="por-qué-ingressclassname">Por qué <code>ingressClassName</code></h2>
<p>Desde Kubernetes 1.18 la clase de un Ingress va en <code>spec.ingressClassName</code>; la anotación <code>kubernetes.io/ingress.class</code> quedó obsoleta y los controllers modernos (ingress-nginx 1.x, Traefik) pueden ignorarla. En cert-manager, el campo <code>class</code> del solver sigue escribiendo la anotación; <code>ingressClassName</code> (desde la 1.12) escribe el campo. Usa uno de los dos, y en 2026 el que toca es <code>ingressClassName</code>.</p>
<p>Sin clase, el Ingress temporal existe pero nadie lo sirve. El controller enruta la petición por host, encuentra el Ingress de la app, y aplica sus reglas: <code>ssl-redirect</code> → 308. De ahí que el error parezca de la app y no de cert-manager.</p>
<h2 id="alternativa-dns-01-con-cloudflare">Alternativa: DNS-01 con Cloudflare</h2>
<p>Cuando el clúster no es alcanzable desde Internet por el puerto 80, o quieres un wildcard, el reto va por DNS. Token en Cloudflare con <strong>Zone → DNS → Edit</strong> y <strong>Zone → Zone → Read</strong> sobre la zona:</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><span class="lnt">24
</span><span class="lnt">25
</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">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Secret</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare-api-token</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">Opaque</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">stringData</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">api-token</span><span class="p">:</span><span class="w"> </span><span class="l">&lt;token&gt;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">cert-manager.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ClusterIssuer</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-dns</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">acme</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">server</span><span class="p">:</span><span class="w"> </span><span class="l">https://acme-v02.api.letsencrypt.org/directory</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">email</span><span class="p">:</span><span class="w"> </span><span class="l">admin@example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">privateKeySecretRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-dns-account-key</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">solvers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">dns01</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">cloudflare</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">apiTokenSecretRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare-api-token</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">api-token</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Con DNS-01 no hay Ingress temporal, ni self-check por HTTP, ni hairpin; el self-check consulta el registro TXT, y si el DNS del clúster resuelve contra un servidor interno con la zona cacheada, el <code>Reason</code> será <code>DNS record for … not yet propagated</code>: paciencia o <code>--dns01-recursive-nameservers=1.1.1.1:53</code> en cert-manager.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong>Staging primero.</strong> <code>https://acme-staging-v02.api.letsencrypt.org/directory</code> no tiene límites de emisión; producción bloquea 5 emisiones por nombre y semana, y cada Challenge fallido que llega a Let&rsquo;s Encrypt cuenta como intento.</li>
<li><strong>Borrar el Challenge, no el Certificate.</strong> Borrar el <code>Order</code> o el <code>Challenge</code> fuerza un reintento con la configuración nueva; borrar el <code>Certificate</code> también borra el secreto y deja la app sin TLS mientras tanto.</li>
<li><strong><code>pathType: Exact</code></strong> en el Ingress del solver (cert-manager reciente) choca con algunos controllers viejos; si ves 503 en OpenShift, es eso.</li>
<li><strong>Un Issuer por namespace vs ClusterIssuer</strong>: el Ingress tiene que anotar el que exista (<code>cert-manager.io/cluster-issuer</code> o <code>cert-manager.io/issuer</code>); anotar el que no existe deja el Certificate en <code>Issuing</code> para siempre sin Challenge que describir.</li>
</ul>
<p>Referencias: <a href="https://cert-manager.io/docs/configuration/acme/http01/">HTTP01 solver</a>, <a href="https://cert-manager.io/docs/configuration/acme/dns01/cloudflare/">Cloudflare DNS01</a> y <a href="https://cert-manager.io/docs/troubleshooting/acme/">Troubleshooting ACME</a> en la documentación de cert-manager.</p>
]]></content:encoded></item></channel></rss>