Gestion des secrets Kubernetes avec Hashicorp Vault

2022-08-13 9 Min. lecture Kubernetes

Hashicorp Vault a la réputation d’être un peu compliqué à mettre en place ou à administrer. Il existe d’autre solution plus simple tel que sealed-secret de Bitnami ou Mozilla SOPS. Sealed-secret est en effet très simple mais ne permet de gérer que des secrets kubernetes. Mozilla SOPS lui va nous permettre de chiffrer toutes les valeurs présentes dans les fichiers yaml et json. Ces 2 outils ont une approche beaucoup plus GitOps de la gestion des secrets que vault, mais ne font que ça (ce qui est déjà pas mal).

1. Introduction

Le but de cet article est de fournir les outils permettant de mettre en place rapidement Vault sur votre cluster kubernetes afin de gérer ses secrets. Ensuite de comprendre le mécanisme d’authentification d’un service account kubernetes afin de récupérer les mots de passe dans Vault sans devoir créer un compte dans Vault pour chaque application.

Je ne vais présenter ni vault ni kubernetes, de nombreux articles le font déjà. Je pars du principe que vous connaissez un peu kubernetes mais pas tellement vault.

J’ai mis en place ce tuto avec rancher-desktop qui permet d’avoir en quelques minutes un cluster kubernetes mono-noeud dans la version que vous le souhaitez.

Cet article est fortement inspiré du tuto Hashicorp Learn.

2. Installation

Je pars du principe que vous avez téléchargé, installé et lancé rancher-desktop, choisi la version de kube et indiqué la mémoire et le nombre de cpu que vous lui attribuez. Le kubeconfig est configuré automatiquement et votre kubectl pointe bien sur votre cluster kubernetes géré par rancher-desktop.

> kubectl get nodes
NAME                   STATUS     ROLES                  AGE   VERSION
lima-rancher-desktop   NotReady   control-plane,master   17m   v1.23.9+k3s1

Pour gérer les secrets kubernetes dans vault nous allons devoir déployer 3 applications avec Helm :

Bien entendu, si vous avez déjà vault installé sur une VM ou sur un autre cluster kubernetes, il est inutile de déployer un nouveau cluster vault.

Les déploiements seront fait avec helm v3.

2.1. Hashicorp Vault

Une documentation complète peut être trouvée sur le site hashicorp.

Pour installer l’application vault, nous allons créer le namespace, ajouter le repo helm et déployer le helm.

Dans ce premier déploiement, nous installons vault sur 3 pods pour avoir la haute-dispo en utilisant le protocole raft. Il existe plusieurs solutions pour gérer la haute-dispo avec vault (bdd, consul, …​), mais cette solution est intégrée et fiable.

Création du namespace vault
kubectl create ns vault
Ajout du repo helm:
helm repo add hashicorp https://helm.releases.hashicorp.com
Vérification que nous récupérons bien le chart helm:
helm search repo hashicorp/vault
Nous allons créer un fichier values.yml comme ceci:
server:
  affinity: ""
  ha:
    enabled: true
    raft:
      enabled: true
injector:
  enabled: false
csi:
  enabled: true
ui:
  enabled: true
  serviceType: "LoadBalancer"
  serviceNodePort: null
  externalPort: 8200
Installation de vault avec le fichier values.yml
helm install vault hashicorp/vault \
    --namespace vault \
    -f values.yml \

2.2. secrets-store-csi-driver

Ajout du repo helm:
helm repo add  secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
Fichier values-secrets-store-csi.yml:
syncSecret:
  enabled: false
Installation de secrets-store-csi-driver avec le fichier values
helm install csi-secret-store secrets-store-csi-driver/secrets-store-csi-driver  \
    --namespace vault-csi \
    -f values-secrets-store-csi.yml \

Ici, j’ai fait le choix de déployer de driver csi dans le namespace vault-csi.

3. Initialisation du cluster Vault

Maintenant que notre cluster vault est déployé, il faut maintenant initialiser les noeuds et les lier entre eux. Nous aurons un noeud principal et 2 noeuds secondaires qui prendront la main en cas de perte du principal.

3.1. Initialisation du noeud principal

Nous allons dans un premier temps initialiser le premier noeud et stocker dans le fichier cluster-keys.json la clef de descellement du coffre ainsi que le token root pour s’y connecter.

kubectl exec vault-0 -- vault operator init \
    -key-shares=1 \
    -key-threshold=1 \
    -format=json > cluster-keys.json

