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

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.