SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-07

Certificat autosigné Ubuntu 24.04 avec Chrome

Créez un certificat TLS autosigné accepté par Chrome sur Ubuntu 24.04 : SAN, commande OpenSSL, nginx ou Apache, puis confiance client sans curl -k.

Ce que vous allez mettre en place

Un certificat TLS autosigné que les navigateurs et les clients modernes acceptent réellement, avec un subjectAltName correct, des permissions cohérentes sur la clé, une configuration dans nginx ou Apache, ainsi que l’étape que presque tous les guides oublient : faire correctement confiance à ce certificat sur vos clients, au lieu de contourner les avertissements et d’inscrire curl -k en dur dans vos scripts pour toujours. À la fin, vous disposerez d’une autorité de certification privée en cinq commandes, lorsque vous passerez d’un service interne à six.

Commençons par la décision à prendre, car un certificat autosigné est beaucoup moins souvent le bon outil qu’on ne le croit. Si le service est accessible depuis Internet sous un vrai nom DNS, arrêtez-vous ici et obtenez un certificat Let's Encrypt gratuit avec certbot sur nginx ou l’équivalent pour Apache. Il ne coûte rien, se renouvelle automatiquement et tous les navigateurs du monde lui font déjà confiance. Un certificat autosigné sur un site public habitue vos utilisateurs à contourner les avertissements de sécurité, ce qui est une pire habitude que le HTTP en clair.

Un certificat autosigné convient lorsqu’Internet public n’entre pas en jeu : un panneau d’administration lié à l’adresse d’un tunnel WireGuard sur votre VPS, un serveur de staging sur un réseau privé, le trafic entre services backend, un équipement de home lab ou le remplacement du certificat par défaut que Webmin génère lui-même sur le port 10000. Let's Encrypt ne peut de toute façon pas émettre de certificat pour 10.8.0.1 ou git.internal.lan ; aucune autorité de certification publique n’inscrira une adresse IP privée ou un TLD inventé dans un certificat. Pour ces noms, c’est vous qui êtes l’autorité de certification.

Tout ce qui suit s’exécute sur une machine Ubuntu 24.04 fraîchement installée, qui fournit OpenSSL 3.0.x (openssl version à confirmer). Rien ne nécessite un accès Internet ; tout fonctionne dans un environnement isolé.

Pourquoi l’ancienne commande en une ligne produit des certificats rejetés par Chrome

La commande que proposent tous les tutoriels antérieurs à 2017 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, place votre nom d’hôte dans le champ Common Name et produit un certificat sans extension subjectAltName. Ce certificat est inutilisable. Chrome a cessé de lire le Common Name dans la version 58, en avril 2017. La RFC 2818 avait déjà déconseillé la correspondance sur le CN en 2000. Firefox, Safari, curl et Python se comportent de la même manière. Un certificat identifie son serveur avec l’extension SAN, ou ne l’identifie pas du tout. Le navigateur vous l’indique avec ces mots précis :

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 modification du trust store ne corrigera cette erreur, car le certificat ne contient réellement aucun nom. Si vous voyez actuellement NET::ERR_CERT_COMMON_NAME_INVALID, votre certificat n’a pas de SAN, ou contient un SAN incorrect, et vous devez en générer un nouveau. Heureusement, une seule commande suffit.

Créer un certificat accepté par les navigateurs : une seule commande

OpenSSL a ajouté l’option -addext dans la version 1.1.1. Vous n’avez donc plus besoin des manipulations complexes avec un fichier de configuration utilisées par les anciens guides pour injecter un SAN. Sous 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"

Rôle de chaque option :

  • -x509 génère 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 commande. RSA 4096 reste compatible avec les anciens clients. Si tous les clients sont modernes, -newkey ec -pkeyopt ec_paramgen_curve:P-256 produit une clé plus petite et plus rapide.
  • -noenc est la syntaxe OpenSSL 3.x correspondant à l’ancienne option -nodes : la clé n’a pas de phrase secrète. Les deux syntaxes fonctionnent. Avec une clé protégée par une phrase secrète, nginx reste bloqué en attendant une saisie à chaque démarrage. Pour une clé de serveur, utilisez donc cette option.
  • -days 730 définit une validité de deux ans. Ce nombre est expliqué plus loin dans la section consacrée à l’expiration.
  • -subj fournit les réponses aux questions interactives directement dans la commande. Le CN est désormais essentiellement décoratif, mais définissez-le malgré tout sur le nom principal ; certains outils l’affichent.
  • -addext "subjectAltName=..." est l’option essentielle. Indiquez chaque nom et chaque adresse IP que les clients utiliseront : les entrées DNS: pour les noms d’hôte, avec des wildcards comme DNS:*.internal.lan, et les entrées IP: pour les adresses IP. Si quelqu’un accède à https://10.8.0.1, l’entrée IP:10.8.0.1 doit être présente. Un SAN contenant uniquement un nom DNS provoquerait de nouveau l’erreur NET::ERR_CERT_COMMON_NAME_INVALID.

