Mutual TLS (mTLS)
mTLS (Mutual TLS) é uma versão do TLS onde não apenas o servidor autentica o cliente, mas o cliente também autentica o servidor. Isso é feito por meio da troca mútua de certificados digitais durante a handshake TLS.
Como funciona o mTLS:
Conexão TLS padrão
: Autenticação do servidor: No TLS tradicional, o cliente verifica a identidade do servidor com base no certificado digital apresentado pelo servidor. Isso garante que o cliente esteja se conectando ao servidor correto e que a comunicação seja criptografada.
Autenticação mútua (mTLS)
: Além da verificação do servidor, o cliente também apresenta um certificado ao servidor. O servidor, por sua vez, verifica a autenticidade desse certificado. É uma autenticação bilateral.
Durante o handshake, ambos os lados (cliente e servidor) trocam certificados e verificam a validade um do outro. Se ambos os certificados forem válidos e confiáveis, a conexão é estabelecida.
No Kubernetes em uma situação normal qualquer pod consegue se comunicar com os outros pods sem criptografia. Isso é garantido pela CNI. Um Ingress que provavelmente esta protegido por https, recebe uma request e encaminhará para os pods dentro do cluster. Geralmente o TLS termina no ingress que descriptografa a requisição e encaminha para os pods sem nenhuma criptografia.
Se um invasor dentro do nosso cluster conseguir algum privilégio dentro de um pod é possível que consiga escutar a porta de comunicação deste pod ou algum outro e os dados não criptografados poderão ser lidos.
Usando o mTLS podemos criptografar a comunicação entre os pods e cada pod terá condição de criptografar e descriptografar o tráfego.
Por que usar mTLS
Garantir que tanto o cliente quanto o servidor são quem dizem ser garantindo uma segurança robusta evitando ataques man-in-the-middle (MITM).
Além da criptografia, mTLS pode ser usado para autenticação e controle de acesso (autorização) em redes e sistemas distribuídos, como APIs, microserviços e ambientes de zero trust.
Aplicações Comuns
-
Em arquiteturas de microsserviços, mTLS pode ser usado para garantir que somente serviços autenticados possam se comunicar entre si.
-
Para proteger APIs e garantir que somente clientes legítimos possam acessá-las.
-
Em um modelo de segurança zero trust, onde cada comunicação dentro da rede precisa ser autenticada e verificada, o mTLS é uma escolha natural.
Implementação
O mTLS requer configuração de infraestrutura, como geração e gestão de certificados para clientes e servidores, configuração de servidores para exigir e verificar certificados de clientes, e utilização de uma Autoridade Certificadora (CA) para emitir e revogar certificados. Por questões de segurança não devemos criar certificados de longo prazo (10 anos) sendo necessário uma rotação, o que causa bastante trabalho para gerenciar. O ideal é criar certificados com curta duração e alta rotatividade mas que seja feita de forma automática.
Essa seria a teoria.
Mas também poderíamos criar somente um certificado para cada contêiner no pod para tornar o processo mais simples inicialmente. Esse certificado poderia atuar como client e server.
Para fazer isso com menos esforço podemos utilizar um sidecar dentro de cada pod atuando como um proxy e este será responsável pelos certificados mtls e o tráfego de entrada/saída de rede. Desta forma isolamos a parte lógica da aplicação para que ela se concentre no que ela tem que fazer não necessitando saber nada sobre certificados e podendo usar http normalmente. Esse sidecar/proxy irá criptografar e descriptografar o tráfego da aplicação automaticamente.
Esse contêiner proxy/sidecar deveria ser injetado automaticamente no contêiner toda vez que for criado e gerenciado externamente por outra aplicação. Essa aplicação seria responsável por gerenciar o CA e os certificados bem como fazer a rotação dos mesmos.
Essa aplicação externa poderia ser o Istio, linkerd ou qualquer uma outra. É exatamente dessa maneira que funciona o Istio.
Se observarmos o contêiner da aplicação, ele não se comunicar diretamente com outros pods, indo direto para o proxy. Esta forma evita o ataque MITM.
Como executar isso?
- Criar uma regra no iptables para rotear todo o tráfego para o proxy durante a criação do pod usando o init contêiner. Somente depois desta configuração ser aplicada os contêineres do pod devem subir.
- Esse init contêiner precisará da capability NET_ADMIN para ter permissão para fazer isso.
- Subir o sidecar
- Subir a aplicação
Vamos tentar fazer mostrar isso manualmente sem ajuda do Istio até onde fica fácil. Vamos fazer uma aplicação que fique pingando o google e depois ir adicionando os steps necessário para que um proxy faça o tráfego.
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
Se fossemos colocar um proxy precisamos de outro contêiner que precisa de ter o iptables instalado. O certo seria buidar uma imagem com todos os itens necessário, mas vamos fazer isso em tempo de execução para ficar mais fácil ver os steps. Vamos colocar outro contêiner com o iptables instalado e rodar um comando para ver se esta 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
# Retirado parte do conteúdo para facilitar a leitura
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 que 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)
...
# Retirado parte do conteúdo para facilitar a leitura
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 precisamos ser root mas na verdade o root precisa de uma permissão para trabalhar com networks. Vamos adicionar essa 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
# Aqui o 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)
Agora podemos implementar o iptables para colocar novas regras. Isso funciona porque todos os contêineres dentro do pod usam o mesmo kernel network namespace.
Agora seria só implementar as regras do iptables contêiner proxy.