Aller au contenu

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 :

  1. il interroge sys/mounts pour savoir si le mount applications existe
  2. s'il est absent, il le crée en kv version 2
  3. il lance medusa import, qui écrit dans OpenBao chaque chemin contenu dans le fichier
  4. il affiche un Secret successfully written par 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']])"