Vérifiez que le SAN a bien été intégré avant de configurer 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 la commande affiche plutôt No extensions in certificate, le certificat ne contient pas de SAN et les navigateurs le rejetteront. Générez-en un nouveau au lieu de poursuivre.

Protéger la clé

Une clé privée lisible par tous les utilisateurs du serveur n’est pas une clé privée. Sur Ubuntu, /etc/ssl/private est déjà 710 root:ssl-cert, ce qui la protège des regards indiscrets, mais définissez explicitement les permissions du fichier lui-même :

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 ; le mode 600, root:root, leur convient donc. Si la clé est destinée à un service qui s’exécute avec son propre utilisateur et charge lui-même la clé, par exemple une application Node, Gitea ou un daemon Python, chown-la plutôt à cet utilisateur de service, toujours avec le mode 600. Ce qu’il ne faut jamais faire : utiliser le mode 644, conserver une copie dans un dépôt git ou une copie dans /tmp.

Configurer 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 n’effectue 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 générations différentes ; consultez la section consacrée aux modes d’échec.

Configurez Apache

sudo a2enmod ssl proxy proxy_http

ssl ne suffit pas ici : le vhost ci-dessous utilise ProxyPass, et sans mod_proxy ni mod_proxy_http, le test de configuration échoue avec Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Enregistrez le vhost dans /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 doit répondre à Syntax OK. Testez maintenant depuis une machine cliente :

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

Vous obtiendrez une erreur :

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

Ce n’est pas un bug. C’est TLS qui fonctionne : curl ne connaît pas votre certificat et refuse de communiquer avec un serveur qu’il ne peut pas authentifier. La section suivante apporte la véritable correction, et ce n’est pas la méthode utilisée par une grande partie d’Internet à cet instant précis.

Faire confiance au certificat côté client et éviter les anti-patterns

Commencez par les mauvaises corrections, en les appelant par leur nom. curl -k (ou --insecure) intégré dans un script, verify=False dans Python requests, NODE_TLS_REJECT_UNAUTHORIZED=0 dans Node : aucune de ces solutions ne rend votre certificat fiable. Elles désactivent la vérification du certificat, ce qui permet au client de communiquer avec n’importe quel serveur présentant n’importe quel certificat, y compris un certificat placé sur le chemin par un attaquant. Vous conservez le coût de TLS, mais vous perdez l’authentification qui justifie son utilisation. Pire encore, ces options se propagent : elles sont copiées dans une tâche cron, puis dans un script de déploiement et enfin dans le code de production, jusqu’à ce que plus personne ne sache quelles connexions étaient censées être temporaires. Si un verify=False reste après la session de débogage qui l’a motivé, la conception est incorrecte.

La bonne solution consiste à apprendre au système d’exploitation de chaque client que ce certificat est une autorité de certification 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 importante de la sortie (suivie d’un bloc Running hooks in /etc/ca-certificates/update.d...) est la suivante :

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

Ces lignes comportent deux pièges. Le fichier doit se terminer par .crt. Une extension .pem est ignorée silencieusement, et vous obtenez 0 added sans message d’erreur. Le contenu doit également être au format PEM ; le fichier commence par -----BEGIN CERTIFICATE-----. Convertissez d’abord un fichier binaire DER avec openssl x509 -inform der -in file.der -out file.crt. Ajouter directement le certificat auto-signé comme racine fonctionne, car un certificat auto-signé est sa propre racine.

Ensuite, curl, wget, git, apt et tout autre client qui utilise OpenSSL avec le bundle système font confiance au serveur sans aucune option. Certains clients utilisent leur propre magasin de certificats et nécessitent une configuration distincte :

  • Chrome/Chromium sous Linux utilise une base NSS, et non 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 pour chaque utilisateur.
  • Firefox utilise son propre magasin : Paramètres → Vie privée et sécurité → Certificats → Importer, ou définissez security.enterprise_roots.enabled sur true dans about:config pour qu’il utilise le magasin système.
  • Python requests fournit son propre bundle d’autorités de certification (certifi) et ignore le magasin système : transmettez 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 .crt et installez-le dans Autorités de certification racines de confiance. Sous macOS, ajoutez-le au trousseau Système dans Trousseaux d’accès, puis définissez-le sur Toujours approuvé.

