Mutual TLS (mTLS)
mTLS (Mutual TLS) es una versión de TLS donde no solo el servidor autentica al cliente, sino que el cliente también autentica al servidor. Esto se hace mediante el intercambio mutuo de certificados digitales durante el handshake TLS.
Cómo funciona el mTLS:
Conexión TLS estándar: Autenticación del servidor: En el TLS tradicional, el cliente verifica la identidad del servidor basándose en el certificado digital presentado por el servidor. Esto garantiza que el cliente se esté conectando al servidor correcto y que la comunicación esté cifrada.
Autenticación mutua (mTLS): Además de la verificación del servidor, el cliente también presenta un certificado al servidor. El servidor, a su vez, verifica la autenticidad de ese certificado. Es una autenticación bilateral.
Durante el handshake, ambos lados (cliente y servidor) intercambian certificados y verifican la validez del otro. Si ambos certificados son válidos y confiables, se establece la conexión.
En Kubernetes en una situación normal cualquier pod consigue comunicarse con los otros pods sin cifrado. Esto es garantizado por el CNI. Un Ingress que probablemente está protegido por https, recibe una petición y la redirige a los pods dentro del clúster. Generalmente el TLS termina en el ingress que descifra la petición y la reenvía a los pods sin ningún cifrado.

Si un invasor dentro de nuestro clúster consigue algún privilegio dentro de un pod es posible que consiga escuchar el puerto de comunicación de este pod o algún otro y los datos no cifrados podrán ser leídos.
Usando mTLS podemos cifrar la comunicación entre los pods y cada pod tendrá capacidad para cifrar y descifrar el tráfico.

Por qué usar mTLS
Garantizar que tanto el cliente como el servidor son quienes dicen ser, garantizando una seguridad robusta evitando ataques man-in-the-middle (MITM).
Además del cifrado, mTLS puede usarse para autenticación y control de acceso (autorización) en redes y sistemas distribuidos, como APIs, microservicios y entornos de zero trust.
Aplicaciones Comunes
-
En arquitecturas de microservicios, mTLS puede usarse para garantizar que solo servicios autenticados puedan comunicarse entre sí.
-
Para proteger APIs y garantizar que solo clientes legítimos puedan acceder a ellas.
-
En un modelo de seguridad zero trust, donde cada comunicación dentro de la red necesita ser autenticada y verificada, el mTLS es una elección natural.
Implementación
El mTLS requiere configuración de infraestructura, como generación y gestión de certificados para clientes y servidores, configuración de servidores para exigir y verificar certificados de clientes, y utilización de una Autoridad Certificadora (CA) para emitir y revocar certificados. Por cuestiones de seguridad no debemos crear certificados de larga duración (10 años), siendo necesaria una rotación, lo que causa bastante trabajo para gestionar. Lo ideal es crear certificados con corta duración y alta rotación pero que se haga de forma automática.
Esta sería la teoría.

Pero también podríamos crear solo un certificado para cada contenedor en el pod para hacer el proceso más simple inicialmente. Este certificado podría actuar como cliente y servidor.

Para hacer esto con menos esfuerzo podemos utilizar un sidecar dentro de cada pod actuando como un proxy y este será responsable de los certificados mtls y del tráfico de entrada/salida de red. De esta forma aislamos la parte lógica de la aplicación para que se concentre en lo que tiene que hacer sin necesidad de saber nada sobre certificados y pudiendo usar http normalmente. Este sidecar/proxy cifrará y descifrará el tráfico de la aplicación automáticamente.
Este contenedor proxy/sidecar debería ser inyectado automáticamente en el contenedor cada vez que se cree y gestionado externamente por otra aplicación. Esta aplicación sería responsable de gestionar el CA y los certificados, así como de hacer la rotación de los mismos.
Esta aplicación externa podría ser Istio, linkerd o cualquier otra. Es exactamente de esta manera que funciona Istio.

