Accéder à une ressource externe via un Ingress Controller et Certificat TLS valide
Sommaire
Pour un besoin particulier, j'ai dû chercher une solution pour accéder à du contenu d'un bucket S3 public, depuis le même domaine qu'une application tournant sur un cluster Kubernetes. Les données devant être accessibles en HTTPS avec un certificat valide.
Le bucket S3 étant chez OVHcloud, il existe des solutions pour cela, notamment celle-ci. Cependant, j'utilise Let's Encrypt pour gérer mes certificats, j'ai déjà un load balancer et tout ce qu'il faut sur mes clusters kubernetes, il y a surement un moyen de faire autrement ?
Dans cet article, je vais alors tenter de récupérer un fichier git-cheat-sheet-education.pdf
, hébergé sur un bucket S3 public, mais via une URL d'un cluster kubernetes, https://demo-ingress-s3.opsrel.io
. Voici alors une solution proposée parmi d'autres, qui utilisera les concepts suivants :
- Bucket S3 avec données accessibles publiquement
- Ingress Controller (ici ingress-nginx) sur un cluster Kubernetes
- Let's Encrypt avec la génération de certificat TLS valide
- Un nom de domaine ;)
Ci-après un schéma simple du cas d'usage :
Je ne traiterai pas ici la création d'un bucket S3 ni la gestion des ACLs. Chez OVHcloud, vous trouverez des informations ici.
Je ne traiterai pas non plus l'installation d'ingress-nginx et Let's Encrypt !
Mais du coup, de quoi vais-je traiter ? Eh bien simplement du lien ingress-nginx -> bucket S3 grâce aux ressources External Names de Kubernetes !
Vérification des pré-requis
Avant d'aller plus loin, vérifions que mon ingress controller et Let's Encrypt sont bien installés :
1$ helm list -n ingress-nginx
2NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
3ingress-nginx ingress-nginx 2 2023-10-16 21:58:04.555376832 +0200 CEST deployed ingress-nginx-4.8.2 1.9.3
4
5$ kubectl get svc ingress-nginx -n ingress-nginx
6NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
7ingress-nginx-controller-admission ClusterIP 10.43.34.243 <none> 443/TCP 5m17s
8ingress-nginx-controller LoadBalancer 10.43.12.211 74.220.23.199 80:30198/TCP,443:31639/TCP 5m17s
9
10$ helm list -n cert-manager
11NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
12cert-manager cert-manager 1 2023-10-16 22:02:52.440865101 +0200 CEST deployed cert-manager-v1.13.1 v1.13.1
13
14$ kubectl get deploy,pods -n cert-manager
15NAME READY UP-TO-DATE AVAILABLE AGE
16deployment.apps/cert-manager 1/1 1 1 79s
17deployment.apps/cert-manager-cainjector 1/1 1 1 79s
18deployment.apps/cert-manager-webhook 1/1 1 1 79s
19
20NAME READY STATUS RESTARTS AGE
21pod/cert-manager-55657857dd-fw8f2 1/1 Running 0 79s
22pod/cert-manager-cainjector-7b5b5d4786-fnbcd 1/1 Running 0 79s
23pod/cert-manager-webhook-55fb5c9c88-kdktw 1/1 Running 0 79s
24
25$ kubectl get clusterissuer
26NAME READY AGE
27letsencrypt-prod True 22s
OK, j'ai bien mon controller ingress-nginx et cert-manager qui tournent, avec une IP publique 74.220.23.199
.
Je vais utiliser l'entrée DNS demo-ingress-s3.opsrel.io
qui va pointer vers cette IP. Pour le moment, la résolution fonctionne bien, mais le certificat par défaut du cluster est présenté :
1$ curl https://demo-ingress-s3.opsrel.io -vkI
2* Trying 74.220.23.199:443...
3* Connected to demo-ingress-s3.opsrel.io (74.220.23.199) port 443 (#0)
4* [...]
5* Server certificate:
6* subject: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
7* start date: Oct 16 19:58:20 2023 GMT
8* expire date: Oct 15 19:58:20 2024 GMT
9* issuer: O=Acme Co; CN=Kubernetes Ingress Controller Fake Certificate
10* SSL certificate verify result: self-signed certificate (18), continuing anyway.
11* [...]
12<
13* Connection #0 to host demo-ingress-s3.opsrel.io left intact
Le certificat est bien celui par défaut, Kubernetes Ingress Controller Fake Certificate
, car aucune ressource n'est associée à ce nom de domaine.
Maintenant, passons à l'étape suivante, créons la ressource de type Ingress + ExternalName.
Ingress et External Name
On va maintenant configurer correctement ce qu'il faut pour récupérer le document git-cheat-sheet-education.pdf, mais via une URL à moi, avec un bon certificat ;)
Service ExternalName
Kubernetes permet d'exposer des services au sein du cluster, pour joindre des ressources. Les plus connus sont les services de type ClusterIP
ou LoadBalancer
. Dans mon cas, je vais utiliser un service un peu particulier, ExternalName
, qui permet d'aller chercher une ressource à l'extérieur du namespace où il se trouve, que ce soit dans le cluster (sur un autre ns) ou carrément en dehors du cluster (mon cas d'usage).
Avec cet ExternalName
, je pourrai donc depuis le cluster accéder à une URL externe via ce service.
Voici le manifest yaml demo-ingress-s3-upstream-svc.yaml
à appliquer :
1apiVersion: v1
2kind: Service
3metadata:
4 name: demo-ingress-s3-upstream
5spec:
6 # FQDN de mon bucket S3
7 externalName: demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net
8 type: ExternalName
1$ kubectl apply -f demo-ingress-s3-upstream-svc.yaml
2service/demo-ingress-s3-upstream created
3
4$ kubectl get svc demo-ingress-s3-upstream
5NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
6demo-ingress-s3-upstream ExternalName <none> demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net <none> 4s
Si maintenant je fais un port-forward sur ce service, je devrais pouvoir récupérer mon fichier.
1$ kubectl port-forward svc/demo-ingress-s3-upstream 80
2error: cannot attach to *v1.Service: invalid service 'demo-ingress-s3-upstream': Service is defined without a selector
Mmmm, ça ne veut pas marcher... En effet, le service n'a pas de selector et ne sait pas vers quel port/pod rediriger, ça ne fonctionnera pas ! Qu'à cela ne tienne, on va tenter depuis un pod à l'intérieur du cluster :
1$ kubectl run debug --image=nginx # on lance un conteneur nginx tout simple, pour avoir un shell ;)
2$ kubectl exec -it debug -- /bin/bash
3root@debug:/# curl http://demo-ingress-s3-upstream/git-cheat-sheet-education.pdf -I
4HTTP/1.1 412 Precondition Failed
5Content-Type: text/html; charset=UTF-8
6Content-Length: 7
7X-Trans-Id: tx7dc078f4c724468e89f50-00652ee47d
8X-Openstack-Request-Id: tx7dc078f4c724468e89f50-00652ee47d
9Date: Tue, 17 Oct 2023 19:46:05 GMT
10X-IPLB-Request-ID: 4ADC14C0:8FA4_5762BBC9:0050_652EE47D_5A23D18:1D243
11X-IPLB-Instance: 48126
Ça ne fonctionne toujours pas... C'est parce que le serveur en face s'attend à avoir une requête vers le vhost du bucket, et pas demo-ingress-s3-upstream
. On retente en spécifiant le vhost :
1root@debug:/# curl http://demo-ingress-s3-upstream/git-cheat-sheet-education.pdf -H "Host: demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net" -I
2HTTP/1.1 200 OK
3Content-Type: application/pdf
4Etag: 1fcf70393d2aed298ef2c521126e8cd3
5Last-Modified: Tue, 17 Oct 2023 19:23:45 GMT
6X-Timestamp: 1697570624.72872
7Accept-Ranges: bytes
8Content-Length: 100194
9X-Trans-Id: tx74195141890c422cba15c-00652ee50b
10X-Openstack-Request-Id: tx74195141890c422cba15c-00652ee50b
11Date: Tue, 17 Oct 2023 19:48:27 GMT
12X-IPLB-Request-ID: 4ADC14C0:7995_3626E64B:0050_652EE50B_4400B4E:71FE
13X-IPLB-Instance: 33617
Ok ! On a bien un 200 OK
, essayant de télécharger mon PDF (Content-Type: application/pdf
).
Par contre, si j'essaie de récupérer le document en HTTPS, j'ai l'erreur suivante, ce qui est normal car le certificat présenté ne matche pas avec l'URL demandée :
1root@debug:/# curl https://demo-ingress-s3-upstream/git-cheat-sheet-education.pdf -I
2curl: (60) SSL: no alternative certificate subject name matches target host name 'demo-ingress-s3-upstream'
Ingress
Maintenant, reste à paramétrer l'ingress pour faire le lien depuis l'extérieur, vers ce service spécifique.
On crée une ressource de type ingress, demo-ingress-s3-upstream-ingress.yaml
, avec les informations suivantes :
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 annotations:
5 # Ressource cert-manager pour demander des certificat Let's Encrypt, non traité ici
6 cert-manager.io/cluster-issuer: letsencrypt-prod
7 # On définit le vhost pour éviter le problème vu plus haut
8 nginx.ingress.kubernetes.io/upstream-vhost: demo-ingress-s3.auth-2bf97a4bab8c4d16969513edf37fcc15.storage.gra.cloud.ovh.net
9 name: demo-ingress-s3-upstream
10spec:
11 ingressClassName: nginx
12 rules:
13 - host: demo-ingress-s3.opsrel.io
14 http:
15 paths:
16 - backend:
17 service:
18 # On pointe vers le service externalName définit plus haut
19 name: demo-ingress-s3-upstream
20 port:
21 # Par défaut, le lien upstream se fait en HTTP, port 80
22 number: 80
23 path: /
24 pathType: ImplementationSpecific
25 tls:
26 - hosts:
27 - demo-ingress-s3.opsrel.io
28 # On stocke le certificat TLS dans le secret associé
29 secretName: demo-ingress-s3-cert
On applique le manifest et on vérifie que tout est bon :
1$ kubectl apply -f demo-ingress-s3-upstream-ingress.yaml
2ingress.networking.k8s.io/demo-ingress-s3-upstream created
3
4$ kubectl get ingress,certificate
5NAME CLASS HOSTS ADDRESS PORTS AGE
6ingress.networking.k8s.io/demo-ingress-s3-upstream nginx demo-ingress-s3.opsrel.io 74.220.23.199 80, 443 2m19s
7
8NAME READY SECRET AGE
9certificate.cert-manager.io/demo-ingress-s3-cert True demo-ingress-s3-cert 2m19s
Ok, reste plus qu'à tester :
1$ curl https://demo-ingress-s3.opsrel.io/git-cheat-sheet-education.pdf -vI
2* Trying 74.220.23.199:443...
3* Connected to demo-ingress-s3.opsrel.io (74.220.23.199) port 443 (#0)
4* [...]
5* Server certificate:
6* subject: CN=demo-ingress-s3.opsrel.io
7* start date: Oct 17 18:53:20 2023 GMT
8* expire date: Jan 15 18:53:19 2024 GMT
9* subjectAltName: host "demo-ingress-s3.opsrel.io" matched cert's "demo-ingress-s3.opsrel.io"
10* issuer: C=US; O=Let's Encrypt; CN=R3
11* SSL certificate verify ok.
12* [...]
13< HTTP/2 200
14HTTP/2 200
15< date: Tue, 17 Oct 2023 19:59:38 GMT
16date: Tue, 17 Oct 2023 19:59:38 GM
17< content-type: application/pdf
18content-type: application/pdf
19< content-length: 100194
20content-length: 100194
21[...]
Cool ! Le document peut être récupéré, en HTTPS, depuis un domaine que je gère depuis mon cluster !
Vous aurez remarqué que la requête entre mon poste et l'ingress est en HTTPS, mais depuis mon cluster vers le bucket S3 on reste en HTTP (port.number: 80
). Pour faire propre, on met à jour la ressource ingress pour gérer le backend AUSSI en HTTPS :
1piVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 annotations:
5 nginx.ingress.kubernetes.io/backend-protocol: HTTPS
6[...]
7spec:
8 rules:
9 - host: demo-ingress-s3.opsrel.io
10 http:
11 paths:
12 - backend:
13 service:
14 name: demo-ingress-s3-upstream
15 port:
16 number: 443
17[...]
Et voilà, Avec 2 ressources dans Kubernetes, vous pouvez maintenant récupérer des documents depuis un bucket S3 (ou tout autre document externe) avec vos propres URLs et certificats TLS.
Notes
Références
Lors de la rédaction de l'article, des confrères de Zenika ont sorti un article sur des possibilités de configuration de l'ingress-controller nginx. N'hésitez pas à aller le lire, c'est ici.
CIVO
En tant que "developer advocate" Civo, je les remercie de me permettre d'utiliser leurs produits, notamment leur offre de service de cluster k8s managé, utilisée pour cet article.