Building Secure and Reliable Systems
Chapitre 5 / 14 · 23 min de lecture

Concevoir pour la compréhensibilité

Un système qu'on ne comprend pas ne peut être ni sûr ni fiable : invariants, frontières de confiance, base de calcul de confiance et frameworks.

Avant de pouvoir affirmer quoi que ce soit sur la posture de sécurité d'un système ou sur sa capacité à tenir ses objectifs de niveau de service (service level objectives, SLO), il faut pouvoir raisonner sur lui : comprendre ses composants, leurs interactions, et la façon dont il se comporte non seulement sous charge nominale mais aussi face à des entrées malveillantes soigneusement forgées. Ce chapitre, signé par des ingénieurs de Google, défend une idée à la fois simple et exigeante : la compréhensibilité (understandability) n'est pas un confort esthétique, c'est le prérequis sur lequel reposent toutes les autres garanties. Les auteurs y livrent un terrain d'entente pratique, à mi-chemin entre la vérification formelle — trop coûteuse à grande échelle — et la simple lecture de code complétée de quelques tests, dont la confiance est illusoire.

Pour les besoins du livre, la compréhensibilité d'un système se définit comme la mesure dans laquelle une personne dotée du bagage technique adéquat peut raisonner avec exactitude et confiance sur deux choses : le comportement opérationnel du système, et ses invariants, y compris ceux de sécurité et de disponibilité. Cette approche est défensive et éducative : on explicite les modes de défaillance et les classes de vulnérabilités précisément pour mieux concevoir des barrières, jamais pour outiller une attaque.

Pourquoi la compréhensibilité est-elle un prérequis ?

Concevoir un système compréhensible, et le maintenir tel au fil du temps, demande un effort. Cet effort est un investissement qui se rembourse en vélocité durable du projet (un thème déjà central dans les travaux sur la performance de livraison). Plus concrètement, un système compréhensible offre trois bénéfices tangibles.

BénéficeMécanismeConséquence si le système est opaque
Moins de vulnérabilités et de pannesToute modification (ajout de fonctionnalité, correction, changement de config) porte un risque d'introduire une faille ou une fragilitéL'ingénieur méconnaît le comportement existant ou ignore une exigence implicite et casse une garantie
Réponse aux incidents efficaceLes intervenants doivent évaluer les dégâts, contenir, identifier et corriger la cause racine viteUn système difficile à comprendre allonge dramatiquement chaque étape
Confiance dans les assertions de sécuritéUne assertion de sécurité s'exprime comme un invariant valable « pour tous les comportements possibles »Impossible de vérifier avec un haut degré de confiance qu'un tel énoncé tient vraiment

Le troisième point est le plus subtil. Les assertions de sécurité portent sur tous les comportements possibles, y compris la réaction à des entrées malformées ou hostiles. Or les tests n'exercent qu'une fraction réduite de ces comportements — ceux qui correspondent au fonctionnement typique attendu. Ce n'est pas un hasard si des classes de failles bien connues comme l'injection SQL, le script intersite (cross-site scripting, XSS) ou le débordement de tampon (buffer overflow) trustent depuis des années les têtes de classement des vulnérabilités. La maxime à retenir : l'absence de preuve n'est pas la preuve de l'absence. Pour établir un « pour tous les comportements », il faut raisonner abstraitement sur le système, pas seulement le tester.

Les invariants du système

Un invariant (system invariant) est une propriété qui demeure vraie quoi qu'il arrive, peu importe la façon dont l'environnement du système se comporte — ou se déforme. Cet environnement englobe tout ce qu'on ne contrôle pas directement : de l'utilisateur malveillant qui martèle le frontend de requêtes forgées jusqu'aux pannes matérielles qui provoquent des crashs aléatoires. Le système est seul responsable de garantir qu'une propriété désirée est effectivement un invariant, y compris quand l'environnement se conduit de manière arbitrairement inattendue ou hostile. Tout l'enjeu d'une analyse de système consiste à déterminer si les propriétés souhaitées sont réellement des invariants.

