La contribution open source comme décision d’architecture et orientation produit, retour d’expérience sur OpenBao

La difficulté en architecture logicielle ne réside pas uniquement dans le fait de trouver la solution à un problème, mais dans le fait de déterminer également où cette solution doit vivre dans le système. C’est précisément la question que j’ai été amené à me poser avec des collègues qui a donné lieu à cette issue sur le projet OpenBao: https://github.com/openbao/openbao/issues/3533

Le problème du certificat renouvelé, mais jamais rechargé

Dans le cadre de l’initialisation d’une nouvelle plateforme (bootstrap), l’instance d’OpenBao doit démarrer avant l’initialisation de la PKI avec une CA intermédiaire. Le service, pour démarrer a donc été créé avec un certificat auto-signé. Une fois OpenBao initialisé, la PKI intermédiaire a pu être configurée. Le certificat provisoire devait être remplacé par un certificat émis par cette PKI, puis renouvelé automatiquement par cert-manager. C’est là que nous avions rencontré un problème, le certificat TLS présenté par OpenBao devait pouvoir être rechargé après sa rotation, sans nécessiter le redémarrage du service.

OpenBao peut utiliser un certificat et une clé privée stockés dans des fichiers pour sécuriser son listener TCP :

listener "tcp" {
  tls_cert_file = "/openbao/tls/tls.crt"
  tls_key_file  = "/openbao/tls/tls.key"
}

Au démarrage, le serveur lit ces fichiers, construit la paire TLS, puis la conserve en mémoire. Le problème apparaît lorsque le certificat est renouvelé pendant que le processus continue de fonctionner. Les fichiers présents sur le disque contiennent le nouveau certificat, mais OpenBao continue de présenter celui qu’il avait chargé au démarrage.

Des états incohérents

Sur Kubernetes en utilisant le cert-manager, on peut se retrouver dans une situation où:

  • cert-manager a correctement renouvelé le certificat
  • le Secret Kubernetes contient la nouvelle version
  • le volume monté dans le pod expose les nouveaux fichiers
  • OpenBao conserve l’ancien certificat en mémoire Pour la plateforme, la rotation est réussie. Par contre pour le client qui établit la connexion TLS, rien n’a changé.

Ce non changement peut rester invisible pendant plusieurs jours, jusqu’à l’expiration du certificat encore conservé en mémoire. Le service commence alors brutalement à refuser les connexions, alors même que le nouveau certificat est disponible sur le disque.

L’invariant que le système doit garantir

Le problème technique est simple à formuler puisque c’est simplement que le processus ne relit pas automatiquement les fichiers. Et l’invariant étant que le listener doive continuer à présenter la dernière paire de certificat valide, puis converger automatiquement vers la nouvelle dès qu’elle est complète.

Comment déterminer le composant qui doit garantir l’invariant?

En l’absence de mécanisme de rechargement natif, cette opération de rotation dépend donc d’un déclencheur externe : envoi d’un signal SIGHUP, ajout d’un sidecar surveillant les fichiers, exécution périodique d’un CronJob, utilisation d’un hook ou redémarrage du pod.

Ces stratégies déplacent la responsabilité du cycle de vie du certificat vers des composants externes au service. Elles augmentent le couplage entre l’application et son environnement d’exécution, et complexifient de mon point de vue l’exploitation. La rotation des certificats TLS est de plus en plus fréquente et doit donc rester une opération transparente, or dans la disposition actuelle avec un déclenchement externe, une interruption du service est d’autant plus probable, et nécessiterait l’intervention potentielle d’un administrateur (restart du pod avec verrouillage Shamir si pas d’auto-unseal par exemple).

Pour répondre à notre problème de boostrap, plusieurs options étaient sur la table:

  • ajouter un sidecar chargé de surveiller les fichiers puis envoyer un signal au processus
  • déclencher un redémarrage
  • ou faire évoluer directement l’application

Toutes ces approches peuvent fonctionner. Mais elles ne placent ni la responsabilité, ni la complexité, ni le coût de maintenance au même endroit.

Dans Kubernetes, le sidecar doit pouvoir accéder au processus OpenBao pour lui transmettre un signal. Ce qui passe par l’activation de l’option shareProcessNamespace: true, qui permet aux conteneurs d’un même pod de partager leur espace de noms de processus. Ce contournement augmente la complexité opérationnelle du déploiement et modifie également les propriétés d’isolation du pod.

