Kubernetes et gestion des secrets dans Vault : comment faciliter l'intégration à vos workloads grâce aux Vault agents

Rédigé par Mohamed Reda LARBI YOUCEF le 4/03/2024
Temps de lecture: 8 minutes

1. Introduction

Nous avons vu dans un précédent article une vue d’ensemble de l’outil Vault. Dans notre contexte actuel, Kubernetes est l’orchestrateur phare du marché. Une problématique qui se pose est de savoir comment récupérer des secrets stockés sur Vault pour être consommés par des workloads Kubernetes. C’est ce que nous allons voir dans cet article, à travers les Vault Agents.

2. Prérequis

Dans le cadre de cet article, j’utilise les outils suivants :

  • Minikube
  • Kubectl
  • Helm
  • Vault

Pour cet article, nous allons donc déployer Vault en dehors du cluster Minikube (Ce qui est une bonne pratique). Une fois déployé, nous devons définir les variables permettant de se connecter à ce dernier (Avec le root token fourni au démarrage) :

1$ vault server -dev -dev-listen-address="0.0.0.0:8200"
2
3$ export VAULT_ADDR='http://0.0.0.0:8200'
4$ export VAULT_TOKEN="XXXXXXXXXXXXXXXXXXXXXXXXXXX"

Petite précision, nous faisons en sorte que Vault écoute sur toutes les IPs de la machine, afin de le rendre joignable depuis le cluster Kubernetes (localhost ne fonctionnera pas depuis un pod du cluster).

Cela étant fait, nous avons un serveur Vault à notre disposition.

3. Principe des Vault Agents

Les Vault agents fonctionnent en suivant le pattern des sidecars. Pour comprendre son fonctionnement, prenons un exemple. Nous avons un pod contenant une application, et nous souhaitons que cette application récupère sur Vault les secrets dont elle a besoin. Un agent Vault est un container supplémentaire ajouté à notre pod qui va se charger de récupérer les secrets. Étant donné que les volumes sont partagés par les containers au sein d’un pod, l’application peut accéder aux secrets, sans nécessité de la modifier. L’application n’est même pas au courant de l’existence de Vault. Pour aller plus loin, ci-dessous un schéma qui illustre cet exemple :

Vault Agent Principe
Principe des Vault Agents

Pour que cela fonctionne, le container Vault Agent doit pouvoir s’authentifier auprès de Vault. Celui-ci supporte l’authentification via un cluster Kubernetes. L’idée est de pouvoir s’authentifier avec des comptes de service, qui seront associés aux pods et permettent à ces derniers de récupérer les secrets. Côté Vault, nous pouvons également gérer les autorisations avec les rôles et les policies. On pourrait donc isoler les comptes de service par applications, et leur donner accès uniquement aux secrets dont elles ont besoin. Voici un schéma illustrant le processus :

Vault Agent Processus
Fonctionnement des Vault Agent

Pour ajouter, automatiquement et à chaud, le conteneur Vault Agent, nous nous appuyons sur le Vault Agent Injector. Ce dernier contacte l’API Kubernetes pour modifier la spécification de pods existants pour y ajouter un container Vault Agent, le tout en se basant sur des annotations spécifiques, ajoutées préalablement sur les pods concernés. Voici un schéma illustrant ce fonctionnement :

Vault Agent Injector
Vault Agent Injector

4. Vault Agents en pratique

Voyons cela en pratique. Nous allons déployer une application sur notre cluster Minikube, et y injecter un container Vault Agent via l’injecteur, afin que celui-ci mette à sa disposition un secret que nous aurons créé au préalable sur Vault.

Commençons par l’installation du Vault Agent injector. Celui-ci s’installe avec un helm chart. Nous devons indiquer en paramètre l’ip de notre serveur Vault. Dans mon cas, Minikube fournit un hostname permettant de joindre des ressources déployées sur la machine host depuis Minikube : host.minikube.internal

Faisons donc l’installation du Vault Agent Injector :

1$ helm repo add hashicorp https://helm.releases.hashicorp.com
2$ helm repo update
3$ helm install vault hashicorp/vault --set "global.externalVaultAddr=http://host.minikube.internal:8200"

Ceci nous déploie le Vault Agent Injector, ainsi qu’un compte de service qui sera utilisé par Vault pour valider d’autres tokens de comptes de service à l’aide de l’API TokenReview de Kubernetes. Nous pouvons les voir via les commandes suivantes :

1$ kubectl get pods
2
3NAME                                    READY   STATUS    RESTARTS   AGE
4vault-agent-injector-64cdc5cb5d-qtmnc   1/1     Running   0          1m
5
6$ kubectl get serviceaccount vault
7
8NAME    SECRETS   AGE
9vault   0         28m

