tenantleakCloud

Documentation

Tout ce qu'il faut pour passer du premier compte à un scan bloquant dans votre CI.

Démarrer

Un scan a besoin de quatre choses, que la vue d'ensemble de votre espace vous demande dans l'ordre :

  1. Un domaine vérifié : la preuve que vous contrôlez l'hôte de l'API.
  2. Une description de l'API : spec OpenAPI, capture HAR ou endpoint GraphQL.
  3. Deux comptes de test appartenant à deux clients différents (deux tenants), par exemple un utilisateur d'Acme et un utilisateur de Globex sur votre préproduction.
  4. Un premier scan, lancé depuis la page de l'API. Il dure en général de quelques secondes à quelques minutes.

Visez de préférence une préproduction peuplée de données de test : le scan lit des objets réels de chaque compte pour vérifier qu'ils ne sont pas accessibles depuis l'autre.

Vérifier un domaine

Nous ne scannons que les hôtes dont votre organisation a prouvé le contrôle. Ajoutez l'hôte de l'API (par exemple api.exemple.fr) dans la page Domaines : un jeton de la forme tlc-verify-… s'affiche. Publiez-le de l'une des deux façons suivantes, puis cliquez sur « Vérifier ».

Par un enregistrement DNS

Créez un enregistrement TXT sur le sous-domaine _tenantleak de l'hôte :

_tenantleak.api.exemple.fr.  300  IN  TXT  "tlc-verify-…"

La propagation prend de quelques secondes à quelques minutes selon votre hébergeur DNS.

Par un fichier

Servez le jeton, seul, dans ce fichier texte :

https://api.exemple.fr/.well-known/tenantleak-verification.txt

tlc-verify-…

La preuve est revérifiée automatiquement tous les 30 jours avant un scan. Si elle a disparu, le scan échoue avec un message explicite, sans envoyer de requête à l'API.

Si la connexion des comptes de test passe par un autre de vos domaines (par exemple auth.exemple.fr), vérifiez-le aussi. Les fournisseurs d'identité courants (Auth0, Okta, Cognito, Microsoft Entra, Firebase, Supabase…) n'ont pas besoin d'être vérifiés, à condition d'indiquer l'URL https de leur point de jeton (par exemple https://votre-tenant.eu.auth0.com/oauth/token). De même, si la spec OpenAPI est publiée sur un autre hôte que l'API, cet hôte doit être vérifié ; sinon, importez le fichier.

Décrire l'API

À la création de l'API, indiquez son URL de base (par exemple https://api.exemple.fr), puis une source :

URL de la spec OpenAPI
OpenAPI 3 ou Swagger 2, en JSON ou YAML, sur un domaine vérifié (en général celui de l'API). Elle est téléchargée à chaque scan, ce qui suit automatiquement l'évolution de l'API.
Fichier OpenAPI
Le même document envoyé une fois, 10 Mo au plus. Pratique quand la spec n'est pas publiée.
Capture HAR
Sans spec, ouvrez les outils de développement du navigateur (onglet Réseau), parcourez l'application connecté avec un compte, puis « Enregistrer tout au format HAR ». Les endpoints sont reconstruits à partir des requêtes vers l'hôte de l'API.
GraphQL (offres Pro et Business)
Le chemin de l'endpoint (par défaut /graphql). Le schéma est lu par introspection ; si elle est désactivée, envoyez le résultat d'une introspection (JSON) ou le schéma SDL (.graphql).

Comptes de test

Une identité est un compte de test de votre API. Chaque identité porte un nom (par exemple alice_acme) et le client (tenant) auquel elle appartient (par exemple acme). Il en faut au moins deux, de deux clients différents ; ajouter un deuxième compte du même client permet en plus de distinguer ce qui est partagé au sein d'un client.

Les secrets sont chiffrés dès l'enregistrement et ne sont plus jamais réaffichés : pour les changer, supprimez l'identité et recréez-la.

Jeton Bearer
Collez le jeton seul, sans le mot « Bearer ». Il est envoyé dans l'en-tête Authorization. Préférez un jeton de longue durée, ou l'une des connexions ci-dessous si vos jetons expirent vite.
En-tête
Un nom et une valeur, par exemple X-Api-Key.
Cookie
Le nom et la valeur du cookie de session.
Connexion par identifiant et mot de passe
Nous appelons votre endpoint de connexion au début de chaque scan. Indiquez son URL (un chemin comme /api/login, ou une URL complète), le format du corps (JSON ou formulaire), les noms des champs (par défaut email et password) et, si la réponse contient un jeton, son chemin dans le JSON (par exemple access_token ou data.token). Sans chemin, nous cherchons un jeton usuel dans la réponse et réutilisons les cookies de session posés par la connexion.
OAuth 2
URL https du serveur de jetons, client_id et secret, avec le flux client_credentials ou password (identifiant et mot de passe de l'utilisateur de test). Portée (scope) et audience sont optionnelles.

Si votre API choisit le client actif par un en-tête ou un paramètre (voir les options), renseignez pour chaque identité la valeur de tenant qui lui correspond, par exemple acme.

Vérifier une identité

Le bouton Vérifier, à côté de chaque identité, envoie une seule requête à votre API avec ce compte (le premier endpoint de liste de la spec, ou { __typename } en GraphQL), en passant par la même connexion et les mêmes garde-fous qu'un scan. Vous savez en quelques secondes si le jeton est accepté, refusé (401, 403) ou si la connexion échoue, sans attendre un scan ni consommer votre quota. Trente vérifications par heure et par API.

Identifiants d'objets connus

Le scan trouve lui-même les objets de chaque compte en lisant les listes de l'API (/invoices, puis /invoices/{id}) et les références qu'elles contiennent. Un endpoint sans liste, ou dont les objets ne sont référencés nulle part, apparaît dans la couverture du scan comme sans identifiant. Donnez-lui alors des identifiants connus sur chaque identité, un par ligne :

contract_id = c-acme-1
report_id = r-acme-1

Le nom à gauche est celui du paramètre dans la spec ({contract_id}). Indiquez pour chaque compte des objets qui lui appartiennent : le scan tentera de les lire avec les comptes des autres clients.

Options du scan

Exclusions
Une expression régulière par ligne ; les chemins correspondants ne sont pas appelés. Par défaut, les déconnexions et l'authentification sont exclues pour ne pas invalider les sessions de test : logout|signout|/auth/.
Chemins partagés
Les données volontairement communes à tous les clients (catalogue, liste de pays, offres…) : une expression régulière par ligne. Une réponse identique pour deux clients n'y est pas signalée.
Sélecteur de tenant
Si un en-tête (par exemple X-Tenant-Id) ou un paramètre de requête désigne le client actif, indiquez son nom : le scan vérifie que changer sa valeur ne donne pas accès à un autre client.
Accès anonyme
Activé par défaut : chaque endpoint est aussi appelé sans authentification.
Données sensibles
Signale les secrets présents dans les réponses (empreintes de mots de passe, clés d'API, jetons). Ajoutez vos propres noms de champs sensibles, séparés par des virgules.

Lire les résultats

Chaque faille devient un problème, identifié par son type et son endpoint, et suivi d'un scan à l'autre. Sa page donne l'explication, les comptes concernés (qui a atteint les données de qui), la requête de reproduction en curl et la correction recommandée.

Gravité

Critique
Un client lit ou modifie les données d'un autre. À traiter immédiatement : c'est une violation de données potentielle.
Élevée
Un contrôle manque mais l'exploitation est plus limitée (accès anonyme, bascule de tenant, référence vers un objet d'un autre client…).
Moyenne
À vérifier à la main, par exemple un secret présent dans une réponse.

États

Nouveau
Trouvé pour la première fois par ce scan, ou réapparu après avoir été corrigé.
Ouvert
Toujours présent.
Corrigé
Le même test a été rejoué par un scan ultérieur et refusé. Un problème qui n'a simplement pas été retesté (endpoint exclu, scan interrompu) reste ouvert.
Risque accepté
Vous avez décidé de vivre avec, en expliquant pourquoi, éventuellement jusqu'à une date : à cette date, le problème redevient ouvert s'il est toujours présent.
Faux positif
Le comportement est voulu (donnée réellement partagée, par exemple). Pensez aussi aux chemins partagés.

Couverture

Un scan sans résultat ne vaut que par ce qu'il a réellement testé. L'onglet Couverture de chaque scan liste tous les endpoints décrits par la spec et dit, pour chacun, s'il a été testé (avec le nombre de refus et de fuites), s'il n'a pas pu l'être faute d'identifiant d'objet (voir les identifiants connus), ou pourquoi il a été ignoré : exclu par vos règles, méthode d'écriture sans tests d'écriture, déclaré public dans la spec.

Re-tester un problème

Une fois votre correctif déployé, le bouton Retester ce problème rejoue uniquement l'endpoint concerné avec vos comptes de test, en une minute environ. Les identifiants sont toujours collectés sur toute l'API, mais rien d'autre n'est sondé. Les re-tests ne comptent pas dans votre quota mensuel de scans ; ils sont limités par jour selon l'offre (3 en Découverte, 30 en Pro, 100 en Business). Le problème passe en « Corrigé » dès que le test rejoué est refusé.

Quand l'API change

Chaque scan complet compare ses endpoints à ceux du scan précédent et signale en tête de page les nouveaux endpoints (avec leur résultat : protégé, fuite, sans identifiant) et ceux qui ont disparu. La même information est reprise dans l'alerte e-mail et le webhook : un endpoint ajouté lundi par un développeur est testé mardi matin par le scan planifié, et vous le savez.

Créer un ticket

Sur la page d'un problème, « Créer un ticket » prépare un texte en Markdown (titre, endpoint, gravité, observation, impact, correction attendue, commande de reproduction, méthode de vérification) à coller dans Jira, GitHub, GitLab ou Linear.

Rapport

Chaque scan terminé propose un rapport en français, prêt à imprimer ou à enregistrer en PDF depuis le navigateur : verdict, couverture, chaque problème avec son observation, son impact et sa correction, les corrections confirmées, les risques acceptés, la méthode et les limites. C'est le document à transmettre à un client, un auditeur ou votre direction. Le rapport technique du moteur (en anglais), le JSON et le SARIF restent disponibles.

Planification et alertes

Avec les offres Pro et Business, chaque API peut être scannée chaque jour ou chaque semaine (le lundi), à l'heure de Paris de votre choix. Un scan planifié est ignoré si le précédent n'est pas terminé ou si le quota mensuel est atteint.

Quand un scan trouve un nouveau problème, en confirme la correction ou échoue :

  • les membres qui ont laissé les notifications activées dans leur compte reçoivent un e-mail ;
  • un message est posté sur le webhook de l'API, si vous en avez indiqué un (bouton « Envoyer un message de test » pour vérifier).

Webhooks

Deux formats, au choix dans les réglages de l'API :

  • Texte : un message lisible, accepté tel quel par les webhooks entrants de Slack, Microsoft Teams, Mattermost et Rocket.Chat ({"text": …}) et par Discord ({"content": …}).
  • JSON signé : pour votre propre intégration (création de ticket, blocage d'un déploiement, tableau de bord). Chaque événement est un POST JSON avec les en-têtes X-Tenantleak-Event (scan.finished, scan.failed ou test) et X-Tenantleak-Signature: t=<horodatage>,v1=<signature> : HMAC-SHA256, avec la clé de signature affichée aux administrateurs, de la chaîne <horodatage>.<corps brut>. L'horodatage (secondes Unix) est couvert par la signature : refusez les événements de plus de cinq minutes, un événement capturé ne peut alors pas être rejoué. La clé se change en un clic.
{
  "event": "scan.finished",
  "sent_at": "2026-10-21T06:12:40Z",
  "project": {"id": "…", "name": "API facturation (préprod)", "base_url": "https://api.staging.exemple.fr"},
  "scan": {"id": "…", "number": 42, "status": "done", "trigger": "schedule", "requests": 318, "tested": 27,
           "url": "…/app/projects/…/scans/…", "report_url": "…/rapport"},
  "issues": {"new": 1, "still_open": 2, "fixed": 1},
  "endpoints": {"new": [{"key": "GET /api/v2/invoices/{id}", "status": "tested", "leak": true}], "removed": [], "since": 41},
  "findings": [{"id": "…", "state": "new", "kind": "cross_tenant_read", "title": "Lecture des données d'un autre client",
                "severity": "high", "method": "GET", "path": "/api/v2/invoices/{id}", "url": "…"}]
}
import hashlib, hmac, time
parts = dict(p.split("=", 1) for p in request.headers["X-Tenantleak-Signature"].split(","))
assert abs(time.time() - int(parts["t"])) < 300            # pas de rejeu d'un vieil événement
expected = hmac.new(SECRET.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
assert hmac.compare_digest(expected, parts["v1"])

Un scan sans changement n'envoie rien. Le lundi matin, les propriétaires et administrateurs reçoivent un résumé de la semaine (problèmes ouverts, nouveaux, corrigés, couverture, prochain scan), seulement s'il s'est passé quelque chose.

Les rapports détaillés d'un scan (couverture, rapport technique, JSON, SARIF) sont conservés 90 jours ; le résumé et les problèmes restent tant que l'API existe.

Sécurité du compte

La double authentification (application TOTP : Google Authenticator, 1Password, Bitwarden…) s'active depuis « Mon compte ». Huit codes de récupération à usage unique sont affichés une seule fois : rangez-les dans votre gestionnaire de mots de passe, ils remplacent l'application si vous perdez votre téléphone. Un propriétaire peut exiger la double authentification de toute l'équipe depuis la page Équipe. En dernier recours, écrivez-nous depuis votre adresse de compte pour une réinitialisation.

Connexion SSO (OpenID Connect, offre Business)

Un propriétaire relie le fournisseur d'identité de l'entreprise depuis la page Équipe : URL de l'émetteur (celle qui répond à /.well-known/openid-configuration), client_id, client_secret et domaine des adresses e-mail. Chez le fournisseur, déclarez une application web confidentielle avec l'adresse de retour https://tenantleak.redsentinel.fr/login/sso/callback et les portées openid email profile. Fonctionne avec Google Workspace, Microsoft Entra ID, Okta, Keycloak, Authentik et tout fournisseur OpenID Connect ; le flux utilise PKCE et vérifie la signature du jeton d'identité.

Le domaine des adresses doit d'abord être prouvé dans « Domaines », comme un hôte d'API (enregistrement DNS TXT _tenantleak.exemple.fr ou fichier /.well-known/tenantleak-verification.txt) : une adresse e-mail sur un domaine ne prouve pas qu'on l'administre. La preuve est reconfirmée tous les trente jours ; si elle disparaît, le SSO est suspendu jusqu'à nouvelle vérification. Les messageries grand public (gmail.com, outlook.fr…) sont refusées.

Les personnes dont l'adresse est sur ce domaine se connectent par « Se connecter avec le SSO de mon entreprise » : la première connexion crée leur compte et les ajoute comme membre, et le compte reste lié à l'identifiant (sub) que le fournisseur a présenté. Le second facteur est celui du fournisseur ; un compte qui existait déjà avec sa propre double authentification la confirme une fois, lors du premier rattachement. La connexion ne s'achève que dans le navigateur qui l'a commencée. Une fois le test réussi, l'option « Imposer le SSO » refuse la connexion par mot de passe aux membres de l'organisation et l'inscription par mot de passe aux nouvelles adresses du domaine ; elle ne touche pas les comptes du même domaine qui appartiennent à une autre organisation.

Référence de l'API

Disponible avec les offres Pro et Business. Créez un jeton dans Intégration CI : il commence par tlc_, n'est affiché qu'une fois et donne accès aux API de votre organisation seulement. Rangez-le dans les secrets de votre CI, jamais dans le dépôt.

Toutes les requêtes portent l'en-tête Authorization: Bearer tlc_… et renvoient du JSON. Limite : 120 requêtes par minute et par jeton.

RequêteRôle
GET /api/v1/projectsLes API de l'organisation, avec leurs problèmes ouverts et leur dernier scan.
GET /api/v1/projects/{id}Une API.
POST /api/v1/projects/{id}/scansLance un scan (réponse 202). Corps JSON facultatif : {"unsafe": true} pour inclure les tests d'écriture, s'ils sont activés.
GET /api/v1/projects/{id}/scans?limit=20Les derniers scans d'une API.
GET /api/v1/scans/{id}?fail_on=newÉtat du scan, problèmes trouvés et verdict gate.
POST /api/v1/scans/{id}/cancelAnnule un scan en file ou en cours.
GET /api/v1/scans/{id}/sarifLe rapport SARIF 2.1.0.
GET /api/v1/scans/{id}/reportLe rapport JSON complet.
GET /api/v1/projects/{id}/issues?status=openLes problèmes : open, fixed, triaged ou all.
POST /api/v1/projects/{id}/issues/{issue}/retestRejoue uniquement l'endpoint de ce problème (réponse 202, même suivi qu'un scan). Hors quota mensuel. Pour un problème d'écriture, le corps {"unsafe": true} est obligatoire.

Lancer un scan

L'identifiant de l'API figure dans l'adresse de sa page (/app/projects/<id>) et dans la page Intégration CI.

curl -X POST \
  -H "Authorization: Bearer $TLC_TOKEN" \
  "https://tenantleak.redsentinel.fr/api/v1/projects/$TLC_PROJECT/scans"
{
  "id": "6f1c0e9a…",
  "number": 14,
  "status": "queued",
  "trigger": "api",
  "status_url": "https://tenantleak.redsentinel.fr/api/v1/scans/6f1c0e9a…",
  "url": "https://tenantleak.redsentinel.fr/app/projects/…/scans/6f1c0e9a…"
}

Attendre le verdict

Interrogez status_url toutes les 15 secondes environ. Tant que le scan tourne, gate.result vaut pending, puis :

pass
Rien qui doive bloquer selon fail_on.
fail
gate.failing problèmes bloquants.
error
Le scan a échoué ou a été annulé ; la raison est dans gate.reason.

Le paramètre fail_on choisit ce qui fait échouer : new (par défaut : problèmes nouveaux ou réapparus dans ce scan), open (tout problème encore ouvert), critical (problèmes critiques ouverts) ou none (jamais, sauf erreur). Les problèmes en risque accepté ou faux positif ne bloquent jamais.

{
  "id": "6f1c0e9a…",
  "number": 14,
  "status": "done",
  "issues": {"new": 1, "still_open": 2, "fixed": 1},
  "findings": [
    {
      "title": "Lecture des données d'un autre client",
      "severity": "critical",
      "state": "new",
      "method": "GET",
      "path": "/api/invoices/{id}",
      "probes": ["bob_globex → alice_acme"],
      "url": "https://tenantleak.redsentinel.fr/app/projects/…/issues/…"
    }
  ],
  "gate": {"result": "fail", "fail_on": "new", "failing": 1, "partial": false},
  "reports": {"sarif": "https://tenantleak.redsentinel.fr/api/v1/scans/6f1c0e9a…/sarif", "report": "…"}
}

Erreurs

Les erreurs renvoient {"error": "…"} avec le code HTTP correspondant :

401
Jeton absent, mal formé ou révoqué.
403
L'offre de l'organisation n'inclut pas l'API.
404
API ou scan inconnu pour cette organisation.
409
Le scan ne peut pas démarrer : un scan est déjà en cours, quota atteint, ou configuration incomplète. Le message dit lequel.
429
Plus de 120 requêtes en une minute : attendez la durée indiquée par Retry-After.

Intégration continue

Le principe est le même partout : lancer le scan, interroger son état jusqu'à obtenir un verdict, puis faire échouer le job si le verdict est fail ou error.

GitHub Actions

Le plus court : l'action publiée avec le moteur open source. Ajoutez le secret TLC_TOKEN au dépôt ; le job échoue sur une nouvelle faille et le rapport SARIF apparaît dans l'onglet Security.

- uses: Chechene953/tenantleak/cloud-action@v0
  with:
    token: ${{ secrets.TLC_TOKEN }}
    project: ID_DE_L_API            # page « Intégration CI »
    fail-on: new                    # new, open, critical ou none
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: tenantleak.sarif

Ou, sans action, le même enchaînement écrit à la main :

# .github/workflows/tenantleak.yml
name: tenantleak
on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  tenantleak:
    runs-on: ubuntu-latest
    timeout-minutes: 40
    permissions:
      contents: read
      security-events: write
    env:
      TLC_URL: https://tenantleak.redsentinel.fr
      TLC_PROJECT: "identifiant-de-l-api"
      TLC_TOKEN: ${{ secrets.TLC_TOKEN }}
    steps:
      - name: Lancer le scan
        id: start
        run: |
          resp=$(curl -fsS -X POST -H "Authorization: Bearer $TLC_TOKEN" \
            "$TLC_URL/api/v1/projects/$TLC_PROJECT/scans")
          echo "scan=$(echo "$resp" | jq -r .id)" >> "$GITHUB_OUTPUT"
          echo "Suivi : $(echo "$resp" | jq -r .url)"

      - name: Attendre le verdict (30 minutes au plus)
        id: wait
        run: |
          result=pending
          for i in $(seq 1 120); do
            resp=$(curl -fsS -H "Authorization: Bearer $TLC_TOKEN" \
              "$TLC_URL/api/v1/scans/${{ steps.start.outputs.scan }}?fail_on=new")
            result=$(echo "$resp" | jq -r .gate.result)
            [ "$result" != "pending" ] && break
            sleep 15
          done
          echo "$resp" | jq '{status, issues, gate}'
          echo "result=$result" >> "$GITHUB_OUTPUT"

      - name: Télécharger le rapport SARIF
        if: always() && steps.start.outputs.scan != ''
        run: |
          curl -fsS -H "Authorization: Bearer $TLC_TOKEN" -o tenantleak.sarif \
            "$TLC_URL/api/v1/scans/${{ steps.start.outputs.scan }}/sarif" || true

      - name: Publier dans GitHub Security
        if: always() && hashFiles('tenantleak.sarif') != ''
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: tenantleak.sarif
          category: tenantleak

      - name: Verdict
        run: |
          case "${{ steps.wait.outputs.result }}" in
            pass) echo "Aucune nouvelle faille entre clients." ;;
            fail) echo "Nouvelle faille entre clients : voir le scan."; exit 1 ;;
            *)    echo "Scan en échec ou non terminé."; exit 1 ;;
          esac

GitLab CI

Déclarez TLC_TOKEN comme variable CI/CD masquée.

# .gitlab-ci.yml
tenantleak:
  stage: test
  image: alpine:3.20
  variables:
    TLC_URL: "https://tenantleak.redsentinel.fr"
    TLC_PROJECT: "identifiant-de-l-api"
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      scan=$(curl -fsS -X POST -H "Authorization: Bearer $TLC_TOKEN" \
        "$TLC_URL/api/v1/projects/$TLC_PROJECT/scans" | jq -r .id)
      result=pending
      for i in $(seq 1 120); do
        resp=$(curl -fsS -H "Authorization: Bearer $TLC_TOKEN" \
          "$TLC_URL/api/v1/scans/$scan?fail_on=new")
        result=$(echo "$resp" | jq -r .gate.result)
        [ "$result" != "pending" ] && break
        sleep 15
      done
      echo "$resp" | jq '{status, issues, gate}'
      test "$result" = "pass"

Notre robot

Toutes les requêtes de scan, de téléchargement de spec et de vérification de domaine portent l'agent :

User-Agent: tenantleak-cloud/1.3 (+https://tenantleak.redsentinel.fr/bot)

Elles respectent 5 requêtes par seconde au plus, 5 000 requêtes et 15 minutes par scan, en lecture seule sauf activation des tests d'écriture. Si votre pare-feu applicatif bloque les scans, autorisez cet agent sur votre préproduction. Plus d'informations pour les administrateurs sur la page Notre robot.

Une question qui n'est pas traitée ici ? Écrivez à alex@redsentinel.fr.