Aller au contenu

Blog ·

Un canari qui tourne face à de vrais serveurs

Un bouchon ne prouve pas l’authentification. Deux défauts que seuls de vrais serveurs pouvaient montrer, et ce qu’une page d’état a le droit de compter.

Par Provibr

Une sonde de supervision qui se connecte à une base de données est un petit morceau de code protocolaire doté d’une propriété désagréable : presque toutes les façons de le tester testent la mauvaise chose.

Le test évident est un serveur bouchon qui parle le protocole. Cela marche, et cela prouve pas mal de choses : le tramage, les délais, ce qui arrive quand la connexion tombe au milieu d’une poignée de main, si un refus produit le bon code d’erreur. Ce qu’il ne peut pas prouver, c’est l’authentification, et pour une raison structurelle. Un bouchon qui recalcule notre propre brouillage utilise notre propre implémentation aux deux bouts du fil. Intervertissez deux arguments dans une empreinte et les deux côtés les intervertissent, et le test reste vert sur du code qu’aucun vrai serveur n’accepterait.

Cet écart figurait comme point ouvert à la fin de la manche de supervision précédente : SCRAM n’avait jamais été mesuré que face à une implémentation que nous avions écrite aussi. Un canari le referme.

Ce qu’est un canari ici

Un canari est un petit ensemble d’épreuves qui tournent face à la chose réelle plutôt qu’à un modèle de celle-ci. Des conteneurs jetables sur notre propre serveur, liés à localhost, contenant un véritable PostgreSQL 17, un Redis 7, un MySQL 8.4 et un MariaDB 11. Les adaptateurs sont appelés directement et non via l’enveloppe de commande, car le sujet est le fil et non la plomberie autour. Onze épreuves, toutes ignorées par défaut, et chacune refuse de tourner tant que son adresse n’est pas réellement définie.

Ce second verrou ne fait pas double emploi avec le premier. Si quelqu’un lance toute la suite en levant l’indicateur d’omission, une épreuve se connecterait sinon en silence à 127.0.0.1:0 et produirait un échec qui se lit comme un bogue de notre code plutôt que comme une variable d’environnement manquante.

Ce que couvrent les onze :

ServeurCe qui est prouvé
PostgreSQL 17SCRAM avec le bon et avec un mauvais mot de passe, md5, trust, TLS exigé face à un serveur au TLS éteint, vérification complète du certificat face à une ancre étrangère
Redis 7Authentification par mot de passe et un utilisateur ACL, plus la liste blanche d’INFO
MySQL 8.4caching_sha2 avec cache froid et avec cache chaud, et un mauvais mot de passe
MariaDB 11.8La même poignée de main face à une autre implémentation de celle-ci

La distinction entre cache froid et chaud compte parce que ce sont deux chemins de code distincts. MySQL utilise dans caching_sha2_password un brouillage bon marché dès que le serveur a déjà vu le compte, et demande une authentification complète la première fois après un redémarrage. Seul un vrai serveur redémarre.

Deux défauts qu’aucun bouchon n’aurait pu montrer

PostgreSQL, mauvais mot de passe. Avec un mauvais mot de passe, PostgreSQL ne répond pas à la première réponse SASL par une continuation. Il répond par une erreur portant le SQLSTATE 28P01, mot de passe invalide. L’adaptateur avait là une déstructuration nue, qui transformait tout ce qui n’était pas une continuation en une erreur de protocole : ce service n’a pas répondu comme un serveur PostgreSQL. Le SQLSTATE partait à la poubelle. Une cliente dont les identifiants de supervision avaient expiré lisait que sa base de données était cassée.

Un bouchon ne passe jamais par cette branche, car un bouchon qui teste le chemin heureux envoie la continuation. Les deux endroits où une erreur peut désormais arriver, après la première étape et après la seconde, la maintiennent en échec d’authentification.

Redis, un champ vide. L’adaptateur lit exactement deux champs d’INFO : la version et le rôle. Il demandait la section server, et role n’est pas dans cette section. Il est dans replication. Le champ était vide depuis la manche qui l’a introduit, et rien ne le disait, car le bouchon répondait par un bloc INFO que nous avions écrit nous-mêmes et où role figurait obligeamment là où nous l’attendions.

Le correctif tient en deux appels INFO distincts et non en un appel nommant les deux sections, car Redis n’accepte plusieurs sections dans un INFO qu’à partir de la version 7.0. En 6.x c’est une erreur de syntaxe, et comme un INFO raté ne met délibérément pas la sonde au rouge, le champ serait simplement redevenu vide sur les serveurs anciens. Une dégradation silencieuse remplaçant une dégradation silencieuse.

Les deux bogues ont la même forme. Aucun des deux n’a produit d’erreur. L’un a produit la mauvaise et l’autre rien du tout, et les deux étaient invisibles pour une suite de tests qui était verte.

MySQL, et la bibliothèque qui n’a pas été ajoutée

