Самопідписаний TLS-сертифікат на Ubuntu 24.04
Створіть сертифікат, який Chrome приймає на Ubuntu 24.04: одна команда openssl із SAN, налаштування nginx або Apache та довіра без curl -k.
Що ви створюєте
Самопідписаний TLS-сертифікат, який справді приймають сучасні браузери та клієнти, із правильним subjectAltName, безпечними дозволами на ключ, налаштуванням для nginx або Apache, а також із компонентом, який пропускає майже кожен посібник: правильним налаштуванням довіри клієнтів до цього сертифіката замість постійного пропускання попереджень і жорсткого задання curl -k у скриптах. Наприкінці наведено приватний CA із п’яти команд для випадку, коли один внутрішній сервіс перетворюється на шість.
Спочатку визначтеся з підходом, оскільки самопідписаний сертифікат значно рідше є правильним інструментом, ніж його використовують. Якщо сервіс доступний із публічного інтернету за справжнім DNS-іменем, припиніть читати й отримайте безкоштовний сертифікат Let's Encrypt за допомогою certbot на nginx або еквівалентний варіант для Apache. Це нічого не коштує, сертифікат поновлюється автоматично, і йому вже довіряє кожен браузер у світі. Самопідписаний сертифікат на загальнодоступному сайті привчає користувачів пропускати попередження системи безпеки. Це гірша звичка, ніж використання звичайного HTTP.
Самопідписаний сертифікат доречний, коли публічний інтернет не використовується: панель адміністрування, прив’язана до адреси тунелю WireGuard на вашому VPS, тестовий сервер у приватній мережі, трафік між внутрішніми сервісами, пристрій домашньої лабораторії або заміна сертифіката-заповнювача, який Webmin генерує для себе на порту 10000. Let's Encrypt все одно не може видати сертифікат для 10.8.0.1 або git.internal.lan, а жоден публічний CA не додасть до сертифіката приватну IP-адресу або вигаданий TLD. Для таких імен CA є ви.
Усе нижче виконується на чистій системі Ubuntu 24.04, яка постачається з OpenSSL 3.0.x (openssl version для перевірки). Для цього не потрібен доступ до інтернету; усе працює в ізольованому середовищі.
Чому старий однорядковий приклад створює сертифікати, які Chrome відхиляє
Команда, яку наведено в кожному посібнику, написаному до 2017 року, має такий вигляд:
# 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Вона ставить низку інтерактивних запитань, записує ім’я хоста в поле Common Name і створює сертифікат без розширення subjectAltName. Такий сертифікат одразу непридатний для використання. У версії 58, випущеній у квітні 2017 року, Chrome припинив перевіряти Common Name. Ще у 2000 році RFC 2818 оголосив зіставлення CN застарілим. Firefox, Safari, curl і Python працюють так само. Сертифікат ідентифікує свій сервер через розширення SAN або не ідентифікує його взагалі. Браузер повідомляє про це саме такими словами:
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.Жодні зміни у сховищі довіри не усунуть цю помилку, оскільки сертифікат справді не містить жодного імені. Якщо ви зараз бачите NET::ERR_CERT_COMMON_NAME_INVALID, ваш сертифікат не має SAN або містить неправильний SAN, тому потрібно створити новий сертифікат. На щастя, виправлення потребує лише однієї команди.
Створіть сертифікат, який приймають браузери: одна команда
OpenSSL додав прапорець -addext у версії 1.1.1. Тому більше не потрібно використовувати складні маніпуляції з конфігураційними файлами, які застосовувалися у старих інструкціях для додавання SAN. В 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"Що робить кожен прапорець:
-x509безпосередньо створює самопідписаний сертифікат замість запиту на підписання.-newkey rsa:4096створює новий ключ на тому самому кроці. RSA 4096 сумісний зі старими клієнтами. Якщо всі підключення виконуються сучасними клієнтами,-newkey ec -pkeyopt ec_paramgen_curve:P-256має менший розмір і працює швидше.-noenc— це синтаксис OpenSSL 3.x для старого-nodes: ключ не захищений парольною фразою. Обидва варіанти працюють. Якщо ключ захищений парольною фразою, nginx під час кожного завантаження системи очікує введення. Для ключа сервера потрібно використовувати цей варіант.-days 730— два роки. Докладніше про це число див. у розділі про завершення строку дії.-subjпередає відповіді на інтерактивні запитання безпосередньо в команді. Тепер CN має лише формальне значення, але все одно вкажіть основне ім’я. Деякі інструменти його відображають.-addext "subjectAltName=..."— ключовий прапорець. Перелічіть кожне ім’я та кожну IP-адресу, які клієнти вводитимуть: записиDNS:для імен хостів (шаблони на кшталтDNS:*.internal.lanдозволені), записиIP:для адрес. Якщо користувачі відкриватимутьhttps://10.8.0.1, записIP:10.8.0.1має бути присутнім. Інакше SAN міститиме лише DNS-ім’я, і проблемаNET::ERR_CERT_COMMON_NAME_INVALIDповториться.
Перш ніж налаштовувати подальше використання сертифіката, переконайтеся, що SAN справді додано:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltNameПравильний результат:
X509v3 Subject Alternative Name:
DNS:git.internal.lan, IP Address:10.8.0.1Якщо натомість виводиться No extensions in certificate, сертифікат не містить SAN, і браузери його відхилять. Створіть сертифікат повторно, а не продовжуйте налаштування.
Захистіть ключ
Закритий ключ, доступний для читання кожному користувачу системи, не є закритим ключем. В Ubuntu /etc/ssl/private уже має значення 710 root:ssl-cert, що захищає від випадкового доступу, але явно встановіть права для самого файла:
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keynginx і Apache спочатку читають сертифікати від імені root, а потім знижують привілеї, тому режим root:root 600 їм підходить. Якщо ключ призначений для служби, яка працює від імені окремого користувача та самостійно завантажує ключ, наприклад Node app, Gitea або Python daemon, chown передайте його цьому користувачу служби й залиште режим 600. Ніколи не використовуйте режим 644, не зберігайте копію в git repository і не зберігайте копію в /tmp.
Підключення до 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 nginxnginx -t має вивести syntax is ok і test is successful, перш ніж перезавантаження щось змінить. Якщо натомість виводиться SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, сертифікат і ключ отримано під час двох різних запусків генерації. Див. розділ про типові причини помилок.
Підключіть це до Apache
sudo a2enmod ssl proxy proxy_httpСамого ssl тут недостатньо: наведений нижче vhost використовує ProxyPass, а без mod_proxy і mod_proxy_http перевірка конфігурації завершується помилкою Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Збережіть vhost у /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 apache2configtest має повертати Syntax OK. Тепер перевірте це з клієнтської машини:
curl -v https://git.internal.lan/У результаті ви отримаєте помилку:
curl: (60) SSL certificate problem: self-signed certificateЦе не помилка в налаштуванні. Так працює TLS: curl нічого не знає про ваш сертифікат і відмовляється встановлювати з'єднання із сервером, автентичність якого не може підтвердити. У наступному розділі наведено правильне виправлення. Воно відрізняється від підходу, який саме зараз використовує значна частина користувачів в інтернеті.
Зробіть так, щоб клієнти йому довіряли, і уникайте антишаблонів
Спочатку розглянемо неправильні виправлення та назвемо їх тим, чим вони є. curl -k (або --insecure), вбудований у скрипт, verify=False у Python requests, NODE_TLS_REJECT_UNAUTHORIZED=0 у Node — жоден із цих варіантів не зробить ваш сертифікат довіреним. Вони вимикають перевірку сертифікатів. Це означає, що клієнт без перевірки взаємодіятиме з будь-яким сервером, який надає будь-який сертифікат, зокрема сертифікат, підставлений зловмисником у мережевий шлях. Ви зберігаєте накладні витрати TLS, але втрачаєте автентифікацію, заради якої його використовують. Гірше того, ці прапорці поширюються: їх вставляють в одне завдання cron, потім у скрипт розгортання, а далі в код production, доки ніхто вже не пам’ятає, які з’єднання мали бути тимчасовими. Якщо verify=False залишається після завершення сеансу налагодження, через який його додали, це помилка проєктування.
Правильне виправлення — налаштувати ОС кожного клієнта так, щоб вона вважала цей сертифікат довіреним кореневим сертифікатом. На клієнтах Ubuntu і Debian:
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificatesРядок у виведенні, який має значення (після нього йде блок Running hooks in /etc/ca-certificates/update.d...):
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.У цих рядках є два важливі нюанси. Файл має закінчуватися на .crt. Розширення .pem мовчки ігнорується, і ви отримуєте 0 added без повідомлення про помилку. Вміст має бути у форматі PEM, а файл має починатися з -----BEGIN CERTIFICATE-----. Спочатку перетворіть двійковий файл DER за допомогою openssl x509 -inform der -in file.der -out file.crt. Додавання самого самопідписаного сертифіката як кореневого працює, оскільки самопідписаний сертифікат є власним коренем.
Після цього curl, wget, git, apt та все інше, що використовує OpenSSL із системним набором сертифікатів, довірятиме серверу без жодних прапорців. Деякі клієнти мають власні сховища довіри, тому для них потрібне окреме налаштування:
- Chrome/Chromium у Linux читає базу даних NSS, а не системне сховище:
sudo apt install libnss3-tools, потімcertutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtдля кожного користувача. - Firefox має власне сховище: Settings → Privacy & Security → Certificates → Import або змініть
security.enterprise_roots.enabledнаtrueуabout:config, щоб браузер читав системне сховище. - Python requests постачається із власним набором CA-сертифікатів (certifi) та ігнорує системне сховище: передайте
verify="/usr/local/share/ca-certificates/git.internal.crt"або експортуйтеREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt. - Node.js: експортуйте
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.
На клієнтах Windows двічі клацніть .crt та встановіть його до Trusted Root Certification Authorities. На macOS додайте його до системного брелока в Keychain Access і встановіть для нього параметр Always Trust.
Один кореневий сертифікат для багатьох служб: невеликий приватний CA
Довіра до окремих сертифікатів швидко стає непрактичною: шість служб на чотири клієнтські машини — це двадцять чотири встановлення сертифікатів довіри, а кожна нова служба збільшує їх кількість. Рішення — приватний CA. Клієнти довіряють одному кореневому сертифікату, а ви підписуєте ним сертифікат кожної служби.
Зручний варіант — mkcert. Він доступний у репозиторіях Ubuntu 24.04 і працює зі сховищами NSS (Chrome, Firefox), які update-ca-certificates не обробляє:
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -install створює кореневий сертифікат і реєструє його в усіх сховищах сертифікатів довіри на цій машині. Третя команда створює git.internal.lan+2.pem і git.internal.lan+2-key.pem, готові для вставлення у наведені вище фрагменти конфігурації nginx або Apache. Це рішення розраховане на машину розробника: ключ кореневого сертифіката зберігається на машині, де було виконано -install. Тому воно добре підходить для ноутбука розробника, але не для парку серверів.
Для серверів звичайний OpenSSL виконує всі операції зі створення CA за п’ять команд:
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Проблема виникає в останній команді: openssl x509 -req за замовчуванням видаляє всі розширення з CSR, зокрема SAN, який ви ретельно додали. -copy_extensions copy — параметр OpenSSL 3.x, тому він працює в 24.04 — переносить ці розширення. Якщо його пропустити, підписаний сертифікат не матиме SAN, і Chrome знову покаже NET::ERR_CERT_COMMON_NAME_INVALID. Перевірте результат тією самою перевіркою openssl x509 -noout -ext subjectAltName, що й раніше.
Поширте lab-ca.crt на клієнти за допомогою наведених вище кроків для сховищ сертифікатів довіри. Це потрібно зробити один раз для кожної машини. Захищайте lab-ca.key так само ретельно, як найцінніші ключі: встановіть режим 600 і, бажано, зберігайте його на машині, яка не є одним із серверів, для яких він підписує сертифікати. Той, хто має цей ключ, може створити сертифікат для будь-якого імені, якому довірятимуть ваші клієнти.
Строк дії та ротація
Строк дії сертифікатів загальнодоступних CA скорочується. CA/Browser Forum у березні 2026 року обмежив строк дії нових сертифікатів, яким довіряють загальнодоступні системи, до 200 днів (замість 398). У 2027 році обмеження становитиме 100 днів, а в березні 2029 року — 47 днів. Проте ці правила поширюються на загальнодоступні CA. На ваш приватний CA вони не поширюються, а браузери не застосовують їх до кореневих сертифікатів, установлених вручну. Є одне практичне обмеження: платформи Apple відхиляють будь-який TLS-сертифікат сервера зі строком дії понад 825 днів, незалежно від видавця. Тому, якщо підключатимуться iPhones або Macs, строк дії сертифікатів leaf має становити не більше двох років. -days 730 відповідає цій вимозі всюди. Кореневий сертифікат зі строком дії десять років і сертифікати leaf зі строком дії два роки — зручна схема для внутрішньої мережі.
Довгострокові сертифікати виходять з ладу лише одним способом: непомітно й одночасно в дату, яку ніхто не пам’ятає. Перевірте наявні сертифікати:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddateВнесіть поновлення до календаря або налаштуйте cron, щоб він нагадував за 30 днів. openssl x509 -checkend 2592000 -in cert.crt завершує роботу з ненульовим кодом, коли до завершення строку дії залишається не більше вказаної кількості секунд. Якщо ви вже використовуєте Uptime Kuma для моніторингу стану, його HTTPS-монітори без додаткової оплати повідомляють про наближення завершення строку дії сертифіката.
Ротація приватного CA не потребує складних процедур: повторно виконайте команди створення CSR і підписування, замініть файли та перезавантажте вебсервер. Кореневий сертифікат не змінився, тому клієнти цього не помітять.
Типові помилки та повідомлення, які ви побачите
NET::ERR_CERT_AUTHORITY_INVALID — це очікуваний стан до встановлення довіри, а не дефект сертифіката. Якщо він зберігається після встановлення кореневого сертифіката: у Linux Chrome читає NSS, а не системне сховище (див. крок certutil); або скопійований файл не закінчується на .crt, і update-ca-certificates повідомила 0 added; або сервер надає інший сертифікат, ніж той, якому ви надали довіру. Порівняйте відбитки за допомогою openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256.
NET::ERR_CERT_COMMON_NAME_INVALID — сертифікат не містить SAN або SAN не охоплює ім’я в адресному рядку. Типовий випадок: SAN містить DNS:git.internal.lan, але користувач відкрив https://10.8.0.1. Зміни у сховищі довіри це не виправлять; випустіть сертифікат повторно з відсутнім записом.
curl: (60) SSL certificate problem: self-signed certificate — curl не довіряє сертифікату. Варіант self-signed certificate in certificate chain означає те саме для сертифіката, підписаного вашим приватним CA. Одноразове виправлення: curl --cacert lab-ca.crt https://...; постійне виправлення: сховище довіри. Не -k.
unable to load certificate ... Expecting: TRUSTED CERTIFICATE (або Expecting: CERTIFICATE REQUEST, або no start line) — проблема з форматом PEM. Ви передали OpenSSL файл неправильного типу: ключ або CSR замість сертифіката або двійковий файл DER замість PEM. head -1 filename показує, що саме у вас є; сертифікат починається з -----BEGIN CERTIFICATE-----. Для DER виконайте перетворення за допомогою openssl x509 -inform der -in file.der -out file.crt.
nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch) — сертифікат і ключ не належать до однієї пари. Зазвичай це відбувається, коли команду генерації виконали двічі й файли змішалися. Перевірте openssl x509 -in git.internal.crt -noout -pubkey | sha256sum і openssl pkey -in git.internal.key -pubout | sha256sum; однакові хеші означають, що пара правильна. Якщо вони відрізняються, згенеруйте обидва файли разом повторно.
FAQ
Чому Chrome і далі показує «Незахищено» після створення самопідписаного сертифіката?
Якщо помилка має код NET::ERR_CERT_AUTHORITY_INVALID, із сертифікатом усе гаразд. Chrome ще не має підстав йому довіряти. Встановіть його або кореневий сертифікат приватного центру сертифікації до сховища довірених сертифікатів клієнта. Пам’ятайте, що в Linux Chrome використовує базу даних NSS через certutil, а не системне сховище. Якщо помилка має код NET::ERR_CERT_COMMON_NAME_INVALID, у сертифікаті немає Subject Alternative Name, що відповідає URL. Сертифікат потрібно випустити повторно з використанням -addext "subjectAltName=...".
Як зробити так, щоб curl довіряв самопідписаному сертифікату без -k?
Скопіюйте сертифікат у форматі PEM із розширенням .crt до /usr/local/share/ca-certificates/ і виконайте sudo update-ca-certificates. У виведенні має бути 1 added. Після цього curl перевірятиме його так само, як будь-який загальнодовірений сертифікат. Для одноразового запиту без зміни системи curl --cacert /path/to/cert.crt виконує перевірку лише за цим файлом. -k повністю вимикає перевірку, тому цей параметр не слід використовувати у скриптах.
Як довго може бути дійсним самопідписаний сертифікат?
Технічно — скільки завгодно. Обмеження CA/Browser Forum, тобто 200 днів зараз і 47 днів до 2029 року, стосуються загальнодовірених центрів сертифікації, а не приватної інфраструктури довіри. На практиці обмежте строк дії сертифікатів сервера 825 днями, оскільки пристрої Apple відхиляють сертифікати з довшим строком незалежно від видавця. Розумним значенням за замовчуванням є приватний кореневий сертифікат строком на десять років і дворічні сертифікати кінцевих об’єктів (-days 730). Заплануйте їх оновлення, оскільки прострочений внутрішній сертифікат може непомітно зупинити все в дату, яку ніхто не пам’ятає.
Що вибрати: самопідписаний сертифікат чи Let's Encrypt?
Якщо сервіс має публічне DNS-ім’я та доступний з інтернету, завжди використовуйте Let's Encrypt: він безкоштовний, автоматизований і вже довірений усіма клієнтами. Самопідписаний сертифікат або приватний центр сертифікації призначені для випадків, у яких Let's Encrypt не може видати сертифікат: приватні IP-адреси, внутрішні імена вузлів на кшталт .lan, ізольовані мережі та сервіси, навмисно приховані за VPN. Вибір визначають доступність і іменування, а не рівень криптографічного захисту. Криптографія однакова.
Чому мій сертифікат відхиляється навіть після додавання до /usr/local/share/ca-certificates?
Перевірте три речі. Ім’я файлу має закінчуватися на .crt. Розширення .pem буде мовчки пропущено, а update-ca-certificates повідомить 0 added. Вміст має бути текстом PEM і починатися з -----BEGIN CERTIFICATE-----, а не двійковими даними DER. Крім того, застосунок справді має використовувати системне сховище. Chrome у Linux, Firefox, Python requests, Node.js і Java мають власні сховища довіри, тому сертифікат потрібно додати до кожного з них окремо.