SSD Nodes Learn
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-07-19

Certificats auto-signés sur Ubuntu, bien faits

Créez un certificat TLS auto-signé que Chrome accepte sur Ubuntu 24.04 : une commande openssl avec SAN, config nginx/Apache et confiance sans curl -k.

Ce que vous allez construire

Un certificat TLS auto-signé que les navigateurs et clients modernes acceptent réellement : un subjectAltName correct, des permissions de clé raisonnables, intégré à nginx ou Apache, plus la partie que presque tous les guides oublient : apprendre à vos clients à lui faire confiance correctement, au lieu de cliquer à travers les avertissements et de coder en dur curl -k dans des scripts pour toujours. À la fin, une autorité de certification (AC) privée en cinq commandes, pour le jour où un service interne en devient six.

D'abord, la décision, car un certificat auto-signé est le bon outil bien moins souvent qu'on ne l'utilise. Si le service est accessible depuis l'internet public sous un vrai nom DNS, arrêtez de lire et obtenez plutôt un certificat Let's Encrypt gratuit avec certbot sur nginx ou l'équivalent pour Apache. Cela ne coûte rien, se renouvelle tout seul, et tous les navigateurs de la planète lui font déjà confiance. Un certificat auto-signé sur un site public habitue vos utilisateurs à cliquer à travers les avertissements de sécurité, ce qui est une pire habitude que le simple HTTP.

L'auto-signé est le bon outil quand il n'y a pas d'internet public dans le tableau : un panneau d'administration lié à une adresse de tunnel WireGuard sur votre VPS, une machine de préproduction sur un réseau privé, du trafic de service à service entre backends, un appareil de home-lab, ou le remplacement du certificat par défaut que Webmin génère pour lui-même sur le port 10000. De toute façon, Let's Encrypt ne peut pas émettre pour 10.8.0.1 ni git.internal.lan : aucune AC publique ne mettra une IP privée ou un TLD inventé dans un certificat. Pour ces noms, c'est vous l'AC.

Tout ce qui suit fonctionne sur une machine Ubuntu 24.04 neuve, qui embarque OpenSSL 3.0.x (openssl version pour confirmer). Rien ici n'a besoin d'un accès internet ; tout fonctionne en environnement isolé (air-gapped).

Pourquoi l'ancienne commande produit des certificats que Chrome refuse

La commande que tous les tutoriels d'avant 2017 vous donnent ressemble à ceci :

# Do not run this — shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key -out selfsigned.crt

Elle pose une série de questions interactives, met votre nom d'hôte dans le champ Common Name, et produit un certificat sans extension subjectAltName. Ce certificat est mort-né. Chrome a cessé de lire le Common Name dans la version 58, en avril 2017 ; la RFC 2818 avait déjà rendu obsolète la correspondance par CN dès l'an 2000, et Firefox, Safari, curl et Python se comportent de la même façon. Un certificat identifie son serveur via l'extension SAN ou pas du tout, et le navigateur vous le dit avec exactement ces mots :

NET::ERR_CERT_COMMON_NAME_INVALID

This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.

Aucune manipulation du magasin de confiance ne corrige cette erreur, car le certificat ne nomme réellement rien. Si vous fixez NET::ERR_CERT_COMMON_NAME_INVALID en ce moment, votre certificat n'a pas de SAN (ou le mauvais) et vous devez en générer un nouveau. Heureusement, la solution tient en une seule commande.

Générer un certificat que les navigateurs acceptent : une commande

OpenSSL a gagné le drapeau -addext en 1.1.1, ce qui veut dire que vous n'avez plus besoin des acrobaties de fichier de configuration que les vieux guides utilisaient pour injecter un SAN. Sur Ubuntu 24.04 :

sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
  -keyout /etc/ssl/private/git.internal.key \
  -out /etc/ssl/certs/git.internal.crt \
  -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"

