KubeVirt - Partie 3: Traefik, Gateway API & CertManager

Introduction

Tout le minimum vital a été déployé lors des parties précédentes pour l’exécution de KubeVirt. La cible a été définie avec la configuration du serveur hôte, puis les composants principaux ont été installés.

Avant d’aller plus loin, il convient de mettre en œuvre quelques briques complémentaires, non directement liées à KubeVirt mais tout de même bien pratique pour la suite et souvent indispensable pour un cluster Kubernetes.

N’oublions pas que ce n’est pas parce qu’on exécute des VMs qu’on ne veut pas se donner la possibilité d’exécuter des applications conteneurisées plus classique.

Le but est d’avoir un usage de Kubernetes aussi bien pour des machines virtuelles que pour des conteneurs.

Dans ce cadre, il devient donc nécessaire d’ajouter au cluster K8S un ingress controler qu’on va rendre compatible avec la Gateway API, en l’occurrence Traefik et une solution de gestion automatisée des certificats, à savoir Cert-Manager.

L’un comme l’autre permettra d’accéder aux GUI des différents outils qu’on aura à déployer par la suite.

Schéma cible

Voici un schéma d’illustration qui résume les composants que nous allons mettre en oeuvre

Schéma des composants

Cliquez sur l'image pour l'agrandir.

Traefik

J’ai déjà eu l’occasion de présenter Traefik. N’hésitez pas à parcourir mon article dédié au sujet et intégré à mon CookBook K8S.

Traefik est un ingress controler, c’est-à-dire qu’il permet d’exposer ses services K8S à l’extérieur du cluster offrant des fonctionnalités de reverse proxy, comme l’ajout automatique de certificats, la réécriture d’URL, la protection par mot de passe….

Avec l’arrivée de la Gateway API que j’ai présenté dans cet article, Traefik est devenu une GatewayClass et permet donc d’exploiter cette nouvelle manière de faire pour la gestion de ses publications externes.

Création du namespace et du certificat par défaut

On démarre par la mise en œuvre d’un namespace dédié : inf-traefik-lan

kubectl create ns inf-traefik-lan
Création du namespace pour Traefik

Cliquez sur l'image pour l'agrandir.

Cela va me permettre d’y exécuter toutes les dépendances nécessaires et de distinguer mon instance de Traefik dédiée à mes applications publiées en interne d’une future instance de Traefik de DMZ. Si je devais avoir l’idée de dédier des nœuds de mon cluster à l’hébergement d’applications en DMZ, je pourrais le faire. C’est cet usage que je décris dans mon cookbook K8S, mais ici, vu que je ne dispose que d’un seul node, je me contenterais de mon instance lan de traefik.

Puis on crée un certificat générique qui va servir à la fois de certificat pour l’accès au dashboard de Traefik et de certificat par défaut pour la Gateway.

Chacun a sa propre technique. Je ne vais pas entrer dans les détails à ce stade.

Me concernant, je vais choisir le nom de publication traefikrubikub-gtw-lan.coolcorp.priv

Je génère la clef privée et le csr (cetificat submit requets) avec openssl

openssl req -new -nodes -sha256 -keyout traefikrubikub-gtw-lan.coolcorp.priv.key -out traefikrubikub-gtw-lan.coolcorp.priv.csr -newkey rsa:4096 -subj "/C=FR/ST=Ile-de-France/L=Paris/O=COOLCORP/OU=Infrastructure/CN=traefikrubikub-gtw-lan.coolcorp.priv" -reqexts SAN -config <(printf "[req]\ndistinguished_name = req_distinguished_name\n[req_distinguished_name]\n[SAN]\nsubjectAltName=DNS:traefikrubikub-gtw-lan.coolcorp.priv")

Puis je soumets mon CSR à ma PKI interne (un CA Windows Serveur intégré à mon domaine AD).

certreq -attrib "CertificateTemplate:TPL-SRV-WEB-DEFAULT" -submit .\traefikrubikub-gtw-lan.coolcorp.priv.csr

Pour récupérer mon certificat.

L’obtention de ce certificat est propre à chacun et à sa solution de certificat interne. Il suffit de pouvoir générer un certificat type serveur WEB, tout ce qu’il y a de plus classique en ayant un attribut CN et un SubjectAltName identique.

On va stoker ce certificat et la clef privée associée dans un secret Kubernetes au sein du namespace dédié à Traefik

kubectl create secret tls sec-gateway-default-cert --cert=traefikrubikub-gtw-lan.coolcorp.priv.cer --key=traefikrubikub-gtw-lan.coolcorp.priv.key -n inf-traefik-lan
Création du secret pour le certificat par défault

