From 32996ce9aa001ee3c1b2c97cfbb37f24e23ac104 Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 28 Oct 2026 00:10:51 -0400 Subject: [PATCH 1/2] Phase 5 (issue #23) : deploiement et mise en service du coffre Secrets --- docs/COFFRE-deploiement-mise-en-service.md | 390 +++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 docs/COFFRE-deploiement-mise-en-service.md diff --git a/docs/COFFRE-deploiement-mise-en-service.md b/docs/COFFRE-deploiement-mise-en-service.md new file mode 100644 index 0000000..739293e --- /dev/null +++ b/docs/COFFRE-deploiement-mise-en-service.md @@ -0,0 +1,390 @@ +# Coffre Secrets ObsiGate — Déploiement et mise en service (Phase 5, issue #23) + +Document de déploiement du coffre Secrets, issu des Phases 1a/1b +(issues #17, #18), 2 (issue #20), 3 (issue #21, côté agent-commun) +et 4 (issue #22, côté TikTak, Flowdeck, Kanbante). + +**Règles de gouvernance (non négociables) :** travail en branche + +pull request, jamais sur `main`, jamais de merge, de release, de tag, +de fermeture d'issue ni de changement de label sans le go explicite +de Bruno. Ce document ne décrit aucune valeur réelle : les exemples +utilisent des valeurs fictives (`TEST-VALUE-XXXX`) et aucune KEK +n'est jamais écrite ici, ni dans un journal, ni dans une note, ni +dans le dépôt. + +Documents compagnons (même branche de référence, Phase 2) : + +- `docs/COFFRE-contrat-distribution.md` — contrat + `POST /api/coffres/distribuer` pour Spark, TikTak, Flowdeck, + Kanbante (authentification par consommateur, contextes, codes + d'erreur stables). +- `docs/COFFRE-sauvegarde-restauration.md` — sauvegarde + (`data/coffres.json` + `data/coffres_valeurs.json`, 0600) et + restauration sur instance vierge, perte de KEK. + +## 1. Prérequis et variables d'environnement + +### 1.1 Dépendance + +- `cryptography>=44.0` dans `backend/requirements.txt` (dépendance + Phase 1a). Sans elle, les opérations de chiffrement répondent + 503 `crypto_indisponible` : c'est un mode dégradé explicite, pas + un repli en clair. Il n'y a jamais de stockage ni de distribution + de valeur en clair. +- Chiffrement par enveloppe AES-256-GCM : KEK (256 bits) → DEK par + coffre → valeur du secret. Un tag GCM invalide refuse le + déchiffrement (testé : `tests/test_coffre_secrets.py`, + `test_tag_invalide_refuse`). + +### 1.2 Variables + +| Variable | Rôle | Défaut | +| --- | --- | --- | +| `OBSIGATE_SECRETS_ENABLED` | Activation du coffre. Tant qu'elle n'est pas à `true`, **toutes** les routes coffre répondent 503 `secrets_desactives`. | désactivé | +| `OBSIGATE_KEK` | KEK, base64 de 32 octets (ou hex 64 caractères). **Prioritaire** sur le fichier si les deux sont définis. | absente | +| `OBSIGATE_KEK_FILE` | Chemin d'un fichier contenant la KEK, **en 0600, hors dépôt et hors arborescence des vaults**. Un fichier trop permissif (droits groupe/autre) est refusé explicitement (`KEKInvalideError`). | absent | +| `OBSIGATE_DATA_DIR` | Répertoire des données (`data/` par défaut) : `coffres.json`, `coffres_valeurs.json` et l'historique `api_keys.json`. | `data` | + +La KEK est générée une seule fois (admin, coffres activés) via +`POST /api/coffres/admin/generer-kek` ; ObsiGate **ne la stocke +jamais**. Au moment de sa génération : la copier immédiatement +dans un gestionnaire de mots de passe ou sur un support hors ligne, +hors du serveur ; vérifier la copie en la relisant depuis le +gestionnaire, jamais depuis l'écran qui ne l'affichera plus. +Ne jamais la coller dans le dépôt, dans une note ObsiGate, dans le +même dossier que `data/`, ni dans un journal. + +**Recommandation de déploiement : préférer `OBSIGATE_KEK_FILE`** +(fichier 0600) à `OBSIGATE_KEK` en variable d'environnement, pour +éviter que la KEK apparaisse dans un historique de processus ou un +dump d'environnement. Si les deux sont définis, `OBSIGATE_KEK` +l'emporte — vérifier qu'aucune ancienne valeur ne traîne dans +l'environnement au moment de basculer sur le fichier. + +### 1.3 Ce que la sauvegarde contient (rappel) + +Deux fichiers seulement portent le coffre, tous deux en 0600 : + +- `data/coffres.json` : métadonnées des coffres, DEK chiffrées, + consommateurs (hashes SHA-256 des jetons seulement, jamais le + jeton en clair), octrois, métadonnées des secrets ; +- `data/coffres_valeurs.json` : versions chiffrées des secrets. + +Copiés seuls, ils ne donnent accès à aucune valeur. La KEK ne fait +pas partie de la sauvegarde : elle est copiée à part, hors serveur. +Sauvegarder les deux fichiers et la KEK au même endroit annulerait +la protection du chiffrement. + +## 2. Décisions ouvertes de la spec — tranchées par défaut + +Les décisions ouvertes de la spec (§15, §16), rappelées dans +l'issue #23, sont confirmées comme tranchées selon les défauts +recommandés, en attendant une décision contraire explicite de +Bruno. Aucune d'elles ne nécessite de changement de code pour la +mise en service décrite ci-dessous : + +| Décision | Choix confirmé par défaut | +| --- | --- | +| Glyphe du nœud Secrets | Cadenas U+1F510 (🔐), distinct de la maison du Home et du diamant du Partage. Le nœud est un nœud virtuel sous le Home de l'utilisateur courant seulement (voir §4). | +| Stockage de la KEK | Par fichier 0600, hors dépôt (`OBSIGATE_KEK_FILE`) ; `OBSIGATE_KEK` reste supportée comme solution de secours. | +| Expiration des octrois | 90 jours par défaut à la création, renouvelable (création d'un nouvel octroi). Un octroi expiré refuse la distribution (`octroi_expire`, 403) ; un octroi révoqué refuse dès l'appel suivant (`octroi_revoque`, 403). | +| Copie manuelle | Presse-papiers seul, sans affichage de la valeur. Patron supporté : consommateur temporaire créé par un admin puis révoqué immédiatement, auditée comme distribution de contexte `copie_manuelle`. **Aucun endpoint dédié de copie par session n'est ajouté** : il contredirait la règle « distribution réservée aux consommateurs » et exigerait une ré-authentification qui n'existe pas encore — décision volontaire, à trancher avec Bruno avant toute implémentation. | +| Coffres partagés | Phase 2 ultérieure. En mise en service : coffres personnels d'abord, plus le coffre d'instance « ObsiGate interne » (slug `obsigate-interne`, admin seulement) issu de l'import. | + +## 3. Mise en service pas à pas + +**Ordre impératif.** Chaque étape est validée avant de passer à la +suivante. En particulier : **les lecteurs internes sont branchés +sur le coffre AVANT toute purge du clair** (étape 5 avant étape 7), +et la purge n'a lieu qu'après validation complète (étape 6). + +### Étape 0 — Préparer, coffre encore désactivé + +1. Déployer le code coffre (Phases 1–2) sur l'instance, **sans + activer le flag** : `OBSIGATE_SECRETS_ENABLED` reste absent ou à + `false`. Vérifier que toutes les routes `/api/coffres/*` + répondent 503. +2. Sauvegarder l'état actuel : copie de `data/` (dont + `api_keys.json`) et de la configuration, hors serveur. +3. Générer la KEK (voir §1.2) et la copier hors serveur. Ne pas + encore la référencer dans la configuration de l'instance si + l'étape 1 n'est pas terminée — une KEK présente sans flag actif + ne distribue rien, mais sa présence doit rester traçable. + +### Étape 1 — Créer les consommateurs et les octrois + +Avant d'activer le flag, préparer les consommateurs nommés, un par +application cliente (convention de nommage, non imposée par le +code — l'autorisation porte sur le jeton, pas sur le nom) : + +| Consommateur | Usage | Contexte de distribution | +| --- | --- | --- | +| `spark` | Agent Spark (agent-commun, Phase 3) | `contexte_type = "mission"`, `contexte_id` = id de mission (forme imbriquée `contexte: {type, id}` acceptée) | +| `tiktak` | Synchronisation TikTak (Phase 4) | `contexte_type = "tiktak_sync"`, `contexte_id` = id utilisateur | +| `flowdeck` | Connecteurs Flowdeck (Phase 4) | Contexte workspace, vérifié côté Flowdeck (voir §5) | +| `kanbante` | Webhooks Kanbante (Phase 4) | Contexte webhook, distribution à chaque appel puis oubli | + +Création en une seule opération avec les octrois : + +``` +POST /api/coffres/consommateurs/avec-octrois +{"nom": "tiktak", "octrois": [{"coffre_slug": "...", "secret_nom": "..."}]} +``` + +- `secret_nom` absent = tout le coffre (octroi coffre-wide). +- Le jeton (`csk_…`) n'est rendu **qu'une seule fois**, à la + création. Le noter immédiatement dans le coffre de + configuration de l'application cliente concernée, jamais dans + une note ObsiGate ni dans le dépôt. Il n'est jamais relisté : + `GET /api/coffres/consommateurs` ne renvoie que `id`, `nom`, + `jeton_prefixe`, `statut`, `cree_par`, `cree_le`. +- La révocation est immédiate : `DELETE + /api/coffres/consommateurs/{id}` révoque toutes les + distributions de ce consommateur, sans toucher aux autres. + Une mission Spark déjà démarrée conserve son environnement + jusqu'à sa fin ; la mission suivante repasse en `acces_requis`. + +À ce stade, les consommateurs et octrois peuvent être créés alors +que les secrets n'existent pas encore : les octrois pointent vers +des coffres/secrets à créer aux étapes suivantes. Vérifier la +liste des consommateurs et des octrois avant d'activer le flag. + +### Étape 2 — Activer le flag + +1. Définir `OBSIGATE_KEK_FILE` (fichier 0600, hors dépôt) puis + `OBSIGATE_SECRETS_ENABLED=true`. +2. Redémarrer l'instance. +3. Vérifier : la vue Secrets s'ouvre ; `GET /api/coffres` répond + 200 (liste vide ou coffres existants) ; sans KEK valide, les + routes répondent 503 `kek_absente` — dans ce cas, corriger la + KEK avant toute création de secret. + +### Étape 3 — Importer l'existant (admin seulement) + +Bouton « Importer l'existant » de la vue Secrets (admin +seulement), en deux temps séparés : + +1. **Aperçu** : `GET /api/coffres/migration/api-keys` liste les + candidats de `data/api_keys.json` (clés IA et clés d'outils). +2. **Import** : `POST /api/coffres/migration/api-keys/import` + copie les entrées avec valeur vers le coffre d'instance + « ObsiGate interne » (slug `obsigate-interne`), chiffrées. + Un secret déjà présent n'est **jamais écrasé**. + `api_keys.json` n'est **pas modifié** par l'import. + +Vérifier après import : les secrets importés apparaissent dans la +vue Secrets avec `has_value = true`, et aucune valeur n'est +visible (métadonnées seules : nom, type, version, dates, octrois +actifs — jamais de valeur, jamais de texte chiffré, jamais de +nonce). + +### Étape 4 — Brancher les lecteurs internes AVANT toute purge + +**Point de vigilance principal de la mise en service.** Les +lecteurs actuels d'ObsiGate (`backend/ai.py`, +`backend/tools/secrets.py`) lisent encore `data/api_keys.json` +directement ; ils ne sont pas encore branchés sur le coffre +(reste à faire explicite des Phases 2–5). Tant que ce branchement +n'est pas livré et validé, **purger le clair priverait l'IA et les +outils de leurs clés**. + +Ordre à respecter : + +1. Ne pas purger. +2. Livrer (en branche + PR, jamais mergé sans go) le branchement + des lecteurs internes sur une distribution depuis le coffre + d'instance, puis le valider sur l'instance : l'IA et les outils + fonctionnent avec `api_keys.json` rendu temporairement + inaccessible en lecture (test de coupure contrôlée), ou à + défaut avec un secret de test distribué depuis le coffre. +3. Seulement après cette validation, passer à l'étape 7. + +Si Bruno accepte une interruption temporaire de l'IA/outils, le +documenter explicitement avant la purge ; sinon, la purge attend +le branchement. + +### Étape 5 — Brancher les applications clientes + +Pour chaque application (branches dédiées, jamais mergées sans +go), appliquer le patron commun **distribuer → utiliser → +oublier** : le serveur distribue à chaque opération, utilise la +valeur en mémoire, puis l'oublie ; aucune valeur en cache, aucune +persistance. Si ObsiGate est indisponible, l'opération échoue +proprement, sans repli sur une ancienne valeur. + +- **Spark (agent-commun, Phase 3)** : identités référencées + `coffre:/` (slug 3–64, nom 3–128, minuscules / + chiffres / `-` / `_`), sans colonne valeur ; le LLM ne voit que + la référence et un accès oui/non. Sans octroi, la mission passe + en `acces_requis` en nommant l'accès manquant ; avec octroi et + mission approuvée (enveloppe budgétaire), distribution puis + injection en variable d'environnement de l'exécuteur, jamais + dans le prompt, jamais dans les étapes d'activité. Fin de + mission = environnement détruit. +- **TikTak (Phase 4)** : référence seule stockée ; distribution + serveur à chaque synchro ; masquage côté Android (ni référence + complète ni valeur dans les réponses vues par une clé `tck_`). + Les configurations existantes sont marquées `a_resaisir = 1` + (migration 019 : colonne en clair supprimée, référence vide) : + il n'y a **pas de conversion automatique** — chaque + configuration doit être re-saisie par référence. +- **Flowdeck (Phase 4)** : `workspace_id` + référence sur les + connecteurs ; distribution en mémoire ; isolation A vs B + testée (un workspace ne distribue pas avec l'octroi d'un autre). + Les connecteurs natifs Gitea/GitHub et les clés LLM ne sont pas + basculés ; pas de sélecteur UI (API seulement). +- **Kanbante (Phase 4)** : secret webhook migré en référence, + distribuée à chaque webhook puis oubliée ; client générique + réutilisable. Les jetons forge chiffrés Fernet ne sont **pas** + migrés (distribution par appel requise, incompatible avec le + cache `ForgeRegistry`) ; les secrets webhook historiques en + clair restent acceptés par compatibilité, sans migration + automatique. + +### Étape 6 — Valider (avant purge) + +Exécuter la checklist de validation de l'instance réelle (§4). +Tant qu'un point échoue, ne pas purger et ne pas considérer la +mise en service comme terminée. + +### Étape 7 — Purger, seulement après validation + +`POST /api/coffres/migration/api-keys/purger`, **après +confirmation explicite** (`confirmer = true`), et uniquement pour +les noms migrés. Sans confirmation, rien n'est supprimé — jamais +automatiquement. + +Après purge, vérifier : + +- `api_keys.json` ne contient plus les noms purgés ; +- l'IA et les outils fonctionnent toujours (lecteurs branchés, + étape 4) ; +- une distribution de test via un consommateur existant aboutit + (succès seul vérifié, valeur ni affichée ni consignée). + +### Étape 8 — Sauvegarder et tester la restauration + +1. Sauvegarder `data/coffres.json` + `data/coffres_valeurs.json` + (0600) par la sauvegarde habituelle du serveur, et la KEK à + part, hors serveur (§1.3). +2. Rejouer la restauration sur une instance vierge (procédure + `docs/COFFRE-sauvegarde-restauration.md` §3 : arrêt de la + cible, copie des deux fichiers en 0600, même KEK, redémarrage, + vérification `has_value = true` + distribution de test + + `distribution_ok` dans le journal). La suite automatisée + (`tests/test_coffre_secrets.py`, classe + `TestRestaurationVierge`) simule cette restauration ; elle ne + remplace pas un essai sur instance réelle. +3. En cas de perte de KEK : perte d'accès assumée, par + construction — aucun moyen de récupérer les valeurs. Recréer + les coffres avec une nouvelle KEK, régénérer les valeurs côté + fournisseur, révoquer les anciennes côté fournisseur, recréer + consommateurs et octrois. Ne jamais tenter de « réparer » les + anciens fichiers avec une nouvelle KEK (le déchiffrement + échouera sur tag GCM invalide, ce qui est attendu). + +## 4. Checklist de validation par Bruno sur instance réelle + +Validation exigée par l'issue #23 avant tout usage réel. Chaque +point est validé sur capture, comme pour les autres livraisons : + +- [ ] **Sidebar** : le nœud « Secrets » (cadenas U+1F510) + apparaît sous le Home de l'utilisateur courant seulement, et + pas sous les autres vaults ; cliquer dessus ouvre la vue + Secrets sans naviguer dans les fichiers. +- [ ] **Secret de test** : création d'un secret de test + (valeur fictive, ex. `TEST-VALUE-XXXX`) dans un coffre + personnel, puis lecture : métadonnées seules, aucune valeur + affichée, aucun texte chiffré, aucun nonce. +- [ ] **Journal d'audit** : après une distribution de test, le + journal contient `distribution_ok` pour la référence, et un + `grep` du journal sur la valeur de test ne renvoie **rien** + (ni valeur, ni texte chiffré, ni nonce, ni jeton). +- [ ] **Autorisations** : un admin ne voit pas le coffre + personnel d'autrui ; une ACL `*` sur un vault ne donne aucun + droit sur un coffre ; une session utilisateur, même admin, ne + distribue pas (401 `consommateur_invalide`). +- [ ] **Distribution** : contexte manquant refusé (400 + `contexte_manquant`) ; octroi révoqué refusé dès l'appel + suivant (403 `octroi_revoque`) ; octroi expiré refusé (403 + `octroi_expire`) ; secret expiré refusé (410 `secret_expire`). +- [ ] **Import / purge** : l'import ne modifie pas + `api_keys.json` et n'écrase pas un secret existant ; la purge + sans confirmation explicite ne supprime rien. +- [ ] **Restauration** : sauvegarde + KEK externe restaurées sur + une instance vierge, distributions fonctionnelles. +- [ ] **Intégrations** : mission Spark en `acces_requis` sans + octroi, puis injection exécuteur sans trace dans les étapes ; + synchro TikTak sans valeur côté Android ; isolation workspace + Flowdeck (A ne distribue pas avec l'octroi de B). +- [ ] **Modale** : la modale de création de secret n'est plus + coupée en haut (`.obsigate-modal` bornée au viewport, corps + seul défilant, en-tête et pied toujours visibles). + +## 5. Limites connues à la mise en service + +À lire avant de déclarer la mise en service terminée : + +- **Lecteurs internes non branchés** (`ai.py`, + `tools/secrets.py` lisent encore `api_keys.json` en clair) : + purger avant de les brancher priverait l'IA/outils de leurs + clés (§3, étape 4). +- **Endpoint dédié de copie manuelle par session non ajouté** + (décision volontaire, §2) : la copie manuelle passe par un + consommateur temporaire admin, presse-papiers seul. +- **Isolation workspace Flowdeck non contrôlée côté ObsiGate** : + `/distribuer` n'autorise aujourd'hui que les octrois de type + `consommateur` ; l'isolation workspace repose sur le serveur + Flowdeck lui-même (il ne demande que les références de son + workspace, avec son propre jeton). Un contrôle serveur strict + reste à spécifier si Bruno l'exige. +- **Identités agent-commun** : détenteur libre hors coffre + conservé (historique) ; révocation Identités non propagée vers + ObsiGate (pas d'endpoint) ; exécuteurs CLI synchrones + uniquement. +- **TikTak** : « sélecteur » = saisie validée, pas de liste + déroulante (aucune API de listage côté ObsiGate) ; + configurations existantes à re-saisir, pas de conversion + automatique. +- **Flowdeck** : natifs Gitea/GitHub et clés LLM non basculés ; + pas de sélecteur UI (API seulement) ; `workspace_id` pas encore + branché depuis AgentEngine. +- **Kanbante** : jetons forge chiffrés Fernet non migrés ; + pas de migration automatique des secrets webhook historiques. +- **Recherche / graphe / index** : par construction, un coffre + n'est pas un vault de notes — il ne passe ni par l'indexeur, ni + par la recherche, ni par le graphe, et aucun outil MCP de secret + n'existe. Aucune métadonnée de secret ne doit apparaître dans + ces index (vérification par revue de code, pas de suite + automatisée dédiée — voir le bilan Phase 5). + +## 6. Repli (rollback) + +- Désactiver le flag (`OBSIGATE_SECRETS_ENABLED` absent ou + `false`) : toutes les routes coffre répondent 503, les fichiers + `coffres.json` / `coffres_valeurs.json` restent intacts et + re-distribueront dès réactivation avec la même KEK. +- Révoquer un consommateur (`DELETE + /api/coffres/consommateurs/{id}`) coupe immédiatement ses + distributions, sans toucher aux autres consommateurs ni aux + secrets. +- Ne jamais « réparer » des fichiers chiffrés avec une nouvelle + KEK, ne jamais restaurer `coffres.json` sans + `coffres_valeurs.json` (et inversement), ne jamais copier la + KEK au même endroit que la sauvegarde. + +## 7. Références + +- Spec « Coffre de secrets ObsiGate » §6 (autorisations), §7/§10/§11 + (API), §13/§14 (sauvegarde/restauration), §15/§16 (tests, + déploiement). +- Issues ObsiGate : #17 (Phase 1a), #18 (Phase 1b), #19 (Phase 1c + UI), #20 (Phase 2), #21 (Phase 3, Spark), #22 (Phase 4, TikTak / + Flowdeck / Kanbante), #23 (Phase 5, ce document). +- Suites de tests : `tests/test_coffre_secrets.py` (ObsiGate, 35 + tests), `tests/test_identites_coffre_phase3.py` (agent-commun, + 16 tests), `tests/test_coffre_reference.py` (TikTak, 17 fonctions + / 26 cas), `tests/test_coffre_reference_flowdeck.py` (Flowdeck, + 16 tests), `tests/test_coffre_client.py` (Kanbante, 11 fonctions + / 25 cas). Voir le bilan Phase 5 pour la matrice de couverture + complète et les manques. -- 2.54.0 From 0a7bed10bfabeefbd665133b58679a76445863de Mon Sep 17 00:00:00 2001 From: bruno Date: Wed, 28 Oct 2026 00:11:20 -0400 Subject: [PATCH 2/2] Phase 5 (issue #23) : correction numerotation etape lecteurs internes (4, pas 5) --- docs/COFFRE-deploiement-mise-en-service.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/COFFRE-deploiement-mise-en-service.md b/docs/COFFRE-deploiement-mise-en-service.md index 739293e..7491a79 100644 --- a/docs/COFFRE-deploiement-mise-en-service.md +++ b/docs/COFFRE-deploiement-mise-en-service.md @@ -95,7 +95,7 @@ mise en service décrite ci-dessous : **Ordre impératif.** Chaque étape est validée avant de passer à la suivante. En particulier : **les lecteurs internes sont branchés -sur le coffre AVANT toute purge du clair** (étape 5 avant étape 7), +sur le coffre AVANT toute purge du clair** (étape 4 avant étape 7), et la purge n'a lieu qu'après validation complète (étape 6). ### Étape 0 — Préparer, coffre encore désactivé -- 2.54.0