8.4 KiB
Kanbante — Références coffre ObsiGate (Phase 4, issue ObsiGate #22)
Patron « même patron, sans variante » de la spec « Coffre de secrets
ObsiGate » §12, appliqué à Kanbante sur la branche
feat/coffre-reference-obsigate (aucun merge, aucune PR dans ce lot).
1. Inventaire honnête des secrets externes (vérifié dans le code, main 0.37.1)
| Secret | Stockage actuel | Statut dans ce lot |
|---|---|---|
Jeton d'accès forge (forge_connections.token_encrypted, admin + profil) |
Chiffré Fernet au repos (app/forge_crypto.py, clé KANBANTE_MASTER_KEY ou clé générée en base) ; jamais réaffiché (masque ••••••••) |
Non migré, à raison : ce n'est pas un secret en clair. Le basculer sur une référence exigerait une distribution à chaque appel forge, car le registre met en cache les clients (ForgeRegistry) : distribuer une fois à la construction puis garder le client reviendrait à conserver la valeur en mémoire pour la durée de vie de l'app, ce qui violerait le « utilise, puis oublie » de la spec. Reste à faire dédié (voir §5). |
Secret webhook (forge_connections.webhook_secret, admin) |
En clair en base (colonne TEXT, saisie via /admin/connections) ; repli environnement KANBANTE_WEBHOOK_SECRET |
Migré : une référence coffre:<slug>/<nom> saisie dans ce même champ est stockée telle quelle, distribuée à chaque webhook, puis oubliée (§2). Les valeurs historiques en clair restent acceptées (compatibilité, migration opportuniste), sans jamais être réécrites. |
GITEA_TOKEN, KANBANTE_NTFY_TOKEN, KANBANTE_OBSIGATE_MCP_TOKEN, KANBANTE_CAPTURE_TOKEN, AGENT_EVENEMENTS_CLE, secrets SSO/session |
Variables d'environnement d'exploitation uniquement, jamais persistées par l'app, non surchargeables en base (OVERRIDABLE_SETTINGS les exclut explicitement) |
Hors périmètre applicatif : leur bascule éventuelle vers le coffre relève du déploiement (l'opérateur), pas d'un champ de saisie de Kanbante. Le jeton MCP ObsiGate (KANBANTE_OBSIGATE_MCP_TOKEN) n'est pas réutilisé ici : le consommateur coffre a son propre jeton, conformément à l'issue. |
| Mots de passe utilisateurs, jetons API Kanbante | Hashés (password_hash, token_hash) |
Hors périmètre (pas des secrets externes). |
Conclusion : Kanbante ne stockait qu'un secret externe en clair persisté par l'app — le secret webhook — et c'est celui qui est migré. Le client générique ci-dessous reste disponible pour le premier usage réel suivant (jetons forge, voir §5).
2. Ce qui est implémenté
app/coffre_client.py (nouveau, dépendance : httpx uniquement)
est_reference()/parse_reference(): validation du formatcoffre:<slug>/<nom>(préfixe, un seul/, slug et nom non vides).config_depuis_env():KANBANTE_OBSIGATE_URL(base ObsiGate, ex.https://og.dracodev.net),KANBANTE_COFFRE_CONSOMMATEUR_TOKEN(jeton du consommateur « kanbante » enregistré dans ObsiGate),KANBANTE_COFFRE_TIMEOUT(défaut 10 s). Absence de l'un des deux premiers →CoffreNonConfigure, échec propre.distribuer()/distribuer_async():POST /api/coffres/distribueravec JSON{reference, contexte_type, contexte_id}etAuthorization: Bearer <jeton consommateur>— contrat vérifié dans le routeur ObsiGate (backend/routers/coffres.py, main). La valeur retournée n'est jamais journalisée, jamais persistée, jamais mise en cache.usage_distribue()/usage_distribue_async(): gestionnaires de contexte « un seul usage » — la valeur ne doit pas sortir du bloc.- Erreurs typées, sans repli, avec messages explicites orientant vers ObsiGate (voir §3). Aucune valeur distribuée n'apparaît dans un message, un log ou une réponse HTTP.
Webhook (app/main.py, _handle_forge_webhook)
- Si
webhook_secretstocké est une référence : distribution asynchrone à chaque webhook (contexte_type="webhook",contexte_id=<slug de la connexion>), HMAC vérifié avec la valeur en mémoire, puis oubli (fin de portée de la fonction). - Échec de distribution → aucun repli sur le secret
d'environnement ni sur une ancienne valeur (il n'y en a pas de
stockée) :
CoffreAccesRefuse→ HTTP 401 « Accès révoqué ou refusé dans ObsiGate : ré-accorder l'octroi… » ; autreCoffreErreur(ObsiGate indisponible, consommateur non configuré) → HTTP 503 explicite. Le webhook doit être renvoyé après correction côté ObsiGate. - Si la valeur stockée n'est pas une référence : comportement historique inchangé (secret en clair, puis repli environnement).
Formulaire admin (app/main.py + app/templates/admin_connections.html)
- Le champ « Secret webhook » accepte désormais une référence
coffre:<slug>/<nom>(création et modification). Une saisie commençant parcoffre:mais mal formée est refusée (redirection d'erreur), pour ne jamais la stocker comme secret en clair historique par accident. - Le champ « Jeton d'accès » est inchangé : une référence saisie là serait aujourd'hui chiffrée Fernet puis rejetée par la forge — c'est volontaire tant que la distribution par appel (§5) n'existe pas. Le bandeau du formulaire le signale.
Aucun changement de schéma n'a été nécessaire : la référence est
stockée dans la colonne webhook_secret existante, détectée par son
préfixe coffre: (un secret historique ne peut pas porter ce préfixe
sans avoir été saisi comme tel, et ce cas est désormais validé).
3. Correspondance des erreurs ObsiGate
| Réponse ObsiGate | Erreur Kanbante | Effet |
|---|---|---|
401 consommateur_invalide, 403 consommateur_revoque |
CoffreAccesRefuse |
Webhook 401, message « ré-accorder » |
403 octroi_absent / octroi_revoque / octroi_expire, 410 secret_expire |
CoffreAccesRefuse |
Webhook 401, message « ré-accorder / renouveler » |
400 reference_invalide / contexte_manquant |
CoffreReferenceInvalide / CoffreContexteManquant |
Webhook 503 (erreur de configuration) |
503 kek_absente / crypto_indisponible, timeout, connexion refusée, 200 sans valeur |
CoffreIndisponible |
Webhook 503, renvoyer le webhook plus tard |
Dans tous les cas : pas de valeur en cache, pas de continuation.
4. Tests
tests/test_coffre_client.py (faux endpoint httpx.MockTransport,
valeurs fictives TEST-VALUE-XXXX uniquement, aucune instance réelle) :
- validation de références (valides / invalides) ;
- succès sync + async, avec vérification de la requête (chemin, Bearer,
corps
reference+ contexte) ; - refus (octroi révoqué/expiré/absent, consommateur révoqué/invalide,
secret expiré) →
CoffreAccesRefuseavec message explicite ; - indisponibilité (503, erreur réseau, 200 sans valeur) →
CoffreIndisponible; - référence invalide / contexte manquant / configuration absente : aucun appel HTTP ;
- webhook de bout en bout (TestClient) : octroi révoqué → 401 explicite sans repli ; distribution réussie → HMAC accepté et ligne en base toujours porteuse de la référence, jamais de la valeur.
5. Restes à faire (exigent validation par Bruno sur instance réelle)
- Validation réelle : enregistrer le consommateur « kanbante »
dans ObsiGate, créer un octroi vers un secret webhook de test,
configurer
KANBANTE_OBSIGATE_URL+KANBANTE_COFFRE_CONSOMMATEUR_TOKENsur l'instance Kanbante, saisir la référence dans/admin/connections, envoyer un vrai webhook Gitea, vérifier l'acceptation, puis révoquer l'octroi et vérifier le 401 explicite, puis couper ObsiGate et vérifier le 503. - Jetons forge par référence : distribution à chaque appel dans
GiteaClient/GitHubClient(ou client jetable par requête), sans passer par le cacheForgeRegistry; seulement alors, accepter une référence dans le champ « Jeton d'accès » et documenter la migration des connexions existantes (chiffrées Fernet → références). - Migration opportuniste des secrets webhook historiques : proposer en admin la conversion d'un secret en clair existant vers une référence (l'admin crée le secret + l'octroi dans ObsiGate, puis remplace la valeur par la référence) — aucun outil automatique ne copie une valeur réelle vers ObsiGate dans ce lot.
- CI complète : la suite n'a été exécutée que localement dans le sandbox (voir rapport) ; le run CI Gitea de la branche reste à observer avant toute PR.