<?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>Ingress-Nginx on inetshell</title><link>https://inet.sh/es/tags/ingress-nginx/</link><description>Recent content in Ingress-Nginx on inetshell</description><generator>Hugo</generator><language>es</language><lastBuildDate>Fri, 25 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/es/tags/ingress-nginx/index.xml" rel="self" type="application/rss+xml"/><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>