Un seul root pour plusieurs services : une petite CA privée

La confiance par certificat ne passe immédiatement plus à l’échelle : six services multipliés par quatre machines clientes représentent vingt-quatre installations de certificats de confiance, et chaque nouveau service en ajoute d’autres. La solution consiste à utiliser une CA privée. Les clients font confiance à un seul root, qui sert à signer le certificat de chaque service.

L’option la plus simple est mkcert, disponible dans les dépôts Ubuntu 24.04 et capable de gérer les magasins NSS (Chrome, Firefox) que update-ca-certificates ne prend pas en charge :

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

mkcert -install crée un root et l’enregistre dans chaque magasin de certificats de confiance de cette machine ; la troisième commande génère git.internal.lan+2.pem et git.internal.lan+2-key.pem, prêts à être utilisés dans les extraits de configuration nginx ou Apache ci-dessus. Cet outil part du principe qu’il s’agit d’une machine de développement : la clé du root reste sur la machine qui a exécuté -install. Il convient donc parfaitement à un laptop de développement, mais pas à un parc de serveurs.

Pour les serveurs, OpenSSL seul permet de configurer toute la CA 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 se trouve dans la dernière commande : openssl x509 -req supprime par défaut toutes les extensions de la CSR, notamment le SAN que vous avez ajouté avec soin. -copy_extensions copy (une option d’OpenSSL 3.x, donc compatible avec 24.04) les conserve ; si vous l’omettez, le certificat signé ne contient aucun SAN et Chrome vous affiche de nouveau NET::ERR_CERT_COMMON_NAME_INVALID. Vérifiez-le avec le même contrôle openssl x509 -noout -ext subjectAltName qu’auparavant.

Distribuez lab-ca.crt aux clients en appliquant les étapes précédentes d’ajout au magasin de certificats de confiance, une seule fois par machine. Protégez lab-ca.key comme une donnée extrêmement sensible : utilisez le mode 600 et, idéalement, conservez-le sur une machine qui ne fait pas partie des serveurs pour lesquels elle signe des certificats. Toute personne qui la détient peut en effet créer un certificat pour n’importe quel nom auquel vos clients feront confiance.

Expiration et rotation

La durée de validité des certificats d’une AC publique diminue rapidement. Le CA/Browser Forum a limité à 200 jours la durée des certificats nouvellement émis et approuvés publiquement en mars 2026, contre 398 auparavant. Cette durée passera à 100 jours en 2027, puis à 47 jours en mars 2029. Ces règles s’appliquent toutefois aux AC approuvées publiquement. Votre AC privée n’est pas concernée, et les navigateurs ne les imposent pas aux certificats racine installés manuellement. Une contrainte concrète s’applique néanmoins : les plateformes Apple refusent tout certificat de serveur TLS valide plus de 825 jours, quel que soit son émetteur. Si des iPhone ou des Mac doivent se connecter, limitez donc les certificats leaf à deux ans. -days 730 respecte cette limite partout. Une racine valable dix ans avec des certificats leaf valables deux ans constitue une configuration interne confortable.

Les certificats à longue durée de validité échouent d’une seule manière : silencieusement, tous en même temps, à une date que personne ne se souvient avoir choisie. Vérifiez ce dont vous disposez :

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

Inscrivez le renouvellement dans un véritable calendrier ou laissez cron vous rappeler l’échéance 30 jours à l’avance. openssl x509 -checkend 2592000 -in cert.crt renvoie un code différent de zéro lorsque l’expiration se trouve à moins de ce nombre de secondes. Si vous utilisez déjà Uptime Kuma pour surveiller l’état, ses monitors HTTPS signalent gratuitement l’approche de l’expiration des certificats.

La rotation avec une AC privée est agréablement simple : réexécutez les commandes de création et de signature du CSR, remplacez les fichiers, puis rechargez le serveur web. La racine n’a pas changé, donc aucun client ne remarque quoi que ce soit.

Modes d’échec et messages affichés

NET::ERR_CERT_AUTHORITY_INVALID, c’est l’état attendu avant l’installation de l’ancre de confiance, et non un défaut du certificat. Si le problème persiste après l’installation de la racine : sous Linux, Chrome utilise NSS plutôt que le magasin système (voir l’étape certutil) ; le fichier copié ne se terminait pas par .crt et update-ca-certificates indiquait 0 added ; ou le serveur présente un certificat différent de celui auquel vous avez accordé votre 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 ne contient aucun SAN, ou le SAN ne couvre pas le nom affiché dans la barre d’adresse. Cas classique : le SAN contient DNS:git.internal.lan, mais l’utilisateur a accédé à https://10.8.0.1. Les modifications du magasin de confiance ne peuvent pas corriger ce problème ; réémettez le certificat avec l’entrée manquante.

