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
186 lines
7.4 KiB
Markdown
186 lines
7.4 KiB
Markdown
# 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 :
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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 :
|
|
|
|
```sh
|
|
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.
|