Ce que fait chaque drapeau :

  • -x509 émet directement un certificat auto-signé au lieu d'une demande de signature.
  • -newkey rsa:4096 génère une nouvelle clé dans la même étape. RSA 4096 ne dérange aucun client ancien ; si tout ce qui se connecte est moderne, -newkey ec -pkeyopt ec_paramgen_curve:P-256 est plus petit et plus rapide.
  • -noenc est l'orthographe OpenSSL 3.x de l'ancien -nodes : aucune phrase secrète sur la clé. Les deux orthographes fonctionnent. Une clé protégée par phrase secrète fait que nginx se bloque en attendant une saisie à chaque démarrage, donc pour une clé de serveur vous voulez ceci.
  • -days 730 : deux ans ; plus de détails sur ce nombre dans la section sur l'expiration.
  • -subj répond aux questions interactives en ligne. Le CN est cosmétique désormais, mais mettez-y quand même le nom principal ; certains outils l'affichent.
  • -addext "subjectAltName=..." est le drapeau porteur. Listez chaque nom et chaque IP que les clients taperont : des entrées DNS: pour les noms d'hôte (les jokers comme DNS:*.internal.lan sont acceptés), des entrées IP: pour les adresses. Si quelqu'un navigue vers https://10.8.0.1, l'entrée IP:10.8.0.1 doit y figurer : un SAN uniquement DNS lui redonne NET::ERR_CERT_COMMON_NAME_INVALID une nouvelle fois.

Confirmez que le SAN est bien présent avant de câbler quoi que ce soit :

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltName

Sortie correcte :

X509v3 Subject Alternative Name:
    DNS:git.internal.lan, IP Address:10.8.0.1

Si cela affiche plutôt No extensions in certificate, le certificat n'a pas de SAN et les navigateurs le refuseront : régénérez-le plutôt que de continuer.

Verrouiller la clé

Une clé privée lisible par tous les utilisateurs de la machine n'est pas une clé privée. Sur Ubuntu, /etc/ssl/private est déjà en 710 root:ssl-cert, ce qui écarte les regards distraits, mais définissez le fichier lui-même explicitement :

sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.key

nginx et Apache lisent tous deux les certificats en tant que root avant d'abandonner leurs privilèges, donc root:root en mode 600 leur convient. Si la clé est destinée à un service qui s'exécute sous son propre utilisateur et charge la clé lui-même (une appli Node, Gitea, un démon Python), faites plutôt un chown vers cet utilisateur de service, toujours en mode 600. Ce que vous ne faites jamais : le mode 644, une copie dans un dépôt git, ou une copie dans /tmp.

L'intégrer à nginx

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name git.internal.lan;

    ssl_certificate     /etc/ssl/certs/git.internal.crt;
    ssl_certificate_key /etc/ssl/private/git.internal.key;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}
sudo nginx -t && sudo systemctl reload nginx

nginx -t doit afficher syntax is ok et test is successful avant que le rechargement ne fasse quoi que ce soit. S'il affiche plutôt SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, le certificat et la clé proviennent de deux exécutions de génération différentes : voyez la section sur les modes d'échec.

L'intégrer à Apache

sudo a2enmod ssl proxy proxy_http

ssl seul ne suffit pas ici : le vhost ci-dessous utilise ProxyPass, et sans mod_proxy ni mod_proxy_http le test de configuration meurt avec Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Enregistrez le vhost sous /etc/apache2/sites-available/git-internal.conf :

<VirtualHost *:443>
    ServerName git.internal.lan
    SSLEngine on
    SSLCertificateFile      /etc/ssl/certs/git.internal.crt
    SSLCertificateKeyFile   /etc/ssl/private/git.internal.key

    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2

configtest devrait répondre Syntax OK. Testez maintenant depuis une machine cliente :

curl -v https://git.internal.lan/

et vous obtiendrez une erreur :

curl: (60) SSL certificate problem: self-signed certificate

Ce n'est pas un bug. C'est TLS qui fonctionne : curl n'a jamais entendu parler de votre certificat et refuse de parler à un serveur qu'il ne peut pas authentifier. La section suivante est la vraie solution, et ce n'est pas ce que fait la moitié d'internet en ce moment précis.

