<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Kubernetes on inetshell</title><link>https://inet.sh/en/posts/kubernetes/</link><description>Recent content in Kubernetes 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/kubernetes/index.xml" rel="self" type="application/rss+xml"/><item><title>helmfile in production: three-way diff, 'already set in map', OCI 403, duplicate dependencies and adopting resources that were already deployed</title><link>https://inet.sh/en/posts/kubernetes/helmfile-in-production-tricks/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/kubernetes/helmfile-in-production-tricks/</guid><description>What you learn about helmfile after months of running it in CI: HELM_DIFF_THREE_WAY_MERGE to see the real cluster drift, HELMFILE_GOCCY_GOYAML for the &amp;#39;key already set in map&amp;#39; error, --skip-diff-on-install for huge installs, the OCI registry 403 that a newer helm fixes, &amp;#39;more than one dependency with name or alias&amp;#39; from version mismatches, filtering with -l name=, and how to make Helm adopt resources that were applied with kubectl. Plus the helmfile 1.x and Helm 4 changes.</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><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HELM_DIFF_THREE_WAY_MERGE</span><span class="o">=</span><span class="nb">true</span>        <span class="c1"># diff against what is in the cluster, not against the last release</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HELM_DIFF_NORMALIZE_MANIFESTS</span><span class="o">=</span><span class="nb">true</span>    <span class="c1"># strip formatting noise from the diff</span>
</span></span><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HELMFILE_GOCCY_GOYAML</span><span class="o">=</span><span class="nb">true</span>            <span class="c1"># helmfile 0.x: avoids &#34;key already set in map&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span>rabbitmq diff        <span class="c1"># a single release</span>
</span></span><span class="line"><span class="cl">helmfile -e prod apply --skip-diff-on-install <span class="c1"># giant first install without a diff</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And in CI, pinned versions of all three pieces (<code>helm</code>, <code>helmfile</code>, <code>helm-diff</code>): half of helmfile&rsquo;s odd errors are combinations nobody tested together.</p>
<h2 id="the-errors-and-what-fixes-them">The errors and what fixes them</h2>
<table>
  <thead>
      <tr>
          <th>Message</th>
          <th>Cause</th>
          <th>Fix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>helmfile diff</code> says nothing changed, but the cluster does not match the chart</td>
          <td>helm-diff compares the last release&rsquo;s manifest with the new one, not the live state; a manual <code>kubectl edit</code> never shows up</td>
          <td><code>HELM_DIFF_THREE_WAY_MERGE=true</code> (or <code>--three-way-merge</code> in helm-diff)</td>
      </tr>
      <tr>
          <td>The diff is full of quoting, key order and indentation changes</td>
          <td>Different Helm or chart versions serialize differently</td>
          <td><code>HELM_DIFF_NORMALIZE_MANIFESTS=true</code></td>
      </tr>
      <tr>
          <td><code>key &quot;image&quot; already set in map</code> while rendering values</td>
          <td>The old YAML library rejects duplicate keys and some anchors in <code>values</code></td>
          <td><code>HELMFILE_GOCCY_GOYAML=true</code> on helmfile 0.x; helmfile 1.x already uses that library</td>
      </tr>
      <tr>
          <td><code>Body is too long (maximum is 65536 characters)</code> or the job takes forever on the first install</td>
          <td>A fresh install&rsquo;s diff is the whole chart, and the PR comment or the log cannot hold it</td>
          <td><code>--skip-diff-on-install</code></td>
      </tr>
      <tr>
          <td><code>Error: failed to authorize: … 403 Forbidden</code> pulling a chart from <code>oci://ghcr.io/…</code></td>
          <td>Helm older than 3.8 with experimental OCI support, or no <code>helm registry login</code></td>
          <td>Upgrade helm; <code>helm registry login ghcr.io</code> if the registry is private</td>
      </tr>
      <tr>
          <td><code>Error: validation: more than one dependency with name or alias</code></td>
          <td>helm and helmfile out of step (helm 3.15 needed helmfile ≥ 0.164; helm 3.13 went with 0.158)</td>
          <td>Upgrade helmfile alongside helm; on 1.x it no longer happens</td>
      </tr>
      <tr>
          <td>The diff has no color in CI</td>
          <td>helm-diff detects there is no TTY</td>
          <td><code>HELM_DIFF_COLOR=true</code></td>
      </tr>
  </tbody>