Voici quelques propriétés désirables, mêlant sécurité et fiabilité :

SÉCURITÉ
  • Seuls les utilisateurs authentifiés et dûment autorisés accèdent
    au magasin de données persistantes.
  • Toute opération sur des données sensibles est consignée dans un
    journal d'audit conforme à la politique.
  • Toute valeur reçue hors de la frontière de confiance est validée
    ou encodée avant d'atteindre une API sujette aux injections.

FIABILITÉ
  • Le nombre de requêtes vers le backend croît proportionnellement
    au nombre de requêtes reçues par le frontend.
  • Si le backend ne répond pas dans un délai donné, le frontend se
    dégrade gracieusement (réponse approchée).
  • En surcharge, un composant sert des erreurs « overload » plutôt
    que de crasher, pour éviter la panne en cascade.
  • Un système ne reçoit des RPC que d'un ensemble désigné de systèmes,
    et n'en envoie qu'à un ensemble désigné de systèmes.

Si le système autorise un comportement qui viole une propriété désirée — autrement dit, si la propriété énoncée n'est pas réellement un invariant —, alors il y a une faiblesse ou une vulnérabilité. Imaginez que la première propriété tombe parce qu'un gestionnaire de requêtes a oublié son contrôle d'accès, ou l'a implémenté de travers : un attaquant pourrait alors accéder aux données privées des utilisateurs. De même, si la quatrième propriété n'est pas tenue — par exemple un frontend qui multiplie les nouvelles tentatives en rafale, sans mécanisme de retrait exponentiel (backoff), dès qu'un appel backend échoue ou traîne —, le système s'expose à un déni de service auto-infligé : le frontend peut submerger le backend et rendre le service muet.

Analyser les invariants : un arbitrage coût / confiance

Vérifier qu'un système respecte un invariant donné implique un compromis entre le préjudice potentiel d'une violation et l'effort consenti pour l'établir. Le spectre va d'une extrémité bon marché mais peu fiable à une extrémité rigoureuse mais inabordable.

ApprocheEffortConfiance obtenue
Quelques tests + lecture partielle du codeFaibleFaible : le comportement non couvert recèle souvent des bugs
Argumentation rigoureuse mais informelle, appuyée sur une conception pensée pour la compréhensibilitéRaisonnableÉlevée, et tenable à grande échelle
Vérification formelle (preuve logique automatisée)ÉnormeTrès élevée, mais réservée à des cas étroits

La vérification formelle prouve mathématiquement que la propriété tient, mais son coût est prohibitif : l'un des plus vastes projets de ce type a démontré l'exactitude et la sécurité d'un micronoyau au niveau du code machine au prix d'environ vingt années-personnes. Elle devient praticable pour des micronoyaux ou du code cryptographique pointu, mais reste hors de portée pour le développement applicatif à grande échelle. Le livre vise donc la voie médiane : en concevant explicitement pour la compréhensibilité, on soutient des arguments principiels mais informels que le système possède certains invariants, et l'on gagne une confiance élevée pour un effort raisonnable. Chez Google, cette approche s'est révélée pratique pour le développement à grande échelle et redoutablement efficace pour réduire les classes courantes de vulnérabilités.

Modèles mentaux et complexité

Les humains raisonnent mal sur des systèmes hautement complexes pris d'un seul tenant. En pratique, ingénieurs et experts se construisent des modèles mentaux (mental models) : des représentations qui expliquent les comportements pertinents tout en laissant de côté les détails superflus. Pour un système complexe, on empile plusieurs modèles qui s'appuient les uns sur les autres : en réfléchissant à un sous-système, on substitue aux composants voisins leurs modèles respectifs.

Ces modèles simplifient le raisonnement — et c'est pour cette raison même qu'ils sont limités. Un modèle forgé sur l'observation d'un système en conditions normales peut être incapable de prédire son comportement dans les scénarios inhabituels. Or l'ingénierie de sécurité et de fiabilité s'intéresse précisément à ces conditions-là : attaque active, surcharge, défaillance de composant. Songez à un système dont le débit croît graduellement avec le taux de requêtes — jusqu'à un seuil où, sous la pression mémoire, l'emballement (thrashing) du gestionnaire de mémoire virtuelle ou du ramasse-miettes fait soudain chuter le débit. Diagnostiquer ce système avec un modèle mental simplifié peut sérieusement égarer l'intervenant, à moins de reconnaître explicitement que le modèle ne s'applique plus.

Astuce

À la conception, anticipez les modèles mentaux que les ingénieurs construiront inévitablement. Un nouveau composant devrait susciter un modèle cohérent avec ceux des sous-systèmes similaires existants. Concevez aussi pour que ces modèles restent prédictifs en conditions extrêmes : par exemple, configurer les serveurs de production sans espace d'échange (swap) sur disque permet à un service qui ne peut plus allouer de mémoire de renvoyer une erreur prévisible, plutôt que de s'effondrer de façon opaque par emballement.

Maîtriser la complexité

L'ennemi numéro un de la compréhensibilité est la complexité non maîtrisée. Une part de complexité est inhérente et incontournable, simplement à cause de l'échelle des systèmes modernes : Google emploie des dizaines de milliers d'ingénieurs travaillant dans un dépôt de plus d'un milliard de lignes de code. Même une petite organisation peut implémenter des centaines de fonctionnalités dans des centaines de milliers de lignes. Gmail, par exemple, n'est pas « un simple service d'e-mail dans le cloud » : multiples frontends, API tierces, interfaces IMAP et POP, gestion et rendu des pièces jointes, client hors-ligne et synchronisation, anti-spam, catégorisation automatique, Smart Reply et Smart Compose… Cette richesse définit le produit ; on ne peut pas demander aux chefs de produit de la supprimer au nom de la sécurité.

La distinction-clé est donc entre complexité subie et complexité maîtrisée.

Complexité subie (non maîtrisée)Complexité maîtrisée
Couplage diffus, dépendances implicitesComposants au périmètre clair et contraint
Exigences de sécurité répétées et codées à la main partoutResponsabilité centralisée dans une bibliothèque ou un framework
Frontières floues, confiance implicite entre composantsFrontières de sécurité explicites, base de calcul de confiance réduite
Données circulant sous forme de types vagues (chaînes brutes)Types forts portant un contrat vérifiable
Modèle mental gigantesque, impossible à tenir en têteModèles mentaux modulaires, composables

L'objectif n'est pas d'éliminer la complexité inhérente — c'est impossible — mais de la compartimenter et la contenir pour qu'un humain puisse raisonner avec fidélité sur les propriétés et comportements pertinents. On rend un système plus compréhensible en le composant de petits composants, dont on raisonne sur chacun isolément, puis qu'on combine de sorte à dériver les propriétés du tout à partir de celles des parties. Cela permet d'établir des invariants à l'échelle du système entier sans devoir le penser d'un seul bloc.

Centraliser la responsabilité des exigences

Les exigences de sécurité et de fiabilité s'appliquent souvent horizontalement, à tous les composants : « toute opération exécutée en réponse à une requête utilisateur doit journaliser l'accès » ou « doit vérifier l'authentification et l'autorisation ». Si chaque composant implémente ces tâches communes de son côté, il devient impossible de savoir si le système entier satisfait l'exigence. La parade : déplacer la responsabilité vers un composant centralisé — typiquement une bibliothèque ou un framework.

            ┌─────────── AD HOC (fragile) ──────────┐
   requête  │  svc A → [auth? log? quota?]           │  chaque
   ────────►│  svc B → [auth? log? quota?]  ← oubli  │  service
            │  svc C → [auth? log? quota?]  ← bug    │  réimplémente
            └───────────────────────────────────────┘
                          vs.
            ┌────────── CENTRALISÉ (robuste) ───────┐
   requête  │  framework RPC : auth + autz + log +   │  défini une
   ────────►│  propagation de deadline + annulation  │  seule fois,
            │  → svc A → svc B → svc C               │  amorti partout
            └───────────────────────────────────────┘

Un framework de service RPC peut garantir authentification, autorisation et journalisation de chaque méthode RPC selon une politique définie centralement. Les méthodes individuelles n'en sont plus responsables : les développeurs ne peuvent ni les oublier ni les implémenter de travers, et un relecteur de sécurité comprend les contrôles d'accès du service sans lire chaque méthode — il lui suffit de comprendre le framework et d'inspecter la configuration spécifique. Côté fiabilité, le même framework peut assurer la propagation automatique des deadlines et la gestion centralisée de l'annulation des requêtes, supprimant le risque qu'un oubli local dégrade tout le système. Les deux bénéfices : compréhensibilité accrue (un seul endroit à valider) et probabilité accrue que le système soit réellement correct. Le coût initial de construction du framework s'amortit sur toutes les applications qui en héritent.

Architecture en couches et interfaces

Structurer en couches et composants permet de raisonner par morceaux. Mais il faut soigner autant les frontières et les interfaces que les composants eux-mêmes : des composants trop fortement couplés sont aussi durs à comprendre qu'un monolithe. Les développeurs aguerris savent qu'il faut traiter les entrées de l'environnement externe comme non fiables ; ils oublient plus facilement que les appelants d'API internes de bas niveau (objets en processus, microservices backend) ne devraient pas, eux non plus, être présumés dignes de confiance.

À retenir

Moins un composant fait d'hypothèses sur ses appelants, plus il est facile à raisonner en isolation. Idéalement, un composant ne fait aucune hypothèse sur ses appelants. Si une propriété de sécurité dépend d'une précondition garantie par l'appelant (ordonnancement correct des opérations, contraintes sur les paramètres), alors vérifier la propriété exige de comprendre chaque site d'appel de l'API à travers tout le système — exercice vite ingérable. Quand une hypothèse est inévitable, capturez-la explicitement dans l'interface ou en restreignant l'ensemble des principaux autorisés à interagir.

Des interfaces étroites et typées

Le choix du modèle d'interface pèse lourd sur la compréhensibilité. Les frameworks qui supportent des types définis par l'utilisateur (gRPC, Thrift, OpenAPI) nomment chaque méthode et le type de ses entrées et sorties ; à l'inverse, un service REST en JSON libre accepte n'importe quelle requête et délègue la validation au code applicatif.

  • Préférez des interfaces étroites, qui laissent peu de place à l'interprétation. Le typage fort permet d'outiller le référencement croisé, les vérifications de conformité, l'évolution sûre (versionnage OpenAPI, règles de compatibilité ascendante des protocol buffers). Une API en JSON libre est opaque tant qu'on n'inspecte pas son implémentation, et un client et un serveur mis à jour séparément peuvent interpréter différemment une charge utile — jusqu'au crash.
  • Préférez un modèle d'objets commun (à la Kubernetes) : chaque objet satisfait un jeu de propriétés de base, on dispose de façons standard de cadrer, annoter, référencer et grouper les objets, et les opérations se comportent uniformément. Un même modèle mental couvre alors de larges pans du système, types personnalisés inclus.
  • Soignez l'idempotence. Une opération idempotente donne le même résultat appliquée plusieurs fois. Dans un système distribué — où les opérations arrivent dans le désordre et où une réponse peut se perdre —, l'idempotence autorise le client à simplement réessayer jusqu'au succès. Sinon, un retry après une réponse perdue crée un doublon. On rend une opération idempotente en exigeant un identifiant unique (un UUID) par mutation : recevoir deux fois le même identifiant signale un doublon.

Identités, authentification et contrôle d'accès compréhensibles

Tout système doit pouvoir dire qui a accès à quelle ressource, surtout sensible. Une identité est l'ensemble des attributs qui se rapportent à une entité ; des identifiants (credentials) — mot de passe, certificat X.509, jeton OAuth2 — assertent cette identité via un protocole d'authentification. Les grands systèmes sont des constellations de microservices qui s'appellent les uns les autres, avec ou sans humain dans la boucle : il faut donc une identité pour toutes les entités actives — humains, composants logiciels, composants matériels —, pas seulement les humains.

Attention

Les pratiques réseau traditionnelles utilisent parfois l'adresse IP comme identifiant primaire pour le contrôle d'accès et l'audit (règles de pare-feu). C'est inadapté aux microservices modernes : les IP manquent de stabilité et de sécurité, sont aisément usurpables, et plusieurs services cohabitent souvent sur un même hôte. Les ports ne valent pas mieux — réutilisables, voire arbitrairement choisis par les services. Une IP ne fournit pas un identifiant stable pour modéliser le niveau de privilège d'un service.

Pour être « parlant », un identifiant doit réunir trois qualités. Il doit être compréhensible : widget-store-frontend-prod identifie clairement une charge de travail, là où 24245223 n'évoque rien — et une erreur dans une liste de contrôle d'accès se repère bien mieux avec des noms lisibles qu'avec des nombres arbitraires. Il doit être robuste à l'usurpation : un jeton porteur en clair s'usurpe trivialement, un certificat adossé à une clé privée scellée dans un module matériel (TPM) et utilisé en TLS bien plus difficilement. Il doit enfin être non réutilisable : si une adresse e-mail sert d'identifiant et qu'une nouvelle recrue hérite de celle d'un administrateur parti, elle pourrait hériter de ses privilèges. Un système d'identité à l'échelle de l'organisation renforce un modèle mental commun : tout le personnel parle le même langage en désignant les entités, là où des systèmes d'identité concurrents pour un même type d'entité compliquent inutilement la compréhension. On peut aussi externaliser ce sous-système, par exemple via OpenID Connect (OIDC) — au prix de l'habituel arbitrage entre simplicité et robustesse perçue du fournisseur de confiance.

Le modèle d'identités de la production Google

Google modélise quatre types d'entités actives. Les administrateurs sont les humains (ingénieurs Google) qui mutent l'état du système — pousser une release, modifier une configuration. Les machines sont les serveurs physiques du datacenter, catalogués dans un inventaire global et adressables par nom DNS, dont l'identité reflète la vocation (machine de labo vs production). Les charges de travail (workloads) sont ordonnancées sur les machines par le système d'orchestration Borg, proche de Kubernetes ; leur identité, choisie par le demandeur, diffère le plus souvent de celle de la machine hôte — et l'orchestrateur vérifie que le demandeur a le droit d'ordonnancer une charge sous l'identité demandée. Les clients accèdent enfin aux services. Les administrateurs sont à la racine de toutes les interactions : même dans un échange charge-à-charge, ce sont eux qui ont initié le démarrage de la charge lors de l'amorçage. C'est ce qui rend l'audit possible — remonter chaque action à un administrateur pour établir la responsabilité et analyser le niveau de privilège d'un employé.

