Introduction

Vault est un outil de gestion des secrets développé par Hashicorp. Il permet de stocker et de gérer ces derniers de manière sécurisée. Dans cet article, nous allons voir comment utiliser Vault pour gérer les secrets de vos applications.


Vault a été publié en 2015 et est devenu un outil incontournable pour la gestion des secrets. Il est utilisé par de nombreuses entreprises pour sa flexibilité et sa sécurité. Son scope est large, il peut être employé pour stocker des secrets, des certificats, des clés SSH, des tokens d’API, etc.

Les grandes notions de Vault

Avant de commencer l’usage de Vault, il est important de comprendre ses grandes notions pour contextualiser son utilisation.

Storage Backends

Vault utilise un système de stockage backend pour stocker les données. Il existe plusieurs types de ‘backends’, chacun ayant ses propres avantages et inconvénients. Les plus courants sont les suivants :

  • Consul
  • Raft
  • Etcd
  • MySQL
  • PostgreSQL
  • S3
  • GCS

Le choix d’un backend dépend de vos besoins et de votre infrastructure, ils ne sont pas tous égaux en termes de performances et de disponibilité. Il n’y a qu’un seul backend par cluster Vault, mais vous pouvez en avoir plusieurs clusters Vault dans votre infrastructure.

Il faut noter que Vault ne stockera rien en clair, les données sont chiffrées avant d’être enregistrées dans le backend.

Secrets Engines

Les secrets engines sont les composants responsables de la génération et de la gestion des secrets. Ceux-ci peuvent stocker, générer ou chiffrer des données.

Certains secrets engines peuvent se connecter à d’autres services pour générer des secrets dynamiquement (Ex: Database secrets engine)

Rien n’empêche d’avoir plusieurs secrets engines (c’est même l’idée), ils sont isolés les un des autres et peuvent être configurés indépendamment. Il est aussi possible d’en avoir plusieurs du même type.

Un secret engine est activé sur un path (chemin) qui permet de l’identifier. Peu importe le type utilisé, on emploie toujours les mêmes méthodes de communication (l’interprétation de l’engine prime).

  • read pour lire un secret
  • write pour écrire un secret
  • delete pour supprimer un secret
  • list pour lister les secrets
  • patch pour modifier un secret

Certains secrets engines auront un comportement différent pour une méthode similaire. Par exemple, le secret engine kv avec read va retourner le contenu d’un secret, tandis que le secret engine database avec read va générer un accès à une base de données.

Auth Methods

Une méthode d’authentification est un composant qui permet de valider l’identité d’un utilisateur ou d’une application. Une fois authentifié, Vault se charge de fournir un token d’accès qui va valider les actions de l’utilisateur ou de l’application.

Par exemple, si je m’authentifie via un LDAP, je vais obtenir un token d’accès qui va me permettre d’effectuer des actions sur Vault.

Il est possible également d’utiliser un token déjà existant pour s’authentifier (sans passer par une auth method).

Chaque token peut avoir un ensemble de politiques qui déterminent les actions que le client peut effectuer ainsi qu’un TTL (Time To Live) qui détermine la durée de validité du token.

Vault Path

Tout comme le système kv de Consul, Vault utilise un système de path pour organiser les données.

Le préfixe d’un path permet de définir vers quel composant la requête doit être envoyée. Par exemple, si vous envoyez une requête à auth/ldap/login, elle sera transmise au composant auth qui gère l’authentification via LDAP.

Les composants ont tous un path par défaut qui peut être modifié via la configuration (vous pouvez même ajouter 2 fois le même composant avec des paths différents).

Certains paths sont réservés et ne peuvent pas être utilisés pour stocker des données. Comme par exemple :

  • sys
  • cubbyhole
  • identity
  • auth

Maintenant que nous avons vu les grandes notions de Vault, nous allons voir comment l’installer et l’utiliser.

Installation d’un serveur Vault

Sur Debian, il est possible d’installer Vault via les dépôts officiels Hashicorp. Pour cela, il faut ajouter la clé GPG et le dépôt à la liste des sources APT.

wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
gpg --no-default-keyring --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg --fingerprint
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault

À la fin de l’installation, je devrais pouvoir lancer la commande vault pour vérifier que celle-ci s’est bien déroulée.

$ vault -version
Vault v1.15.4 (9b61934559ba31150860e618cf18e816cbddc630), built 2023-12-04T17:45:28Z

Je peux également vérifier si le serveur est bien démarré en utilisant la commande vault status.

$ vault status
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": dial tcp 127.0.0.1:8200: connect: connection refused

Si le serveur n’est pas lancé (ce qui est le cas ici), vous pouvez taper la commande systemctl start vault.

Maintenant que le serveur est en exécution, je peux vérifier son statut.

$ vault status
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs

Cette erreur est normale, car le certificat utilisé par Vault ne contient pas l’adresse IP du serveur. Il faut donc utiliser l’option -tls-skip-verify pour ignorer cette erreur ou définir la variable d’environnement VAULT_SKIP_VERIFY à true.

$ vault status
Key                Value
---                -----
Seal Type          shamir
Initialized        false
Sealed             true
Total Shares       0
Threshold          0
Unseal Progress    0/0
Unseal Nonce       n/a
Version            1.15.4
Build Date         2023-12-04T17:45:28Z
Storage Type       file
HA Enabled         false

Nous obtenons énormément d’informations, mais nous allons nous concentrer sur les suivantes :

  • Seal Type : Type de scellement utilisé par Vault. Il existe plusieurs types de scellement, mais nous allons utiliser le type shamir qui est le plus courant.
  • Initialized : Indique si le serveur a été initialisé ou non.
  • Sealed : Indique si le serveur est scellé ou non. Un serveur scellé ne peut pas être utilisé.
  • Storage Type : Type de stockage utilisé par Vault. Par défaut, Vault utilise un stockage local, mais il est possible d’en utiliser d’autres types.

Initialisation du serveur

Au démarrage, Vault est scellé et ne peut pas être utilisé. Pour le déverrouiller (desceller, unseal), il faut lui fournir les informations nécessaires pour qu’il puisse déchiffrer les données qu’il contient. Pour cela, il utilise une clé unique : la clé racine (root key).

Pour fournir la clé racine à Vault, il faut lui donner 3 à 5 clés annexes qui seront utilisées pour reconstituer la clé racine. Ces dernières sont appelées clés de descellement (unseal keys).

L’objectif est de répartir les clés de descellement entre plusieurs personnes afin d’éviter qu’une seule personne puisse desceller le serveur.

Unseal

Une fois que le serveur est descellé, celui-ci devient utilisable et peut enfin être requêté pour stocker des secrets.

Mais pour l’instant, notre serveur n’est pas initialisé. Nous devons d’abord générer les clés de descellement et la clé racine.


Notre serveur Vault est accessible mais non-initialisé. Initialisons-le en créant les clés de descellement et la clé racine via la commande vault operator init. Deux paramètres sont nécessaires pour initialiser le serveur :

  • -key-shares : Nombre de clés de descellement à générer.
  • -key-threshold : Nombre de clés de descellement nécessaires pour desceller le serveur.

Astuce

Si vous souhaitez partager les clés de descellement avec 10 personnes, assurez-vous de permettre le déverrouillage du serveur avec moins de 10 clés (par exemple 6 personnes) au risque de ne pouvoir travailler si l’un des acteurs est en vacances.

Pour mon installation, je vais générer 5 clés de descellement et définir le seuil à 3.

$ vault operator init -key-shares=5 -key-threshold=3
Unseal Key 1: ctjl5Sf2MOUUAO7tLvlL1cHfzpxHHf1cbyxT7cfJfAr+
Unseal Key 2: eVK/kl9LJdyosgUiiEqWO+w5OZZBlT5zAGqAA9qGT9sD
Unseal Key 3: IgVFH1wh1wgEtlqtvnYZbTdsiHvBgt+WImTE1cDFgXCq
Unseal Key 4: 5Nvrc7gTt/VQwX7JvA5hboAivjB59/xfn9daatmsf4RH
Unseal Key 5: ulxOPPscUryTjRb0yp4dLDHAOQgGSwKhYykGTJqXrzyh

Initial Root Token: hvs.PjCYLqBpCKfxOjqHrRKXc0Eq

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

Vault does not store the generated root key. Without at least 3 keys to
reconstruct the root key, Vault will remain permanently sealed!

It is possible to generate new unseal keys, provided you have a quorum of
existing unseal keys shares. See "vault operator rekey" for more information.

Nous obtenons cinq clés de descellement et la clé racine. Il est important de stocker ces clés dans un endroit sûr, car sans celles-ci, il sera impossible de desceller le serveur.

$ vault status
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    0/3
...

