On reprend les articles sur Envoy Gateway ! Comment faire quand on accède directement à l'IP publique de notre cluster porté par une Gateway, ou qu'aucune HTTPRoute ne matche ? On va voir dans cet article comment servir une page de maintenance, la servir en HTTPS sur l'IP elle-même, grâce aux certificats Let's Encrypt sur adresse IP et au profil shortlived.
Imaginons une Envoy Gateway exposée sur l'IP publique 203.0.113.14, et une application maintenance (le service maintenance dans le namespace maintenance, sur le port 8080) qui sert une simple page "site en maintenance". On va voir comment servir cette page par défaut, en HTTPS sur l'IP publique elle-même.
⚠️ 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 routing par défaut et le certificat sur IP.
Une réponse HTTP(S) par défaut "moche"
Quand on déclare des HTTPRoute avec des hostnames bien précis (app1.gravitek.io, app2.gravitek.io, etc.), tout le reste, qui ne correspond à aucune route (typiquement un curl https://203.0.113.14/, un scan, ou un DNS pas encore basculé) tombe dans le vide. Avec en prime une erreur de certificat TLS.
1$ curl https://203.0.113.14/
2curl: (60) SSL certificate problem: unable to get local issuer certificate
3More details here: https://curl.se/docs/sslcerts.html
4
5curl failed to verify the legitimacy of the server and therefore could not
6establish a secure connection to it. To learn more about this situation and
7how to fix it, please visit the web page mentioned above.
8
9$ curl -k https://203.0.113.14/
10404 page not found
On va voir ci-après comment résoudre 2 problèmes d'un coup :
- Un backend par défaut : tout ce qui ne matche aucune route applicative atterrit sur une page de maintenance propre.
- Du HTTPS même sur l'IP : 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é 🎉.
Pour le point 2, ça résoud notamment un "problème" quand le mail du RSSI arrive : "Bonjour, votre IP 203.0.113.14 présente un certificat invalide, merci de corriger 🙏". Jusqu'à présent je lui répondais qu'on ne pouvait pas avoir un certificat sur une IP, mais c'est maintenant possible 😅.
Pré-requis : configuration des composants
Pour activer le support de la Gateway API & du Backend par défaut, deux paramètres doivent être activés dans les values des différents charts (si vous utilisez Helm, sinon je vous laisse chercher les équivalents niveau configuration) :
cert-manager : support de la Gateway API
cert-manager doit savoir résoudre un challenge ACME via une HTTPRoute (et non un Ingress). C'est l'option enableGatewayAPI :
1# custom-values-cert-manager.yaml
2cert-manager:
3 # CRDs
4 crds:
5 enabled: true
6
7 # Enable Gateway API Support
8 config:
9 enableGatewayAPI: true
Sans ça, le solver http01 de type gatewayHTTPRoute (qu'on verra plus bas) est tout simplement ignoré.
Envoy Gateway : activer l'API Backend
Pour router vers un endpoint qui n'est pas un Service Kubernetes (ici une app dans un autre namespace, qu'on veut joindre par son FQDN interne), Envoy Gateway propose une ressource Backend. C'est une extension API, on l'active :
1# custom-values-envoy.yaml
2# https://gateway.envoyproxy.io/v1.7/install/install-helm/#enable-backend-api
3config:
4 envoyGateway:
5 extensionApis:
6 enableBackend: true
La Gateway et son IP publique
Côté GatewayClass, rien de particulier : une classe standard rattachée au contrôleur Envoy Gateway.
1# gatewayclass-eg.yaml
2apiVersion: gateway.networking.k8s.io/v1
3kind: GatewayClass
4metadata:
5 name: eg
6spec:
7 controllerName: gateway.envoyproxy.io/gatewayclass-controller
La Gateway elle-même expose deux listeners, HTTP et HTTPS, ce dernier terminant le TLS avec le secret ip-cert-tls, qu'on va générer juste après :
1# gateway-eg.yaml
2apiVersion: gateway.networking.k8s.io/v1
3kind: Gateway
4metadata:
5 name: eg
6 namespace: envoy-gateway-system
7spec:
8 gatewayClassName: eg
9 listeners:
10 - name: http
11 protocol: HTTP
12 port: 80
13 allowedRoutes:
14 namespaces:
15 from: All
16 - name: https
17 protocol: HTTPS
18 port: 443
19 tls:
20 mode: Terminate
21 certificateRefs:
22 - group: ""
23 kind: Secret
24 name: ip-cert-tls # Certificat IP
25 allowedRoutes:
26 namespaces:
27 from: All
Un certificat Let's Encrypt sur une IP
Pendant des années, Let's Encrypt ne signait que des noms de domaine. Taper https://203.0.113.14/ donnait un certificat invalide. Depuis l'annonce de Let's Encrypt sur les certificats pour adresses IP, c'est devenu possible (c'est désormais en GA (janvier 2026)) avec une contrainte : ces certificats ne sont délivrés que via le profil shortlived, des certificats à durée de vie très courte (~6 jours).
Le ClusterIssuer en profil shortlived
cert-manager supporte la sélection de ce profil ACME via le champ profile. On déclare un ClusterIssuer dédié, qui résout le challenge HTTP-01 via une HTTPRoute rattachée à notre gateway :
1# clusterissuer-letsencrypt-prod-eg-shortlived.yaml
2apiVersion: cert-manager.io/v1
3kind: ClusterIssuer
4metadata:
5 name: letsencrypt-prod-shortlived
6spec:
7 acme:
8 email: remi@gravitek.io
9 server: https://acme-v02.api.letsencrypt.org/directory
10 privateKeySecretRef:
11 name: letsencrypt-prod-shortlived
12 profile: shortlived # Profil ACME
13 solvers:
14 - http01:
15 gatewayHTTPRoute: # Ref à ma Gateway
16 parentRefs:
17 - name: eg
18 namespace: envoy-gateway-system
19 group: gateway.networking.k8s.io
20 kind: Gateway
Deux points à noter :
profile: shortlived: c'est obligatoire pour obtenir un certificat sur IP. Un profil classique refusera la demande.solvers.http01.gatewayHTTPRoute: cert-manager crée automatiquement uneHTTPRoutetemporaire sur la gatewayegle temps de résoudre le challenge/.well-known/acme-challenge/.... Pour un cert sur IP, seul HTTP-01 est possible : pas de DNS-01 (pas de zone DNS pour une IP, CQFD 🤷).
Le Certificate qui demande l'IP
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 dnsNames par ipAddresses :
1# certificate-ip.yaml
2apiVersion: cert-manager.io/v1
3kind: Certificate
4metadata:
5 name: ip-cert
6 namespace: envoy-gateway-system
7spec:
8 secretName: ip-cert-tls
9 secretTemplate:
10 labels:
11 x509-certificate-exporter/exclude: "true"
12 issuerRef:
13 name: letsencrypt-prod-shortlived
14 kind: ClusterIssuer
15 ipAddresses:
16 - 203.0.113.14
L'IP doit être l'IP publique de la gateway (celle exposée par le Service LoadBalancer d'Envoy chez moi), et le secretName doit correspondre au certificateRefs du listener HTTPS.
💡 Pourquoi Le label
x509-certificate-exporter/exclude: "true"? Si vous monitorez l'expiration de vos certificats avecx509-certificate-exporter, 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.
⚠️ Les certificats shortlived durent quelques jours seulement : cert-manager les renouvelle très fréquemment, attention au rate-limit Let's Encrypt et au solver HTTP-01.
Une fois le certificat émis :
1$ kubectl -n envoy-gateway-system get certificate ip-cert
2NAME READY SECRET AGE
3ip-cert True ip-cert-tls 2m
Et curl https://203.0.113.14/ ne hurle plus au certificat invalide 🎉.
Le backend par défaut, mais avec la page de maintenance
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.
La ressource Backend
L'app de maintenance vit dans son propre namespace maintenance. Plutôt que de jouer avec un Service cross-namespace et une ReferenceGrant, on déclare un Backend Envoy Gateway qui pointe vers le FQDN interne du service :
1# backend-maintenance.yaml
2apiVersion: gateway.envoyproxy.io/v1alpha1
3kind: Backend
4metadata:
5 name: maintenance
6 namespace: envoy-gateway-system
7spec:
8 endpoints:
9 - fqdn:
10 hostname: maintenance.maintenance.svc.cluster.local
11 port: 8080
C'est exactement le cas d'usage de l'API Backend qu'on a activée plus haut : un endpoint arbitraire (FQDN, IP...) consommable depuis une HTTPRoute, dans le namespace de la gateway.
La HTTPRoute par défaut (catch-all)
Le "backend par défaut" est simplement une HTTPRoute sans hostnames. Sans hostname déclaré, elle matche tout le trafic du listener HTTPS qui n'est pas capté par une route applicative plus spécifique :
1# httproute-default-backend.yaml
2apiVersion: gateway.networking.k8s.io/v1
3kind: HTTPRoute
4metadata:
5 name: default-backend
6 namespace: envoy-gateway-system
7spec:
8 parentRefs:
9 - group: gateway.networking.k8s.io
10 kind: Gateway
11 name: eg
12 sectionName: https
13 rules:
14 - backendRefs:
15 - group: gateway.envoyproxy.io
16 kind: Backend
17 name: maintenance
18 weight: 1
19 matches:
20 - path:
21 type: PathPrefix
22 value: /
Le backendRefs ne pointe pas un Service mais bien un kind: Backend du groupe gateway.envoyproxy.io. Les HTTPRoute applicatives, elles, ont des hostnames précis et sont donc plus spécifiques : Gateway API les fait gagner sur le catch-all. Notre default-backend ne capte donc que le reste IP directe, host inconnu, DNS pas encore basculé.
⚠️ Je vous invite à lire la doc Envoy Gateway, quelques warnings & restrictions existent.
Redirection "IP" HTTP vers HTTPS
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 http :
1# httproute-https-redirect.yaml
2apiVersion: gateway.networking.k8s.io/v1
3kind: HTTPRoute
4metadata:
5 name: http-to-https-redirect
6 namespace: envoy-gateway-system
7spec:
8 parentRefs:
9 - group: gateway.networking.k8s.io
10 kind: Gateway
11 name: eg
12 sectionName: http
13 rules:
14 - filters:
15 - type: RequestRedirect
16 requestRedirect:
17 scheme: https
18 statusCode: 301
19 matches:
20 - path:
21 type: PathPrefix
22 value: /
⚠️ Attention à l'ordre des opérations avec le challenge ACME : le solver HTTP-01 a besoin de répondre en HTTP sur /.well-known/acme-challenge/. La HTTPRoute temporaire créée par cert-manager est plus spécifique (elle matche ce path précis) que notre redirect catch-all /, donc le challenge passe avant la redirection, mais faites attention on sait jamais.
Le résultat
Au final, n'importe quel accès qui ne tombe pas sur une app connue :
1# Accès direct par IP, en HTTP → redirigé en HTTPS
2$ curl -IL http://203.0.113.14/
3HTTP/1.1 301 Moved Permanently
4location: https://203.0.113.14/
5
6# ... puis servi par la page de maintenance, certificat valide
7$ curl https://203.0.113.14/
8<html><body><h1>Page non trouvée.</h1></body></html>
Plus de 404 page not found moche, plus de warning de certificat : un point d'entrée propre, en HTTPS, même sur l'IP nue.
Voici "en joli" ce que ça peut donner:

Conclusion
En assemblant quelques ressources, on obtient un comportement par défaut soigné sur la gateway :
- 🧭 Un backend par défaut via une
HTTPRoutesans hostname + une ressourceBackendEnvoy Gateway, qui rattrape tout le trafic non routé. - 🔒 Du HTTPS valide sur l'IP elle-même, grâce aux certificats Let's Encrypt sur adresse IP et au profil
shortlived. - 🤖 Le tout auto-renouvelé par cert-manager en solver HTTP-01, même si le certificat est court.
Avec tout ça, notre RSSI est content 😎. Ah, et pour les curieux, je n'ai pas testé en IPv6 !
Ressources
- Let's Encrypt - Premier certificat sur adresse IP : letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate
- Let's Encrypt - Certificats short-lived : letsencrypt.org/2025/02/20/first-short-lived-cert-issued
- Let's Encrypt - Disponibilité générale (6 jours & IP) : letsencrypt.org/2026/01/15/6day-and-ip-general-availability
- cert-manager - ACME profiles : cert-manager.io/docs/configuration/acme/#acme-profiles
- Envoy Gateway - Backend : gateway.envoyproxy.io/docs/tasks/traffic/backend