Faire en sorte que les clients lui fassent confiance, et les anti-modèles à refuser

Les mauvaises solutions d'abord, nommées pour ce qu'elles sont. curl -k (ou --insecure) intégré à un script, verify=False dans Python requests, NODE_TLS_REJECT_UNAUTHORIZED=0 dans Node : aucune de ces solutions ne rend votre certificat digne de confiance. Elles désactivent la vérification du certificat, ce qui veut dire que le client parlera volontiers à n'importe quel serveur présentant n'importe quel certificat, y compris celui qu'un attaquant a placé sur le chemin. Vous gardez le coût de TLS et perdez l'authentification qui en était le but. Pire, ces drapeaux se propagent comme des métastases : collés dans une tâche cron, puis dans un script de déploiement, puis dans du code de production, jusqu'à ce que personne ne se souvienne quelles connexions étaient censées être temporaires. Si un verify=False survit à la session de débogage qui l'a engendré, la conception est mauvaise.

La bonne solution consiste à apprendre à chaque système d'exploitation client que ce certificat est une racine de confiance. Sur les clients Ubuntu et Debian :

sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificates

La ligne qui compte dans la sortie (un bloc Running hooks in /etc/ca-certificates/update.d... la suit) :

Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

Deux pièges se cachent dans ces lignes. Le fichier doit se terminer par .crt : une extension .pem est ignorée en silence et vous obtenez 0 added sans aucun message d'erreur. Et le contenu doit être au format PEM : le fichier commence par -----BEGIN CERTIFICATE----- ; convertissez d'abord un binaire DER avec openssl x509 -inform der -in file.der -out file.crt. Ajouter le certificat auto-signé lui-même comme racine fonctionne parce qu'un certificat auto-signé est sa propre racine.

Après cela, curl, wget, git, apt, et tout ce qui utilise OpenSSL avec le paquet système font confiance au serveur sans aucun drapeau. Une poignée de clients transportent leur propre magasin de confiance et nécessitent un traitement individuel :

  • Chrome/Chromium sur Linux lit une base de données NSS, pas le magasin système : sudo apt install libnss3-tools, puis certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt par utilisateur.
  • Firefox a son propre magasin : Paramètres → Vie privée et sécurité → Certificats → Importer, ou basculez security.enterprise_roots.enabled sur true dans about:config pour qu'il lise le magasin système.
  • Python requests embarque son propre paquet d'AC (certifi) et ignore le magasin système : passez verify="/usr/local/share/ca-certificates/git.internal.crt" ou exportez REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt.
  • Node.js : exportez NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.

Sur les clients Windows, double-cliquez sur le .crt et installez-le dans Autorités de certification racines de confiance ; sur macOS, ajoutez-le au trousseau Système dans Trousseaux d'accès et marquez-le Toujours approuver.

Une racine pour de nombreux services : une petite AC privée

La confiance par certificat cesse de passer à l'échelle immédiatement : six services multipliés par quatre machines clientes font vingt-quatre installations de confiance, et chaque nouveau service en ajoute d'autres. La solution est une AC privée : les clients font confiance à une racine, et vous signez le certificat de chaque service avec elle.

L'option conviviale est mkcert, présent dans les dépôts Ubuntu 24.04 et qui gère les magasins NSS (Chrome, Firefox) que update-ca-certificates laisse de côté :

sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1

mkcert -install crée une racine et l'enregistre dans chaque magasin de confiance de cette machine ; la troisième commande émet git.internal.lan+2.pem et git.internal.lan+2-key.pem, prêts à insérer dans les extraits nginx ou Apache ci-dessus. Son hypothèse de conception est une machine de développement (la clé racine réside sur la machine qui a lancé -install), donc il est parfait pour un portable de dev et mal adapté à une flotte de serveurs.

Pour les serveurs, OpenSSL tout simple réalise toute l'AC en cinq commandes :

openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
  -out lab-ca.crt -subj "/CN=Lab Internal CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
  -CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crt