</table>
<h2 id="adopting-resources-that-already-exist">Adopting resources that already exist</h2>
<p>The typical case: someone installed ArgoCD (or whatever) with <code>kubectl apply -f</code>, and now you want a helmfile release to manage it. Helm refuses to install over resources that are not its own (<code>rendered manifests contain a resource that already exists … invalid ownership metadata</code>). Since Helm 3.2 it is enough to <strong>mark them as Helm&rsquo;s</strong> before installing:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">NS</span><span class="o">=</span>argocd<span class="p">;</span> <span class="nv">REL</span><span class="o">=</span>argocd
</span></span><span class="line"><span class="cl"><span class="k">for</span> kind in deploy sts svc cm secret sa ingress role rolebinding networkpolicy<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  kubectl -n <span class="nv">$NS</span> get <span class="nv">$kind</span> -o name 2&gt;/dev/null <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r r<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    kubectl -n <span class="nv">$NS</span> label <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> app.kubernetes.io/managed-by<span class="o">=</span>Helm --overwrite
</span></span><span class="line"><span class="cl">    kubectl -n <span class="nv">$NS</span> annotate <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> meta.helm.sh/release-name<span class="o">=</span><span class="nv">$REL</span> meta.helm.sh/release-namespace<span class="o">=</span><span class="nv">$NS</span> --overwrite
</span></span><span class="line"><span class="cl">  <span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl"><span class="c1"># cluster-scoped resources: same labels, no -n</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> r in <span class="k">$(</span>kubectl get crd,clusterrole,clusterrolebinding -o name <span class="p">|</span> grep -i argo<span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  kubectl label <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> app.kubernetes.io/managed-by<span class="o">=</span>Helm --overwrite
</span></span><span class="line"><span class="cl">  kubectl annotate <span class="s2">&#34;</span><span class="nv">$r</span><span class="s2">&#34;</span> meta.helm.sh/release-name<span class="o">=</span><span class="nv">$REL</span> meta.helm.sh/release-namespace<span class="o">=</span><span class="nv">$NS</span> --overwrite
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span><span class="nv">$REL</span> diff     <span class="c1"># should be nearly empty</span>
</span></span><span class="line"><span class="cl">helmfile -e prod -l <span class="nv">name</span><span class="o">=</span><span class="nv">$REL</span> apply
</span></span></code></pre></td></tr></table>
</div>
</div><p>Only mark what the chart is going to render: a labeled resource the chart does not include is harmless, but one the chart renders under another name ends up duplicated.</p>
<h2 id="helmfile-1x-and-helm-4">helmfile 1.x and Helm 4</h2>
<p>Coming from 0.x, this is what changes when you upgrade:</p>
<ul>
<li>A <code>helmfile.yaml</code> with Go templates must be called <strong><code>helmfile.yaml.gotmpl</code></strong>; a plain <code>.yaml</code> is no longer rendered.</li>
<li><code>environments:</code> and <code>releases:</code> go in <strong>separate parts split by <code>---</code></strong> within the same file.</li>
<li><code>helmfile delete</code> is gone (it is <code>destroy</code>), so is <code>helmfile charts</code>, and <code>--args</code> is removed.</li>
<li><code>HELMFILE_SKIP_INSECURE_TEMPLATE_FUNCTIONS</code> becomes <code>HELMFILE_DISABLE_INSECURE_FEATURES</code>.</li>
<li>helmfile 1.x works with <strong>Helm 3 and Helm 4</strong>. With Helm 4, install helm-diff with <code>helm plugin install https://github.com/databus23/helm-diff --verify=false</code> (Helm 4 verifies plugin provenance and helm-diff does not publish it).</li>
</ul>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>apply</code> with no prior diff in CI</strong>: <code>helmfile apply</code> already diffs and only applies when there are changes; <code>sync</code> always applies. In pipelines, <code>diff</code> on the PR and <code>apply</code> on main.</li>
<li><strong><code>installed: false</code></strong> uninstalls the release on the next <code>apply</code>; handy to retire things through Git, dangerous if copied from another environment.</li>
<li><strong>Secrets in <code>values</code></strong>: helmfile leaves them in the release manifest (Helm&rsquo;s Secret in the namespace); use <code>helm-secrets</code> with SOPS or external references.</li>
<li><strong><code>--skip-diff-on-install</code> is not <code>--skip-diff</code></strong>: it only skips the diff for releases that do not exist yet. Upgrades still show their diff.</li>
<li><strong>Three binaries, three versions</strong>: <code>helm version</code>, <code>helmfile version</code> and <code>helm plugin list</code> as the first pipeline step, pinned by tag in the CI image.</li>
</ul>
<p>References: <a href="https://github.com/databus23/helm-diff">helm-diff</a>, <a href="https://helmfile.readthedocs.io/">helmfile</a> and the <a href="https://github.com/helmfile/helmfile/blob/main/docs/proposals/towards-1.0.md">towards 1.0 proposal</a>.</p>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="/en/posts/k3s/import-images-without-registry/">k3s: import images without a registry</a> — when the release depends on an image that lives in no registry.</li>
</ul>
]]></content:encoded></item><item><title>Removing Rancher from a cluster: cattle-system stuck Terminating, clusters that will not delete, the webhook that blocks everything, and getting the admin back</title><link>https://inet.sh/en/posts/kubernetes/rancher-cleanup-cattle-system-namespaces-terminating/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/kubernetes/rancher-cleanup-cattle-system-namespaces-terminating/</guid><description>After uninstalling Rancher (or after it breaks), cattle-system and cattle-fleet-system stay in Terminating because of the controller.cattle.io/namespace-auth finalizer, Cluster objects refuse to delete, and the rancher.cattle.io webhooks reject every change because the pod serving them is gone. The official rancher-cleanup job to wipe everything at once, the manual surgery when you only want to fix one thing, and reset-password / ensure-default-admin when the problem is that you cannot log in.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p>To <strong>remove Rancher completely</strong> from a cluster where it was installed (irreversible, backup taken):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl create -f https://raw.githubusercontent.com/rancher/rancher-cleanup/main/deploy/rancher-cleanup.yaml
</span></span><span class="line"><span class="cl">kubectl -n kube-system logs -l job-name<span class="o">=</span>cleanup-job -f
</span></span><span class="line"><span class="cl">kubectl create -f https://raw.githubusercontent.com/rancher/rancher-cleanup/main/deploy/verify.yaml
</span></span><span class="line"><span class="cl">kubectl -n kube-system logs -l job-name<span class="o">=</span>verify-job -f      <span class="c1"># should print nothing</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>To <strong>fix a single thing</strong> without razing everything, read on.</p>
<h2 id="namespace-stuck-in-terminating">Namespace stuck in Terminating</h2>
<p><code>cattle-system</code>, <code>cattle-fleet-system</code>, <code>cattle-impersonation-system</code>, <code>local</code>, the <code>cluster-fleet-*</code> ones, or any namespace Rancher created, stay in <code>Terminating</code> forever:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get ns cattle-system -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.metadata.finalizers}{&#34;\n&#34;}{.spec.finalizers}{&#34;\n&#34;}{.status.conditions}{&#34;\n&#34;}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Two different cases:</p>
<ol>
<li>
<p><strong><code>metadata.finalizers</code> contains <code>controller.cattle.io/namespace-auth</code></strong> (or any <code>*.cattle.io</code>). Rancher put it there and only Rancher removes it; if Rancher is gone, nobody will. Remove it yourself:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl patch ns cattle-system --type<span class="o">=</span>merge -p <span class="s1">&#39;{&#34;metadata&#34;:{&#34;finalizers&#34;:[]}}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p><strong><code>spec.finalizers</code> contains <code>kubernetes</code> and <code>status.conditions</code> says <code>SomeResourcesRemain</code> or <code>DiscoveryFailed</code></strong>. The API server will not delete the namespace because resources remain inside, almost always CRs of an API group whose service disappeared (a Rancher <code>apiservice</code>). Before forcing, look at what it is:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl api-resources --verbs<span class="o">=</span>list --namespaced -o name <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> xargs -n1 kubectl -n cattle-system get --ignore-not-found --show-kind 2&gt;/dev/null
</span></span><span class="line"><span class="cl">kubectl get apiservice <span class="p">|</span> grep -v True        <span class="c1"># dead APIs that break discovery</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Delete the dead <code>apiservice</code> or the CRs with finalizers, and the namespace goes away on its own. Forcing <code>spec.finalizers</code> to empty is the last resort and leaves orphans behind:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get ns cattle-system -o json <span class="p">|</span> jq <span class="s1">&#39;.spec.finalizers=[]&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> kubectl replace --raw /api/v1/namespaces/cattle-system/finalize -f -
</span></span></code></pre></td></tr></table>
</div>
</div></li>
</ol>
<h2 id="cluster-that-cannot-be-deleted">Cluster that cannot be deleted</h2>
<p>An imported cluster that no longer exists stays in <em>Removing</em> in the UI, or <code>kubectl delete</code> never finishes. There are two objects with finalizers, one per API:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl -n fleet-default get clusters.provisioning.cattle.io
</span></span><span class="line"><span class="cl">kubectl -n fleet-default patch clusters.provisioning.cattle.io my-cluster --type<span class="o">=</span>merge -p <span class="s1">&#39;{&#34;metadata&#34;:{&#34;finalizers&#34;:[]}}&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">kubectl get clusters.management.cattle.io          <span class="c1"># c-m-xxxxxxxx (v2.6+) or c-xxxxx</span>
</span></span><span class="line"><span class="cl">kubectl patch clusters.management.cattle.io c-m-xxxxxxxx --type<span class="o">=</span>merge -p <span class="s1">&#39;{&#34;metadata&#34;:{&#34;finalizers&#34;:[]}}&#39;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then clean up the <code>cluster-fleet-default-&lt;name&gt;-&lt;hash&gt;</code> namespace and whatever <code>clusters.fleet.cattle.io</code> remain in <code>fleet-default</code>.</p>
<h2 id="the-webhook-that-rejects-everything">The webhook that rejects everything</h2>
<p><code>rancher-webhook</code> validates and mutates <code>*.cattle.io</code> resources (and some core ones, such as namespaces and ServiceAccounts on managed clusters). If the pod is gone or broken, every change fails with:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error from server (InternalError): Internal error occurred: failed calling webhook &#34;rancher.cattle.io&#34;: … connect: connection refused
</span></span></code></pre></td></tr></table>
</div>
</div><p>Export the configurations, delete them, let the pod come back healthy, and restore them:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl get validatingwebhookconfiguration rancher.cattle.io -o yaml &gt; vwc-rancher.yaml
</span></span><span class="line"><span class="cl">kubectl get mutatingwebhookconfiguration rancher.cattle.io -o yaml &gt; mwc-rancher.yaml
</span></span><span class="line"><span class="cl">kubectl delete validatingwebhookconfiguration rancher.cattle.io
</span></span><span class="line"><span class="cl">kubectl delete mutatingwebhookconfiguration rancher.cattle.io
</span></span><span class="line"><span class="cl"><span class="c1"># … fix rancher-webhook (kubectl -n cattle-system rollout restart deploy/rancher-webhook) …</span>
</span></span><span class="line"><span class="cl">kubectl apply -f vwc-rancher.yaml -f mwc-rancher.yaml
</span></span></code></pre></td></tr></table>
</div>
</div><p>If you are uninstalling, do not restore them: they are the first thing to remove so the rest of the deletion can proceed.</p>
<h2 id="getting-access-back">Getting access back</h2>
<p>Lost admin password, or the admin user disabled by mistake:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl -n cattle-system <span class="nb">exec</span> -it deploy/rancher -- reset-password
</span></span><span class="line"><span class="cl"><span class="c1"># New password for default administrator (user-xxxxx): …</span>
</span></span><span class="line"><span class="cl">kubectl -n cattle-system <span class="nb">exec</span> -it deploy/rancher -- ensure-default-admin
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>ensure-default-admin</code> re-enables the user and gives the administrator role back; <code>reset-password</code> only changes the password.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>rancher-cleanup</code> deletes the downstream cluster objects</strong> (the clusters keep running, but are no longer registered) and everything Rancher installed: monitoring, logging, Gatekeeper. Longhorn stays. Do not run it on the Rancher cluster you want to keep.</li>
<li><strong>Backup first</strong>: <code>kubectl get -A -o yaml</code> of the <code>*.cattle.io</code> resources you care about, or Rancher&rsquo;s backup-operator. Finalizers go away in a second; what was behind them does not come back.</li>
<li><strong><code>kubectl delete ns --force --grace-period=0</code> does nothing</strong> for a namespace in Terminating: the block is in the finalizers, not in the grace period.</li>
<li><strong>Rancher puts the finalizers back</strong> while it is alive: if you are cleaning a cluster still connected to a Rancher, disconnect it first (delete the <code>cattle-cluster-agent</code>) or your patches undo themselves.</li>
<li><strong>The <code>*.cattle.io</code> CRDs survive</strong> a Helm uninstall; <code>rancher-cleanup</code> removes them, a manual uninstall does not.</li>
</ul>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="/en/posts/k3s/cluster-reset-lost-quorum/">k3s: cluster reset after losing quorum</a> — the other cluster surgery worth keeping at hand.</li>
<li><a href="https://github.com/rancher/rancher-cleanup">rancher-cleanup</a> and the <a href="https://ranchermanager.docs.rancher.com/faq/technical-items">Rancher technical FAQ</a>.</li>
</ul>
]]></content:encoded></item><item><title>Vault Agent Injector on a private GKE cluster: no sidecar and pods take 30 s (the control plane cannot reach the webhook on 8080)</title><link>https://inet.sh/en/posts/kubernetes/vault-agent-injector-gke-private-cluster-no-sidecar/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/kubernetes/vault-agent-injector-gke-private-cluster-no-sidecar/</guid><description>On a private GKE cluster, pods annotated with vault.hashicorp.com/agent-inject start without the vault-agent-init and vault-agent containers, take half a minute longer to be created, and the injector logs no request at all. The control plane can only open connections to nodes on ports 443 and 10250; the injector listens on 8080 and the webhook fails silently because failurePolicy is Ignore. Firewall rule from the control plane range, how to validate in the injector logs, and the same problem with any other admission webhook.</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><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">CLUSTER</span><span class="o">=</span>my-cluster<span class="p">;</span> <span class="nv">REGION</span><span class="o">=</span>us-central1<span class="p">;</span> <span class="nv">NETWORK</span><span class="o">=</span>my-vpc
</span></span><span class="line"><span class="cl"><span class="nv">MASTER_CIDR</span><span class="o">=</span><span class="k">$(</span>gcloud container clusters describe <span class="nv">$CLUSTER</span> --region <span class="nv">$REGION</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --format<span class="o">=</span><span class="s1">&#39;value(privateClusterConfig.masterIpv4CidrBlock)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">NODE_TAG</span><span class="o">=</span><span class="k">$(</span>gcloud compute firewall-rules list --filter<span class="o">=</span><span class="s2">&#34;name~^gke-</span><span class="nv">$CLUSTER</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --format<span class="o">=</span><span class="s1">&#39;value(targetTags.list())&#39;</span> <span class="p">|</span> head -1<span class="k">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud compute firewall-rules create gke-<span class="nv">$CLUSTER</span>-master-to-vault-injector <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --network<span class="o">=</span><span class="nv">$NETWORK</span> --direction<span class="o">=</span>INGRESS --action<span class="o">=</span>ALLOW <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --source-ranges<span class="o">=</span><span class="nv">$MASTER_CIDR</span> --target-tags<span class="o">=</span><span class="nv">$NODE_TAG</span> --rules<span class="o">=</span>tcp:8080
</span></span></code></pre></td></tr></table>
</div>
</div><p>Delete an annotated pod and check that the injector gets the call:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">kubectl -n app delete pod -l <span class="nv">app</span><span class="o">=</span>api
</span></span><span class="line"><span class="cl">kubectl -n vault logs deploy/vault-agent-injector <span class="p">|</span> grep mutate
</span></span><span class="line"><span class="cl"><span class="c1"># [INFO]  handler: Request received: Method=POST URL=/mutate?timeout=30s</span>
</span></span><span class="line"><span class="cl">kubectl -n app get pod -l <span class="nv">app</span><span class="o">=</span>api -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.items[0].spec.initContainers[*].name} {.items[0].spec.containers[*].name}{&#34;\n&#34;}&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># vault-agent-init api vault-agent</span>
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="the-symptom">The symptom</h2>
<ul>
<li>The pod carries <code>vault.hashicorp.com/agent-inject: &quot;true&quot;</code> and the rest of the annotations, but starts <strong>with the application container only</strong>: no <code>vault-agent-init</code>, no <code>vault-agent</code>.</li>
<li>Creating the pod <strong>takes about 30 seconds longer</strong> than usual.</li>
<li><code>kubectl get events</code> says nothing about the webhook. The injector logs do not have a single <code>Request received</code> line.</li>
<li>On other clusters (public zonal, kind, k3s) the same chart and the same annotations work.</li>
</ul>
<h2 id="why">Why</h2>
<p>On a <strong>private</strong> GKE cluster the control plane lives in a Google-owned VPC and reaches your nodes through the firewall rule GKE creates by itself (<code>gke-&lt;cluster&gt;-&lt;hash&gt;-master</code>). That rule only allows <strong>TCP 443 and 10250</strong> from the control plane range to the nodes. Everything else is closed.</p>
<p>An admission webhook does not go through the Service like a normal client would: the API server resolves the Service to its endpoints and <strong>connects straight to the pod IP on the <code>targetPort</code></strong>. The injector&rsquo;s Service (<code>vault-agent-injector-svc</code>) exposes 443, but the pod listens on <strong>8080</strong> (<code>injector.port</code> in the chart), so the actual connection goes to 8080 and the firewall drops it.</p>
<p>What happens next is explained by two chart defaults:</p>
<table>
  <thead>
      <tr>
          <th>Value</th>
          <th>Default</th>
          <th>Effect</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>injector.webhook.timeoutSeconds</code></td>
          <td><code>30</code></td>
          <td>The API server waits 30 s for the webhook&rsquo;s answer</td>
      </tr>
      <tr>
          <td><code>injector.webhook.failurePolicy</code></td>
          <td><code>Ignore</code></td>
          <td>If the webhook does not answer, the pod is created <strong>unmutated</strong>, with no error</td>
      </tr>
  </tbody>
</table>
<p>Hence the 30-second delay and the pod with no sidecar and no message. With <code>failurePolicy: Fail</code> you would have seen <code>failed calling webhook &quot;vault.hashicorp.com&quot;: … context deadline exceeded</code> in the ReplicaSet events, which at least points at the right place.</p>
<h2 id="not-just-vault">Not just Vault</h2>
<p>Any admission webhook, CRD conversion webhook or aggregated API listening on a port other than 443 or 10250 fails the same way on private GKE: the OpenTelemetry operator (9443), Kyverno (9443), Gatekeeper (8443)… cert-manager listens on <strong>10250</strong> precisely to avoid needing this rule. The GKE documentation lists it as a requirement for &ldquo;admission webhooks, aggregated API servers, webhook conversion&rdquo;: one rule per port, from <code>masterIpv4CidrBlock</code> to the node tags.</p>
<p>If the cluster lives in Terraform, keep the rule next to the cluster so it survives a rebuild:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="cl"><span class="k">resource</span> <span class="s2">&#34;google_compute_firewall&#34; &#34;master_to_vault_injector&#34;</span> {
</span></span><span class="line"><span class="cl"><span class="n">  name</span>          <span class="o">=</span> <span class="s2">&#34;gke-${google_container_cluster.main.name}-master-to-vault-injector&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  network</span>       <span class="o">=</span> <span class="k">google_compute_network</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">name</span>
</span></span><span class="line"><span class="cl"><span class="n">  direction</span>     <span class="o">=</span> <span class="s2">&#34;INGRESS&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">  source_ranges</span> <span class="o">=</span> <span class="p">[</span><span class="k">google_container_cluster</span><span class="p">.</span><span class="k">main</span><span class="p">.</span><span class="k">private_cluster_config</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="k">master_ipv4_cidr_block</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">  target_tags</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;gke-${google_container_cluster.main.name}&#34;</span><span class="p">]</span><span class="c1">   # the tag your node pools use
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">allow</span> {
</span></span><span class="line"><span class="cl"><span class="n">    protocol</span> <span class="o">=</span> <span class="s2">&#34;tcp&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">    ports</span>    <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;8080&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="traps">Traps</h2>
<ul>
<li><strong>The wrong tag</strong>: nodes carry <code>gke-&lt;cluster&gt;-&lt;hash&gt;-node</code>; copy it from the existing <code>gke-&lt;cluster&gt;-*</code> rules instead of typing it. With several node pools, check that all of them have the tag.</li>
<li><strong>The control plane range changes if you recreate the cluster</strong>; a hand-written rule with the old CIDR silently stops working.</li>
<li><strong><code>failurePolicy: Fail</code></strong> makes the failure visible, but while the webhook is down <strong>no pod gets created</strong> in the namespaces matching the <code>namespaceSelector</code>; the chart excludes the injector itself, but not Vault if they share namespace and labels. Use it with <code>replicas: 2</code>.</li>
<li><strong>The injector does receive the request and there is still no sidecar</strong>: then it is no longer the network. Read the <code>vault-agent-init</code> logs: <code>permission denied</code> is the Kubernetes auth role (<code>bound_service_account_names</code>, <code>bound_service_account_namespaces</code>) or the policy; <code>connection refused</code> is <code>vault.hashicorp.com/service</code> or the Vault address.</li>
<li><strong>One port per rule does not scale</strong>: as webhooks pile up, one rule with <code>--rules=tcp:8080,8443,9443</code> from the control plane is easier to maintain than five.</li>
</ul>
<p>References: <a href="https://github.com/hashicorp/vault-k8s/issues/46">vault-k8s #46</a>, <a href="https://github.com/hashicorp/vault-k8s/issues/32">vault-k8s #32</a> and <a href="https://docs.cloud.google.com/kubernetes-engine/docs/how-to/private-clusters">firewall rules on private clusters</a> in the GKE documentation.</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> — another component that &ldquo;does nothing&rdquo; until you look in the right place.</li>
<li><a href="/en/posts/gcp/github-actions-workload-identity-federation/">GitHub Actions → Google Cloud without keys</a> — to deploy to that GKE without JSON keys.</li>
</ul>
]]></content:encoded></item><item><title>cert-manager: the HTTP-01 challenge returns 308 (missing ingressClassName in the solver) and other self-check failures</title><link>https://inet.sh/en/posts/kubernetes/cert-manager-http01-308-ingressclassname/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/kubernetes/cert-manager-http01-308-ingressclassname/</guid><description>The cert-manager Challenge gets stuck on &amp;#39;wrong status code 308, expected 200&amp;#39;: your ingress controller does not serve the solver&amp;#39;s temporary Ingress and the request lands on the app&amp;#39;s Ingress with ssl-redirect. Fix with ingressClassName in the http01 solver (cert-manager 1.12+), how to diagnose with kubectl describe challenge, the other causes (404, hairpin NAT, TLS), and DNS-01 with Cloudflare as the alternative.</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></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 creates a temporary Ingress (<code>cm-acme-http-solver-…</code>) to answer the challenge, but without a class your ingress controller does not adopt it; the request to <code>/.well-known/acme-challenge/…</code> gets answered by the application&rsquo;s Ingress, which redirects to HTTPS with <strong>308</strong>. Put the class in the 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; before: 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>Apply and delete the stuck challenge so it retries:</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="diagnosis">Diagnosis</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;name&gt;              <span class="c1"># Status → Reason is the line you want</span>
</span></span><span class="line"><span class="cl">kubectl get ingress -n &lt;ns&gt;                            <span class="c1"># does cm-acme-http-solver-xxxx show CLASS &lt;none&gt;?</span>
</span></span><span class="line"><span class="cl">curl -i http://app.example.com/.well-known/acme-challenge/test   <span class="c1"># from outside and from a pod</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Before asking Let&rsquo;s Encrypt to validate, cert-manager runs a <strong>self-check</strong>: it tries the challenge URL itself. The Challenge&rsquo;s <code>Reason</code> is the result of that self-check and almost always points at the cause.</p>
<table>
  <thead>
      <tr>
          <th><code>Reason</code></th>
          <th>Cause</th>
          <th>Fix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>wrong status code '308'</code> (or 301/302)</td>
          <td>The solver Ingress has no class; the app&rsquo;s one answers with <code>ssl-redirect</code></td>
          <td><code>ingressClassName</code> in the solver</td>
      </tr>
      <tr>
          <td><code>wrong status code '404'</code></td>
          <td>The solver is not running, or the host&rsquo;s DNS points at another 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>The public IP is unreachable from inside the cluster: NAT without hairpin, different DNS inside and outside</td>
          <td>NAT reflection or split DNS on the firewall (<a href="/en/posts/pfsense/nat-reflection-port-forward-from-lan/">pfSense</a>)</td>
      </tr>
      <tr>
          <td><code>tls: handshake failure</code></td>
          <td>The app&rsquo;s Ingress already has TLS with an empty secret</td>
          <td>Annotation <code>cert-manager.io/issue-temporary-certificate: &quot;true&quot;</code></td>
      </tr>
      <tr>
          <td><code>wrong status code '503'</code></td>
          <td>Several Ingresses with the same host and the controller does not merge them</td>
          <td><code>acme.cert-manager.io/http01-edit-in-place: &quot;true&quot;</code> on the Ingress</td>
      </tr>
  </tbody>
