Aller au contenu
Sécurité medium

Vault KV : stocker et versionner vos secrets statiques

28 min de lecture

logo vault

Le secrets engine KV (Key-Value) est le plus utilisé dans Vault. Il stocke des paires clé/valeur comme des mots de passe, clés API ou tokens. La version KV v2 ajoute le versioning : chaque modification crée une nouvelle version, permettant de récupérer ou restaurer des valeurs précédentes.

Ce guide couvre toutes les opérations sur les secrets KV v2, du stockage basique au contrôle de concurrence avec Check-and-Set.

  • Vault installé et démarré (mode dev ou production)
  • Variables d'environnement configurées (VAULT_ADDR, VAULT_TOKEN)

Vault propose deux versions du secrets engine KV, et elles ne diffèrent pas seulement par des options : ce sont deux moteurs distincts, avec des chemins API différents et des policies incompatibles. Une policy écrite pour un mount v1 ne fonctionne pas sur un mount v2. La ligne décisive du tableau est Versioning : c'est elle qui entraîne tout le reste, le soft delete comme le Check-and-Set. Retenez la dernière ligne comme règle de choix : KV v2 est le défaut, KV v1 ne se justifie que sur un mount hérité ou sur un volume d'écriture où le coût du stockage des versions devient un problème mesuré.

AspectKV v1KV v2
VersioningNonOui (conserve les versions)
Soft deleteNonOui (récupérable)
Check-and-SetNonOui (évite les écrasements)
MétadonnéesBasiquesComplètes (timestamps, custom)
Cas d'usageLegacy, très haute performanceStandard, recommandé

Le mount point que vous choisissez ici est définitif dans les faits : il apparaît dans toutes vos policies, dans les URL de l'API et dans les configurations de vos applications. Le renommer plus tard signifie réécrire tout cela. Prenez donc un nom explicite (secret, kv, ou un nom par domaine métier) plutôt que le nom par défaut du mode dev par habitude. La commande de vérification confirme deux choses : que le moteur est monté au bon endroit, et que la colonne Options de la sortie détaillée contient bien version:2. Sans ce -detailed, la liste affiche seulement le type kv et vous ne pouvez pas distinguer une v1 d'une v2.

Fenêtre de terminal
# Activer KV v2 sur le path "secret/"
vault secrets enable -path=secret kv-v2
# Vérifier l'activation
vault secrets list -detailed | grep '^secret/'

vault kv put sert aussi bien à la création qu'à la mise à jour : la commande est la même, c'est Vault qui décide selon l'existence du chemin. Un point mérite d'être posé tout de suite car il surprend souvent : l'écriture remplace l'intégralité du secret, elle ne fusionne pas. Réécrire un seul champ efface donc tous les autres. Pour ne modifier qu'une clé sans toucher aux autres, la commande à utiliser est vault kv patch.

Deux écritures coexistent pour désigner un secret, et elles ne se valent pas. L'aide intégrée de vault kv put qualifie la première de syntaxe dépréciée (« deprecated path-like syntax ») justement parce qu'elle ressemble à un chemin API sans en être un : secret/chemin/du/secret n'existe pas côté serveur, le vrai chemin est secret/data/chemin/du/secret. La seconde forme sépare explicitement le mount point du chemin relatif et supprime cette ambiguïté. Utilisez -mount= partout où vous le pouvez.

Fenêtre de terminal
# Forme dépréciée pour KV v2 : ressemble à un chemin API sans en être un
vault kv put secret/chemin/du/secret clé1="valeur1" clé2="valeur2"
# Forme recommandée avec -mount
vault kv put -mount=secret chemin/du/secret clé1="valeur1" clé2="valeur2"

Imaginons une application web qui a besoin de se connecter à une base de données PostgreSQL :

Fenêtre de terminal
vault kv put -mount=secret apps/webapp/database \
host="db.example.com" \
port="5432" \
username="webapp_user" \
password="SuperSecret123!"

Sortie :

========== Secret Path ==========
secret/data/apps/webapp/database
======= Metadata =======
Key Value
--- -----
created_time 2026-03-16T13:32:40.176016777Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1

Comprendre les chemins API (path réel vs logique)

Section intitulée « Comprendre les chemins API (path réel vs logique) »

Vous écrivez sur secret/apps/webapp/database, mais Vault stocke sur secret/data/apps/webapp/database. Pourquoi ?

KV v2 utilise des sous-chemins API pour séparer les opérations :

