Aller au contenu principal

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.
info

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 :

Liste des instances Secret Manager dans la console

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
info

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 ExternalSecret qui lit votre HashiCorp Vault, suivi d'un PushSecret qui é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 -
info

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 utilise kv/. Adaptez les chemins dans chaque commande et dans les champs remoteRef — 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​