Notre serveur est bien initialisé et scellé. Pour le desceller, il faut lui fournir 3 clés de descellement. Pour cela, nous allons utiliser la commande vault operator unseal et lui donner 3 des 5 clés de descellement.

$ vault operator unseal ctjl5Sf2MOUUAO7tLvlL1cHfzpxHHf1cbyxT7cfJfAr+
Key                Value
---                -----
Seal Type          shamir
Initialized        true
Sealed             true
Total Shares       5
Threshold          3
Unseal Progress    1/3
Unseal Nonce       b93329ef-1518-f145-eefb-d4f8cf1054cc
Version            1.15.4
Build Date         2023-12-04T17:45:28Z
Storage Type       file
HA Enabled         false
$ vault operator unseal eVK/kl9LJdyosgUiiEqWO+w5OZZBlT5zAGqAA9qGT9sD > /dev/null
$ vault operator unseal IgVFH1wh1wgEtlqtvnYZbTdsiHvBgt+WImTE1cDFgXCq > /dev/null

Le serveur est maintenant descellé et prêt à être utilisé. Celui-ci sera re-scellé automatiquement au redémarrage ou si un administrateur le scelle manuellement.

$ vault status | grep Sealed
Sealed          false

Et si je souhaite desceller automatiquement le serveur au démarrage ?

Dans ce cas-là, il existe une méthode qui permet de définir une clé de descellement automatique en utilisant AWS-KMS, GCP-KMS ou Azure Key Vault.

Si je suis on-premise ?

Si vous êtes en on-premise, vous pouvez utiliser un HSM (Hardware Security Module) pour stocker la clé de descellement, cette fonctionnalité est disponible dans la version Enterprise de Vault.

Nous n’aborderons pas ces méthodes dans cet article, mais je vous invite à consulter la documentation officielle pour plus d’informations.

Je n’ai pas envie de payer pour Vault Enterprise, existe-t-il une autre méthode ?

Il reste une dernière méthode qui consiste à utiliser Transit Auto Unseal qui nous permet d’utiliser un autre cluster Vault pour stocker la clé de descellement. Cette méthode est disponible dans la version Open Source de Vault et est très simple à mettre en place.


Maintenant que notre serveur Vault est initialisé et descellé, voyons comment interagir avec lui.

Interfaces de Vault

Vault dispose de plusieurs interfaces qui permettent d’interagir avec le serveur. Nous allons voir les suivantes :

  • CLI
  • API HTTP
  • WEBUI

En pratique, je vais toujours privilégier l’utilisation de la CLI pour les opérations d’administration et l’API HTTP pour les applications.

Installer le client Vault

Pour installer l’utilitaire vault en cli, vous pouvez passer par la même procédure que l’installation sur Debian, mais celui-ci installe également les fichiers pour la partie ‘serveur’ (service systemd, répertoire etc/). Je préfère donc l’installer via Nix ou télécharger directement la cli sans passer par un gestionnaire de paquet.

nix-env -i vault -v # Installation
nix-shell -p vault # Environnement éphémère

ou

VAULT_VERSION="1.15.4"
ARCH="amd64"
OS="linux" # darwin si macos
wget "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_${OS}_${ARCH}.zip" -O vault.zip
unzip vault.zip
sudo mv vault /usr/bin

S’authentifier via le CLI

Depuis notre poste de travail, si nous tentons d’interagir avec le serveur, nous obtenons une erreur car le serveur https://127.0.0.1:8200 est injoignable (plutôt logique : nous sommes en local).

$ vault status
Error checking seal status: Get "https://127.0.0.1:8200/v1/sys/seal-status": dial tcp 127.0.0.1:8200: connect: connection refused

Il faut indiquer à l’utilitaire Vault l’adresse du serveur. Pour cela, nous allons utiliser la variable d’environnement VAULT_ADDR.

export VAULT_ADDR="https://vault-01.servers.une-pause-cafe.fr:8200"
export VAULT_SKIP_VERIFY=true
vault status # Le serveur est joignable !

La même commande est possible en passant par des arguments.

$ vault status -address="https://vault-01.servers.une-pause-cafe.fr:8200" -tls-skip-verify
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    5
Threshold       3
Version         1.15.4
Build Date      2023-12-04T17:45:28Z
Storage Type    file
Cluster Name    vault-cluster-d7b71039
Cluster ID      ebb07014-7c6c-635c-f74e-c8fd103c4aea
HA Enabled      false

Maintenant que nous sommes connectés au serveur, nous allons nous authentifier. Pour cela, nous allons utiliser la commande vault login et lui donner le token racine.

$ vault login
Token (will be hidden):

Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                hvs.PjCYLqBpCKfxOjqHrRKXc0Eq
token_accessor       R9OD9o4U3uES0vc0tkyVj82U
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

Nous sommes désormais authentifiés et nous pouvons commencer à utiliser le gestionnaire de secrets.

Utiliser un certificat valide

L’usage d’un certificat auto-signé est bien pour un environnement de développement, mais pour un environnement de production il est recommandé d’utiliser un certificat valide.

Pour imiter la production, je vais générer une CA (Certificate Authority) avec mkcert, un programme permettant de générer des certificats valides pour le développement.

J’installe sur mon poste utilisateur mkcert:

nix-env -i mkcert
# ou
apt install mkcert
# ou
yum install mkcert
# ou
apk add mkcert

Je génère et j’installe la CA:

mkcert -install

Nous pouvons maintenant générer un certificat valide pour notre serveur Vault.

mkcert -cert-file vault-01.servers.une-pause-cafe.fr.crt -key-file vault-01.servers.une-pause-cafe.fr.key vault-01.servers.une-pause-cafe.fr

Celui-ci sera valide sur le domaine vault-01.servers.une-pause-cafe.fr.

Je commence par envoyer les fichiers .crt et .key sur le serveur Vault.

scp vault-01.servers.une-pause-cafe.fr.crt vault-01.servers.une-pause-cafe.fr.key vault-01.servers.une-pause-cafe.fr:

Maintenant, on stoppe le service et on copie les certificats dans le dossier /opt/vault/tls/.

systemctl stop vault
cp vault-01.servers.une-pause-cafe.fr.crt /opt/vault/tls/tls.crt
cp vault-01.servers.une-pause-cafe.fr.key /opt/vault/tls/tls.key
systemctl start vault

Par défaut, ma configuration utilise déjà ces fichiers, je n’ai donc rien à modifier. Mais si ce n’est pas le cas pour vous, il faut modifier le fichier /etc/vault.d/vault.hcl et ajouter les lignes suivantes :

# HTTPS listener
listener "tcp" {
  address       = "0.0.0.0:8200"
  tls_cert_file = "/opt/vault/tls/tls.crt"
  tls_key_file  = "/opt/vault/tls/tls.key"
}

J’en profite aussi pour installer la CA sur le serveur Vault pour qu’il puisse communiquer avec son propre service sans avoir à ignorer les erreurs TLS.

scp $(mkcert -CAROOT)/rootCA.pem vault-01.servers.une-pause-cafe.fr:
ssh vault-01.servers.une-pause-cafe.fr
cp rootCA.pem /usr/local/share/ca-certificates/homelab.crt

On peut maintenant mettre à jour la liste des certificats.

$ update-ca-certificates
Updating certificates in /etc/ssl/certs...
rehash: warning: skipping ca-certificates.crt,it does not contain exactly one certificate or CRL
1 added, 0 removed; done.
Running hooks in /etc/ca-certificates/update.d...
done.

Je peux désormais tenter une requête vers le serveur Vault sans avoir à ignorer les erreurs TLS.

$ curl https://vault-01.servers.une-pause-cafe.fr:8200 -v -q
*   Trying 100.64.0.12:8200...
* Connected to vault-01.servers.une-pause-cafe.fr (100.64.0.12) port 8200 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/certs/ca-certificates.crt
*  CApath: /etc/ssl/certs
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_CHACHA20_POLY1305_SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: O=mkcert development certificate; OU=quentinj@pop-os (Quentin JOLY)
*  start date: Jan 12 07:15:47 2024 GMT
*  expire date: Apr 12 06:15:47 2026 GMT
*  subjectAltName: host "vault-01.servers.une-pause-cafe.fr" matched cert's "vault-01.servers.une-pause-cafe.fr"
*  issuer: O=mkcert development CA; OU=quentinj@pop-os (Quentin JOLY); CN=mkcert quentinj@pop-os (Quentin JOLY)
*  SSL certificate verify ok.

Nous pouvons d’ailleurs voir les informations du certificat dans la réponse du serveur.

Créer un cluster Vault

Maintenant que nous avons un serveur Vault, nous allons ajouter un couche de haute disponibilité en créant un cluster. Pour cela, nous allons d’abord configurer notre noeud actuel pour qu’il rejoigne notre groupe.