Bien entendu, ce n’est pas très sécurisé. Il est préférable de spécifier plus de clef, par exemple un par membre de l’équipe key-shares=5 et de demander qu’au moins 2 ou 3 membres de l’équipe soit présent pour ouvrir le coffre key-threshold=3. Afin d’éviter que la personne qui initialise le coffre accède à l’ensemble des clefs, nous pouvons demander à vault de chiffrer chaque clef avec la clef publique GPG de chaque membre de l’équipe.

3.2. Descellement du coffre et connexion des autres noeuds vault

Comme ici je n’ai qu’une seule clef de descellement et qu’elle est présente dans le fichier json, j’automatise l’opération.

# Récupération de la clef de descellement
export VAULT_UNSEAL_KEY=$(jq -r ".unseal_keys_b64[]" cluster-keys.json)
# On ouvre le premier vault
kubectl exec -ti vault-0 -- vault operator unseal $VAULT_UNSEAL_KEY
# On rattache vaul-1 à vault-0
kubectl exec -ti vault-1 -- vault operator raft join http://vault-0.vault-internal:8200
# On l'ouvre
kubectl exec -ti vault-1 -- vault operator unseal $VAULT_UNSEAL_KEY
# On rattache vaul-2 à vault-0
kubectl exec -ti vault-2 -- vault operator raft join http://vault-0.vault-internal:8200
# On l'ouvre
kubectl exec -ti vault-2 -- vault operator unseal $VAULT_UNSEAL_KEY

Votre cluster vault est opérationnel. Si vous voulez le desceller automatiquement lors du redémarrage de votre rancher-desktop, vous pouvez utiliser le script ci-dessous :

Descellement automatique
#!/bin/bash
VAULT_UNSEAL_KEY=$(jq -r ".unseal_keys_b64[]" cluster-keys.json)
kubens vault
kubectl exec -ti vault-0 -- vault operator unseal "$VAULT_UNSEAL_KEY"
kubectl exec -ti vault-1 -- vault operator unseal "$VAULT_UNSEAL_KEY"
kubectl exec -ti vault-2 -- vault operator unseal "$VAULT_UNSEAL_KEY"
kubens -

4. Authentification de kubernetes dans vault

Vault propose de nombreux moyen d’authentification. Nous pouvons le lier au ldap, créer des comptes avec login/password, générer des token, …​

L’idée ici est de déléguer la gestion de l’authentification à kubernetes et d’avoir une politique de sécurité qui n’autorise le service account à ne se connecter qu’à certain secret en fonction de son nom et son namespace.

Il est possible de configurer tout cela dans l’interface web de vault que j’ai activé sur http://localhost:9200 mais nous allons faire tout cela en ligne de commande, c’est plus rapide.

4.1. Connexion avec le token root

Pour toutes les opérations qui vont suivre nous aurons besoin d’être authentifié avec un compte d’administration sur le cluster vault.

export VAULT_TOKEN=$(jq -r ".root_token" cluster-keys.json)
export VAULT_ADDR='http://127.0.0.1:8200'

Ici le token root est stocké dans le fichier json. Le fait de positionner ces 2 variables d’environnement nous permet d’être authentifié lors de l’appel des prochaines commande vault.

Pour une authentification par login/mot-de-passe, nous utiliserions plutôt la commande vault login.

4.2. Enregistrement de kubernetes dans vault

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

clustername="rancher-desktop"
clusterurl="https://kubernetes.default.svc:443"

# Activation de la méthode d'authentification
vault auth enable -path=${clustername} kubernetes

# Je déclare le cluster attaché à kubernetes
vault write auth/${clustername}/config \
    kubernetes_host="${clusterurl}"

ACCESSOR=$(vault auth list -format=json | jq -r ".\"${clustername}/\".accessor")
export ACCESSOR

vault policy write "${clustername}-kv-read" - <<EOF
path "kv/{{identity.entity.aliases.$ACCESSOR.metadata.service_account_namespace}}/{{identity.entity.aliases.$ACCESSOR.metadata.service_account_name}}/*" {
  capabilities = ["read"]
}
EOF

Lignes 3-4, j’indique le nom du cluster kubernetes et l’url de l’API. Il est préférable de ne pas laisser le nom par défaut, cela nous permet de bien identifier le cluster et de gérer plusieurs cluster kubernetes sur un seul vault.

Lignes 7-11, j’ajoute la méthode d’authentification pour le cluster rancher-desktop et je le lie à l’API.

Lignes 13-14, je récupère l’identifiant unique correspondant à l’authentification du cluster rancher-desktop, que l’on appelle ACCESSOR.

Lignes 16-20, je crée une policy rancher-desktop-kv-read générique autorisant les services account du cluster à joindre uniquement les secrets qui sont dans leur dossier.

