alpinux-owni/docs/certificats.md
Cédrix 89cc919906 Absorber alpinux-infra : un seul dépôt pour la machine
Il y avait deux dépôts d'infrastructure et aucune frontière entre eux. La
seule distinction défendable — « documentation » d'un côté, « fichiers de
configuration » de l'autre — ne tenait plus : infra avait fini par contenir
plus de documentation qu'owni, pendant qu'owni décrivait du concret (IP,
bases, comptes).

Tout vient donc ici : conf/ (vhosts de référence), dns/ (export de zone),
services/ (units systemd), scripts/ (sauvegarde) et docs/ (courrier sortant,
certificats, sauvegardes, et le déploiement par service).

Dans ce sens plutôt que l'inverse parce qu'« owni » nomme la machine, là où
« infra » ne dit pas de quoi il s'agit — et c'est le nom que Cédric emploie
spontanément, y compris pour le dossier de secrets.

Les renvois croisés entre les deux dépôts deviennent des liens internes : un
lien vers un dépôt qu'on s'apprête à archiver aurait pourri en silence.

Reste une incohérence, signalée plutôt que corrigée à la hâte :
docs/admin.md, static.md, wiki.md, dynamic.md et proxy-calendar.md décrivent
le déploiement de sites, pas la machine. Ils ont la même place ici que celle
que la messagerie n'avait pas — ils devraient rejoindre leurs dépôts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SFbwnJurBwTs7x7t93ecku
2026-09-27 12:55:45 +02:00

7.4 KiB

Certificats TLS sur owni

État relevé le 27/09/2026. Deux mécanismes coexistent, et ce n'est pas un désordre : c'est une histoire. Ce document dit lequel gère quoi, où chercher quand un site passe en « connexion non sécurisée », et ce qui a déjà cassé.

Qui gère quoi

certbot — un certificat multi-domaines pour huit sites

/etc/letsencrypt/live/alpid.alpinux.org/ porte un seul certificat couvrant dix noms :

alpid.alpinux.org      dolibarr.alpinux.org    owni.alpinux.org
alpinux.org            gitea.alpinux.org       wiki.alpinux.org
autoconfig.alpinux.org installparty.alpinux.org www.alpinux.org
cloud.alpinux.org

Huit vhosts le servent directement. Renouvellement : certbot renew chaque nuit à 3 h 00, par cronwrap (donc visible sur admin.alpinux.org).

Dix noms sur les cent autorisés : le nombre n'est pas un problème, le couplage l'est. Un seul domaine dont le DNS casse ou dont la validation échoue empêche le renouvellement des neuf autres. Et un nom couvert par un SAN devient un cas particulier que les outils traitent mal — voir l'incident cloud plus bas.

ISPConfig et acme.sh — un certificat par site

ISPConfig émet par acme.sh (/root/.acme.sh/<domaine>_ecc/) et dépose une copie dans /var/www/clients/<client>/<web>/ssl/<domaine>-le.crt. C'est cette copie que le vhost lit, jamais l'original.

Concernés : admin, messagerie, portail, static.

Le piège, et il a déjà mordu deux fois

ISPConfig ne rafraîchit sa copie que lorsqu'il crée le certificat. Un renouvellement par certbot passe inaperçu : le vhost continue de servir l'ancien fichier jusqu'à son expiration, puis le site tombe. Re-sauvegarder le site dans l'interface ne suffit pas à déclencher la copie.

Quand Quoi Vu le
1er août 2026 static et portail perdent leur HTTPS 5 septembre
4 août 2026 cloud perd son HTTPS 27 septembre

Cinq semaines, puis sept. C'est le délai qu'il faut pour que quelqu'un s'aperçoive qu'un site ne répond plus.

Le dispositif qui l'évite aujourd'hui

  1. Le hook /etc/letsencrypt/renewal-hooks/deploy/ispconfig-cert.sh s'exécute après chaque renouvellement certbot. Il itère sur $RENEWED_DOMAINS — tous les noms du certificat, SAN compris —, cherche */ssl/${domain}-le.crt sous /var/www/clients et le met à jour. C'est le cas nominal.
  2. Le filet /usr/local/sbin/owni-certs-sync.sh, chaque nuit à 4 h 20, rattrape ce que le hook n'a pas traité : hook absent lors d'un renouvellement passé, copie écrasée par ISPConfig, certificat posé à la main. Mode audit par défaut, --fix pour corriger, alerte à 21 jours.
  3. La surveillance : depuis le 27/09/2026 ce script passe par cronwrap, donc son état remonte sur admin.alpinux.org. Auparavant il alertait par MAILTO=root, c'est-à-dire nulle part — l'expiration de cloud a été signalée chaque nuit pendant 54 jours sans que personne la voie.

Retenir de cette histoire : une alerte qui n'atteint personne n'est pas une alerte. Le script fonctionnait parfaitement ; c'est sa sortie qui se perdait.

Diagnostiquer

