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. Si le cluster est indisponible, allez directement à l'accès d'urgence.
Version courte¶
# depuis tooling-infra/build/k8s-openbao-restore, sur le cluster de service
kubectl -n openbao-exporter apply -f restore-pod.yaml
kubectl -n openbao-exporter exec -it openbao-restore -- sh
# dans le pod : telecharge et dechiffre le dernier export
python /app/main.py --mount applications --destination /tmp/export.json
# reinjecte, seule etape qui ecrit dans OpenBao
/app/medusa_import.sh -f /tmp/export.json -m applications
main.py récupère la clé de chiffrement, la fait ouvrir par Key Manager et calcule les en-têtes
SSE-C du téléchargement. C'est ce qui remplace un curl, impossible ici puisque la clé doit voyager
dans la requête.
La suite de la page déroule ces trois commandes, avec le choix de l'export et les vérifications.
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
$VAULT_ADDR, $VAULT_TOKEN et $POD sont des variables définies au fil de la procédure, laissez-les
telles quelles. Ce qui est en MAJUSCULES sans $ est à remplacer par votre valeur.
Les clés S3 et la clé de chiffrement sont déjà dans le cluster et seront montées dans le pod, vous
n'avez aucun identifiant à manipuler. Le root token, en revanche, est indispensable : l'identité de
l'exporter est en lecture seule, et un token OIDC APPSVC-* donne accès aux données mais pas à la
configuration du coffre.
Vérifiez d'abord que vous êtes sur le bon cluster¶
OpenBao n'est déployé que sur le cluster de service, et le nom du contexte kubectl varie d'un poste
à l'autre.
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. Lancez d'abord terraform apply sur 034.openbao-setup pour
recréer les mounts, les policies et l'authentification. Puis revenez à l'étape 1.
Cas C, vous repartez d'un cluster neuf. L'ordre compte, et deux étapes ne sont pas devinables :
- cluster reconstruit par Terraform, puis ArgoCD, puis external-secrets
- le pod de l'étape 1 tire son image du registre privé avec le secret
docker-registry-auth, que External Secrets lit dans Vault, pas dans Secret Manager. Si Vault n'est pas encore revenu, ce secret n'existe pas : créez-le à la main depuisSCW_DOCKER_REGISTRY_TOKEN, qui est dans Secret Manager - OpenBao déployé, puis
bao operator init: le root token et les nouvelles parts de descellement sortent de là. Reportez les parts dans les secretsOPENBAO_UNSEAL_CONFIG_1à3de Secret Manager, sinon l'unsealer travaille avec celles de l'ancien coffre terraform applysur034.openbao-setup- étape 1 de cette page
Les clés S3 et la clé de chiffrement de l'exporter viennent de Secret Manager, hors cluster : elles survivent à la perte du cluster et ne bloquent rien.
Séquence non éprouvée
Elle est déduite de la chaîne de dépendances des secrets, pas d'une reconstruction réelle. L'étape 1 et les suivantes, elles, ont été jouées.
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']])"
Accès d'urgence sans le cluster¶
Ce chemin sert quand le cluster est indisponible. Deux variantes : cinq étapes avec les CLI scw
et aws, ou une image Docker à construire. La première ne demande aucun dépôt cloné, c'est
celle à privilégier.
Prérequis¶
| Quoi | Pourquoi |
|---|---|
CLI scw |
Key Manager est une API propre à Scaleway, aws ne sait pas lui parler |
CLI aws |
le bucket est en S3 |
jq, openssl |
lire la réponse JSON, calculer l'empreinte de la clé |
| Réseau | api.scaleway.com et s3.fr-par.scw.cloud |
| Root token OpenBao | uniquement si vous réinjectez |
Les deux CLI sont indispensables et ne se remplacent pas l'une l'autre : la première déchiffre, la seconde télécharge. La variante en conteneur évite de les installer, au prix d'un clone de dépôt et d'un build.
Si scw manque, c'est un binaire unique, sans dépendance :
URL=$(curl -s https://api.github.com/repos/scaleway/scaleway-cli/releases/latest \
| grep -o '"browser_download_url": "[^"]*linux_amd64"' | sed 's/.*: "//;s/"$//')
curl -sL -o /tmp/scw "$URL" && chmod +x /tmp/scw
Les valeurs à récupérer¶
| Valeur | Où la trouver | Rôle |
|---|---|---|
s3-access-key |
Secret Manager, chemin /openbao-exporter |
accès au bucket et à Key Manager, c'est la même paire de clés |
s3-secret-key |
idem | idem |
backup-dek-encrypted |
idem | la clé qui a chiffré l'export, elle-même chiffrée |
| ID de l'organisation | console Scaleway, page de l'organisation | portée des appels à Key Manager |
| ID du projet | console Scaleway, page du projet du cluster de service | idem |
Les exports vivent dans le bucket scw-openbao-backup, région fr-par, un objet par mount et par
jour, nommé AAAA/MM/JJ/<mount>-export.json. La console Scaleway permet de les lister, pas de
les télécharger : la clé de déchiffrement voyage dans la requête (SSE-C), la console ne l'envoie
pas, et le téléchargement répond une erreur 400.
Préparer le shell¶
Remplacez les deux identifiants, puis saisissez les trois secrets — ils ne touchent ainsi ni le disque, ni l'historique.
export SCW_DEFAULT_ORGANIZATION_ID=<id-organisation>
export SCW_DEFAULT_PROJECT_ID=<id-projet-du-cluster-de-service>
export SCW_DEFAULT_REGION=fr-par
read -rp "s3-access-key : " SCW_ACCESS_KEY
read -rsp "s3-secret-key : " SCW_SECRET_KEY; echo
read -rsp "backup-dek-encrypted : " CIPHERTEXT; echo
export SCW_ACCESS_KEY SCW_SECRET_KEY
# la meme paire de cles, sous les noms attendus par le CLI aws
export AWS_ACCESS_KEY_ID="$SCW_ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="$SCW_SECRET_KEY"
export AWS_DEFAULT_REGION=fr-par
export AWS_ENDPOINT_URL=https://s3.fr-par.scw.cloud
1. Ouvrir la clé¶
Le backup-dek-encrypted ne sert à rien tel quel : seul Key Manager peut l'ouvrir, la clé maîtresse
ne sort jamais du service. On passe par un fichier parce qu'un secret donné en argument reste
visible dans ps.
umask 077
printf '%s' "$CIPHERTEXT" > /tmp/dek.b64
unset CIPHERTEXT
KEY_ID=$(scw -o json keymanager key list name=scw-openbao-backup-key | jq -r '.keys[0].id')
KEY_B64=$(scw -o json keymanager key decrypt "$KEY_ID" ciphertext=@/tmp/dek.b64 | jq -r .plaintext)
rm -f /tmp/dek.b64
Contrôlez la clé sans l'afficher, la réponse doit être 32 :
printf '%s' "$KEY_B64" | base64 -d | wc -c
2. Calculer l'empreinte¶
S3 la compare à la clé reçue et rejette la requête si les deux ne concordent pas. Seule subtilité de la procédure : la clé se transmet en base64, l'empreinte se calcule sur la clé binaire.
KEY_MD5=$(printf '%s' "$KEY_B64" | base64 -d | openssl dgst -md5 -binary | base64)
3. Trouver le dernier export¶
Lister ne demande pas la clé. Aucune date à deviner : le nom des objets commence par la date, donc l'ordre alphabétique est l'ordre chronologique.
MOUNT=applications
OBJECT=$(aws s3 ls s3://scw-openbao-backup/ --recursive \
| awk -v m="$MOUNT-export.json" '$4 ~ m {print $4}' | sort | tail -1)
echo "export retenu : $OBJECT"
4. Télécharger¶
aws s3api get-object \
--bucket scw-openbao-backup \
--key "$OBJECT" \
--sse-customer-algorithm AES256 \
--sse-customer-key "$KEY_B64" \
--sse-customer-key-md5 "$KEY_MD5" \
export.json
jq . export.json | head -20
Deux erreurs à ne pas confondre : une 400 signifie clé ou empreinte incohérentes, une 403 un problème de droits.
5. Nettoyer¶
Le fichier contient tous les secrets du mount, en clair
Ne remettez pas cette étape à plus tard.
rm -f export.json /tmp/dek.b64
unset KEY_B64 KEY_MD5 SCW_SECRET_KEY AWS_SECRET_ACCESS_KEY
Hors urgence, préférez le pod : son disque est éphémère et aucune clé ne transite par votre poste.
Variante en conteneur¶
L'image enchaîne les étapes 1 à 4 en un seul appel. Elle évite d'installer scw et aws, au prix du
clone d'un dépôt et d'un build.
read -rp "s3-access-key : " S3_ACCESS_KEY
read -rsp "s3-secret-key : " S3_SECRET_KEY; echo
read -rsp "backup-dek-encrypted : " SCW_DEK_ENCRYPTED_KEY; echo
export S3_ACCESS_KEY S3_SECRET_KEY SCW_DEK_ENCRYPTED_KEY \
SCW_DEFAULT_PROJECT_ID SCW_DEFAULT_ORGANIZATION_ID
Construisez l'outil depuis le dépôt :
git clone git@gitlab.ademe.fr:ademe-group/infrastructure/tooling-infra.git
docker build -t openbao-restore:local tooling-infra/build/k8s-openbao-restore
Le téléchargement, ici l'export du jour du mount applications :
docker run --rm \
--user "$(id -u):$(id -g)" -e HOME=/tmp \
-e S3_ACCESS_KEY -e S3_SECRET_KEY -e SCW_DEK_ENCRYPTED_KEY \
-e SCW_DEFAULT_PROJECT_ID -e SCW_DEFAULT_ORGANIZATION_ID \
-e S3_BUCKET=scw-openbao-backup \
-e S3_REGION=fr-par \
-e SCW_DEK_MASTER_KEY=scw-openbao-backup-key \
-v "$PWD:/out" \
openbao-restore:local \
--mount applications --destination /out/export.json
Les deux identifiants figurent aussi dans
tooling-infra/build/k8s-openbao-restore/restore-pod.yaml. Pour une autre date, remplacez
--mount applications par --object-name 2026/07/31/applications-export.json.
Vous obtenez export.json dans le répertoire courant, lisible avec cat export.json. La clé
chiffrée a été ouverte par Key Manager au passage, vous n'avez rien à déchiffrer.
Le fichier contient tous les secrets du mount, en clair
Supprimez-le dès que vous avez ce que vous cherchiez :
rm export.json
unset S3_ACCESS_KEY S3_SECRET_KEY SCW_DEK_ENCRYPTED_KEY
Hors urgence, préférez le pod : son disque est éphémère et aucune clé ne transite par votre poste.
Réinjecter depuis votre poste¶
Cette étape passe forcément par l'image : le script d'import y est embarqué. Lisez d'abord ce qu'elle fait.
read -rsp "Root token OpenBao : " VAULT_TOKEN; echo
export VAULT_TOKEN
docker run --rm -it -v "$PWD:/out" \
-e VAULT_ADDR=https://openbao.svc.ademe-scw.fr \
-e VAULT_TOKEN \
--entrypoint /app/medusa_import.sh \
openbao-restore:local \
-f /out/export.json -m applications
La clé de chiffrement, et ce qui arrive si on y touche¶
Un seul DEK chiffre toutes les sauvegardes. Il est stocké uniquement sous forme chiffrée, et
c'est la clé scw-openbao-backup-key de Key Manager qui l'ouvre. Trois actions, trois
conséquences très différentes.
| Action | Effet sur les sauvegardes |
|---|---|
| Rotation | aucun. Les versions précédentes restent utilisables en déchiffrement, et le retour du decrypt fournit le ciphertext ré-emballé avec la version courante. Une rotation automatique tous les 30 jours est déjà active. |
| Désactivation | plus aucun decrypt possible, donc le DEK reste scellé et les sauvegardes sont inexploitables. Réversible en réactivant la clé. |
| Suppression | définitif. Supprimer la clé supprime toutes ses versions, et tout ce qu'elle protégeait devient indéchiffrable — le DEK compris. Les objets restent dans le bucket mais ne sont plus que des octets aléatoires. |
Deux garde-fous limitent le dernier cas : la clé est déclarée protégée dans Terraform
(074.key-manager/main.tf, unprotected = false) et Scaleway impose sept jours d'état
« Scheduled for Deletion », pendant lesquels la clé est déjà inutilisable mais encore restaurable.
Point unique de défaillance
Aucune copie du DEK en clair n'existe. La restauration dépend donc entièrement d'une seule ressource Scaleway. Conserver le DEK déchiffré hors de Scaleway donnerait un chemin de secours indépendant, puisque SSE-C n'a besoin que de lui — au prix d'un secret en clair de plus, qui ouvrirait à lui seul toutes les sauvegardes passées. Arbitrage à faire, pas encore tranché.