curl: (60) SSL certificate problem: self-signed certificate, curl n’accorde pas sa confiance au certificat. La variante self-signed certificate in certificate chain signifie la même chose pour un certificat signé par votre autorité de certification privée. Correction ponctuelle : curl --cacert lab-ca.crt https://... ; correction permanente : le magasin de confiance. N’utilisez pas -k.

unable to load certificate ... Expecting: TRUSTED CERTIFICATE (ou Expecting: CERTIFICATE REQUEST, ou no start line), il s’agit d’une confusion PEM. Vous avez fourni à OpenSSL le mauvais type de fichier : une clé ou une CSR alors qu’il attendait un certificat, ou un fichier binaire DER alors qu’il attendait du PEM. head -1 filename indique ce que vous avez réellement ; un certificat commence par -----BEGIN CERTIFICATE-----. Pour le DER, convertissez-le 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 correspondent pas, généralement parce que la commande de génération a été exécutée deux fois et que les fichiers ont été mélangés. Vérifiez openssl x509 -in git.internal.crt -noout -pubkey | sha256sum par rapport à openssl pkey -in git.internal.key -pubout | sha256sum ; des hachages identiques indiquent une paire correspondante. S’ils diffèrent, régénérez les deux fichiers ensemble.

FAQ

Pourquoi Chrome indique-t-il toujours « Non sécurisé » après la création d’un certificat auto-signé ?

Si l’erreur est NET::ERR_CERT_AUTHORITY_INVALID, le certificat est correct, mais Chrome n’a encore aucune raison de lui faire confiance. Installez-le, ou installez la racine de votre CA privée, dans le trust store du client. Sous Linux, Chrome utilise la base NSS via certutil, et non le magasin système. Si l’erreur est NET::ERR_CERT_COMMON_NAME_INVALID, le certificat ne contient pas de Subject Alternative Name correspondant à l’URL. Il faut alors le réémettre avec -addext "subjectAltName=...".

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

Copiez le certificat au format PEM, avec l’extension .crt, dans /usr/local/share/ca-certificates/, puis exécutez sudo update-ca-certificates. La sortie doit indiquer 1 added. Ensuite, curl le vérifiera comme n’importe quel certificat public. Pour une requête ponctuelle sans modifier le système, curl --cacert /path/to/cert.crt vérifie uniquement ce fichier. -k désactive complètement la vérification et ne doit figurer dans aucun script.

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

Techniquement, aussi longtemps que vous le souhaitez. Les limites du CA/Browser Forum, soit 200 jours actuellement et 47 jours d’ici 2029, s’appliquent aux CA approuvées publiquement, pas à la confiance privée. En pratique, limitez les certificats serveur à 825 jours, car les appareils Apple refusent toute durée supérieure, quel que soit l’émetteur. Une racine privée valable dix ans avec des certificats leaf valables deux ans (-days 730) constitue une valeur par défaut raisonnable. Planifiez simplement leur renouvellement, car un certificat interne expiré peut interrompre silencieusement tous les services à une date que personne ne retient.

Dois-je utiliser un certificat auto-signé ou Let's Encrypt ?

Si le service possède un nom DNS public et est accessible depuis Internet, utilisez toujours Let's Encrypt : c’est gratuit, automatisé et déjà approuvé par tous les clients. Un certificat auto-signé, ou une CA privée, sert pour les cas où Let's Encrypt ne peut pas émettre de certificat : adresses IP privées, noms d’hôte internes comme .lan, réseaux isolés et services volontairement masqués derrière un VPN. Le choix dépend de l’accessibilité et du nommage, pas de la robustesse de la sécurité. La cryptographie est identique.

Pourquoi mon certificat est-il rejeté même après son ajout dans /usr/local/share/ca-certificates ?

Vérifiez trois éléments. Le fichier doit se terminer par .crt. Une extension .pem est ignorée silencieusement et update-ca-certificates signale 0 added. Le contenu doit être du texte PEM commençant par -----BEGIN CERTIFICATE-----, et non un fichier binaire DER. Enfin, l’application doit effectivement utiliser le magasin système. Chrome sous Linux, Firefox, Python requests, Node.js et Java utilisent chacun un trust store distinct et nécessitent un ajout séparé du certificat.