La première chose à faire est de créer un fichier ~/migrate.hcl qui va nous permettre de migrer notre stockage local vers un stockage partagé avec les futurs noeuds du cluster.

storage_source "file" {
  path = "/opt/vault/data"
}

storage_destination "raft" {
  path = "/opt/vault/raft/"
  node_id = "vault_node_1"
}

cluster_addr = "https://100.64.0.12:8201" # à changer dans votre cas

Information

Le raft est un protocole de consensus distribué qui permet de synchroniser les données entre plusieurs noeuds. Il est utilisé par Vault pour synchroniser les données entre les noeuds d’un cluster. Nous l’avons déjà vu dans l’article sur Consul

Raft fonctionne dans un mode leader-follower, un noeud est élu leader et les autres noeuds sont des followers. Le leader est responsable de la synchronisation des données entre les noeuds. Si le leader est injoignable, un nouveau leader est élu parmi les followers.

Protocole de raft

Avertissement

Attention, le dossier /opt/vault/raft est à créer manuellement

Ce fichier détaille les informations nécessaires pour migrer le stockage local vers un stockage partagé raft, il contient également l’adresse du cluster cluster_addr qui sera utilisée par les autres noeuds pour rejoindre le cluster.

$ sudo -u vault vault operator migrate -config=migrate.hcl
2024-01-12T10:37:23.927+0100 [INFO]  creating Raft: config="&raft.Config{ProtocolVersion:3, HeartbeatTimeout:5000000000, ElectionTimeout:5000000000, CommitTimeout:50000000, MaxAppendEntries:64, BatchApplyCh:true, ShutdownOnRemove:true, TrailingLogs:0x2800, SnapshotInterval:120000000000, SnapshotThreshold:0x2000, LeaderLeaseTimeout:2500000000, LocalID:\"vault_node_1\", NotifyCh:(chan<- bool)(0xc0026cb570), LogOutput:io.Writer(nil), LogLevel:\"DEBUG\", Logger:(*hclog.intLogger)(0xc002dbc1e0), NoSnapshotRestoreOnStart:true, skipStartup:false}"
2024-01-12T10:37:23.930+0100 [INFO]  initial configuration: index=1 servers="[{Suffrage:Voter ID:vault_node_1 Address:100.64.0.12:8201}]"
2024-01-12T10:37:23.930+0100 [INFO]  entering follower state: follower="Node at vault_node_1 [Follower]" leader-address= leader-id=
2024-01-12T10:37:32.910+0100 [WARN]  heartbeat timeout reached, starting election: last-leader-addr= last-leader-id=
2024-01-12T10:37:32.910+0100 [INFO]  entering candidate state: node="Node at vault_node_1 [Candidate]" term=2
2024-01-12T10:37:32.918+0100 [INFO]  election won: term=2 tally=1
2024-01-12T10:37:32.918+0100 [INFO]  entering leader state: leader="Node at vault_node_1 [Leader]"
2024-01-12T10:37:32.928+0100 [INFO]  copied key: path=core/audit
[...]
2024-01-12T10:37:32.957+0100 [INFO]  copied key: path=sys/policy/default
Success! All of the keys have been migrated.

Avertissement

La commande vault operator migrate est à utiliser avec l’utilisateur vault et non pas root car elle va modifier les permissions des fichiers du dossier /opt/vault/raft/.

Si vous l’avez exécuté avec l’utilisateur root, vous pouvez corriger les permissions avec la commande suivante :

chown vault:vault -R /opt/vault/raft/

Nous pouvons maintenant éditer la configuration de vault /etc/vault.d/vault.hcl:

ui = true

disable_mlock = true # recommandé lorsqu'on utilise Raft

storage "raft" {
  path = "/opt/vault/raft/"
  node_id = "vault_node_1"
}

cluster_addr = "https://vault-01.servers.une-pause-cafe.fr:8201" 
api_addr = "https://vault-01.servers.une-pause-cafe.fr:8200" 

# HTTPS listener
listener "tcp" {
  address       = "0.0.0.0:8200"
  tls_cert_file = "/opt/vault/tls/tls.crt"
  tls_key_file  = "/opt/vault/tls/tls.key"
}

Après avoir enregistré la configuration, nous pouvons redémarrer le service Vault.

systemctl restart vault

Maintenant, je vais créer 2 autres serveurs Vault et les configurer pour qu’ils rejoignent le cluster.

Voici les informations des serveurs :

NodeIPfqdn
vault-01100.64.0.12vault-01.servers.une-pause-cafe.fr
vault-02100.64.0.14vault-02.servers.une-pause-cafe.fr
vault-03100.64.0.15vault-03.servers.une-pause-cafe.fr
Création des autres machines

Génération des certificats:

mkcert -cert-file vault-02.servers.une-pause-cafe.fr.crt -key-file vault-02.servers.une-pause-cafe.fr.key vault-02.servers.une-pause-cafe.fr
mkcert -cert-file vault-03.servers.une-pause-cafe.fr.crt -key-file vault-03.servers.une-pause-cafe.fr.key vault-03.servers.une-pause-cafe.fr
scp $(mkcert -CAroot)/rootCA.pem vault-02.servers.une-pause-cafe.fr.crt vault-02.servers.une-pause-cafe.fr.key vault-02.servers.une-pause-cafe.fr:
scp $(mkcert -CAroot)/rootCA.pem vault-03.servers.une-pause-cafe.fr.crt vault-03.servers.une-pause-cafe.fr.key vault-03.servers.une-pause-cafe.fr:
ssh vault-02.servers.une-pause-cafe.fr
cp vault-02.servers.une-pause-cafe.fr.crt /opt/vault/tls/tls.crt
cp vault-02.servers.une-pause-cafe.fr.key /opt/vault/tls/tls.key
cp rootCA.pem /usr/local/share/ca-certificates/homelab.crt
update-ca-certificates
mkdir /opt/vault/raft
chown vault:vault -R /opt/vault/raft/
ssh vault-03.servers.une-pause-cafe.fr
cp vault-03.servers.une-pause-cafe.fr.crt /opt/vault/tls/tls.crt
cp vault-03.servers.une-pause-cafe.fr.key /opt/vault/tls/tls.key
cp rootCA.pem /usr/local/share/ca-certificates/homelab.crt
update-ca-certificates
mkdir /opt/vault/raft
chown vault:vault -R /opt/vault/raft/

Fichier de configuration pour le serveur vault-02 :

ui = true

disable_mlock = true

storage "raft" {
  path = "/opt/vault/raft/"
  node_id = "vault_node_2"
}

cluster_addr = "https://vault-02.servers.une-pause-cafe.fr:8201"
api_addr = "https://vault-02.servers.une-pause-cafe.fr:8200"
cluster_name = "vault_coffee_prod"

# HTTPS listener
listener "tcp" {
  address       = "0.0.0.0:8200"
  cluster_addr  = "0.0.0.0:8201"
  tls_cert_file = "/opt/vault/tls/tls.crt"
  tls_key_file  = "/opt/vault/tls/tls.key"
}

Fichier de configuration pour le serveur vault-03 :

ui = true

disable_mlock = true

storage "raft" {
  path = "/opt/vault/raft/"
  node_id = "vault_node_3"
}

cluster_addr = "https://vault-03.servers.une-pause-cafe.fr:8201"
api_addr = "https://vault-03.servers.une-pause-cafe.fr:8200"
cluster_name = "vault_coffee_prod"

# HTTPS listener
listener "tcp" {
  address       = "0.0.0.0:8200"
  cluster_addr  = "0.0.0.0:8201"
  tls_cert_file = "/opt/vault/tls/tls.crt"
  tls_key_file  = "/opt/vault/tls/tls.key"
}

Je vais démarrer Vault sur les 2 autres serveurs et les configurer pour qu’ils rejoignent le cluster.

# sur vault-02.servers.une-pause-cafe.fr et vault-03.servers.une-pause-cafe.fr
systemctl start vault

Et maintenant, je vais les configurer pour qu’ils rejoignent le cluster.

# sur vault-02.servers.une-pause-cafe.fr
export VAULT_ADDR="https://vault-02.servers.une-pause-cafe.fr:8200"
vault operator raft join https://vault-01.servers.une-pause-cafe.fr:8200
# sur vault-03.servers.une-pause-cafe.fr
export VAULT_ADDR="https://vault-03.servers.une-pause-cafe.fr:8200"
vault operator raft join https://vault-01.servers.une-pause-cafe.fr:8200

Le résultat devrait être celui-ci :

Key       Value
---       -----
Joined    true

Nous devons unseal les deux serveurs que nous venons d’ajouter au cluster.

# sur vault-02.servers.une-pause-cafe.fr et vault-03.servers.une-pause-cafe.fr
vault operator unseal
# [...]

