Aller au contenu

Serveur Controller

Le serveur Controller (security-review-controller) est le processus des personnes et des pipelines : il sert l'interface web, l'API REST et les routes de pipeline, gère le schéma de la base et exécute les tâches de fond. Il n'appelle jamais le serveur MCP : les deux partagent la base (voir Architecture).

Ce qu'il sert

Route Utilisée par Identifiant
/ui/ les personnes (navigateur) : interface web aucun pour les fichiers statiques ; les données viennent de l'API
/api/v1/auth/ la page de connexion : mot de passe et connexion OIDC aucun
/api/v1/ l'interface web, les scripts, Swagger session web, jeton admin ou api
/api/v1/ci/ security-review-ci : gate, SARIF, tendance, scans du code et des dépendances jeton ci (personnel ou non)
/metrics Prometheus (métriques) jeton metrics ou admin
/api/docs Swagger : documentation OpenAPI de l'API REST, sans données, seulement avec SECREVIEW_SWAGGER_ENABLED=true aucun (les appels demandent un jeton API)
/api/ci-client/ uv : téléchargement de security-review-ci, sauf avec SECREVIEW_CI_CLIENT_DOWNLOAD_ENABLED=false aucun
/api/health l'interface web, qui vérifie la disponibilité de l'API au chargement (ne révèle ni la version ni les réglages) aucun
/api/favicon.svg icône de l'interface web et de Swagger aucun
/.well-known/oauth-authorization-server, /authorize, /token, /register, /revoke les hôtes MCP : connexion OAuth, seulement avec SECREVIEW_MCP_OAUTH_ENABLED=true aucun (PKCE ; l'autorisation passe par une session web)
/healthz sondes (HEALTHCHECK Docker, Kubernetes) aucun

Les rôles des jetons sont décrits dans Rôles, leur gestion dans Jetons Bearer.

Démarrage

security-review-controller serve (port 8000 dans l'image, --host et --port sinon) :

  1. migre le schéma (voir Migrations du schéma) ;
  2. vérifie les réglages (TLS, OIDC, OSV, syslog, connecteurs) : une valeur invalide l'arrête avec le code 2 ;
  3. crée le premier administrateur tant qu'aucun compte n'existe ;
  4. tant qu'OpenGrep est activé et que son exécutable est présent (image -opengrep), installe ou met à jour les règles OpenGrep par défaut (un échec est journalisé et ne l'arrête pas) ;
  5. démarre les tâches de fond, puis sert les routes.

Migrations du schéma

Le serveur Controller migre le schéma à son démarrage ; le serveur MCP HTTP ne le fait jamais. Commandes, verrou entre réplicas et tâche de migration dédiée : voir Base de données.

Tâches de fond

Tâche Activée par Détails
Mise à jour quotidienne des avis OSV SECREVIEW_OSV_AUTO_UPDATE=true (désactivée par défaut) Mise à jour automatique par le serveur Controller
Purge des rapports sans finding et du journal d'audit SECREVIEW_EMPTY_REPORT_RETENTION_DAYS (90), SECREVIEW_AUDIT_RETENTION_DAYS (365), SECREVIEW_AUDIT_MAX_EVENTS Conservation, conservation de l'audit
Envoi du journal d'audit vers syslog un serveur syslog défini dans l'environnement ou dans l'interface Envoi vers syslog
Envoi automatique des findings par les connecteurs un connecteur activé, un seuil de sévérité sur le projet ; SECREVIEW_CONNECTORS_ENABLED=false verrouille tout Connecteurs

Le serveur MCP n'en exécute aucune.

Connexions sortantes

Le serveur Controller est le seul processus qui peut se connecter à l'extérieur, et chaque connexion est désactivée par défaut : le téléchargement OSV, le fournisseur OIDC, la recherche d'une nouvelle version sur la page À propos (SECREVIEW_UPDATE_CHECK_ENABLED), le serveur syslog et les connecteurs. Elles passent en HTTPS (syslog : UDP, TCP ou TLS), par le proxy sortant (HTTPS_PROXY, NO_PROXY, ou la page Paramètres). Voir Sécurité et limites.

Exposition

Exposez le serveur Controller avec TLS (intégré ou reverse proxy) aux personnes (interface web, jetons personnels) et aux runners des pipelines. Derrière un reverse proxy ou un Ingress :

  • réglez SECREVIEW_FORWARDED_ALLOW_IPS sur l'adresse du proxy, sinon tous les clients partagent son adresse pour la limitation des connexions et la limite de débit ;
  • réglez SECREVIEW_PUBLIC_URL sur l'URL publique racine du serveur Controller, sans /ui (ou renseignez la page Général) : le serveur Controller la recopie en base au démarrage et le serveur MCP l'y lit, pour qu'un agent sans jeton valide sache où en créer un. Tant qu'elle est inconnue, un bandeau en haut de l'interface web le signale aux administrateurs (permission settings.manage) et renvoie au champ.

Plusieurs réplicas

Le serveur Controller peut tourner en plusieurs réplicas sur la même base PostgreSQL : les migrations s'attendent (verrou consultatif), les sessions sont partagées grâce à SECREVIEW_SECRET_KEY, les compteurs de connexion et de limite de débit sont tenus en base, un seul réplica à la fois télécharge les avis OSV, et un lot syslog n'est envoyé qu'une fois. Donnez le même SECREVIEW_SECRET_KEY à tous les réplicas.

Commandes d'administration

Les autres commandes de security-review-controller s'exécutent là où tourne le serveur Controller (même SECREVIEW_DATABASE_URL), par exemple docker compose exec controller … ou kubectl exec deploy/secreview-controller -c controller -- … :

Commande Rôle
db migrer le schéma, afficher sa révision, purger des rapports
token créer, lister, renouveler et révoquer les jetons (Jetons Bearer)
user, role comptes de l'interface web et leurs rôles, rôles et correspondances OIDC (Gérer les comptes)
project valider ou refuser les projets demandés par les agents MCP des utilisateurs (enrôlement)
rules importer et exporter les règles internes et les checklists
advisories importer ou télécharger la base OSV
audit lire ou purger le journal d'audit

Toutes leurs options : Ligne de commande. Les variables du serveur Controller : Configuration.

Gérer les comptes

Les comptes utilisateurs se gèrent dans la page Utilisateurs, par l'API REST /api/v1/users (permission users.manage) ou par la CLI. Un identifiant contient des lettres, des chiffres et . _ @ -, 64 caractères au plus ; le type d'un compte est son auth_source (local ou oidc).

security-review-controller user create alice --project shop=reports.read,findings.triage
# {"id": 2, "username": "alice", "auth_source": "local", "roles": [], "all_projects_permissions": [], "projects": [{"project": "shop", "permissions": ["findings.triage", "reports.read"]}], "oidc": [], "permissions": {…}, …, "password": "q3V…"}
printf '%s\n' "$PASSWORD" | security-review-controller user create bob --password-stdin
security-review-controller user create carol@example.com --sso   # correspondances OIDC uniquement
security-review-controller user list
security-review-controller user set-access alice --role Auditor --project shop --project crm=reports.read,findings.triage
security-review-controller user set-access alice --role admin --all-projects   # restaure un administrateur
security-review-controller user reset-password alice   # compte local : nouveau mot de passe, sessions fermées
security-review-controller user delete alice
security-review-controller role list       # rôles et leurs permissions
security-review-controller role mappings   # correspondances OIDC

Par l'API, un compte OIDC se crée avec "auth_source": "oidc", sans password ; role_ids, all_projects_permissions et projects donnent ses rôles, ses droits par projet sur tous les projets et ses projets avec leurs droits :

curl -X POST https://secreview.example.com/api/v1/users \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"username": "carol@example.com", "auth_source": "oidc", "projects": [{"project": "shop"}]}'

Un compte OIDC est aussi créé à sa première connexion avec SECREVIEW_OIDC_AUTO_CREATE_USERS=true (comptes OIDC).

Pour un compte local, sans --password-stdin (ou sans mot de passe dans l'interface), un mot de passe aléatoire de 24 caractères est généré et affiché une seule fois. Un mot de passe choisi respecte la politique de mot de passe : 12 à 256 caractères, avec au moins une minuscule, une majuscule, un chiffre et un caractère qui n'est ni une lettre ni un chiffre. La politique s'applique aux nouveaux mots de passe : un mot de passe défini avant reste valable jusqu'à son changement. Un compte OIDC n'a jamais de mot de passe : la connexion par identifiant et mot de passe, la réinitialisation et Mon compte le refusent.

Authentification requise pour la Web UI

L'API de l'interface refuse les requêtes avec le statut 503 tant qu'aucun utilisateur ni jeton n'existe ; la mire de connexion reste affichée et indique que l'authentification doit être configurée. security-review-controller serve crée le premier administrateur à son premier démarrage : cela n'arrive donc qu'avant ; un compte peut aussi être créé avec security-review-controller user create. Cette restriction concerne la Web UI ; l'authentification MCP reste indépendante (un compte utilisateur n'ouvre jamais /mcp). Avec la connexion OIDC, l'API demande une connexion (401) même sans utilisateur ni jeton en base.

Les variables de l'interface sont dans la Configuration.

Premier administrateur

À son premier démarrage, tant qu'aucun compte n'existe, le serveur Controller crée le compte local admin (SECREVIEW_ADMIN_USERNAME), avec le rôle admin et la lecture et le triage de tous les projets, avec un mot de passe aléatoire que personne ne connaît et qui n'est jamais écrit dans les journaux. Le seul moyen d'obtenir un mot de passe est la CLI, qui en définit un nouveau et l'affiche une fois :

security-review-controller user reset-password admin
# {"id": 1, "username": "admin", …, "must_change_password": true, "password": "q3V…"}
WARNING security_review_mcp.controller: Initial Web UI administrator 'admin' created: set its password with `security-review-controller user reset-password admin`

Traitez ce mot de passe comme un secret et connectez-vous avec admin. L'interface exige alors un nouveau mot de passe avant toute autre chose : elle ouvre Mon compte, et l'API refuse toutes les autres routes (403) tant que le mot de passe n'est pas changé. S'il est perdu, voir mot de passe perdu.

Il en va de même de tout mot de passe défini par quelqu'un d'autre : un compte local créé ou réinitialisé par un administrateur (page ou CLI) change son mot de passe à sa première connexion. La page Utilisateurs affiche mot de passe à changer jusque-là.

Mot de passe perdu

Le mot de passe est stocké sous forme d'empreinte scrypt : il est impossible de le relire. Remplacez-le avec la CLI du serveur Controller, sur la même base :

security-review-controller user reset-password admin

La commande affiche le compte en JSON, avec un mot de passe généré dans le champ password (| jq -r .password pour ne garder que lui). L'ancien mot de passe est aussitôt invalidé, les sessions ouvertes du compte prennent fin et le nouveau mot de passe doit être changé à la prochaine connexion. Pour le choisir, donnez-le sur la première ligne de l'entrée standard (il n'apparaît alors ni dans l'historique du shell ni dans la liste des processus, et le JSON n'a pas de champ password) :

printf '%s\n' "$NEW_PASSWORD" | security-review-controller user reset-password admin --password-stdin

Lancez-la là où tourne le serveur Controller :

# Docker Compose
docker compose exec controller security-review-controller user reset-password admin
# Kubernetes
kubectl -n secreview exec deploy/secreview-controller -c controller -- \
    security-review-controller user reset-password admin

Un compte OIDC n'a pas de mot de passe (code de sortie 2). S'il ne reste aucun administrateur, créez-en un avec security-review-controller user create NAME --role admin --all-projects (mot de passe généré, affiché de la même façon) : le compte admin n'est créé au démarrage que tant qu'aucun compte n'existe.

Connexion et sessions

Quand le serveur Controller n'accepte plus la session pendant la navigation (session expirée, mot de passe changé dans un autre onglet, compte supprimé), une fenêtre Session expirée s'ouvre : Se reconnecter ouvre la page de connexion puis revient à la même page.

La page de connexion propose l'identifiant et le mot de passe d'un compte local et, avec la connexion OIDC, le bouton du fournisseur d'identité ; elle n'accepte pas de jeton. POST /api/v1/auth/login vérifie le mot de passe d'un compte local et renvoie un jeton de session signé, valable SECREVIEW_SESSION_HOURS heures (8 par défaut), que l'onglet utilise comme un jeton Bearer :

curl -s -X POST https://secreview.example.com/api/v1/auth/login \
  -H 'Content-Type: application/json' -d '{"username": "alice", "password": "…"}'
# {"session": "eyJ…", "expires_in": 28800, "username": "alice"}

Chaque session désigne un compte (local ou OIDC) ; à chaque requête, le serveur Controller le relit en base :

  • un changement de ses rôles ou projets, d'un rôle ou des correspondances OIDC s'applique immédiatement aux sessions ouvertes ;
  • une suppression ou un nouveau mot de passe (changé par l'utilisateur ou réinitialisé par un administrateur) ferme toutes les sessions du compte ; PUT /api/v1/me/password remet une nouvelle session à l'onglet qui a changé le mot de passe ;
  • une déconnexion (POST /api/v1/me/logout) ferme aussi toutes les sessions du compte, dans tous les onglets et navigateurs : un jeton de session volé cesse de fonctionner quand son utilisateur se déconnecte.

Les sessions sont signées avec SECREVIEW_SECRET_KEY (32 caractères au moins), partagé par toutes les répliques du serveur Controller. Sans lui, un secret aléatoire est tiré à chaque démarrage (un avertissement est journalisé) : les sessions se terminent au redémarrage du serveur Controller et ne sont pas partagées entre répliques. Définissez-le en production (en Kubernetes, depuis un Secret : voir Kubernetes).

Les échecs de connexion sont limités : après 5 échecs en 15 minutes pour un identifiant depuis une même adresse, ou 20 échecs depuis une adresse tous identifiants confondus, la connexion est refusée (429, en-tête Retry-After), même avec le bon mot de passe. Les compteurs sont tenus en base : tous les workers et réplicas du serveur Controller les partagent. Chaque adresse est aussi limitée sur toute l'API REST (par défaut 600 requêtes par minute, 30 sur les routes de connexion, voir SECREVIEW_RATE_LIMIT_*). Derrière un reverse proxy, l'adresse vue est celle du proxy, et la limite s'appliquerait alors à tous les clients du proxy : déclarez le proxy dans SECREVIEW_FORWARDED_ALLOW_IPS pour que l'adresse du client soit lue dans X-Forwarded-For. Chaque connexion et chaque échec sont journalisés, sans le mot de passe :

INFO security_review_mcp.web: Web login of alice (role triager) from 10.0.0.12
WARNING security_review_mcp.web: Failed web login of alice from 10.0.0.12

Réglages enregistrés en base

Les pages Général (langue des rapports, adresse du serveur Controller, proxy sortant) et Connexions (OIDC) de l'interface web enregistrent des réglages en base, partagés par le serveur MCP et le serveur Controller (permission settings.manage ; les anciennes adresses /ui/settings, /ui/settings/language et /ui/settings/proxy ouvrent Général). Une variable d'environnement définie (environnement ou fichier .env, non vide) est toujours prioritaire : son champ est alors verrouillé et affiche la variable, et une valeur enregistrée avant que la variable soit définie est signalée comme ignorée, avec un bouton Supprimer. Un changement s'applique sans redémarrage : le serveur MCP lit l'adresse du serveur Controller en moins d'une minute (le changement est enregistré dans le journal d'audit, controller_url.changed), et le proxy s'applique au prochain téléchargement OSV et à la prochaine connexion OIDC. Utiliser l'adresse actuelle remplit l'adresse de ce serveur Controller.

Route Effet
GET /api/v1/settings Pour chaque réglage : la valeur enregistrée value, la variable env, env_set (la variable est définie et prioritaire) et la valeur effective
PUT /api/v1/settings/controller-url value (null la supprime)
PUT /api/v1/settings/proxy url et no_proxy : null supprime la valeur enregistrée, "url": "" force la connexion directe, un champ absent est inchangé. Les mots de passe ne sont jamais réaffichés (***) : renvoyer *** conserve celui enregistré

Une valeur dont la variable est définie est refusée avec 422. Les réglages OIDC ont leurs propres routes (configuration OIDC). Le thème choisi par un utilisateur connecté est conservé dans son compte (GET et PUT /api/v1/me/preferences), pour tous les navigateurs qu'il utilise.

Sécurité de l'interface web

  • Les fichiers statiques de /ui/ sont publics (ils ne contiennent aucune donnée) ; l'API /api/v1/ exige une session ou un jeton admin ou api (les routes de pipeline /api/v1/ci/ prennent les jetons ci, voir Ce qu'il sert). Seules les routes de connexion (/api/v1/auth/…) sont publiques, ainsi que le téléchargement du client de pipeline (/api/ci-client/) sauf s'il est désactivé.
  • Les mots de passe des comptes ne sont jamais stockés : seule une empreinte scrypt salée (N=2¹⁴, r=8, p=5) est enregistrée et aucune réponse de l'API ne la renvoie. La vérification prend le même temps que l'identifiant existe ou non, et la réponse d'échec est identique. La documentation Swagger (/api/docs) et son schéma (/api/openapi.json) sont désactivés par défaut (SECREVIEW_SWAGGER_ENABLED=true les sert) ; une fois activés, ils sont publics et ne donnent accès à aucune donnée. Leurs fichiers sont fournis avec la build de l'interface web : rien n'est chargé depuis Internet, et Swagger n'envoie jamais le schéma au validateur en ligne. Dans Swagger, le cadenas d'une route indique qu'elle exige un jeton : il reste ouvert tant que vous n'avez pas saisi de jeton avec Authorize, et ne veut pas dire que la route est accessible sans jeton. Saisissez-y un jeton API.
  • Le serveur Controller n'expose aucune route d'écriture sans jeton. Un projet est demandé par un agent muni d'un jeton, avec l'outil request_project du serveur MCP (enrôlement) ; le serveur Controller liste et traite les demandes (GET /api/v1/projects/pending, POST /api/v1/projects/pending/{id}/approve, DELETE /api/v1/projects/pending/{id}, permission projects.create).
  • Le jeton est envoyé dans l'en-tête Authorization : pas de cookie, donc pas de CSRF. Seule la connexion OIDC pose un cookie, HttpOnly, SameSite=Lax, Secure en HTTPS, limité à l'URL de retour et valable 10 minutes : il lie le retour du fournisseur au navigateur qui a lancé la connexion (protection contre le login CSRF). Le jeton de session est remis dans le fragment d'URL (#session=...), jamais envoyé à un serveur, et l'interface l'efface aussitôt de la barre d'adresse et de l'historique.
  • Il est conservé dans le sessionStorage de l'onglet et disparaît à sa fermeture.
  • Les pages sont servies avec une CSP stricte (script-src 'self', aucune ressource externe), X-Frame-Options: DENY, X-Content-Type-Options: nosniff et Referrer-Policy: no-referrer.
  • Servez l'interface en HTTPS (TLS intégré ou reverse proxy) dès qu'elle n'écoute plus sur localhost : le jeton circule à chaque requête.

Le paquet publié et l'image Docker contiennent l'application déjà compilée. Si vous installez depuis les sources sans la compiler, le serveur Controller ne sert que l'API REST et un avertissement est journalisé. Voir Contribuer pour la compiler.