<?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>Acme on inetshell</title><link>https://inet.sh/es/tags/acme/</link><description>Recent content in Acme 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/acme/index.xml" rel="self" type="application/rss+xml"/><item><title>acme-dns: DNS-01 sin dar la llave de toda la zona (delegación CNAME de _acme-challenge) con certbot y cert-manager</title><link>https://inet.sh/es/posts/dns/acme-dns-dns-01-delegacion-cname/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/dns/acme-dns-dns-01-delegacion-cname/</guid><description>Para un certificado wildcard hace falta el reto DNS-01, y eso suele significar guardar en el servidor un token de API con permiso sobre toda la zona DNS. acme-dns es un servidor DNS mínimo que solo sabe responder TXT bajo su propio subdominio: delegas _acme-challenge.tu-dominio con un CNAME y cada cliente recibe credenciales que solo sirven para su registro. Instalación del servidor, registro con allowfrom, el hook acme-dns-auth.py de certbot (primera ejecución con --debug-challenges), el solver acmeDNS de cert-manager con su Secret JSON, y las trampas de TTL y de renovación.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</h2>
<ol>
<li>
<p><strong>Delega una zona pequeña</strong> a la máquina donde correrá acme-dns (registros en tu DNS real):</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">auth.example.com.   NS   acme.example.com.
</span></span><span class="line"><span class="cl">acme.example.com.   A    203.0.113.10
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>acme-dns</strong> en esa máquina (<code>/etc/acme-dns/config.cfg</code>), escuchando DNS en el 53 y la API en el 443:</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-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">general</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">listen</span> <span class="p">=</span> <span class="s2">&#34;0.0.0.0:53&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">protocol</span> <span class="p">=</span> <span class="s2">&#34;both&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">domain</span> <span class="p">=</span> <span class="s2">&#34;auth.example.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">nsname</span> <span class="p">=</span> <span class="s2">&#34;acme.example.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">nsadmin</span> <span class="p">=</span> <span class="s2">&#34;hostmaster.example.com&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">records</span> <span class="p">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;auth.example.com. A 203.0.113.10&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;auth.example.com. NS acme.example.com.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">api</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">ip</span> <span class="p">=</span> <span class="s2">&#34;0.0.0.0&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">port</span> <span class="p">=</span> <span class="s2">&#34;443&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">tls</span> <span class="p">=</span> <span class="s2">&#34;letsencrypt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">disable_registration</span> <span class="p">=</span> <span class="kc">false</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>Registra</strong> un cliente y crea el CNAME:</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">curl -s -X POST https://acme.example.com/register -H <span class="s1">&#39;Content-Type: application/json&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  -d <span class="s1">&#39;{&#34;allowfrom&#34;: [&#34;203.0.113.0/24&#34;]}&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># {&#34;username&#34;:&#34;eabcdb41-…&#34;,&#34;password&#34;:&#34;pbAX…&#34;,&#34;fulldomain&#34;:&#34;d420c923-….auth.example.com&#34;,&#34;subdomain&#34;:&#34;d420c923-…&#34;,&#34;allowfrom&#34;:[&#34;203.0.113.0/24&#34;]}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><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">_acme-challenge.example.com.   CNAME   d420c923-….auth.example.com.
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong>certbot</strong> con el hook:</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">curl -fsSL -o /etc/letsencrypt/acme-dns-auth.py https://raw.githubusercontent.com/joohoi/acme-dns-certbot-joohoi/master/acme-dns-auth.py
</span></span><span class="line"><span class="cl">chmod +x /etc/letsencrypt/acme-dns-auth.py
</span></span><span class="line"><span class="cl">sed -i <span class="s1">&#39;s|^#!/usr/bin/env python$|#!/usr/bin/env python3|; s|^ACMEDNS_URL = .*|ACMEDNS_URL = &#34;https://acme.example.com&#34;|&#39;</span> /etc/letsencrypt/acme-dns-auth.py
</span></span><span class="line"><span class="cl">certbot certonly --manual --manual-auth-hook /etc/letsencrypt/acme-dns-auth.py <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --preferred-challenges dns --debug-challenges -d <span class="s1">&#39;*.example.com&#39;</span> -d example.com
</span></span></code></pre></td></tr></table>
</div>
</div><p>La primera vez el hook se registra solo, guarda las credenciales en <code>/etc/letsencrypt/acmedns.json</code> y <strong>se para</strong> para que crees el CNAME que imprime; Enter, y valida. Las renovaciones (<code>certbot renew</code>) ya no preguntan nada.</p>
</li>
</ol>
<h2 id="por-qué">Por qué</h2>
<p>DNS-01 es el único reto que da <strong>wildcards</strong> y el único que funciona para máquinas sin puerto 80 público. El precio habitual es un token de API de tu proveedor DNS en cada servidor que pide certificados; ese token normalmente puede cambiar <strong>cualquier registro de la zona</strong>, y vive en texto plano en <code>/etc/letsencrypt</code>.</p>
<p>acme-dns le da la vuelta: es un servidor DNS autoritativo de una zona propia (<code>auth.example.com</code>) que solo sabe servir registros TXT bajo subdominios aleatorios, con una API para actualizarlos. Como <code>_acme-challenge.example.com</code> es un <strong>CNAME</strong> hacia uno de esos subdominios, Let&rsquo;s Encrypt sigue la cadena y valida el TXT allí. Las credenciales de cada cliente solo sirven para su subdominio; robadas, lo peor que pasa es que alguien pida un certificado para ese nombre, no que reescriba tu zona.</p>
<h2 id="cert-manager">cert-manager</h2>
<p>El mismo esquema, con las credenciales en un Secret:</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 -n cert-manager create secret generic acme-dns --from-file<span class="o">=</span>acmedns.json<span class="o">=</span>./acmedns.json
</span></span></code></pre></td></tr></table>
</div>
</div><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-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;example.com&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;username&#34;</span><span class="p">:</span> <span class="s2">&#34;eabcdb41-d89f-4580-826f-3e62e9755ef2&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;password&#34;</span><span class="p">:</span> <span class="s2">&#34;&lt;password del registro&gt;&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;fulldomain&#34;</span><span class="p">:</span> <span class="s2">&#34;d420c923-bbd7-4056-ab64-c3ca54c9b3cf.auth.example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;subdomain&#34;</span><span class="p">:</span> <span class="s2">&#34;d420c923-bbd7-4056-ab64-c3ca54c9b3cf&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&#34;allowfrom&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;203.0.113.0/24&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><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></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-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">acmeDNS</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">https://acme.example.com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">accountSecretRef</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">acme-dns</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">acmedns.json</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>La clave del JSON es el dominio del certificado; para <code>*.example.com</code> y <code>example.com</code> vale la misma entrada (el CNAME <code>_acme-challenge.example.com</code> cubre ambos). Cada dominio distinto necesita su registro y su CNAME.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>allowfrom</code> con la IP que ve acme-dns</strong>: la pública de salida del servidor o del clúster, no la del pod. Si falla el <code>/update</code> con 401, es esto o la contraseña.</li>
<li><strong>TTL del CNAME</strong>: ponlo bajo (60–300 s) la primera vez; con un TTL de una hora, un error en el CNAME tarda una hora en corregirse a ojos de Let&rsquo;s Encrypt. El TXT lo sirve acme-dns con TTL 1.</li>
<li><strong>Puerto 53 de verdad</strong>: acme-dns tiene que ser alcanzable por UDP y TCP desde Internet, y ser el <strong>NS de <code>auth.example.com</code></strong> en tu zona padre. Sin el registro NS, la delegación no existe y la validación devuelve NXDOMAIN.</li>
<li><strong>Un registro por dominio</strong>, no por servidor: si tres servidores piden certificados para <code>example.com</code>, comparten el mismo registro y el mismo <code>acmedns.json</code>, o cada uno registra el suyo y el CNAME solo puede apuntar a uno. Comparte las credenciales.</li>
<li><strong><code>--manual</code> renueva bien</strong> porque el hook está guardado en <code>/etc/letsencrypt/renewal/&lt;dominio&gt;.conf</code>; lo que no puedes es cambiar el hook sin volver a ejecutar <code>certonly</code>.</li>
<li><strong>Versión</strong>: acme-dns 2.0.2 es de febrero de 2025 y el proyecto sigue activo; el hook de certbot no ha cambiado en años y funciona con Python 3 tras corregir el shebang.</li>
</ul>
<p>Referencias: <a href="https://github.com/joohoi/acme-dns">acme-dns</a>, <a href="https://github.com/joohoi/acme-dns-certbot-joohoi">acme-dns-certbot-joohoi</a> y <a href="https://cert-manager.io/docs/configuration/acme/dns01/acme-dns/">ACMEDNS en cert-manager</a>.</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> — cuando HTTP-01 es suficiente y no lo es.</li>
<li><a href="/es/posts/pfsense/haproxy-acme-letsencrypt-cloudflare/">HAProxy en pfSense con ACME y Cloudflare</a> — DNS-01 con el token del proveedor, el caso que acme-dns evita.</li>
<li><a href="/es/posts/dns/dkim-2048-txt-255-caracteres/">DKIM de 2048 bits en un TXT partido</a> — otro registro DNS que se rompe por detalles.</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><item><title>pfSense: certificado Let's Encrypt con ACME y DNS de Cloudflare, y usarlo en HAProxy</title><link>https://inet.sh/es/posts/pfsense/haproxy-acme-letsencrypt-cloudflare/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/pfsense/haproxy-acme-letsencrypt-cloudflare/</guid><description>Emitir un certificado de Let&amp;#39;s Encrypt en pfSense con el paquete ACME y validación DNS-01 en Cloudflare (solo API token, Account ID y Zone ID; sin la Global API Key), incluido wildcard, y colgarlo del frontend de HAProxy con reinicio automático en cada renovación. Con la trampa del filtro de IP del token y del DNS Sleep.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</h2>
<ol>
<li><strong>Cloudflare</strong> → My Profile → API Tokens → <em>Create Token</em> → plantilla <em>Edit zone DNS</em>: permisos <strong>Zone → Zone → Read</strong> y <strong>Zone → DNS → Edit</strong>, <em>Zone Resources</em> limitado a tu zona. Copia el token; el <strong>Account ID</strong> y el <strong>Zone ID</strong> están en la página <em>Overview</em> de la zona.</li>
<li><strong>pfSense</strong> → System → Package Manager → instalar <strong>acme</strong> y <strong>haproxy</strong>.</li>
<li>Services → ACME → <em>Account keys</em> → Add: <em>ACME Server</em> <strong>Let&rsquo;s Encrypt Production ACME v2</strong> (o <em>Staging</em> para probar) → <em>Create new account key</em> → <em>Register</em> → Save.</li>
<li>Services → ACME → <em>Certificates</em> → Add: nombre, la cuenta, <em>Domain SAN list</em>: <code>app.example.com</code> (y <code>*.example.com</code> si quieres wildcard) con método <strong>DNS-Cloudflare</strong>, y en sus campos solo <strong>Token</strong>, <strong>Account ID</strong> y <strong>Zone ID</strong>. <em>Actions list</em>: <strong>Restart Local Service</strong> → <code>haproxy</code>. Save → <strong>Issue</strong>.</li>
<li>Services → HAProxy → <em>Frontend</em> → editar → <em>SSL Offloading</em> ✔ en la dirección de escucha → <em>Certificate</em>: el que acaba de crear ACME → Save → <em>Apply Changes</em>.</li>
</ol>
<p>Renovación automática (cron diario del paquete) y HAProxy reiniciado en cada renovación.</p>
<h2 id="cloudflare-el-token">Cloudflare: el token</h2>
<p>La <em>Global API Key</em> que piden las guías antiguas da acceso a toda la cuenta; un <strong>API token</strong> con permisos mínimos es lo correcto y es lo que el método <em>DNS-Cloudflare</em> del paquete usa cuando rellenas <code>Token</code>:</p>
<table>
  <thead>
      <tr>
          <th>Campo del paquete</th>
          <th>Valor</th>
          <th>Dónde</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Token</td>
          <td>El API token</td>
          <td>My Profile → API Tokens</td>
      </tr>
      <tr>
          <td>Account ID</td>
          <td>ID de la cuenta</td>
          <td>Zona → Overview → <em>API</em> (columna derecha)</td>
      </tr>
      <tr>
          <td>Zone ID</td>
          <td>ID de la zona</td>
          <td>Mismo sitio</td>
      </tr>
      <tr>
          <td>Key / Email</td>
          <td>Vacíos</td>
          <td>Solo para la Global API Key (no la uses)</td>
      </tr>
  </tbody>
