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 :

  1. Bucket S3 avec données accessibles publiquement
  2. Ingress Controller (ici ingress-nginx) sur un cluster Kubernetes
  3. Let's Encrypt avec la génération de certificat TLS valide
  4. Un nom de domaine ;)

Ci-après un schéma simple du cas d'usage :

Requête HTTP via Ingress Controller et service ExternalName

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.

Logo CIVO