</table>
<h2 id="why-ingressclassname">Why <code>ingressClassName</code></h2>
<p>Since Kubernetes 1.18 an Ingress&rsquo;s class goes in <code>spec.ingressClassName</code>; the <code>kubernetes.io/ingress.class</code> annotation is deprecated and modern controllers (ingress-nginx 1.x, Traefik) may ignore it. In cert-manager, the solver&rsquo;s <code>class</code> field still writes the annotation; <code>ingressClassName</code> (since 1.12) writes the field. Use one of the two, and in 2026 the right one is <code>ingressClassName</code>.</p>
<p>Without a class, the temporary Ingress exists but nobody serves it. The controller routes the request by host, finds the app&rsquo;s Ingress, and applies its rules: <code>ssl-redirect</code> → 308. That is why the error looks like the app&rsquo;s and not cert-manager&rsquo;s.</p>
<h2 id="alternative-dns-01-with-cloudflare">Alternative: DNS-01 with Cloudflare</h2>
<p>When the cluster is not reachable from the Internet on port 80, or you want a wildcard, the challenge goes through DNS. Token in Cloudflare with <strong>Zone → DNS → Edit</strong> and <strong>Zone → Zone → Read</strong> on the zone:</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>With DNS-01 there is no temporary Ingress, no HTTP self-check, no hairpin; the self-check queries the TXT record, and if the cluster&rsquo;s DNS resolves against an internal server with the zone cached, the <code>Reason</code> will be <code>DNS record for … not yet propagated</code>: patience, or <code>--dns01-recursive-nameservers=1.1.1.1:53</code> on cert-manager.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>Staging first.</strong> <code>https://acme-staging-v02.api.letsencrypt.org/directory</code> has no issuance limits; production blocks 5 issuances per name per week, and every failed Challenge that reaches Let&rsquo;s Encrypt counts as an attempt.</li>
<li><strong>Delete the Challenge, not the Certificate.</strong> Deleting the <code>Order</code> or the <code>Challenge</code> forces a retry with the new configuration; deleting the <code>Certificate</code> also deletes the secret and leaves the app without TLS meanwhile.</li>
<li><strong><code>pathType: Exact</code></strong> on the solver Ingress (recent cert-manager) clashes with some old controllers; if you see 503 on OpenShift, that is it.</li>
<li><strong>Issuer per namespace vs ClusterIssuer</strong>: the Ingress must annotate the one that exists (<code>cert-manager.io/cluster-issuer</code> or <code>cert-manager.io/issuer</code>); annotating the one that does not leaves the Certificate in <code>Issuing</code> forever with no Challenge to describe.</li>
</ul>
<p>References: <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>, and <a href="https://cert-manager.io/docs/troubleshooting/acme/">Troubleshooting ACME</a> in the cert-manager documentation.</p>
]]></content:encoded></item></channel></rss>