Depuis la version 1.24 de Kubernetes, nous devons créer un token manuellement pour ce compte de service. Faisons cela :

 1$ kubectl apply -f - <<EOF
 2apiVersion: v1
 3kind: Secret
 4metadata:
 5  name: vault-token
 6  annotations:
 7    kubernetes.io/service-account.name: vault
 8type: kubernetes.io/service-account-token
 9EOF
10
11$ kubectl describe serviceaccount vault
12 
13Name:                vault
14Namespace:           default
15Labels:              app.kubernetes.io/instance=vault
16                     app.kubernetes.io/managed-by=Helm
17                     app.kubernetes.io/name=vault
18                     helm.sh/chart=vault-0.27.0
19Annotations:         meta.helm.sh/release-name: vault
20                     meta.helm.sh/release-namespace: default
21Image pull secrets:  <none>
22Mountable secrets:   <none>
23Tokens:              vault-token
24Events:              <none>

Nous allons maintenant permettre à Kubernetes de s’authentifier auprès de Vault. Pour cela, comme nous l’avons vu dans un précédent article, il faut activer une nouvelle méthode d’authentification. Celle-ci sera de type Kubernetes :

1$ vault auth enable kubernetes
2$ vault auth list
3
4Path           Type          Accessor                    Description                Version
5----           ----          --------                    -----------                -------
6kubernetes/    kubernetes    auth_kubernetes_b07cfe4c    n/a                        n/a
7token/         token         auth_token_03d67951         token based credentials    n/a

Une fois notre méthode d’authentification créée, il faut la configurer en lui fournissant l’url de l’API Kubernetes, ainsi que le certificat permettant de s’authentifier. Nous lui fournissons également le token du compte de service créé par le Helm Chart pour que Vault puisse appeler l’API TokenReview. Ces informations sont visibles dans le kubeconfig. Configurons notre méthode d’authentification :

 1$ TOKEN_REVIEW_JWT=$(kubectl get secret vault-token --output='go-template={{ .data.token }}' | base64 --decode)
 2$ KUBE_CA_CERT=$(kubectl config view --raw --minify --flatten --output='jsonpath={.clusters[].cluster.certificate-authority-data}' | base64 --decode)
 3$ KUBE_HOST=$(kubectl config view --raw --minify --flatten --output='jsonpath={.clusters[].cluster.server}')
 4$ vault write auth/kubernetes/config \
 5     token_reviewer_jwt="$TOKEN_REVIEW_JWT" \
 6     kubernetes_host="$KUBE_HOST" \
 7     kubernetes_ca_cert="$KUBE_CA_CERT" \
 8     issuer="https://kubernetes.default.svc.cluster.local"
 9
10Success! Data written to: auth/kubernetes/config

Notre authentification Kubernetes est configurée. Nous allons maintenant pouvoir créer un secret et le récupérer depuis un pod.

1$ vault kv put secret/demo-app/config \
2      username='demo-user' \
3      password='demo-p@ssw0rd!' \
4      ttl='30s'

Créons le namespace où notre application va être déployée, ainsi que le compte de service associé :

1$ kubectl create namespace demo
2
3namespace/demo created
1$ kubectl create serviceaccount demo-app -n demo
2
3serviceaccount/demo-app created
 1$ kubectl apply -f - <<EOF
 2apiVersion: v1
 3kind: Secret
 4metadata:
 5  name: vault-demo-app-token
 6  namespace: demo
 7  annotations:
 8    kubernetes.io/service-account.name: demo-app
 9type: kubernetes.io/service-account-token
10EOF
11
12secret/vault-demo-app-token created

Nous devons maintenant autoriser ce compte de service à accéder au secret dans Vault. Pour cela, nous allons d’abord créer une policy :

1$ vault policy write demo-app-policy - <<EOF
2path "secret/data/demo-app/*" {
3    capabilities = ["read"]
4}
5EOF
6
7Success! Uploaded policy: demo-app-policy

Une fois la policy créée, nous allons créer un role auquel nous allons attacher la policy, ainsi que le namespace et le compte de service que nous avons créé :

1$ vault write auth/kubernetes/role/demo-app \
2      bound_service_account_names=demo-app \
3      bound_service_account_namespaces=demo \
4      policies=demo-app-policy \
5      ttl=24h
6
7Success! Data written to: auth/kubernetes/role/demo-app

Tout est en place, il ne nous reste plus qu’à déployer une application. Nous allons déployer un container python et lui permettre de récupérer le secret que nous avons créé dans Vault, à l’aide d’annotations :

 1$ kubectl apply -f - <<EOF
 2apiVersion: apps/v1
 3kind: Deployment
 4metadata:
 5  name: demo-app
 6  namespace: demo
 7  labels:
 8    app: demo-app
 9spec:
