Как создать самоподписанный сертификат на Ubuntu 24.04
Создайте TLS-сертификат с SAN, который принимает Chrome. В статье приведена одна команда openssl, настройка nginx и добавление сертификата в доверенные без использования 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. Такой сертификат неработоспособен с самого начала. Chrome перестал считывать Common Name в версии 58 еще в апреле 2017 года, RFC 2818 признал сопоставление по CN устаревшим еще в 2000 году, а 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 (или указан неверно), и вам нужно выпустить новый. К счастью, исправление выполняется одной командой.
Выпуск сертификата, принимаемого браузерами: одна команда
В 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создает самоподписанный сертификат напрямую, минуя этап создания запроса на подпись.-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должна присутствовать; наличие только DNS-имен в SAN приведет к ошибке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-приложение, Gitea или Python-демон), chown его этому пользователю, сохранив режим 600. Чего делать нельзя: устанавливать режим 644, хранить копию в git-репозитории или в /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-задачи, затем в скрипты развертывания, а потом и в промышленный код, пока никто уже не помнит, какие соединения должны были быть временными. Если 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 имеет собственное хранилище: Настройки → Приватность и защита → Сертификаты → Просмотреть сертификаты → Импортировать, либо измените
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 и установите его в хранилище Доверенные корневые центры сертификации; на macOS добавьте его в системную связку ключей (System keychain) через приложение «Связка ключей» (Keychain Access) и установите значение Всегда доверять.
Один корневой сертификат для множества сервисов: создание локального CA
Доверие к каждому сертификату по отдельности перестает масштабироваться: шесть сервисов на четыре клиентские машины — это двадцать четыре установки доверия, и каждый новый сервис только увеличивает это число. Решение — создание собственного CA (Certificate Authority). Клиенты доверяют одному корневому сертификату, которым вы подписываете сертификат для каждого сервиса.
Удобный вариант — 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 сокращаются. В марте 2026 года CA/Browser Forum ограничил срок действия новых публично доверенных сертификатов 200 днями (ранее 398), с переходом к 100 дням в 2027 году и 47 дням к марту 2029 года. Однако эти правила обязательны только для публично доверенных CA. Ваш частный CA им не подчиняется, а браузеры не применяют эти ограничения к вручную установленным корневым сертификатам. Существует одно реальное ограничение: платформы Apple отклоняют любые TLS-сертификаты сервера со сроком действия более 825 дней, независимо от того, кто их выдал. Поэтому, если к сервису будут подключаться iPhone или Mac, ограничивайте срок действия конечных (leaf) сертификатов двумя годами или менее. -days 730 проходит этот порог везде; корневой сертификат на 10 лет с конечными сертификатами на 2 года — это удобная конфигурация для внутренней сети.
Сертификаты с длительным сроком действия выходят из строя всегда одинаково: незаметно и внезапно в дату, которую никто не помнит. Проверьте текущие сертификаты:
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 указан 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, а не системное хранилище. Если ошибка имеет код 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, изолированные сети (air-gapped) и сервисы, намеренно скрытые за 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 имеют собственные хранилища доверенных сертификатов, в которые сертификат нужно добавлять отдельно.