Cliquez sur l'image pour l'agrandir.

Déploiement de Traefik

Déploiement du compte de service

Traefik a besoin d’interagir avec le cluster pour interroger l’API.

On va donc exploiter un compte de service pour cela. Plutôt que de laisser l’installation créer un compte, on va le générer en amont via le fichier svc-traefik-lan.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: "svc-prd-traefik"
  namespace: "inf-traefik-lan"

Il suffit de l'appliquer.

kubectl apply -f svc-traefik-lan.yaml
Application du compte de service

Cliquez sur l'image pour l'agrandir.

Je ne rentre pas plus dans le détail, n’hésitez pas à parcourir mon article dédié dédié au besoin.

Déploiement du package helm

Comme pour Cilium déployé dans l’article précédent, on va utiliser helm pour déployer Traefik.

On déclare les repos

helm repo add traefik https://helm.traefik.io/traefik
helm repo update
Ajout du repo pour traefik

Cliquez sur l'image pour l'agrandir.

On labélise mon node pour qu’il soit sélectionné dans l’exécution des pods de traefik, en lien avec la configuration que je vais associer à Traefik.

kubectl label node prdk8sctp001 network=lan
kubectl label node prdk8sctp001 traefik-lan=yes
Labelisation du node

Cliquez sur l'image pour l'agrandir.

Cette configuration, justement, est dans le fichier traefik-config-lan.yaml

image:
  tag: "3.6.4"
  pullPolicy: IfNotPresent

deployment:
  enabled: true
  kind: DaemonSet
  labels:
    network: lan
  podLabels:
    network: lan

gatewayClass:
  enabled: true
  name: traefik-lan
  labels:
    network: lan

gateway:
  enabled: false

providers:
  kubernetesGateway:
    enabled: true
    labelSelector: "network=lan"
  kubernetesIngress:
    labelSelector: "network=lan"
    allowExternalNameServices: true
  kubernetesCRD:
    enabled: true
    allowCrossNamespace: false
    allowExternalNameServices: true
    allowEmptyServices: false
    namespaces:
      - inf-traefik-lan
      - longhorn-system
      - inf-kubevirtmanager-lan

serviceAccount:
  name: "svc-prd-traefik"

logs:
  general:
    level: ERROR

hostNetwork: true


ports:
  web:
    port: 80
    expose:
      default: true
    redirections:
      entryPoint:
        to: websecure
        scheme: https
        permanent: true
  websecure:
    port: 443
    expose:
      default: true
    proxyProtocol:
      trustedIPs:
        - "192.168.10.160"
    forwardedHeaders:
      trustedIPs:
        - "192.168.10.160"
    tls:
      enabled: true

securityContext:
  capabilities:
    drop: [ALL]
    add: [NET_BIND_SERVICE]
  readOnlyRootFilesystem: true
  runAsGroup: 0
  runAsNonRoot: false
  runAsUser: 0

updateStrategy:
  rollingUpdate:
    maxSurge: 0
    maxUnavailable: 1
  type: RollingUpdate

additionalArguments:
  - "--providers.kubernetesingress.ingressclass=traefik-lan"
  - "--serversTransport.insecureSkipVerify=true"

Globalement on indique qu’on va déployer Traefik en tant que DaemonSet, ce qui permettra d’avoir une mise à l’échelle automatique du nombre de pod associé sur tout nouveau node qui rejoindrait le cluster et qui répondrait au critère d’éligibilité.

Ensuite, on indique qu’on va utiliser la Gateway API en activant Traefik comme gatewayClass, qu’on va nommer traefik-lan.

Néanmoins, on continu d’activer les providers plus classiques qui sont :

  • kubernetesIngress: solution par défaut d’origine fournit par K8S pour l’exposition des assets
  • kubernetesCRD: Custom Ressource Definition de Traefik, soit les objets custom propre à traefik qui permettent d’utiliser des fonctionnalités spécifiques à Traefik.

À noter que les CRD s’activent par namespace, ce qui implique qu’il faudra peut-être, par la suite, redéployer Traefik si un nouveau namespace nécessite l’usage d’objets personnalisés de Traefik. Dans mon cas j’ai anticipé pour la suite en indiquant les namespaces qu’on exploitera plus tard.

Ainsi, on peut couvrir tous les besoins.

On précise l’usage du service account qu’on a déployé précédemment.

Il ne reste plus qu’à exécuter la commande helm suivante:

helm install --namespace=inf-traefik-lan traefik-lan traefik/traefik -f traefik-config-lan.yaml --set nodeSelector.traefik-lan=yes --set tolerations[0].key=node-role.kubernetes.io/control-plane --set tolerations[0].operator=Exists --set tolerations[0].effect=NoSchedule

On notera les complements à la commande en charge de supporter l’exécution d’un pod traefik sur un control plane et l’usage du selector traefik-lan à yes pour correspondre à la labélisation du node réalisé précédement.

Installation de traefik

Cliquez sur l'image pour l'agrandir.

À ce stade on est en mesure de vérifier que traefik c’est bien déployé.

kubectl get pod -n inf-traefik-lan
Controle de traefik

Cliquez sur l'image pour l'agrandir.

Déploiement de la Gateway API.

On souhaite exploiter l’API Gateway avec Traefik, de manière a être en conformité avec les nouveaux usage qui pousse à la Gateway API en lieu et place des Ingress.

N’hésitez pas à parcourir mon article dédié sur le sujet pour mieux comprendre ce point.

À noter que, dans cet article, je parle de l’installation en amont de la Gateway API car elle n’est pas incluse par défaut dans Kubernetes. Mais ici, elle est implicitement installée avec son activation dans la configuration de Traefik.

C’est pourquoi on peut directement exploiter le fichier gateway-lan.yaml pour créer l’objet gateway.

Voici son contenu:


apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: traefik-gateway-lan
  namespace: inf-traefik-lan
  labels:
    network: lan
spec:
  gatewayClassName: traefik-lan
  listeners:
  - name: web
    port: 80
    protocol: HTTP
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            network: lan
  - name: websecure
    port: 443
    protocol: HTTPS
    allowedRoutes:
      namespaces:
        from: Selector
        selector:
          matchLabels:
            network: lan
    tls:
      mode: Terminate
      certificateRefs:
        # Domaine pour les certificat dynamique
        - name: sec-tls-wildcard-coolcorp-priv
          namespace: inf-cert-lan
        # Certificat par défaut
        - name: sec-gateway-default-cert
            

On l’héberge dans le même namespace que Traefik.

À noter qu’on évoque l’usage de certificat type wildcard référencer sous le secret sec-tls-wildcard-coolcorp-priv. Pour l’instant ne vous en souciez pas, ça viendra un peu plus tard. Pour l’instant on a notre certificat par défaut.

Il est crucial de noter que, dans l’architecture utilisant la Gateway API, l’objet Gateway sert à lier un certificat à une URL, ce qui permet de la rendre accessible depuis l’extérieur en s’appuyant sur la classe Gateway, en l’occurrence Traefik.

On déploie la gateway:

kubectl apply -f gateway-lan.yaml
Déploiement de la gateway

Cliquez sur l'image pour l'agrandir.

À ce stade, si on vérifie son statut, il est possible qu’elle ne soit pas opérationnelle en raison du secret manquant, mais cela viendra par la suite

kubectl describe gateway -n inf-traefik-lan
Problème sur la gateway

Cliquez sur l'image pour l'agrandir.

gateway non prete

Cliquez sur l'image pour l'agrandir.

Déploiement du dashboard.

Il ne reste plus qu’à déployer le dashboard de Traefik pour offrir une GUI à l’outil et suivre ce qui se passe.

Bien que la GUI de Traefik n’autorise que de la lecture, on va tout de même la protéger avec un login et un mot de passe d’accès.

Pour ça on s’appuie d’abord sur l’outil htpasswd disponible dans le package httpd-tools (sudo dnf install httpd-tools) pour générer ce qu’il faut au bon format

htpasswd -Bbn bvivi57 'pass' > traefik-dashboard-lan-password
Génération des credentials pour le dashboard Traefik

Cliquez sur l'image pour l'agrandir.

On exploite le fichier contenant les credential pour l’inclure dans un secret Kubernetes

kubectl create secret generic sec-traefik-dashboard-lan-password --from-file=traefik-dashboard-lan-password --namespace inf-traefik-lan

Puis on applique le contenu du fichier traefik-lan-dashboard.yaml.


apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: traefik-lan-dashboard
  namespace: "inf-traefik-lan"
