La demande tenait dans une capture d'écran d'Easypanel : un bouton « Enable/Disable Auto Deploy » et une URL de webhook qu'on peut appeler de n'importe où. sh0 déployait déjà sur push Git. Ce n'est pas la même chose, et la différence tient à la question de savoir pour qui sh0 existe.
C'est l'utilisateur qui décide de la forme
Le premier public de sh0, c'est la clientèle de l'hébergeur : plus de 1 200 développeurs venus de cPanel. Ils
livrent leur propre code sous forme de Dockerfile ou d'archive, et ils n'utilisent presque pas GitHub. Un
webhook qui n'accepte qu'une signature GitHub, GitLab ou Bitbucket ne leur sert à rien. Jusqu'à cette session,
leur seul déclencheur à distance était POST /apps/:id/deployments avec une clé d'API, et une clé d'API est un
identifiant de compte : elle ouvre toutes les applications auxquelles son propriétaire a accès.
Ce qu'il leur faut, c'est un point d'appel qu'un curl -X POST collé dans un job de CI, une tâche cron ou un
script de build puisse appeler, et qui ne sache rien faire d'autre que redéployer une seule application.
La conception, en quatre contraintes
- Le jeton est l'authentification, il vit donc dans le chemin. Une seule forme, jamais l'en-tête et le
- chemin. C'est ce qu'un
curlcopié-collé appelle sans aucune configuration. Le jeton fait 256 bits tirés du - générateur aléatoire du système, et il n'est affiché qu'une fois. Il est stocké sous forme de hash SHA-256 (la
- même primitive que pour les clés d'API ; avec une entropie aussi élevée, un hash lent ne sert à rien) et
- comparé en temps constant.
- Un refus ne dit rien. Une application inconnue, une URL désactivée et un mauvais jeton renvoient tous le
- même 404, à l'octet près. Quand l'application n'a pas d'URL du tout, le handler calcule quand même un hash
- contre une empreinte factice, pour que cette branche coûte autant qu'un mauvais jeton.
- Exactement le bouton Deploy. Nous n'avons pas écrit de second chemin de déploiement. Le handler du bouton a
- été découpé, et l'URL appelle la même fonction
launch_deployment. Ce refactoring a aussi corrigé une vieille - fuite : le client Docker du nœud de l'application est désormais résolu avant l'insertion de la ligne, si
- bien qu'un nœud injoignable ne laisse plus de déploiement
queuedorphelin. - Dix appels n'empilent pas dix déploiements. Chaque application garde en mémoire un seul emplacement « en
- attente ». Un appel qui trouve l'emplacement occupé rejoint ce déploiement-là. L'emplacement se libère quand le
- pipeline acquiert le verrou de déploiement de l'application. Le garder en mémoire était voulu : une ligne en
- base survivrait à un crash et pourrait bloquer l'URL pour toujours.
La preuve a tourné sur la machine de démo : dix appels lancés en parallèle pendant un déploiement en cours ont
produit exactement un déploiement supplémentaire, et neuf réponses indiquaient "coalesced": true.
Ce que l'audit a trouvé
L'agent d'audit en lecture seule a rendu un GO-WITH-FIXES. Deux de ses constats ont modifié le code avant que la release candidate ne soit produite.
Le jeton était à un niveau de log de finir dans journald. Le span par défaut du TraceLayer de tower_http enregistre
l'URI complète. Au niveau info, ce span est désactivé. Lancer sh0 avec --log-level debug pour traquer un bug
sans rapport aurait écrit en clair chaque jeton de déploiement actif. Rien dans le code de la fonctionnalité ne
journalisait le jeton. C'est le framework qui le faisait. Le correctif est un span personnalisé qui masque le
segment du jeton et n'enregistre plus du tout les query strings. La campagne de vérification l'a ensuite
contrôlé : journalctl -u sh0 | grep <token> n'a renvoyé aucune ligne.
Un développeur pouvait créer un identifiant qui lui survit. La spec disait « les rôles qui peuvent déployer
l'application ». Le problème, c'est qu'une URL de déploiement est un identifiant au porteur. Retirer le
développeur du projet ne révoque pas l'URL qu'il a créée. Émettre ou réarmer un identifiant est un acte
d'administrateur : la rotation et l'activation ou la désactivation exigent donc désormais le rôle admin du
projet, et les développeurs gardent l'accès en lecture. Le log de session consigne ce point comme une décision
prise sans le CEO, parce qu'elle s'écarte du prompt et se défait en une ligne.
L'interrupteur, prouvé par un clic
L'interrupteur « Auto deploy » de la barre d'actions écrit le drapeau auto_deploy existant. La preuve a été un
clic, pas un PATCH : un Chromium headless, piloté par CDP, a cliqué sur [data-testid=auto-deploy-toggle].
aria-pressed est passé de true à false, et l'API a ensuite lu auto_deploy=false. Un test de route ne peut
montrer qu'une chose : la route fonctionne. Le clic montre que le bouton appelle la route.
Ce qui reste ouvert
- Si l'insertion du premier déploiement échoue, un appelant qui s'y est rattaché reçoit un identifiant qui ne
- pointe vers rien. C'est rare, c'est documenté, et ça se corrige.
- Un déploiement qui ne libère jamais le verrou fige l'emplacement en attente jusqu'au redémarrage.
- Sur une installation atteinte par IP sans domaine de panneau, l'URL est en
http://, donc le jeton circule en - clair. C'est la même limite que pour les webhooks Git existants.
Chacun de ces points est écrit dans le log de session, sous « Deferred ». Aucun n'est caché derrière un « corrigé ».
Depuis, lors de la session 261120 du 17 octobre, le côté Git a rattrapé son retard : activer le déploiement automatique fait désormais créer le webhook GitHub par sh0 lui-même, et quand il n'y parvient pas, sh0 se rabat sur une interrogation périodique de la branche. La preuve de bout en bout contre un vrai dépôt GitHub reste due, et le ticket reste ouvert tant qu'elle n'est pas produite.