En externalisant l’émission d’un signal pour la mise à jour du certificat, le comportement ne dépend plus directement de l’état réel des fichiers mais suppose qu’un événement externe sera correctement généré, transmis et traité. Ce qui laisse apparaitre de nouvelles questions:

  • Comment rejouer l’événement en cas d’échec ?
  • Comment vérifier que le signal a réellement été pris en compte ?
  • Qui est responsable de la cohérence entre l’état du disque et l’état du listener ?
  • Le mécanisme converge-t-il automatiquement vers l’état attendu après une erreur transitoire ?
  • Que se passe-t-il lorsque les fichiers sont modifiés de manière partielle ? La rotation du certificat et de la clé n’est pas nécessairement atomique. Le certificat peut être écrit avant la clé, ou inversement. Pendant quelques instants, les deux fichiers présents sur le disque peuvent ne pas former une paire valide. Ce qui fait apparaitre une règle intéressante, la détection d’un changement ne doit pas être confondue avec l’acceptation d’un nouvel état (détection d’une nouvelle version, chargement et validation hors de l’état actif, remplacement de l’état courant uniquement lorsque la nouvelle version est valide). Pour moi il y a un sujet ici qui nécessite de distinguer le flux de données du flux de contrôle. Le problème ne se limite pas au rechargement des fichiers, il concerne également leur validation, qui relève du flux de contrôle et garantit la convergence du système vers l’état attendu.

D’autres solutions existent comme Stakater Reloader qui peut surveiller les Secrets et déclencher automatiquement un rolling restart des workloads, mais un redémarrage n’est pas toujours neutre comme énoncé précédemment.

Aucune de ces solutions n’est intrinsèquement mauvaise. Mais chacune déplace la responsabilité vers l’environnement d’exécution et oblige chaque intégrateur à reconstruire autour d’OpenBao une capacité similaire. Comme toujours en architecture il est question de compromis. Parfois, la bonne solution consiste à adapter l’infrastructure. Parfois, il faut définir un processus avec une procédure d’exploitation. Et parfois, la décision la plus pérenne consiste à faire évoluer le logiciel lui-même. C’est ce que nous avons pu faire grâce à l’open source.

Le besoin initial semblait très lié à Kubernetes et à cert-manager. Mais en prenant un peu de recul ça faisait ressortir une limitation plus générale liée au logiciel.

Toutes les limites rencontrées dans un logiciel open source ne justifient évidemment pas une contribution au produit. Certaines sont propres à une organisation, à une plateforme… D’autres seraient trop coûteuses à maintenir par rapport au bénéfice attendu.

Avant de faire évoluer un projet, plusieurs questions peuvent aider à prendre la décision de qui doit porter la responsabilité:

  • Le problème est-il réellement générique ?
  • Plusieurs utilisateurs peuvent-ils être amenés à user des mêmes solutions de contournement ?
  • Le produit dispose-t-il déjà d’une partie du mécanisme qui interviendra dans la solution ? Par exemple dans notre cas, le serveur disposait déjà d’un mécanisme de rechargement déclenché par un signal SIGHUP sur lequel on pouvait capitaliser.
  • Le périmètre de la modification est-il suffisamment délimité ? L’application est la seule couche capable de décider ce qu’il faut faire lorsque la nouvelle paire est invalide. Mon parti prix est de suivre le principe Tell, Don’t Ask pour lequel il est préférable de placer le comportement auprès des données et de l’état sur lesquels il agit. Faire évoluer un projet open source quelqu’il soit est une décision d’architecture à part entière qui permet de supprimer une complexité locale, déléguer la responsabilité en mutualisant la maintenance avec la communauté, proposer un comportement cohérent entre plusieurs environnements…

La solution finale est probablement plus robuste que le contournement que nous aurions mis en place. Et l’ensemble des échanges sont à retrouver ici: https://github.com/openbao/openbao/pull/3530 Ce que je trouve intéressant, c’est que d’un problème concret, rencontré sur le terrain dans un contexte local, a pu être généralisé et conduire à l’ajout d’une nouvelle fonctionnalité.

Publié le
Catégorisé comme tech

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *