Vault de A à Y
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 secretwrite
pour écrire un secretdelete
pour supprimer un secretlist
pour lister les secretspatch
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 typeshamir
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.
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.
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 :
Node | IP | fqdn |
---|---|---|
vault-01 | 100.64.0.12 | vault-01.servers.une-pause-cafe.fr |
vault-02 | 100.64.0.14 | vault-02.servers.une-pause-cafe.fr |
vault-03 | 100.64.0.15 | vault-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 stockagekv
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 secretvault kv put
pour créer ou mettre à jour un secretvault kv list
pour lister les secretsvault 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), unvault 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é :
- L’utilisateur s’authentifie sur Vault.
- Vault vérifie l’identité de l’utilisateur.
- Vault donne un token à l’utilisateur.
- 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.
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 :
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 cheminkv2/cuistops
ainsi que lire les secrets du cheminkv2/sre
. - Les
sre
doivent pouvoir lire et écrire les secrets du cheminkv2/sre
. - Les
gophers
doivent pouvoir utiliser le moteur de stockagetransit
ainsi que la base de donnéesgophers
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 cheminkv2/cuistops
ainsi que lire les secrets du cheminkv2/sre
.sre-policy
qui va permettre de lire et écrire les secrets du cheminkv2/sre
.gophers-policy
qui va permettre d’utiliser le moteur de stockagetransit
ainsi que la base de donnéesgophers
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 ☕ !