<?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>Let's Encrypt on Rémi Tech Notes</title><link>https://www.vrchr.fr/tags/lets-encrypt/</link><description>Recent content in Let's Encrypt on Rémi Tech Notes</description><generator>Hugo</generator><language>fr-fr</language><lastBuildDate>Wed, 29 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.vrchr.fr/tags/lets-encrypt/index.xml" rel="self" type="application/rss+xml"/><item><title>Envoy Gateway : backend par défaut &amp; certificat IP</title><link>https://www.vrchr.fr/posts/2026/07/29/envoy-gateway-default-backend-ip-cert/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0000</pubDate><guid>https://www.vrchr.fr/posts/2026/07/29/envoy-gateway-default-backend-ip-cert/</guid><description>Servir une page de maintenance par défaut sur une Envoy Gateway, en HTTPS, même sans FQDN, grâce aux certificats Let&amp;#39;s Encrypt sur adresse IP via le profil shortlived.</description><content:encoded><![CDATA[<p>On reprend les articles sur Envoy Gateway ! Comment faire quand on accède directement à l'IP publique de notre cluster porté par une <code>Gateway</code>, ou qu'aucune <code>HTTPRoute</code> ne matche ? On va voir dans cet article comment servir une <strong>page de maintenance</strong>, la servir <strong>en HTTPS</strong> sur l'<strong>IP elle-même</strong>, grâce aux certificats Let's Encrypt sur adresse IP et au profil <code>shortlived</code>.</p>
<p>Imaginons une Envoy Gateway exposée sur l'IP publique <code>203.0.113.14</code>, et une application <code>maintenance</code> (le service <code>maintenance</code> dans le namespace <code>maintenance</code>, sur le port <code>8080</code>) qui sert une simple page &quot;site en maintenance&quot;. On va voir comment servir cette page par défaut, en HTTPS sur l'IP publique elle-même.</p>
<p>⚠️ Je ne détaille pas l'installation d'Envoy Gateway ni de cert-manager (tous deux via Helm), ni non plus le déploiement de l'app de maintenance (simple page statique), qu'on suppose déjà en place. Nous allons nous concentrer sur le <strong>routing par défaut</strong> et le <strong>certificat sur IP</strong>.</p>
<h2 id="une-réponse-https-par-défaut-moche">Une réponse HTTP(S) par défaut &quot;moche&quot;</h2>
<p>Quand on déclare des <code>HTTPRoute</code> avec des <code>hostnames</code> bien précis (<code>app1.gravitek.io</code>, <code>app2.gravitek.io</code>, etc.), tout le reste,  qui ne correspond à aucune route (typiquement un <code>curl https://203.0.113.14/</code>, un scan, ou un DNS pas encore basculé) tombe dans le vide. Avec en prime une erreur de certificat TLS.</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">$ curl https://203.0.113.14/
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">curl: <span class="o">(</span>60<span class="o">)</span> SSL certificate problem: unable to get <span class="nb">local</span> issuer certificate
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">More details here: https://curl.se/docs/sslcerts.html
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">curl failed to verify the legitimacy of the server and therefore could not
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">establish a secure connection to it. To learn more about this situation and
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">how to fix it, please visit the web page mentioned above.
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">$ curl -k https://203.0.113.14/
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="m">404</span> page not found
</span></span></code></pre></div><p>On va voir ci-après comment résoudre 2 problèmes d'un coup :</p>
<ol>
<li><strong>Un backend par défaut</strong> : tout ce qui ne matche aucune route applicative atterrit sur une page de maintenance propre.</li>
<li><strong>Du HTTPS même sur l'IP</strong> : plus de gros warning de certificat invalide quand on accède à l'IP directement. Historiquement, impossible avec Let's Encrypt (que des domaines)... mais ça a changé 🎉.</li>
</ol>
<p>Pour le point 2, ça résoud notamment un &quot;problème&quot; quand le mail du RSSI arrive : <em>&quot;Bonjour, votre IP 203.0.113.14 présente un certificat invalide, merci de corriger 🙏&quot;</em>. Jusqu'à présent je lui répondais qu'on ne pouvait pas avoir un certificat sur une IP, mais c'est maintenant possible 😅.</p>
<h2 id="pré-requis--configuration-des-composants">Pré-requis : configuration des composants</h2>
<p>Pour activer le support de la Gateway API &amp; du Backend par défaut, deux paramètres doivent être activés dans les <code>values</code> des différents charts (si vous utilisez Helm, sinon je vous laisse chercher les équivalents niveau configuration) :</p>
<h3 id="cert-manager--support-de-la-gateway-api">cert-manager : support de la Gateway API</h3>
<p>cert-manager doit savoir résoudre un challenge ACME <strong>via une <code>HTTPRoute</code></strong> (et non un <code>Ingress</code>). C'est l'option <code>enableGatewayAPI</code> :</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"># custom-values-cert-manager.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">cert-manager</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="c"># CRDs</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">crds</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">enabled</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">  </span><span class="c"># Enable Gateway API Support</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">config</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">enableGatewayAPI</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>Sans ça, le solver <code>http01</code> de type <code>gatewayHTTPRoute</code> (qu'on verra plus bas) est tout simplement ignoré.</p>
<h3 id="envoy-gateway--activer-lapi-backend">Envoy Gateway : activer l'API <code>Backend</code></h3>
<p>Pour router vers un endpoint qui <strong>n'est pas un <code>Service</code> Kubernetes</strong> (ici une app dans un autre namespace, qu'on veut joindre par son FQDN interne), Envoy Gateway propose une ressource <code>Backend</code>. C'est une <strong>extension API</strong>, on l'active :</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"># custom-values-envoy.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c"># https://gateway.envoyproxy.io/v1.7/install/install-helm/#enable-backend-api</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nt">config</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">envoyGateway</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">extensionApis</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">enableBackend</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><h2 id="la-gateway-et-son-ip-publique">La Gateway et son IP publique</h2>
<p>Côté <code>GatewayClass</code>, rien de particulier : une classe standard rattachée au contrôleur Envoy Gateway.</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"># gatewayclass-eg.yaml</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">GatewayClass</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">eg</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">controllerName</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io/gatewayclass-controller</span><span class="w">
</span></span></span></code></pre></div><p>La <code>Gateway</code> elle-même expose deux listeners, HTTP et HTTPS, ce dernier terminant le TLS avec le secret <code>ip-cert-tls</code>, qu'on va générer juste après :</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"># gateway-eg.yaml</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">Gateway</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">eg</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">envoy-gateway-system</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">gatewayClassName</span><span class="p">:</span><span class="w"> </span><span class="l">eg</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">listeners</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">http</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">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">HTTP</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">port</span><span class="p">:</span><span class="w"> </span><span class="m">80</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">allowedRoutes</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">namespaces</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">from</span><span class="p">:</span><span class="w"> </span><span class="l">All</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">https</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">protocol</span><span class="p">:</span><span class="w"> </span><span class="l">HTTPS</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">port</span><span class="p">:</span><span class="w"> </span><span class="m">443</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">tls</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">mode</span><span class="p">:</span><span class="w"> </span><span class="l">Terminate</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">certificateRefs</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">group</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</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">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="ln">24</span><span class="cl"><span class="w">            </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ip-cert-tls</span><span class="w"> </span><span class="c"># Certificat IP</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">allowedRoutes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">        </span><span class="nt">namespaces</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">          </span><span class="nt">from</span><span class="p">:</span><span class="w"> </span><span class="l">All</span><span class="w">
</span></span></span></code></pre></div><h2 id="un-certificat-lets-encrypt-sur-une-ip">Un certificat Let's Encrypt sur une IP</h2>
<p>Pendant des années, Let's Encrypt ne signait que des <strong>noms de domaine</strong>. Taper <code>https://203.0.113.14/</code> donnait un certificat invalide. Depuis <a href="https://letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate/">l'annonce de Let's Encrypt sur les certificats pour adresses IP</a>, c'est devenu possible (c'est désormais en <a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability/">GA (janvier 2026)</a>) avec <strong>une contrainte</strong> : ces certificats ne sont délivrés que via le <strong>profil <code>shortlived</code></strong>, des certificats à durée de vie très courte (~6 jours).</p>
<h3 id="le-clusterissuer-en-profil-shortlived">Le ClusterIssuer en profil <code>shortlived</code></h3>
<p>cert-manager supporte la sélection de ce <a href="https://cert-manager.io/docs/configuration/acme/#acme-profiles">profil ACME</a> via le champ <code>profile</code>. On déclare un <code>ClusterIssuer</code> dédié, qui résout le challenge HTTP-01 <strong>via une <code>HTTPRoute</code></strong> rattachée à notre gateway :</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-letsencrypt-prod-eg-shortlived.yaml</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">letsencrypt-prod-shortlived</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">acme</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">email</span><span class="p">:</span><span class="w"> </span><span class="l">remi@gravitek.io</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">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="ln">10</span><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="ln">11</span><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-shortlived</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">profile</span><span class="p">:</span><span class="w"> </span><span class="l">shortlived</span><span class="w"> </span><span class="c"># Profil ACME</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">solvers</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">http01</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">gatewayHTTPRoute</span><span class="p">:</span><span class="w"> </span><span class="c"># Ref à ma Gateway</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">parentRefs</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">eg</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">namespace</span><span class="p">:</span><span class="w"> </span><span class="l">envoy-gateway-system</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">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.io</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">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Gateway</span><span class="w">
</span></span></span></code></pre></div><p>Deux points à noter :</p>
<ul>
<li><strong><code>profile: shortlived</code></strong> : c'est <em>obligatoire</em> pour obtenir un certificat sur IP. Un profil classique refusera la demande.</li>
<li><strong><code>solvers.http01.gatewayHTTPRoute</code></strong> : cert-manager crée automatiquement une <code>HTTPRoute</code> temporaire sur la gateway <code>eg</code> le temps de résoudre le challenge <code>/.well-known/acme-challenge/...</code>. Pour un cert sur IP, seul HTTP-01 est possible : pas de DNS-01 (pas de zone DNS pour une IP, CQFD 🤷).</li>
</ul>
<h3 id="le-certificate-qui-demande-lip">Le Certificate qui demande l'IP</h3>
<p>Maintenant qu'on a notre Gateway, Route et ClusterIssuer, on peut déclarer notre certificat. Il ressemble à n'importe quel autre, sauf qu'on remplace <code>dnsNames</code> par <strong><code>ipAddresses</code></strong> :</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"># certificate-ip.yaml</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">ip-cert</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">envoy-gateway-system</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">secretName</span><span class="p">:</span><span class="w"> </span><span class="l">ip-cert-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">secretTemplate</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">labels</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">x509-certificate-exporter/exclude</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;true&#34;</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">issuerRef</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">letsencrypt-prod-shortlived</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">ClusterIssuer</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">ipAddresses</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="m">203.0.113.14</span><span class="w">
</span></span></span></code></pre></div><p>L'IP doit être l'IP publique de la gateway (celle exposée par le <code>Service</code> <code>LoadBalancer</code> d'Envoy chez moi), et le <code>secretName</code> doit correspondre au <code>certificateRefs</code> du listener HTTPS.</p>
<blockquote>
<p>💡 <strong>Pourquoi Le label <code>x509-certificate-exporter/exclude: &quot;true&quot;</code> ?</strong> Si vous monitorez l'expiration de vos certificats avec <a href="https://github.com/enix/x509-certificate-exporter"><code>x509-certificate-exporter</code></a>, un certificat qui expire tous les ~6 jours va générer une alerte en permanence. Je décide de le dégager de la supervision.</p>
</blockquote>
<p>⚠️ Les certificats <code>shortlived</code> durent quelques jours seulement : cert-manager les renouvelle très fréquemment, attention au rate-limit Let's Encrypt et au solver HTTP-01.</p>
<p>Une fois le certificat émis :</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 envoy-gateway-system get certificate ip-cert
</span></span><span class="line"><span class="ln">2</span><span class="cl">NAME         READY   SECRET           AGE
</span></span><span class="line"><span class="ln">3</span><span class="cl">ip-cert      True    ip-cert-tls      2m
</span></span></code></pre></div><p>Et <code>curl https://203.0.113.14/</code> ne hurle plus au certificat invalide 🎉.</p>
<h2 id="le-backend-par-défaut-mais-avec-la-page-de-maintenance">Le backend par défaut, mais avec la page de maintenance</h2>
<p>On a maintenant un certificat valide pour l'IP, mais on a toujours une page par défaut moche. On va améliorer cela avec un backend par défaut.</p>
<h3 id="la-ressource-backend">La ressource <code>Backend</code></h3>
<p>L'app de maintenance vit dans son propre namespace <code>maintenance</code>. Plutôt que de jouer avec un <code>Service</code> cross-namespace et une <code>ReferenceGrant</code>, on déclare un <code>Backend</code> Envoy Gateway qui pointe vers le <strong>FQDN interne</strong> du service :</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"># backend-maintenance.yaml</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.envoyproxy.io/v1alpha1</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">Backend</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">maintenance</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">envoy-gateway-system</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">endpoints</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">fqdn</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">hostname</span><span class="p">:</span><span class="w"> </span><span class="l">maintenance.maintenance.svc.cluster.local</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">port</span><span class="p">:</span><span class="w"> </span><span class="m">8080</span><span class="w">
</span></span></span></code></pre></div><p>C'est exactement le cas d'usage de l'API <code>Backend</code> qu'on a activée plus haut : un endpoint arbitraire (FQDN, IP...) consommable depuis une <code>HTTPRoute</code>, dans le namespace de la gateway.</p>
<h3 id="la-httproute-par-défaut-catch-all">La HTTPRoute par défaut (catch-all)</h3>
<p>Le &quot;backend par défaut&quot; est simplement <strong>une <code>HTTPRoute</code> sans <code>hostnames</code></strong>. Sans hostname déclaré, elle matche <em>tout</em> le trafic du listener HTTPS qui n'est pas capté par une route applicative plus spécifique :</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-default-backend.yaml</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">default-backend</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">envoy-gateway-system</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">parentRefs</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">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.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">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">11</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></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">https</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">rules</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">backendRefs</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">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.envoyproxy.io</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">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Backend</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">name</span><span class="p">:</span><span class="w"> </span><span class="l">maintenance</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">weight</span><span class="p">:</span><span class="w"> </span><span class="m">1</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">matches</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">path</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">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">22</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><p>Le <code>backendRefs</code> ne pointe pas un <code>Service</code> mais bien un <code>kind: Backend</code> du groupe <code>gateway.envoyproxy.io</code>. Les <code>HTTPRoute</code> applicatives, elles, ont des <code>hostnames</code> précis et sont donc <strong>plus spécifiques</strong> : Gateway API les fait gagner sur le catch-all. Notre <code>default-backend</code> ne capte donc que le reste IP directe, host inconnu, DNS pas encore basculé.</p>
<blockquote>
<p>⚠️ Je vous invite à lire la <a href="https://gateway.envoyproxy.io/docs/tasks/traffic/backend/">doc Envoy Gateway</a>, quelques warnings &amp; restrictions existent.</p>
</blockquote>
<h3 id="redirection-ip-http-vers-https">Redirection &quot;IP&quot; HTTP vers HTTPS</h3>
<p>Tant qu'on y est, on évite de servir la maintenance en clair sur le port 80 : on redirige tout HTTP vers HTTPS, là aussi via une route catch-all (sans hostname) attachée au listener <code>http</code> :</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-https-redirect.yaml</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">http-to-https-redirect</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">envoy-gateway-system</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">parentRefs</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">group</span><span class="p">:</span><span class="w"> </span><span class="l">gateway.networking.k8s.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">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">11</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></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">sectionName</span><span class="p">:</span><span class="w"> </span><span class="l">http</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">rules</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">filters</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">type</span><span class="p">:</span><span class="w"> </span><span class="l">RequestRedirect</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">requestRedirect</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">scheme</span><span class="p">:</span><span class="w"> </span><span class="l">https</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">statusCode</span><span class="p">:</span><span class="w"> </span><span class="m">301</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">matches</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">path</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">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">22</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><p>⚠️ Attention à l'ordre des opérations avec le challenge ACME : le solver HTTP-01 a besoin de répondre en <strong>HTTP</strong> sur <code>/.well-known/acme-challenge/</code>. La <code>HTTPRoute</code> temporaire créée par cert-manager est plus spécifique (elle matche ce path précis) que notre redirect catch-all <code>/</code>, donc le challenge passe avant la redirection, mais faites attention on sait jamais.</p>
<h2 id="le-résultat">Le résultat</h2>
<p>Au final, n'importe quel accès qui ne tombe pas sur une app connue :</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"><span class="c1"># Accès direct par IP, en HTTP → redirigé en HTTPS</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ curl -IL http://203.0.113.14/
</span></span><span class="line"><span class="ln">3</span><span class="cl">HTTP/1.1 <span class="m">301</span> Moved Permanently
</span></span><span class="line"><span class="ln">4</span><span class="cl">location: https://203.0.113.14/
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># ... puis servi par la page de maintenance, certificat valide</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">$ curl https://203.0.113.14/
</span></span><span class="line"><span class="ln">8</span><span class="cl">&lt;html&gt;&lt;body&gt;&lt;h1&gt;Page non trouvée.&lt;/h1&gt;&lt;/body&gt;&lt;/html&gt;
</span></span></code></pre></div><p>Plus de <code>404 page not found</code> moche, plus de warning de certificat : un point d'entrée propre, en HTTPS, même sur l'IP nue.</p>
<p>Voici &quot;en joli&quot; ce que ça peut donner:</p>
<p><img alt="Page not found" class="zoomable" decoding="async" loading="lazy" src="/2026/07/2026-07-29-page-not-found.webp"></p>
<h2 id="conclusion">Conclusion</h2>
<p>En assemblant quelques ressources, on obtient un comportement par défaut soigné sur la gateway :</p>
<ul>
<li>🧭 Un <strong>backend par défaut</strong> via une <code>HTTPRoute</code> sans hostname + une ressource <code>Backend</code> Envoy Gateway, qui rattrape tout le trafic non routé.</li>
<li>🔒 Du <strong>HTTPS valide sur l'IP elle-même</strong>, grâce aux certificats Let's Encrypt sur adresse IP et au profil <code>shortlived</code>.</li>
<li>🤖 Le tout <strong>auto-renouvelé</strong> par cert-manager en solver HTTP-01, même si le certificat est court.</li>
</ul>
<p>Avec tout ça, notre RSSI est content 😎. Ah, et pour les curieux, je n'ai pas testé en IPv6 !</p>
<h2 id="ressources">Ressources</h2>
<ul>
<li><strong>Let's Encrypt - Premier certificat sur adresse IP</strong> : <a href="https://letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate/">letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate</a></li>
<li><strong>Let's Encrypt - Certificats short-lived</strong> : <a href="https://letsencrypt.org/2025/02/20/first-short-lived-cert-issued/">letsencrypt.org/2025/02/20/first-short-lived-cert-issued</a></li>
<li><strong>Let's Encrypt - Disponibilité générale (6 jours &amp; IP)</strong> : <a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability/">letsencrypt.org/2026/01/15/6day-and-ip-general-availability</a></li>
<li><strong>cert-manager - ACME profiles</strong> : <a href="https://cert-manager.io/docs/configuration/acme/#acme-profiles">cert-manager.io/docs/configuration/acme/#acme-profiles</a></li>
<li><strong>Envoy Gateway - Backend</strong> : <a href="https://gateway.envoyproxy.io/docs/tasks/traffic/backend/">gateway.envoyproxy.io/docs/tasks/traffic/backend</a></li>
</ul>]]></content:encoded></item></channel></rss>