From 7d48f65ff9a3367ec2991e5ac12b6b85326918c1 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 23:45:13 -0400 Subject: [PATCH] Phase 2 coffre: documentation sauvegarde/restauration KEK --- docs/COFFRE-sauvegarde-restauration.md | 155 +++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 docs/COFFRE-sauvegarde-restauration.md diff --git a/docs/COFFRE-sauvegarde-restauration.md b/docs/COFFRE-sauvegarde-restauration.md new file mode 100644 index 0000000..318519d --- /dev/null +++ b/docs/COFFRE-sauvegarde-restauration.md @@ -0,0 +1,155 @@ +# 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.