<?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>TLS on Rémi Tech Notes</title><link>https://www.vrchr.fr/tags/tls/</link><description>Recent content in TLS on Rémi Tech Notes</description><generator>Hugo</generator><language>fr-fr</language><lastBuildDate>Mon, 04 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.vrchr.fr/tags/tls/index.xml" rel="self" type="application/rss+xml"/><item><title>Gateway API, Backend TLS auto-signé &amp; trust-manager</title><link>https://www.vrchr.fr/posts/2026/05/04/gateway-api-backendtls-trust-manager/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/05/04/gateway-api-backendtls-trust-manager/</guid><description>Comment gérer un backend HTTPS auto-signé avec Gateway API en gérant proprement la vérification du certificat avec trust-manager</description><content:encoded><![CDATA[<p>Dans le cadre de la migration de ressources Ingress vers <a href="https://gateway-api.sigs.k8s.io/">Gateway API</a>, j'ai dû gérer le cas d'un backend qui expose son service en <strong>HTTPS</strong> avec un certificat <strong>auto-signé</strong>. Avec <code>ingress-nginx</code> ça passait à coups d'annotations, sans trop se poser de questions. En Gateway API, c'est plus restrictif : il faut une vraie CA. Voici comment je m'en suis sorti proprement avec <strong>trust-manager</strong>.</p>
<p>Pour l'exemple, on va imaginer une app <code>mysecureapp</code> dans le namespace <code>mysecurens</code>, qui expose son service <code>mysecureapp</code> sur le port <code>8443</code> en HTTPS, avec un certificat auto-signé.</p>
<h2 id="avant--ingress-nginx--annotations">Avant : ingress-nginx &amp; annotations</h2>
<p>Côté <code>ingress-nginx</code>, c'était assez direct. Une ressource <code>Ingress</code> avec une seule annotation, et go :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Ingress</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">nginx.ingress.kubernetes.io/backend-protocol</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;HTTPS&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="p">[</span><span class="l">...]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">ingressClassName</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span>- <span class="nt">host</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">http</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">        </span><span class="nt">paths</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">          </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">            </span><span class="nt">pathType</span><span class="p">:</span><span class="w"> </span><span class="l">Prefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">            </span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">              </span><span class="nt">service</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">                </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">                </span><span class="nt">port</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">                  </span><span class="nt">number</span><span class="p">:</span><span class="w"> </span><span class="m">8443</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">  </span><span class="nt">tls</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">    </span>- <span class="nt">hosts</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">        </span>- <span class="l">mysecureapp.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">      </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp.gravitek.io-tls</span><span class="w"> </span><span class="c"># Certificat public Let&#39;s Encrypt</span><span class="w">
</span></span></span></code></pre></div><p>L'annotation <code>backend-protocol: &quot;HTTPS&quot;</code> est claire, elle indique à <code>nginx</code> de parler en TLS au backend. Et c'est tout. Par défaut, <code>ingress-nginx</code> <strong>ne vérifie pas</strong> le certificat du backend 🙈. Tant qu'on ne lui demande pas explicitement (<code>proxy-ssl-verify: &quot;on&quot;</code> + <code>proxy-ssl-secret</code>), il chiffre mais ne valide rien.</p>
<h2 id="après--gateway-api--backendtlspolicy">Après : Gateway API &amp; BackendTLSPolicy</h2>
<p>En Gateway API (j'utilise <a href="https://gateway.envoyproxy.io/">Envoy Gateway</a>, mais le principe est le même côté API), la route HTTP devient une <code>HTTPRoute</code>, et le TLS vers le backend se déclare avec une <a href="https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/"><code>BackendTLSPolicy</code></a> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPRoute</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">hostnames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp.gravitek.io</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">parentRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</span><span class="w"> </span><span class="c"># Nom de ma gateway</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w"> </span><span class="c"># Pour l&#39;exemple on a la GW dans le même NS que l&#39;appli</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-https</span><span class="w"> </span><span class="c"># Section de Gateway qui écoute en HTTPS</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">  </span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span>- <span class="nt">backendRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">        </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">          </span><span class="nt">port</span><span class="p">:</span><span class="w"> </span><span class="m">8443</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">matches</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">        </span>- <span class="nt">path</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">            </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">PathPrefix</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">            </span><span class="nt">value</span><span class="p">:</span><span class="w"> </span><span class="l">/</span><span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># BackendTLSPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io/v1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">BackendTLSPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-backend-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">targetRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Service</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">validation</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp.mysecurens.svc.cluster.local</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">caCertificateRefs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">ConfigMap</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca</span><span class="w"> </span><span class="c"># on va voir cela après ;)</span><span class="w">
</span></span></span></code></pre></div><p>Côté flux, ça donne :</p>
<p><img alt="Workflow Gateway API : du client au pod" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-04-gateway-api-flow.svg"></p>
<p>C'est propre, c'est déclaratif, c'est versionné dans un objet bien identifié... mais 💥, contrairement à <code>nginx</code>, <strong>il n'y a pas de mode <code>verify: off</code></strong>. La spec impose soit une <code>caCertificateRefs</code>, soit un <code>wellKnownCACertificates: System</code>. Impossible de faire du <code>SkipVerify</code> (et c'est tant mieux).</p>
<p>Sauf que : mon backend a un certificat <strong>auto-signé</strong>, donc pas de CA &quot;publique&quot; à pointer. Et il faut bien matérialiser cette CA dans un <code>ConfigMap</code> du namespace <code>mysecurens</code> pour que la <code>BackendTLSPolicy</code> puisse la référencer. Comment faire ça proprement ? 🤔</p>
<h3 id="backendtlspolicy--deux-modes-de-validation">BackendTLSPolicy : deux modes de validation</h3>
<p>Avant d'aller plus loin, un mot sur les <a href="https://gateway-api.sigs.k8s.io/guides/tls/#upstream-tls">options possibles côté Gateway API</a> pour parler en TLS au backend. La spec <code>BackendTLSPolicy</code> définit deux modes de validation, mutuellement exclusifs :</p>
<ul>
<li><strong><code>wellKnownCACertificates: System</code></strong> : le gateway utilise le bundle de CA du système (Let's Encrypt, DigiCert et autres autorités publiques). Si mon backend expose un certificat signé par une CA publique — typiquement un service managé ou un endpoint SaaS — c'est l'option la plus simple : rien à gérer, rien à distribuer.</li>
<li><strong><code>caCertificateRefs</code></strong> : le gateway charge la (ou les) CA depuis un <code>ConfigMap</code> que je fournis dans le namespace de la <code>BackendTLSPolicy</code>. C'est le mode obligatoire dès qu'on a une <strong>CA privée</strong> : certificat auto-signé, PKI maison, etc.</li>
</ul>
<p>C'est ce <strong>deuxième cas</strong> qui motive cet article : un service interne avec certificat auto-signé, qu'on veut consommer en TLS validé. Si le backend utilise un certificat public, <code>wellKnownCACertificates: System</code> et c'est plié — pas besoin de trust-manager.</p>
<h3 id="cacertificaterefs--secret-ou-configmap-">caCertificateRefs : Secret ou ConfigMap ?</h3>
<p>Avec Envoy Gateway, on pourrait très bien utiliser un <code>Secret</code> comme <code>caCertificateRefs</code>, et donc éviter en partie la suite de cet article (merci @Shinro pour la remarque, cf issue <a href="https://github.com/envoyproxy/gateway/issues/2777">#2777</a>). Mais c'est un support <strong>Implementation-Specific</strong> au sens de la <a href="https://gateway-api.sigs.k8s.io/reference/spec/">spec Gateway API</a> — Envoy Gateway le permet, d'autres implémentations non.</p>
<p>Cependant, ça ne résoudrait pas un problème de rotation de <code>ca.crt</code>, par exemple. Continuons alors la lecture ! 😉</p>
<h2 id="trois-tentatives-pour-définir-la-ca-dans-un-configmap">Trois tentatives pour définir la CA dans un ConfigMap</h2>
<p>Parmi mes expérimentations, j'ai noté 3 possibilités permettant d'avoir la CA du certificat auto-signé de mon application.</p>
<h3 id="1-la-recopie-manuelle-du-cacrt">1. La recopie manuelle du <code>ca.crt</code></h3>
<p>La méthode &quot;quick &amp; dirty&quot; : récupérer le <code>ca.crt</code> du <code>Secret</code> source, et le re-créer en <code>ConfigMap</code> à la main :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl -n mysecurens get secret mysecureapp-internal-tls -o <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">&#39;{.data.ca\.crt}&#39;</span> <span class="p">|</span> base64 -d &gt; ca.crt
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ kubectl -n mysecurens create configmap mysecureapp-ca --from-file<span class="o">=</span>ca.crt
</span></span></code></pre></div><p>Ça marche... jusqu'à la <strong>première rotation de la CA</strong> : il faut repasser à la main, régénérer, recommiter, rolling-restart. Et si on oublie, c'est <code>BackendTLSPolicy</code> rouge et erreur 503 à la clé. ❌</p>
<p>Cette solution simple ne faisait que décaler le problème pour mon moi futur 😬.</p>
<h3 id="2-synchro-automatique-avec-kyverno">2. Synchro automatique avec Kyverno</h3>
<p>Pour automatiser, j'ai pensé à <a href="https://kyverno.io/">Kyverno</a> avec une <code>GeneratingPolicy</code> qui prend le <code>Secret</code> source et génère le <code>ConfigMap</code> cible :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">policies.kyverno.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">GeneratingPolicy</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">sync-ca-to-configmap</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">matchConstraints</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">resourceRules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span>- <span class="nt">apiGroups</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">apiVersions</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;v1&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">resources</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;secrets&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="nt">operations</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CREATE&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;UPDATE&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">        </span><span class="nt">resourceNames</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;mysecureapp-internal-tls&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">generate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span>- <span class="nt">expression</span><span class="p">:</span><span class="w"> </span><span class="p">|</span><span class="sd">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="sd">        [...]</span><span class="w">
</span></span></span></code></pre></div><p>Sur le papier, ça paraît simple : le mode synchronisé était censé garder le <code>ConfigMap</code> aligné sur le <code>Secret</code> à chaque update. En vrai, <strong>chaque <code>UPDATE</code></strong> du secret source <strong>supprimait le <code>ConfigMap</code></strong> au lieu de le mettre à jour, avant de le recréer. Concrètement : une <code>BackendTLSPolicy</code> qui flappe à chaque renouvellement, donc pire qu'avant. ❌</p>
<p>Étant sur une version de kyverno assez &quot;vieille&quot; (1.15) avec une CR <code>v1alpha1</code>, je suspecte un problème de stabilité / maturité, sur les dernières versions en <code>v1</code> j'espère que ça devrait aller mieux.</p>
<p>Et donc comme pour le point précédent, ça pourrait le faire pour mon moi du futur qui fera la maj kyverno 😅.</p>
<h3 id="3-trust-manager--le-bon-outil-qui-juste-marche">3. trust-manager : le bon outil qui juste marche</h3>
<p>Et puis je suis tombé sur <a href="https://cert-manager.io/docs/trust/trust-manager/"><code>trust-manager</code></a>, l'outil <strong>officiel</strong> de l'écosystème <strong>cert-manager</strong>, dont le seul boulot est exactement ce qu'on cherche : prendre une CA en source et la <strong>distribuer</strong> sous forme de <strong><code>ConfigMap</code></strong> dans les namespaces ciblés, avec sync automatique en cas de rotation.</p>
<p>C'est cette troisième approche qu'on va retenir pour la suite.</p>
<h2 id="la-solution--cert-manager--trust-manager">La solution : cert-manager + trust-manager</h2>
<p>L'idée : émettre tous les certificats internes du cluster avec une <strong>CA interne</strong> gérée par <code>cert-manager</code>, et <strong>distribuer le <code>ca.crt</code></strong> dans les namespaces qui en ont besoin avec <code>trust-manager</code>.</p>
<p>Plutôt qu'une CA par appli (chacune dans son ns, à répliquer cross-namespace), on part sur <strong>une CA unique</strong> au niveau du cluster, qui signe tous les certificats feuilles internes. C'est un pattern PKI privée classique : une seule racine de confiance pour tout le trafic interne du cluster.</p>
<p><img alt="Architecture PKI interne avec cert-manager et trust-manager" class="zoomable" decoding="async" loading="lazy" src="/2026/05/2026-05-04-pki-trust-manager.svg"></p>
<h3 id="1-une-ca-interne-avec-cert-manager">1. Une CA interne avec cert-manager</h3>
<p>On bootstrap avec un <code>ClusterIssuer</code> self-signed, qui ne sert qu'à générer la CA racine, puis on déclare un <code>ClusterIssuer</code> qui signera tous les certificats feuilles :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># ClusterIssuer self-signed</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><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="ln">3</span><span class="cl"><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="ln">4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">selfsigned</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">selfSigned</span><span class="p">:</span><span class="w"> </span>{}<span class="w">
</span></span></span></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># CA Interne</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><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="ln"> 3</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Certificate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><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="ln"> 7</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">isCA</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="c"># On définit comme CA interne / privée</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">commonName</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">  </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span><span class="l">87600h0m0s</span><span class="w"> </span><span class="c"># 10 ans</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">renewBefore</span><span class="p">:</span><span class="w"> </span><span class="l">2160h0m0s</span><span class="w"> </span><span class="c"># 90 jours</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">privateKey</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">algorithm</span><span class="p">:</span><span class="w"> </span><span class="l">ECDSA</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">256</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">rotationPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">  </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">selfsigned</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><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></code></pre></div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c"># ClusterIssuer utilisant la CA interne</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><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="ln">3</span><span class="cl"><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="ln">4</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-issuer</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="nt">ca</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">    </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-tls</span><span class="w">
</span></span></span></code></pre></div><p>À partir de là, <code>internal-ca-issuer</code> peut signer le certificat feuille de <code>mysecureapp</code> (et de tous les autres services internes du cluster). Côté <code>mysecureapp</code>, on remplace donc le certificat auto-signé par un certificat signé par cette CA interne :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><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="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Certificate</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-internal-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">mysecurens</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="nt">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">mysecureapp-internal-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">duration</span><span class="p">:</span><span class="w"> </span><span class="l">2160h0m0s</span><span class="w"> </span><span class="c"># 90 jours</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">renewBefore</span><span class="p">:</span><span class="w"> </span><span class="l">360h0m0s</span><span class="w"> </span><span class="c"># 15 jours</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">privateKey</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">algorithm</span><span class="p">:</span><span class="w"> </span><span class="l">ECDSA</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">size</span><span class="p">:</span><span class="w"> </span><span class="m">256</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">rotationPolicy</span><span class="p">:</span><span class="w"> </span><span class="l">Always</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">  </span><span class="nt">dnsNames</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="c"># Nom DNS interne pour joindre le service</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp.mysecurens.svc.cluster.local</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp.mysecurens.svc</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span>- <span class="l">mysecureapp</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="nt">issuerRef</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-issuer</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><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></code></pre></div><p>⚠️ Point d'attention : le <code>dnsNames</code> doit contenir le <code>hostname</code> validé par la <code>BackendTLSPolicy</code> (ici <code>mysecureapp.mysecurens.svc.cluster.local</code>). Sinon Envoy rejette le certificat avec une erreur de SAN, même si la chaîne de confiance est parfaitement valide.</p>
<h3 id="2-distribuer-le-cacrt-avec-trust-manager">2. Distribuer le <code>ca.crt</code> avec trust-manager</h3>
<p><code>trust-manager</code> s'installe via Helm, dans le même namespace que <code>cert-manager</code> par défaut :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ helm upgrade --install trust-manager jetstack/trust-manager <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">    --namespace cert-manager <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl">    --wait
</span></span></code></pre></div><p>⚠️ Remarque : trust-manager lit ses sources dans <strong>un seul namespace</strong>, défini par <code>--app.trust.namespace</code> (par défaut <code>cert-manager</code>, donc pas besoin de le spécifier). C'est pour ça qu'on a émis <code>internal-ca</code> directement dans <code>cert-manager</code> plutôt que de chercher à répliquer le secret depuis ailleurs.</p>
<p>Une fois trust-manager en place, on déclare un <code>Bundle</code> : il prend une source (notre secret <code>internal-ca-tls</code>) et <strong>synchronise</strong> un <code>ConfigMap</code> <strong>cible</strong> dans tous les namespaces qui matchent un selector :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">trust.cert-manager.io/v1alpha1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Bundle</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="nt">sources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">secret</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">internal-ca-tls</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">ca.crt</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">target</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">configMap</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">key</span><span class="p">:</span><span class="w"> </span><span class="l">ca.crt</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">namespaceSelector</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span><span class="nt">matchLabels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">        </span><span class="nt">trust-ca</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Pour qu'un namespace récupère la CA, il suffit de le tagger :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl label namespace mysecurens trust-ca<span class="o">=</span><span class="nb">true</span>
</span></span></code></pre></div><p>Et c'est tout. Le <code>ConfigMap internal-ca</code> (clé <code>ca.crt</code>) apparaît automatiquement dans <code>mysecurens</code> :</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="ln">1</span><span class="cl">$ kubectl -n mysecurens get cm internal-ca
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME          DATA   AGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">internal-ca   <span class="m">1</span>      42m
</span></span></code></pre></div><p>Ce <code>ConfigMap</code> est ensuite référencé par la <code>BackendTLSPolicy</code> (cf. plus haut). Le jour où la CA tourne, <code>trust-manager</code> propage le nouveau <code>ca.crt</code> dans tous les namespaces consommateurs, sans rien à toucher.</p>
<h3 id="3-bonus--un-configmap-qui-ne-bouge-pas-et-cest-tant-mieux-">3. Bonus : un <code>ConfigMap</code> qui ne bouge pas, et c'est tant mieux 🎉</h3>
<p>Un effet de bord plutôt sympa de cette archi : tant que la <strong>CA</strong> ne tourne pas (soit dans 10 ans 😅), le <code>ConfigMap internal-ca</code> est strictement immuable. Les certificats <strong>feuilles</strong> de chaque appli, eux, peuvent tourner toutes les 90 jours sans que la <code>BackendTLSPolicy</code> ni le <code>ConfigMap</code> ne bougent — la chaîne de confiance reste valide tant que la racine ne change pas.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Avec la migration Gateway API, et malgré la perte du <code>SkipVerify</code>, on y gagne au final sur plusieurs plans :</p>
<ul>
<li>🔒 Du TLS de bout en bout, avec validation de la chaîne, pas du <code>verify: off</code> qui traîne dans une annotation</li>
<li>🧹 Une CA centralisée, des certificats feuilles auto-renouvelés, et un <code>ca.crt</code> qui se diffuse tout seul</li>
<li>📦 Tout en déclaratif Kubernetes, versionné, sans vieilles annotations 🙈 (on prend goût à ne plus gérer d'annotations tordues)</li>
</ul>
<p>Le ticket d'entrée est juste un poil plus élevé qu'une annotation <code>nginx</code>, mais le résultat est nettement plus solide. Et une fois la PKI interne en place, ajouter un nouveau backend HTTPS interne devient <em>très simple</em> : un <code>Certificate</code>, un label sur le namespace, une <code>BackendTLSPolicy</code>, et zou !</p>
<h2 id="ressources">Ressources</h2>
<ul>
<li><strong>Gateway API - BackendTLSPolicy</strong> : <a href="https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/">gateway-api.sigs.k8s.io/api-types/backendtlspolicy</a></li>
<li><strong>cert-manager</strong> : <a href="https://cert-manager.io/">cert-manager.io</a></li>
<li><strong>trust-manager</strong> : <a href="https://cert-manager.io/docs/trust/trust-manager/">cert-manager.io/docs/trust/trust-manager</a></li>
<li><strong>Envoy Gateway</strong> : <a href="https://gateway.envoyproxy.io/">gateway.envoyproxy.io</a></li>
</ul>]]></content:encoded></item></channel></rss>