Exemple, le service account webapp-sa dans le namespace apps ne sera autorisé à lire que les secrets présent dans /kv/apps/webapp-sa/*.

4.3. Création d’un rôle et d’un secret

Nous allons maintenant créer un rôle que je vais appeler database et que j’ai vais lier au service account, au namespace et à la policy.

kubectl create ns apps

# bound_service_account_names Nom des services account autorisés
# bound_service_account_namespaces Nom des namespaces autorisés

vault write auth/${clustername}/role/database \
    bound_service_account_names=webapp-sa \
    bound_service_account_namespaces=apps \
    policies=${clustername}-kv-read \
    ttl=20m

Enfin nous activons le moteur de secret si ce n’est pas déjà fait et nous créons notre premier secret.

# création d'un secrets engine kv
vault secrets enable -path kv kv

# ajout d'une clé pour test
vault kv put kv/apps/webapp-sa/db-pass user=myapp password=supersecret

Cette partie est identique si nous choisissons d’utiliser le sidecar injector ou vault-csi.

5. Création de mon application et récupération des secrets

L’idée de départ étant de rendre disponible des secrets dans le pod d’une application kubernetes, nous allons devoir créer 3 fichiers yaml :

  • webapp-sa.yaml
  • spc-vault-database.yaml
  • deployment.yaml

5.1. Création du ServiceAccount

Pour m’authentifier sur vault, j’ai besoin du ServiceAccount webapp-sa.

webapp-sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: webapp-sa

Que j’applique dans le namespace apps.

kubectl apply -n apps -f webapp-sa.yaml

5.2. Création du SecretProviderClass

spc-vault-database.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: vault-database
spec:
  provider: vault
  parameters:
    roleName: "database"
    vaultAddress: "http://vault.vault.svc:8200"
    vaultKubernetesMountPath: rancher-desktop
    objects: |
      - objectName: "db-user"
        secretPath: "kv/apps/webapp-sa/db-pass"
        secretKey: "user"
      - objectName: "db-password"
        secretPath: "kv/apps/webapp-sa/db-pass"
        secretKey: "password"

On retrouve ici le nom du rôle database dans vault, ainsi que l’url de l’API vault tel que le voit le cluster. Le champ vaultKubernetesMountPath permet de spécifier le nom du cluster.

Dans la partie objects, je mets en relation le secret stocké dans vault et le secret kubernetes.

J’applique également ce fichier yaml dans le namespace apps.

kubectl apply -n apps -f webapp-sa.yaml

5.3. Déploiement de l’application webapp

Tout est prêt, je peux créer le fichier de déploiement et déployer mon application.

deployment.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      serviceAccountName: webapp-sa
      containers:
      - name: webapp
        image: jweissig/app:0.0.1
        volumeMounts:
        - name: secrets-store-inline
          mountPath: "/mnt/secrets-store"
          readOnly: true
      volumes:
        - name: secrets-store-inline
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: "vault-database"

Ici je spécifie dans mon déploiement que les secrets doivent être présenté sous forme de fichier texte dans le dossier /mnt/secrets-store. On retrouve le nom du secretProviderClass déclaré juste au-dessus.

Il est également possible de présenter les secrets sous forme de variable d’environnement.

kubectl apply -n apps -f deployment.yaml

Je vais maintenant me connecter sur le pod et vérifier que les secrets sont bien présent.

❯ k exec -it webapp-7dccbc6469-w2689 -- ls -la /mnt/secrets-store/
total 4
drwxrwxrwt    3 root     root           120 Aug 13 06:27 .
drwxr-xr-x    1 root     root          4096 Aug 13 06:27 ..
drwxr-xr-x    2 root     root            80 Aug 13 06:27 ..2022_08_13_06_27_02.1354016230
lrwxrwxrwx    1 root     root            32 Aug 13 06:27 ..data -> ..2022_08_13_06_27_02.1354016230
lrwxrwxrwx    1 root     root            18 Aug 13 06:27 db-password -> ..data/db-password
lrwxrwxrwx    1 root     root            14 Aug 13 06:27 db-user -> ..data/db-user

6. Conclusion

Comme souvent en informatique, une fois que l’on a compris comment ça fonctionne, c’est tout de suite moins compliqué. L’utilisation de vault-csi et du secrets-store-csi-driver permet de mettre en place une solution relativement simple à administrer. On peut remarquer que contrairement à d’autre solution, il n’y a pas de secret kubernetes stocké dans la base etcd.

Voilà et avec un peu d’automatisation, il est possible de mettre en place tout ça en quelques minutes. On verra ça dans un autre article sur ArgoCD ou FluxCD.