La même manche a ajouté MySQL et MariaDB comme type de sonde. Il n’y a aucune bibliothèque cliente derrière, et c’est une répétition délibérée d’une décision antérieure et non une décision nouvelle.

Pour PostgreSQL, le choix avait été de ne pas écrire notre propre SCRAM, car SCRAM comporte une étape où le serveur signe quelque chose que le client est censé vérifier, et une implémentation qui saute cette étape est verte face à un vrai serveur et tout aussi verte face à un serveur contrefait. C’est le raccourci dangereux : il n’échoue jamais bruyamment.

MySQL n’a pas d’étape de ce genre. L’authentification native par mot de passe est une chaîne d’empreintes SHA1 et le chemin rapide de caching_sha2_password une chaîne d’empreintes SHA256, et dans aucune des deux le serveur ne signe quoi que ce soit dont nous pourrions omettre la vérification. Restent le tramage et deux empreintes, et les deux bibliothèques d’empreinte étaient déjà dans l’arbre. Aucun constructeur de requêtes n’était nécessaire non plus : il y a une requête, SELECT 1, elle est une constante, et sa réponse attendue aussi.

Cette dernière se lit comme une fonctionnalité manquante. Au-dessus d’une requête fixe, une attente configurable est un bouton dont toute position autre que la valeur par défaut fait échouer la sonde, et un tel bouton apprend à l’exploitant que les boutons ne veulent rien dire. Il y aura sa place le jour où une deuxième requête fixe existera, et pas un jour plus tôt. La version n’est pas non plus une deuxième requête : MySQL l’envoie sans qu’on la demande dans le paquet de poignée de main, et la deuxième requête est toujours celle à laquelle pousse un champ de texte libre un an plus tard.

Deux choses sont refusées net. La capacité cliente permettant à un serveur d’ordonner au client de lire un fichier sur son disque et de l’envoyer n’est pas demandée, et l’octet de réponse correspondant est une erreur de protocole plutôt qu’une branche qui fait quelque chose. Et l’authentification caching_sha2 complète ne se fait que sur TLS : l’alternative sur une ligne nue consiste à récupérer la clé publique du serveur par cette même ligne non authentifiée, et qui se trouve au milieu répond avec la sienne. Le refus nomme la réparation, à savoir activer TLS.

Une page d’état compte des ticks, pas des résultats

La même manche a posé une page d’état publique devant tout cela, et l’arithmétique de cette page demandait une décision facile à rater.

Chaque exécution due d’une règle est un tick. Un tick peut se terminer en up, degraded ou down, et il peut aussi se terminer en source_unavailable (notre propre source de mesure n’était pas là), configuration_error (la règle n’avait pas de sens), unsupported (l’agent ne connaissait pas ce type de sonde) ou manqué. Ces quatre derniers ne disent rien du service de la clientèle.

Si le dénominateur d’une disponibilité quotidienne était chaque tick dû, une panne de notre côté ferait baisser le chiffre sur la page d’état de la clientèle. Le dénominateur est donc up plus degraded plus down : les ticks où quelqu’un a réellement regardé le service. S’il n’en reste rien, la réponse pour ce jour est qu’il n’y a pas de réponse, explicitement ni zéro ni cent pour cent. Sur la page elle-même, ces quatre états apparaissent comme inconnus et jamais comme down.

Degraded compte pour moitié. C’est un choix et non une loi de la nature : un service qui répond toute la journée mais reste au-dessus de son seuil de latence n’est ni hors service ni en ordre, et le compter d’un côté ou de l’autre rend l’un des deux dénué de sens.

Les trous sont montrés comme des trous. Une règle créée la semaine dernière n’a pas quatre-vingt-dix jours d’historique, et une barre qui comble cela avec la valeur du jour voisin affirme quelque chose qui n’a jamais été mesuré.

Ce que nous a coûté une suite de tests verte

Chacun des défauts ci-dessus se trouvait sous un test réussi. La suite de bouchons n’était pas fausse ; elle répondait à une autre question que celle que tout le monde croyait. La valeur du canari n’est pas d’être une meilleure épreuve. C’est d’être une épreuve où l’autre bout du fil a été écrit par quelqu’un qui ne partage pas nos hypothèses.

Cela vaut six conteneurs jetables, et il vaut la peine d’écrire qu’ils sont jetables : ils tournent sur notre propre machine, ils sont supprimés quand ils s’arrêtent, et les épreuves refusent de tourner si les adresses ne sont pas là.

Issu du travail de supervision de Provibr. Pourquoi une sonde hébergée n’accepte qu’un seul type de message se trouve dans une sonde qu’on ne peut convaincre de rien, et pourquoi un secret de supervision ne voyage qu’une fois se trouve dans un secret qui ne voyage qu’une fois.

Commencez avec un hôte et un agent

Créez un compte, installez l’agent avec une seule commande et connectez votre premier hôte Proxmox. La période d’essai dure 14 jours et ne demande aucune coordonnée de paiement.