Certificats TLS : générer, vérifier et gérer avec OpenSSL
```bash openssl version -a ```
Tu as besoin d'un accès shell avec les droits d'écriture dans le répertoire où tu vas stocker les clés. Le reste — créer une CA, signer, convertir, révoquer — se fait avec le binaire `openssl` seul, sans dépendance externe.
Un point qui coûte cher à oublier : la clé privée ne quitte jamais la machine qui l'a générée. Si tu la copies sur un partage réseau « pour la sauvegarde », tu viens de transformer ton certificat en document public.
Étape 1 — Générer une clé privée et une demande de signature
On commence par la clé privée, puis la CSR (Certificate Signing Request) qui contient la clé publique et les métadonnées d'identité.```bash openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out server.key openssl req -new -key server.key -out server.csr \ -subj "/C=BE/ST=Liege/L=Liege/O=Pixzo/CN=api.exemple.be" ```
`genpkey` remplace l'ancien `genrsa`, déprécié depuis OpenSSL 3.0. Le choix de 4096 bits est un compromis : 2048 suffit pour la plupart des usages, 4096 double le temps de handshake sur du matériel modeste. Si tu as beaucoup de connexions courtes, 2048 bits est plus raisonnable.
La commande `req` ne signe rien. Elle produit une demande que seule une autorité — la tienne ou une publique — pourra transformer en certificat.
Étape 2 — Ajouter les Subject Alternative Names
Le CN seul ne suffit plus. Depuis 2017, les navigateurs ignorent le CN et n'acceptent que les SAN. Un certificat avec `CN=api.exemple.be` mais sans SAN `DNS:api.exemple.be` sera rejeté.Crée un fichier `san.cnf` :
```ini [req] distinguished_name = req_dn req_extensions = v3_req prompt = no
[req_dn] C = BE ST = Liege L = Liege O = Pixzo CN = api.exemple.be
[v3_req] subjectAltName = @alt_names keyUsage = critical, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth
[alt_names] DNS.1 = api.exemple.be DNS.2 = www.exemple.be IP.1 = 192.0.2.10 ```
Puis régénère la CSR avec ce fichier :
```bash openssl req -new -key server.key -out server.csr -config san.cnf ```
Le bloc `keyUsage` limite ce que le certificat autorise. `digitalSignature` et `keyEncipherment` couvrent TLS classique. Si tu ajoutes `clientAuth` dans `extendedKeyUsage`, le même certificat pourra servir en mTLS — utile, mais ça élargit la surface d'usage. À toi de voir si c'est justifié.
Étape 3 — Monter une autorité de certification locale
Pour signer toi-même, il te faut une CA. Elle vit séparément du serveur, idéalement hors ligne.```bash openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out ca.key openssl req -x509 -new -key ca.key -sha256 -days 3650 \ -subj "/C=BE/O=Pixzo Internal CA/CN=Pixzo Root CA" \ -out ca.crt ```
L'option `-x509` fait de cette CSR un certificat auto-signé. C'est ce fichier `ca.crt` que tu distribueras aux clients — navigateurs, conteneurs, autres serveurs — pour qu'ils reconnaissent tes certificats. Dix ans de validité pour une CA racine est courant ; c'est long, mais c'est la racine, pas un certificat de service.
Étape 4 — Signer le certificat serveur
```bash openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out server.crt -days 397 -sha256 \ -extfile san.cnf -extensions v3_req ```Les 397 jours ne sont pas un choix arbitraire : Apple et Chrome refusent les certificats de serveur au-delà de 398 jours depuis septembre 2020. Si tu signes pour deux ans, ton certificat sera rejeté par Safari et Chrome avant même d'expirer. La CA racine, elle, n'est pas concernée.
L'option `-extfile` est celle qu'on oublie. Sans elle, les SAN du CSR ne sont pas recopiés dans le certificat final. Le résultat : un certificat syntaxiquement valide et fonctionnellement inutile.
Étape 5 — Vérifier la chaîne et les extensions
Avant de déployer, inspecte ce que tu viens de produire.```bash openssl x509 -in server.crt -noout -text | grep -A1 "Subject Alternative Name" openssl verify -CAfile ca.crt server.crt ```
La première commande doit afficher tes DNS et IP. La seconde doit retourner `server.crt: OK`. Si `verify` échoue, la chaîne est cassée — souvent parce que tu as signé avec une CA différente de celle que tu passes en `-CAfile`.
Étape 6 — Convertir entre PEM et DER
PEM est du base64 avec des en-têtes `-----BEGIN`. DER est le binaire brut. Certains systèmes — Java keystores, appliances réseau, clients Windows anciens — n'acceptent que DER.```bash openssl x509 -in server.crt -outform DER -out server.der openssl x509 -in server.der -inform DER -out server.pem ```
Pour un keystore PKCS#12, qui combine clé, certificat et chaîne dans un seul fichier protégé par mot de passe :
```bash openssl pkcs12 -export -out server.p12 \ -inkey server.key -in server.crt -certfile ca.crt \ -name "api.exemple.be" ```
Le mot de passe du `.p12` est la seule chose qui protège la clé privée à l'intérieur. Un `.p12` sans mot de passe qui traîne dans un dépôt Git, c'est une clé privée publique.
Étape 7 — Vérifier la révocation via OCSP
Un certificat peut être révoqué avant sa date d'expiration. OCSP permet de poser la question en temps réel, contrairement à une CRL qui peut avoir plusieurs heures de retard.```bash openssl ocsp -issuer ca.crt -cert server.crt \ -url http://ocsp.exemple.be -resp_text -noverify ```
Le champ `Cert Status` dans la réponse contient `good`, `revoked` ou `unknown`. `-noverify` est là parce qu'on teste un responder local ; en production, tu veux valider la signature de la réponse OCSP elle-même, sinon n'importe qui peut te dire que tout va bien.
OCSP a un défaut connu : il révèle à l'autorité quels sites tu visites. C'est la raison pour laquelle OCSP stapling existe — le serveur attache lui-même la réponse à la poignée de main, et le client ne contacte personne. Nginx et Apache le supportent tous les deux.
Étape 8 — Automatiser le renouvellement
Un certificat oublié qui expire un vendredi soir, c'est un classique. Certbot gère ça pour Let's Encrypt avec un timer systemd :```bash sudo certbot certonly --webroot -w /var/www/html -d api.exemple.be sudo systemctl enable --now certbot.timer sudo systemctl list-timers certbot.timer ```
Pour une CA interne, tu écris ton propre script et tu le mets dans un timer systemd ou un cron. La règle : renouvelle à 30 jours de l'expiration, pas à 3. Ça laisse le temps de voir un échec avant qu'il devienne une panne.
Vérification finale
Depuis une autre machine, avec le certificat de ta CA installé dans le trust store :```bash openssl s_client -connect api.exemple.be:443 -servername api.exemple.be -CAfile ca.crt
Cherche `Verify return code: 0 (ok)` et la ligne `Verification: OK`. Si tu vois `unable to verify the first certificate`, la chaîne intermédiaire manque côté serveur. Si tu vois `certificate has expired`, ton renouvellement n'a pas tourné.
Problèmes courants
`unable to get local issuer certificate` — le client ne connaît pas ta CA. Installe `ca.crt` dans `/usr/local/share/ca-certificates/` puis lance `sudo update-ca-certificates`. Pour un navigateur, importe-le manuellement dans les autorités racines.Le certificat est accepté par `curl` mais pas par Chrome — presque toujours un SAN manquant ou un CN utilisé seul. Vérifie avec `openssl x509 -noout -text` que la section `X509v3 Subject Alternative Name` contient bien ton nom d'hôte.
`key values mismatch` au démarrage de Nginx — la clé privée et le certificat ne correspondent pas. Confirme avec :
```bash openssl x509 -noout -modulus -in server.crt | openssl md5 openssl rsa -noout -modulus -in server.key | openssl md5 ```
Les deux empreintes MD5 doivent être identiques. Si elles diffèrent, tu as mélangé deux paires lors d'une rotation — et la rotation, justement, c'est l'étape où on copie vite fait le mauvais fichier.