Quant à l'authentification et à la sécurité du transport — cryptographie, conception de protocole, OS —, on ne peut raisonnablement attendre de chaque ingénieur qu'il les maîtrise. Le système ALTS (Application Layer Transport Security) de Google fournit une sécurité de transport et une authentification service-à-service sans configuration : le développeur n'a pas à se soucier de l'approvisionnement des identifiants ni de l'algorithme cryptographique. Son modèle mental se résume à : une application tourne sous une identité parlante (l'identité de l'administrateur pour un outil sur son poste, celle de la machine pour un processus privilégié, une identité de charge comme myservice-frontend-prod pour un service déployé) ; ALTS sécurise le transport ; une API récupère les informations du pair authentifié. Sans une telle approche systématique, vérifier l'authentification exigerait de lire à la main tout le code de chaque application — ce qui ne passe pas à l'échelle. Côté autorisation, des frameworks qui codifient des politiques de contrôle d'accès déclaratives offrent une façon unifiée de décrire les règles, savent gérer les interactions complexes (la chaîne de charges Ingress → Frontend → Backend portant la requête d'un client authentifié), et — parce qu'elles sont déclaratives — rendent possible un outillage automatique d'évaluation de l'exposition, impensable avec une logique ad hoc.

Frontières de sécurité et base de calcul de confiance

La base de calcul de confiance (trusted computing base, TCB) d'un système est, selon la définition classique, « l'ensemble des composants (matériel, logiciel, humain…) dont le bon fonctionnement suffit à garantir que la politique de sécurité est respectée — ou, plus parlant, dont la défaillance pourrait causer une brèche ». La TCB doit donc tenir la politique même si toute entité hors d'elle se comporte de façon arbitraire et malveillante. L'interface entre la TCB et « tout le reste » est la frontière de sécurité (security boundary), et la TCB doit traiter avec suspicion tout ce qui la franchit — les données comme leurs propriétés annexes (l'ordre des messages, par exemple).

La forme d'une TCB dépend de la politique visée. Au niveau OS, la TCB qui assure la séparation entre utilisateurs se compose du noyau, des processus privilégiés et des démons système, adossés à des mécanismes matériels comme la mémoire virtuelle. Un serveur applicatif tournant sous un rôle non privilégié n'appartient pas à cette TCB-là — mais le code qui applique sa propre politique applicative (l'accès aux données utilisateur uniquement via un partage explicite) en fait, lui, partie pour cette politique. Pour garantir une politique, il faut donc comprendre et raisonner sur toute la TCB pertinente, car un bug n'importe où en son sein peut causer une brèche. D'où l'impératif : garder la TCB aussi petite que possible et en exclure tout composant qui ne participe pas à la politique — chaque composant superflu nuit à la compréhensibilité et ajoute du risque.

Reprenons l'application qui vend des widgets en ligne. On veut garantir que seuls les utilisateurs accèdent à leurs propres données sensibles (adresses de livraison) ; notons TCB_AdressesData la base de calcul de confiance pour cette propriété.

ARCHITECTURE MONOLITHIQUE — TCB large
┌──────────────────────────────────────────────┐
│  Serveur web monolithique (un seul processus) │
│  ┌────────┐ ┌──────────┐ ┌──────────────┐     │
│  │catalogue│ │ panier   │ │ achat/paiement│ ◄── TCB
│  └────────┘ └──────────┘ └──────────────┘     │     = TOUT
│         toutes les requêtes → une seule BDD    │     le serveur
└──────────────────────────────────────────────┘     + la BDD
   Une injection SQL dans la recherche catalogue,
   ou une RCE dans le serveur, expose TOUTES les données.

ARCHITECTURE MICROSERVICES — TCB réduite
┌──────────┐   RPC   ┌────────────────┐
│ Frontend │────────►│ Backend ACHAT  │ ◄── TCB_AdressesData
│   web    │         │  + sa BDD      │     (lui seul + ses
└────┬─────┘         └────────────────┘      dépendances)
     │ RPC   ┌────────────────┐
     └──────►│ Backend CATALOGUE + sa BDD │  ✗ hors TCB
             └────────────────┘
   Une faille dans le catalogue n'atteint plus les données d'achat :
   le backend catalogue n'y a tout simplement pas accès.

Dans le monolithe, la moindre faille — injection SQL dans la recherche, exécution de code à distance (remote code execution) dans le serveur — expose l'ensemble des données : tout le serveur et sa base sont dans la TCB. En découpant en microservices qui communiquent par RPC et traitent toute requête entrante comme potentiellement non fiable (même venant d'un microservice interne), la TCB_AdressesData se réduit au seul backend d'achat et à sa base. Un attaquant ne peut plus exploiter une faille du catalogue pour atteindre les données de paiement : ce composant n'y a pas accès.

Piège courant

On ne décrète pas une TCB en traçant un trait pointillé autour d'un composant : la TCB dépend du modèle de menace. Si le backend d'achat livre l'adresse de n'importe quel utilisateur dès que le frontend la demande, alors un attaquant qui compromet le frontend accède à tout — et le frontend redevient partie de la TCB. La parade Google : le ticket de contexte d'utilisateur final (end-user context ticket, EUC), un jeton interne à courte durée de vie frappé par un service d'authentification central en échange d'un identifiant externe (cookie, jeton OAuth2). Le backend ne répond qu'aux requêtes portant un EUC valide ; compromettre le frontend ne donne plus accès qu'aux utilisateurs actifs pendant l'attaque, pas à tous.

Un raisonnement analogue vaut côté navigateur : une origine web (protocole + hôte + port) est un domaine de confiance, et la politique de même origine (same-origin policy) cloisonne le code entre origines. Si le frontend sert toute son interface depuis https://widgets.example.com, une faille XSS dans l'affichage du catalogue peut lire le profil d'un utilisateur — et TCB_AdressesData réintègre tout le frontend. On y remédie en érigeant une frontière supplémentaire fondée sur l'origine : un frontend de catalogue sur https://widgets.example.com et un frontend de paiement séparé sur https://checkout.example.com (en veillant à ce que le second ne soit pas aussi accessible via une sous-URL du premier). Enfin, une TCB est souvent son propre domaine de défaillance (failure domain) : isolée, dotée d'une interface propre et raisonnable en isolation, elle facilite aussi la compréhension du comportement face aux bugs et aux attaques par déni de service.

Conception logicielle : frameworks et types forts

Une fois le système découpé par des frontières de sécurité, il reste à raisonner sur le code à l'intérieur de chaque frontière — souvent encore vaste. Deux leviers le rendent tractable.

Le premier est l'usage de frameworks applicatifs (application frameworks), aussi dits « tout compris » (batteries-included). Un système possède un framework d'authentification, d'autorisation, RPC, d'orchestration, de monitoring, de release… Toutes les combinaisons et configurations possibles submergent les ingénieurs. Un framework applicatif fournit un jeu canonique de sous-frameworks, avec des configurations par défaut raisonnables et l'assurance qu'ils fonctionnent ensemble. Le scénario qu'il prévient : un développeur expose un service, configure l'authentification mais oublie l'autorisation ; le service marche, mais tout client authentifié peut l'appeler — violant le moindre privilège (least privilege), et peut-être la méthode qui reconfigure tous les commutateurs d'un datacenter. Un bon framework garantit que toute application a une politique d'autorisation valide, avec des défauts sûrs (interdire tout client non explicitement autorisé), et adresse aussi monitoring, alerting, équilibrage de charge, planification de capacité — au passage, il fait parler le même langage à des équipes de départements différents.

Le second levier est le typage fort au service du suivi des flux de données. Beaucoup de propriétés de sécurité reposent sur des assertions à propos de valeurs qui circulent dans le système. Représenter une URL comme une simple chaîne paraît commode, mais le code aval suppose souvent implicitement que l'URL est bien formée ou d'un schéma précis — hypothèse non portée par le type string, qui n'affirme qu'« une suite de caractères ». Raisonner sur la correction du code aval exige alors de comprendre tout le code amont et de vérifier qu'il valide bien.

// Un type dédié porte le contrat « URL bien formée ».
// La fabrique valide à l'exécution : soit une Url valide,
// soit une erreur. Impossible d'obtenir une Url non validée.
class Url {
  private constructor(public readonly value: string) {}

  static parse(raw: string): Url {
    if (!isWellFormedHttpsUrl(raw)) {
      throw new Error("URL malformée");
    }
    return new Url(raw);
  }
}

// Le code aval consomme une Url, pas une string :
// la bonne formation est garantie par le type, pas par
// la discipline de chaque appelant.
function fetchResource(target: Url): Promise<Response> {
  return httpGet(target.value);
}

En représentant la valeur par un type dont le contrat stipule la propriété désirée — par exemple une fabrique Url.parse qui valide puis renvoie une instance —, on rétrécit drastiquement le code à lire et vérifier. Il suffit alors d'inspecter, d'une part, l'implémentation du type (ses constructeurs garantissent la bonne formation), et d'autre part le code qui consomme la valeur (en prenant le contrat comme hypothèse). Le code qui ne fait que transporter des instances n'a plus besoin d'être examiné. En un sens, le type se comporte comme une mini-TCB responsable de la propriété « toutes les URL sont bien formées ».

Note

Cette garantie tient sous l'hypothèse que le code environnant est non malveillant : dans la plupart des langages, l'encapsulation des types n'est pas une frontière de sécurité (la réflexion ou les casts permettent de violer l'état interne). Le typage protège contre les erreurs honnêtes, pas contre du code activement hostile — ce dernier relève de contrôles au niveau du dépôt (revue de code, contrôle d'accès, journaux d'audit). La même logique s'étend à la prévention des injections via des types comme SafeSql ou SafeHtml : leurs constructeurs garantissent l'innocuité, et les API sensibles (les « puits » d'injection, ou injection sinks) n'acceptent que ces types — rendant l'API sûre par construction (secure by construction). On peut alors affirmer qu'une application entière est exempte d'injection SQL ou de XSS en ne comprenant que l'implémentation des types et des puits typés.

Reste l'utilisabilité des API : si elles sont pénibles, les développeurs rechignent à les adopter. Les API sûres par construction ont le double mérite de rendre le code plus compréhensible et d'ancrer la sécurité dans la culture, sans surcharger le développeur. Un moteur de templates HTML à échappement contextuel automatique prend l'entière responsabilité de valider et échapper les données interpolées : du point de vue du développeur, il s'utilise comme un template ordinaire, mais sans avoir à se soucier de l'échappement. L'exemple emblématique est Tink, la bibliothèque cryptographique née de l'expérience de Google : sûre par défaut (l'API interdit par exemple la réutilisation de nonce en mode GCM), simple, lisible, agile (rotation de clés intégrée), et refusant le matériel de clé brut au profit d'un service de gestion de clés. Tink prévient les failles cryptographiques de bas niveau — mais ne corrige pas une erreur de conception de plus haut niveau (hacher une donnée à faible entropie comme un numéro de carte au lieu de la chiffrer). Développeurs et relecteurs doivent toujours comprendre ce qu'une bibliothèque garantit — et ce qu'elle ne garantit pas.

À retenir

  • La compréhensibilité est un prérequis : on ne peut affirmer une propriété de sécurité ou de fiabilité que si l'on peut raisonner avec confiance sur le système ; l'absence de preuve n'est pas la preuve de l'absence, et les tests seuls ne couvrent jamais « tous les comportements possibles ».
  • Un invariant est une propriété qui tient quoi que fasse l'environnement, y compris hostile ; le système en est seul responsable. Le but de l'analyse est de déterminer si les propriétés voulues sont vraiment des invariants — via une argumentation rigoureuse mais informelle, voie médiane entre tests superficiels et vérification formelle hors de prix.
  • L'ennemi est la complexité non maîtrisée : on la contient en composant de petits composants, en soignant les frontières et les interfaces (étroites, typées, idempotentes), et en centralisant les exigences horizontales (auth, autorisation, log, deadlines) dans un framework — défini une fois, amorti partout.
  • Des identités parlantes pour toutes les entités actives (administrateurs, machines, charges Borg, clients) fondent l'audit et le contrôle d'accès ; les frameworks déclaratifs (ALTS, autorisation, EUC) rendent ces contrôles compréhensibles et outillables, là où l'IP est un identifiant inadapté.
  • La base de calcul de confiance (TCB) doit tenir la politique même si tout le reste devient malveillant ; il faut la garder petite et raisonner sur l'intégralité de la TCB pertinente. Découper en microservices et ériger des frontières de sécurité (jusqu'à l'origine web) réduit le rayon d'impact d'une faille.
  • La TCB dépend du modèle de menace : le ticket de contexte d'utilisateur final (EUC) montre comment retirer un composant compromis de la TCB en limitant l'accès aux seuls utilisateurs actifs pendant l'attaque.
  • Le typage fort rétrécit le code à vérifier en portant les contrats (Url, SafeSql, SafeHtml) ; les frameworks applicatifs et les API sûres par construction (Tink, templates à échappement automatique) inscrivent les bonnes pratiques par défaut — mais ne dispensent jamais de comprendre ce qu'ils garantissent et ce qu'ils ne garantissent pas.