OpérationChemin APIExemple
Données (CRUD)secret/data/...secret/data/apps/webapp/database
Métadonnéessecret/metadata/...secret/metadata/apps/webapp/database
Suppression (versions)secret/delete/...secret/delete/apps/webapp/database
Restaurationsecret/undelete/...secret/undelete/apps/webapp/database
Destructionsecret/destroy/...secret/destroy/apps/webapp/database

La CLI masque cette complexité, mais vous la verrez dans :

  • les policies Vault (capabilities sur secret/data/*)
  • l'API HTTP directe
  • les intégrations tierces (Terraform, Kubernetes operator...)

Vault accepte plusieurs formats d'entrée. Pour des secrets complexes, vous pouvez lire depuis un fichier.

Option 1 : Paires clé=valeur depuis stdin

Fenêtre de terminal
# L'argument "-" seul demande à la CLI de lire un objet JSON sur stdin
echo '{"api_key":"sk-abc123","api_secret":"secret-xyz789"}' | \
vault kv put -mount=secret apps/webapp/api-config -

Le contenu JSON est interprété comme plusieurs paires clé=valeur. Vault stockera deux champs : api_key et api_secret. L'intérêt dépasse le confort : les valeurs ne figurent plus dans les arguments du processus vault, donc plus dans la sortie de ps que n'importe quel utilisateur de la machine peut lire. Le echo de la démonstration reste, lui, écrit dans l'historique du shell ; en usage réel, faites arriver le JSON depuis un fichier à permissions 0600 ou depuis la sortie d'un autre outil.

Option 2 : Stocker un blob entier

Si vous voulez stocker un fichier entier (certificat, clé privée) comme une seule valeur :

Fenêtre de terminal
vault kv put -mount=secret apps/webapp/tls-cert \
cert="$(cat server.crt)" \
key="$(cat server.key)"

Option 3 : Fichier JSON avec @

Fenêtre de terminal
# Le fichier doit contenir un objet JSON avec les paires clé-valeur
install -m 0600 /dev/null creds.json
cat > creds.json << 'EOF'
{
"username": "admin",
"password": "secret123"
}
EOF
vault kv put -mount=secret apps/webapp/creds @creds.json
# Effacer le contenu avant de retirer le fichier
shred -u creds.json

Cette commande crée un secret avec deux champs : username et password. Le install -m 0600 crée le fichier vide avec les bonnes permissions avant d'y écrire quoi que ce soit : créer puis chmod laisse une fenêtre pendant laquelle le secret est lisible par tout le monde. shred -u écrase le contenu avant de supprimer l'entrée de répertoire, ce que rm seul ne fait pas. Sa propre page d'aide prévient toutefois qu'il suppose une réécriture sur place : sur un système de fichiers journalisé, copy-on-write ou sur un SSD avec wear leveling, la garantie n'est pas totale. Le vrai réflexe reste d'écrire ce type de fichier dans un tmpfs (/dev/shm, jamais persisté sur disque).

Sans option, vault kv get renvoie la version la plus récente et affiche les valeurs en clair dans le terminal. Deux blocs se succèdent dans la sortie : Metadata, qui décrit l'état de la version (numéro, dates, drapeau destroyed), et Data, qui contient les champs. Le premier réflexe devant un secret qui « ne marche pas » est de lire version et deletion_time : un secret supprimé en douceur affiche encore ses métadonnées mais plus aucune donnée.

Fenêtre de terminal
vault kv get -mount=secret apps/webapp/database

Sortie :

========== Secret Path ==========
secret/data/apps/webapp/database
======= Metadata =======
Key Value
--- -----
created_time 2026-03-16T13:32:40.176016777Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
host db.example.com
password SuperSecret123!
port 5432
username webapp_user

Pour l'intégration dans des scripts, extrayez une seule valeur :

Fenêtre de terminal
# Extraire le mot de passe
vault kv get -mount=secret -field=password apps/webapp/database
# SuperSecret123!

Pour le parsing automatisé, utilisez le format JSON :

Fenêtre de terminal
vault kv get -mount=secret -format=json apps/webapp/database | \
jq -r '.data.data.password'
# SuperSecret123!

Quand plusieurs processus écrivent sur le même secret, il y a un risque d'écrasement silencieux. Check-and-Set (CAS) résout ce problème.

Sans CAS, si deux processus modifient le même secret en parallèle :

  1. Processus A lit la version 5
  2. Processus B lit la version 5
  3. Processus A écrit → version 6
  4. Processus B écrit → version 7, écrasant les changements de A

L'option -cas=<version> impose que l'écriture ne réussisse que si la version courante correspond :

Fenêtre de terminal
# Écrire seulement si la version actuelle est 2
vault kv put -mount=secret -cas=2 apps/webapp/database \
password="NewSecurePassword!"

Si un autre processus a modifié le secret entre-temps :

Error writing data to secret/data/apps/webapp/database: Error making API request.
Code: 400. Errors:
* check-and-set parameter did not match the current version

Le cas particulier -cas=0 signifie « version courante attendue : aucune ». Il transforme l'écriture en création exclusive et échoue si le chemin existe déjà. C'est l'outil du provisionnement : un script de bootstrap qui pose un mot de passe initial doit refuser d'écraser celui d'une installation déjà en service, plutôt que de découvrir la casse au premier redémarrage d'application.

Fenêtre de terminal
# Échoue si le secret existe déjà
vault kv put -mount=secret -cas=0 apps/webapp/new-secret \
api_key="sk-newkey123"

Vous pouvez imposer l'usage de CAS pour tous les écrivains :

Fenêtre de terminal
# Toute écriture sans -cas échouera
vault kv metadata put -mount=secret -cas-required=true apps/webapp/database

Tentative d'écriture sans CAS :

Error writing data to secret/data/apps/webapp/database: Error making API request.
Code: 400. Errors:
* check-and-set parameter required for this call

Chaque modification d'un secret crée une nouvelle version. C'est l'un des avantages majeurs de KV v2.

Quand vous modifiez un secret, la version incrémente automatiquement :

Fenêtre de terminal
# Mise à jour du mot de passe (crée version 2)
vault kv put -mount=secret apps/webapp/database \
host="db.example.com" \
port="5432" \
username="webapp_user" \
password="NewPassword456!"

Sortie :

...
version 2

Toutes les versions restent lisibles tant qu'elles n'ont pas été détruites ou évincées par max-versions. C'est ce qui permet de répondre en quelques secondes à « quelle valeur ce secret avait-il avant le déploiement de mardi », sans restaurer une sauvegarde. Attention à la contrepartie : un mot de passe révoqué reste lisible dans l'historique par quiconque a read sur le chemin, ce qui rend max-versions et delete-version-after nécessaires sur les secrets sensibles.

Fenêtre de terminal
# Lire la version 1 (ancien mot de passe)
vault kv get -mount=secret -version=1 apps/webapp/database

vault kv metadata get liste toutes les versions sans jamais afficher leurs valeurs. C'est la commande à donner à quelqu'un qui doit auditer un secret sans avoir le droit de le lire : elle ne demande la capability read que sur secret/metadata/.... Dans la sortie, current_version indique ce que renvoie un get sans option, oldest_version la plus ancienne version encore récupérable, et chaque bloc Version N porte son deletion_time et son drapeau destroyed. Une version détruite reste listée : c'est la métadonnée qui subsiste, pas la donnée.

Fenêtre de terminal
vault kv metadata get -mount=secret apps/webapp/database

Sortie (extrait) :

======= Metadata =======
Key Value
--- -----
cas_required false
created_time 2026-03-16T13:32:40.176016777Z
current_version 2
delete_version_after 0s
max_versions 0
oldest_version 0
updated_time 2026-03-16T13:45:12.234567890Z
====== Version 1 ======
Key Value
--- -----
created_time 2026-03-16T13:32:40.176016777Z
deletion_time n/a
destroyed false
====== Version 2 ======
Key Value
--- -----
created_time 2026-03-16T13:45:12.234567890Z
deletion_time n/a
destroyed false

Si une mise à jour pose problème, vous pouvez "rollback" :

Fenêtre de terminal
# Restaurer la version 1 comme nouvelle version (crée version 3)
vault kv rollback -mount=secret -version=1 apps/webapp/database

KV v2 distingue plusieurs niveaux de suppression. Le modèle mental :

ActionEffetRéversible ?
deleteMasque la version (soft delete)Oui (undelete)
undeleteRend la version visible à nouveauSans objet
destroySupprime les données de la versionNon
metadata deleteSupprime tout : versions + métadonnéesNon

Marque la version comme supprimée mais conserve les données :

Fenêtre de terminal
vault kv delete -mount=secret apps/webapp/database

Les données ne sont plus accessibles par un get normal :

Fenêtre de terminal
vault kv get -mount=secret apps/webapp/database
# Affiche uniquement les métadonnées, pas les données

undelete efface le deletion_time d'une version et la rend de nouveau lisible. Deux différences avec delete méritent l'attention. La restauration exige la liste explicite des versions via -versions=, il n'y a pas de comportement par défaut sur la dernière. Et elle n'a d'effet que sur une version simplement supprimée : sur une version détruite, la commande réussit sans rien ramener, puisque les données n'existent plus.

Fenêtre de terminal
# Restaurer la version 2
vault kv undelete -mount=secret -versions=2 apps/webapp/database

Les données sont à nouveau accessibles.

Supprime définitivement les données d'une ou plusieurs versions :

Fenêtre de terminal
# Supprimer définitivement les versions 1 et 2
vault kv destroy -mount=secret -versions=1,2 apps/webapp/database

Pour supprimer complètement un secret (toutes versions + métadonnées) :

Fenêtre de terminal
vault kv metadata delete -mount=secret apps/webapp/database

Cette commande supprime tout : le secret n'existe plus dans Vault.

Une bonne organisation facilite la gestion des policies et la maintenance. La structure de vos chemins détermine directement vos policies de sécurité.

L'arborescence ci-dessous n'est pas une convention Vault : le moteur accepte n'importe quel chemin. Elle est construite pour que chaque branche corresponde à une policy. Regardez le premier niveau (apps/, infra/, teams/) : c'est lui qui portera vos wildcards. Un secret rangé au mauvais endroit oblige soit à élargir une policy existante, soit à en écrire une pour une seule clé, et c'est ainsi que les autorisations dérivent.

secret/
├── apps/ # Secrets applicatifs
│ ├── webapp/
│ │ ├── database # Credentials DB
│ │ ├── api-keys # Clés API tierces
│ │ └── config # Config sensible
│ └── mobile-app/
│ └── firebase
├── infra/ # Secrets infrastructure
│ ├── aws/
│ │ └── credentials
│ └── docker-registry/
│ └── credentials
└── teams/ # Secrets par équipe
├── dev/
└── ops/

Cette hiérarchie permet d'écrire des policies précises sans wildcards trop larges :

# Policy pour l'application webapp (lecture seule)
path "secret/data/apps/webapp/*" {
capabilities = ["read"]
}
# Policy pour l'équipe ops (lecture/écriture sur infra)
path "secret/data/infra/*" {
capabilities = ["create", "update", "read", "delete"]
}
path "secret/metadata/infra/*" {
capabilities = ["list", "read"]
}

Quatre axes de découpage sont possibles et vous devrez en choisir un comme niveau le plus haut de l'arborescence, puis combiner les autres en dessous. Le critère à placer en premier est celui sur lequel vous voulez la frontière d'isolation la plus stricte. Sur la plupart des installations c'est l'environnement : une policy de production ne doit jamais pouvoir déraper vers un chemin de développement à cause d'un wildcard mal placé.

CritèreExemple de cheminAvantage
Par applicationsecret/apps/webapp/...Isolation par service
Par environnementsecret/prod/..., secret/dev/...Séparation prod/dev
Par équipesecret/teams/backend/...Ownership clair
Par typesecret/databases/..., secret/api-keys/...Gestion transverse

Combinez ces critères selon votre contexte : secret/prod/apps/webapp/database ou secret/apps/webapp/prod/database.

vault kv list explore un niveau de l'arborescence à la fois : il ne descend pas récursivement. Les entrées terminées par / sont des répertoires intermédiaires qu'il faut lister à leur tour, celles sans slash sont des secrets. Cette commande frappe le chemin secret/metadata/... et non secret/data/... : une application qui a le droit de lire ses secrets peut donc très bien recevoir un permission denied sur un list, ce qui est le comportement souhaité.

Fenêtre de terminal
# Lister les secrets sous un chemin
vault kv list -mount=secret apps/

Sortie :

Keys
----
webapp/
mobile-app/

custom_metadata attache des paires clé/valeur au secret entier, pas à une version. Ces données sont lisibles par quiconque a read sur secret/metadata/..., donc sans accès aux valeurs : c'est exactement ce qu'il faut pour qu'une équipe d'exploitation sache qui possède un secret et depuis quand il n'a pas tourné, sans lui donner le secret. N'y mettez jamais de donnée sensible, elles ne bénéficient d'aucune protection supplémentaire.

Fenêtre de terminal
vault kv metadata put -mount=secret \
-custom-metadata="owner=team-backend" \
-custom-metadata="environment=production" \
-custom-metadata="rotation=90days" \
apps/webapp/database

Par défaut, Vault conserve toutes les versions. Cela peut poser problème :

  • Croissance du stockage
  • Conservation d'anciennes valeurs sensibles plus longtemps que nécessaire

Limitez avec -max-versions :

Fenêtre de terminal
# Conserver uniquement les 10 dernières versions
vault kv metadata put -mount=secret -max-versions=10 apps/webapp/database

La même limite se définit une fois pour tout le moteur, via son endpoint de configuration secret/config. Les clés qui ne fixent pas leur propre max_versions héritent alors de cette valeur :

Fenêtre de terminal
vault write secret/config max_versions=10
vault read secret/config

max-versions borne le nombre de versions, delete-version-after borne leur durée de vie. Les deux répondent à des besoins différents : le premier contient la volumétrie, le second répond à une exigence de rétention (« aucun mot de passe révoqué ne doit rester récupérable plus de 24 heures »). Le délai s'applique aux nouvelles versions écrites après ce réglage : Vault leur inscrit un deletion_time calculé à l'écriture. Attention au piège : ce que déclenche l'échéance est un soft delete, pas un destroy. Les données restent présentes dans le stockage et un undelete les ramène. Pour une véritable exigence d'effacement, combinez ce réglage avec max-versions ou un destroy explicite.

Fenêtre de terminal
# Marquer les nouvelles versions pour un soft delete 24h après leur écriture
vault kv metadata put -mount=secret -delete-version-after=24h apps/webapp/database

Ce guide montre la gestion via CLI. En production, vos applications consomment les secrets via d'autres méthodes :

MéthodeUsage typique
API HTTPIntégration directe dans le code
Agent + templatesInjection dans fichiers de config
CSI driverMontage comme volume Kubernetes
Vault Secrets OperatorSynchronisation vers Secrets K8s

Ces quatre méthodes ne changent rien au moteur KV, elles changent qui s'authentifie. Aucune ne doit dépendre d'un token écrit dans un fichier de configuration : l'application prouve son identité (compte de service Kubernetes, rôle IAM, certificat) et Vault lui rend un token à durée de vie courte. Le guide Vault comme courtier d'identité décrit ce mécanisme, et Authentification détaille les méthodes disponibles.

KV v2 améliore énormément la gestion des secrets statiques, mais il a des limites structurelles :

CapacitéKV v2Alternative Vault
Générer des secrets dynamiquesNonDatabase, AWS, Azure...
Rotation automatique côté cibleNonDatabase secrets engine
Chiffrement de données applicativesNonTransit
Certificats X.509NonPKI
Clés SSH signéesNonSSH secrets engine

Règle simple : si vous pouvez éviter un secret statique, faites-le.

  • Base de données → Database secrets engine (credentials éphémères)
  • Cloud provider → AWS/Azure/GCP secrets engine
  • Certificats → PKI secrets engine
  • SSH → SSH secrets engine avec certificats

KV v2 reste indispensable pour :

  • Clés API tierces (imposées par le fournisseur)
  • Secrets legacy (systèmes non intégrables)
  • Tokens partagés (Slack webhooks, API tokens...)
  • Bootstrap secrets (premiers credentials pour accéder au reste)

Deux familles d'erreurs dominent sur KV v2, et elles se distinguent au message. Tout ce qui parle de chemin (no value found, permission denied) vient presque toujours d'un oubli du segment data/ ou metadata/ dans une policy : la CLI l'ajoute pour vous, le moteur de policies non. Tout ce qui parle de check-and-set vient d'une écriture concurrente ou d'un cas-required posé sur la clé ; dans les deux cas la réponse est la même, relire la version courante avec vault kv get puis rejouer l'écriture avec le bon -cas.

SymptômeCause probableSolution
no value found at secret/data/...Secret inexistant ou suppriméVérifier le path, utiliser undelete
permission deniedPolicy insuffisanteVérifier les capabilities read, list sur secret/data/...
check-and-set parameter did not matchÉcriture concurrenteRelire la version actuelle, utiliser -cas=<version>
check-and-set parameter requiredcas-required=true sur la cléAjouter -cas=<version> à la commande
Version non trouvéeVersion détruiteVérifier avec metadata get
Versions anciennes inaccessiblesmax-versions atteintAugmenter la limite ou accepter la perte
  1. KV v2 est le standard pour les secrets statiques, préférez les secrets dynamiques quand c'est possible
  2. Check-and-Set (-cas) évite les écrasements concurrents, activez -cas-required sur les secrets critiques
  3. La CLI masque les chemins API, vos policies doivent cibler secret/data/* et secret/metadata/*
  4. delete = soft delete récupérable, destroy = définitif, metadata delete = suppression totale
  5. Organisez vos secrets pour faciliter les policies : par application/environnement/équipe
  6. Les métadonnées personnalisées documentent mais ne déclenchent rien
  7. Limitez les versions avec -max-versions pour éviter la croissance infinie et la rétention excessive

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn