Sécurité et identité
Architecture de sécurité applicative : threat modeling et principes de conception
Intermédiaire11 min de lectureMis à jour
Cette page a été traduite de l'anglais avec l'aide de l'IA, puis vérifiée par rapport à l'original. Lire la version anglaise
Une sécurité ajoutée après coup coûte cher et reste incomplète. Ce guide montre comment traiter la sécurité comme une propriété de l'architecture : représenter le système sous forme de flux de données et de frontières de confiance, recenser les menaces avec STRIDE, appliquer les grands principes de protection, choisir la couche qui porte chaque contrôle et intégrer toute la démarche au pipeline de livraison.
Ce que vous allez apprendre
- Dessiner un diagramme de flux de données avec ses frontières de confiance et s'en servir pour raisonner sur la surface d'attaque
- Recenser et hiérarchiser les menaces avec STRIDE, puis traduire chacune en contrôle concret
- Appliquer le moindre privilège, la défense en profondeur, la médiation complète, l'échec sécurisé et la sécurité par défaut à des conceptions réelles
- Choisir la bonne couche pour l'authentification, l'autorisation et la validation des entrées
- Gérer les secrets applicatifs sans jamais les placer dans le contrôle de version
- Intégrer les activités de sécurité au pipeline de livraison plutôt qu'à un audit avant mise en production
Presque toutes les brèches sérieuses remontent à une décision, pas à une faute de frappe : un service à qui l'on a donné des identifiants de base de données dont il n'a jamais eu besoin, un endpoint interne que tout le monde croyait inaccessible, une intégration qui faisait confiance à un identifiant fourni par l'appelant. Ce sont des problèmes d'architecture, et aucun soin apporté au code en aval ne les compense entièrement. Concevoir la sécurité dès le départ, c'est se demander, tant que les boîtes et les flèches coûtent encore peu à déplacer, qui peut parler à quoi, ce que chacun a le droit de faire et ce qui se passe quand l'une des boîtes tombe sous le contrôle d'un attaquant.
Ce guide porte sur ce travail de conception. Il explique comment modéliser un système pour rendre les menaces visibles, présente le petit ensemble de principes qui ont résisté à cinquante ans de pratique, indique où chaque type de contrôle a sa place dans une application en couches, traite de la gestion des secrets et montre comment faire vivre tout cela dans la livraison courante plutôt que dans une course de dernière minute avant la mise en production. Les catégories de risques elles-mêmes sont recensées dans le guide OWASP Top 10 ; ici, on se place un niveau au-dessus, pour décider de la forme du système qui rend ces risques improbables.
La sécurité est une propriété de la conception
Les exigences fonctionnelles décrivent ce que le système doit faire. Les exigences de sécurité décrivent ce qu'il ne doit jamais faire, quel que soit le demandeur, et des exigences négatives ne peuvent pas être testées de manière exhaustive : c'est pour cela qu'elles doivent être structurelles. « Le service de reporting ne peut pas lire les données de paiement des clients » est une affirmation solide quand ce service ne détient aucun identifiant donnant accès à cette table ; c'est une affirmation fragile quand elle ne repose que sur une convention de revue de code.
Trois questions d'architecture font l'essentiel du travail :
- Quels sont les actifs ? Données personnelles, mouvements d'argent, identifiants, pistes d'audit, disponibilité d'un parcours critique. Hiérarchisez-les, car vous ne pouvez pas tout protéger de la même façon.
- Où sont les frontières de confiance ? Chaque point où des données passent d'un endroit que vous contrôlez à un endroit que vous ne contrôlez pas, ou entre deux composants aux privilèges différents, est une frontière qui exige authentification, autorisation et validation.
- Quel est le rayon d'impact ? Si un composant est entièrement compromis, jusqu'où l'attaquant peut-il aller ? Une conception dont la réponse est « partout » doit être retravaillée, quelle que soit la qualité de son traitement des entrées.
Threat modeling avec les flux de données et STRIDE
Un threat model utile part d'un diagramme que n'importe qui dans l'équipe pourrait dessiner : acteurs externes, processus, stockages de données, flux qui les relient et pointillés pour les frontières de confiance. Le détail est l'ennemi : une page, au niveau de « le navigateur parle à l'API gateway, qui appelle le service de commandes, qui écrit dans la base des commandes et publie un événement ».
[Browser] --https--> | TB1 | [API gateway] --mTLS--> [Order service] --TLS--> (Orders DB)
| |
v v
[Identity provider] | TB2 | [Payment provider]
TB1 internet / platform boundary — untrusted input, session establishment
TB2 organisation / third party — outbound secrets, response we do not control
Passez ensuite chaque élément et chaque flux au crible de STRIDE, un moyen mnémotechnique qui associe chaque catégorie de menace à la propriété qu'elle viole :
| Menace | Propriété perdue | Contrôle typique |
|---|---|---|
| Spoofing (usurpation d'identité) | Authenticité | Authentification forte des utilisateurs et des services (TLS mutuel, jetons signés) |
| Tampering (altération) | Intégrité | Signatures, transport à intégrité protégée, validation côté serveur, journaux d'audit immuables |
| Repudiation (répudiation) | Non-répudiation | Journaux en ajout seul avec une identité fiable, transactions signées |
| Information disclosure (divulgation d'informations) | Confidentialité | Chiffrement en transit et au repos, minimisation, messages d'erreur soignés |
| Denial of service (déni de service) | Disponibilité | Quotas, rate limiting, timeouts, bulkheads, plafonds de ressources lors du parsing |
| Elevation of privilege (élévation de privilèges) | Autorisation | Moindre privilège, médiation complète, contrôles au niveau de la ressource |
Consignez chaque constat sous forme de risque tenant en une ligne, avec le contrôle qui y répond, un responsable et une décision. Classer par probabilité et impact suffit ; un scoring élaboré change rarement ce que l'équipe fera ensuite.
- id: "TM-014"
element: "Order service -> Payment provider"
threat: "Information disclosure"
description: "API key for the payment provider is read from an environment variable and printed by the outbound HTTP logger."
likelihood: "medium"
impact: "high"
control: "Redact Authorization headers in the client interceptor; move the key to the secret manager with 90-day rotation."
status: "accepted-fix"
owner: "payments"
En parallèle du modèle, rédigez des abuse cases à côté des user stories qu'ils prennent à contre-pied. Si la story est « un client télécharge sa facture », l'abuse case est « un client télécharge la facture de quelqu'un d'autre en modifiant l'identifiant dans l'URL », et il devient un test. Les équipes qui ajoutent un abuse case par story significative interceptent la plupart des défauts de contrôle d'accès avant même qu'ils n'existent.
Les principes qui continuent de payer
Moindre privilège. Chaque identité (humain, service, batch) reçoit le jeu de permissions le plus restreint qui lui permet de faire son travail, et rien de plus. Concrètement : un utilisateur de base de données distinct par service, avec des droits limités aux tables qu'il manipule, des rôles cloud restreints à des ressources précises et des conteneurs qui ne tournent pas en root. Cela signifie aussi que les privilèges expirent : une élévation temporaire jamais révoquée équivaut à une élévation permanente.
Défense en profondeur. Partez du principe que tout contrôle isolé finira par échouer. Une requête paramétrée, un compte de base de données sans droit DROP et une alerte sur un volume de requêtes inhabituel défendent le même actif par des mécanismes différents.
Médiation complète. Chaque accès à chaque objet protégé est vérifié, à chaque fois, côté serveur. Mettre en cache une décision d'autorisation pour toute la durée d'une session, c'est exactement ce qui permet à « l'utilisateur était admin au moment de sa connexion » de survivre à sa rétrogradation.
Échec sécurisé. Quand quelque chose tourne mal (le service de politiques ne répond pas à temps, un claim manque, un certificat ne peut pas être validé), l'issue sûre est le refus. Il est facile de faire l'inverse dans un code qui traite une exception comme « inconnu, donc autorisé ».
public boolean canView(User user, Document doc) {
try {
return policyEngine.evaluate(user, "document:read", doc);
} catch (PolicyUnavailableException e) {
log.warn("policy engine unavailable, denying document {}", doc.id(), e);
return false; // deny on failure, never "return true" to keep the page working
}
}
Sécurité par défaut. L'état initial doit être l'état sûr : inscription fermée, endpoints de debug désactivés, TLS obligatoire, aucun compte d'exemple. Quiconque a besoin de quelque chose de plus permissif l'active explicitement et laisse une trace.
Réduire la surface d'attaque. Chaque endpoint exposé, feature flag activé, paquet installé et port ouvert est une chose de plus à défendre. Supprimer une console d'administration inutilisée est un contrôle plus fort que de bien la protéger.
Séparation des tâches. La personne ou le pipeline qui écrit une modification ne doit pas être le seul à pouvoir l'approuver et la déployer, et le service qui émet des avoirs ne doit pas aussi les approuver.
Ne pas compter sur l'obscurité. Partez du principe que l'attaquant a lu votre code source et votre documentation. Les secrets, ce sont des clés et des jetons, pas des URL ni des noms de schéma.
Où placer chaque contrôle
Une erreur de conception récurrente consiste à faire respecter une règle dans une couche qui peut être contournée. Placez les contrôles en vous demandant ce qui pourrait atteindre l'opération si la couche du dessus était entièrement court-circuitée.
| Préoccupation | Où elle se place | Pourquoi |
|---|---|---|
| Chiffrement du transport, rate limiting, règles de routage grossières | Edge / gateway | Peu coûteux à appliquer uniformément ; voir API management et API gateways |
| Authentification de l'appelant | L'edge pour l'identité, revérifiée dans chaque service | Un appelant interne doit lui aussi prouver qui il est |
| Autorisation grossière (rôles, scopes) | Filtre ou intercepteur du framework | S'applique par défaut à toutes les routes |
| Autorisation au niveau de la ressource (« cette commande est-elle la vôtre ? ») | Service de domaine ou applicatif | Seule cette couche connaît la règle de propriété |
| Validation syntaxique des entrées | Frontière, côté serveur | Rejeter tôt et à peu de frais les entrées impossibles |
| Validation des règles métier (« le solde ne peut pas devenir négatif ») | Modèle de domaine | C'est un invariant du domaine, pas un problème d'entrée |
| Intégrité référentielle et unicité | Contraintes de base de données | Le dernier rempart qu'aucun chemin de code ne peut contourner |
| Encodage des sorties | Couche de vue / de rendu | L'encodage dépend du contexte dans lequel la donnée atterrit |
La validation côté client est une fonctionnalité d'ergonomie, rien de plus. Dans un système distribué, ce tableau s'applique service par service : dans une architecture microservices, la gateway authentifie l'utilisateur final et chaque service vérifie le jeton de manière indépendante et applique sa propre autorisation, car une position dans le réseau n'est pas une permission. C'est le cœur pratique du zero trust tel que le décrit le NIST SP 800-207.
Pour l'implémentation de ces couches côté servlet (chaînes de filtres, contrôles au niveau des méthodes et validation de jetons stateless), consultez les bases de Spring Security.
Les secrets relèvent aussi de l'architecture
Un secret présent dans un dépôt est compromis dès l'instant où il est poussé, et réécrire l'historique n'annule pas la fuite : le secret doit être renouvelé. L'objectif de conception est qu'aucun secret de longue durée ne soit jamais consigné quelque part par un humain.
- Préférez l'identité de workload aux secrets partagés. Les plateformes cloud peuvent délivrer des identifiants de courte durée à une instance en cours d'exécution en fonction de son identité, ce qui supprime complètement la clé statique.
- Stockez ce qui reste dans un gestionnaire de secrets, injecté à l'exécution sous forme de variable d'environnement ou de fichier monté, jamais intégré à une image ni à un fichier de configuration livré avec le build.
- Procédez à une rotation planifiée et au moindre soupçon, et concevez l'application pour qu'elle prenne en compte une nouvelle valeur sans rebuild.
- Séparez les secrets par environnement, pour qu'une clé de test divulguée ne vaille rien en production.
- Recherchez les secrets dans le pipeline et dans des hooks pre-commit, et traitez chaque détection comme un incident, pas comme une remarque mineure de revue de code.
- Chiffrez les données sensibles au repos avec des clés que l'application ne détient pas directement, via un service de gestion de clés, pour qu'une sauvegarde de base de données volée ne soit pas un jeu de données volé.
La sécurité dans le pipeline de livraison
Faire du shift left, c'est laisser le pipeline faire respecter ce que la conception a décidé, pour que les constats de sécurité arrivent pendant que la modification est encore fraîche dans l'esprit de son auteur. Un pipeline de livraison pragmatique enchaîne, à peu près dans cet ordre : détection de secrets et analyse des dépendances à chaque commit ; analyse statique réglée pour un faible taux de faux positifs afin que les développeurs continuent de lui faire confiance ; analyse du conteneur et de l'image de base au build ; contrôles de politiques sur l'infrastructure as code ; scan dynamique d'un environnement de test déployé ; enfin des artefacts signés avec leur provenance, pour démontrer que ce qui tourne en production est bien ce qui a été construit.
stages:
- build # unit tests, SAST, secret scan, SBOM generation
- package # container build, image scan, sign the artefact
- verify # deploy to ephemeral env, DAST, authorization test suite
- release # policy gate: no critical findings, provenance verified
Deux habitudes comptent davantage que le choix des outils. D'abord, ne faites échouer le build que sur les constats que l'équipe a jugés inacceptables, et suivez les autres avec une échéance : un pipeline qui échoue en permanence apprend aux gens à le contourner. Ensuite, écrivez un test de non-régression pour chaque défaut de sécurité corrigé, en particulier pour l'autorisation ; ce sont des tests ordinaires qui vérifient qu'une requête renvoie 403, et ils constituent la protection la moins chère contre le retour du même bug.
Erreurs fréquentes
- Ne modéliser que le cas nominal. Le diagramme montre le navigateur et l'API, mais omet l'outil d'administration, l'export nocturne et l'accès en lecture de l'équipe support, c'est-à-dire précisément là où se trouvent les privilèges intéressants.
- Considérer le réseau interne comme sûr. Dès qu'un service est compromis, un maillage interne à plat et sans authentification livre tout le reste.
- Autoriser sur la base d'identifiants fournis par le client. Un identifiant de tenant dans le corps d'une requête est une affirmation, pas un fait ; déduisez-le du principal authentifié.
- Un compte de base de données partagé par tous les services. Le moindre privilège devient impossible et le rayon d'impact maximal.
- Journaliser le payload. Jetons, numéros de carte et données personnelles finissent dans un stockage de logs dont le contrôle d'accès est bien plus faible que celui de la base d'où ils viennent.
- Valider un threat model et ne jamais le rouvrir. Le modèle est un artefact vivant ; une nouvelle intégration le rend caduc.
- Acheter un scanner au lieu de prendre une décision. Les outils trouvent des classes de bugs connues ; ils ne peuvent pas vous dire qu'un service n'aurait jamais dû avoir telle permission.
Points clés à retenir
- La sécurité se décide dans l'architecture : les actifs, les frontières de confiance et le rayon d'impact déterminent ce que tout effort de code ultérieur pourra accomplir.
- Un diagramme de flux de données d'une page et un passage en revue STRIDE produisent des contrôles concrets, attribuables à un responsable, plus vite que n'importe quel modèle de document.
- Moindre privilège, défense en profondeur, médiation complète, échec sécurisé, sécurité par défaut et surface d'attaque réduite sont les principes qui continuent de rentabiliser l'effort.
- Placez chaque contrôle dans la couche impossible à contourner : les règles grossières à l'edge, l'autorisation au niveau de la ressource dans le domaine, les invariants dans la base de données.
- Les secrets se rangent dans un gestionnaire ou, mieux, sont remplacés par une identité de workload de courte durée ; tout ce qui a été commité dans un dépôt doit être renouvelé.
- Faites respecter la conception par le pipeline (scans, gates de politiques, tests de non-régression sur l'autorisation) et faites vivre le threat model au fil des évolutions du système.
Sources et lectures complémentaires
Questions fréquentes
- À quel moment du projet faut-il faire le threat modeling ?
- Dès qu'il existe une ébauche des composants et des données qui circulent entre eux, puis à chaque fois que cette ébauche change de façon significative : nouvelle intégration, nouveau stockage de données, nouvelle catégorie d'utilisateurs. Un premier modèle esquissé en une heure au tableau blanc vaut mieux qu'un document parfait rédigé une fois le code écrit, car en phase de conception, la correction consiste généralement à faire un autre choix, pas à tout réécrire.
- La défense en profondeur, n'est-ce pas simplement du travail en double ?
- Non, parce que chaque couche échoue différemment. Une règle de gateway peut être contournée par un appelant interne, un contrôle du framework peut être court-circuité par un nouveau chemin de code, une contrainte de base de données ne peut pas être contournée du tout. On choisit les couches de sorte que l'angle mort d'un contrôle soit le point fort d'un autre. Ce qui est du gaspillage, c'est de recopier deux fois la même vérification dans la même couche ; ce qui a de la valeur, c'est de faire respecter le même invariant par des mécanismes indépendants.
- Comment convaincre une équipe de consacrer du temps de conception à la sécurité ?
- Parlez-en en termes de coût. Un contrôle d'autorisation manquant repéré sur un diagramme coûte une discussion ; découvert en production, il coûte un incident, une notification et un correctif fait dans l'urgence. Apportez un abuse case par user story en refinement, gardez le modèle sur le même tableau que l'architecture et consignez les décisions prises, y compris les risques acceptés délibérément, pour que personne ne les remette en cause plus tard.
- Le zero trust remplace-t-il la sécurité réseau ?
- Il remplace l'hypothèse selon laquelle être à l'intérieur du réseau vaut confiance. Chaque requête doit toujours être authentifiée, autorisée et, si possible, chiffrée, qu'elle traverse Internet ou un simple sous-réseau. La segmentation réseau reste utile comme couche supplémentaire qui limite les déplacements d'un intrus, mais ce n'est plus la frontière sur laquelle repose toute la conception.