Le piège est dans la dernière commande : openssl x509 -req abandonne toutes les extensions de la CSR (demande de signature de certificat) par défaut, y compris le SAN que vous avez ajouté avec soin. -copy_extensions copy (une option OpenSSL 3.x, donc elle fonctionne sur 24.04) les reporte ; omettez-la et le certificat signé n'a pas de SAN, et Chrome vous accueille de nouveau avec NET::ERR_CERT_COMMON_NAME_INVALID. Vérifiez avec le même contrôle openssl x509 -noout -ext subjectAltName que précédemment.

Distribuez lab-ca.crt aux clients via les étapes de magasin de confiance ci-dessus : une seule fois par machine, à jamais. Gardez lab-ca.key comme les joyaux de la couronne qu'elle est désormais : mode 600, idéalement conservée sur une machine qui n'est pas l'un des serveurs pour lesquels elle signe, car quiconque la détient peut générer un certificat pour n'importe quel nom auquel vos clients croiront.

Expiration et rotation

Les durées de vie des certificats d'AC publiques s'effondrent : le CA/Browser Forum a plafonné les certificats publiquement approuvés nouvellement émis à 200 jours en mars 2026 (contre 398), passant à 100 jours en 2027 et 47 jours d'ici mars 2029, mais ces règles ne lient que les AC publiquement approuvées. Votre AC privée n'est pas régie par elles, et les navigateurs ne les imposent pas aux racines installées manuellement. Une contrainte bien réelle s'applique tout de même : les plateformes Apple rejettent tout certificat de serveur TLS valable plus de 825 jours, peu importe qui l'a émis, donc si des iPhone ou des Mac doivent se connecter, gardez les certificats feuilles à deux ans ou moins. -days 730 passe cette barre partout ; une racine de dix ans avec des feuilles de deux ans est une forme interne confortable.

Les certificats à longue durée de vie échouent d'exactement une seule manière : silencieusement, d'un seul coup, à une date que personne ne se souvient avoir choisie. Vérifiez ce que vous avez :

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate

Mettez le renouvellement dans un vrai calendrier, ou faites en sorte que cron vous rappelle 30 jours à l'avance : openssl x509 -checkend 2592000 -in cert.crt retourne un code non nul dès que l'expiration est à moins de ce nombre de secondes. Si vous utilisez déjà Uptime Kuma pour la supervision de statut, ses moniteurs HTTPS signalent gratuitement l'expiration prochaine des certificats.

La rotation avec une AC privée est agréablement ennuyeuse : relancez les commandes de CSR et de signature, remplacez les fichiers, rechargez le serveur web. La racine n'a pas changé, donc aucun client ne remarque quoi que ce soit.

Modes d'échec, avec les messages que vous verrez

NET::ERR_CERT_AUTHORITY_INVALID : l'état attendu avant d'installer la confiance, pas un défaut du certificat. S'il persiste après l'installation de la racine : sur Linux, Chrome lit NSS plutôt que le magasin système (voyez l'étape certutil) ; ou le fichier copié ne se terminait pas par .crt et update-ca-certificates a indiqué 0 added ; ou le serveur présente un certificat différent de celui auquel vous faites confiance. Comparez les empreintes avec openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256.

NET::ERR_CERT_COMMON_NAME_INVALID : le certificat n'a pas de SAN, ou le SAN ne couvre pas le nom dans la barre d'adresse. Le cas classique : le SAN liste DNS:git.internal.lan mais l'utilisateur a navigué vers https://10.8.0.1. Les modifications du magasin de confiance ne corrigent pas celui-ci ; réémettez avec l'entrée manquante.

curl: (60) SSL certificate problem: self-signed certificate : curl ne fait pas confiance au certificat. La variante self-signed certificate in certificate chain signifie la même chose pour un certificat signé par votre AC privée. Solution ponctuelle : curl --cacert lab-ca.crt https://... ; solution permanente : le magasin de confiance. Pas -k.

unable to load certificate ... Expecting: TRUSTED CERTIFICATE (ou Expecting: CERTIFICATE REQUEST, ou no start line) : confusion de PEM. Vous avez donné à OpenSSL le mauvais type de fichier : une clé ou une CSR là où il attendait un certificat, ou un binaire DER là où il attendait du PEM. head -1 filename vous dit ce que vous avez réellement : un certificat commence par -----BEGIN CERTIFICATE-----. Pour du DER, convertissez avec openssl x509 -inform der -in file.der -out file.crt.

nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch) : le certificat et la clé ne vont pas ensemble, généralement parce que la commande de génération a été lancée deux fois et que les fichiers ont été mélangés. Confirmez avec openssl x509 -in git.internal.crt -noout -pubkey | sha256sum face à openssl pkey -in git.internal.key -pubout | sha256sum ; des empreintes identiques signifient une paire correspondante. Si elles diffèrent, régénérez les deux ensemble.

