K3s + Nginx Proxy Manager : résoudre le conflit de double certificat TLS
Quand vous déployez une app Kubernetes derrière Nginx Proxy Manager, le challenge ACME HTTP-01 de cert-manager échoue silencieusement — parce que NPM intercepte le port 80 avant que Traefik ne le voit jamais. Voici la cause racine et la solution propre.
Le contexte
Vous avez un cluster K3s self-hosted. Nginx Proxy Manager (NPM) tourne comme conteneur Docker et gère tout le trafic entrant sur les ports 80 et 443. Votre app vit dans K3s, exposée via Traefik comme ingress controller.
Vous ajoutez cert-manager avec un ClusterIssuer pointant sur Let's Encrypt, annotez votre ingress, lancez kubectl apply. Le pod est healthy. Mais le site affiche 404 Not Found — et le certificat TLS ne se crée jamais.
Diagnostic : où atterrit vraiment le port 80 ?
Le premier réflexe : vérifier ce qui écoute.
ss -tlnp | grep ':80'
Vous obtenez quelque chose comme :
LISTEN 0 4096 0.0.0.0:80 0.0.0.0:*
Puis identifiez le process qui l'occupe :
ps aux | grep docker-proxy
root 1476001 docker-proxy -host-port 80 -container-ip 172.18.0.7 ...
NPM est un conteneur Docker avec ports: ["80:80", "443:443"]. C'est lui qui bind les ports 80 et 443 sur l'hôte — pas Traefik.
Traefik, dans une installation K3s standard, est exposé en NodePort :
kubectl get svc -A | grep traefik
traefik NodePort 10.43.147.1 9000:30900/TCP,8080:30080/TCP,8443:30443/TCP
Traefik n'écoute que sur 30080 (HTTP) et 30443 (HTTPS) côté hôte. Il ne reçoit jamais le trafic extérieur sur le port 80.
La cause racine
Le challenge HTTP-01 de cert-manager fonctionne ainsi :
- cert-manager crée un pod temporaire et un ingress solver dans K3s
- Il demande à Let's Encrypt de valider :
http://mondomaine.com/.well-known/acme-challenge/<token> - Let's Encrypt tape votre serveur sur le port 80
Le problème : cette requête arrive sur NPM, qui n'a aucun proxy host configuré pour votre domaine. NPM répond 404. Let's Encrypt rejette le challenge. Le certificat n'est jamais émis.
Vous pouvez le confirmer directement :
curl -v http://mondomaine.com/.well-known/acme-challenge/sometoken
Les headers de réponse montreront Server: openresty — c'est le nginx de NPM, pas Traefik.
Pendant ce temps, dans le cluster, les logs cert-manager indiquent :
Waiting for HTTP-01 challenge propagation: wrong status code '404', expected '200'
Le problème du double TLS
L'annotation traefik.ingress.kubernetes.io/router.tls: "true" combinée à entrypoints: web,websecure pousse Traefik à vouloir gérer le HTTPS lui-même. Mais :
- Le certificat TLS n'est pas émis (challenge échoué)
- Même s'il l'était, le trafic HTTPS arrive chez NPM sur le port 443 — pas sur le NodePort 30443 de Traefik
Vous vous retrouvez avec deux systèmes TLS concurrents :
- NPM : prêt à gérer le SSL pour votre domaine, mais sans proxy host configuré
- cert-manager : essaie d'émettre un certificat qu'il ne peut jamais obtenir car le port 80 appartient à NPM
Aucun ne fonctionne. D'où le 404.
La solution
L'architecture doit être :
Internet → NPM (port 80/443, SSL géré par NPM)
↓
Traefik NodePort 30080 (HTTP simple)
↓
Pod de votre app
NPM est le point de terminaison SSL. K3s/Traefik n'a besoin de gérer que le routage HTTP interne.
Étape 1 — Nettoyer l'ingress K8s
Supprimez les annotations cert-manager et la section TLS. Dites à Traefik d'utiliser uniquement l'entrypoint web :
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: monapp-ingress
namespace: monapp
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
ingressClassName: traefik
rules:
- host: mondomaine.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: monapp-web
port:
number: 80
Appliquez et nettoyez les ressources cert-manager cassées :
kubectl apply -f k8s/ingress.yaml
kubectl delete certificate,certificaterequest,challenge --all -n monapp
Étape 2 — Vérifier que Traefik route correctement
Testez le routage en interne avant de toucher NPM :
curl -H "Host: mondomaine.com" http://127.0.0.1:30080/
# attendu : 200 ou 307 (redirect vers /fr ou /home)
Si vous obtenez 200, Traefik route correctement.
Étape 3 — Configurer NPM
Dans le panel admin de NPM (port 81), ajoutez un nouveau Proxy Host :
- Domain Names :
mondomaine.com - Scheme :
http - Forward Hostname / IP :
127.0.0.1 - Forward Port :
30080 - Onglet SSL : Demandez un certificat Let's Encrypt, activez "Force SSL"
NPM possède le port 80, donc son challenge ACME HTTP-01 réussit du premier coup. Le certificat est émis, le HTTPS fonctionne, et le trafic transite proprement dans la chaîne.
Pourquoi ne pas garder cert-manager ?
cert-manager est le bon outil quand Traefik est exposé directement sur les ports 80/443 — typiquement dans un setup K3s bare-metal sans reverse proxy au niveau de l'hôte. Dès que vous ajoutez NPM en amont, cert-manager perd l'accès direct au port 80 et devient redondant : NPM gère déjà les certificats Let's Encrypt nativement.
Règle d'or : un seul système doit posséder le port 80 sur un hôte. Si c'est NPM, c'est NPM qui gère le TLS. Si c'est Traefik directement, c'est cert-manager qui gère le TLS.
Points clés à retenir
Server: openrestydans une réponse curl signifie que c'est NPM qui a répondu, pas Traefik- Vérifiez ce qui occupe le port 80 avec
ss -tlnpavant de déboguer les certificats - Le challenge HTTP-01 de cert-manager nécessite un accès direct au port 80 — NPM le bloque silencieusement
- La correction est architecturale : supprimez le TLS de K8s, laissez NPM terminer le SSL, proxy vers le NodePort Traefik
- cert-manager et NPM sont redondants — choisissez une seule stratégie TLS par hôte