Synchroniser les secrets avec un HashiCorp Vault
Mise à jour le · Voir sur GitHub
Une instance Secret Manager est une instance OpenBao, compatible avec l'API KV version 2 de HashiCorp Vault. Votre outillage Vault existant — la CLI officielle vault, les SDK, l'External Secrets Operator — se connecte nativement à l'instance, sans passerelle ni adaptation de protocole.
Ce guide montre comment synchroniser les secrets dans les deux sens :
- Import : propager les secrets de votre HashiCorp Vault vers votre instance Secret Manager (onboarding, migration).
- Export : propager les secrets de votre instance Secret Manager vers votre HashiCorp Vault (réversibilité).
- Synchronisation continue : maintenir les deux magasins alignés automatiquement avec l'External Secrets Operator, rotations incluses.
Il n'existe pas de produit de réplication intégré entre un HashiCorp Vault et le Secret Manager : la synchronisation s'appuie sur les outils clients standards. Chaque commande de ce guide a été validée de bout en bout sur une instance Secret Manager exécutant OpenBao 2.5.4, avec la CLI officielle HashiCorp Vault 1.20 et l'External Secrets Operator 2.14 authentifié par JWT/OIDC — aucun token statique.
Prérequis
- Une instance Secret Manager à l'état
RUNNING— voir Créer une instance ; - Le token admin de l'instance — voir Récupérer le token admin ;
- La CLI officielle HashiCorp Vault installée localement (toute version compatible avec l'API KV version 2) ;
- Un HashiCorp Vault contenant vos secrets existants, joignable depuis votre poste ;
- Un fournisseur d'identité capable d'émettre des JWT de courte durée, avec sa clé publique de signature — utilisé par la section External Secrets Operator.
Le parcours console pour les deux premiers prérequis :

Définissez les variables d'environnement utilisées dans ce guide :
export BAO_ADDR="https://{instance-host}" # instance Secret Manager Numspot
export BAO_TOKEN="{admin-token}" # token admin de l'instance
export VAULT_ADDR="http://{votre-vault}:8200" # votre HashiCorp Vault
export VAULT_TOKEN="{vault-token}"
Connecter la CLI officielle Vault
Le token admin renvoyé par l'API Numspot authentifie chaque appel à l'instance. La CLI officielle Vault ne lit que les variables VAULT_ADDR et VAULT_TOKEN — elle ignore BAO_ADDR/BAO_TOKEN, qui portent les identifiants de l'instance. Les commandes qui ciblent l'instance portent donc un préfixe explicite, et une commande nue s'exécute toujours contre votre propre Vault :
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" vault status
Résultat attendu (l'instance utilise l'auto-unseal Transit et le stockage Raft) :
Key Value
--- -----
Seal Type transit
Initialized true
Sealed false
Version 2.5.4
Storage Type raft
HA Mode active
La même convention s'applique à toutes les commandes ciblant l'instance ci-dessous (vault kv put, vault kv get, vault kv list, vault kv metadata get) : VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" sélectionne l'instance Secret Manager, tandis qu'une commande nue lit $VAULT_ADDR/$VAULT_TOKEN et cible votre propre Vault.
Vérifier le moteur KV version 2
L'instance expose un moteur KV version 2 sur le mount kv/. Listez les mounts et confirmez la version :
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" \
vault secrets list -format=json | jq 'with_entries(select(.value.type == "kv"))'
{
"kv/": {
"type": "kv",
"options": { "version": "2" }
}
}
Une première écriture-lecture sur l'instance valide l'accès de bout en bout :
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" \
vault kv put kv/onboarding/check value=demo-value
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" \
vault kv get kv/onboarding/check
Importer les secrets depuis votre Vault
Les secrets se transportent un par un : lecture depuis le Vault source, écriture vers le Secret Manager. Pour un secret isolé :
# Lecture du secret depuis votre HashiCorp Vault ($VAULT_ADDR / $VAULT_TOKEN)
vault kv get -format=json secret/mon-chemin/api-key | jq '.data.data' > api-key.json
# Écriture de la carte de données vers le Secret Manager ($BAO_ADDR / $BAO_TOKEN)
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" \
vault kv put kv/mon-chemin/api-key @api-key.json
Pour plusieurs chemins, répétez la paire de commandes pour chaque secret. L'étape jq '.data.data' extrait la carte de données du secret : vault kv get -format=json affiche la réponse API complète (request_id, lease_id, data, …) et vault kv put … @fichier.json stocke le fichier tel quel comme données du secret — sans cette extraction, c'est l'enveloppe de la réponse qui serait stockée à la place de la valeur. Le passage par un fichier (@fichier.json) garantit le transfert à l'identique des valeurs multi-lignes (certificats, clés privées).
Vérifiez que la valeur transférée est strictement identique des deux côtés en comparant un digest SHA-256 :
# Comparaison des digests (les deux doivent être identiques)
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" \
vault kv get -field=key kv/mon-chemin/api-key | sha256sum
vault kv get -field=key secret/mon-chemin/api-key | sha256sum
# Les deux commandes affichent le même digest
Réexporter les secrets vers votre Vault
Le chemin inverse propage les secrets du Secret Manager vers votre HashiCorp Vault — c'est le mécanisme de réversibilité :
# Lecture depuis le Secret Manager ($BAO_ADDR / $BAO_TOKEN)
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" \
vault kv get -format=json kv/mon-chemin/client-cert | jq '.data.data' > client-cert.json
# Écriture vers le Vault client ($VAULT_ADDR / $VAULT_TOKEN)
vault kv put secret/mon-chemin/client-cert @client-cert.json
La synchronisation transporte les valeurs des secrets. L'historique de versions KV version 2 n'est pas répliqué : un secret passé en version 2 côté source est écrit en version 1 côté cible, avec la dernière valeur. Anticipez ce comportement dans vos workflows de rotation.
Synchronisation continue avec l'External Secrets Operator
Pour une synchronisation automatique et continue, utilisez l'External Secrets Operator (ESO) avec son provider Vault. L'exemple ci-dessous déploie ESO dans un cluster Kubernetes et enchaîne les deux directions :
- un
ExternalSecretqui lit votre HashiCorp Vault, suivi d'unPushSecretqui écrit dans le Secret Manager ; - la chaîne symétrique dans l'autre sens.
Installez ESO avec le chart officiel :
helm repo add external-secrets https://charts.external-secrets.io
helm install eso external-secrets/external-secrets \
--namespace eso --create-namespace \
--set installCRDs=true
La synchronisation utilise la méthode d'authentification JWT/OIDC de l'API Vault des deux côtés : ESO présente un JWT de courte durée émis par votre fournisseur d'identité, et chaque magasin le mappe vers un rôle à privilèges minimum. Aucun token statique n'est stocké dans le cluster — seul le JWT y figure, qui expire et se renouvelle comme toute identité de workload OIDC.
Côté instance Secret Manager, activez la méthode sur un mount dédié, enregistrez la clé publique de signature de votre fournisseur d'identité et créez le rôle de synchronisation. La policy de push couvre à la fois les données et les métadonnées KV du chemin cible (ESO relit la clé distante et met à jour ses métadonnées à chaque push), plus l'accès en lecture aux chemins réexportés vers votre Vault :
# Instance Secret Manager ($BAO_ADDR / $BAO_TOKEN)
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" vault auth enable -path=client-idp jwt
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" \
vault write auth/client-idp/config jwt_validation_pubkeys=@idp-public.pem
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" vault policy write eso-push - <<'EOF'
path "kv/data/via-eso/*" {
capabilities = ["create", "update", "read"]
}
path "kv/metadata/via-eso/*" {
capabilities = ["create", "update", "read"]
}
path "kv/data/mon-chemin/*" {
capabilities = ["read"]
}
EOF
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" vault write auth/client-idp/role/eso \
role_type=jwt bound_issuer="https://idp.exemple.com" \
bound_audiences="numspot-sync" user_claim=sub \
token_policies="eso-push" token_ttl=15m
Sur votre propre Vault, la même méthode avec un rôle symétrique : lecture des chemins sources, écriture du chemin de synchronisation :
# Votre HashiCorp Vault ($VAULT_ADDR / $VAULT_TOKEN)
vault auth enable jwt
vault write auth/jwt/config jwt_validation_pubkeys=@idp-public.pem
vault policy write eso-sync - <<'EOF'
path "secret/data/mon-chemin/*" {
capabilities = ["read"]
}
path "secret/data/via-eso/*" {
capabilities = ["create", "update", "read"]
}
path "secret/metadata/via-eso/*" {
capabilities = ["create", "update", "read"]
}
EOF
vault write auth/jwt/role/eso role_type=jwt \
bound_issuer="https://idp.exemple.com" \
bound_audiences="numspot-sync" user_claim=sub \
token_policies="eso-sync" token_ttl=15m
Avec un fournisseur d'identité de production, préférez oidc_discovery_url à une clé publique statique : le point de terminaison JWKS permet aux deux magasins de prendre en compte automatiquement les rotations de clés de signature. Les valeurs bound_issuer, bound_audiences et user_claim doivent correspondre aux claims des JWT émis par votre fournisseur pour le workload de synchronisation.
Stockez le JWT émis par votre fournisseur d'identité dans un secret Kubernetes, puis déclarez un SecretStore par Vault — le champ auth.jwt.path doit correspondre au mount de la méthode d'authentification de chaque magasin :
kubectl create secret generic idp-jwt -n eso --from-literal=token="$IDP_JWT"
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: numspot-store
namespace: eso
spec:
provider:
vault:
server: https://{instance-host}
path: kv
version: v2
auth:
jwt:
path: client-idp
role: eso
secretRef:
name: idp-jwt
key: token
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: client-store
namespace: eso
spec:
provider:
vault:
server: http://{votre-vault}:8200
path: secret
version: v2
auth:
jwt:
path: jwt
role: eso
secretRef:
name: idp-jwt
key: token
Enchaînez les deux directions : import depuis votre Vault puis push vers le Secret Manager ; lecture du Secret Manager puis push retour vers votre Vault.
# Sens 1 : Vault client -> Secret Manager
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: from-client-api-key
namespace: eso
spec:
refreshInterval: 5s
secretStoreRef:
name: client-store
kind: SecretStore
target:
name: demo-api-key
data:
- secretKey: key
remoteRef:
key: mon-chemin/api-key
property: key
---
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: push-api-key-to-numspot
namespace: eso
spec:
refreshInterval: 5s
secretStoreRefs:
- name: numspot-store
kind: SecretStore
selector:
secret:
name: demo-api-key
dataTo:
- storeRef:
name: numspot-store
remoteKey: via-eso/api-key
match:
regexp: "^key$"
---
# Sens 2 : Secret Manager -> Vault client
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: from-numspot-client-cert
namespace: eso
spec:
refreshInterval: 5s
secretStoreRef:
name: numspot-store
kind: SecretStore
target:
name: demo-client-cert
data:
- secretKey: cert
remoteRef:
key: mon-chemin/client-cert
property: cert
---
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: push-cert-to-client
namespace: eso
spec:
refreshInterval: 5s
secretStoreRefs:
- name: client-store
kind: SecretStore
selector:
secret:
name: demo-client-cert
dataTo:
- storeRef:
name: client-store
remoteKey: via-eso/client-cert
match:
regexp: "^cert$"
Vérifiez l'état de la synchronisation, puis relisez les valeurs synchronisées dans les deux magasins :
kubectl get externalsecrets,pushsecrets -n eso
# NAME STORE STATUS READY
# from-client-api-key client-store SecretSynced True
# push-api-key-to-numspot numspot-store Synced True
# ...
VAULT_ADDR="$BAO_ADDR" VAULT_TOKEN="$BAO_TOKEN" \
vault kv get kv/via-eso/api-key # sur l'instance Secret Manager
vault kv get secret/via-eso/client-cert # sur votre Vault
Avec un refreshInterval de 5 secondes, une rotation effectuée sur le Vault source se propage automatiquement vers le magasin cible en quelques secondes — environ 20 secondes mesurées pour la chaîne complète (lecture puis écriture), sans intervention manuelle.
Expiration et révocation des tokens
Le TTL du JWT est le contrôle de révocation de la synchronisation : émettez des tokens à durée courte, et la révocation du workload de synchronisation consiste simplement à cesser de les renouveler. À l'expiration du JWT, l'opérateur remonte une erreur d'authentification explicite — la méthode jwt de Vault/OpenBao rejette le token expiré avec token is expired, et le statut des deux ressources le signale :
kubectl get externalsecrets,pushsecrets -n eso
# NAME STORE READY STATUS
# from-client-api-key client-store False SecretSyncedError
# push-api-key-to-numspot numspot-store False Errored
Les secrets déjà synchronisés dans Kubernetes sont conservés tels quels : l'opérateur ne supprime ni ne vide jamais un secret cible parce qu'une authentification a échoué. Les magasins continuent de servir les dernières valeurs synchronisées à vos workloads pendant que l'erreur reste visible dans le statut des ressources.
Rétablir l'accès tient en une commande — remplacez le JWT du secret Kubernetes par un token fraîchement émis ; l'opérateur le récupère au prochain cycle de refresh et la synchronisation reprend, sans modification des secrets synchronisés :
kubectl create secret generic idp-jwt -n eso --from-literal=token="$NOUVEAU_IDP_JWT" \
--dry-run=client -o yaml | kubectl apply -f -
La méthode d'authentification jwt de Vault/OpenBao valide la signature, l'émetteur, l'audience et l'expiration du token — elle ne tient pas de liste de révocation en ligne. Des TTL courts (minutes, pas des jours) borneront l'exposition d'un token révoqué ou compromis, et oidc_discovery_url permet aux magasins de prendre en compte automatiquement les rotations de clés de signature.
Points d'attention
- Pas de réplication de l'historique : la synchronisation transporte des valeurs (voir l'encadré ci-dessus). La réplication complète de l'historique d'un magasin Vault relève de fonctionnalités Vault Enterprise, hors périmètre d'un déploiement Vault standard.
- Chemins de mount différents : un Vault de développement expose le KV sur
secret/tandis que l'instance Secret Manager utilisekv/. Adaptez les chemins dans chaque commande et dans les champsremoteRef— la version du moteur (v2) est identique des deux côtés. - Récupération du token admin : le token se récupère une seule fois via l'API Numspot — conservez-le dans un emplacement sécurisé, il n'est pas réaffiché ensuite. Le token admin est destiné aux opérations d'administration (imports initiaux, configuration) : pour les workloads récurrents comme la synchronisation ci-dessus, utilisez la méthode d'authentification JWT/OIDC — voir Configurer le SSO et Méthodes d'authentification.
- Sécurité du transport : le point d'accès de l'instance sert un certificat TLS signé publiquement ; la CLI officielle Vault s'y connecte sans option de certificat particulière.
- Migration complète d'un magasin : pour déplacer tout le contenu d'une instance (tous les secrets, versions et la configuration), utilisez les snapshots Raft plutôt que la synchronisation secret par secret — voir Backups et restores.
Pour aller plus loin
- Concepts Secret Manager : architecture et cycle de vie du service.
- Configurer le SSO : authentification OIDC, JWT et LDAP pour la production.
- Backups et restores : migration complète par snapshot Raft.
- Réversibilité : politique générale de réversibilité du service.