spec:
  entryPoints:
    - websecure
  routes:
    - kind: Rule
      match: Host(`traefikrubikub-gtw-lan.coolcorp.priv`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
      services:
        - name: api@internal
          kind: TraefikService
      middlewares:
        - name: traefik-dashboard-lan-auth # Referencing the BasicAuth middleware
          namespace: inf-traefik-lan
  tls:
     secretName: sec-gateway-default-cert
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: traefik-dashboard-lan-auth
  namespace: inf-traefik-lan
spec:
  basicAuth:
    secret: sec-traefik-dashboard-lan-password
            

avec la commande kubectl apply -f traefik-lan-dashboard.yaml

Application du dashboard Traefik

Cliquez sur l'image pour l'agrandir.

Dans mon exemple, cela me permet d’exposer la GUI sur l’URL https://traefikrubikub-gtw-lan.coolcorp.priv/dashboard avec comme certificat celui crée précédemment et partager par la gateway.

L’URL est protégée par le login et le mot de passe qu’on a choisi.

Il ne reste plus qu’à vérifier que l’URL est accessible. (Il est important de noter que j’ai créé un alias DNS sur mon serveur DNS pour rediriger l’IP par défaut de mon nœud et que celui-ci dispose bien entendu des ports 443 et 80 ouverts (configurés lors du déploiement du nœud dans le premier article via le rôle Ansible)).

Dans ce cas de figure, l’URL est desservie par un objet custom de Traefik et non par la Gateway, par contre, on a bien dans la GUI la confirmation que la Gateway API est prise en compte par Traefik.

Dashboard Traefik

Cliquez sur l'image pour l'agrandir.

On l’utilisera plus tard pour d’autres besoins.

Cert-Manager

Cert-Manager est un composant additionnel déployable dans Kubernetes qui va permettre d’automatiser tout le cycle de vie des certificats associés aux applications déployées sur le cluster.

Je détaille son fonctionnement et son usage ici. N’hésitez pas à y jeter un œil.

Dans notre tutoriel sur kubevirt, il va servir à traiter tous les certificats dont nous pourrions avoir besoin pour l’outillage tiers associé à KubeVirt, et même à couvrir tout besoin futur de publication d’applications.

Contrairement à l’article où j’associe Cert-Manager à acme2certifier, un proxy ACME permettant d’interagir avec une PKI Windows, je vais choisir ici une autre stratégie, plus simple et efficace.

Je vais simplement demander à ma PKI root hébergée sur mon serveur Windows (Services de certificats Active Directory (AD CS) ) de générer un certificat d’autorité intermédiaire.

Je vais associer un objet ClusterIssuer, soit un objet propre à Cert-Manager en charge de délivrer des certificats en interne du cluster, le certificat d’autorité intermédiaire.

De cette manière, ce ClusterIssuer va être une autorité de confiance validée par ma CA ROOT. Elle pourra ainsi délivrer un certificat wildcard sur mon domaine, renouvelable automatiquement tout les trois mois.

Cette stratégie va me permettre d’être totalement libéré de la contrainte des certificats. Le point critique sera le secret Kubernetes qui va stocker la clef privée et le certificat de l’autorité intermédiaire. Ce secret ne devra absolument pas être interrogeable par un tiers, n’oubliez pas que, sous K8S, un secret n’est rien d’autre qu’une valeur en base 64 dont il est possible de retrouver le contenu très simplement. D’où l’importance de bien gérer vos stratégies d’accès à vos clusters K8S. N’hésitez pas à parcourir mon articles sur le sujet.

Déploiement du Cert-Manager

Comme pour traefik on va passer par Helm.

Mais avant, on crée un namespace dédié

kubectl create ns inf-cert-lan
Création du namespace pour certmanager

Cliquez sur l'image pour l'agrandir.

On peut y déployer directement Cert-Manager en ayant au préalable pris soin de déclarer le repo.

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager --namespace inf-cert-lan --version v1.19.2 --set installCRDs=true --set nodeSelector.network=lan
Ajout des repo pour certmanager

Cliquez sur l'image pour l'agrandir.

Installation de certmanager

Cliquez sur l'image pour l'agrandir.

À noter qu’ici on n’exploite pas de fichier de configuration. On se contente de demander une version précise de Cert-Manager (ici la 1.19.2), d’installer les Custom Ressource de Cert-Manager, et de retenir le node avec le bon label (le meme qu’on a utilisé pour traefik)

À ce stade, on peut vérifier que tout est en ordre au niveau de Cert-Manager.

Kubectl get pod -n inf-cert-lan

Déploiement de l’issuer

Comme cela a été expliqué précédemment, on va créer un certificat propre à une CA intermédiaire.

On commence par créer la clef et le csr

openssl req -new -newkey rsa:4096 -nodes -keyout subordinate-ca-rubikube.key -out subordinate-ca-rubikube.csr -config ca-csr.conf

Puis on créer le certificat. Dans l’univers Microsoft, il y a un template pour cela, SubCA

certreq -attrib "CertificateTemplate:SubCA" -submit .\subordinate-ca-rubikube.csr
Ensemble certificat clef pour la subordonate CA

Cliquez sur l'image pour l'agrandir.

Avec la clef et le certificat (qui sont tout deux des données très sensibles, je le répète), on peut créer le secret qui va contenir l’ensemble

kubectl create secret tls sec-subordinate-ca-rubikube --cert=subordinate-ca-rubikube.cer --key=subordinate-ca-rubikube.key --namespace=inf-cert-lan
Création du secret pour le certificat SubCA

Cliquez sur l'image pour l'agrandir.

Tout est prêt pour l’issuer dont la description est contenue dans le fichier 01-clusterissuer-lan.yml


apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: clusterissuer-lan
spec:
  ca:
    secretName: sec-subordinate-ca-rubikube
            

On y référence le secret qu’on vient de créer et on décide bien qu’il s’agit d’un ClusterIssuer, soit un issuer compatible avec n’importe quel namespace.

N’hésitez pas à consulter mon article sur Cert-Manager issu du mon cookbook K8S si vous souhaitez en savoir plus.

kubectl apply -f 01-clusterissuer-lan.yml
Déploiment ClusterIssuer

Cliquez sur l'image pour l'agrandir.

On peut vérifier que l’issuer est opérationnelle.

kubectl get clusterissuer

Création du wildcard

Maintenant que l’issuer est up, on peut le solliciter pour créer notre wildcard via le fichier 02-certificat-wildcard.yml


apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: clusterissuer-lan
spec:
  ca:
    secretName: sec-subordinate-ca-rubikube
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: cert-wildcard-coolcorp
  namespace: inf-cert-lan
spec:
  secretName: sec-tls-wildcard-coolcorp-priv
  issuerRef:
    name: clusterissuer-lan
    kind: ClusterIssuer
  commonName: "*.coolcorp.priv"
  dnsNames:
  - "coolcorp.priv"
  - "*.coolcorp.priv"
  # Optionnel : Renouvellement auto 30 jours avant expiration
  renewBefore: 720h
            

Au sein de de ce fichier, je sollicite mon ClusterIssuer fraichement crée pour qu’il me génère un wildcard pour mon domaine interne

kubectl apply -f 02-certificat-wildcard.yml
Déploiement du certificat wildcard

Cliquez sur l'image pour l'agrandir.

Je me retrouve bien avec ce certificat dans mon namespace dédié

kubectl get cert -n inf-cert-lan
Controle du certificat

Cliquez sur l'image pour l'agrandir.

Ce namespace justement, n’est pas le namespace de la gateway. Hors si vous regardez plus haut dans la configuration de cette derniere, je faisais déjà référence à ce certificat via son secret associé dans ce namespace spécifique.

Pour autoriser la passerelle à utiliser un certificat hébergé en dehors de son namespace par défaut, on doit ajouter une délégation via l’objet ReferenceGrant.

Celui-ci est décrit dans mon fichier 03-referentgrant-wildcart.yml


---
apiVersion: gateway.networking.k8s.io/v1beta1
kind: ReferenceGrant
metadata:
  name: allow-traefik-read-coolcorp
  namespace: inf-cert-lan
spec:
  from:
  - group: gateway.networking.k8s.io
    kind: Gateway
    namespace: inf-traefik-lan
  to:
  - group: ""
    kind: Secret
    name: sec-tls-wildcard-coolcorp-priv[
            

Cela autorise la gateway à exploiter le nouveau certificat wildcard que je viens de créer grâce à mon ClusterIssuer, lui-même autorisé comme CA intermédiaire.

kubectl apply -f 03-referentgrant-wildcart.yml
Application du RefereantGrant

Cliquez sur l'image pour l'agrandir.

On peut vérifier que cela fonctionne, car si on interroge à nouveau la gateway, désormais celle-ci est à l’état true.

kubectl get gateway -n inf-traefik-lan
Controle de la Gateway

Cliquez sur l'image pour l'agrandir.

Conclusion intermediaire

Cette partie du tutoriel s’éloigne un peu de kubevirt et touche un usage plus global de Kubernetes. Mais il me semblait important de l’inclure, tant il facilitera la suite des opérations en n’ayant plus à se préoccuper des problématiques liées à la publication des GUI, comme celle de Longhorn dont on va avoir besoin par la suite.

N’hésitez pas à vous inspirez de cette configuration pour vos clusters K8S, même si kubevirt ne fait pas partie de vos besoins. La combinaison Traefik, Gateway API, Cert-Manager est exploitable pour bien des usages.

Pour la suite, c'est par