<?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>DNS on inetshell</title><link>https://inet.sh/en/posts/dns/</link><description>Recent content in DNS on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/posts/dns/index.xml" rel="self" type="application/rss+xml"/><item><title>acme-dns: DNS-01 without handing over the key to the whole zone (CNAME delegation of _acme-challenge) with certbot and cert-manager</title><link>https://inet.sh/en/posts/dns/acme-dns-dns-01-cname-delegation/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/dns/acme-dns-dns-01-cname-delegation/</guid><description>A wildcard certificate needs the DNS-01 challenge, and that usually means storing on the server an API token with power over the whole DNS zone. acme-dns is a minimal DNS server that can only answer TXT records under its own subdomain: you delegate _acme-challenge.your-domain with a CNAME and every client gets credentials that only work for its own record. Server install, registration with allowfrom, certbot&amp;#39;s acme-dns-auth.py hook (first run with --debug-challenges), cert-manager&amp;#39;s acmeDNS solver with its JSON Secret, and the TTL and renewal traps.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<ol>
<li>
<p><strong>Delegate a small zone</strong> to the machine that will run acme-dns (records in your real DNS):</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> on that machine (<code>/etc/acme-dns/config.cfg</code>), DNS on 53 and the API on 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>Register</strong> a client and create the 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> with the 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>The first time, the hook registers itself, stores the credentials in <code>/etc/letsencrypt/acmedns.json</code> and <strong>pauses</strong> so you can create the CNAME it prints; Enter, and it validates. Renewals (<code>certbot renew</code>) never ask again.</p>
</li>
</ol>
<h2 id="why">Why</h2>
<p>DNS-01 is the only challenge that gives <strong>wildcards</strong> and the only one that works for machines with no public port 80. The usual price is an API token from your DNS provider on every server that requests certificates; that token can typically change <strong>any record in the zone</strong>, and it sits in plain text under <code>/etc/letsencrypt</code>.</p>
<p>acme-dns turns that around: it is an authoritative DNS server for a zone of its own (<code>auth.example.com</code>) that only serves TXT records under random subdomains, with an API to update them. Since <code>_acme-challenge.example.com</code> is a <strong>CNAME</strong> to one of those subdomains, Let&rsquo;s Encrypt follows the chain and validates the TXT there. Each client&rsquo;s credentials only work for its subdomain; if stolen, the worst case is someone requesting a certificate for that name, not rewriting your zone.</p>
<h2 id="cert-manager">cert-manager</h2>
<p>The same scheme, with the credentials in a 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 from the registration&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>The JSON key is the certificate&rsquo;s domain; <code>*.example.com</code> and <code>example.com</code> share the same entry (the <code>_acme-challenge.example.com</code> CNAME covers both). Every distinct domain needs its own registration and CNAME.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>allowfrom</code> with the IP acme-dns sees</strong>: the public egress IP of the server or the cluster, not the pod&rsquo;s. If <code>/update</code> fails with 401, it is this or the password.</li>
<li><strong>CNAME TTL</strong>: keep it low (60–300 s) the first time; with a one-hour TTL, a mistake in the CNAME takes an hour to correct in Let&rsquo;s Encrypt&rsquo;s eyes. The TXT is served by acme-dns with TTL 1.</li>
<li><strong>A real port 53</strong>: acme-dns must be reachable over UDP and TCP from the Internet, and be the <strong>NS for <code>auth.example.com</code></strong> in your parent zone. Without the NS record there is no delegation and validation returns NXDOMAIN.</li>
<li><strong>One registration per domain</strong>, not per server: if three servers request certificates for <code>example.com</code>, they share the same registration and the same <code>acmedns.json</code>, or each registers its own and the CNAME can only point to one. Share the credentials.</li>
<li><strong><code>--manual</code> renews fine</strong> because the hook is stored in <code>/etc/letsencrypt/renewal/&lt;domain&gt;.conf</code>; what you cannot do is change the hook without running <code>certonly</code> again.</li>
<li><strong>Version</strong>: acme-dns 2.0.2 is from February 2025 and the project is still active; the certbot hook has not changed in years and works with Python 3 after fixing the shebang.</li>
</ul>
<p>References: <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> and <a href="https://cert-manager.io/docs/configuration/acme/dns01/acme-dns/">ACMEDNS in cert-manager</a>.</p>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="/en/posts/kubernetes/cert-manager-http01-308-ingressclassname/">cert-manager: the HTTP-01 challenge returns 308</a> — when HTTP-01 is enough, and when it is not.</li>
<li><a href="/en/posts/pfsense/haproxy-acme-letsencrypt-cloudflare/">HAProxy on pfSense with ACME and Cloudflare</a> — DNS-01 with the provider&rsquo;s token, the case acme-dns avoids.</li>
<li><a href="/en/posts/dns/dkim-2048-txt-255-characters/">2048-bit DKIM in a split TXT</a> — another DNS record that breaks over details.</li>
</ul>
]]></content:encoded></item><item><title>2048-bit DKIM: the key does not fit in a 255-character TXT</title><link>https://inet.sh/en/posts/dns/dkim-2048-txt-255-characters/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/dns/dkim-2048-txt-255-characters/</guid><description>How to publish a 2048-bit DKIM key (about 410 characters) when the DNS provider rejects the TXT for exceeding 255: split it into several quoted strings inside the same record. Commands for Route 53 (CharacterStringTooLong), Google Cloud DNS, BIND and Terraform, and verification with dig and openssl.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</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></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">PUB</span><span class="o">=</span><span class="k">$(</span>openssl rsa -in dkim.key -pubout -outform DER 2&gt;/dev/null <span class="p">|</span> base64 <span class="p">|</span> tr -d <span class="s1">&#39;\n&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;v=DKIM1; k=rsa; p=</span><span class="nv">$PUB</span><span class="s2">&#34;</span> <span class="p">|</span> fold -w <span class="m">255</span> <span class="p">|</span> sed <span class="s1">&#39;s/.*/&#34;&amp;&#34;/&#39;</span> <span class="p">|</span> paste -sd<span class="s1">&#39; &#39;</span> -
</span></span></code></pre></td></tr></table>
</div>
</div><p>That prints the value ready to paste into the DNS provider, two quoted strings separated by a space:</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">&#34;v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvs7EROOYh35C/1s7M//pIaRvu1XWXzdexgBR2dO7eq1oDw+X08BNJUVUEte3z7os5CNeP7YfZrAqyDJvA59wMK1qul6oHyy7z5P1aB4zNczvO7SZ20HogWjHjzYJ0qwmSUUtjlKvECQzoTCbS/F0gB9rdUZN02MJopDs8YWcohy8oqNLnrflHdxWm6jAb3sSa&#34; &#34;EQReu6iRKKPiXTGUISSetY+iXLo2j/2c5g5ppiIYkUTq58asamMNiCzB7BeF4WNkjz2P+9IJmmtkkJ0M/U4oThUMvBBtQ/XUV4i2N4PGB359G5d/PIMiCj4go2axA4p8ul7Qafbtv0fyPwOIGSojwIDAQAB&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><p>Both go into <strong>the same TXT record</strong>. Whoever verifies DKIM concatenates them with no spaces. If Google Workspace or Microsoft 365 handed you the text and you do not have the private key, it is the same without the <code>openssl</code>: <code>echo &quot;&lt;value&gt;&quot; | fold -w 255 | sed 's/.*/&quot;&amp;&quot;/' | paste -sd' ' -</code>.</p>
<h2 id="the-problem">The problem</h2>
<p>A TXT record is made of one or more <em>character-strings</em>, and each one is capped at <strong>255 bytes</strong> (RFC 1035). The whole record can be much longer; what it cannot have is a single 410-character string.</p>
<table>
  <thead>
      <tr>
          <th>RSA key</th>
          <th>Length of <code>p=</code></th>
          <th>With <code>v=DKIM1; k=rsa; p=</code></th>
          <th>Fits in one string?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1024 bits</td>
          <td>216</td>
          <td>234</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>2048 bits</td>
          <td>392</td>
          <td>410</td>
          <td>No: two are needed</td>
      </tr>
      <tr>
          <td>4096 bits</td>
          <td>736</td>
          <td>754</td>
          <td>No: three</td>
      </tr>
  </tbody>
