Envoy Gateway : backend par défaut & certificat IP

Servir une page de maintenance par défaut sur une Envoy Gateway, en HTTPS, même sans FQDN, grâce aux certificats Let's Encrypt sur adresse IP via le profil shortlived.

RV
Rémi Verchère
Platform & Cloud Native
9 min de lecture

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.

$ curl https://203.0.113.14/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
 
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
 
$ curl -k https://203.0.113.14/
404 page not found

On va voir ci-après comment résoudre 2 problèmes d'un coup :

  1. Un backend par défaut : tout ce qui ne matche aucune route applicative atterrit sur une page de maintenance propre.
  2. 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 :

# custom-values-cert-manager.yaml
cert-manager:
  # CRDs
  crds:
    enabled: true
 
  # Enable Gateway API Support
  config:
    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 :

# custom-values-envoy.yaml
# https://gateway.envoyproxy.io/v1.7/install/install-helm/#enable-backend-api
config:
  envoyGateway:
    extensionApis:
      enableBackend: true

La Gateway et son IP publique

Côté GatewayClass, rien de particulier : une classe standard rattachée au contrôleur Envoy Gateway.

# gatewayclass-eg.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: eg
spec:
  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 :

# gateway-eg.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg
  namespace: envoy-gateway-system
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces:
          from: All
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - group: ""
            kind: Secret
            name: ip-cert-tls # Certificat IP
      allowedRoutes:
        namespaces:
          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 :

# clusterissuer-letsencrypt-prod-eg-shortlived.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-shortlived
spec:
  acme:
    email: remi@gravitek.io
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-shortlived
    profile: shortlived # Profil ACME
    solvers:
      - http01:
          gatewayHTTPRoute: # Ref à ma Gateway
            parentRefs:
              - name: eg
                namespace: envoy-gateway-system
                group: gateway.networking.k8s.io
                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 une HTTPRoute temporaire sur la gateway eg le 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 :

# certificate-ip.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: ip-cert
  namespace: envoy-gateway-system
spec:
  secretName: ip-cert-tls
  secretTemplate:
    labels:
      x509-certificate-exporter/exclude: "true"
  issuerRef:
    name: letsencrypt-prod-shortlived
    kind: ClusterIssuer
  ipAddresses:
    - 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 avec x509-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 :

$ kubectl -n envoy-gateway-system get certificate ip-cert
NAME         READY   SECRET           AGE
ip-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 :

# backend-maintenance.yaml
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: Backend
metadata:
  name: maintenance
  namespace: envoy-gateway-system
spec:
  endpoints:
    - fqdn:
        hostname: maintenance.maintenance.svc.cluster.local
        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 :

# httproute-default-backend.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: default-backend
  namespace: envoy-gateway-system
spec:
  parentRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: eg
      sectionName: https
  rules:
    - backendRefs:
        - group: gateway.envoyproxy.io
          kind: Backend
          name: maintenance
          weight: 1
      matches:
        - path:
            type: PathPrefix
            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 :

# httproute-https-redirect.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: http-to-https-redirect
  namespace: envoy-gateway-system
spec:
  parentRefs:
    - group: gateway.networking.k8s.io
      kind: Gateway
      name: eg
      sectionName: http
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301
      matches:
        - path:
            type: PathPrefix
            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 :

# Accès direct par IP, en HTTP → redirigé en HTTPS
$ curl -IL http://203.0.113.14/
HTTP/1.1 301 Moved Permanently
location: https://203.0.113.14/
 
# ... puis servi par la page de maintenance, certificat valide
$ curl https://203.0.113.14/
<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 HTTPRoute sans hostname + une ressource Backend Envoy 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

© Gravitek. Tous droits réservés.

logo

Gravitek est une Société à taille humaine, guidée par la qualité de service et la construction d'une relation durable avec ses clients.