10  selector:
11    matchLabels:
12      app: demo-app
13  replicas: 1
14  template:
15    metadata:
16      annotations:
17        vault.hashicorp.com/agent-inject: 'true'
18        vault.hashicorp.com/role: 'demo-app'
19        vault.hashicorp.com/agent-inject-secret-config.ini: 'secret/data/demo-app/config'
20      labels:
21        app: demo-app
22    spec:
23      serviceAccountName: demo-app
24      containers:
25        - name: demo-app
26          image: python:3.11
27          command: ["tail"]
28          args: ["-f"]
29EOF

L’annotation agent-inject permet de dire à l’injecteur d’injecter un nouveau container Vault Agent à ce pod. L’annotation role spécifie le role avec lequel le Vault Agent s’authentifie auprès de Vault et enfin l’annotation agent-inject-secret-config-ini permet de spécifier le secret à récupérer, ainsi que le fichier sur lequel le secret sera écrit.

Nous allons maintenant vérifier si notre secret est bien présent au sein de notre container applicatif :

1$ kubectl get pods -n demo
2
3NAME                        READY   STATUS        RESTARTS   AGE
4demo-app-5dcc48bd6-xw9zg    2/2     Running       0          29s

Vérifions maintenant l’existence du secret au sein du container applicatif :

1$ kubectl exec -it -n demo demo-app-5dcc48bd6-xw9zg -c demo-app -- cat /vault/secrets/config.ini
2
3data: map[password:demo-p@ssw0rd! ttl:30s username:demo-user]
4metadata: map[created_time:2024-01-09T16:30:55.118322625Z custom_metadata:<nil> deletion_time: destroyed:false version:1]

Mais il faut avouer que ce secret n’est pas très exploitable. Heureusement, il existe une annotation permettant d’écrire un template pour le format du fichier. Nous allons l’écrire sous un format JSON. Voyons cela :

 1$ kubectl apply -f - <<EOF
 2apiVersion: apps/v1
 3kind: Deployment
 4metadata:
 5  name: demo-app
 6  namespace: demo
 7  labels:
 8    app: demo-app
 9spec:
10  selector:
11    matchLabels:
12      app: demo-app
13  replicas: 1
14  template:
15    metadata:
16      annotations:
17        vault.hashicorp.com/agent-inject: 'true'
18        vault.hashicorp.com/role: 'demo-app'
19        vault.hashicorp.com/agent-inject-secret-config.json: 'secret/data/demo-app/config'
20        vault.hashicorp.com/agent-inject-template-config.json: |
21            {{- with secret "secret/data/demo-app/config" -}}
22            {"username": "{{ .Data.data.username }}", "password":"{{ .Data.data.password }}"}
23            {{- end -}}
24      labels:
25        app: demo-app
26    spec:
27      serviceAccountName: demo-app
28      containers:
29        - name: demo-app
30          image: python:3.11
31          command: ["tail"]
32          args: ["-f"]
33EOF

Nous avons renommé notre fichier en config.json. Nous avons également ajouté l’annotation vault.hashicorp.com/agent-inject-template-config.json qui permet de spécifier le format du fichier. Vu que c’est un template, nous pouvons écrire le fichier sous n’importe quel format. Nous pouvons accéder également à n’importe quelle clef de notre secret. Voyons le résultat :

1$ kubectl get pods -n demo
2
3NAME                       READY   STATUS        RESTARTS   AGE
4demo-app-5dcc48bd6-xw9zg   2/2     Terminating   0          13m
5demo-app-c447c5b9-sdq7c    2/2     Running       0          8s
1$ kubectl exec -it -n demo demo-app-c447c5b9-sdq7c -c demo-app -- cat /vault/secrets/config.json
2
3{"username": "demo-user", "password":"demo-p@ssw0rd!"}

Et voila ! Nous avons donc récupéré notre secret et nous l’avons monté sous forme de fichier JSON via un Vault Agent.

5. Conclusion

Nous avons donc vu comment utiliser Vault depuis un cluster Kubernetes, depuis l’authentification jusqu’à la récupération des secrets. La méthode des Vault Agent est très puissante et relativement simple à mettre en oeuvre pour permettre aux applications de récupérer les secrets dont elles ont besoin de manière sécurisée. Il existe une seconde méthode pour récupérer les secrets, qui consiste à monter les secrets à l’aide du Secret Store CSI Driver avec le provider Vault, mais nous verrons cela dans un prochain épisode :).