Exponiendo varios certificados bajo una misma IP en Kubernetes
En esta ocasión, vamos a aprovechar la aplicación (kubapp) que utilizamos en la introducción práctica a Kubernetes (K8s) para simular que tenemos en realidad dos aplicaciones dentro del clúster de K8s. Que la aplicación nos devuelva el hostname nos sirve para nuestro propósito actual, ya veremos por qué.
La idea, como se intuye por el título tan descriptivo del post ;), es exponer ambos servicios fuera del clúster, pero con la peculiaridad de exponer cada uno con un certificado diferente y bajo una misma IP.
Creación de los certificados
Lo primero, necesitamos crear los certificados para ambas aplicaciones, para ello, ejecutamos la siguiente instrucción dos veces, para así poder crear un certificado para cada aplicación:
# Aplicación 1
$ openssl req -newkey rsa:2048 -nodes -keyout tls1.key -x509 -days 365 -out tls1.crt -subj '/CN=kubapp1'# Aplicación 2
$ openssl req -newkey rsa:2048 -nodes -keyout tls2.key -x509 -days 365 -out tls2.crt -subj '/CN=kubapp2'
Ahora, tenemos el par de ficheros generados para cada aplicación:
.
├── tls1.crt
├── tls1.key
├── tls2.crt
└── tls2.key
El fichero crt es el certificado y el fichero key es la clave privada. Como su nombre indica, en un entorno real no debería caer en manos de quien no debe. Aquí, por simplicidad, no vamos a meternos en temas de firmas ni nada, van autofirmados y ya.
Para poder dar de alta estos dos ficheros como secretos en el clúster hay que obtener su base 64 y copiar (y salvar) cada salida:
$ cat tls1.key | base64
$ cat tls1.crt | base64
$ cat tls2.key | base64
$ cat tls2.crt | base64
Copiamos la respuesta a cada instrucción y la añadimos a un fichero YAML que será la definición del secreto en el clúster, siguiendo el ejemplo de abajo:
Ahora que tenemos nuestros secretos definidos en ficheros YAML, ya podemos darlos de alta. Aunque sólo se muestra uno, debemos tener un YAML para cada secreto. Para darlos de alta en el clúster ejecutamos la siguientes instrucciones:
$ kubectl apply -f kubapp1-secret-tls.yaml
$ kubectl apply -f kubapp2-secret-tls.yaml
Aquí no vamos a usar permisos, cuentas de servicios, etc. de forma que cada aplicación sólo tenga acceso a su secreto únicamente. En un entorno productivo, deberíamos hacerlo.
Creando el ingress
Este tipo de objetos (ingress) se encarga de gestionar el acceso a servicios del clúster. Básicamente, es un recurso de Kubernetes que le permite configurar un balanceador de carga HTTP para aplicaciones que se ejecutan en Kubernetes, representado por uno o más servicios. Es necesario para entregar esas aplicaciones a clientes fuera del clúster de Kubernetes.
Como ya tenemos los secretos en el clúster, podemos crear el ingress, que es la definición vía YAML del estado del “inyector” de tráfico en el clúster:
Hemos expuesto dos dominios, uno para cada aplicación, siguiendo el siguiente formato:
<app>.tlsexample.com
Ya podemos dar de alta el ingress con la siguiente instrucción:
kubectl apply -f ingress-tls.yaml
Instalando un Ingress Controller
Para poder llamar a nuestros servicios en el clúster desde el exterior necesitamos instalar un Ingress Controller. Es una aplicación que se ejecuta en el clúster y configura un balanceador de carga HTTP de acuerdo con los recursos del Ingress. El balanceador de carga puede ser un balanceador de carga de software que se ejecuta en el clúster o un balanceador de carga de hardware o en la nube que se ejecuta externamente. Los diferentes balanceadores de carga requieren diferentes implementaciones de controlador de entrada.
En mi caso he usado una implementación sobre NGINX. Las instrucciones se encuentran aquí. En este caso, el Ingress Controller está implementado en un pod junto a un balanceador de carga.
Para instalarlo lo más sencillo es usar Helm. Una vez instalado el cliente de Helm (y el servidor, llamado Tiller), ejecutamos los siguientes pasos:
$ git clone https://github.com/nginxinc/kubernetes-ingress/
$ cd kubernetes-ingress/deployments/helm-chart
$ helm install --name my-release .
Con esto, ya tenemos un Ingress Controller instalado. Se puede realizar una instalación avanzada siguiendo los links. O, también, se puede usar otra implementación.
Creando entradas DNS
Vamos a crear entradas en el DNS local para poder llamar a los hostname configurados. Pero antes, necesitamos conocer la IP del Ingress Controller:
$ kubectl get ingresses
NAME HOSTS ADDRESS PORTS AGE
tls-ingress kubapp1.tlsexample.com,kubapp2.tlsexample.com localhost 80, 443 11d
El campo ADDRESS tiene el valor localhost. Esa será la IP que utilizaremos:
$ sudo vim /etc/hosts
Añadimos las siguientes lineas y guardamos:
127.0.0.1 kubapp1.tlsexample.com
127.0.0.1 kubapp2.tlsexample.com
Creando las aplicaciones y exponiendo servicios
Por último, ya sólo nos queda desplegar las aplicaciones y los servicios en el clúster.
La definición de cada aplicación y su respectivo servicio es la siguiente:
Para darlos de alta, sólo hay que guardarlos en un fichero YAML cada uno (o todos en un único YAML) y desplegar:
$ kubectl apply -f kubapp1-deployment.yaml
$ kubectl apply -f kubapp2-deployment.yaml
$ kubectl apply -f kubapp1-service.yaml
$ kubectl apply -f kubapp2-service.yaml
Pruebas
Antes de probar, vamos a inspeccionar que los servicios están enlazados al Ingress:
$ kubectl describe ingress tls-ingress
Name: tls-ingress
Namespace: default
Address: localhost
Default backend: default-http-backend:80 (<none>)
TLS:
kubapp1-secret-tls terminates kubapp1.tlsexample.com
kubapp2-secret-tls terminates kubapp2.tlsexample.com
Rules:
Host Path Backends
---- ---- --------
kubapp1.tlsexample.com
/ kubapp1:5000 (10.1.0.37:5000)
kubapp2.tlsexample.com
/ kubapp2:5000 (10.1.0.38:5000)
Annotations:
kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"networking.k8s.io/v1beta1","kind":"Ingress","metadata":{"annotations":{},"name":"tls-ingress","namespace":"default"},"spec":{"rules":[{"host":"kubapp1.tlsexample.com","http":{"paths":[{"backend":{"serviceName":"kubapp1","servicePort":5000},"path":"/"}]}},{"host":"kubapp2.tlsexample.com","http":{"paths":[{"backend":{"serviceName":"kubapp2","servicePort":5000},"path":"/"}]}}],"tls":[{"hosts":["kubapp1.tlsexample.com"],"secretName":"kubapp1-secret-tls"},{"hosts":["kubapp2.tlsexample.com"],"secretName":"kubapp2-secret-tls"}]}}
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal AddedOrUpdated 35s (x3 over 17h) nginx-ingress-controller Configuration for default/tls-ingress was added or updated
En Rules podemos ver cómo efectivamente cada host tiene asignado un backend.
Vamos a lanzar peticiones a cada aplicación. Primero kubapp1:
$ curl -v -k https://kubapp1.tlsexample.com
* Rebuilt URL to: https://kubapp1.tlsexample.com/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to kubapp1.tlsexample.com (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=kubapp1
* start date: Aug 7 10:37:14 2019 GMT
* expire date: Aug 6 10:37:14 2020 GMT
* issuer: CN=kubapp1
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: kubapp1.tlsexample.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.17.2
< Date: Thu, 08 Aug 2019 07:37:24 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 35
< Connection: keep-alive
<
You've hit kubapp1-d98c9d6b-zb9hx
* Connection #0 to host kubapp1.tlsexample.com left intact
Ahora a la otra aplicación o kubapp2:
$ curl -v -k https://kubapp2.tlsexample.com
* Rebuilt URL to: https://kubapp2.tlsexample.com/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to kubapp2.tlsexample.com (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: CN=kubapp2
* start date: Aug 7 10:40:09 2019 GMT
* expire date: Aug 6 10:40:09 2020 GMT
* issuer: CN=kubapp2
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET / HTTP/1.1
> Host: kubapp2.tlsexample.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.17.2
< Date: Thu, 08 Aug 2019 07:37:14 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 36
< Connection: keep-alive
<
You've hit kubapp2-dd7ddc758-c69wr
* Connection #0 to host kubapp2.tlsexample.com left intact
Por un lado, podemos ver que en cada aplicación el certificado que devuelve el servidor para establecer la conexión es diferente, el campo CN nos ayuda.
Por otro lado, tenemos la propia respuesta del servidor, que nos devuelve el hostname de cada aplicación, en este caso, coincide con el nombre del pod.