</table>
<p>Con <code>Token</code> basta uno de los dos IDs; poner ambos no molesta. Permisos del token: <strong>Zone:Read</strong> y <strong>DNS:Edit</strong> sobre la zona concreta (<em>Include → Specific zone</em>).</p>
<p>Si activas <em>Client IP Address Filtering</em> en el token, tiene que incluir la IP pública <strong>IPv4 y también IPv6</strong> del firewall: pfSense sale por la que tenga a mano, y un token limitado a la v4 falla en cuanto la llamada a la API va por v6.</p>
<h2 id="acme-cuenta-y-certificado">ACME: cuenta y certificado</h2>
<p><strong>Cuenta.</strong> Crea dos: una contra <em>Staging</em> para probar y otra contra <em>Production</em>. Staging no tiene límites de emisión pero sus certificados no son de confianza; cuando todo funcione, cambia la cuenta del certificado a la de producción y vuelve a emitir.</p>
<p><strong>Certificado.</strong> En Services → ACME → <em>Certificates</em> → Add:</p>
<ul>
<li><em>Domain SAN list</em>: una entrada por nombre. Para un wildcard <code>*.example.com</code> la validación tiene que ser DNS-01 (HTTP-01 no lo permite). Un wildcard no cubre el apex: añade <code>example.com</code> como segunda entrada.</li>
<li><em>Method</em>: <strong>DNS-Cloudflare</strong> en cada entrada, con los tres campos.</li>
<li><em>DNS Sleep</em>: Cloudflare propaga en segundos; el valor por defecto sobra. Si la emisión falla por <code>TXT not found</code>, súbelo a <code>120</code>.</li>
<li><em>Key</em>: RSA 2048 o ECDSA 256; ambos van bien con HAProxy.</li>
<li><em>Actions list</em>: <strong>Restart Local Service</strong> → <strong>haproxy</strong>. Sin esto, HAProxy sigue sirviendo el certificado viejo hasta que alguien lo reinicie, aunque ACME ya haya renovado.</li>
</ul>
<p><strong>Issue</strong> desde la lista de certificados. El resultado aparece en System → Certificates con el nombre que le diste; ahí lo verá HAProxy.</p>
<h2 id="haproxy-usar-el-certificado">HAProxy: usar el certificado</h2>
<p>Services → HAProxy:</p>
<ol>
<li><em>Settings</em>: activar HAProxy, y en <em>Max SSL Diffie-Hellman size</em> deja 2048 o más.</li>
<li><em>Frontend</em> → tu frontend HTTPS → <em>External address</em>: puerto <code>443</code>, <strong>SSL Offloading</strong> ✔ → sección <em>SSL Offloading</em> → <strong>Certificate</strong>: el de ACME. Para varios dominios en el mismo frontend, <em>Additional certificates</em> con los demás; HAProxy elige por SNI.</li>
<li>Save → <strong>Apply Changes</strong>.</li>
</ol>
<p>Prueba desde fuera:</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">openssl s_client -connect app.example.com:443 -servername app.example.com &lt;/dev/null 2&gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> openssl x509 -noout -issuer -dates
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>issuer=C = US, O = Let's Encrypt</code> y <code>notAfter</code> a 90 días.</p>
<p>De regalo, la GUI de pfSense con el mismo certificado: System → Advanced → <em>Admin Access</em> → <em>SSL/TLS Certificate</em>, y una segunda acción en el certificado ACME: <strong>Shell Command</strong> <code>/etc/rc.restart_webgui</code>.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong>Cloudflare en modo proxy (nube naranja)</strong> no afecta a DNS-01, pero sí a cómo llega el tráfico: si el registro <code>A</code> está proxificado, HAProxy ve la IP de Cloudflare como origen. Para un servicio interno detrás de pfSense, nube gris.</li>
<li><strong><code>Issue</code> falla con <code>Invalid request headers</code> o <code>Authentication error</code></strong>: token sin permiso sobre esa zona, filtro de IP del token, o Zone ID de otra zona.</li>
<li><strong>Rate limits de producción</strong>: cinco certificados por conjunto de nombres a la semana. Prueba en staging.</li>
<li><strong>El certificado se renueva; HAProxy no lo recarga solo.</strong> La acción de reinicio es obligatoria. Comprueba en Services → ACME que la última renovación tiene fecha y en <code>openssl s_client</code> que <code>notAfter</code> avanzó.</li>
<li><strong>Los ACME se renuevan a los 2/3 de vida</strong> (unos 60 días). Si ves el cron ejecutándose y nada cambia antes de eso, es normal.</li>
</ul>
<p>Referencia: <a href="https://docs.netgate.com/pfsense/en/latest/packages/acme/index.html">ACME package</a> en la documentación de Netgate y la <a href="https://github.com/acmesh-official/acme.sh/wiki/dnsapi">lista de proveedores DNS de acme.sh</a> (sección <code>dns_cf</code>).</p>
]]></content:encoded></item></channel></rss>