Si observamos el contenedor de la aplicación, este no se comunica directamente con otros pods, va directamente al proxy. Esta forma evita el ataque MITM.
¿Cómo ejecutar esto?
- Crear una regla en iptables para enrutar todo el tráfico hacia el proxy durante la creación del pod usando el init container. Solo después de que esta configuración se aplique los contenedores del pod deben iniciarse.
- Este init container necesitará la capability NET_ADMIN para tener permiso para hacer esto.
- Iniciar el sidecar
- Iniciar la aplicación
Vamos a intentar mostrar esto manualmente sin ayuda de Istio hasta donde sea fácil. Vamos a hacer una aplicación que haga ping a google y después ir añadiendo los pasos necesarios para que un proxy haga el tráfico.
root@cks-master:~# k run app --image=bash --command -oyaml --dry-run=client -- sh -c 'ping google.com' > app.yaml
root@cks-master:~# cat app.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: app
name: app
spec:
containers:
- command:
- sh
- -c
- ping google.com
image: bash
name: app
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
root@cks-master:~# k apply -f app.yaml
pod/app created
root@cks-master:~# k logs app
PING google.com (74.125.126.100): 56 data bytes
64 bytes from 74.125.126.100: seq=0 ttl=117 time=2.892 ms
64 bytes from 74.125.126.100: seq=1 ttl=117 time=0.881 ms
64 bytes from 74.125.126.100: seq=2 ttl=117 time=0.934 ms
64 bytes from 74.125.126.100: seq=3 ttl=117 time=0.612 ms
64 bytes from 74.125.126.100: seq=4 ttl=117 time=0.695 ms
Si fuéramos a poner un proxy necesitamos otro contenedor que tenga iptables instalado. Lo correcto sería construir una imagen con todos los elementos necesarios, pero vamos a hacer esto en tiempo de ejecución para que sea más fácil ver los pasos. Vamos a poner otro contenedor con iptables instalado y ejecutar un comando para ver si está funcionando.
root@cks-master:~# vim app.yaml
root@cks-master:~# cat app.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: app
name: app
spec:
containers:
- command:
- sh
- -c
- ping google.com
image: bash
name: app
resources: {}
- command:
- sh
- -c
- 'apt update && apt install iptables -y && iptables -L && sleep 1d'
image: ubuntu
name: proxy
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
root@cks-master:~# k apply -f app.yaml
pod/app created
root@cks-master:~# k get pod app
NAME READY STATUS RESTARTS AGE
app 1/2 Error 1 (21s ago) 30s
# Contenido parcialmente eliminado para facilitar la lectura
root@cks-master:~# k describe pod app
Name: app
Namespace: default
...
Containers:
app:
Container ID: containerd://7197c63caa13f83e570b31efdbf0adf66bcfc843b061a1b481167e804c202f49
Image: bash
Image ID: docker.io/library/bash@sha256:05de6634ac35e4ac2edcb1af21889cec8afcc3798b11a9d538a6f0c315608c48
Port: <none>
Host Port: <none>
Command:
sh
-c
ping google.com
State: Running
Started: Fri, 30 Aug 2024 00:16:24 +0000
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-k9c6f (ro)
proxy:
Container ID: containerd://d364ccd908db1fc5b827e3613110b22b83b918081a67bcec0e0b46f9e5e6b922
Image: ubuntu
Image ID: docker.io/library/ubuntu@sha256:8a37d68f4f73ebf3d4efafbcf66379bf3728902a8038616808f04e34a9ab63ee
Port: <none>
Host Port: <none>
Command:
sh
-c
apt update && apt install iptables -y && iptables -L && sleep 1d
State: Running
Started: Fri, 30 Aug 2024 00:16:57 +0000
Last State: Terminated
Reason: Error # <<<< ¿Por qué será?
Exit Code: 4
Started: Fri, 30 Aug 2024 00:16:33 +0000
Finished: Fri, 30 Aug 2024 00:16:42 +0000
Ready: True
Restart Count: 2
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-k9c6f (ro)
...
# Contenido parcialmente eliminado para facilitar la lectura
root@cks-master:~# k logs app -c proxy
...
Processing triggers for libc-bin (2.39-0ubuntu8.2) ...
iptables v1.8.10 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)
Vimos que necesitamos ser root, pero en realidad el root necesita un permiso para trabajar con redes. Vamos a añadir esa capability.
root@cks-master:~# k delete pod app --force --grace-period 0
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "app" force deleted
root@cks-master:~# vim app.yaml
root@cks-master:~# cat app.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: app
name: app
spec:
containers:
- command:
- sh
- -c
- ping google.com
image: bash
name: app
resources: {}
- command:
- sh
- -c
- 'apt update && apt install iptables -y && iptables -L && sleep 1d'
image: ubuntu
name: proxy
securityContext:
capabilities:
add: ["NET_ADMIN"]
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
root@cks-master:~# k apply -f app.yaml
pod/app created
root@cks-master:~# k get pods
NAME READY STATUS RESTARTS AGE
app 2/2 Running 0 6s
# Aquí el comando iptables -L
root@cks-master:~# k logs app -c proxy --tail=10
update-alternatives: using /usr/sbin/ebtables-nft to provide /usr/sbin/ebtables (ebtables) in auto mode
Processing triggers for libc-bin (2.39-0ubuntu8.2) ...
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
Ahora podemos implementar iptables para añadir nuevas reglas. Esto funciona porque todos los contenedores dentro del pod usan el mismo kernel network namespace.
Ahora solo sería implementar las reglas de iptables en el contenedor proxy.