</table>
<p>Google Workspace and Microsoft 365 generate 2048-bit keys by default, and rightly so: 1024 has been discouraged since RFC 8301. So almost any new DKIM setup today runs into this, and each provider reacts differently:</p>
<table>
  <thead>
      <tr>
          <th>Provider</th>
          <th>What happens with 410 characters in a row</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Route 53</td>
          <td>Rejects it with <code>CharacterStringTooLong</code>, in the console and the CLI</td>
      </tr>
      <tr>
          <td>Google Cloud DNS</td>
          <td>Rejects the value; you have to type the quoted strings yourself</td>
      </tr>
      <tr>
          <td>Cloudflare</td>
          <td>Accepts it and splits it into 255-character chunks on its own</td>
      </tr>
      <tr>
          <td>BIND and most self-hosted servers</td>
          <td>The zone file does not load until you use several strings</td>
      </tr>
  </tbody>
</table>
<p>RFC 6376 (§3.6.2.2) requires verifiers to concatenate the strings before using them, so splitting the key is not a trick: it is the intended format.</p>
<h2 id="per-provider">Per provider</h2>
<h3 id="bind--zone-file">BIND / zone file</h3>
<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">selector._domainkey  300  IN  TXT  ( &#34;v=DKIM1; k=rsa; p=MIIBIjANBgkq...first part&#34;
</span></span><span class="line"><span class="cl">                                     &#34;second part...IDAQAB&#34; )
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="route-53">Route 53</h3>
<p>In the console, paste both quoted strings into <strong>Value</strong> separated by a space or a line break. From the CLI the value is a single string with escaped quotes:</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">aws route53 change-resource-record-sets --hosted-zone-id Z0123456789EXAMPLE --change-batch <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;Changes&#34;: [{&#34;Action&#34;: &#34;UPSERT&#34;, &#34;ResourceRecordSet&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;Name&#34;: &#34;selector._domainkey.example.com&#34;, &#34;Type&#34;: &#34;TXT&#34;, &#34;TTL&#34;: 300,
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;ResourceRecords&#34;: [{&#34;Value&#34;: &#34;\&#34;v=DKIM1; k=rsa; p=...first part\&#34; \&#34;second part...\&#34;&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s1">  }}]}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="google-cloud-dns">Google Cloud DNS</h3>
<p>In the console, same format in the data field. With <code>gcloud</code>:</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">gcloud dns record-sets create selector._domainkey.example.com. --zone<span class="o">=</span>example-zone <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --type<span class="o">=</span>TXT --ttl<span class="o">=</span><span class="m">300</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --rrdatas<span class="o">=</span><span class="s1">&#39;&#34;v=DKIM1; k=rsa; p=...first part&#34; &#34;second part...&#34;&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>With Terraform (<code>google_dns_record_set</code>), a single <code>rrdatas</code> element with escaped quotes:</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-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="n">rrdatas</span> <span class="o">=</span><span class="n"> [&#34;\&#34;v</span><span class="o">=</span><span class="n">DKIM1; k</span><span class="o">=</span><span class="n">rsa; p</span><span class="o">=</span><span class="p">...</span><span class="k">first</span> <span class="k">part</span><span class="err">\</span><span class="s2">&#34; \&#34;second part...\&#34;&#34;</span><span class="p">]</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h3 id="cloudflare">Cloudflare</h3>
<p>Paste the full value into <strong>Content</strong>; the dashboard splits it on save. When you query it with <code>dig</code> you will see two strings even though you entered one: that is normal.</p>
<h2 id="verifying">Verifying</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-shell" data-lang="shell"><span class="line"><span class="cl">dig +short TXT selector._domainkey.example.com
</span></span></code></pre></td></tr></table>
</div>
</div><p>Expected: <code>&quot;v=DKIM1; k=rsa; p=MIIB...&quot; &quot;...IDAQAB&quot;</code>, two strings, one record. Then check that the key decodes to 2048 bits, which confirms no character was lost at the cut:</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">dig +short TXT selector._domainkey.example.com <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> tr -d <span class="s1">&#39;&#34; \n&#39;</span> <span class="p">|</span> sed -E <span class="s1">&#39;s/.*p=([A-Za-z0-9+/=]+).*/\1/&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> base64 -d <span class="p">|</span> openssl pkey -pubin -inform DER -noout -text <span class="p">|</span> head -1
</span></span><span class="line"><span class="cl"><span class="c1"># Public-Key: (2048 bit)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>If <code>base64</code> complains, a space slipped inside the key or characters are missing. Finish with a test email and <code>Authentication-Results: ... dkim=pass</code> in the headers; for a closer look, <a href="https://dmarcian.com/dkim-inspector/">dmarcian&rsquo;s DKIM Inspector</a>.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>The cut can go anywhere</strong>, even mid-base64, but <strong>never put spaces inside the key</strong>. The space between the two strings separates strings; it is not data.</li>
<li><strong>Do not double the quotes.</strong> If the panel adds them for you and you paste <code>&quot;...&quot;</code>, <code>dig</code> returns <code>&quot;\&quot;v=DKIM1...\&quot;&quot;</code> and the verifier fails.</li>
<li><strong>One record, not two.</strong> Two separate TXT records with the same name are two independent values, and neither is a valid key.</li>
<li><strong>2048 is the sweet spot.</strong> 4096 means three strings and a DNS answer close to the UDP limit; it works, but gains nothing for DKIM.</li>
<li><strong>Rotation</strong>: publish the new key under a new selector, switch the signer, and delete the old selector after the TTL. Never edit the selector in use.</li>
<li><strong>SPF has the same limit</strong>, but an SPF that does not fit in 255 is usually a sign of too many <code>include:</code>; there the fix is shortening it, not splitting it.</li>
</ul>
<p>References: <a href="https://knowledge.workspace.google.com/admin/security/troubleshoot-dkim-issues">Google Workspace — troubleshoot DKIM issues</a>, <a href="https://repost.aws/knowledge-center/route53-resolve-dkim-text-record-error">AWS re:Post — CharacterStringTooLong</a>, and the <a href="https://www.mailhardener.com/tools/dns-record-splitter">Mailhardener DNS record splitter</a> if you would rather not use <code>fold</code>.</p>
]]></content:encoded></item></channel></rss>