Як створити самопідписаний сертифікат на Ubuntu
Інструкція з генерації TLS сертифіката з SAN для Ubuntu 24.04. Налаштування nginx та Apache без використання curl -k для повної довіри браузерів.
Що ви створюєте
Самопідписаний TLS-сертифікат, який фактично приймають сучасні браузери та клієнти — з правильним subjectAltName, коректними правами доступу до ключа та інтеграцією в nginx або Apache. Також ми розберемо те, про що майже всі інструкції забувають: як змусити клієнтів довіряти йому належним чином, замість того, щоб постійно ігнорувати попередження та жорстко прописувати curl -k у скриптах. У результаті ви отримаєте приватний CA з п'ятьма командами, який знадобиться, коли кількість внутрішніх сервісів зросте з одного до шести.
Спочатку — вибір інструменту, оскільки самопідписаний сертифікат доцільно використовувати набагато рідше, ніж це прийнято. Якщо сервіс доступний з публічного інтернету за справжнім DNS-іменем, припиніть читання та отримайте безкоштовний сертифікат Let's Encrypt за допомогою certbot на nginx або аналогічний для Apache. Це безкоштовно, сертифікат оновлюється автоматично, і кожен браузер у світі вже йому довіряє. Самопідписаний сертифікат на публічному сайті привчає користувачів ігнорувати попередження безпеки, що є гіршою звичкою, ніж використання звичайного HTTP.
Самопідписаний сертифікат є правильним вибором, коли немає доступу до публічного інтернету: панель адміністратора, прив'язана до адреси WireGuard тунелю на вашому VPS, стейджинг-сервер у приватній мережі, трафік між бекендами (service-to-service), пристрій у домашній лабораторії або заміна стандартного сертифіката, який 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 для підтвердження). Для роботи не потрібен доступ до інтернету; все працює в ізольованому середовищі (air-gapped).
Чому старий однорядковий метод створює сертифікати, які 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Вона ставить серію інтерактивних запитань, вписує ваш hostname у поле Common Name і створює сертифікат без розширення subjectAltName. Такий сертифікат недієвий. Chrome перестав зчитувати Common Name у версії 58 у квітні 2017 року — RFC 2818 ще у 2000 році оголосив відповідність 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.Жодні маніпуляції з trust-store не виправлять цю помилку, оскільки сертифікат фактично не містить імені. Якщо ви бачите NET::ERR_CERT_COMMON_NAME_INVALID прямо зараз, ваш сертифікат не має SAN (або має неправильний SAN), тому вам потрібно створити новий. На щастя, виправлення потребує лише однієї команди.
Створення сертифіката, який приймають браузери: одна команда
У версії OpenSSL 1.1.1 було додано прапор -addext. Тепер не потрібно використовувати складні конфігураційні файли для додавання 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створює самопідписаний сертифікат замість запиту на підпис (CSR).-newkey rsa:4096генерує новий ключ на тому ж етапі. RSA 4096 підтримується всіма застарілими клієнтами; якщо всі клієнти сучасні,-newkey ec -pkeyopt ec_paramgen_curve:P-256працює швидше і займає менше місця.-noenc— це назва прапора-nodesу OpenSSL 3.x: ключ без пароля. Обидва варіанти назв працюють. Ключ із паролем змусить nginx чекати на введення даних при кожному запуску, тому для серверних ключів краще використовувати цей варіант.-days 730— термін дії два роки; детальніше про цей термін у розділі про закінчення терміну дії.-subj— автоматично відповідає на інтерактивні запитання. Поле CN зараз має лише косметичне значення, але все одно вкажіть основне ім'я; деякі інструменти його відображають.-addext "subjectAltName=..."— основний прапор. Перелічіть кожне ім'я та кожну IP-адресу, які використовуватимуть клієнти:DNS:для хостнеймів (wildcards на кшталтDNS:*.internal.lanдозволені),IP:для 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 до того, як reload виконає будь-які дії. Якщо замість цього виводиться SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, це означає, що сертифікат і ключ були згенеровані під час різних запусків — див. розділ failure-modes.
Налаштування для 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 job, потім у скрипті розгортання, а потім у робочому коді, доки ніхто не пам'ятатиме, які з'єднання мали бути тимчасовими. Якщо 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 і зберігайте його на машині, яка не є одним із серверів, для яких він підписує сертифікати. Будь-хто, хто отримає доступ до цього ключа, зможе створити сертифікат для будь-якого імені, якому довіряють ваші клієнти.
Expiry and rotation
Термін дії сертифікатів публічних CA скорочується. У березні 2026 року CA/Browser Forum обмежив термін дії нових публічно довірених сертифікатів 200 днями (замість 398). У 2027 році цей термін становитиме 100 днів, а до березня 2029 року — 47 днів. Ці правила стосуються лише публічно довірених CA. Ваша приватна CA не підпадає під ці обмеження, а браузери не застосовують їх до вручну встановлених кореневих сертифікатів. Одна реальна зміна вже діє: платформи Apple відхиляють будь-які TLS-сертифікати серверів, термін дії яких перевищує 825 днів, незалежно від емітента. Якщо ви використовуєте iPhone або Mac, тримайте термін дії кінцевих (leaf) сертифікатів не більше двох років. -days 730 забезпечує відповідність цим вимогам у всіх системах; схема з десятирічним кореневим сертифікатом та дворічними кінцевими сертифікатами є оптимальною для внутрішнього використання.
Довгострокові сертифікати відмовляють лише одним способом: вони перестають працювати одночасно і без попереджень у дату, про яку ніхто не пам'ятить. Перевірте наявні сертифікати:
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 все ще показує "Not secure" після створення самопідписаного сертифіката?
Якщо виникає помилка NET::ERR_CERT_AUTHORITY_INVALID, сертифікат справний — Chrome просто ще не має причин йому довіряти. Встановіть його (або ваш приватний корінь CA) у сховище довірених сертифікатів клієнта. Пам'ятайте, що в 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 днів, до 2029 року — 47 днів) стосуються лише публічних CA, а не приватних. На практиці встановлюйте термін дії серверних сертифікатів не більше 825 днів, оскільки пристрої Apple відхиляють сертифікати з довшим терміном незалежно від видавця. Раціональним стандартом є приватний корінь на десять років та сертифікати кінцевих вузлів на два роки (-days 730). Обов'язково заплануйте оновлення, оскільки прострочений внутрішній сертифікат безпосередньо зупиняє роботу всіх сервісів у непередбачуваний момент.
Що краще використовувати: самопідписаний сертифікат чи Let's Encrypt?
Якщо сервіс має публічне DNS-ім'я та доступний з інтернету, завжди використовуйте 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 та Java мають власні сховища довірених сертифікатів, тому сертифікат потрібно додавати в кожне з них окремо.