Sécurité et limites¶
Sécurité du serveur lui-même¶
- Deux processus : le serveur MCP (exposé aux agents) et le contrôleur (API REST, interface
web, administration) tournent séparément et ne partagent que la base (voir
Architecture et déploiement). Le serveur MCP n'accepte que les
jetons
mcpetadmin, le contrôleur que les sessions web et les jetonsadminetapi, pluscisur ses routes de pipeline (/api/v1/ci/: gate, SARIF et tendance en lecture seule, scans qui enregistrent des rapports de ses projets) et nulle part ailleurs. Le contrôleur n'expose aucune route d'écriture sans jeton : il peut rester sur un réseau interne, et la compromission du serveur MCP exposé ne donne pas accès à l'interface d'administration. - Transport HTTP : authentification MCP par jeton Bearer et TLS intégrés mais facultatifs (voir
Serveur HTTP, authentification et TLS). Les jetons sont stockés en
base et seules leurs empreintes SHA-256 sont conservées. Chaque requête recherche l'empreinte
du jeton présenté, ce qui ne révèle rien d'exploitable sans préimage, puis la vérifie en temps
constant : une révocation est donc immédiate sur toutes les instances. Un jeton créé avec
--projectest limité à ce projet dans les outils MCP et les routes de pipeline : tout autre projet, rapport ou finding est traité comme inexistant (voir jetons restreints à un projet). Tant qu'aucun jeton n'existe, le serveur MCP reste fermé (seul l'enrôlement est accessible), sauf s'il écoute sur localhost ou avecSECREVIEW_ALLOW_UNAUTHENTICATED=true. Si l'écoute n'est pas sur localhost, un avertissement est journalisé quand l'authentification ou le TLS manque. Le serveur ne génère jamais de certificat TLS : le HTTPS utilise le certificat fourni par l'opérateur, signé par une autorité de certification. Il n'y a pas de limitation de débit. Chaque jeton porte un rôle (mcp,api,admin,ci,metrics) ; un jetonreaderoutriagerstocké, laissé par une version antérieure, n'authentifie rien. Un jetonapine porte que des droits du contrôle d'accès, lus à chaque requête : ceux de son utilisateur (jeton personnel, supprimé avec l'utilisateur, jamais renouvelé par un tiers, dont le nouveau secret porterait les droits de l'utilisateur) ou ceux de son rôle sur ses projets (jeton de rôle, dont le créateur doit détenir chaque permission). Un jeton ne crée jamais un autre jeton personnel. Un jetonmcpsans projet accède à tous les outils MCP et à tous les projets ; un jetonmcppersonnel atteint les projets que son utilisateur peut lire à l'instant, aucun quand l'utilisateur n'en a pas (jamais tous les projets par défaut), et s'arrête quand l'utilisateur est supprimé ou doit changer un mot de passe défini par un administrateur. Un utilisateur web détient au plusSECREVIEW_PERSONAL_TOKENS_PER_USERjetons personnels de chaque rôle (1 par défaut). - Expiration et renouvellement des jetons : un jeton expiré n'authentifie plus rien (
401), etSECREVIEW_TOKEN_MAX_DAYSborne la validité de tout jeton créé ou renouvelé. Les jetons expirés comptent toujours comme configurés : un serveur dont tous les jetons ont expiré reste fermé. Un renouvellement n'enregistre que l'empreinte du nouveau secret ; l'empreinte précédente est conservée pendant le délai de grâce (168 heures au plus), comparée en temps constant comme l'actuelle, et l'ancien secret d'un jeton expiré ne revient jamais (voir expiration et renouvellement). - Jetons refusés sur
/mcp: un jeton inconnu, révoqué ou expiré reçoit un401dont la description invite l'utilisateur à se connecter à l'interface web pour créer un nouveau jeton MCP personnel (voir jeton refusé) ; il n'atteint jamais le serveur d'enrôlement. Chaque refus est aussi journalisé en avertissement par le serveur MCP (Rejected unauthenticated request/Rejected expired token, avec l'adresse du client) ; la métriquesecreview_auth_rejections_totalne compte que les refus du contrôleur.SECREVIEW_WEB_URL, donnée dans cette description, est une URL publique : elle ne contient aucun identifiant (refusé au démarrage). - Journal d'audit : les décisions de triage, les propositions des agents, les créations,
renouvellements et révocations de jetons et les enrôlements sont écrits
dans la même transaction que le changement, avec leur acteur (
user:,token:,cli,anonymous). Les détails sont bornés et masqués, et ne contiennent jamais de jeton ni de mot de passe ; aucun outil ni aucune route ne modifie un événement, seule la purge deSECREVIEW_AUDIT_RETENTION_DAYSles supprime (voir journal d'audit). - Métriques :
/metricsn'existe que sur le contrôleur (le serveur MCP garde/mcp,/api/v1/enrollet/healthz), derrière un jetonmetricsouadmin; un jetonmetricsn'atteint rien d'autre. La réponse contient des nombres et des noms de projets, jamais un finding, un label de jeton ou un secret, et chaque valeur de label est échappée (voir métriques Prometheus). - Jetons de pipeline (
ci) : toujours restreints à des projets (un jetoncisans projet ne peut pas être créé, et un jeton trouvé sans projet n'en atteint aucun). Il ne lit que le gate, le journal SARIF et la tendance de ses projets ; les autres routes du contrôleur,/mcpet l'interface web répondent403, les autres projets404. Le client de pipeline (security-review-ci) n'ouvre aucune base, ne lit le jeton que dans l'environnement ou sur stdin, refuse le HTTP en clair hors localhost, les identifiants dans l'URL et les redirections, et borne les réponses. - Enrôlement (actif par défaut,
SECREVIEW_ENROLLMENT) : un client sans jeton atteint un serveur MCP restreint qui ne permet que de demander son projet, etPOST /api/v1/enrollest public sur le serveur MCP (corps limité à 4 Kio). L'enrôlement ne crée jamais de jeton et ne donne aucun accès : un projet demandé reste en attente jusqu'à ce qu'un utilisateur portantprojects.createle valide sur le contrôleur, ce qui crée son groupe de projet sans aucun membre ; seul un administrateur y ajoute un utilisateur, et le développeur crée un jeton MCP personnel dans l'interface web, après s'être connecté. La réponse est la même que le projet existe ou non. Les enrôlements sont bornés (20 projets en attente, 5 par adresse cliente et par heure,429au-delà), une demande que personne n'a validée sousSECREVIEW_PROJECT_ENROLLMENT_TTL_DAYSest supprimée, et le texte libre du demandeur est masqué et tronqué : vérifiez le demandeur et l'adresse avant de valider. Chaque enrôlement est audité. Voir l'enrôlement. - Appels réseau sortants : le serveur MCP n'en fait aucun. Les commandes
security-review-mcp enrolletscan-depscontactent, à la demande de l'opérateur, l'URL qu'il indique (scan-depsenvoie les fichiers de dépendances donnés sur sa ligne de commande, jamais via un lien symbolique) ; elles refusent HTTP en clair hors localhost (sauf--allow-http), les identifiants dans l'URL et les redirections. Le contrôleur ne télécharge les avis OSV que si un administrateur l'active (SECREVIEW_OSV_AUTO_UPDATEouadvisories update) : uniquement depuisSECREVIEW_OSV_URLen HTTPS avec vérification du certificat, redirections limitées à HTTPS, taille bornée (SECREVIEW_OSV_MAX_DOWNLOAD_MB), proxy facultatif dont les identifiants ne sont jamais journalisés. Le bouton Mettre à jour maintenant de l'interface web (permissionsettings.manage) n'est accepté que siSECREVIEW_OSV_AUTO_UPDATE=true. Le proxy sortant (HTTPS_PROXY/NO_PROXY, ou l'écran Proxy sortant) sert au téléchargement OSV et au fournisseur OIDC ; seul le nom des réglages modifiés est journalisé. Un proxy défini dans l'interface web est enregistré en base avec ses identifiants, en clair : l'API ne les renvoie jamais (mot de passe remplacé par***) et les messages d'erreur de l'historique en sont expurgés, mais préférezHTTPS_PROXY(secret Kubernetes) si la base n'est pas protégée comme un secret. Voir la mise à jour automatique. Avec la connexion OIDC (SECREVIEW_OIDC_ENABLED, désactivée par défaut), le contrôleur contacte aussi le fournisseur d'identité configuré : HTTPS uniquement (HTTP seulement sur localhost), redirections limitées à HTTPS, réponses bornées à 1 Mio, délai de 10 secondes. Cette configuration peut aussi être modifiée dans l'interface par un administrateur, avec les mêmes contrôles (refus d'une configuration invalide) ; le bouton Tester la connexion contacte l'émetteur saisi, sous les mêmes conditions. Le secret client saisi dans l'interface est enregistré en base en clair : l'API ne le renvoie jamais (***) et les journaux ne citent que le nom des réglages modifiés ; préférezSECREVIEW_OIDC_CLIENT_SECRET(secret Kubernetes) si la base n'est pas protégée comme un secret. AvecSECREVIEW_UPDATE_CHECK_ENABLED=true(désactivé par défaut), la page À propos demande àapi.github.comla dernière version publiée du dépôt : HTTPS uniquement, par le proxy sortant, redirections limitées au HTTPS, réponse bornée à 512 Kio, délai de 10 secondes, au plus une requête par processus du contrôleur toutes les 6 heures (15 minutes après un échec) ; seuls la version et le lien de la version sont conservés (un lien hors des versions du dépôt est remplacé), et la page ne reçoit qu'un code d'erreur court. Une fois un connecteur activé dans l'écran Connecteurs (tous désactivés par défaut, tous verrouillés parSECREVIEW_TICKETING_ENABLED=false), le contrôleur crée des tickets dans les Jira et ServiceNow configurés : HTTPS uniquement, par le proxy sortant, aucune redirection, réponse bornée à 1 Mio, délai de 15 secondes ; les textes envoyés sont de nouveau masqués et, dans Jira, jamais interprétés comme balisage wiki ; les secrets saisis dans l'écran Connecteurs sont stockés en clair, jamais renvoyés (***) et liés à l'hôte de leur URL ; un connecteur tiers n'est chargé que siSECREVIEW_TICKETING_PLUGINSle nomme et ne joint que son URL configurée par le client du contrôleur, voir ticketing. AvecSECREVIEW_AUDIT_SYSLOG_ADDRESSou un serveur défini sur la page Journal d'audit (désactivé par défaut ; permissionsettings.manage,udp://outcp://uniquement, modification inscrite au journal d'audit), le contrôleur envoie les événements d'audit à ce serveur syslog (UDP, TCP ou socket Unix, ni TLS ni proxy : passez par un relais local pour le TLS) ; les événements ne contiennent aucun secret (détails masqués), et le serveur MCP ne se connecte toujours nulle part. - Administrateur initial : créé par le contrôleur à son premier démarrage avec un mot de
passe aléatoire que personne ne connaît, jamais journalisé ni passé par une variable ; le seul
moyen d'obtenir un mot de passe est
security-review-controller user reset-password, lancé par un opérateur, qui l'affiche une fois. AvecSECREVIEW_OIDC_ONLY=trueet le OIDC activé, la connexion par mot de passe des comptes locaux est refusée (403) ; une configuration OIDC désactivée ou invalide ne bloque jamais les comptes locaux. - Interface web (contrôleur) : l'API REST exige toujours une session ou un jeton
adminouapi; sans utilisateur ni jeton, elle répond503au lieu d'accorder les droits anonymes. Seuls une session ou un jetonapiqui ont la permissionfindings.triage, ou un jetonadmin, changent le statut d'un finding (action journalisée avec l'utilisateur ou le label du jeton), et seule une personne connectée à l'interface (session) accepte un risque ou marque un faux positif : un jeton Bearer est refusé (403), mêmeadmin. Ces deux décisions exigent un motif et une date d'expiration (au plusSECREVIEW_MAX_DISMISSAL_DAYSjours) ; l'approbateur est l'utilisateur connecté, et le finding est de nouveau compté comme ouvert une fois la date passée. Seule une acceptation peut être sans date d'expiration, sauf siSECREVIEW_PERMANENT_ACCEPTANCE_ENABLED=false: réglez-le ainsi pour imposer une revue périodique de chaque risque accepté. Un risque moins grave queSECREVIEW_RISK_ACCEPTANCE_MIN_SEVERITY(mediumpar défaut) est accepté sans motif ni date, toujours par une personne connectée, enregistrée comme approbateur ; laissez la variable vide pour les exiger à chaque acceptation. Les commentaires d'un finding suivent la même règle (session avecfindings.triage) et sont en ajout seul : aucune route ne les modifie ni ne les supprime. De même pour relever la criticité d'un finding : avec une justification, journalisé, jamais sous la criticité du rapport (baisser un finding passe par une acceptation de risque). - Les agents ne ferment jamais un finding : le serveur MCP n'offre que
propose_finding_status, qui enregistre une proposition (motif masqué et borné, auteur tiré du jeton) sans changer le statut ni la gate. Le code relu est non fiable : une injection de prompt ne peut pas neutraliser un finding. Pages servies avec une CSP stricte et les en-têtes anti-clickjacking, et chaque réponse du contrôleur (API, refus compris) porteX-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENY, une CSP fermée quand la route n'en définit pas et, en HTTPS (TLS du contrôleur, ouX-Forwarded-Protod'un proxy listé dansSECREVIEW_FORWARDED_ALLOW_IPS),Strict-Transport-Security: max-age=31536000; jeton gardé dans lesessionStoragede l'onglet. La documentation Swagger et son schéma OpenAPI sont désactivés par défaut (SECREVIEW_SWAGGER_ENABLED) ; une fois activés, ils sont publics, servis depuis des fichiers locaux uniquement avec une CSP stricte, et les routes de données restent protégées. Le contrôle est fait deux fois : par le middleware Bearer, puis par chaque route de l'API, qui refuse (401) toute requête sans identité authentifiée. Ainsi, l'API ne donne jamais de droits par défaut, même si elle est servie sans le middleware. Chaque réponse JSON passe par son modèle de réponse déclaré : un champ qu'un service ajouterait par erreur (une empreinte, un secret) est retiré au lieu d'être renvoyé. Les réponses qui contiennent un secret (session, jeton créé ou renouvelé, mot de passe généré) sont envoyées avecCache-Control: no-store. - Comptes utilisateurs (identifiant et mot de passe) :
seule une empreinte scrypt salée (N=2¹⁴, r=8, p=5, sel aléatoire de 16 octets) est
stockée ; une empreinte stockée aux paramètres hors bornes est refusée au lieu d'épuiser la
mémoire. Mots de passe de 12 à 256 caractères, avec une
minuscule, une majuscule, un chiffre et un caractère qui n'est ni l'un ni l'autre (contrôlés par
le contrôleur, mots de passe générés compris) ; un mot de passe défini par quelqu'un d'autre
(premier administrateur, création ou réinitialisation par un administrateur) doit être changé
à la première connexion : jusque-là, la session n'atteint que Mon compte (
403ailleurs), identifiants limités à[A-Za-z0-9._@-]. La vérification coûte le même temps que l'identifiant existe ou non (empreinte factice), et l'échec renvoie toujours le même401. Les échecs sont limités en base, partagés par toutes les réplicas (5 par identifiant et adresse, 20 par adresse, sur 15 minutes, puis429), comme les requêtes d'une adresse vers l'API REST (SECREVIEW_RATE_LIMIT_*, comptées avant le contrôle du jeton, pour limiter aussi les essais de jetons ; les compteurs enregistrent des empreintes SHA-256, jamais les adresses) ; derrière un reverse proxy, l'adresse du client n'est lue dansX-Forwarded-Forque pour les proxies listés dansSECREVIEW_FORWARDED_ALLOW_IPS(*laisse tout client choisir son adresse et contourner les limites). Seuls les identifiants valides sont journalisés, jamais les mots de passe. La session est un jeton signé avecSECREVIEW_SECRET_KEY, comme celle du OIDC, mais le compte est relu en base à chaque requête : suppression, nouveau rôle ou nouveaux projets s'appliquent immédiatement, et un changement de mot de passe tire une nouvelle version de session aléatoire qui invalide toutes les sessions précédentes. La session doit aussi porter l'identifiant du compte : elle n'ouvre jamais un compte recréé sous le même numéro interne. Le dernier administrateur, local ou OIDC (par son rôle ou celui de ses groupes), ne peut être ni supprimé ni rétrogradé. Le mot de passe du compteadmininitial apparaît une fois dans les journaux du contrôleur : protégez-les et changez-le dès la première connexion. - Connexion OIDC (OpenID Connect), facultative : flux authorization code avec PKCE
S256, state lié au navigateur par un cookie signéHttpOnly/SameSite=Laxde 10 minutes, nonce, ID token vérifié (signature par les clés JWKS du fournisseur, algorithmes asymétriques uniquement, jamaisnoneni HMAC ; émetteur, audience,azp, expiration). Les permissions viennent des groupes du compte : ceux donnés par un administrateur et les groupes ouverts au OIDC dont le nom est exactement une valeur du claim des groupes (les autres valeurs sont ignorées, un groupe non ouvert au OIDC n'est jamais rejoint par le claim) ; une session OIDC n'ouvre jamais/mcp. Le compte est identifié par l'émetteur et lesubde sa première connexion (uniques en base), jamais par un nom : un nom changé chez le fournisseur garde le compte. Il est nommé à sa création par l'identifiant exact du claimSECREVIEW_OIDC_USERNAME_CLAIM(sans repli suremailousub, valeur refusée si ce n'est pas un identifiant valide), qui lie aussi un compte créé à l'avance par un administrateur : choisissez un claim que l'utilisateur ne peut pas modifier chez le fournisseur. Par défaut (SECREVIEW_OIDC_AUTO_CREATE_USERS=false), seul un compte OIDC créé par un administrateur peut se connecter. Une connexion OIDC n'ouvre jamais un compte local du même nom, et le compte est lié à l'émetteur et ausubde sa première connexion : un autresubou un autre émetteur est refusé. Un compte OIDC supprimé est révoqué, par son nom et par son émetteur et sonsub: la création automatique ne le recrée pas, tant qu'un administrateur ne l'a pas recréé ou autorisé à nouveau. Vérifiez le demandeur d'un enrôlement (texte libre, non vérifié) avant de lui donner la lecture du projet. Une session restreinte à des projets l'est comme un jeton--project. La session est un jeton signé (HMAC-SHA256, clé dérivée deSECREVIEW_SECRET_KEY) qui désigne le compte, relu à chaque requête : la suppression du compte la coupe aussitôt. Elle est acceptée par le contrôleur seulement, jamais par le serveur MCP. L'URL de retour après connexion est limitée aux chemins locaux. Codes, jetons et secrets ne sont jamais journalisés par le contrôleur ; le code d'autorisation apparaît toutefois dans les journaux d'accès d'un reverse proxy ou d'uvicorn, ce qui est sans danger : il est à usage unique et inutilisable sans le vérificateur PKCE du cookie. Les réglages secrets (SECREVIEW_SECRET_KEY,SECREVIEW_OIDC_CLIENT_SECRET,SECREVIEW_TLS_KEYFILE_PASSWORD,SECREVIEW_DATABASE_URL) sont typés comme secrets : ils n'apparaissent jamais dans un affichage des réglages. SansSECREVIEW_SECRET_KEY, le contrôleur tire un secret de session aléatoire à chaque démarrage, gardé en mémoire seulement (un avertissement est journalisé) : les sessions se terminent au redémarrage. - Pas d'accès disque côté MCP :
scan_codetravaille uniquement sur le contenu fourni ; le serveur ne lit aucun fichier du disque, ce qui évite les traversées de chemin. Pour le scan du dépôt complet, le client de pipeline (security-review-ci scan-code), lancé par l'opérateur sur un dossier qu'il choisit, lit les fichiers sans suivre les liens symboliques et envoie leur contenu au contrôleur, qui le scanne sans lire son propre disque. - Scans de pipeline :
POST /api/v1/ci/scan/codeet/scan/depsdemandent un jetonciouadmin(une session web est refusée) et restent dans les projets du jeton ; ils n'enregistrent un rapport qu'avecsave. Le corps de la requête est borné (SECREVIEW_MAX_SCAN_REQUEST_MB,413au-delà, lu après la vérification du jeton), comme le nombre de fichiers (SECREVIEW_MAX_SCAN_FILES) et leur taille ; les secrets sont masqués dans les extraits et dans le rapport enregistré. - Corps des requêtes du contrôleur : chaque corps est borné avant d'être analysé,
413au-delà : 512 Kio pour les routes JSON, publiques comprises (connexion, SSO), 8 Mio pour les imports de packs etSECREVIEW_MAX_SCAN_REQUEST_MBpour les scans de pipeline. LeContent-Lengthdéclaré est vérifié d'abord, un corps fragmenté (chunked) au fil de la lecture. - Corps des requêtes du serveur MCP : un corps sur
/mcpn'est lu qu'après la vérification du jeton et il est borné (413au-delà) :SECREVIEW_MAX_MCP_REQUEST_MBpour un client authentifié, 64 Kio pour un client sans jeton servi par le serveur d'enrôlement. Un jeton refusé reçoit son401avant que son corps soit lu. - Base de données : les requêtes SQL passent par l'ORM (paramètres liés) ; les tailles d'entrée, le nombre de findings et la pagination sont bornés. Chaque nom de projet est normalisé avant usage : seuls des lettres, des chiffres et des tirets simples atteignent la base, les journaux, les libellés des métriques et les noms de fichiers. Le schéma est migré par Alembic ; le serveur MCP HTTP ne le modifie jamais et refuse de servir une base qui n'est pas à la révision attendue.
- Règles de scan : les règles par défaut (
default_rules.toml) sont copiées en base au premier lancement. Si la base est inaccessible ou n'a jamais été initialisée, le serveur applique directement ce fichier : le scan ne tourne jamais sans règles. Une règle par défaut peut être modifiée ou désactivée par un administrateur, mais pas supprimée (voir Règles internes). - Normes de développement : leur contenu est transmis tel quel à l'assistant IA comme
consignes ; seuls les administrateurs du contrôleur les écrivent (champs validés et bornés,
200 normes de 50 000 caractères au plus). Le serveur MCP les lit seulement, et la tâche
fournie au prompt
development-standardsest encadrée pour ne pas pouvoir en sortir. Le prompt rappelle qu'une norme ne prime jamais sur la sécurité (voir Normes de développement). - Journaux sur stderr (stdout est réservé au protocole en stdio).
Protégez la base
Les rapports contiennent des informations sensibles sur vos vulnérabilités : protégez la base (droits fichier, compte PostgreSQL dédié, sauvegardes chiffrées).
Limites connues¶
- Le scan par motifs produit des pistes et des faux positifs ; il ne remplace ni un SAST, ni une analyse SCA complète (pas de résolution transitive des manifestes, base à tenir à jour), ni une relecture humaine.
- Les règles sont des motifs ligne par ligne : elles ne voient ni le flux de données ni les contrôles absents (autorisation manquante, par exemple). Ce travail reste celui de l'agent guidé par les checklists, et de vos outils SAST/SCA.
- La couverture d'une revue (résultats de
checklist, règles des pistes du scan) est déclarée par l'agent : le serveur ne garde que les
identifiants qu'il connaît et marque
not_coveredles points sautés, il ne peut pas vérifier que l'agent a réellement lu le code. - L'
authord'un rapport (nom d'utilisateur du développeur) est déclaré par le client : quiconque détient un jeton du projet peut nommer quelqu'un d'autre. Seulsubmitted_by, le libellé du jeton qui a enregistré le rapport, est fixé par le serveur (voir auteur d'un rapport). Unauthorest souvent une adresse e-mail, donc une donnée personnelle : elle est visible de tous ceux qui atteignent le projet et conservée aussi longtemps que le rapport.