Après avoir déverrouillé les deux serveurs, nous pouvons vérifier l’état du cluster.

$ vault operator raft list-peers
Node            Address                                    State       Voter
----            -------                                    -----       -----
vault_node_1    100.64.0.12:8201                           leader      true
vault_node_2    vault-02.servers.une-pause-cafe.fr:8201    follower    true
vault_node_3    vault-03.servers.une-pause-cafe.fr:8201    follower    true

Oui, je ne sais pas pourquoi, mais Vault utilise l’adresse IPv4 sur le premier noeud et le FQDN sur les autres noeuds.

Le cluster est fonctionnel et nous pouvons commencer à l’utiliser.

Remarque

Il convient de noter que si le noeud vault_node_1 est injoignable, l’endpoint https://vault-01.servers.une-pause-cafe.fr:8200 le sera également. Il est donc recommandé d’utiliser une adresse IP flottante à la keepalived pour toujours rediriger le trafic vers un noeud fonctionnel.

Créer un secret

Nous allons enfin créer notre premier secret. Pour cela, nous allons utiliser le moteur de stockage kv qui permet d’enregistrer des données dans un espace de nommage. Comme le reste des données de Vault, les données sont chiffrées en AES-GCM 256 bits.

Il existe 2 versions du moteur de stockage kv : kv et kv-v2. La version kv-v2 est la plus récente et permet de gérer les versions des secrets, les dates d’expiration et les données structurées.

vault secrets enable -path=kv kv
  • secrets enable permet d’activer un moteur de stockage
  • -path=kv permet de définir le chemin du moteur de stockage
  • kv est le type de moteur de stockage ( kv pour Key-Value )

Lorsqu’on crée un secret avec le moteur de stockage, on utilise un chemin pour définir l’emplacement du secret. Par exemple, si je souhaite stocker un secret dans le chemin kv/secret/mon-secret, je vais utiliser la commande suivante :

vault kv put kv/secret/mon-secret password="Un3T4ss32Kafé"

Pour récupérer le secret, je lance la commande vault kv get et lui donne le chemin du secret.

$ vault kv get kv/secret/mon-secret
====== Data ======
Key         Value
---         -----
password    Un3T4ss32Kafé

Un secret est composé d’un ensemble de clés/valeurs. Je ne possède qu’une seule clé password pour le moment, mais je pourrai en ajouter d’autres en recréant le secret suivi de la nouvelle clé/valeur à ajouter.

vault kv put kv/secret/mon-secret username="Quentin" password="Un3T4ss32Kafé"

Avertissement

Attention, pour ajouter une clé/valeur à un secret existant, il faut quand même préciser les valeurs des clés déjà existantes. Sinon, le secret sera écrasé et les clés non précisées seront supprimées.

Pour voir les secrets existants dans un chemin, je peux utiliser la commande vault kv list et lui donner le chemin du secret.

$ vault kv list kv/secret
Keys
----
mon-secret

Les commandes possibles sont :

  • vault kv get pour récupérer un secret
  • vault kv put pour créer ou mettre à jour un secret
  • vault kv list pour lister les secrets
  • vault kv delete pour supprimer un secret

Maintenant, je souhaite utiliser la version kv-v2 du moteur de stockage. Pour cela, je vais créer un nouveau moteur de stockage avec le chemin kv2 et le type kv-v2.

vault secrets enable -path=kv2 -version=2 kv

Astuce

