MCP Vault
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
đ MCP Vault
Gestion sĂ©curisĂ©e des secrets pour agents IA â OpenBao embedded
đŹđ§ English version
MCP Vault est un serveur MCP qui fournit un coffre-fort de secrets pour les agents IA et les missions. Il embarque OpenBao (fork open-source de HashiCorp Vault, Linux Foundation) comme moteur de chiffrement.
Pensez 1Password, mais pour vos agents IA.
đž Console d'administration
Dashboard | Vaults & Secrets | Audit & Alertes |
|
|
|
đ Documentation
Document | Description |
SpĂ©cification complĂšte â vision, architecture ASGI 6 couches, vaults, SSH CA, policies MCP (6 exemples prĂȘts Ă l'emploi), sĂ©curitĂ© des clĂ©s unseal (3 facteurs), roadmap HSM | |
Documentation technique â modules source (incl. PKI v0.5.1), stack ASGI 6 couches, Docker, tests, dĂ©pendances, roadmap | |
Rapport d'audit de sĂ©curitĂ© consolidĂ© â 60 findings V2.1, 28 corrigĂ©s, 13 rĂ©siduels documentĂ©s | |
Guide CLI complet â groupes de commandes, exemples, confort d'utilisation | |
Guide d'exĂ©cution des tests â 4 niveaux, ~600 tests, commandes pour auditeurs | |
Catalogue des tests e2e â 15 catĂ©gories, 349 assertions, objectif de chaque section (pour auditeurs) |
Related MCP server: agentkeychain
⥠Démarrage rapide
# 1. Cloner et configurer
cp .env.example .env
# Adapter les credentials S3 dans .env
# 2. Build et démarrer
docker compose build
docker compose up -d
# 3. Vérifier (depuis le conteneur)
docker compose exec mcp-vault python scripts/mcp_cli.py health
# 4. Tester (349 tests e2e)
docker compose exec mcp-vault python tests/test_e2e.pyLifecycle automatique
Au démarrage, MCP Vault :
Charge les tokens depuis S3
Restaure les donnĂ©es OpenBao (volume Docker en prioritĂ© ; sinon S3 â un Ă©chec S3 ambigu, ex. rĂ©seau, refuse le dĂ©marrage plutĂŽt que d'initialiser un coffre vide par-dessus une sauvegarde distante valide)
Démarre OpenBao, l'initialise (1Úre fois) et le déverrouille
ClĂ©s unseal : chiffrĂ©es (AES-256-GCM) sur S3, jamais en clair sur disque â uniquement en mĂ©moire
Active le sync S3 pĂ©riodique (60s, conditionnel : un PUT n'est effectuĂ© que si l'Ă©tat a changĂ© depuis le dernier upload rĂ©ussi â cf.
s3_sync.py)
Ă l'arrĂȘt (docker compose stop) :
Scelle OpenBao đ
Upload final vers S3 đ€
ArrĂȘte le processus â clĂ©s effacĂ©es de la mĂ©moire
đ ïž Outils MCP (39)
System (2)
Outil | Description |
| Ătat de santĂ© (OpenBao + S3) |
| Informations service (version, outils, plateforme) |
đĄ Introspection : l'endpoint
/admin/api/whoamiet la commande CLIwhoamipermettent de vérifier l'identité et les permissions du token courant.
Vaults â coffres de secrets (5)
Outil | Perm | Description |
| write | Crée un vault (mount KV v2) + métadonnées (owner, date) |
| read | Liste les vaults accessibles (filtrés par token) |
| read | Détails d'un vault (métadonnées, secrets_count, owner) |
| write | Met Ă jour la description d'un vault |
| admin | Supprime un vault et tous ses secrets â ïž |
Secrets (6)
Outil | Perm | Description |
| write | Ăcrit un secret typĂ© |
| read | Lit un secret (derniÚre version ou spécifique) |
| read | Liste les clés d'un vault |
| write | Supprime un secret et toutes ses versions |
| read | Liste les 14 types de secrets |
| read | GénÚre un mot de passe CSPRNG |
SSH Certificate Authority (5)
Chaque vault possĂšde sa propre CA SSH isolĂ©e â les CA sont cryptographiquement diffĂ©rentes entre vaults. Un certificat signĂ© par la CA d'un vault ne fonctionne PAS sur les serveurs configurĂ©s pour un autre vault.
Outil | Perm | Description |
| write | Configure une CA SSH + rĂŽle dans un vault |
| read | Signe une clĂ© publique â certificat Ă©phĂ©mĂšre |
| read | Clé publique CA (pour |
| read | Liste les rÎles SSH CA configurés dans un vault |
| read | Détails d'un rÎle (TTL, allowed_users, extensions) |
AccÚs SSH JIT opérateur (2) (v0.9.0)
Outil | Perm | Description |
| read | Liste les profils fermés utilisables par le bearer nominatif courant |
| write | Ămet un certificat court liĂ© Ă une clĂ© publique prĂ©-enrĂŽlĂ©e |
Le demandeur ne choisit jamais le coffre CA, le rĂŽle, le principal, la cible
ou le TTL. L'identité de l'opérateur repose sur un bearer token nominatif
scopĂ© par une policy dĂ©diĂ©e (mĂȘme mĂ©canisme que le reste du systĂšme), plus
une clé publique SSH standard (ssh-ed25519/ecdsa-sha2-nistp256)
pré-enrÎlée par empreinte. Ce parcours humain exceptionnel reste distinct du
breaking-glass externe et du parcours agentique.
Les profils peuvent ĂȘtre fournis par SSH_OPERATOR_PROFILES_JSON ou, pour un
renderer .env strict, par son équivalent Base64 URL-safe
SSH_OPERATOR_PROFILES_B64 ; les deux sources sont mutuellement exclusives.
Policies MCP â contrĂŽle d'accĂšs granulaire (4)
Les policies permettent de restreindre finement les outils accessibles par token, avec support des wildcards (system_*, ssh_*...) et des rĂšgles par vault (prod-* â lecture seule).
Outil | Perm | Description |
| admin | Crée une policy avec rÚgles d'accÚs |
| admin | Liste les policies avec compteurs |
| admin | Détails complets (allowed/denied tools, path_rules) |
| admin | Supprime une policy â ïž |
đ 6 policies prĂȘtes Ă l'emploi documentĂ©es dans ARCHITECTURE.md §6.4.1 :
readonly,operator-ssh-jit,developer,prod-reader-dev-writer,ci-cd-agent,security-auditorđ Fail-close sur panne S3 (v0.8.3, #86) : aprĂšs expiration du cache (5 min), une panne ou une corruption S3 dĂ©tectĂ©e fait refuser explicitement les dĂ©cisions de policy, plutĂŽt que de continuer Ă servir silencieusement d'anciennes rĂšgles.
Token Management (1)
Outil | Perm | Description |
| admin | Modifier un token existant (policy, permissions, vaults) |
Purge des tokens rĂ©voquĂ©s (v0.7.0 â opĂ©ration admin, exposĂ©e en REST / CLI / SPA ; ce n'est PAS un outil MCP) : supprime dĂ©finitivement les tokens rĂ©voquĂ©s depuis plus de N jours (rĂ©tention, dĂ©faut 30). Fail-close (jamais un token actif ni un token expirĂ© non rĂ©voquĂ©), dry-run + confirmation, rollback si S3 indisponible, chaque purge auditĂ©e. Via
POST /admin/api/tokens/purge, la commande CLItoken purge-revoked, ou le bouton « đ§č Purger rĂ©voquĂ©s » de la console/admin.đ Validation stricte (v0.8.3, #86) :
permissions/allowed_resourcessont dĂ©sormais validĂ©s strictement Ă la crĂ©ation, la modification et le chargement â jamais permissifs par dĂ©faut sur une entrĂ©e malformĂ©e (corrige une Ă©lĂ©vation de privilĂšge possible via une valeur du mauvais type).
PKI interne â CA + ACME (8) (v0.5.0)
CA souveraine pour l'écosystÚme : les WAF Caddy s'enrÎlent via ACME exactement comme avec Let's Encrypt, mais sur une CA interne isolée du réseau public. Utilisable en lab (*.lesur.lan) et en prod air-gapped.
Outil | Perm | Description |
| admin | Initialise CA racine + intermédiaire + rÎle ACME |
| read | CA racine PEM, empreinte SHA-256, URL stable |
| read | Liste les rÎles d'émission |
| read | Détails rÎle (domaines, TTL, flags TLS) |
| admin | Inventaire paginé des certificats émis |
| admin | Ămission manuelle d'un certificat (hors ACME) â clĂ© privĂ©e one-shot |
| admin | Révocation + mise à jour CRL |
| admin | Rotation CA intermédiaire sans coupure |
Endpoints publics (non-auth, standard ACME/PKI) :
/acme/directory,/pki/ca/root.pem,/pki/ca/chain.pem,/pki/ca/crl.pem
PKI_BASE_URL(optionnel) : URL de base pour les CDPs et le cluster path OpenBao ACME. Vide = dĂ©duit deMCP_ALLOWED_HOSTS. Override test Docker :http://mcp-vault:8030. Doit ĂȘtrehttp(s)://.
JIT Wrap Broker + consommation mĂ©diĂ©e â C18 (5) (v0.4.13 / v0.6.x)
Contrat pour le CredentialBrokerService de mcp-mission : livraison de credentials single-use via response wrapping OpenBao (cubbyhole), registry write-ahead sur S3 pour la compensation des orphelins, et validation anti-confused-deputy (C18).
â ïž
ttl_secondsborne la fenĂȘtre pour OBTENIR le secret, PAS la durĂ©e de vie du secret OBTENU (#156). L'enveloppe est Ă usage unique et meurt Ă son TTL (bornĂ© ici entre 60 s et 3600 s ;mcp-missionse plafonne Ă 300 s, ce qui est son choix et non notre plafond). MCP Vault n'impose aucune expiration au secret KV ni au credential qu'il contient, et sa validitĂ© rĂ©elle dĂ©pend de son Ă©metteur â pas de nous. â ïž Et ni l'un ni l'autre de nos deux verbes ne suffit Ă l'invalider :secret_writeajoute une version (la prĂ©cĂ©dente reste lisible par son numĂ©ro), etsecret_deleteretire les versions du coffre, pas le mot de passe de la machine ni la clĂ© chez son fournisseur. Seule une rĂ©vocation ou une rotation coordonnĂ©e chez l'Ă©metteur invalide effectivement un credential externe. â Si une mission meurt aprĂšs un dĂ©ballage, la durĂ©e d'exposition n'est bornĂ©e par personne, et le remĂšde n'est pas dans cet outil. Ce que nous ne gardons pas : aucune copie de la valeur dĂ©ballĂ©e. Elle est rendue Ă l'appelant sans ĂȘtre stockĂ©e, le champdataest retirĂ© de la piste d'audit, et aucun journal ne la porte. L'entrĂ©e de registre conserve l'accessor,vault_idetsecret_pathâ jamais la valeur.
Outil | Perm | Description |
| wrap | CrĂ©e un wrap token single-use (write-ahead registry). â ïž |
| wrap | RĂ©vocation idempotente, limitĂ©e au pĂ©rimĂštre de l'appelant ; registre non initialisĂ© â |
| wrap | Retrouve & rĂ©voque les wraps du couple (compensation orphelins #74), limitĂ© au pĂ©rimĂštre de l'appelant. â ïž |
| wrap | Consulte l'Ă©tat d'un wrap sans le rĂ©voquer â lecture seule, instantanĂ© best-effort (#77), limitĂ© au pĂ©rimĂštre de l'appelant. â ïž |
| admin | Valide JWT ES256/JWKS (contrat PEP complet depuis (#86) : exp/iat/iss/aud/mission_id/jti/scope/tenant_id + |
(#115) Permission
wrap(ouadmin) : un jetonwrapnon-admin doit porterallowed_resourcesnon vide et une policy explicite (allowed_toolsnommĂ©s,path_ruleavecallowed_pathsnon vides â Ă©valuation stricte). Un jeton wrap-only (sans read/write/admin) est confinĂ© Ă ces 4 outils (MCP) et refusĂ© sur tout/admin/api/*. â ïž Downgrade †0.9.2 : rĂ©voquer/purger les jetonswrapd'abord (sinontokens.jsonrejetĂ© en bloc, cf. CHANGELOG). Activer la validation C18 avecENFORCE_MISSION_TOKEN_VALIDATION=true. Par dĂ©faut (false) : un jeton invalide est journalisĂ© puis non opposĂ© â zĂ©ro impact standalone sans mcp-mission. â ïž RectifiĂ© en v0.16.3 (#154) : « continue » Ă©tait trompeur. Sans enforcement, un Ă©chec de validation laissemission_idvide, donc la clĂ© composite ne correspond Ă rien et la consommation est refusĂ©e au registre. Et surtout, ce qui exprime notre propre incapacitĂ© â service de clĂ©s injoignable, audience non rĂ©solue, validateur non initialisĂ© â est refusĂ© dans les deux postures, enbackend_unavailableoumisconfigured:enforcearbitre si un jeton invalide est fatal, il n'arbitre pas si nous disons la vĂ©ritĂ© sur le motif du refus.tenant_idetexpected_auddanssecret_wrapalimentent le binding C18 complet cĂŽtĂ©secret_consume(v0.6.8). â ïž (#86) DĂšs queENFORCE_MISSION_TOKEN_VALIDATION=true(seul, ou via le PEP/mcpci-dessous),MISSION_JWKS_URL,MCP_INSTANCE_ID/MISSION_TOKEN_AUDetMISSION_STATUS_URLdeviennent obligatoires (fail-fast au boot) â sans quoi une mission abortĂ©e conserverait l'accĂšs jusqu'Ă expiration dumission_token. â ïž (#86) Le validateur applique dĂ©sormais EXACTEMENT le mĂȘme contrat que le PEP/mcpci-dessous (mĂȘmes claims requis, mĂȘme vĂ©rificationcomponent_id) â un JWT authentique mais destinĂ© Ă une autre instance vault est rejetĂ© par les deux points d'application, pas seulement le premier.
Matrice d'effet externe â ce qu'un code d'erreur permet de DĂDUIRE (v0.14.0)
Tous les codes métier arrivent en ENVELOPPE, isError = false. Un appelant qui
ne regarde que isError les prend pour des succĂšs. Lire status, puis
error_type â dans cet ordre, car certains refus n'ont pas encore de code
(voir la note en fin de section).
isError = true ne signale que deux choses : une non-conformité au schéma
(paramĂštre requis absent ou de mauvais type â le corps de l'outil n'a pas
tourné, donc aucune écriture, aucun appel OpenBao, aucune révocation), ou un
défaut non prévu. Un paramÚtre conforme au schéma mais refusé sémantiquement
revient en enveloppe.
secret_wrap â le nom du code ne suffit pas : une ressource peut exister.
Code | Appel OpenBao | Ressource Ă compenser |
| absent | non, pour cet appel |
| absent pour cet appel | â ïž possiblement, d'une tentative ANTĂRIEURE â sans accessor, donc non compensable |
| absent pour cet appel | â ïž possiblement, d'une provision ANTĂRIEURE â mais elle porte un accessor, donc elle est compensable |
| absent pour cet appel | non â provision antĂ©rieure tenue pour terminĂ©e, rien Ă compenser |
| absent pour cet appel | â ïž inconnu â entrĂ©e illisible, rien ne peut en ĂȘtre dĂ©duit |
| absent | non |
| wrap créé | non â rĂ©vocation confirmĂ©e |
| wrap créé | â ïž possiblement â active jusqu'au TTL, non compensable |
| Ă©mis | â ïž inconnu â ne pas rejouer la mĂȘme clĂ© |
â ïž backend_error peut suivre un appel qu'OpenBao a reçu (dĂ©lai dĂ©passĂ©, coupure) ;
not_found est déduit d'un motif cherché dans le texte de l'exception, donc non
prouvé. Dans les deux cas l'intention passe failed sans accessor.
La clĂ© est alors BLOQUĂE DĂFINITIVEMENT â un rejeu rend operation_failed,
sans aucun appel OpenBao. (Jusqu'en v0.12.1 elle redevenait rejouable, et le
rejeu créait un second wrap pendant que le premier pouvait vivre.) Si la
persistance du marquage échoue, l'entrée revient pending et la clé est bloquée
sous operation_pending : le blocage est le mĂȘme, seul le code diffĂšre.
Reprise = nouvel operation_id, dans tous les cas.
â ïž v0.14.0 â UNE CLĂ EST Ă USAGE UNIQUE, SANS CONDITION. La moindre entrĂ©e pour le couple
(operation_id, mission_id)bloquesecret_wrap, quel que soit son état, entrée corrompue comprise : une entrée illisible est une raison de refuser, jamais d'autoriser. Plus rien ne libÚre une clé.En v0.13.0,
revoked,consumedetunusablela rouvraient â pour prĂ©server la reprise « rĂ©voquer puis recrĂ©er » du broker. Celle-ci a Ă©tĂ© retirĂ©e de sa conception, et le recensement des porteurs de jetonwrapen production n'en a trouvĂ© qu'un seul. Cette libĂ©ration Ă©tait aussi le dernier chemin par lequel un jeton Ă©tranger prĂ©sentĂ© Ăsecret_consumepouvait marquer une entrĂ©eunusableâ et libĂ©rer une clĂ© dont le wrap rĂ©el vivait encore.â ïž La garde ne porte QUE sur la crĂ©ation.
secret_revoke_wrap,secret_wrap_statusetsecret_wrap_lookuprestent ouverts sur une clĂ© engagĂ©e : sans cela l'appelant serait enfermĂ© avec des ressources qu'il ne pourrait plus ni voir ni couper. Un test compte les appelants de la garde et Ă©choue s'il en apparaĂźt un second.â ïž Deux contournements ne sont PAS fermĂ©s : la perte du fichier de registre (registre vide = toutes les clĂ©s redeviennent vierges) et la course entre deux instances, faute de CAS/ETag (#51). Ce ne sont pas des libĂ©rations mĂ©tier, mais l'unicitĂ© n'est pas absolue pour autant.
â ïž Ce premier contournement Ă©tait plus large que cette phrase ne le disait, jusqu'Ă v0.15.0 incluse (#149, corrigĂ© en v0.16.0). Il ne fallait pas perdre le fichier : il suffisait d'une erreur de stockage mal lue. Le registre concluait « objet absent » dĂšs que le texte de l'erreur contenait « 404 » â un identifiant de requĂȘte, un port, un 404 Ă©mis par un intermĂ©diaire â puis repartait vide en se dĂ©clarant fiable, si bien que le refus fail-close ne se dĂ©clenchait pas et que la sonde ne signalait rien. Une seconde porte menait au mĂȘme effet sans aucune erreur : une lecture qui rĂ©ussissait en renvoyant un JSON valide mais sans la clĂ©
wraps(un{}, un corps tronqué) était prise pour un registre vide. Désormais, seule une absence authentifiée par le champ d'erreur structuré (NoSuchKey) vide le registre, et le document est validé avant toute mutation d'état ; tout le reste refuse la création. C'est la rÚgle que les trois magasins d'autorisation appliquent depuis #69.
Une intention
failed(oupending) sans accessor rendfound_unattachedsursecret_wrap_lookup: aucune rĂ©vocation n'est possible, seul le TTL borne la ressource Ă©ventuelle. â ïž Ce verdict ne dit pas qu'une ressource existe â il dit que nous ne pouvons pas l'exclure. (v0.12.1 : ce cas rĂ©pondaitalready_revoked, affirmant une rĂ©vocation inexistante.)
Les quatre autres outils, sur les codes d'indisponibilité :
Outil | Effet externe | Conduite |
| rĂ©vocation FAITE, mais non inscrite au registre | rejouer la compensation plus tard â voir l'encadrĂ© ci-dessous |
| absent, aucune révocation tentée | rejeu sûr et nécessaire |
| appel émis, effet inconnu | rejeu sûr (révocation idempotente) |
| absent : les deux gardes fail-close précÚdent toute révocation | attente puis rejeu, sûr |
| inconnu sur les entrées non comptées ( | rejeu sûr, mais il ne résoudra pas les entrées figées |
| absent : registre absent ou dernier rafraßchissement S3 échoué | rejeu toujours sûr |
â ïž UN EFFET CONFIRMĂ PEUT NE PAS SURVIVRE Ă UN REDĂMARRAGE (limite structurelle, #140 â arbitrĂ©e, cf. #123). Quand une rĂ©vocation rĂ©ussit cĂŽtĂ© coffre mais que l'Ă©criture du registre Ă©choue, le fait n'est inscrit nulle part de durable : au redĂ©marrage, l'entrĂ©e peut rĂ©apparaĂźtre non rĂ©voquĂ©e, dans un Ă©tat que nous venons d'annoncer rĂ©voquĂ© Ă l'appelant.
« Peut » et non « va » : une exception S3 ne prouve pas que l'Ă©criture n'a pas abouti â un dĂ©lai d'attente peut suivre une Ă©criture dĂ©jĂ acceptĂ©e â et le registre est en dernier-Ă©crivain-gagne sans verrou, une autre instance a pu Ă©crire entre-temps. L'appelant ne peut donc pas dĂ©duire l'Ă©tat durable de l'Ă©chec ; il ne peut que le tenir pour indĂ©terminĂ©.
Ce n'est pas rĂ©parable avec un seul support durable â inscrire le fait exigerait un support disponible au moment prĂ©cis oĂč le support habituel ne l'est pas. Le tenir supposerait un second support : un journal local, rejouĂ© en rĂ©conciliation avant de resservir le registre. Le volume local existe dĂ©jĂ dans le dĂ©ploiement ; le mĂ©canisme, non â et il serait local Ă l'instance, invisible d'une autre et perdu avec l'hĂŽte.
â ïž DĂCISION (#123) : ce second support ne sera pas construit. La limite est assumĂ©e et publiĂ©e, elle n'est pas en attente d'un correctif. Ne dimensionnez pas votre contrat en supposant qu'elle disparaĂźtra.
Ce que nous faisons Ă la place, depuis v0.14.1 : nous ne rendons pas le fait durable, mais nous le disons â la rĂ©ponse porte alors
registry_persisted: falseet unwarning. Le champ est absent sur le chemin nominal.â ïž Le signal a la MĂME forme sur les DEUX verbes de rĂ©vocation â
secret_revoke_wrapetsecret_wrap_lookup:registry_persisted: false,count_not_persisted(le nombre d'entrĂ©es effectivement muĂ©es et non inscrites) et unwarning. Sur la compensation il est portĂ© aussi par les sortiespartial_revocationetconsume_terminalâ l'appelant y gĂšre dĂ©jĂ un incident, c'est le pire moment pour lui taire une rĂ©vocation non consignĂ©e.â Conduite : traiter un retour portant
registry_persisted: falsecomme une rĂ©vocation rĂ©elle mais non consignĂ©e â ne pas clore son propre registre dessus, et repasser la compensation plus tard. La mĂȘme limite touche la rĂ©vocation d'urgence desecret_wrap, oĂč l'entrĂ©e restependinget oĂčsecret_wrap_lookuprĂ©pondrafound_unattachedpour une ressource que nous tenons pour morte â le serveur traite un retour OpenBao sans exception comme une rĂ©vocation confirmĂ©e, il ne relit rien pour l'attester.
secret_wrap_statusne mute ni OpenBao ni le registre (cache et journal d'audit, eux, bougent). Il ne distingue pas le transitoire du durable : instantanĂ© best-effort. â ïž CorrigĂ© en v0.15.0 â une panne S3 survenant pendant la fenĂȘtre de cache n'Ă©tait alors pas dĂ©tectĂ©e du tout ; le rafraĂźchisseur de fond la constate dĂ©sormais Ă sa prochaine tentative (mi-TTL, puis toutes les 10 s), etstatusbascule enbackend_unavailable. La dĂ©tection n'est donc pas instantanĂ©e, mais elle a lieu.
Ce que le registre PROUVE â et ce qu'il ne prouve pas
đŽ secret_wrap_status rend ce que le REGISTRE CROIT, pas un constat OpenBao.
Aucun client OpenBao n'est appelé. Chaque réponse est une croyance datée :
activene prouve pas qu'un wrap vit â il a pu expirer, ĂȘtre consommĂ© ou rĂ©voquĂ© chez OpenBao sans qu'aucune transition de registre ne soit Ă©crite.consumingn'atteste pas qu'un dĂ©ballage soit parti : l'Ă©tat est persistĂ© avant l'appel d'unwrap.consumed,revoked,unusablene parlent que d'UNE entrĂ©e, jamais du couple : plusieurs entrĂ©es peuvent coexister pour un mĂȘme(operation_id, mission_id)â historiquement, parce que le verrou de clĂ© ne regardait quependingjusqu'en v0.12.1. â ïž Un Ă©tat terminal sur l'une ne dit rien des autres : une entrĂ©eunusablepeut coexister avec uneactive.revokedest aussi posĂ© quand OpenBao rĂ©pond 400/404 : c'est de l'idempotence, pas une preuve du sort du jeton.
â ïž ambiguous de secret_wrap_status et ambiguous de secret_wrap_lookup ne
sont pas le mĂȘme mot. Le status est pur ; le lookup est destructif â
face à plusieurs entrées il révoque les actives. Ne jamais appeler
secret_wrap_lookup pour comprendre un ambiguous : il détruirait ce qu'on
cherche à diagnostiquer. Aucun verbe ne détaille un ambiguous (ni cardinalité,
ni état individuel) : c'est un refus terminal, sans investigation.
Quand peut-on recréer aprÚs secret_revoke_wrap ?
Réponse | Recréer ? |
| â rĂ©vocation Ă©mise et acceptĂ©e, ou jeton dĂ©clarĂ© invalide/absent par OpenBao |
| â aucun appel OpenBao tentĂ© â accessor inconnu du registre ou hors pĂ©rimĂštre |
| â rien d'attestĂ© |
secret_consume â « aucun appel Ă©mis » ne veut pas dire « jeton intact » :
sur ces chemins nous ne regardons pas le jeton, qui peut avoir expiré ou avoir été
consommé en parallÚle.
Code | Cette invocation a prĂ©sentĂ© le wrap ? | Ătat du jeton |
| non | non constaté |
| non | non constaté |
| non | non constaté |
| non | non constaté |
| non | inconsommable en mode durci â reprovisionner |
| non | dépensé ou révoqué |
| selon le chemin | mort |
| possible | à considérer comme perdu |
| oui | brûlé |
â ïž LĂ oĂč l'Ă©tat est « non constatĂ© », dĂ©truire l'accĂšs de son propre fait est
une perte sĂšche. Mais une nouvelle tentative n'est pas une sonde : si la cause
du refus est levée, elle peut consommer le wrap.
â ïž backend_error n'est pas atteignable sur secret_consume (toute exception
d'unwrap tombe en wrap_unusable ou consume_outcome_unknown), et wrap_expired
n'existe pas â il a figurĂ© Ă tort dans une version antĂ©rieure de ce contrat.
â ïž already_consumed regroupe consumed et revoked ; secret_wrap_status
ne désambiguïse que s'il n'existe qu'une entrée, sinon il rend ambiguous.
Refus sans
error_typeâ la taxonomie fermĂ©e (unauthenticated,permission_denied,access_denied,invalid_input,invalid_token,policy_store_unavailable) couvre les gardes d'autorisation partagĂ©es. Des refus de validation propres Ă des outils hors pĂ©rimĂštre wrapping (secret_list, surfaces/admin/api) rendent encore une enveloppe sans code. Testerstatusavanterror_type.
PEP mission JWT â porte /mcp (v0.8.0, #47 + #69)
Second point d'application (PEP) du mission_token JWT ES256 de mcp-mission, à l'entrée /mcp (le premier étant secret_consume/C18, à la consommation). Piloté par MCP_AUTH_MODE :
Mode | Comportement |
| Bearer opaque obligatoire : une requĂȘte sans jeton, ou avec un jeton invalide, reçoit |
| â ïž Mode d'exception â ancien comportement de |
|
|
| JWT valide OU bearer opaque valide (migration). MĂȘmes exigences que |
En jwt/dual-stack, le middleware refuse activement : 401 (token absent/opaque/JWT invalide), 403 (aud/component_id â instance, mission inactive), 503 (JWKS indisponible â fail-close). Un JWT invalide ne retombe jamais sur le bearer. Le token est vĂ©rifiĂ© contre le contrat rĂ©el mcp-mission (aud contient MCP_INSTANCE_ID, component_id[MCP_COMPONENT_KIND] == MCP_INSTANCE_ID, iss, exp leeway 0, iat anti-skew). La bootstrap key admin reste acceptĂ©e (break-glass).
Une identité mission valide est authentifiée et liée à l'instance, puis mcp-vault (PDP local) lui résout un périmÚtre vault provisionné localement. Sans octroi, aucun accÚs (deny-by-default) ; les outils d'infra (
system_*, inventaire PKI) et l'admin-plane (vault_create/delete,ssh_*) lui sont refusés. L'endpoint adminPOST /admin/api/auth/jwks/reloadforce le rechargement du JWKS (révocation urgente dekid).
Octroi de pĂ©rimĂštre vault â MissionBindingStore (#69)
Le mission_token ne porte aucune autorisation vault : l'octroi est provisionné localement, indexé par tenant_id (claim stable), et stocké un fichier par instance sur S3.
Un octroi (« binding ») =
{tenant_id, allowed_resources:[coffresâŠ], permissions, policy_id?, enabled, expires_at?}.Permissions strictes :
readouread,write(jamaiswriteseul, jamaisadmin). Data-plane uniquement : lire/Ă©crire des secrets dans les coffres autorisĂ©s â jamais crĂ©er/dĂ©truire un coffre ni gĂ©rer une CA SSH.Fail-close : binding absent/dĂ©sactivĂ©/expirĂ© â aucun accĂšs. Registre indisponible/corrompu â
503(refus observable, jamais un deny silencieux ni un Ă©crasement destructeur).Administration (bearer/bootstrap admin uniquement â jamais via mission JWT) :
# Octroyer un périmÚtre lecture à un client mission
mcp-cli mission-binding create acme-corp --vaults prod-app --permissions read
# Lecture + écriture sur plusieurs coffres
mcp-cli mission-binding create acme-corp --vaults prod-app,staging --permissions read,write
mcp-cli mission-binding list
mcp-cli mission-binding get acme-corp
mcp-cli mission-binding delete acme-corp
mcp-cli mission-binding purge --older-than 30 --dry-run # bindings expirĂ©sGranularitĂ© tenant : toutes les missions actives d'un mĂȘme client (
tenant_id) partagent le périmÚtre octroyé. Le grain fin par mission existe déjà à la consommation (secret_consume/C18).
Audit (1)
Outil | Perm | Description |
| admin | Journal d'audit filtrable (ring buffer 5000 entrées + JSONL persistant) |
TraçabilitĂ© du cycle de vie des accĂšs (v0.7.0 â conformitĂ© SecNumCloud/HDS) : toute mutation du plan de contrĂŽle â crĂ©ation / mise Ă jour / rĂ©vocation / purge de token, crĂ©ation / suppression de policy â est journalisĂ©e, via l'API REST admin comme via les outils MCP. Les Ă©checs de persistance des opĂ©rations de retrait (rĂ©vocation, purge, suppression de policy â celles qui laissent un accĂšs actif si elles Ă©chouent) sont Ă©galement tracĂ©s. La trace vit dans
audit-mcp.jsonl, physiquement indĂ©pendant de_system/tokens.jsonâ elle survit Ă une purge. Le token brut n'apparaĂźt jamais dans le dĂ©tail d'audit.
# 1. Setup initial (ONE-TIME) â crĂ©er vault + rĂŽles SSH
vault_create("llmaas-infra", description="SSH CA LLMaaS")
ssh_ca_setup("llmaas-infra", "adminct", allowed_users="adminct", ttl="1h")
ssh_ca_setup("llmaas-infra", "agentic", allowed_users="agentic,iaagentic", ttl="30m")
# 2. Déployer la CA sur les serveurs (ONE-TIME)
result = ssh_ca_public_key("llmaas-infra")
# â Mettre result["public_key"] dans /etc/ssh/trusted-user-ca-keys.pem
# 3. Usage quotidien â signer une clĂ© publique
cert = ssh_sign_key("llmaas-infra", "adminct", public_key="ssh-ed25519 AAAA...", ttl="1h")
# â cert["signed_key"] = certificat signĂ©, valide 1h
# â OpenSSH l'utilise automatiquement si placĂ© Ă cĂŽtĂ© de la clĂ© privĂ©eđ Types de secrets (style 1Password)
Type | IcĂŽne | Champs requis | Usage |
| đ | username, password | Identifiants web/app |
| đ | password | Mot de passe simple |
| đ | content | Notes sĂ©curisĂ©es |
| đ | key | ClĂ©s API |
| đïž | private_key | Paires de clĂ©s SSH |
| đïž | host, username, password | Connexions BDD |
| đ„ïž | host, username | AccĂšs serveur |
| đ | certificate, private_key | Certificats TLS/SSL |
| đ | content | Fichiers .env |
| đł | number, expiry, cvv | Cartes bancaires |
| đ€ | name | IdentitĂ©s |
| đ¶ | ssid, password | RĂ©seaux Wi-Fi |
| âż | (tout optionnel) | Wallets crypto |
| âïž | (champs libres) | Tout le reste |
Chaque secret supporte : tags, favorite, versioning KV v2 automatique.
đ Authentification
â ïž Seul le header
Authorization: Bearer <token>est accepté. L'authentification par query string (?token=) a été supprimée pour des raisons de sécurité (v0.3.1).
Authorization: Bearer <token>Permission | Lecture | Ăcriture | Admin |
| â | â | â |
| â | â | â |
| â | â | â |
3 couches d'isolation :
Vault-level :
allowed_resources=[]â owner-based (seuls les vaults créés par le token), ou liste expliciteTool-level : policies avec
allowed_tools/denied_tools(wildcards fnmatch)Path-level :
allowed_pathsdans lespath_rulesâ contrĂŽle par secret individuel
đ„ïž CLI
MCP Vault inclut un CLI complet avec Click + Rich :
# Commandes scriptables
python scripts/mcp_cli.py health
python scripts/mcp_cli.py about
python scripts/mcp_cli.py whoami # Identité du token courant
python scripts/mcp_cli.py vault list
python scripts/mcp_cli.py vault create serveurs-prod -d "Clés SSH prod"
python scripts/mcp_cli.py secret write serveurs-prod web/github -d '{"username":"me","password":"s3cr3t"}' -t login
python scripts/mcp_cli.py secret read serveurs-prod web/github
python scripts/mcp_cli.py secret password -l 32
python scripts/mcp_cli.py token create agent-sre --vaults prod --policy readonly
python scripts/mcp_cli.py token list
python scripts/mcp_cli.py token purge-revoked --older-than 30 --dry-run # aperçu (rien supprimé)
python scripts/mcp_cli.py token purge-revoked --older-than 30 --yes # purge effective
python scripts/mcp_cli.py policy create no-ssh -d "Pas de SSH" --denied "ssh_*"
python scripts/mcp_cli.py policy create team-x --allowed "secret_*" --path-rules '[{"vault_pattern":"shared-*","allowed_paths":["shared/*"]}]'
python scripts/mcp_cli.py audit --status denied --limit 10
# AccÚs SSH JIT opérateur
python scripts/mcp_cli.py ssh profiles
python scripts/mcp_cli.py ssh request bastion-prod \
--key ~/.ssh/id_ed25519.pub --reason "INC-230 maintenance bastion"
# PKI interne (v0.5.0)
python scripts/mcp_cli.py pki setup --lab --domains '*.lesur.lan,lesur.lan'
python scripts/mcp_cli.py pki ca-key
python scripts/mcp_cli.py pki certs
python scripts/mcp_cli.py pki revoke 12:34:ab:cd:ef:12:34:56L'aide
--helpde chaque commande explique le modÚle de sécurité à 3 couches et guide l'utilisateur.
Voir scripts/README.md pour la documentation complĂšte du CLI.
âïž Variables d'environnement
Copier .env.example â .env et adapter. .env.example est le contrat de
configuration de référence : il déclare exactement les 45 variables
consommées par Settings (src/mcp_vault/config.py),
ni plus ni moins, et il est vérifié par
tests/test_env_example_contract.py.
â ïž
extra="forbid": une clé absente de ce contrat, portant une valeur non vide dans votre.env, fait échouer le démarrage. Ne composez jamais un.envdepuis une autre source que.env.example.Marqueur
REQUIRED: une variable dont la valeur d'exemple estREQUIREDn'a aucun dĂ©faut utilisable. Le marqueur Ă©choue volontairement la validation â le service refuse de dĂ©marrer tant qu'un dĂ©ploiement ne l'a pas substituĂ© par une vraie valeur.
Groupe | Variables | Obligatoire |
Serveur |
| Non â tous ont un dĂ©faut. Mais derriĂšre un reverse-proxy, |
WAF |
| Non â dĂ©faut |
Auth |
| Oui â seule variable sans dĂ©faut utilisable. Chiffre les clĂ©s unseal et sert de credential admin de secours. Minimum 32 caractĂšres, â„ 3 classes de caractĂšres ; dĂ©marrage refusĂ© sinon |
SSH JIT opérateur (v0.9.0) |
| Non â vide = dĂ©sactivĂ© ; aucun secret dans le profil |
S3 |
| Oui pour un dĂ©ploiement fonctionnel. Le processus dĂ©marre sans S3, mais en mode dĂ©gradĂ© : les stores restent en mĂ©moire (tokens perdus Ă chaque redĂ©marrage) et, sur un volume vierge, la tentative de restauration Ă©choue de façon ambiguĂ«, ce qui empĂȘche OpenBao de dĂ©marrer (fail-close volontaire, cf. |
Storage sync |
| Non |
OpenBao |
| Non |
PKI (v0.5.x) |
| Non â vide = dĂ©duit du premier FQDN ; override de l'URL ACME en test Docker |
Mission JWT (v0.6.0) |
| Non â standalone sans mcp-mission |
PEP mission JWT (v0.8.0) |
| â ïž Oui depuis #116 â le dĂ©faut |
Tokens sensibles du CLI â ce ne sont PAS des variables du serveur.
VAULT_WRAP_TOKENetVAULT_MISSION_TOKENsont lus uniquement par le CLI (scripts/cli/) et ne figurent pas dansSettings: les placer dans.envferait Ă©chouer le dĂ©marrage du serveur. Ils changent Ă chaque opĂ©ration (usage unique, TTL court) â les passer inline devant la commande, jamais dans un fichier, sinon ils fuitent dans l'historique de votre shell systĂšme :VAULT_WRAP_TOKEN=hvs.CAES... mcp-vault secret consume op-123
Voir .env.example pour la documentation détaillée de chaque variable.
đïž Architecture
đ Documentation complĂšte : le dossier
DESIGN/mcp-vault/contient la spĂ©cification dĂ©taillĂ©e (ARCHITECTURE.md â vision, sĂ©curitĂ©, SSH CA, policies, roadmap HSM) et la documentation technique (TECHNICAL.md â modules, Docker, tests, dĂ©pendances).
Internet â WAF (Caddy + Coraza :8085) â MCP Vault (Python :8030) â OpenBao (:8200 localhost)
â OWASP CRS v4 â
Protection L7 S3 Dell ECS (persistance)WAF â Caddy + Coraza (OWASP CRS v4)
Le WAF protĂšge l'API contre les attaques L7 (injections SQL, XSS, LFI, RCE, SSRF) :
Caddy v2.11.2 compilé avec coraza-caddy v2.5.0 (coraza v3.7.0) via
xcaddyâ montĂ©e depuis v2.2.0/coraza 3.3.3 en v0.9.2, requise parSecRequestBodyJsonDepthLimit(cf. #107)23 fichiers de rĂšgles OWASP CoreRuleSet v4.7.0 chargĂ©s (dĂ©compte vĂ©rifiable :
grep -c '^Include /opt/crs/rules/' waf/coraza.conf)Mode Blocking sur TOUS les endpoints (health,
/mcp,/admin/api)Corps JSON-RPC réellement parsé sur
POST /mcp(ctl:requestBodyProcessor=JSON, v0.9.2). Sans ce rĂ©glage, Coraza traitait le corps entier comme une variable unique : toute chaĂźne d'une liste de phrases CRS prĂ©sente n'importe oĂč â y compris dans une valeur de secret â dĂ©clenchait un blocage, et aucun ciblage fin d'exclusion n'Ă©tait possibleExclusions de RĂGLES (
ctl:ruleRemoveById) dĂ©clarĂ©es avant l'Include CRS â pattern « exclusions before CRS », requis pour neutraliser les rĂšgles Ă©valuĂ©es en phase:1. Faux positifs couverts : Unicode français 920540, noms PowerShell 932120, distribution CA/CRL.pem920440, Content-Type JOSE ACME 920420Exclusions de CIBLES (
SecRuleUpdateTargetById) dĂ©clarĂ©es aprĂšs l'Include CRS â sĂ©mantique inverse : cette directive modifie la dĂ©finition de la rĂšgle, qui doit donc ĂȘtre dĂ©jĂ chargĂ©e. Couvre le faux positif 930120 sur.env(v0.9.2) : les noms d'arguments sous l'enveloppe MCPparams._metaet les valeurs de la charge opaque desecret_write(seul outil Ă en recevoir une â transmise Ă OpenBao, qui la chiffre au repos). Le reste demeure inspectĂ© â chemin, nom de coffre, Ă©tiquettes, nom d'outil, et toutes les rĂšgles de path traversalProfondeur JSON bornĂ©e Ă 32 sur
POST /mcp(SecRequestBodyJsonDepthLimit, v0.9.2). Sans elle, un corps de 15 Ko d'imbrication â JSON valide, 2 octets par niveau â faisait tomber le WAF en OOM. La limite seule crĂ©ait toutefois une Ă©vasion silencieuse (au-delĂ du seuil, plus aucune inspection) : la rĂšgle 10011 refuse donc explicitement (400) tout corps dĂ©clarĂ© non analysable parREQBODY_ERROR. â ïž Limite de compatibilitĂ© : un secret dontdatadĂ©passe ~28 niveaux d'imbrication interne est refusĂ© â les types de secrets du produit sont des dictionnaires plats, aucun usage connu n'y toucheGardes anti-Ă©vasion 10009/10010 (v0.9.2). Ces exclusions sont des directives globales dont le sĂ©lecteur est un nom d'argument, donc une chaĂźne choisie par le client :
ARGS/ARGS_NAMESagrĂšgent aussi query string et formulaire. Avant ces gardes,GET /health?json.params.arguments.data.x=.envpassait (200) lĂ oĂčGET /health?legit=.envĂ©tait refusĂ© (403) â soit une neutralisation globale de 930120 par simple choix du nom de paramĂštre. Les gardes refusent dĂ©sormais tout nom prĂ©fixĂ©json.hors du corps JSON MCP analysĂ©, discriminĂ© par un drapeau serveur non forgeable. La propriĂ©tĂ© est imposĂ©e, pas supposĂ©eHeaders de sĂ©curitĂ© : CSP, X-Frame-Options DENY, X-XSS-Protection, nosniff
Méthodes autorisées adaptées au protocole MCP : GET, POST, DELETE, PUT, PATCH
Stack ASGI (6 couches)
PkiMiddleware â AdminMiddleware â HealthCheckMiddleware â AuthMiddleware â LoggingMiddleware â MCPServerPkiMiddleware (v0.5.0) est la couche la plus externe â intercepte /acme/* et /pki/ca/*.pem avant l'auth (endpoints publics par design PKI/ACME).
Depuis la v0.20.0, le serveur utilise mcp==2.1.1 et accepte Ă la fois le
protocole moderne 2026-07-28 et les clients legacy jusqu'Ă 2025-11-25.
Le transport HTTP est stateless dans les deux Ăšres : il n'exige plus
d'affinité de session. MCP Vault reste néanmoins mono-instance : OpenBao
embarqué, la synchronisation S3 last-write-wins et les caches d'autorisation ne
permettent pas encore un déploiement multi-réplica sûr. Le SDK annonce automatiquement la
capability subscriptions/listen, mais MCP Vault ne publie ni ressource ni
événement et ne configure aucun store de reprise ; cette capability est donc
inerte et doit ĂȘtre rĂ©auditĂ©e Ă chaque montĂ©e de version du SDK.
AprÚs une fermeture de connexion sans réponse terminale, un client ne doit pas
réessayer automatiquement une mutation : son effet peut déjà avoir eu lieu.
Cela vaut notamment pour secret_wrap_lookup, qui révoque des wraps malgré son
nom de consultation.
Le point de décision lit la mémoire, jamais le réseau (v0.15.0, #123)
Un appel S3 bloquant ne gelait pas une requĂȘte : il gelait le service entier, sonde de santĂ© comprise â 488 s mesurĂ©es en production (#110). La cause est toujours la mĂȘme : boto3 est synchrone, et un appel synchrone dans la boucle asyncio monopolise l'unique thread qui sert toutes les requĂȘtes.
Le lot 1 (v0.10.1) a bornĂ© la durĂ©e de ces appels, le lot 2 (#122) en a sorti cinq de la boucle. Ce lot ferme les derniers, et ce sont les plus frĂ©quents : les magasins d'autorisation, interrogĂ©s Ă chaque requĂȘte authentifiĂ©e.
Plus aucun appel S3 ne part d'un point de décision. Les gardes (
check_policy,check_path_policy,check_wrap_*,get_by_hash,resolve) lisent un instantanĂ© publiĂ© en mĂ©moire. Des tĂąches de fond â une par magasin â le renouvellent hors boucle, Ă la moitiĂ© du TTL, et toutes les 10 s aprĂšs une panne.
â
Le dernier appel réseau synchrone du chemin d'authentification est fermé
(v0.16.1, #148). Le rafraĂźchissement du cache JWKS (mission_jwt.py,
httpx.get synchrone) était déclenché depuis la validation d'un jeton mission,
appelĂ©e sans await dans une coroutine. MĂȘme classe de dĂ©faut que #110, sur
un chemin aussi chaud. Il était déclaré ici comme résidu depuis v0.15.0 ;
il ne l'est plus.
Mesuré en production le 17/08 avant correction : 117,2 ms de gel du service
entier par rafraĂźchissement, au plus une fois par TTL de 60 s. Trois chemins y
menaient, tous des coroutines, et les trois sont déportés hors boucle par
run_blocking :
Site | Ce qui l'active |
PEP transport ( |
|
| â ïž la simple prĂ©sence de |
| appel opĂ©rateur â le plus lent, il force le fetch |
â ïž
JWKSCachen'est PAS réécrit. Il est dĂ©jĂ thread-safe (threading.Lock) : plusieurs threads d'exĂ©cuteur s'y sĂ©rialisent, un seul fait le fetch, les autres trouvent l'instantanĂ© frais. Son fail-close (un cache pĂ©rimĂ© n'est jamais servi), son backoff et son throttle « kid inconnu » sont inchangĂ©s â les dĂ©placer aurait transformĂ© un gel en martĂšlement de l'endpoint demcp-mission.
â ïž
MISSION_JWKS_CACHE_TTLn'est pas un rĂ©glage de latence. Une clĂ© rĂ©voquĂ©e disparaĂźt du JWKS demcp-mission: ce TTL est donc la fenĂȘtre de propagation d'une rĂ©vocation de clĂ© de signature. Il vaut 60 s, cinq fois plus strict que lemax-age=300qu'ils publient, et il ne doit pas ĂȘtre allongĂ© pour espacer les appels.
Ce qui change | Ce qui ne change pas |
Le chargement quitte la boucle | Le fail-close de #86/#69 : sur le Policy Store, le Mission Binding Store et le registre wrap, un instantané périmé au-delà de son TTL ne sert plus aucune décision |
Les mutations aussi ( | La forme, les codes d'erreur et les signatures des outils existants. Deux évolutions bornées : |
Une opération wrap est atomique vis-à -vis d'un rechargement | La posture du Token Store au point de décision : |
Deux horloges, pas une. Le code historique n'en portait qu'une, qui servait Ă la fois Ă dater l'instantanĂ© et Ă brider les re-tentatives. Une panne les mettait en dĂ©saccord : repousser la date pour ne pas marteler S3 rajeunissait aussi, mĂ©caniquement, un instantanĂ© jamais rechargĂ© â un magasin en panne restait donc « frais » indĂ©finiment. Elles sont dĂ©sormais sĂ©parĂ©es, et en temps monotone : un recul d'horloge murale ne peut plus rajeunir un Ă©tat pĂ©rimĂ©.
Copy-on-write sur les trois magasins d'autorisation : une mutation construit un candidat, le persiste, et ne le publie qu'aprĂšs confirmation du PUT. Une garde synchrone ne peut donc plus observer une policy créée mais non persistĂ©e, ni une suppression que l'Ă©chec du PUT annulera. Les rollbacks disparaissent â donc aussi le risque d'en oublier un.
â ïž Le registre wrap n'a dĂ©libĂ©rĂ©ment PAS de copy-on-write. Aucun lecteur synchrone ne l'interroge, et la publication-aprĂšs-PUT y dĂ©truirait un acquis de v0.14.1 :
mark_entries_revokedconserve volontairement en mémoire une révocation confirmée mais non inscrite, parce que la mémoire y dit la vérité et que c'est le support durable qui est en retard.
Observabilité. system_health porte désormais la fraßcheur de chaque magasin
(stores, et stores_stale s'il y a lieu). Sans cela, un rafraĂźchisseur mort ne
se verrait qu'au premier refus : le service répondrait normalement, puis
refuserait tout d'un coup.
Le registre wrap y ajoute sa volumétrie (v0.16.0, #146) : entries, le
nombre d'entrĂ©es, et last_write_bytes, le poids du dernier corps Ă©crit â null
tant qu'aucune écriture n'a eu lieu depuis le démarrage. Le registre n'a ni purge
ni écriture incrémentale : il est réécrit intégralement à chaque transition,
et nous n'avions aucun chiffre pour dimensionner ce coût.
â ïž
last_write_bytesest relevé à l'écriture, jamais calculé par la sonde. Sérialiser le registre à chaque appel de supervision ferait de la sonde une cause du problÚme qu'elle mesure.entries, lui, est un simple décompte en mémoire.
/healthet/readylisent dĂ©sormais la fraĂźcheur en mĂ©moire : un magasin configurĂ© jamais chargĂ© ou pĂ©rimĂ© rend le service503 unavailable, car les dĂ©cisions d'autorisation ne peuvent plus ĂȘtre garanties. Ces sondes ne font aucun appel S3 ; les rafraĂźchisseurs existants peuvent les rĂ©tablir sans redĂ©marrage./healthzreste une liveness indĂ©pendante.system_healthreste plus strict et vĂ©rifie en plus la connectivitĂ© S3 courante.
Ce fail-close de readiness est uniforme, y compris pour le Token Store : il ne bloque pas lui-mĂȘme une requĂȘte MCP dĂ©jĂ routĂ©e, mais demande Ă l'orchestrateur de retirer l'instance. Le produit Ă©tant mono-instance, une panne S3 assez longue pour pĂ©rimer tous les magasins provoque donc une indisponibilitĂ© totale cĂŽtĂ© load balancer. C'est le contrat explicitement retenu en v0.20.1 : une dĂ©cision d'autorisation non garantie ne doit pas ĂȘtre annoncĂ©e disponible.
â ïž Limite inchangĂ©e : rien ici ne ferme la race d'Ă©criture multi-instance (#13/#51). Le dernier Ă©crivain gagne toujours, et un rafraĂźchissement de fond Ă©largit mĂȘme la fenĂȘtre pendant laquelle deux instances peuvent diverger sans le savoir.
Sondes de santé (v0.11.0, #103)
Deux questions distinctes, deux contrats :
Endpoint | Question | Sain | Dégradé |
| liveness â le processus rĂ©pond-il ? |
|
|
| disponibilitĂ© â le coffre peut-il servir ? |
|
|
status appartient Ă une Ă©numĂ©ration fermĂ©e, une par question : healthy | sealed | unavailable pour la disponibilitĂ©, alive pour la liveness. Un coffre scellĂ© est donc identifiable de l'extĂ©rieur, sans qu'aucun dĂ©tail de dĂ©pendance ne soit exposĂ© â le diagnostic reste dans les journaux et sur /admin/api/health, qui exige un bearer valide.
Le HEALTHCHECK du conteneur vise /health : un coffre indisponible apparaĂźt dĂ©sormais unhealthy. Branchez une Ă©ventuelle sonde de liveness ou un autoheal sur /healthz, jamais sur /health â redĂ©marrer un coffre scellĂ© ne le descelle pas, et redĂ©marrer ne rĂ©tablit pas S3.
AprÚs un démarrage non abouti, l'état est latché : le signal ne repasse pas
au vert tout seul, un redémarrage est requis. Cette rÚgle ne concerne pas un
magasin temporairement périmé aprÚs un démarrage abouti : son refresher peut
rétablir /health sans nouvelle génération.
Lifecycle OpenBao
STARTUP: S3 download (3 Ă©tats : restaurĂ© / absence confirmĂ©e / Ă©chec ambigu â
un Ă©chec ambigu refuse le dĂ©marrage) â bao server â init/unseal â periodic sync
RUNTIME: secrets via hvac â sync S3 toutes les 60s, SEULEMENT si l'Ă©tat a changĂ©
SHUTDOWN: seal â S3 upload final (sautĂ© si le dĂ©marrage n'a pas abouti, pour ne pas Ă©craser une sauvegarde valide) â stop process
CRASH: Docker volume local â redĂ©marrage immĂ©diatSĂ©curitĂ© des clĂ©s unseal
Les clés unseal d'OpenBao sont protégées par séparation physique à 3 facteurs :
Facteur | Stockage | Compromis seul = insuffisant |
Données chiffrées (barrier OpenBao) | Volume Docker + S3 | Illisibles sans unseal key |
Clés unseal (chiffrées AES-256-GCM) | S3 uniquement | Indéchiffrables sans bootstrap key |
ADMIN_BOOTSTRAP_KEY | Variable d'env uniquement | Inutile sans les clés chiffrées |
Invariants : les clĂ©s unseal ne sont jamais en clair sur disque â uniquement en mĂ©moire pendant le runtime. Un crash efface automatiquement les clĂ©s.
Roadmap sécurité :
Version | Approche |
v0.8.0 (actuel) | Clés sur S3 chiffrées AES-256-GCM+AAD, mémoire seule au runtime |
v1.0 | Transit Auto-Unseal via OpenBao dédié (KMS Cloud Temple) |
v2.0 | Connexion HSM (Hardware Security Module) Cloud Temple â les clĂ©s ne quittent jamais le module matĂ©riel certifiĂ© |
đ Voir DESIGN/mcp-vault/ARCHITECTURE.md §8 et §11 pour les dĂ©tails complets.
đ Tests (~1500 unitaires mockĂ©s + 349 e2e rĂ©els)
đ Voir tests/README.md pour le guide complet d'exĂ©cution.
# 1. Tests CLI â parsing + affichage (197 tests, SANS serveur)
python tests/test_cli_all.py
# 2. Tests CLI LIVE â cycle complet (79 tests, serveur rĂ©el)
MCP_URL=http://localhost:8085 MCP_TOKEN=<key> python tests/test_cli_live.py
# 3. Tests e2e MCP (349 tests, dans Docker)
docker compose exec mcp-vault python tests/test_e2e.py
# 4. Tests crypto (18 tests, SANS serveur â AES-256-GCM + AAD + validation entropie)
python tests/test_crypto.py
# Un seul groupe CLI
python tests/test_cli_all.py --only policy
# Un seul groupe e2e
docker compose exec mcp-vault python tests/test_e2e.py --test enforcementCouverture e2e (349 tests, 15 catégories)
Catégorie | Tests | Description |
SystĂšme | 7 | health, about, services, tools_count (37) |
Vault CRUD | 28 | create + métadonnées, list, info + owner, update, delete, confirm, erreurs |
Secrets CRUD | 24 | 10 types écrits, read/list/delete, validation |
Versioning | 8 | v1âv2âv3, read latest, read spĂ©cifique |
Passwords | 14 | longueurs, options, exclusions, CSPRNG |
Isolation | 7 | secrets cloisonnés entre vaults |
Erreurs | 10 | edge cases, vault inexistant, type invalide, protection |
S3 Sync | 3 | archive tar.gz sur S3 |
SSH CA | 33 | setup, rĂŽles multiples, signature ed25519, list/info roles, isolation CA, cleanup |
Types | 14 | 14 types vérifiés individuellement |
Admin API | 15 | health, whoami, generate-password, logs, unicité CSPRNG |
Policies MCP | 43 | CRUD, validation, wildcards, path_rules, doublons, erreurs, Admin API REST |
Policy Enforcement | 37 | check_policy, token_update, denied/allowed, changement policy, Admin API |
Audit Log | 31 | audit_log MCP, filtres (category/tool/status/since/limit), stats, Admin API /audit |
WAF Security | 62 | LFI, SQLi, XSS, RCE, scanners â 403 ; faux positif 930120 ; anti-Ă©vasion |
đ Structure du projet
mcp-vault/
âââ .env.example # Configuration (copier en .env)
âââ docker-compose.yml # WAF + MCP Vault + volumes
âââ Dockerfile # Multi-stage (OpenBao 2.5.1 + Python 3.12)
âââ requirements.txt # DĂ©pendances Python
âââ requirements.lock # DĂ©pendances pinnĂ©es (versions exactes)
âââ VERSION # version courante du service
âââ DESIGN/mcp-vault/
â âââ ARCHITECTURE.md # SpĂ©cification dĂ©taillĂ©e (v0.15.0)
â âââ TECHNICAL.md # Documentation technique (v0.15.0)
â âââ SECURITY_AUDIT.md # Rapport d'audit consolidĂ© (60 findings V2.1)
âââ scripts/
â âââ mcp_cli.py # CLI entry point
â âââ README.md # Documentation CLI
â âââ cli/ # Module CLI (Click + Rich)
â âââ __init__.py # Config (.env, BASE_URL, TOKEN)
â âââ client.py # MCPClient (Streamable HTTP)
â âââ commands.py # Groupes Click
â âââ display.py # Affichage Rich
âââ src/mcp_vault/
â âââ config.py # Configuration pydantic-settings
â âââ server.py # MCPServer + 39 outils MCP + lifecycle + audit
â âââ lifecycle.py # Orchestrateur startup/shutdown
â âââ s3_client.py # Client S3 hybride SigV2/SigV4
â âââ s3_sync.py # Sync file backend â S3
â âââ auth/ # Bearer tokens, check_access, ContextVar
â âââ admin/ # Console web /admin + API REST
â âââ openbao/ # Process manager, HCL config, lifecycle
â âââ vault/ # Spaces, secrets, SSH CA, types
â âââ static/ # Console admin SPA (paritĂ© CLI 100%)
â âââ admin.html # HTML structure + 7 modals
â âââ css/admin.css # Design Cloud Temple (dark theme)
â âââ js/ # 9 modules JS (config, api, app, dashboard, vaults, tokens, policies, activity, pki)
â âââ img/ # logo-cloudtemple.svg
âââ tests/
â âââ README.md # Guide d'exĂ©cution des tests (auditeurs)
â âââ TEST_CATALOG.md # Catalogue des tests pour auditeurs
â âââ test_cli_all.py # 197 tests CLI parsing (sans serveur)
â âââ test_cli_live.py # 79 tests CLI live (serveur rĂ©el)
â âââ test_e2e.py # 349 tests MCP e2e (15 catĂ©gories)
â âââ test_crypto.py # 18 tests AES-256-GCM + AAD
â âââ test_service.py # 78 tests bas niveau
â âââ test_integration.py # Tests pytest
â âââ cli/ # Tests CLI dĂ©coupĂ©s par groupe (7 fichiers)
âââ waf/ # WAF Caddy + Coraza (OWASP CRS v4)
âââ Dockerfile # Multi-stage (xcaddy + CRS v4.7.0)
âââ Caddyfile # Reverse proxy + coraza_waf
âââ coraza.conf # Config Coraza + exceptions MCPđ ĂcosystĂšme MCP Cloud Temple
Serveur | RĂŽle | Port |
MCP Tools | BoĂźte Ă outils (SSH, HTTP, shell) | :8010 |
Live Memory | Mémoire de travail partagée | :8002 |
Graph Memory | Mémoire long terme (graphe) | :8080 |
MCP Vault | đ Coffre-fort Ă secrets | :8030 |
MCP Agent | Runtime d'agents autonomes | :8040 |
MCP Mission | Orchestrateur de missions | :8020 |
Licence : Apache 2.0 | Auteur : Cloud Temple | Version : 0.20.1
This server cannot be deployed
Maintenance
Related MCP Connectors
Encrypted secret store and rotation for autonomous agent credentials
Secrets for developers and agentsâsecure context and workflows without exposing secret values.
Give your AI hands. Identity, credential vault, and API gateway for autonomous agents.
Credential broker for AI agents: scoped, revocable API access with policy enforcement and audit.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceMCP-native credential vault that enables AI agents to authenticate with external services without exposing secrets, supporting bearer, basic, OAuth2, and other auth patterns via MCP tools.10 npm1MIT
- AlicenseNot gradedqualityAmaintenanceProvides a zero-knowledge credential vault for AI agents, allowing secure storage, retrieval, and management of secrets with cross-agent delegation and tamper-evident audit.MIT
- AlicenseNot gradedqualityDmaintenanceAgent Vault is a local credential vault for AI agents. It enables agents to use secrets by name without ever seeing their values, with host allowlists, output scrubbing, and audit logging.4MIT
- AlicenseAqualityBmaintenanceEnables AI agents to make authenticated API calls and run commands with secrets injected, while keeping credentials completely hidden from the model, with policy enforcement, grants, and audit logging.6MIT


