Phase 2 coffre: documentation sauvegarde/restauration KEK

This commit is contained in:
2026-10-27 23:45:13 -04:00
parent 3acadf6ad7
commit 7d48f65ff9
+155
View File
@@ -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_<nom>` 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.