Aller au contenu

Blog ·

Une frontière appartient au noyau

Révoquer un agent hébergé le détachait de sa commande et laissait tourner une machine Proxmox injoignable que tout le monde continuait de payer.

Par Provibr

Un Cloud Agent est la version de notre agent dont nous exploitons la machine. Le client en commande un, nous clonons un modèle Proxmox, nous y installons l’agent et nous le facturons au mois. Depuis le panneau, il ressemble à n’importe quel autre agent, et cette ressemblance est toute l’histoire de cette ronde.

Tout autre agent appartient au client. Le révoquer est une opération propre et sûre : la plateforme envoie un avis de révocation, l’agent efface son identité, son certificat et son coffre d’identifiants, et la ligne peut ensuite être supprimée. Rien ne reste en marche nulle part, parce que la chose qui tournait n’a jamais été la nôtre.

Pour un Cloud Agent, ce n’est pas vrai, et rien ne l’empêchait.

Ce que nous avons mesuré avant de changer quoi que ce soit

Sur la base de développement, avec une vraie commande derrière un vrai agent, nous avons appelé le chemin de suppression exactement comme le fait le bouton du panneau.

Il a rapporté mode: "hard". La ligne de l’agent avait disparu. La ligne de commande était toujours là, toujours active, portant toujours le vmid 5998, et son agent_id était discrètement devenu NULL, parce que la clé étrangère de la commande vers l’agent est en ON DELETE SET NULL.

Il y avait donc une machine virtuelle sur un hyperviseur, en marche, avec dessus un agent qui avait effacé sa propre identité et ne pourrait donc plus jamais se connecter, rattachée à une commande qui produisait toujours une facture mensuelle, et aucune ligne nulle part qui pointait de l’une vers l’autre. Injoignable, impossible à arrêter depuis le panneau, et pas gratuite.

La contrainte qui a produit cela est correcte. Mettre la référence à NULL est le bon comportement pour une colonne qui dit quel agent est actuellement installé ici ; une cascade aurait supprimé la commande et l’enregistrement de facturation avec elle. Le défaut est que rien ne posait la question que le schéma ne pouvait pas poser.

Deux garde-fous existants, tous deux tournés du mauvais côté

Supprimer un agent avait déjà deux refus, et il vaut la peine d’être précis sur la raison pour laquelle aucun des deux ne s’est déclenché.

Le premier refuse de supprimer un agent actuellement inscrit : révoquez-le d’abord, pour qu’il fasse son ménage. Au moment où la ligne était supprimée, l’agent venait d’être révoqué dans le même appel, il n’était donc plus inscrit et le contrôle était satisfait.

Le second refuse de supprimer un agent auquel pendent encore des intégrations ou des serveurs, parce que ces clés étrangères sont en cascade et qu’un seul clic emporterait tout l’inventaire. Un Cloud Agent n’a ni les unes ni les autres : c’est une machine neuve qui existe pour faire tourner l’agent, le contrôle est donc passé parce qu’il n’y avait rien à compter.

Les deux garde-fous portaient sur ce que la suppression détruirait. La chose qui survit à la suppression, et qui coûte de l’argent précisément parce qu’elle survit, était en dehors de la forme de la question.

La frontière va dans le noyau, pas dans le bouton

La réparation évidente consiste à contrôler dans l’action serveur derrière le bouton. Elle est aussi fausse, et la raison est une brèche dans laquelle cette base de code est déjà tombée avec un point d’entrée d’authentification standard qui contournait tous les contrôles côté serveur.

Nous avons compté les points d’appel. En dehors des deux fonctions du noyau, il y a sept appels à la révocation, à la suppression ou à la suppression combinée, répartis sur trois fichiers : trois actions serveur sur la page des agents, l’action groupée, et deux routes de l’API v1. Un contrôle dans l’action du panneau aurait laissé les routes d’API ouvertes, et une route d’API est exactement ce qu’utilise le script d’un client.

La frontière est donc à l’intérieur de revokeAgentCore() et de deleteAgentCore() eux-mêmes, le point de passage obligé par lequel passent les sept appels. Une fonction, interrogée deux fois.

Elle se place après la branche idempotente et non avant. Un agent déjà révoqué n’a plus rien à révoquer, et dire à cet appelant d’annuler d’abord sa commande serait un mensonge à propos d’un appel qui, de toute façon, ne fera rien. Une frontière appartient à l’opération qui change réellement quelque chose.

Le prédicat est peut-on encore annuler, pas est-ce vivant

Il existe dans le paquet partagé une liste nommée CLOUD_AGENT_LIVE_STATUSES : les états dans lesquels une machine existe et coûte de l’argent. Elle compte quatre membres et c’est exactement la mauvaise liste à réutiliser ici.

Le prédicat que nous employons est cloudAgentActionAllowed("cancel", status), qui est vrai pour sept des huit états de commande, tout sauf une commande déjà annulée.

Les trois états qui font la différence sont les intéressants, et failed est le plus tranchant. Une commande en échec ne coûte rien, elle n’est donc pas vivante. Mais une commande en échec est précisément celle qui a pu aller assez loin pour créer une machine avant de tomber, ce qui est la raison même pour laquelle l’annulation est permise depuis cet état. Réutiliser la liste de facturation aurait laissé sans protection le seul cas comportant une machine orpheline, et cela aurait eu l’air parfaitement raisonnable en relecture.

Deux listes qui veulent dire à peu près la même chose divergent. La question ici n’est pas est-ce que cela coûte de l’argent, c’est l’annulation est-elle encore la voie, et exactement une fonction dans la base de code répond à celle-là.

Deux chemins d’écriture autorisés à s’en passer

Le garde-fou que nous avons écrit dérive chaque endroit de la source du panneau qui révoque, supprime durement ou masque une ligne d’agent, au moyen d’un parcours de l’arbre syntaxique des arborescences applicative et de bibliothèque, et exige par point d’appel soit la frontière, soit une raison écrite. Deux points portent une raison : le chemin d’annulation lui-même, qui supprime la machine, libère l’adresse et seulement ensuite révoque l’agent, et la clôture de compte, qui révoque chaque agent d’une organisation et traite le Cloud Agent séparément dans le même flux. Bloquer l’un ou l’autre fermerait une sortie correcte au lieu d’économiser de l’argent.

Le garde-fou possède les deux tamis qui empêchent un analyseur de source d’être vert par vacuité : une borne inférieure sur le nombre de points d’écriture qu’il doit trouver, dont trois nommés, et un test qui échoue sur une entrée de liste d’exceptions pointant vers un fichier qui n’écrit plus rien. Il exécute 4 tests et 11 assertions.

Deux faits à l’écran, pas un

Le côté panneau avait besoin de deux booléens distincts et nous avons failli n’en livrer qu’un.

orderOpen est un fait sur la commande : peut-elle encore être annulée. canCancel est un fait sur la personne qui regarde : a-t-elle le droit de l’annuler. Les fondre en un unique puis-je agir ici signifierait qu’un membre dépourvu du droit d’annulation voit le bouton de révocation, s’en sert, jette la machine et ne peut pas arrêter la facture. Les boutons disparaissent donc sur le fait de la commande, et la carte reste avec la raison à l’intérieur, parce que pourquoi ne puis-je pas révoquer celui-ci est la question que pose toute personne ayant vu le bouton sur ses autres agents.

Une chose a suivi parce que le changement l’a rendue fausse : les instructions de désinstallation conseillaient de révoquer d’abord l’agent dans le panneau. C’est un bouton qui n’existe plus sur cette page.

Ce que nous noterions

  1. Une ressemblance dans l’interface n’est pas une ressemblance dans les conséquences. Deux agents, un bouton, et l’un des deux coûte de l’argent une fois que vous avez appuyé dessus.
  2. ON DELETE SET NULL peut être juste et vous laisser quand même un orphelin. Le schéma ne peut pas poser la question.
  3. Des garde-fous portant sur ce qu’une suppression détruit ne verront pas ce qu’une suppression abandonne.
  4. Comptez les points d’appel avant de choisir où placer le contrôle. Les nôtres étaient sept, dans trois fichiers, dont deux routes d’API.
  5. Placez la frontière au point de passage obligé, pas sur le bouton. Un bouton n’est qu’une entrée parmi d’autres.
  6. Placez-la après la branche idempotente. Un refus sur un appel qui ne ferait rien est un mensonge.
  7. Réutiliser une liste qui veut dire à peu près la même chose est une panne lente. Coûte de l’argent et peut encore être annulé diffèrent de trois états, et c’est là que se trouvent les dégâts.
  8. Un état qui ne coûte rien peut tout de même avoir laissé quelque chose derrière lui. Un provisionnement en échec est le dangereux.
  9. Séparez le fait sur l’objet du fait sur la personne qui regarde. Fusionnés, ils tendent un bouton destructeur à un membre restreint.
  10. Laissez le garde-fou lire l’arbre. Un test par entrée connue ne peut pas voir l’entrée que quelqu’un ajoutera le mois prochain.

Pour être clair sur ce dont il s’agit : le défaut a été reproduit contre la base de développement avant tout changement, la vérification de bout en bout s’est terminée à 26 verts et avait été prouvée rouge à 16 verts et 10 échecs auparavant, la vérification en navigateur à 29 verts et prouvée rouge à 18 et 11 avec l’indicateur de commande forcé à l’arrêt, les neuf langues à 178 verts, et la vérification de fusion a tourné sans ligne d’échec. Non prouvé : aucune machine de client réel n’a été impliquée, et le côté facturation a été inspecté plutôt qu’éprouvé. Trois pièges de mesure ont coûté du temps et méritent d’être nommés : la zone dangereuse vit derrière un paramètre d’onglet, si bien qu’une vérification qui l’omet mesure zéro bouton et lit cela comme un succès ; une séquence d’échappement par barre oblique inverse à l’intérieur d’un gabarit remis à une évaluation de page n’est pas un échappement ; et l’un des trois scripts de catalogue a aplati une espace insécable que les deux autres préservaient.

Issu du travail de plateforme sur Provibr. Pourquoi une machine ne peut être vendue qu’une seule fois est dans Deux commandes, une machine, pourquoi un raccourci est une référence et jamais une permission est dans Un raccourci est une référence, pas un droit, et un mot de statut emprunté à la mauvaise table est dans Un scan qui fermait son propre créneau.

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.