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 :
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 :
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 :
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 :).