La seule méthode qui ne dépend pas du mécanisme

Ce que voit un visiteur, quel que soit l'outil qui a émis le certificat :

for d in messagerie.alpinux.org alpinux.org wiki.alpinux.org \
         gitea.alpinux.org cloud.alpinux.org; do
  fin=$(echo | openssl s_client -servername $d -connect $d:443 2>/dev/null \
        | openssl x509 -noout -enddate | cut -d= -f2)
  echo "$d : $(( ($(date -d "$fin" +%s) - $(date +%s)) / 86400 )) jours"
done

Un echo | openssl s_client qui ne rend rien = TLS cassé, donc site inaccessible en HTTPS.

L'audit complet, sur le serveur

sudo /usr/local/sbin/owni-certs-sync.sh          # audit, n'écrit rien
sudo /usr/local/sbin/owni-certs-sync.sh --fix    # corrige ce qu'il peut

Il rend 0 si tout est cohérent, 1 s'il subsiste une anomalie. Sa sortie distingue :

  • ok — à jour ;
  • ORPHELIN (majuscules) — expiré, c'est une panne ;
  • orphelin (minuscules) — inconnu de certbot mais valide, simple avertissement ;
  • inutilisé — un certificat existe et aucun service ne le lit.

Quel certificat un vhost lit-il vraiment

for f in /etc/apache2/sites-enabled/*.vhost; do
  cert=$(sudo awk '/SSLCertificateFile/ {print $2; exit}' "$f")
  [ -n "$cert" ] && printf "%-34s %s\n" "$(basename "$f")" "$cert"
done

Ce que couvre un certificat multi-domaines

sudo openssl x509 -noout -text -in /etc/letsencrypt/live/alpid.alpinux.org/fullchain.pem \
  | grep -A1 "Subject Alternative Name"

Réparer un site dont le certificat a expiré

Cas rencontré le 27/09/2026 avec cloud.alpinux.org.

1. Établir ce qui manque vraiment. Le nom est-il couvert par un certificat valide existant ? S'il est dans le SAN, il n'y a rien à émettre — seulement à recopier.

2. Recopier le certificat valide à l'emplacement que le vhost attend, en gardant l'ancien plutôt qu'en l'écrasant :

D=/var/www/clients/client1/web19/ssl
L=/etc/letsencrypt/live/alpid.alpinux.org

sudo cp -a $D/cloud.alpinux.org-le.crt $D/cloud.alpinux.org-le.crt.expire-AAAAMMJJ
sudo cp -a $D/cloud.alpinux.org-le.key $D/cloud.alpinux.org-le.key.expire-AAAAMMJJ
sudo cp $L/fullchain.pem $D/cloud.alpinux.org-le.crt
sudo cp $L/privkey.pem   $D/cloud.alpinux.org-le.key
sudo chmod 644 $D/cloud.alpinux.org-le.crt
sudo chmod 600 $D/cloud.alpinux.org-le.key

sudo apache2ctl configtest && sudo systemctl reload apache2

3. Vérifier de l'extérieur, pas seulement que la commande a rendu la main.

Une fois le fichier en place, le hook le maintiendra : il cherche */ssl/${domain}-le.crt et ne peut mettre à jour que ce qui existe. C'est pourquoi cette réparation n'est pas un pansement.

Alternative propre, si le domaine mérite son propre certificat : le réémettre depuis ISPConfig (Sites → le site → SSL → Let's Encrypt, décocher, enregistrer, recocher, enregistrer).

Ce qui reste en suspens

Neuf certificats ISPConfig ne servent à rien. Chaque site couvert par le SAN possède en plus un certificat à son nom, généré et jamais lu :

inutilisé  wiki.alpinux.org, gitea.alpinux.org, alpid.alpinux.org,
           dolibarr.alpinux.org, installparty.alpinux.org,
           autoconfig.alpinux.org, alpinux.org, owni.alpinux.org, cloud…

Rien n'est cassé de ce fait, et les supprimer demanderait de vérifier un par un qu'aucun service — Postfix, Dovecot, pure-ftpd — ne les lit. Le script les signale précisément pour éviter qu'on cesse de mettre à jour un fichier qui sert ailleurs.

owni-certs-sync.sh raisonne par nom de dossier, pas par SAN. Il cherche /etc/letsencrypt/live/<domaine>/ et classe « orphelin » tout nom qui n'en a pas — même couvert par le SAN d'un autre. C'est pourquoi il a su signaler l'expiration de cloud sans pouvoir la réparer. Lui apprendre à lire les SAN le rendrait autonome sur ce cas.

Le script n'est pas versionné

/usr/local/sbin/owni-certs-sync.sh et le hook n'existent que sur la machine. Choix assumé : ce sont des rustines autour d'un comportement d'ISPConfig, et les versionner reviendrait à les pérenniser. Mais cela veut dire qu'un serveur reconstruit les perdra, avec la connaissance qu'ils portent — d'où ce document, qui doit suffire à les réécrire.