
🌐 Aussi en: English · Deutsch · Español
Une migration se profile, et je tourne autour depuis un moment. Une instance Nextcloud que j’administre tourne encore en installation native tout ce qu’il y a de plus classique – paquets, serveur web, PHP-FPM, le tout géré à la main sur une seule machine. Elle déménage sur la stack K3s que je construis dans Ansible.
La vraie raison de ce déménagement, c’est que l’installation native a vieilli. Elle fonctionne toujours, mais elle a accumulé dix ans de petites décisions que personne n’a notées, et chaque mise à jour est un événement plutôt qu’une routine. Ce que j’attends du côté K3s, c’est une mise à jour plus ennuyeuse : la version de Nextcloud et celle de chaque conteneur figées dans le code Ansible, si bien qu’une mise à jour devient un tag modifié dans un fichier que je peux relire, déployer deux fois exactement de la même façon et remettre en arrière si ça tourne mal. L’application et les données se retrouvent à des endroits réellement séparés au lieu d’être entremêlées dans une seule arborescence. Et Collabora devient un conteneur à part entière, qui parle à Nextcloud via une interface définie au lieu d’être câblé dans la même installation – une chose de moins qui casse parce que deux applications partagent le même environnement d’exécution.
Rien de tout cela n’est une supposition, et c’est la principale raison pour laquelle je suis prêt à le faire. Ce blog tourne sur la même stack K3s depuis un bon moment, et le mettre à jour s’est révélé exactement aussi ennuyeux que je l’espérais : changer un tag de conteneur, relancer le playbook, terminé. Comme le playbook est idempotent, le relancer est un non-événement – c’est la même opération, que le serveur soit neuf, à moitié configuré ou déjà correct, et cela supprime la plupart des raisons d’hésiter avant de l’exécuter. Tout ce que ce blog a accumulé en chemin – le durcissement, la supervision, l’ingress et la gestion des certificats, l’optimisation des performances, les petites corrections qui m’ont chacune coûté une soirée – est du code que je peux tout simplement pointer vers le côté Nextcloud. C’est au fond ce qu’est cette migration : prendre ce que le projet sans enjeu m’a appris et l’investir dans celui qui compte.
Avant d’infliger ça à une instance avec de vrais utilisateurs, j’ai d’abord tout déroulé de bout en bout sur un serveur jetable : restaurer les données, restaurer la base, suivre le chemin des versions, casser des choses, comprendre pourquoi. Trente-six fichiers et mille cent lignes de modifications plus tard, le code Ansible est nettement meilleur qu’au départ.
Tout est sur GitHub – playbooks, rôles, durcissement, vérification. Le côté Nextcloud en particulier a reçu beaucoup d’attention ces derniers temps.
Mais ce dont je veux vraiment parler, ce ne sont pas les correctifs. C’est la forme des problèmes.
🎭 Tout a échoué en se faisant passer pour autre chose
Aucun de ces problèmes ne s’est annoncé. Chacun est arrivé déguisé en un autre problème, plus plausible – en général un problème qui m’aurait envoyé chercher complètement au mauvais endroit.
L’installation d’un paquet a échoué sur une erreur de permissions qui n’avait rien à voir avec les permissions. chmod: Operation not permitted, en root, avec toutes les capabilities, sur un système de fichiers monté normalement. J’ai vérifié tout ce que le message désignait, et tout était en ordre. La vraie cause était une directive de durcissement systemd sur le démon SSH – RestrictSUIDSGID=yes – dont le filtre seccomp est hérité par chaque processus descendant de sshd. Autrement dit, chaque session SSH, et chaque commande qu’Ansible exécute à travers l’une d’elles.
Voilà la leçon à retenir : un durcissement appliqué à sshd n’est pas un durcissement au niveau du service. sshd est une usine à processus pour sessions interactives, donc tout ce que vous y restreignez, vous le restreignez pour chaque humain et chaque outil qui passe la porte. Pendant un moment, j’ai sincèrement soupçonné l’hébergeur d’avoir verrouillé quelque chose.
Une exécution de playbook s’est « figée » à l’étape du pare-feu. Pas d’erreur, pas de timeout, juste le silence pendant des minutes. Le rôle bascule SSH sur un port non standard et active nftables – sur la connexion même qu’il est en train de reconfigurer. Le nouveau jeu de règles accepte les connexions établies, ce qui semble complet, sauf qu’au tout premier démarrage de nftables, conntrack n’a aucune trace d’une connexion antérieure à lui. La session ne correspondait donc à aucune règle et ses paquets étaient tout simplement jetés. Pas refusés – jetés. Pour Ansible, c’est rigoureusement identique à une tâche qui prend du temps.
Un site était totalement injoignable, et le navigateur ne disait rien d’utile. Ce contrôleur d’ingress répond à un nom d’hôte dont le certificat TLS n’est pas prêt en refusant purement et simplement le handshake – aucun certificat, pas même un autosigné. Vu de l’extérieur, impossible de distinguer cela d’un serveur en panne. La vraie cause se trouvait trois couches plus loin : une liste d’images autorisées bloquait le pod de challenge de cert-manager, si bien que le certificat ne pouvait jamais être émis.
Et mon préféré : une protection contre le brute force qui ne protégeait rien. Les jails fail2ban qui surveillent les connexions web lisent le journal de l’ingress via un chemin contenant l’ID du pod. fail2ban résout ce chemin une seule fois, au démarrage. Chaque fois que le pod d’ingress est recréé – mise à jour d’image, helm upgrade, redémarrage –, les jails continuent de tourner sur un chemin qui n’existe plus :
Status for the jail: nginx-k3s-nc-login
|- Filter
| |- Currently failed: 0
| `- File list:Une liste de fichiers vide. Activé, en marche, et structurellement incapable de bannir qui que ce soit. Le seul contrôle de supervision existant demandait uniquement si le processus fail2ban était vivant – et il l’était, et bien vivant.
🩹 Ce qui en est sorti
La plupart des correctifs sont banals une fois qu’on sait quel était le problème – c’est généralement comme ça que ça se passe :
- La bascule SSH et pare-feu se fait désormais via un unique redémarrage sur un serveur neuf, de sorte que les deux démarrent ensemble depuis un boot propre au lieu d’être modifiés sous une connexion active.
- Tout nom d’hôte qui n’a pas encore de vrai certificat reçoit un certificat provisoire autosigné de courte durée, si bien qu’un avertissement du navigateur remplace une socket morte. Un avertissement vous dit que le serveur est vivant et que le problème vient du certificat ; une socket morte ne vous dit rien.
- Deux nouveaux contrôles de supervision : l’un pour les certificats proches de l’expiration, l’autre qui vérifie que les jails fail2ban surveillent réellement un fichier au lieu de simplement tourner.
- Plus la moitié ennuyeuse – un expéditeur de mail assemblé à partir de la mauvaise moitié d’une adresse (ce qui faisait carrément tomber Grafana au démarrage), un rôle de vérification qui interrompait tout le play avec une erreur de parseur précisément quand quelque chose était cassé, et des en-têtes de sécurité définis deux fois avec deux valeurs différentes.
⬆️ Une chose à savoir avant de planifier une fenêtre de maintenance
Nextcloud refuse de sauter des versions majeures. Faire passer une instance restaurée de 32 à 34 implique une halte en 33, avec une étape de mise à jour à chaque arrêt.
Deux détails sur lesquels la documentation n’insiste pas : le conteneur lance sa propre mise à jour au démarrage et n’accepte aucun trafic tant qu’elle n’est pas terminée, donc lancer la mise à jour à la main ne fait qu’entrer en concurrence avec celle déjà en cours – changez le tag de l’image et attendez. Et vérifiez la liste des applications après chaque version majeure, pas une seule fois à la fin. Certaines applications se désactivent temporairement puis reviennent ; d’autres sont réellement incompatibles et restent désactivées. L’une des miennes est carrément supprimée au lieu d’être mise à jour, parce qu’elle a été installée par téléchargement direct plutôt que via l’App Store.
🧱 Ce que je n’ai délibérément pas changé
Pendant un temps, j’ai été tenté de faire la mise à jour d’AlmaLinux 9 vers 10 dans la même fenêtre. Nouveau serveur, nouveau départ, autant commencer directement sur la version majeure actuelle, non ?
Je m’en suis dissuadé, et je pense que le raisonnement se généralise. Cette migration change déjà beaucoup de choses à la fois : le modèle de déploiement (bare metal vers K3s), la version de Nextcloud, la façon dont Collabora est exploité, et la machine sous tout cela. Ajouter un saut de version majeure de l’OS, c’est ajouter une seconde variable totalement indépendante. Si quelque chose se comporte bizarrement deux semaines plus tard – une anomalie de performances, un problème de permissions subtil, quelque chose dans SELinux –, je n’aurais aucun moyen de savoir lequel des deux changements en est la cause. Le débogage fonctionne en gardant le reste constant, et une migration est précisément le moment où il vous reste le moins de constantes.
Et rien ne presse. AlmaLinux 9 est supporté jusqu’en 2032. Ce n’est pas une échéance autour de laquelle je dois m’organiser.
Mais la raison la plus intéressante est celle qui n’est apparue qu’en construisant cette stack. Dans une architecture à conteneurs, le système d’exploitation de l’hôte pèse bien moins lourd qu’avant. Tout ce qui détermine réellement le comportement du service – la version de Nextcloud, PHP, le serveur web, Collabora – vit dans des images de conteneurs dont je fige les versions dans le code Ansible. L’hôte fournit un noyau, systemd, K3s et le durcissement autour. Les décisions intéressantes sont montées d’une couche, et ce qui se trouve en dessous est devenu relativement interchangeable.
C’est une prise de conscience un peu étrange : l’un des avantages de cette migration, c’est qu’elle rend la prochaine migration moins importante. La mise à jour de l’OS devient un exercice séparé et ennuyeux, que je peux faire selon mes propres termes, par une soirée tranquille, sans rien d’autre en cours – exactement ce que devrait être une mise à jour d’OS.
🔒 La seule chose que la répétition n’a pas pu résoudre
Il y a un problème que je n’ai pas du tout pu contourner sur le serveur de test : obtenir des certificats Let’s Encrypt valides.
La raison est structurelle plutôt que technique. Le challenge HTTP-01 fonctionne ainsi : Let’s Encrypt résout le domaine et récupère un jeton en HTTP – depuis l’adresse vers laquelle pointe l’enregistrement A public. Or c’est toujours l’ancien serveur, et ce le sera jusqu’au moment où je bascule le DNS. J’ai simulé le DNS en local avec une entrée /etc/hosts pointant vers la nouvelle machine, ce qui suffit à convaincre mon propre navigateur mais ne sert strictement à rien pour Let’s Encrypt, qui fait sa propre résolution sur le DNS public. Le nouveau serveur ne peut donc pas détenir de certificat valide pour un nom d’hôte dont il n’est pas encore officiellement responsable.
Il existe une vraie réponse à cela : le challenge DNS-01 prouve le contrôle du domaine en publiant un enregistrement TXT sur _acme-challenge.<domain> au lieu de servir un fichier. Peu lui importe où pointe l’enregistrement A, donc les certificats pourraient être émis à l’avance et prêts dès le premier jour. Il faut pour cela un fournisseur DNS doté d’une API que le client ACME peut piloter, et câbler tout ça dépasse ce que je veux entreprendre pour cette migration.
J’accepte donc ce trou : pendant quelques minutes après la bascule DNS, le nouveau serveur sera joignable sans certificat valide. C’est exactement la situation pour laquelle le certificat provisoire évoqué plus haut a été conçu – au lieu d’un handshake refusé qui ressemble à une panne, les visiteurs voient un avertissement du navigateur, qui dit au moins le serveur est là et le certificat n’est pas encore prêt. Pas élégant. Mais un trou connu, borné et visible vaut mieux qu’un trou invisible – ce qui est plus ou moins le thème de tout ce qui précède.
📅 La suite
La vraie migration a lieu dans les prochains jours.
Le plan pour le jour J est volontairement banal :
- Un ou deux jours avant : baisser le TTL DNS, pour que la bascule se propage ensuite en quelques minutes plutôt qu’en quelques heures.
- Ancien serveur en mode maintenance. À partir de là, plus rien ne change sous mes pieds, et aucun client ne peut écrire dans l’instance que je m’apprête à copier.
- Dernière sauvegarde de l’ancien serveur – données et base de données – avant de toucher à quoi que ce soit.
- Dernière synchronisation vers le nouveau serveur, pour combler l’écart apparu depuis la copie de répétition.
- Basculer le DNS – A et AAAA. Oublier l’enregistrement IPv6 est un excellent moyen de laisser la moitié de vos utilisateurs sur l’ancienne machine alors que tout semble parfait depuis votre propre portable.
- Attendre les certificats, qui ne peuvent être émis qu’à ce moment-là (voir plus haut).
- Tester : connexion, synchronisation desktop dans les deux sens, partages toujours intacts, Collabora qui ouvre un document, envoi de mails.
- Laisser l’ancien serveur exactement là où il est, en mode maintenance, comme solution de retour arrière.
Cet ordre compte plus qu’il n’y paraît. Comme l’ancien serveur passe en mode maintenance au lieu d’être éteint ou effacé, il reste là, complet et inchangé – données, base, configuration, tout exactement comme au moment où je l’ai arrêté. Si la nouvelle stack s’avère être un désastre pour une raison que je n’ai pas anticipée, le chemin du retour consiste à repointer les enregistrements DNS vers l’ancienne machine et à lever le mode maintenance. Pas de restauration, pas de perte de données, rien à remonter, parce que rien n’a jamais été démonté. L’ancien serveur n’est pas décommissionné le jour de la migration ; c’est mon retour arrière, et il le reste jusqu’à ce que le nouveau ait fait ses preuves pendant un certain temps.
La seule partie que je ne peux vraiment pas prévoir, c’est la propagation DNS. Le TTL indique aux résolveurs combien de temps ils peuvent mettre un enregistrement en cache, et le baisser un ou deux jours avant raccourcit considérablement la traîne – mais c’est une borne supérieure, pas une promesse. Bon nombre de résolveurs l’arrondissent vers le haut, certains l’ignorent, et quelques clients s’accrochent à une adresse jusqu’à ce que quelque chose redémarre. En pratique, il y a donc une fenêtre pendant laquelle certains utilisateurs atteignent le nouveau serveur et d’autres atterrissent encore sur l’ancien. C’est inconfortable pour un service de synchronisation de fichiers en particulier, parce que des écritures pourraient atterrir d’un côté comme de l’autre. C’est la principale raison pour laquelle l’ancienne instance reste en mode maintenance au lieu de simplement continuer à tourner : un utilisateur qui n’a pas encore basculé voit une page de maintenance, ce qui est agaçant mais inoffensif, au lieu de téléverser avec succès un fichier sur un serveur que plus personne ne regarde.
J’aimerais dire que grâce à la répétition tout se passera sans accroc, et je l’espère. Mais pour être honnête, une répétition sur un serveur jetable avec des données copiées n’est pas la même chose que l’instance en production avec de vrais utilisateurs, de vrais partages et de vrais clients de synchronisation qui ont leur avis sur ce qui a changé pendant qu’ils ne regardaient pas. Il y aura quelque chose. Il y a toujours quelque chose.
Donc : je ferai un compte rendu ici après coup – ce qui a vraiment mal tourné, ce que la répétition n’a pas su prévoir et ce que j’ai dû changer. Et quoi que ce soit, cela repartira directement dans le code Ansible du dépôt, parce qu’un playbook qui ne fonctionne que sur le serveur sur lequel on l’a testé n’est pas terminé.
Si le prochain article est court et ennuyeux, c’est que tout s’est bien passé.
Le motif sous-jacent
Avec le recul, une chose relie tout cela.
Aucun de ces problèmes n’était une erreur. C’étaient des absences – une jail sans fichier, un certificat jamais émis, une connexion qui a cessé de répondre au lieu de refuser, un filtre bloquant un appel système que personne n’avait pensé à chercher. Chacun a produit une sortie impossible à distinguer d’un fonctionnement parfaitement normal.
Voilà ce qu’une répétition vous apporte vraiment. Pas la certitude que ça marchera – ça, personne ne peut l’avoir. Juste une occasion bon marché de rencontrer les problèmes silencieux avant qu’ils ne coûtent quelque chose.




