Jusqu’à maintenant je générais manuellement mes certificats TLS via Let’s Encrypt avec certbot. Et je configurais ces certificats via un reverse proxy nginx au sein de mon réseau local. Comme un certificat Let’s Encrypt expire au bout de 3 mois, il faut gérer le renouvellement manuellement un peu avant 3 mois, ce que j’ai fini par trouver fastidieux.
Pour éviter ça, j’ai entrepris de migrer le plus possible mes services internes vers Traefik Proxy qui est installé par défaut dans un cluster K3s. Traefik supporte la génération automatique de certificats Let’s Encrypt, et ce sans même utiliser cert-manager, grâce à la librairie LEGO intégrée à Traefik.
Les services à exposer en https sont dans mon réseau local et ne sont pas exposés sur Internet. Au lieu d’utiliser le classique et simple HTTP Challenge (aka « HTTP-01 »), je dois donc utliser le DNS Challenge (DNS-01). Ce dernier requiert d’avoir à disposition une API chez son fournisseur DNS. Le mien est Cloudflare et propose bien une API, c’est donc parfait. Pour comprendre le fonctionnement que je ne décrirai pas ici, voir cet article: Challenge Types: DNS-01
Mise en oeuvre
Important: mon installation utilise le HelmChart Traefik v25.0, qui utilise l’image de Traefik en version 2.11. Ces instructions ne conviendront pas exactement sur une version très différente. De plus, j’utilise le paramètre de configuration disablePropagationCheck: true
qui n’est pas conseillé si l’on peut s’en passer, et qui est propre à ma configuration réseau comme expliqué plus loin.
Point de départ avec une page en http
J’utilise le projet homepage en guise de page d’accueil, avec le lien vers mes services. C’est un service très simple, composé d’une seule page web, c’est donc par lui que j’ai commencé sur Traefik en https. C’est aussi un bon exemple pour illustrer cet article.
Pour l’installer dans Kubernetes, les instructions sont ici.
Dans mon installation, je n’ai besoin que des manifestes ConfigMap, Deployment et Service. Les autres manifestes des instructions d’installation ne sont pas indispensables (ils sont destinés à une configuration bien particulière avec des widgets dont on n’a pas spécialement besoin).
On remplacera le manifeste Ingress par un IngressRoute qui est spécifique à Traefik. Je préfère utiliser le type IngressRoute car il est plus facile à lire que Ingress, qui est générique dans Kubernetes, et donc plus abstrait:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: homepage-ingress-http
spec:
entryPoints:
- web
routes:
- match: Host(`homepage.eric-bml.net`)
kind: Rule
services:
- name: homepage-service
namespace: default
port: 3000
scheme: http
Une fois les manifestes déployés, si tout va bien, http://homeage.eric-bml.net est accessible en http via Traefik (à adapter avec votre nom de domaine bien entendu).
Ajout du https
Maintenant, si on souhaite activer l’accès https avec un certificat généré automatiquement par Traefik, on peut ajouter ce nouveau manifeste:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: homepage-ingress-https
spec:
entryPoints:
- websecure
routes:
- match: Host(`homepage.eric-bml.net`)
kind: Rule
services:
- name: homepage-service
namespace: default
port: 3000
scheme: http
tls:
certResolver: acme
Si on teste directement, cela ne va pas fonctionner correctement car il faut déclarer le certResolver: acme
dans la configuration statique de Traefik. Le nom « acme » est la convention qu’on retrouve dans beaucoup d’exemples sur internet et dans la documentation officielle, et n’importe quel nom peut être utilisé du moment que le même nom est utilisé dans la configuration statique (« acme » étant le nom du protocole avec lequel Let’s Encrypt est compatible).
Pour modifier la configuration statique de Traefik sur un cluster K3s, il faut créer (ou modifier si vous l’avez déjà) un manifeste de type HelmChartConfig
. Il s’agit d’un fichier sur la machine hôte qui héberge K3s, dans le répertoire /var/lib/rancher/k3s/server/manifests/
(on pourra nommer ce fichier traefik-config.yaml par exemple):
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
persistence:
enabled: true
name: data
accessMode: ReadWriteOnce
size: 32Mi
env:
- name: CF_DNS_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-credentials
key: CF_DNS_API_TOKEN
- name: CF_API_EMAIL
valueFrom:
secretKeyRef:
name: cloudflare-api-credentials
key: CF_API_EMAIL
certResolvers:
acme:
caServer: https://acme-staging-v02.api.letsencrypt.org/directory
email: your@email.com
storage: /data/acme-staging.json
dnsChallenge:
provider: cloudflare
# Note: disablePropagationCheck is NOT recommended but required in my personal network setup
disablePropagationCheck: true
delayBeforeCheck: 120s
deployment:
initContainers:
- name: volume-permissions
image: busybox:latest
command: ["sh", "-c", "touch /data/acme-staging.json; touch /data/acme.json; chmod -v 600 /data/acme-staging.json; chmod -v 600 /data/acme.json"]
volumeMounts:
- mountPath: /data
name: data
Il vous faudra adapter le contenu présenté à votre cas d’usage. Dans mon cas, mon fournisseur DNS est Cloudflare, j’utilise donc ce provider mais ce n’est pas nécessairement le vôtre. Le champ email
correspond à l’adresse e-mail transmise à Let’s Encrypt, qui n’est pas nécessairement la même valeur que l’email utilisé pour s’authentifier au provider Cloudflare.
Les point particuliers de cette configuration sont les suivants:
- persistence: il est important d’activer la persistence, sinon Traefik va tenter de regénérer les certificats à chaque fois que son pod redémarre.
- Variables d’environnement
CF_API_EMAIL
etCF_DNS_API_TOKEN
: elles sont requises par l’API du provider DNS Cloudflare (l’API token se génère dans le dashboard de Cloudflare). La valeur de ces variables est chargée à partir d’un Secret Kubernetes nommé « cloudflare-api-credentials » qui doit se trouver dans le même namespace que Traefik (« kube-system » étant le namespace dans un cluster K3s). - caServer: on utilise
caServer: https://acme-staging-v02.api.letsencrypt.org/directory
à des fins de test, pour éviter d’atteindre le rate limiting sur le serveur de production de Let’s Encrypt en cas d’erreurs répétées. Il faudra commenter ce paramètre quand on aura validé le bon fonctionnement. - initContainers: trouvé dans la documentation officielle de Traefik, il s’agit d’un initContainer qui s’assure que le fichier acme.json existe bien et que les permissions (chmod 600) sont celles prévues.
J’ai légèrement adapté l’exemple de la documentation pour créer deux fichiers, l’un pour Staging (acme-staging.json), l’autre pour Production (acme.json). C’est utile quand on veut switcher de l’un à l’autre.
Sur Cloudflare, l’API Token doit avoir les permissions suivantes: Zone:Read
, DNS:Edit
et on peut filtrer la Zone sur le domaine à gérer (sans filtre, le token a accès à tous les domaines du compte).
Pour que cette configuration fonctionne, il faut également déployer le Secret contenant les deux valeurs pour l’authentification à l’API Cloudflare avec CF_API_EMAIL
et CF_DNS_API_TOKEN
:
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-credentials
namespace: kube-system
stringData:
CF_API_EMAIL: "<username>"
CF_DNS_API_TOKEN: "<password>"
Une fois ce secret déployé dans Kubernetes et le fichier de configuration statique de Traefik mis à jour sur la machine hôte du cluster Kubernetes, Traefik devrait se relancer tout seul, et être en mesure de générer des certificats.
La première chose à faire est de vérifier que le pod Traefik est bien relancé après la mise à jour de la configuration, et de regarder ses premiers logs pour dépister des erreurs évidentes. Ensuite, on testera l’accès au domaine en https depuis son réseau local: si tout va bien, la première fois, Traefik va retourner son certificat par défaut (qui n’est donc pas un certificat de confiance, d’où un avertissement dans le navigateur).
Mais en arrière-plan, Traefik devrait gérer la génération du certificat avec Let’s Encrypt. Si on reteste un peu plus de 2 minutes plus tard, le certificat nouvellement généré devrait être servi. Celui-ci ne sera toujours pas reconnu par le navigateur car souvenez-vous, on utilise le serveur staging de Let’s Encrypt pour notre premier test.
Une fois que ça fonctionne sur Staging, on peut mettre à jour la configuration statique de Traefik en commentant le paramètre caServer
. Il faut aussi renommer le fichier « acme-staging.json » vers « acme.json », sinon Traefik va continuer d’utiliser le certificat existant. Ainsi c’est bien le serveur officiel de Production de Let’s Encrypt qui sera utilisé pour générer un certificat reconnu par le navigateur:
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
[...]
certResolvers:
acme:
# caServer: https://acme-staging-v02.api.letsencrypt.org/directory
storage: /data/acme.json
[...]
Une fois que ça fonctionne, Traefik gère le renouvellement automatiquement (vérifié une fois par jour, avec renouvellement à moins de 30 jours).
Forcer la redirection de http vers https
On peut maintenant forcer la redirection du http vers https avec un middleware Traefik de type redirectScheme:
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: https-redirect
spec:
redirectScheme:
scheme: https
Une fois le middleware déployé, on l’utilise ainsi (en modifiant le manifeste de l’IngressRoute en http):
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: homepage-ingress-http
spec:
entryPoints:
- web
routes:
- match: Host(`homepage.eric-bml.net`)
kind: Rule
services:
- name: homepage-service
namespace: default
port: 3000
middlewares:
- name: https-redirect
Si tout fonctionne bien, le domaine en http devrait être redirigé vers https.
Quelques pistes d’investigation en cas d’erreur
En théorie, c’était censé être assez simple, car il n’y a rien de particulier à installer sur son cluster K3s existant, juste de la configuration. En pratique, trouver la configuration adéquate m’a pris un certain temps à cause de quelques complications:
- Mon réseau local est derrière un pare-feu pfSense sur lequel j’utilise le composant DNS Resolver (aka « unbound ») et toutes les requêtes DNS de mon réseau local sont capturées et traitées par ce composant. Le problème est que LEGO, le client Let’s Encrypt utilisé par Traefik, fonctionne mal dans cette configuration. Sans en être certain, je crois que le problème est lié au cache de DNS Resolver.
- Curieusement, je n’ai trouvé aucun site montrant un exemple de configuration correspondant à ma situation. Beaucoup utilisent le HTTP Challenge (HTTP-01) qui est certes beaucoup plus simple à mettre en place, mais qui requiert d’exposer ses services sur Internet. Beaucoup d’autres utilisent cert-manager qui est un outil supplémentaire à déployer, ce que je préférais éviter.
- La structure de la configuration de Traefik via son manifeste HelmChartConfig a connu de petites évolutions et la documentation officielle n’a pas fonctionné sur mon installation, qui utilise une version plus ancienne. La documentation de Traefik ne permet pas de choisir la version. De plus, la documentation de Traefik n’est elle-même pas toujours à jour ou cohérente, avec des exemples obsolètes qui ne fonctionnent pas (par exemple « delayBeforeCheck » est devenu « delayBeforeChecks », « certResolvers » est devenu « certificatesResolvers »).
Il faut donc chercher un peu sur Internet, notamment le dépôt GitHub, pour trouver la bonne structure en fonction de la version de son HelmChart (voir la commandekubectl describe pod <traefik_pod_name> -n kube-system
). A noter: K3s ne met pas à jour automatiquement le HelmChart de Traefik pour éviter de casser son installation. Dans mon cas, la version du HelmChart Traefik est 25.0, qui utilise l’image de Traefik en version 2.11.
Une petite astuce qu’on découvre vite: des fois Traefik n’est pas relancé automatiquement après la modification de sa configuration statique. Si cela se produit, c’est probablement que la nouvelle configuration n’est pas valide et contient une section inconnue. Dans d’autres cas de configuration invalide, Traefik est bien relancé mais crash en indiquant qu’une clé de configuration est inconnue. Dans ce cas, c’est que vous avez mis une propriété inconnue dans une section qui, elle, est bien connue et dont la modification a été détectée. Ce comportement m’a été utile pour rapidement comprendre que certains exemples trouvés sur Internet ne s’appliquaient pas à ma version de Traefik.
Tester LEGO en ligne de commande
Mon premier conseil est de tester le CLI LEGO manuellement, puisque c’est ce qui est utilisé par Traefik.
En testant hors de mon réseau local, j’ai très facilement pu générer mon certificat avec une commande similaire à:
$env:CF_API_EMAIL="..."
$env:CF_DNS_API_TOKEN="..."
.\lego.exe --server https://acme-staging-v02.api.letsencrypt.org/directory --email <email> --dns cloudflare -d homepage.eric-bml.net run
Pour rappel, les variables d’environnement CF_API_EMAIL
et CF_DNS_API_TOKEN
sont pour l’authentification à l’API de Cloudflare (puisque j’utilise ce provider, comme l’indique l’option de ligne de commande --dns cloudflare
), tandis que l’option de ligne de commande --email
est l’adresse e-mail transmise à Let’s Encrypt pour la génération du certificat.
La même commande dans mon réseau local ne fonctionne pas, j’ai donc fait le lien avec mon pare-feu pfSense et son composant DNS Resolver que je ne souhaite pas désactiver pour autant.
En testant d’autres options de cet outil, j’ai fini par réussir à générer le certificat depuis mon réseau local avec l’option supplémentaire --dns.propagation-wait 120s
. Cette option fait qu’au lieu de vérifier la propagation DNS de l’enregistrement TXT (ce qui ne marche pas dans mon réseau local), LEGO doit juste attendre 2 minutes avant de demander au serveur Let’s Encrypt de vérifier lui-même, ce qui réussit.
Activer les logs détaillés dans Traefik
Pour activer les logs détaillés dans le manifeste HelmChartConfig de Traefik:
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
logs:
general:
level: DEBUG
Une fois ce fichier modifié, Traefik doit être automatiquement redéployé et on doit voir beaucoup plus de logs dans le pod Traefik.
Après avoir trouvé l’option --dns.propagation-wait 120s
sur LEGO, j’ai pensé que son équivalent dans la configuration de Traefik était delayBeforeCheck: 120s
. Cela n’a pas fonctionné et les logs montraient clairement que ce nouveau paramètre était bien utilisé, mais qu’au bout de ces 2 minutes, LEGO (orchestré par Traefik) vérifiait lui-même la propagation de l’enregistrement TXT dans les DNS. Ce qui échouait depuis mon réseau local. En cherchant, j’ai trouvé que Traefik avait une option supplémentaire: disablePropagationCheck: true
à combiner avec la première option. Ces deux options ensemble font que LEGO orchestré par Traefik va attendre 2 minutes avant de demander au serveur de l’API Let’s Encrypt de vérifier lui-même, ce qui a fonctionné.