Blog ·
Un secret erroné ressemble au silence
Trois protocoles de supervision écrits à la main plutôt qu’importés, et une panne que le protocole lui-même interdit de distinguer d’un serveur mort.
Par Provibr
Ajouter une vérification de supervision pour un nouveau protocole ressemble à une question de dépendances. Il existe une crate pour MongoDB, une pour BSON, une qui parle le protocole réseau de SQL Server. Trois lignes dans un manifeste, quarante lignes de colle, terminé.
Les trois protocoles ont été écrits à la main, et les raisons méritent d’être notées car aucune n’est « pas inventé ici ». Deux sont des mesures, et la troisième est une promesse déjà faite aux clients sur ce que cet agent sait faire.
Ce qu’une dépendance apporte vraiment
La crate BSON entraîne quarante et une crates. Cela se mesure et cela a été mesuré, mais le nombre n’a pas tranché.
La vérification lit quatre champs dans une réponse du serveur. Un type de document générique garde toute la réponse en mémoire : la liste des hôtes, les étiquettes, l’horodatage de la dernière écriture, l’heure du cluster, tout ce que le serveur a bien voulu envoyer. Dès lors, « nous ne lisons que quatre champs » devient un accord entre développeurs au lieu d’une propriété du code, et les accords sont rompus par la ronde suivante qui a besoin d’un cinquième champ pour une bonne raison.
Il existe donc un lecteur qui parcourt les éléments, rend les quatre demandés et saute le reste sans le déposer nulle part. Il ne peut pas retenir la réponse, la promesse est donc une forme et non une habitude. La même décision que la liste d’autorisation sur la commande INFO de Redis, et la même qui a tenu un pilote de base de données complet hors de la vérification PostgreSQL : un pilote apporte un constructeur de requêtes, et alors il existe, dans le binaire qui tourne sur le réseau d’un client, du code capable de poser une question arbitraire à sa base.
La crate pour SQL Server est la version la plus tranchante. Elle compile pour notre cible de publication, ce que nous attendions justement comme obstacle. Ce qu’elle apporte, c’est une deuxième pile TLS : une ligne sortie de maintenance à côté de l’actuelle que nous livrons déjà. Le fournisseur cryptographique du processus est installé pour la ligne actuelle et ne fait rien pour l’ancienne, donc le TLS d’une vérification utiliserait un fournisseur différent de toute autre connexion de cet agent ; une faille dans cette ligne est une faille que nous livrons et que nous ne pouvons pas corriger sans attendre l’éditeur ; et la politique de certificats, qui vit dans un seul module précisément pour ne pas dériver, recevrait une deuxième implémentation pour une autre interface.
Le saut est l’endroit où un lecteur maison se trompe
Qui écrit son propre lecteur pour un format binaire peut commettre une erreur qui pèse plus que les autres, et elle n’est pas dans l’analyse. Elle est dans le saut.
Chaque valeur BSON porte un octet de type et une longueur qui en découle. Un lecteur qui connaît les types qu’il a croisés par hasard, et qui saute un type inconnu de zéro octet, n’échoue pas : il continue à lire le reste du document depuis le mauvais endroit. Tout ce qui suit est décalé. On obtient des valeurs plausibles qui ne sont pas celles qui ont été envoyées.
La table des longueurs est donc complète, avec un refus fermé pour tout le reste. Un octet de type inconnu est une erreur de protocole et jamais un saut.
La même famille d’erreurs se trouve un cran plus haut, dans la lecture des nombres. Un serveur MongoDB répond ok: 1 en virgule flottante et un code d’erreur en entier 32 bits, dans le même document. Un lecteur qui ne connaît que 32 bits manque le premier en silence, et alors chaque connexion échouée se lit comme réussie. Le lecteur accepte un nombre BSON dans ses trois largeurs et rend un entier.
L’étape d’authentification qui doit pouvoir échouer
La vérification se connecte avec SCRAM-SHA-256 et non avec SCRAM-SHA-1, pour deux raisons qui pointent dans le même sens.
Le mécanisme ancien n’injecte pas le mot de passe dans SCRAM. Il injecte un MD5 hexadécimal du nom d’utilisateur, d’une chaîne fixe et du mot de passe. Le prendre en charge reviendrait à écrire du code qui fabrique un MD5 d’identifiants, pour un mécanisme qu’aucun serveur actuel n’a besoin de proposer.
La raison silencieuse est pire. Un serveur qui propose les deux atterrirait, en repli, sur le plus faible sans que rien ne passe au rouge, et alors une mesure verte surmonte une connexion plus faible que ce que le client croit avoir. Si le serveur ne propose pas SHA-256 pour cet utilisateur, la vérification refuse avec un code à elle et le dit.
Écrire SCRAM à la main avait été refusé dans une ronde précédente pour la vérification PostgreSQL, et l’objection était réelle : l’échange comporte une signature du serveur que le client doit vérifier, et une implémentation qui saute cette étape est verte face à un vrai serveur et verte de la même façon face à un faux. Ici l’objection n’a pas été écartée mais traitée. La vérification de la signature est une fonction à part avec son propre code d’erreur, pour que « l’étape manque » ne soit pas la même chose que « l’étape a réussi », et il existe un test qui présente une signature falsifiée et exige que la connexion échoue. Ce test a été prouvé au rouge en retirant la vérification.
La crate qu’utilise la vérification PostgreSQL n’était pas utilisable ici, et c’est mesuré et non supposé. Son implémentation de SCRAM écrit le premier message client avec un nom d’utilisateur vide, parce que PostgreSQL envoie le nom dans son paquet de démarrage. MongoDB n’a pas ce paquet et lit le nom précisément dans ce champ. Le glisser après coup changerait les octets mêmes sur lesquels la signature porte déjà, et le serveur rejetterait notre preuve. Aucune interface ne permet de le transmettre.
Une panne que le protocole interdit de nommer
Vient la partie qu’aucun bouchon n’aurait trouvée.
RADIUS est vérifié par une requête Status-Server, qui demande au serveur s’il est vivant et rien d’autre. L’autre solution évidente, une requête d’accès avec un compte de test, a été écartée : c’est une vraie tentative de connexion sur le réseau du client, avec un vrai compte dont quelqu’un pourrait se servir, qui écrit une ligne dans son journal d’authentification au milieu de ses vrais utilisateurs. À une minute d’intervalle, cela fait mille quatre cent quarante lignes par jour qui ne disent rien de son service, et sur beaucoup d’installations un mot de passe erroné suffit à verrouiller un compte, ce qui fait que la supervision provoque exactement l’incident qu’elle devait voir venir.
Nous avons ensuite mesuré, contre un vrai serveur, ce que fait un secret partagé erroné.
Rien. Un serveur qui ne peut pas vérifier l’authentificateur de message d’une requête jette le paquet en silence, sans réponse d’erreur. C’est dans la spécification, et cela veut dire que le tableau d’une panne due à un secret erroné ne se distingue pas d’une règle de pare-feu, d’un mauvais port ou d’une machine morte.
Cela ne se corrige pas dans un adaptateur, car c’est une propriété du protocole. Ce que l’on peut faire, c’est cesser de faire semblant. Le message « pas de réponse » cite un secret partagé erroné comme cause possible, dans le texte que lit le client, pour qu’un administrateur ne passe pas un après-midi dans son pare-feu à chercher une chose qui se trouve dans un fichier de configuration. Et si le serveur répond quand même, l’authentificateur de réponse est vérifié, et un écart à cet endroit est signalé comme un fait d’authentification et non comme une erreur de protocole, car dans ce montage il en est vraiment un.
| Situation | Ce que voit le client | Pourquoi |
|---|---|---|
| Mauvais secret, pas de réponse | Pas de réponse, avec le secret comme cause possible | Le serveur a jeté le paquet ; rien n’est revenu à examiner |
| Mauvais secret, une réponse arrive | Un mauvais secret, comme résultat d’authentification | L’authentificateur de réponse ne concordait pas |
| Bon secret, pas de réponse | Pas de réponse | Le même silence, et il mérite les mêmes mots |
Cette ligne du milieu est le seul cas où la différence se voit, et ce n’est pas le cas courant.
Un serveur impossible à mesurer, dit à voix haute
La dernière mesure est celle qui nous a le moins plu.
SQL Server sous Linux fabrique un certificat au premier démarrage. Ce certificat est un X.509 version 1, une forme sans extensions qui n’est plus destinée à TLS depuis longtemps, et l’analyseur de certificats en dessous la refuse. Le mode chiffré échoue donc sur une version de certificat non prise en charge.
Il vaut la peine de préciser pourquoi ce mode vérifie encore quelque chose. L’identité n’y est pas pesée : le nom du certificat n’est pas contrôlé. La signature, si, car sans elle il ne reste pas une poignée de main mais un échange d’octets avec qui a répondu. Peser une signature suppose de lire la clé publique du certificat, et c’est précisément l’étape qui refuse.
Le résultat est donc : un SQL Server avec son propre certificat généré ne peut pas être mesuré par cette vérification. Le client lit la vraie erreur de l’analyseur comme détail, citée plutôt que paraphrasée, et le remède est un vrai certificat dans la configuration du serveur, ce qu’un administrateur qui rend son serveur supervisable fait de toute façon.
Deux choses n’ont volontairement pas été faites. La vérification de la signature n’est pas sautée dans ce mode, car cela romprait la promesse précisément sur le réglage que la plupart des gens choisissent et transformerait un refus visible en faiblesse invisible. Et il n’y a pas encore de code d’erreur dédié à ce cas, parce que le seul moyen de le reconnaître serait de comparer au texte lisible d’une bibliothèque, ce qui est une hypothèse sur le code d’autrui qui peut glisser en silence. Cela mérite une ronde à part, avec son épreuve.
Il n’y a pas non plus de mode sans chiffrement du tout, pour une raison propre à ce protocole : le mot de passe du paquet de connexion est masqué par un ou exclusif fixe et un échange de quartets. C’est un encodage, pas un chiffrement. Qui voit les octets lit le mot de passe, donc un mode non chiffré enverrait le mot de passe d’un client en clair sur son réseau, sous un nom qui suggère autre chose.
Ce que le canari a trouvé
Tout a tourné contre de vrais serveurs et non des bouchons : quatorze vérifications contre MongoDB nu et avec TLS, un vrai serveur RADIUS et un vrai SQL Server. Le résultat le plus utile est ennuyeux. Un mot de passe erroné est partout un échec d’authentification et jamais un échec de protocole. C’est cette distinction qui dit à un client s’il doit regarder ses identifiants ou son réseau, et une ronde antérieure l’avait ratée dans la vérification PostgreSQL : une erreur pendant l’étape d’authentification y devenait « ce n’est pas un serveur PostgreSQL ».
Ce que nous noterions
- Comptez ce qu’une dépendance retient, pas seulement ce qu’elle coûte. Un type de document générique fait de « nous lisons quatre champs » un accord au lieu d’une propriété.
- Un pilote apporte un constructeur de requêtes. Si la promesse est qu’aucune requête arbitraire ne peut partir, le code pour en envoyer une ne devrait pas exister dans le binaire.
- Deux piles TLS dans un binaire, c’est un problème de politique et non de taille. Celle que vous n’avez pas choisie est celle que vous ne pouvez pas corriger.
- Dans un lecteur maison, la table des sauts est la partie dangereuse. Un type inconnu doit être une erreur ; le sauter de zéro octet décale tout ce qui suit.
- Acceptez un nombre dans chaque largeur autorisée par le protocole. Un champ qui arrive en virgule flottante suffit à lire chaque échec comme un succès.
- Un repli vers un mécanisme plus faible doit être un refus, et non un repli. Du vert au-dessus d’une connexion plus faible est pire que du rouge.
- Si le protocole rend une panne indiscernable, dites-le dans le message. La formulation est le seul endroit où cette distinction subsiste.
- Certains serveurs ne sont pas mesurables, et cela va dans la documentation et non dans un contournement. Le contournement, ici, aurait été de cesser de vérifier les signatures.
Pour être clair : les trois vérifications sont construites, éprouvées contre de vrais serveurs et derrière un interrupteur de déploiement. Reste non prouvé leur passage à trois sur le fil vers un vrai agent client, la comptabilité RADIUS au-delà de la requête d’état, et SQL Server dans d’autres montages qu’une instance unique sous Linux.
Issu du travail de plateforme sur Provibr. Comment la même approche par canari a révélé deux défauts dans les vérifications de bases de données précédentes est expliqué dans un canari qui tourne contre de vrais serveurs, et comment les identifiants utilisés par ces vérifications parviennent à l’agent dans un secret qui ne voyage qu’une fois.