<?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>Haproxy on inetshell</title><link>https://inet.sh/en/tags/haproxy/</link><description>Recent content in Haproxy on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Fri, 25 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/haproxy/index.xml" rel="self" type="application/rss+xml"/><item><title>pfSense: Let's Encrypt certificate with ACME and Cloudflare DNS, then using it in HAProxy</title><link>https://inet.sh/en/posts/pfsense/haproxy-acme-letsencrypt-cloudflare/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/pfsense/haproxy-acme-letsencrypt-cloudflare/</guid><description>Issue a Let&amp;#39;s Encrypt certificate on pfSense with the ACME package and DNS-01 validation on Cloudflare (API token, Account ID and Zone ID only; no Global API Key), wildcard included, and hang it off the HAProxy frontend with an automatic restart on every renewal. With the token IP filter and DNS Sleep traps.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<ol>
<li><strong>Cloudflare</strong> → My Profile → API Tokens → <em>Create Token</em> → <em>Edit zone DNS</em> template: permissions <strong>Zone → Zone → Read</strong> and <strong>Zone → DNS → Edit</strong>, <em>Zone Resources</em> limited to your zone. Copy the token; the <strong>Account ID</strong> and <strong>Zone ID</strong> are on the zone&rsquo;s <em>Overview</em> page.</li>
<li><strong>pfSense</strong> → System → Package Manager → install <strong>acme</strong> and <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> (or <em>Staging</em> to test) → <em>Create new account key</em> → <em>Register</em> → Save.</li>
<li>Services → ACME → <em>Certificates</em> → Add: name, the account, <em>Domain SAN list</em>: <code>app.example.com</code> (and <code>*.example.com</code> if you want a wildcard) with method <strong>DNS-Cloudflare</strong>, and in its fields only <strong>Token</strong>, <strong>Account ID</strong> and <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> → edit → <em>SSL Offloading</em> ✔ on the listen address → <em>Certificate</em>: the one ACME just created → Save → <em>Apply Changes</em>.</li>
</ol>
<p>Automatic renewal (the package&rsquo;s daily cron) and HAProxy restarted on every renewal.</p>
<h2 id="cloudflare-the-token">Cloudflare: the token</h2>
<p>The <em>Global API Key</em> old guides ask for gives access to the whole account; an <strong>API token</strong> with minimal permissions is the right thing and is what the package&rsquo;s <em>DNS-Cloudflare</em> method uses when you fill in <code>Token</code>:</p>
<table>
  <thead>
      <tr>
          <th>Package field</th>
          <th>Value</th>
          <th>Where</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Token</td>
          <td>The API token</td>
          <td>My Profile → API Tokens</td>
      </tr>
      <tr>
          <td>Account ID</td>
          <td>Account ID</td>
          <td>Zone → Overview → <em>API</em> (right column)</td>
      </tr>
      <tr>
          <td>Zone ID</td>
          <td>Zone ID</td>
          <td>Same place</td>
      </tr>
      <tr>
          <td>Key / Email</td>
          <td>Empty</td>
          <td>Only for the Global API Key (do not use it)</td>
      </tr>
  </tbody>
</table>
<p>With <code>Token</code> one of the two IDs is enough; setting both does no harm. Token permissions: <strong>Zone:Read</strong> and <strong>DNS:Edit</strong> on the specific zone (<em>Include → Specific zone</em>).</p>
<p>If you enable <em>Client IP Address Filtering</em> on the token, it must include the firewall&rsquo;s public <strong>IPv4 and also IPv6</strong>: pfSense goes out through whichever it has, and a token limited to v4 fails as soon as the API call goes over v6.</p>
<h2 id="acme-account-and-certificate">ACME: account and certificate</h2>
<p><strong>Account.</strong> Create two: one against <em>Staging</em> to test and one against <em>Production</em>. Staging has no issuance limits but its certificates are not trusted; once everything works, switch the certificate&rsquo;s account to production and issue again.</p>
<p><strong>Certificate.</strong> Under Services → ACME → <em>Certificates</em> → Add:</p>
<ul>
<li><em>Domain SAN list</em>: one entry per name. For a <code>*.example.com</code> wildcard the validation has to be DNS-01 (HTTP-01 does not allow it). A wildcard does not cover the apex: add <code>example.com</code> as a second entry.</li>
<li><em>Method</em>: <strong>DNS-Cloudflare</strong> on each entry, with the three fields.</li>
<li><em>DNS Sleep</em>: Cloudflare propagates in seconds; the default is plenty. If issuance fails with <code>TXT not found</code>, raise it to <code>120</code>.</li>
<li><em>Key</em>: RSA 2048 or ECDSA 256; both work fine with HAProxy.</li>
<li><em>Actions list</em>: <strong>Restart Local Service</strong> → <strong>haproxy</strong>. Without it, HAProxy keeps serving the old certificate until someone restarts it, even though ACME has already renewed.</li>
</ul>
<p><strong>Issue</strong> from the certificate list. The result shows up under System → Certificates with the name you gave it; that is where HAProxy will see it.</p>
<h2 id="haproxy-using-the-certificate">HAProxy: using the certificate</h2>
<p>Services → HAProxy:</p>
<ol>
<li><em>Settings</em>: enable HAProxy, and leave <em>Max SSL Diffie-Hellman size</em> at 2048 or more.</li>
<li><em>Frontend</em> → your HTTPS frontend → <em>External address</em>: port <code>443</code>, <strong>SSL Offloading</strong> ✔ → <em>SSL Offloading</em> section → <strong>Certificate</strong>: the ACME one. For several domains on the same frontend, <em>Additional certificates</em> with the rest; HAProxy picks by SNI.</li>
<li>Save → <strong>Apply Changes</strong>.</li>
</ol>
<p>Test from outside:</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> and <code>notAfter</code> 90 days out.</p>
<p>As a bonus, the pfSense GUI with the same certificate: System → Advanced → <em>Admin Access</em> → <em>SSL/TLS Certificate</em>, and a second action on the ACME certificate: <strong>Shell Command</strong> <code>/etc/rc.restart_webgui</code>.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>Cloudflare in proxy mode (orange cloud)</strong> does not affect DNS-01, but it does affect how traffic arrives: if the <code>A</code> record is proxied, HAProxy sees Cloudflare&rsquo;s IP as the source. For an internal service behind pfSense, grey cloud.</li>
<li><strong><code>Issue</code> fails with <code>Invalid request headers</code> or <code>Authentication error</code></strong>: token without permission on that zone, token IP filter, or the Zone ID of another zone.</li>
<li><strong>Production rate limits</strong>: five certificates per set of names per week. Test on staging.</li>
<li><strong>The certificate renews; HAProxy does not reload it by itself.</strong> The restart action is mandatory. Check under Services → ACME that the last renewal has a date and in <code>openssl s_client</code> that <code>notAfter</code> moved.</li>
<li><strong>ACME certificates renew at 2/3 of their lifetime</strong> (about 60 days). If you see the cron running and nothing changes before then, that is normal.</li>
</ul>
<p>Reference: <a href="https://docs.netgate.com/pfsense/en/latest/packages/acme/index.html">ACME package</a> in the Netgate documentation and acme.sh&rsquo;s <a href="https://github.com/acmesh-official/acme.sh/wiki/dnsapi">DNS provider list</a> (<code>dns_cf</code> section).</p>
]]></content:encoded></item></channel></rss>