SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor

Самопідписаний 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 tunnel на вашому VPS, staging-сервера в приватній мережі, трафіку між бекендами, пристрою в домашній лабораторії або заміни сертифіката-заглушки, який 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. Такий сертифікат одразу непридатний. Chrome припинив читати Common Name у версії 58 у квітні 2017 року. RFC 2818 уже оголосив зіставлення за CN застарілим у 2000 році. Firefox і Safari працюють так само. curl і модуль ssl у Python досі використовують Common Name як резервний варіант, якщо в сертифікаті взагалі немає SAN. Через це сертифікат без SAN може пройти швидку перевірку curl, але все одно не працювати в жодному браузері. Для браузера сертифікат ідентифікує сервер через розширення 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 24.04 /etc/ssl/private уже має права 700 root:root, тому лише root може навіть переглянути його вміст; він набуває статусу 710 root:ssl-cert лише якщо встановлено пакет ssl-cert (apache2 має його серед рекомендованих пакетів). У будь-якому разі явно встановіть права для самого файла:

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

nginx і 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 nginx

nginx -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 apache2

configtest має повертати 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.

У цих рядках є 2 важливі нюанси. Файл має закінчуватися на .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/.local/share/pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt для кожного користувача. Цей шлях є типовим для Chrome, починаючи з версії 146. Якщо в користувача досі є застарілий ~/.pki/nssdb від старішої інсталяції, Chrome і надалі читатиме його. Тому на таких машинах вкажіть у -d шлях до sql:$HOME/.pki/nssdb.
  • Firefox має власне сховище: Settings → Privacy & Security → Certificates → Import. У Windows і macOS натомість можна встановити security.enterprise_roots.enabled у значення true в about:config, щоб браузер читав сховище ОС. Mozilla документує цей параметр для Windows і macOS, але не для Linux, тому в Ubuntu скористайтеся імпортом.
  • 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 або налаштуйте читання щойно оновленого системного сховища за допомогою прапорця --use-system-ca (Node 23.9 і новіші версії в Linux) або NODE_USE_SYSTEM_CA=1 (Node 24.6, а також Node 22.19 і новіші версії).

На клієнтах Windows двічі клацніть .crt і встановіть його до сховища Trusted Root Certification Authorities. У macOS додайте його до системного keychain у Keychain Access і встановіть для нього режим Always Trust.

Один root для багатьох сервісів: невеликий приватний CA

Довіра до окремих сертифікатів одразу перестає масштабуватися: шість сервісів помножити на чотири клієнтські машини — це 24 інсталяції сертифікатів довіри, а кожен новий сервіс збільшує їх кількість. Рішення — приватний CA: клієнти довіряють одному root, а ви підписуєте ним сертифікат кожного сервісу.

Зручний варіант — mkcert. Він є в репозиторіях Ubuntu 24.04 і працює зі сховищами NSS (Firefox і Chrome, але з одним застереженням нижче), які update-ca-certificates не обробляє:

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

mkcert -install створює root і реєструє його в системному сховищі, у доступних сховищах NSS браузерів, а також у сховищі Java, якщо встановлено JAVA_HOME. Node не читає системне сховище, якщо явно не вказати це, тому запускайте Node-застосунки з --use-system-ca (або NODE_USE_SYSTEM_CA=1), як зазначено вище, або експортуйте NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem". Третя команда створює git.internal.lan+2.pem і git.internal.lan+2-key.pem, готові для вставлення у фрагменти конфігурації nginx або Apache вище. Це рішення розраховане на машину розробника: ключ root зберігається на машині, де виконано -install. Тому воно добре підходить для ноутбука розробника, але не для серверного парку.

Станом на October 2026 є одне застереження: mkcert шукає базу даних NSS Chrome лише за застарілим шляхом ~/.pki/nssdb, а не за шляхом ~/.local/share/pki/nssdb, який Chrome використовує за замовчуванням починаючи з версії 146. На машині без застарілої бази даних додайте root до Chrome вручну командою certutil з попереднього розділу, використовуючи -i "$(mkcert -CAROOT)/rootCA.pem".

Для серверів звичайний 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 — очікуваний стан до встановлення довіри, а не дефект сертифіката. Якщо він зберігається після встановлення root-сертифіката, можливі такі причини: у Linux Chrome читає NSS, а не системне сховище (див. крок certutil); починаючи з Chrome 146, ця база даних розташована в ~/.local/share/pki/nssdb, якщо не існує застарілий ~/.pki/nssdb, тому root-сертифікат, доданий до іншого шляху, не буде знайдено; скопійований файл не закінчується на .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 поки що не має підстав йому довіряти. Додайте його або кореневий сертифікат вашого приватного CA до сховища довіри клієнта. Пам’ятайте, що в Linux Chrome використовує базу даних NSS через certutil: ~/.local/share/pki/nssdb починаючи з Chrome 146 або застарілий ~/.pki/nssdb, якщо він існує, а не системне сховище. Якщо помилка — 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 року — стосуються CA, яким публічно довіряють, а не приватної довіри. На практиці обмежуйте термін дії сертифікатів сервера 825 днями, оскільки пристрої Apple відхиляють довші сертифікати незалежно від видавця. Десятирічний приватний кореневий сертифікат і дворічні кінцеві сертифікати (-days 730) — розумне налаштування за замовчуванням. Заплануйте поновлення в календарі, оскільки прострочений внутрішній сертифікат може непомітно зупинити всі сервіси в дату, про яку ніхто не пам’ятає.

Що вибрати: самопідписаний сертифікат чи Let's Encrypt?

Якщо сервіс має публічне DNS-ім’я та доступний з інтернету, завжди використовуйте Let's Encrypt: він безкоштовний, автоматизований і вже має довіру більшості браузерів та операційних систем. Якщо потрібно підключати дуже старі пристрої, перевірте список сумісності Let's Encrypt. Самопідписаний сертифікат або приватний CA потрібні для випадків, у яких 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, якщо його не запущено з --use-system-ca, і Java використовують окремі сховища довіри. Сертифікат потрібно додати до кожного з них окремо.