SSD Nodes Learn
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-07-24

Как создать самоподписанный TLS сертификат

Инструкция по созданию TLS сертификата с SAN для Ubuntu 24.04. Настройка nginx и Apache без использования curl -k для корректной работы в Chrome.

Что вы создаете

Самоподписанный TLS-сертификат, который принимают современные браузеры и клиенты — с корректным subjectAltName, правильными правами доступа к ключам и интеграцией в nginx или Apache. Также вы решите проблему, которую пропускают почти все руководства: вы настроите доверие клиентов к сертификату, чтобы им не приходилось игнорировать предупреждения и прописывать curl -k в скриптах. В итоге вы получите приватный CA из пяти команд, который пригодится, когда количество внутренних сервисов вырастет с одного до шести.

Сначала нужно принять решение, так как самоподписанный сертификат подходит для задач гораздо реже, чем принято считать. Если сервис доступен из публичного интернета по реальному DNS-имени, прекратите чтение и используйте бесплатный сертификат Let's Encrypt с помощью certbot на nginx или аналог для Apache. Это бесплатно, сертификат обновляется автоматически, и все браузеры в мире уже доверяют ему. Самоподписанный сертификат на публичном сайте приучает пользователей игнорировать предупреждения безопасности, что хуже, чем использование обычного HTTP.

Самоподписанный сертификат подходит, когда нет доступа к публичному интернету: панель администратора, привязанная к адресу WireGuard tunnel на вашем VPS, staging-сервер в частной сети, трафик между бэкендами (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 для проверки). Для работы не требуется доступ к интернету; все работает в изолированной среде.

Почему старая команда в одну строку создает сертификаты, которые отклоняет 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, использование 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.

Изменение хранилища доверенных сертификатов (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 — это синтаксис OpenSSL 3.x для старого флага -nodes: ключ без пароля. Работают оба варианта. Ключ с паролем заставит nginx ожидать ввода при каждой загрузке, поэтому для серверного ключа следует использовать этот флаг.
  • -days 730 — срок действия в два года; подробности см. в разделе об истечении срока действия.
  • -subj — позволяет отвечать на интерактивные вопросы в командной строке. Поле CN сейчас носит косметический характер, но все равно укажите там основное имя; некоторые инструменты используют его для отображения.
  • -addext "subjectAltName=..." — основной флаг. Перечислите каждое имя и каждый IP-адрес, которые будут вводить клиенты: записи DNS: для хостов (допустимы wildcard-записи, например 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.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 до того, как команда reload вступит в силу. Если вместо этого выводится 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 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 добавьте его в связку ключей System в приложении Keychain Access и установите параметр Always Trust.

Один корневой сертификат для множества сервисов: небольшой частный CA

Доверительные отношения для каждого сертификата отдельно не масштабируются: шесть сервисов и четыре клиентские машины требуют 24 установки сертификатов, и каждый новый сервис увеличивает это число. Решением является частный 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.1

mkcert -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 как самое ценное: установите права доступа mode 600 и храните его на машине, отличной от серверов, для которых он подписывает сертификаты. Любой, кто получит к нему доступ, сможет выпустить сертификат для любого имени, которому будут доверять ваши клиенты.

Срок действия и ротация

Срок действия сертификатов публичных CA сокращается. В марте 2026 года CA/Browser Forum ограничил срок действия новых публичных сертификатов 200 днями (вместо 398), в 2027 году этот срок составит 100 дней, а к марту 2029 года — 47 дней. Однако эти правила распространяются только на публичные CA. Ваша частная CA не подчиняется этим правилам, и браузеры не применяют их к сертификатам с вручную установленными корневыми сертификатами. Существует одно реальное ограничение: платформы Apple отклоняют любые TLS-сертификаты серверов, срок действия которых превышает 825 дней, независимо от издателя. Если вы используете iPhone или Mac, срок действия конечных сертификатов должен составлять не более двух лет. -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 не содержит имя из адресной строки. Типичный случай: в 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 имеют собственные хранилища доверенных сертификатов, и сертификат нужно добавлять в каждое из них отдельно.

#openssl#tls#self-signed#ubuntu#security