Medusa — export et restauration¶
Chaque nuit, un CronJob exporte les données KV du coffre OpenBao avec
Medusa, les chiffre et les dépose dans le bucket
scw-openbao-backup. Cette page décrit comment récupérer un de ces exports et le réinjecter.
Pour savoir quel dispositif couvre quelle perte, voir la vue d'ensemble.
Prérequis¶
| Quoi | Détail |
|---|---|
| Cluster | service-prod, namespace openbao-exporter |
| Droits | kubectl en écriture sur ce namespace : créer un pod, exécuter dedans |
| Dépôt | tooling-infra cloné sur votre poste, il contient le manifeste du pod |
| Root token OpenBao | nécessaire à l'import |
Le dépôt est nécessaire parce que la procédure applique un manifeste qui s'y trouve :
git clone git@gitlab.ademe.fr:ademe-group/infrastructure/tooling-infra.git
cd tooling-infra/build/k8s-openbao-restore
Convention des commandes
$VAULT_ADDR, $VAULT_TOKEN et $POD sont des variables que vous définissez au fil de la
procédure, laissez-les telles quelles.
Tout ce qui est en MAJUSCULES sans $ est à remplacer par votre valeur.
Le reste (clés S3, clé de chiffrement) est déjà dans le cluster et sera monté dans le pod. Vous n'avez aucun identifiant à manipuler.
Le root token est indispensable
L'identité utilisée par l'exporter est en lecture seule : elle sait lire les secrets, pas les écrire ni créer un mount.
Un token OIDC de type APPSVC-* ne suffit pas non plus : il donne accès aux données, pas à la
configuration du coffre.
Si vous ne l'avez pas, demandez-le à l'équipe DevOps. N'improvisez pas.
Vérifiez d'abord que vous êtes sur le bon cluster¶
Cette procédure ne concerne que le cluster de service
OpenBao n'est déployé que là. Le nom du contexte kubectl varie d'un poste à l'autre, ne vous
y fiez pas.
kubectl -n openbao get statefulset openbao
Vous devez voir le StatefulSet et ses 4 réplicas. Une erreur NotFound signifie que vous n'êtes
pas sur le bon cluster, changez de contexte avant de continuer :
kubectl config get-contexts
kubectl config use-context <contexte du cluster de service>
Ce qu'un export contient¶
Uniquement les données d'un mount, sous forme d'arborescence :
{
"<projet>": {
"<env>": {
"<nom du Deployment>": {
"MA_VARIABLE": "sa valeur"
}
}
}
}
Le nom du mount n'apparaît pas dans le fichier : il est passé en argument à l'import. Le même fichier peut donc être réinjecté dans n'importe quel mount, y compris le mauvais.
Ce qu'il ne contient pas
Ni les policies, ni l'authentification OIDC, ni les mounts eux-mêmes. Ceux-là sont
décrits dans Terraform, module 034.openbao-setup.
C'est un choix : la configuration vit dans git, les données vivent dans le bucket.
Conséquence sur l'ordre des opérations : si le coffre a été reconstruit, il faut
terraform apply sur 034.openbao-setup avant de restaurer les données, sinon les mounts
n'existent pas.
Quel scénario traitez-vous¶
Cas A, un secret a été écrasé ou supprimé. Le coffre fonctionne, les mounts existent. Allez directement à l'étape 1.
Cas B, le coffre a été reconstruit ou vous repartez d'un cluster neuf. Lancez d'abord
terraform apply sur 034.openbao-setup pour recréer les mounts, les policies et
l'authentification. Puis revenez à l'étape 1.
1. Ouvrir un pod de travail¶
Depuis tooling-infra/build/k8s-openbao-restore, le nom du pod est daté du jour pour savoir
quand il a été créé et ne pas entrer en collision avec un pod oublié :
POD=openbao-restore-$(date +%Y%m%d)
sed "s/name: openbao-restore$/name: $POD/" restore-pod.yaml | kubectl apply -f -
kubectl -n openbao-exporter wait --for=condition=Ready pod/$POD --timeout=120s
La seconde commande rend la main dès que le pod est prêt, en affichant condition met. Entrez
alors dedans :
kubectl -n openbao-exporter exec -it $POD -- sh
Gardez ce terminal
La variable $POD n'existe que dans le shell où vous l'avez définie. Si vous ouvrez un autre
terminal, retrouvez le nom avec kubectl -n openbao-exporter get pod | grep openbao-restore.
Toutes les commandes qui suivent se lancent dans ce pod. Il monte les identifiants nécessaires et son disque est éphémère.
L'export contient tous les secrets en clair
Il ne doit pas quitter le cluster. Le pod est là pour ça : son disque est éphémère et disparaît avec lui.
Dans le pod, déclarez la cible et votre token :
export VAULT_ADDR=https://openbao.svc.ademe-scw.fr
read -rsp "Root token OpenBao : " VAULT_TOKEN; echo
export VAULT_TOKEN
La saisie est masquée : le token n'apparaît ni à l'écran, ni dans l'historique du shell.
2. Choisir l'export¶
Les objets sont rangés par date, un par mount et par jour. Exemple pour les mounts applications
et infrastructure :
2026/08/03/applications-export.json
2026/08/03/infrastructure-export.json
Les 5 derniers exports de chaque mount, avec le total disponible :
python -c "
import boto3, os
from collections import defaultdict
from botocore.client import Config
c = boto3.client('s3',
endpoint_url='https://s3.%s.scw.cloud' % os.environ['S3_REGION'],
aws_access_key_id=os.environ['S3_ACCESS_KEY'],
aws_secret_access_key=os.environ['S3_SECRET_KEY'],
region_name=os.environ['S3_REGION'],
config=Config(signature_version='s3v4'))
par_mount = defaultdict(list)
for page in c.get_paginator('list_objects_v2').paginate(Bucket=os.environ['S3_BUCKET']):
for o in page.get('Contents', []):
mount = o['Key'].rsplit('/', 1)[-1].removesuffix('-export.json')
par_mount[mount].append(o)
for mount in sorted(par_mount):
print('%s (%d exports au total)' % (mount, len(par_mount[mount])))
for o in sorted(par_mount[mount], key=lambda x: x['Key'], reverse=True)[:5]:
print(' ', o['Key'], o['Size'], 'octets')
"
Utile pour vérifier que l'export du jour existe, ou pour remonter à une date antérieure si le dernier est suspect. Une taille qui s'effondre d'un jour à l'autre est un signal : l'export a peut-être réussi sur un coffre à moitié vide.
3. Télécharger et déchiffrer¶
--mount télécharge l'export du jour, soit le plus récent puisque le CronJob tourne à 3 h :
python /app/main.py --mount applications --destination /tmp/export.json
Pour toute autre date, désignez l'objet explicitement :
python /app/main.py --object-name 2026/07/31/applications-export.json --destination /tmp/export.json
Précisez toujours --mount ou --object-name
L'outil refuse de deviner. Il y a un export par mount et par jour, restaurer le mauvais serait silencieux.
La date est calculée en UTC
Entre minuit et 2 h locales, l'export « du jour » est celui de la veille. Utilisez
--object-name si vous avez un doute, ou listez le bucket à l'étape précédente.
À ce stade, rien n'a été modifié dans OpenBao. Si cette étape échoue, la cause est dans l'accès au bucket ou dans le déchiffrement, pas dans le coffre.
4. Inspecter avant d'importer¶
python -m json.tool /tmp/export.json
Vérifiez que vous voyez bien ce que vous comptez restaurer : les bons projets, les bons environnements, les bonnes clés.
Cette commande affiche tous les secrets en clair
Elle se lance dans le pod, et le fichier y reste.
5. Importer¶
C'est l'étape qui pousse les données dans OpenBao
Jusqu'ici, rien n'a été modifié dans le coffre.
/app/medusa_import.sh -f /tmp/export.json -m applications
Ce que le script fait, dans cet ordre :
- il interroge
sys/mountspour savoir si le mountapplicationsexiste - s'il est absent, il le crée en
kvversion 2 - il lance
medusa import, qui écrit dans OpenBao chaque chemin contenu dans le fichier - il affiche un
Secret successfully writtenpar chemin écrit
Tous les chemins du fichier sont écrits, pas seulement celui qui vous intéresse
L'export contient l'intégralité du mount à l'heure de la sauvegarde. Les secrets modifiés depuis reprennent donc leur valeur de cette sauvegarde.
Les valeurs actuelles ne sont pas perdues : chaque écriture crée une nouvelle version KV v2 et l'ancienne reste accessible. Mais il faudra les rétablir une par une.
6. Vérifier¶
Que l'aller-retour est sans perte : réexportez et comparez les données :
medusa export applications --format json --address $VAULT_ADDR --token $VAULT_TOKEN > /tmp/reexport.json
python -c "
import json
a = json.load(open('/tmp/export.json'))
b = json.load(open('/tmp/reexport.json'))
print('donnees identiques :', a == b)
"
Quelles versions ont été créées sur un chemin précis :
curl -sS -H "X-Vault-Token: $VAULT_TOKEN" \
"$VAULT_ADDR/v1/applications/metadata/PROJET/ENV/DEPLOYMENT" | python -m json.tool
Le champ current_version doit avoir augmenté, et les versions précédentes apparaître avec
"destroyed": false.
7. Nettoyer¶
kubectl -n openbao-exporter delete pod $POD
Si vous avez perdu la variable, listez les pods :
kubectl -n openbao-exporter get pod | grep openbao-restore
À ne pas oublier
Le disque du pod contient l'export déchiffré. Le pod s'arrête de lui-même au bout de 8 h, mais son disque reste lisible jusqu'à sa suppression.
Deux propriétés à connaître avant d'agir¶
Une restauration n'est pas un point de non-retour¶
Importer sur un chemin qui existe déjà crée une nouvelle version KV v2, l'ancienne reste intacte. En cas d'erreur, un retour à la version précédente est possible.
Le pod ne contient pas la CLI OpenBao, ce retour se fait depuis un poste qui l'a :
vault kv rollback -version=NUMERO applications/PROJET/ENV/DEPLOYMENT
Réserve : OpenBao conserve 10 versions par secret. max_versions n'est pas défini sur les
mounts, c'est donc le défaut qui s'applique. Au-delà, la plus ancienne est supprimée
définitivement. Des restaurations répétées consomment donc cette réserve.
Une restauration est une fusion, pas un remplacement¶
Medusa parcourt le fichier et écrit chaque chemin qu'il contient. Il ne supprime pas les clés présentes dans OpenBao mais absentes du fichier.
Restaurer un export d'il y a un mois ne ramène donc pas le coffre à son état d'il y a un mois : les anciennes valeurs sont réécrites, et tout ce qui a été créé depuis reste en place.
À vérifier si une décision en dépend
Ce comportement découle du fonctionnement de medusa mais n'a pas été testé. Éprouvez-le sur un mount jetable avant de vous appuyer dessus.
Si quelque chose échoue¶
| Symptôme | Cause probable |
|---|---|
Provide --mount or --object-name |
ni mount ni objet précisé, l'outil refuse de deviner |
NoSuchKey au téléchargement |
l'export de cette date n'existe pas, listez le bucket à l'étape 2 |
| erreur 403 à l'import | token en lecture seule, il faut le root token |
no keys found au réexport |
le mount est vide, l'import n'a rien écrit |
| l'import s'arrête au milieu | relancez-le, il est idempotent : chaque chemin est réécrit en nouvelle version |
Lister les mounts existants¶
L'interface OpenBao n'affiche que les mounts couverts par vos policies. Pour voir ceux qui existent réellement, notamment après une reconstruction du coffre :
curl -sS -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/sys/mounts" \
| python -c "import json,sys; print([k for k in json.load(sys.stdin)['data']])"