FAQ

Pourquoi Chrome affiche-t-il encore « Non sécurisé » après la création d'un certificat auto-signé ?

Si l'erreur est NET::ERR_CERT_AUTHORITY_INVALID, le certificat va bien : Chrome n'a simplement encore aucune raison de lui faire confiance. Installez-le (ou la racine de votre AC privée) dans le magasin de confiance du client, et souvenez-vous que sur Linux, Chrome utilise la base de données NSS via certutil, pas le magasin système. Si l'erreur est NET::ERR_CERT_COMMON_NAME_INVALID, le certificat n'a pas de Subject Alternative Name correspondant à l'URL et doit être réémis avec -addext "subjectAltName=...".

Comment faire en sorte que curl fasse confiance à un certificat auto-signé sans -k ?

Copiez le certificat (format PEM, extension .crt) dans /usr/local/share/ca-certificates/ et lancez sudo update-ca-certificates ; la sortie doit indiquer 1 added. À partir de là, curl le vérifie comme n'importe quel certificat public. Pour une requête ponctuelle sans toucher au système, curl --cacert /path/to/cert.crt vérifie uniquement face à ce fichier ; -k désactive entièrement la vérification et n'a sa place dans les scripts de personne.

Combien de temps un certificat auto-signé peut-il rester valable ?

Techniquement aussi longtemps que vous voulez : les limites du CA/Browser Forum (200 jours désormais, 47 d'ici 2029) lient les AC publiquement approuvées, pas la confiance privée. En pratique, plafonnez les certificats de serveur à 825 jours, car les appareils Apple rejettent tout ce qui dépasse, quel que soit l'émetteur. Une racine privée de dix ans avec des certificats feuilles de deux ans (-days 730) est une valeur par défaut raisonnable ; calendez simplement le renouvellement, car un certificat interne expiré fait tout tomber silencieusement à une date dont personne ne se souvient.

Faut-il utiliser un certificat auto-signé ou Let's Encrypt ?

Si le service a un nom DNS public et est accessible depuis internet, toujours Let's Encrypt : gratuit, automatisé, déjà approuvé par tous les clients. L'auto-signé (ou une AC privée) est pour ce que Let's Encrypt ne peut pas émettre : les IP privées, les noms d'hôte internes comme .lan, les réseaux isolés, et les services délibérément cachés derrière un VPN. La décision porte sur l'accessibilité et le nommage, pas sur la force de la sécurité : la cryptographie est identique.

Pourquoi mon certificat est-il rejeté même après l'avoir ajouté à /usr/local/share/ca-certificates ?

Vérifiez trois choses. Le fichier doit se terminer par .crt : une extension .pem est ignorée en silence et update-ca-certificates signale 0 added. Le contenu doit être du texte PEM commençant par -----BEGIN CERTIFICATE-----, pas du binaire DER. Et l'application doit réellement utiliser le magasin système : Chrome sur Linux, Firefox, Python requests, Node.js et Java conservent chacun un magasin de confiance privé et nécessitent l'ajout séparé du certificat.

#openssl#tls#self-signed#ubuntu#sécurité