# Coffre Secrets ObsiGate — Sauvegarde et restauration (Phase 2, issue #20) Ce document décrit la seule procédure supportée pour sauvegarder et restaurer le coffre Secrets, ainsi que le cas de perte de la KEK. Il complète la spécification du coffre (§13, §14) ; il ne décrit aucune valeur réelle — les exemples utilisent des valeurs fictives. ## 1. Ce qui constitue une sauvegarde Deux fichiers, et seulement ces deux fichiers, portent le coffre : | Fichier | Contenu | | --- | --- | | `data/coffres.json` | Coffres (métadonnées + DEK chiffrées), consommateurs (hashes de jetons seulement), octrois, métadonnées des secrets. | | `data/coffres_valeurs.json` | Versions chiffrées des secrets (AES-256-GCM). | Ces deux fichiers forment une **sauvegarde ordinaire chiffrée** : copiés seuls, ils ne donnent accès à aucune valeur — chaque DEK est chiffrée par la KEK, chaque valeur est chiffrée par la DEK de son coffre. Ils peuvent donc transiter par la sauvegarde habituelle du serveur, avec les mêmes permissions restrictives que sur l'instance (0600). Ils ne doivent jamais être déposés dans le dépôt Git, ni dans une note ObsiGate, ni dans un journal. La **KEK ne fait pas partie de la sauvegarde** : elle est copiée à part, hors serveur (voir §2). Sauvegarder les deux fichiers et la KEK au même endroit annulerait la protection du chiffrement. ## 2. Sauvegarder la KEK (copie externe, hors serveur) La KEK est une clé maître 256 bits (base64 de 32 octets), générée une seule fois via `POST /api/coffres/admin/generer-kek` (admin, coffres activés) et **jamais stockée par ObsiGate**. Au moment de sa génération : 1. Copiez-la immédiatement dans un gestionnaire de mots de passe ou sur un support hors ligne, hors du serveur ObsiGate. 2. Ne la collez jamais dans le dépôt, jamais dans une note ObsiGate, jamais dans le même dossier que `data/`, jamais dans un journal. 3. Vérifiez la copie (relisez-la depuis le gestionnaire, pas depuis l'écran qui ne l'affichera plus jamais). Variables d'environnement (documentées aussi dans `.env.example`, sans aucune valeur) : | Variable | Rôle | Défaut | | --- | --- | --- | | `OBSIGATE_SECRETS_ENABLED` | Activation du coffre. Toute route répond 503 tant qu'elle n'est pas activée. | désactivé | | `OBSIGATE_KEK` | KEK, base64 de 32 octets (ou hex 64 caractères). Prioritaire sur le fichier. | absente | | `OBSIGATE_KEK_FILE` | Chemin d'un fichier 0600, hors dépôt et hors arborescence des vaults, contenant la KEK. Un fichier trop permissif est refusé. | absent | | `OBSIGATE_DATA_DIR` | Répertoire des données (`data/` par défaut), donc des deux fichiers du coffre et de `api_keys.json`. | `data` | **Aucune autre variable ne doit contenir une valeur de secret du coffre.** En particulier, ne créez jamais de variable du type `COFFRE_` avec une valeur : la seule valeur externe autorisée est la KEK, et elle n'est pas une valeur de secret du coffre. ## 3. Restauration sur une instance (y compris vierge) Prérequis : une instance ObsiGate avec le coffre activé (`OBSIGATE_SECRETS_ENABLED`), et la **même KEK** que l'instance d'origine. 1. Arrêtez l'instance cible (ou, à défaut, assurez-vous qu'aucune écriture n'a lieu pendant la copie). 2. Restaurez les deux fichiers `coffres.json` et `coffres_valeurs.json` dans le répertoire de données cible, en permissions 0600. 3. Re-présentez la même KEK, via `OBSIGATE_KEK` ou via `OBSIGATE_KEK_FILE` (fichier 0600, hors dépôt). 4. Redémarrez l'instance. 5. Vérifiez, sans jamais afficher ni journaliser de valeur : - la vue Secrets liste les coffres et les secrets avec `has_value = true` ; - une distribution de test via un consommateur existant (ou un consommateur de test, révoqué ensuite) aboutit — côté serveur, le succès seul est vérifié, la valeur n'est ni affichée ni consignée ; - le journal d'audit contient `distribution_ok` pour cette référence. **Test de restauration obligatoire avant la mise en service réelle (issue #20)** : la procédure ci-dessus a été automatisée dans la suite de tests (`tests/test_coffre_secrets.py`, classe `TestRestaurationVierge`) — copie des deux fichiers vers un répertoire vierge, même KEK, distributions fonctionnelles — et doit aussi être rejouée une fois sur une instance réelle par l'opérateur avant de considérer la sauvegarde comme valide. ## 4. Perte de la KEK : perte d'accès assumée, par construction Si la KEK est perdue, **il n'existe aucun moyen de récupérer les valeurs** : les DEK ne sont stockées que chiffrées par la KEK, et les valeurs ne sont stockées que chiffrées par les DEK (AES-256-GCM, authentifié). Il n'y a ni clé de secours, ni dérivation depuis le secret d'application, ni porte dérobée. Procédure en cas de perte : 1. Considérez toutes les valeurs du coffre comme perdues. 2. Créez de nouveaux coffres avec une nouvelle KEK. 3. Pour chaque secret perdu, régénérez la valeur **côté fournisseur** (nouvelle clé API, nouveau jeton) et créez un nouveau secret avec cette nouvelle valeur ; révoquez les anciennes valeurs côté fournisseur, car elles restent valides hors d'ObsiGate tant qu'elles ne sont pas révoquées là-bas. 4. Recréez les consommateurs et les octrois (les anciens octrois pointent vers des secrets qui n'existent plus dans les nouveaux coffres). 5. Ne tentez pas de « réparer » les anciens fichiers avec une nouvelle KEK : le déchiffrement échouera (tag GCM invalide), ce qui est le comportement attendu, pas un bug. La rotation KEK (`POST /api/coffres/admin/rotation-kek`) n'est pas une récupération : elle exige l'ancienne KEK encore valide pour re-chiffrer les DEK, puis le remplacement de la KEK dans l'environnement ou le fichier KEK. ## 5. Migration `api_keys.json` (Phase 2) — avertissement important Le bouton « Importer l'existant » de la vue Secrets (admin seulement) copie les clés IA et clés d'outils de `data/api_keys.json` vers le coffre d'instance « ObsiGate interne » (slug `obsigate-interne`), en deux temps : 1. **Import** : les entrées avec valeur sont chiffrées dans le coffre (un secret déjà présent n'est jamais écrasé) ; `api_keys.json` n'est pas modifié. 2. **Purge** : après confirmation explicite, et uniquement pour les noms migrés, les entrées sont supprimées du clair. Sans confirmation, rien n'est supprimé — jamais automatiquement. **Avertissement** : les lecteurs actuels d'ObsiGate (`backend/ai.py`, `backend/tools/secrets.py`) lisent encore `api_keys.json` directement ; ils ne sont pas encore branchés sur le coffre. Purger le clair prive donc ces lecteurs de la clé tant que leur lecteur n'est pas branché sur une distribution depuis le coffre. Ne purgez que si vous avez vérifié que le consommateur concerné distribue depuis le coffre, ou si vous acceptez cette interruption. Ce branchement des lecteurs internes est un reste à faire explicite (voir §6). ## 6. Restes à faire (après cette Phase 2) - Brancher les lecteurs internes (`ai.py`, `tools/secrets.py`) sur une distribution depuis le coffre d'instance, pour que la purge du clair ne coupe pas l'IA ni les outils. - Rejouer le test de restauration sur une instance réelle (la suite automatisée simule une instance vierge, elle ne remplace pas un essai sur l'instance de production). - Endpoint de distribution par session utilisateur (copie manuelle sans consommateur temporaire) : non ajouté ici, car il contredirait la règle Phase 1b (distribution réservée aux consommateurs) et exigerait une ré-authentification qui n'existe pas encore — décision à prendre avec l'opérateur avant toute implémentation.