Vous pouvez migrer un moteur de stockage kv vers `kv-v2* avec la commande vault kv enable-versioning

vault kv enable-versioning kv/

Que change la version 2 du moteur de stockage ?

La v2 est meilleure en plusieurs points :

  • Elle permet de gérer les versions des secrets.
  • Elle permet de gérer les dates d’expiration des secrets.
  • Elle ajoute des métadonnées sur les secrets.
  • Elle ajoute la commande patch qui permet de modifier une clé/valeur sans écraser le secret.

En kv2, nous avons les mêmes commandes que pour kv avec en plus :

  • vault kv get -version=<version> pour récupérer une version d’un secret.
  • vault destroy pour supprimer un secret de manière permanente.
  • vault kv patch pour modifier une clé/valeur sans écraser le secret.
  • vault kv undelete pour restaurer un secret supprimé.
  • vault kv rollback pour revenir à une version précédente d’un secret.
$ vault kv put kv2/db/dev user="app01" password="Un3T4ss32Kafé" dbname="db01"

= Secret Path =
kv2/data/db/dev

======= Metadata =======
Key                Value
---                -----
created_time       2024-01-24T19:30:10.37122978Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

Des métadonnées sont ajoutées au secret, on peut voir la version du secret, la date de création, la potentielle date de suppression, etc.

Information

P’tite astuce, vous pouvez utiliser un fichier JSON pour créer un secret.

vault kv put kv2/db/dev @db.json

Maintenant, je vais modifier mon secret en ajoutant une clé.

$ vault kv patch kv2/db/dev ip="192.168.1.123"
= Secret Path =
kv2/data/db/dev

======= Metadata =======
Key                Value
---                -----
created_time       2024-01-24T19:40:53.002194415Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            2

J’ai donc une version 2 de mon secret. Je peux voir les versions précédentes avec la commande vault kv get -version=1 kv2/db/dev.

Mais si je souhaite revenir à la version précédente, je peux utiliser la commande vault kv rollback, lui donner le chemin du secret et la version à laquelle je souhaite revenir.

vault kv rollback -version=1 kv2/db/dev

Le rollback va créer une nouvelle version (3) du secret avec le contenu de la version 1.

Pour supprimer un secret, je peux le faire de 2 manières :

  • vault kv delete pour le supprimer de manière logique (les données sont toujours présentes, mais le secret est marqué comme inaccessible), un vault kv undelete permet de le restaurer.
  • vault destroy pour supprimer un secret de manière permanente (les métadonnées sont encore présentes, mais les données sont supprimées).

Information

Attention ! En kv1, le delete est permanent, mais pas en kv2 (qui dispose du undelete).

Les métadonnées sont accessibles via la commande vault kv metadata get suivi du chemin du secret.

$ vault kv metadata get kv2/db/dev
== Metadata Path ==
kv2/metadata/db/dev

========== Metadata ==========
Key                     Value
---                     -----
cas_required            false
created_time            2024-01-24T19:30:10.37122978Z
current_version         6
custom_metadata         <nil>
delete_version_after    0s
max_versions            0
oldest_version          0
updated_time            2024-01-24T19:40:53.002194415Z
  • max_versions permet de définir le nombre maximum de versions d’un secret.
  • delete_version_after permet de définir la durée de rétention des versions d’un secret.
  • cas_required permet de définir si le secret doit être mis à jour avec la dernière version.

Database secret engine

Le secret engine database permet de gérer les identifiants de connexion à une base de donnée. Il permet de créer des “rôles” qui vont générer des identifiants temporaires pour se connecter à une base de donnée. De cette manière les identifiants ne sont jamais exposés et sont régénérés à chaque utilisation.

Information

Un rôle est un composant de ce moteur de stockage qui va définir comment créer les utilisateurs éphémères et quand les supprimer.

Je vais créer une base de données MariaDB dans un conteneur Docker.

version: '3.3'
services:
    #serveur de base de donnees
    database:
       image: 'mariadb:11'
       container_name: database
       restart: always
       environment:
          MYSQL_USER: user
          MYSQL_PASSWORD: mypassword
          MYSQL_DATABASE: myvaultdb
          MYSQL_ROOT_PASSWORD: rootpassword
       ports:
           - '3306:3306'
       volumes:
           - ${PWD}/mariadb/:/var/lib/mysql/

Cette base de données est accessible via l’URI database.servers.une-pause-cafe.fr:3306.

Pour la configurer dans Vault, je vais devoir créer 2 objets:

  • l’objet database/config qui va contenir les informations de connexion à la base de données,
  • l’objet database/roles qui va définir les manières de générer les identifiants.

Je vais même aller plus loin en créant 2 rôles :

  • app-01-readonly qui permettra de générer des identifiants avec des droits de lecture seule,
  • app-01-readwrite qui permettra de générer des identifiants avec des droits de lecture et d’écriture.

J’active le moteur de stockage database avec la commande vault secrets enable database et je crée la configuration de la base de données.

$ vault write database/config/db-01 \
  plugin_name=mysql-database-plugin \
  connection_url="{{username}}:{{password}}@tcp(database.servers.une-pause-cafe.fr:3306)/" \
  allowed_roles="app-01-readonly, app-01-readwrite" \
  username="root" \
  password="rootpassword"
Success! Data written to: database/config/db-01

Le message Success! Data written to: database/config/db-01 indique que la configuration a bien été enregistrée et que Vault a bien pu se connecter à la base de données (une erreur aurait été affichée sinon).

Avertissement

Il est important de noter que Vault ne vous permettra pas d’afficher le mot de passe de la base de données une fois qu’il aura été enregistré. Seul Vault et le créateur de la base de données connaissent le mot de passe.

Avant de passer à la suite, je vais me connecter à la base de données pour vérifier les utilisateurs existants.

$ mysql -h database.servers.une-pause-cafe.fr -uroot -prootpassword -e "SELECT User, Host FROM mysql.user;"

+-------------+-----------+
| User        | Host      |
+-------------+-----------+
| root        | %         |
| user        | %         |
| healthcheck | 127.0.0.1 |
| healthcheck | ::1       |
| healthcheck | localhost |
| mariadb.sys | localhost |
| root        | localhost |
+-------------+-----------+

Je vais maintenant créer le premier rôle app-01-readonly qui va permettre de générer des identifiants avec des droits de lecture.

$ vault write database/roles/app-01-readonly \
    db_name=db-01 \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY 
'{{password}}';GRANT SELECT ON myvaultdb.* TO '{{name}}'@'%';" \
    default_ttl="1h" \
    max_ttl="4h"
Success! Data written to: database/roles/app-01-readonly

Maintenant, essayons de générer des identifiants avec ce rôle.

$ vault read database/creds/app-01-readonly                                                                
Key                Value
---                -----
lease_id           database/creds/app-01-readonly/3OXkaaCLTB9JWtWPCgdH18F8
lease_duration     1h
lease_renewable    true
password           LWz-KGe2-umtdH-ote4k
username           v-root-app-01-rea-8dbHBPfruwXvBW

Si j’affiche les utilisateurs de la base de donnée, je peux voir que l’utilisateur a bien été créé.

$ mysql -h database.servers.une-pause-cafe.fr -uroot -prootpassword -e "SELECT User, Host FROM mysql.user;"

+----------------------------------+-----------+
| User                             | Host      |
+----------------------------------+-----------+
| root                             | %         |
| user                             | %         |
| v-root-app-01-rea-8dbHBPfruwXvBW | %         |
| healthcheck                      | 127.0.0.1 |
| healthcheck                      | ::1       |
| healthcheck                      | localhost |
| mariadb.sys                      | localhost |
| root                             | localhost |
+----------------------------------+-----------+

Je peux alors me connecter à la base de données avec l’utilisateur généré.

$ mysql -h database.servers.une-pause-cafe.fr -uv-root-app-01-rea-8dbHBPfruwXvBW -pLWz-KGe2-umtdH-ote4k -e "SHOW GRANTS FOR 'v-root-app-01-rea-8dbHBPfruwXvBW'@'%';"

+---------------------------------------------------------------------------------------------------------------------------------+
| Grants for v-root-app-01-rea-8dbHBPfruwXvBW@%                                                                                   |
+---------------------------------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `v-root-app-01-rea-8dbHBPfruwXvBW`@`%` IDENTIFIED BY PASSWORD '*FFC103215DF59EEC3ED29CF52631DDEC1811171C' |
| GRANT SELECT ON `myvaultdb`.* TO `v-root-app-01-rea-8dbHBPfruwXvBW`@`%`                                                         |
+---------------------------------------------------------------------------------------------------------------------------------+

Créons maintenant le rôle app-01-readwrite qui va permettre de générer des identifiants avec des droits de lecture et d’écriture.

$ vault write database/roles/app-01-readwrite \
    db_name=db-01 \
    creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY
'{{password}}';GRANT SELECT, INSERT, UPDATE, DELETE ON myvaultdb.* TO '{{name}}'@'%';" \
    default_ttl="1h" \
    max_ttl="4h"
Success! Data written to: database/roles/app-01-readwrite

Je génère un identifiant avec ce rôle.

$ vault read database/creds/app-01-readwrite

Key                Value
---                -----
lease_id           database/creds/app-01-readwrite/Hywb4tK1qmuNxic9FARqOhfq
lease_duration     1h
lease_renewable    true
password           74kgvl7ETH-CFDKNX3cG
username           v-root-app-01-rea-Ic8E5z4s6ydBPA

Je peux donc me connecter à la base de données avec cet identifiant.

$ mysql -h database.servers.une-pause-cafe.fr -uv-root-app-01-rea-Ic8E5z4s6ydBPA -p74kgvl7ETH-CFDKNX3cG -e "SHOW GRANTS FOR 'v-root-app-01-rea-Ic8E5z4s6ydBPA'@'%';"

+---------------------------------------------------------------------------------------------------------------------------------+
| Grants for v-root-app-01-rea-Ic8E5z4s6ydBPA@%                                                                                   |
+---------------------------------------------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO `v-root-app-01-rea-Ic8E5z4s6ydBPA`@`%` IDENTIFIED BY PASSWORD '*11C9016D84F17BF80819EBCC1ABF093EAB0D4AAA' |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `myvaultdb`.* TO `v-root-app-01-rea-Ic8E5z4s6ydBPA`@`%`                                 |
+---------------------------------------------------------------------------------------------------------------------------------+

Chaque identifiant généré possède un TTL (Time To Live) d’une heure (renouvelable jusqu’à 4 heures) et sera supprimé après ce délai. Cela permet de limiter l’exposition des identifiants.

Révoquons maintenant les baux (lease) des identifiants générés.

$ vault lease revoke database/creds/app-01-readonly/3OXkaaCLTB9JWtWPCgdH18F8
All revocation operations queued successfully!
$ vault lease revoke database/creds/app-01-readwrite/Hywb4tK1qmuNxic9FARqOhfq
All revocation operations queued successfully!

En révoquant les baux, les identifiants générés sont immédiatement supprimés de la base de données.

$ mysql -h database.servers.une-pause-cafe.fr -uroot -prootpassword -e "SELECT User, Host FROM mysql.user;"
+-------------+-----------+
| User        | Host      |
+-------------+-----------+
| root        | %         |
| user        | %         |
| healthcheck | 127.0.0.1 |
| healthcheck | ::1       |
| healthcheck | localhost |
| mariadb.sys | localhost |
| root        | localhost |
+-------------+-----------+

Avant de terminer cette partie sur le secret engine database, il me reste une dernière chose à sécuriser : le mot de passe de l’utilisateur root de la base de données, connu de Vault et de l’administrateur de la base de donnée.

Je peux demander à Vault de changer le mot de passe de l’utilisateur root avec la commande vault write database/rotate-root/db-01.

$ vault write -f database/rotate-root/db-01
Success! Data written to: database/rotate-root/db-01

Si je retente de me connecter à la base de données avec l’ancien mot de passe, je reçois une erreur.

$ mysql -h database.servers.une-pause-cafe.fr -uroot -prootpassword -e "SELECT User, Host FROM mysql.user;"
ERROR 1045 (28000): Access denied for user 'root'@'100.64.0.13' (using password: YES)

Notre mot de passe root a bien été changé par Vault et n’est connu que par lui-même.

Transit secret engine

Le moteur de stockage transit permet d’intégrer le chiffrement/déchiffrement de données de Vault dans votre application sans avoir à gérer les clés de chiffrement dans celle-ci.

Imaginons une entreprise développant plusieurs applications, chaque équipe dispose de sa propre base de données et voulant chiffrer les données de chaque application avec une clé différente. Lorsque les OPS gèrent ces applications, ils se retrouvent avec plusieurs clés et types de chiffrement différents.

L’idée du moteur de stockage transit est de centraliser le chiffrement/déchiffrement des données dans Vault en agissant comme un micro-service recevant des données en clair et les renvoyant chiffrées (ou l’inverse). Il est aussi possible que chaque application utilise une clé différente, mais Vault reste acteur de ces opérations.

⚠️ Le moteur de stockage transit ne permet pas de stocker des données, il permet uniquement de chiffrer/déchiffrer des données et d’en renvoyer le résultat.

Pour utiliser le moteur de stockage transit, il faut l’activer avec la commande vault secrets enable transit.

Je vais générer ma première clé qui servira pour l’application une-tasse-de.cafe-app. Ensuite, je vais chiffrer mes données avec cette clé.

$ vault write -f transit/keys/une-tasse-de.cafe-app
$ vault write transit/encrypt/une-tasse-de.cafe-app plaintext="$(echo 'hello-world' | base64)"
Key            Value
---            -----
ciphertext     vault:v1:otgrC6o55oIX9awXC8KERLlFijBDC9cSODaeBxOjvlQ6fwP1fN6fGQ==
key_version    1

Je dispose maintenant d’une chaîne de caractère chiffrée que je peux stocker dans ma base de données.

Si jamais je souhaite déchiffrer cette chaîne, je peux fournir ce texte au path transit/decrypt/une-tasse-de.cafe-app

$ vault write transit/decrypt/une-tasse-de.cafe-app  ciphertext="vault:v1:otgrC6o55oIX9awXC8KERLlFijBDC9cSODaeBxOjvlQ6fwP1fN6fGQ=="
Key          Value
---          -----
plaintext    aGVsbG8td29ybGQK
$ echo "aGVsbG8td29ybGQK" | base64 -d
hello-world

Je retrouve bien mon hello-world initial.

Astuce

Il n’est pas obligatoire de convertir le texte en base64 avant de l’envoyer à Vault mais pour éviter les problèmes d’encodage, il est recommandé de le faire.

Si jamais je souhaite changer la clé de chiffrement, je peux utiliser la commande vault write -f transit/keys/une-tasse-de.cafe-app/rotate.

$ vault write -f transit/keys/une-tasse-de.cafe-app/rotate
Key                       Value
---                       -----
allow_plaintext_backup    false
auto_rotate_period        0s
deletion_allowed          false
derived                   false
exportable                false
imported_key              false
keys                      map[1:1706550426 2:1706551374]
latest_version            2
min_available_version     0
min_decryption_version    1
min_encryption_version    0
name                      une-tasse-de.cafe-app
supports_decryption       true
supports_derivation       true
supports_encryption       true
supports_signing          false
type                      aes256-gcm96

Cette commande va créer une nouvelle clé et la définir comme active. Les données chiffrées avec l’ancienne seront toujours déchiffrables par la nouvelle mais pas l’inverse.

$ vault write transit/encrypt/une-tasse-de.cafe-app plaintext="$(echo 'hello-world' | base64)"
Key            Value
---            -----
ciphertext     vault:v2:FRVS0KhzJy46F4OdimD1ONJ8P5Dvn5SqLVRdqwBFZEJ3v4q+zxZjJw==
key_version    2
# Je génère un nouveau texte chiffré avec la nouvelle clé

$ vault write transit/decrypt/une-tasse-de.cafe-app ciphertext="vault:v2:FRVS0KhzJy46F4OdimD1ONJ8P5Dvn5SqLVRdqwBFZEJ3v4q+zxZjJw=="
Key          Value
---          -----
plaintext    aGVsbG8td29ybGQK
# Je peux déchiffrer avec la nouvelle clé

$ vault write transit/decrypt/une-tasse-de.cafe-app ciphertext="vault:v1:otgrC6o55oIX9awXC8KERLlFijBDC9cSODaeBxOjvlQ6fwP1fN6fGQ=="
Key          Value
---          -----
plaintext    aGVsbG8td29ybGQK
# Je peux toujours déchiffrer avec le contenu de l'ancienne clé

Astuce

Administrateurs, vous pouvez forcer vos utilisateurs à utiliser une certaine version de clé (par exemple, si la version 1 est compromise, vous pouvez forcer l’utilisation de la version 2).

Dans ce cas, les développeurs pourront rewrap pour générer une nouvelle version du texte chiffré en utilisant la dernière clé disponible.

vault write transit/rewrap/une-tasse-de.cafe-app ciphertext="vault:v1:otgrC6o55oIX9awXC8KERLlFijBDC9cSODaeBxOjvlQ6fwP1fN6fGQ=="
Key            Value
---            -----
ciphertext     vault:v2:dLbhwjAdiPujmwu2vf6iyiGrAJ7nULwdgJixAynbz1UOvxaxFbU3Ug==
key_version    2

S’authentifier avec Vault

Nous avons utilisé le root token depuis le début de cet article. Celui-ci donne les permissions les plus élevées sur Vault et il est recommandé de ne pas l’utiliser pour les opérations courantes.

Pour une gestion fine-grained, nous devons pouvoir authentifier des utilisateurs différents, ou des applications. Pour cela, nous allons d’abord nous intéresser à l’authentification des utilisateurs sur Vault.

Lorsque l’on s’authentifie sur Vault, celui-ci va vérifier que nous sommes bien un utilisateur autorisé. Pour cela, il va utiliser un mécanisme/méthode d’authentification qui va vérifier notre identité.

Une fois que Vault nous fait confiance, il va nous donner un token qui va nous permettre d’effectuer des opérations. Ce jeton d’authentification contient des informations sur l’utilisateur et les permissions qui lui sont accordées. Il est à conserver précieusement et vous servira pour vos actions sur Vault.

En résumé :

  1. L’utilisateur s’authentifie sur Vault.
  2. Vault vérifie l’identité de l’utilisateur.
  3. Vault donne un token à l’utilisateur.
  4. L’utilisateur utilise le token pour effectuer des opérations sur Vault.

Information

Un token est toujours associé à un TTL. Une fois le token expiré, l’utilisateur doit se réauthentifier. Il est aussi possible de demander à Vault de renouveler le TTL du token (dans une certaine limite).

Les méthodes d’authentification sont nombreuses mais les plus courantes sont :

  • Token (par défaut)
  • LDAP
  • GitHub
  • Kubernetes
  • Kerberos
  • Userpass

Astuce

Il est possible de voir les mécanismes d’authentification activés sur l’interface web de Vault:

Ou via la CLI :

$ vault auth list
Path      Type     Accessor               Description                Version
----      ----     --------               -----------                -------
token/    token    auth_token_b9ef4560    token based credentials    n/a

Chaque méthode est associée à un path qui va permettre de s’authentifier.

Par exemple, pour s’authentifier avec un token, il faut utiliser le path token/. Pour s’authentifier avec AWS, il faut utiliser le path aws/. (Il est possible de choisir un autre path, par défaut Vault utilise le nom du mécanisme d’authentification).

J’active le mécanisme d’authentification userpass qui permet de créer des utilisateurs et de s’authentifier avec un nom d’utilisateur et un mot de passe.

$ vault auth enable userpass
Success! Enabled userpass auth method at: userpass/

Pour créer un utilisateur, je peux utiliser la commande vault write, lui donner le path userpass/users/<username> ainsi qu’un mot de passe.

vault write auth/userpass/users/quentinj password="password"

Il est maintenant possible de s’authentifier avec l’utilisateur quentinj et le mot de passe password.

Je peux récupérer mon token via l’API HTTP de cette manière :

$ curl -s --request POST --data '{"password": "password"}' https://vault-01.servers.une-pause-cafe.fr:8200/v1/auth/userpass/login/quentinj | jq -r .auth.client_token
hvs.CAESIB3YP7YmYQhXQJuc0DHRtakofronBc31oxKzq-9Z3Ew8Gh4KHGh2cy5ROFlKUGxOQkF1SWIzSFV4SnJOTXFBRVc

Comme deuxième méthode d’authentification, je souhaite activer le mécanisme github qui permet d’utiliser un compte GitHub comme clé. Celui-ci se repose sur un token d’authentification généré par GitHub possédant la permission read:org.

vault auth enable github
vault write auth/github/config organization=RubxKube

Mon compte Github est présent dans l’organisation RubxKube, je vais donc pouvoir m’authentifier avec mon compte sur la WEBUI avec mon token PAT.

Astuce

Si vous avez installé le client cli de Github, vous pouvez vous authentifier avec le token utilisé par l’outil. Celui-ci s’affiche via la commande gh auth token.

Il faut alors se connecter à la méthode “GitHub” et fournir la variable ’token’ avec le token généré par l’outil.

$ vault login -method=github token="$(gh auth token)"
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                    Value
---                    -----
token                  hvs.CAESIG42A5ySjPoxNeCud9VKc5fhCox1fxqScV4ArJhcbE3KGh4KHGh2by5NUktMNzdBeUkyTWdpOUNEUnh1NXB0NWU
token_accessor         r7yd1ctaeJhczJz3JEGv4ES4
token_duration         768h
token_renewable        true
token_policies         ["default"]
identity_policies      []
policies               ["default"]
token_meta_org         RubxKube
token_meta_username    QJoly

En dehors de l’usage d’un couple utilisateur/mot de passe, il est possible de s’authentifier avec un token d’application.

Pour cela, on peut utiliser le mécanisme d’authentification approle qui permet de créer des tokens d’applications.

$ vault auth enable approle
$ vault write auth/approle/role/engineering policies=engineering-policy
$ vault write -f auth/approle/role/engineering/secret-id
Key                   Value
---                   -----
secret_id             9681a754-d4cd-9d8c-f9c1-a5a3b5088831
secret_id_accessor    1189f3fd-f6e1-61cb-2636-8c84a024e387
secret_id_ttl         0s

Gérer vos utilisateurs (Entité et Alias)

Maintenant que nous avons vu comment s’authentifier sur Vault, nous allons voir comment gérer les utilisateurs. Pour cela, nous allons utiliser les entités et les alias.

Une entité est un objet qui représente un utilisateur ou une application. Elle est associée à un groupe, des politiques, des métadonnées et des alias. Quant à eux, les alias sont des liens entre une entité et un mécanisme d’authentification.

Par exemple, j’ai créé un utilisateur quentinj avec le mécanisme d’authentification userpass et l’utilisateur qjoly avec le mécanisme d’authentification github. Dès que je me suis authentifié avec ces utilisateurs, Vault a créé une entité et un alias pour chacun d’eux.

Entité et Alias

C’est à l’entité que je vais associer des politiques (qui définissent les permissions de l’entité). Si je dispose de plusieurs moyens d’authentification pour le même acteur, je peux lier plusieurs alias à une entité.

quentinj(userpass) et qjoly(github) sont deux identifiants différents mais représentent la même personne. Je peux donc les lier à une seule entité.

Créer une entité

Je commence donc par créer une entité.

Via l’interface web :

Fusionner entity

Via la CLI :

$ vault list identity/entity/name # Affiche les entités existantes
Keys
----
entity_526e8698
entity_608ec3ec
$ vault write identity/entity name="Quentin JOLY" metadata=organization="RubxKube" metadata=team="DevOps"
$ vault list identity/entity/name
Keys
----
Quentin JOLY
entity_526e8698
entity_608ec3ec

Je dispose maintenant d’une entité Quentin JOLY que je vais lier à mes alias quentinj et qjoly.

Pour se faire, je dois passer par deux étapes :

  • Récupérer l’accessor de mon mécanisme d’authentification (lié à mon alias).
  • Récupérer mon entity_id (lié l’entité que je viens de créer).

Information

Un accessor est un identifiant unique pour chaque mécanisme d’authentification.

mount_accessor=$(vault auth list -format=json | jq '."userpass/".accessor' -r)
entity_id=$(vault read identity/entity/name/"Quentin JOLY" -format=json | jq .data.id -r)
vault write identity/entity-alias name="quentinj" canonical_id="$entity_id" mount_accessor="$mount_accessor"

Afin de vérifier que l’alias a bien été ajouté, je peux utiliser la commande vault read identity/entity/name/"Quentin JOLY" -format=json | jq .data.aliases qui va me retourner la liste des alias de l’entité.

// vault read identity/entity/name/"Quentin JOLY" -format=json | jq .data.aliases
[
  {
    "canonical_id": "16dcc20e-e718-04a2-3b9a-f811b57912c9",
    "creation_time": "2024-01-14T11:20:56.920359453Z",
    "custom_metadata": null,
    "id": "55993c02-66c5-aa50-eb5d-8abaddf14ec7",
    "last_update_time": "2024-01-14T11:22:13.083223732Z",
    "local": false,
    "merged_from_canonical_ids": null,
    "metadata": null,
    "mount_accessor": "auth_userpass_c1dafbb1",
    "mount_path": "auth/userpass/",
    "mount_type": "userpass",
    "name": "quentinj"
  }
]

Même chose pour l’alias qjoly (méthode d’authentification github).

mount_accessor=$(vault auth list -format=json | jq '."github/".accessor' -r)
entity_id=$(vault read identity/entity/name/"Quentin JOLY" -format=json | jq .data.id -r)
vault write identity/entity-alias name="qjoly" canonical_id="$entity_id" mount_accessor="$mount_accessor"

Mon entité est maintenant associée à deux alias. Je peux alors supprimer les entités générées automatiquement par Vault. (entity_526e8698 entity_608ec3ec)

vault delete identity/entity/name/entity_526e8698 
vault delete identity/entity/name/entity_608ec3ec

Gérer les groupes d’entités

Il existe deux types de groupe d’entités : les groupes internes et les groupes externes.

  • Les groupes internes sont utilisés pour propager des permissions similaires à un ensemble d’utilisateurs.
  • Les groupes externes sont créés par Vault en fonction des méthodes d’authentification utilisées.

Par exemple, sur un LDAP ou un Active Directory, les groupes sont créés automatiquement par Vault et sont associés aux groupes de l’annuaire.

Ainsi : on peut donner des permissions à un groupe d’utilisateurs directement depuis le serveur d’authentification LDAP (nous n’en parlerons pas ici, mais si le sujet vous intéresse je vous invite à consulter la documentation).

Pour les groupes internes, je vais créer un groupe cuistops qui va regrouper les entités Quentin JOLY, Joël SEGUILLON. Un second groupe SRE qui va regrouper les entités Denis GERMAIN, Rémi VERCHERE et Stéphane ROBERT. Enfin, un troisième groupe gophers qui va regrouper les entités Denis GERMAIN et Rémi VERCHERE.

Je vais créer les groupes suivants:

  • cuistops
    • Quentin JOLY
    • Joël SEGUILLON
  • sre
    • Denis GERMAIN
    • Rémi VERCHERE
    • Stéphane ROBERT
  • gophers
    • Denis GERMAIN
    • Rémi VERCHERE

On va jouer sur les permissions de chaque groupe :

  • Les cuistops doivent pouvoir lire et écrire les secrets du chemin kv2/cuistops ainsi que lire les secrets du chemin kv2/sre.
  • Les sre doivent pouvoir lire et écrire les secrets du chemin kv2/sre.
  • Les gophers doivent pouvoir utiliser le moteur de stockage transit ainsi que la base de données gophers en lecture-écriture.

Commençons par créer les groupes.

vault write identity/group name="cuistops" policies="cuistops-policy"
vault write identity/group name="sre" policies="sre-policy"
vault write identity/group name="gophers" policies="gophers-policy"

On liste les groupes pour vérifier qu’ils ont bien été créés.

$ vault list identity/group/name
Keys
----
cuistops
gophers
sre

Je dois aussi créer les entités.

vault write identity/entity name="Joël SEGUILLON"
vault write identity/entity name="Denis GERMAIN"
vault write identity/entity name="Rémi VERCHERE"
vault write identity/entity name="Stéphane ROBERT"

Je n’ai pas trouvé de méthode pour ajouter les entités aux groupes depuis la CLI. Je suis donc dans l’obligation de passer par l’API HTTP qui est plus permissive.

Pour cela, je vais construire un fichier JSON cuistops.json qui va contenir les entités du groupe cuistops ( Quentin JOLY et Joël SEGUILLON).

// cuistops.json
{
  "member_entity_ids": [
    "16dcc20e-e718-04a2-3b9a-f811b57912c9",
    "c6b72e53-fc29-8ec4-eaca-b526c8783319"
  ]
}
cuistops_group_id=$(vault read -format=json identity/group/name/cuistops | jq -r ".data.id")
curl \
    --header "X-Vault-Token: hvs.PjCYLqBpCKfxOjqHrRKXc0Eq" \
    --request POST \
    --data @cuistops.json \
    https://vault-01.servers.une-pause-cafe.fr:8200/v1/identity/group/id/${cuistops_group_id}

Je vais faire la même chose pour les groupes sre et gophers.

Les politiques cuistops-policy, sre-policy et gophers-policy n’existent pas encore, je vais donc les créer.

Créer une politique

Les politiques (ou policies) sont des objets qui définissent les permissions accordées à une entité ou un groupe d’entités. Elles sont utilisées pour définir les permissions accordées à un utilisateur ou une application.

Pour définir une politique, nous pouvons utiliser des fichiers de configuration au format HCL ou JSON. Je préfère utiliser le format HCL car il est plus lisible.

À savoir :

  • Par défaut, ne pas avoir de politique est équivalent à un deny all.
  • Il est possible de cumuler plusieurs politiques sur un token.
  • Le fait de ’lister’ est une permission sur les métadonnées d’un chemin.

Il existe 2 politiques créées par défaut : default et root. La politique root est associée au token racine et donne toutes les permissions sur Vault et la politique default est associée à tous les tokens et donne les permissions de base sur Vault (il est possible de la modifier, mais pas de la supprimer).

Astuce

Pour voir les permissions accordées par une politique, nous pouvons utiliser la commande vault policy read default

# Allow tokens to look up their own properties
path "auth/token/lookup-self" {
    capabilities = ["read"]
}

# Allow tokens to renew themselves
path "auth/token/renew-self" {
    capabilities = ["update"]
}

[...]

En résumé, les permissions par défaut sont:

  • Lire les propriétés de son propre token.
  • Renouveler le TTL de son propre token.

Je vais ensuite créer les politiques suivantes :

  • cuistops-policy qui va permettre de lire et écrire les secrets du chemin kv2/cuistops ainsi que lire les secrets du chemin kv2/sre.
  • sre-policy qui va permettre de lire et écrire les secrets du chemin kv2/sre.
  • gophers-policy qui va permettre d’utiliser le moteur de stockage transit ainsi que la base de données gophers en lecture-écriture.

cuistops-policy.hcl:

path "kv2/data/cuistops/*" {
  capabilities = ["create", "read", "update", "delete"]
}

path "kv2/metadata/cuistops/" {
  capabilities = ["list"]
}

path "kv2/data/sre/*" {
  capabilities = ["read"]
}

path "kv2/metadata/sre/" {
  capabilities = ["list"]
}

sre-policy.hcl:

path "kv2/data/sre/*" {
  capabilities = ["create", "read", "update", "delete"]
}

path "kv2/metadata/sre/" {
  capabilities = ["list"]
}

gophers-policy.hcl:


path "database/creds/gophers/*" {
  capabilities = ["read"]
}

path "transit/encrypt/gophers/*" {
  capabilities = ["create", "read", "update", "delete"]
}

path "transit/decrypt/gophers/*" {
  capabilities = ["create", "read", "update", "delete"]
}

J’applique maintenant ces politiques.

vault policy write cuistops-policy cuistops-policy.hcl
vault policy write sre-policy sre-policy.hcl
vault policy write gophers-policy gophers-policy.hcl

Les policies sont créées et déjà associées aux bons groupes (puisque je les ai précisés lors de la création des groupes).

Tester les permissions

Je vais ensuite tester les permissions accordées par les politiques. En commençant par m’authentifier en tant que Quentin JOLY et essayant de lire et écrire des secrets dans le chemin kv2/cuistops et kv2/sre.

$ vault login -method=userpass username="quentinj" password="password"

Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                    Value
---                    -----
token                  hvs.CAESIEgs5KsUISf1joUtQua8KMrvOz9C3YWXy-6FIFxRtf3cGh4KHGh2cy5DTncyTFkzZjNzZlJUc2x5STEza0NTMm4
token_accessor         L9kHlxrAj0i63MiIlFauhkbL
token_duration         768h
token_renewable        true
token_policies         ["default"]
identity_policies      ["cuistops-policy" "default" "gophers-policy"]
policies               ["cuistops-policy" "default" "gophers-policy"]
token_meta_username    quentinj

Puis j’essaie de lire et écrire des secrets dans le chemin kv2/cuistops et kv2/sre.

$ vault kv put kv2/cuistops/twitch_channel name="cuistops" description="On sait pas cuire des pâtes, mais on sait faire de l'informatique"

Je peux donc lire et écrire des secrets dans le chemin kv2/cuistops mais je ne peux que lire les secrets du chemin kv2/sre.

$ vault kv get -format=json kv2/sre/gitlab-prod-01 | jq -r ".data.data.org"
bgthree-project

Si j’essaye de créer un secret dans le chemin kv2/sre, je reçois une erreur.

$ vault kv put kv2/sre/kubeconfig-prod-01 data="$(base64 < ~/.kube/config )"
Error writing data to kv2/data/sre/kubeconfig-prod-01: Error making API request.

URL: PUT https://vault-01.servers.une-pause-cafe.fr:8200/v1/kv2/data/sre/kubeconfig-prod-01
Code: 403. Errors:

* 1 error occurred:
        * permission denied

Maintenant, je vais m’authentifier en tant que Denis GERMAIN et essayer de lire et écrire des secrets dans le chemin kv2/sre.

$ vault login -method=userpass username="dgermain" password="chaoticgood"
$ vault kv put kv2/sre/kubeconfig-prod-01 data="$(base64 < ~/.kube/config )"
========= Secret Path =========
kv2/data/sre/kubeconfig-prod-01

======= Metadata =======
Key                Value
---                -----
created_time       2024-02-02T17:56:47.19235595Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

Denis devrait aussi pouvoir accéder à la base de données gophers.

// vault read -format=json database/creds/gophers
{
  "request_id": "ddc04383-fe04-43a2-6661-aa1b04e57f31",
  "lease_id": "database/creds/gophers/jD4TGhM2WNQYojPN6e57bEVX",
  "lease_duration": 3600,
  "renewable": true,
  "data": {
    "password": "Y6p0Frd7Kjmsoo5JMTI-",
    "username": "v-userpass-d-gophers-lK8HGEpt"
  },
  "warnings": null
}

Il peut également utiliser le moteur de stockage transit et chiffrer des données.

$ vault write transit/encrypt/gophers plaintext="$(echo "hello-world" | base64)"
Key            Value
---            -----
ciphertext     vault:v1:J6Q4fH0STG3ZexKXagllrwvywhOA/wWo3+w0YThAhh+IZJDUIJlzUQ==
key_version    1

Chaque acteur a bien les permissions qui lui sont accordées (et seulement celles-ci).

Si (admettons), Rémi VERCHERE souhaite saboter les travaux de CuistOps parce qu’il n’a pas été convaincu par la propagande autour de Kubevirt, il n’aura aucun pouvoir en dehors de son groupe gophers et sre.

$ vault login -method=userpass username="rverchere" password="cloudsavior"
$ vault kv delete kv2/cuistops/twitch_channel
Error deleting kv2/data/cuistops/twitch_channel: Error making API request.

URL: DELETE https://vault-01.servers.une-pause-cafe.fr:8200/v1/kv2/data/cuistops/twitch_channel
Code: 403. Errors:
* 1 error occurred:
        * permission denied

Le live de CuistOps est sain et sauf 😄 (lundi à 21h).

Audit Devices

Il est possible de mettre en place un audit device pour enregistrer les actions effectuées par les utilisateurs et les applications. Cela permet de suivre les modifications apportées aux secrets. Celui-ci va créer des fichiers au format JSON qui contiennent les actions et les réponses du serveur. Les informations sensibles sont chiffrées avant d’être stockées.

vault audit enable file file_path=/var/log/vault_audit.log

Il est possible d’utiliser un agent syslog pour envoyer les logs vers un serveur distant ou un agent de collecte de logs comme Fluentd, Logstash ou Loki.

Je peux ainsi voir l’attaque de Rémi sur les secrets de CuistOps.

{
  "time": "2024-02-03T08:04:35.367089471Z",
  "type": "response",
  "auth": {
    "client_token": "hmac-sha256:9195377f74e705992be845acf07dd622bb255185dc3dcd84c3be24d1e138aa5d",
    "accessor": "hmac-sha256:111fef38c3b949f2a5a649c74c6cf3f0ee1e08b9c90243fe8f8ccd96dbbadcb7",
    "display_name": "userpass-rverchere",
    "policies": [
      "default",
      "gophers-policy",
      "sre-policy"
    ],
    "token_policies": [
      "default"
    ],
    "identity_policies": [
      "gophers-policy",
      "sre-policy"
    ],
    "policy_results": {
      "allowed": false
    },
    "metadata": {
      "username": "rverchere"
    },
    "entity_id": "bf4db72e-fba7-d210-9a1e-b8348c112dd9",
    "token_type": "service",
    "token_ttl": 2764800,
    "token_issue_time": "2024-02-03T09:03:27+01:00"
  },
  "request": {
    "id": "98473b8c-5d05-b986-827d-46eca65fd3e4",
    "client_id": "bf4db72e-fba7-d210-9a1e-b8348c112dd9",
    "operation": "delete",
    "mount_point": "kv2/",
    "mount_type": "kv",
    "mount_running_version": "v0.16.1+builtin",
    "mount_class": "secret",
    "client_token": "hmac-sha256:64eee3b469b8e06d370be7fedf94a8c43797dcf15b59ff125d5a1239b8f423b2",
    "client_token_accessor": "hmac-sha256:111fef38c3b949f2a5a649c74c6cf3f0ee1e08b9c90243fe8f8ccd96dbbadcb7",
    "namespace": {
      "id": "root"
    },
    "path": "kv2/data/cuistops/twitch_channel",
    "remote_address": "100.64.0.1",
    "remote_port": 35682
  },
  "response": {
    "mount_point": "kv2/",
    "mount_type": "kv",
    "mount_running_plugin_version": "v0.16.1+builtin",
    "mount_class": "secret",
    "data": {
      "error": "hmac-sha256:746cc2a128105b981aa2198c8c67090bb1b20d5c150d4d253894ae895e893f47"
    }
  },
  "error": "1 error occurred:\n\t* permission denied\n\n"
}

Je peux donc tracer qui a fait quoi, quand et depuis quelle adresse IP !

Conclusion

Après ce long article, j’ai encore l’impression de n’avoir abordé que la partie émergée de l’iceberg.

En effet, j’utilise la fonctionnalité kv de Vault depuis plusieurs années, mais je n’avais jamais pris le temps de me pencher sur les autres particularités de Vault.

Cet article n’est alors que le début de mon exploration de Vault. J’ai encore beaucoup de choses à apprendre sur ses fonctionnalités et sur la manière de les utiliser.

On en reparlera peut-être dans de futurs articles 😉.

Information

Je vous invite à consulter les blogs des copains qui publient régulièrement des articles incroyables :

Allez-y, c’est de la bonne lecture ☕ !