На домашньому NAS крутиться багато web-сервісів – Grafana, VictoriaMetrics, мій власний щоденник на WordPress і ще пів-десятка дрібниць.
Вся серія постів по FreeBSD та NAS починається тут – FreeBSD: Home NAS, part 1 – налаштування ZFS mirror, там станом на зараз 15 частин.
Окремо описаний NGINX+PHP, див. FreeBSD: налаштування FEMP – NGINX, PHP-FPM, MariaDB.
В цілому, хоча це все і доступне тільки в рамках VPN або домашньої мережі – але внутрішня параноя кричить, коли бачить HTTP замість HTTPS, а тому хочеться мати SSL/TLS і налаштувати NGINX з ним.
Купувати сертифікат для такого use case сенсу нема, Let’s Encrypt теж не підійде – бо доступу до NGINX з інтернету нема, а DNS challenge для домашньої зони .setevoy піднімати – то трохи геморою, бо TXT має бути для публічно доступної зони.
Тому просто зробимо свій Certificate Authority з блекджеком і дівчатами, а потім ним підпишемо власний wildcard self-signed сертифікат для NGINX.
Ну а заодно згадаємо як взагалі працюють CA та приватні та публічні сертифікати.
Зміст
Домени для Home NAS
В мене є “домашня” top level domain зона .setevoy в якій живуть всі мої сервіси і яка включає в себе два внутрішні домени:
.aws.setevoy: ресурси в AWS – EC2 для самого блогу RTFM, окремий EC2 для NAT Gateway, і інстанс RDS- зроблено окремою зоною, бо це виключно AWS-related ресурси
.net.setevoy: це вже ресурси в моїх локальних мережах – одна квартира під “офіс”, в якій більшість хостів (сам NAS, MikroTik, робочий ноутбук тощо), та домашня мережа – там тільки домашній ноутбук
Відповідно в домені .net.setevoy будуть адреси:
work.net.setevoy: робочий ноутnas.net.setevoy: ThinkCentre з FreeBSD/NASgw.net.setevoy: MikroTik RB4011
А для веб-сервісів будуть адреси типу grafana.net.setevoy для Grafana, victoria.net.setevoy – для VictoriaMetircs, logs.net.setevoy для VictoriaLogs тощо.
Власне, що для цього всього треба зробити – це wildcard SSL-сертифікат, який потім буде використовуватись в NGINX.
Аби браузери не сварились на нього – створимо власний CA-сертифікат, який потім я додам на свої робочий та домашній ноутбуки, і з власним Certificate Authority підпишемо wildcard-сертифікат для веб-сервісів.
Чому wildcard не на сам .setevoy
Перша думка була “зроблю собі *.setevoy, і буде один сертифікат на все” – але так не вийде, бо wildcard на TLD заборонений і, наприклад, Chrome відкидає такий сертифікат з помилкою ERR_CERT_COMMON_NAME_INVALID.
Формально RFC 6125 – 6.4.3 каже тільки те, що wildcard має бути в найлівішому лейблі (*.example.com – ОК, bar.*.example.net – ні) – і цьому *.setevoy відповідає.
Крім того, RFC прямо нічого не каже про мінімум лейблів (рівнів домену) – це навіть описана проблема цього RFC.
Але на практиці TLS-клієнти додають своє правило, наприклад, GnuTLS документує це явно – див. gnutls_x509_crt_check_hostname2:
wildcards […] are only considered if the domain name consists of three components or more
Тобто *.setevoy (2 компоненти) – не валідно, *.net.setevoy (3 компоненти) – валідно.
Chrome (через BoringSSL) і Firefox (через NSS), судячи з помилки яку я отримав, поводяться так само – хоча я не копав, де саме це у них задокументовано.
Окремо є CA/Browser Forum Baseline Requirements, які забороняють публічним CA видавати такі сертифікати в принципі. Мій CA не публічний – але правила браузерів від цього не перестають діяти.
Тому веб-сервіси будуть в зоні .net.setevoy, а wildcard буде для *.net.setevoy.
SSL vs TLS
Їх часто плутають, та я і сам в блозі пишу то “SSL”, то “TLS”, то просто “SSL/TLS”.
Власне, в чому різниця:
- SSL (Secure Sockets Layer):
- оригінальний протокол від Netscape, 90-ті роки, див. The Origins of Web Security and the Birth of Security Socket Layer (SSL) Protocol
- SSL 2.0, SSL 3.0 давно deprecated, див. Deprecated SSL/TLS Versions
- TLS (Transport Layer Security):
- це фактично SSL 4.0, просто перейменований коли стандарт передали в IETF, див. History of SSL/TLS
- зараз актуальні TLS 1.2 і TLS 1.3
Тобто коли хтось каже “SSL сертифікат” або “налаштувати SSL” – мається на увазі TLS. Це як “ксерокс” замість “копіювальний апарат” – всі розуміють, але технічно неточно. В тексті далі буду казати “SSL/TLS” або просто “SSL” чи “TLS” – це все про одне й те саме.
Що таке Certificate Authority
Certificate Authority – це центр, який має право підписувати сертифікати, і якому довіряють клієнти (браузери, операційні системи).
Коли ми створюємо сертифікат через Let’s Encrypt – він підписується сертифікатом компанії Let’s Encrypt.
Коли через AWS Certificate Manager – то сертифікатом Amazon.
У випадку Cloudflare – issuer буде Google Trust Services:
$ openssl s_client -connect rtfm.co.ua:443 </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject issuer=C=US, O=Google Trust Services, CN=WE1 subject=CN=rtfm.co.ua
Тобто, сертифікат виданий issuer=Google Trust Services, підписаний Google Trust Services CA, а виданий сертифікат для subject=rtfm.co.ua.
Всі публічні Certificate Authority сертифікати йдуть “в комплекті” браузера або операційної системи.
Наприклад, в Google Chrome список доступний в chrome://settings/certificates > Chrome Root Store:
Linux та ca-certificates
В Arch Linux за CA-сертифікати відповідають кілька пакетів – ca-certificates-utils та ca-certificates-mozilla.
Хоча насправді тут доволі цікавий ланцюжок.
Наприклад, пакет curl має в залежностях мета-пакет
$ pacman -Qi curl Name : curl ... Depends On : ca-certificates ...
Пакет ca-certificates має в залежностях пакет ca-certificates-mozilla:
$ pacman -Qi ca-certificates Name : ca-certificates ... Depends On : ca-certificates-mozilla ...
А ca-certificates-mozilla тягне за собою пакет ca-certificates-utils:
$ pacman -Qi ca-certificates-mozilla Name : ca-certificates-mozilla ... Depends On : ca-certificates-utils>=20181109-3 ...
Пакет ca-certificates-utils створює каталоги (/etc/ca-certificates/, /etc/ssl/certs/), додає man pages та встановлює утиліту /usr/bin/update-ca-trust.
Пакет ca-certificates-mozilla додає в систему файл /usr/share/ca-certificates/trust-source/mozilla.trust.p11-kit, який містить всі публічні CA-сертифікати.
Наприклад, вже згаданий вище “Organization=Google Trust Services” з “CommonName=GTS Root R1“:
$ cat /usr/share/ca-certificates/trust-source/mozilla.trust.p11-kit | grep -A 10 "Google Trust Services" # Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R1 # Validity # Not Before: Jun 22 00:00:00 2016 GMT # Not After : Jun 22 00:00:00 2036 GMT # Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R1 # Subject Public Key Info: # Public Key Algorithm: rsaEncryption # Public-Key: (4096 bit) # Modulus: # 00:b6:11:02:8b:1e:e3:a1:77:9b:3b:dc:bf:94:3e: # b7:95:a7:40:3c:a1:fd:82:f9:7d:32:06:82:71:f6: # f6:8c:7f:fb:e8:db:bc:6a:2e:97:97:a3:8c:4b:f9: # 2b:f6:b1:f9:ce:84:1d:b1:f9:c5:97:de:ef:b9:f2: # a3:e9:bc:12:89:5e:a7:aa:52:ab:f8:23:27:cb:a4: # b1:9c:63:db:d7:99:7e:f0:0a:5e:eb:68:a6:f4:c6: ...
А update-ca-trust – це bash-скрипт, який викликає утиліту /usr/bin/trust і витягує сертифікати в каталог DEST=/etc/ca-certificates/extracted/cadir:
$ ll /etc/ca-certificates/extracted/cadir/ | grep GTS_Root_R1 -r--r--r-- 1 root root 1.9K Apr 3 14:57 GTS_Root_R1.pem
Та потім створює сімлінки в /etc/ssl/certs/.
$ ll /etc/ssl/certs/ | grep GTS_Root_R1 lrwxrwxrwx 1 root root 53 Apr 3 14:57 GTS_Root_R1.pem -> ../../ca-certificates/extracted/cadir/GTS_Root_R1.pem
Глянути наявні сертифікати можемо з trust list:
$ trust list | grep -B2 -A 2 "GTS Root R1"
pkcs11:id=%E4%AF%2B%26%71%1A%2B%48%27%85%2F%52%66%2C%EF%F0%89%13%71%3E;type=cert
type: certificate
label: GTS Root R1
trust: anchor
category: authority
Власне, що ми будемо робити: створимо власний root-key нашого Certificate Authority, ним підпишемо TLS-сертифікат для NGINX, а потім сертифікат нашого Certificate Authority додамо в trusted store на робочих машинах.
Файли CA, CSR, CRT, KEY
Тут хочу окремо зупинитись, бо насправді не так часто щось роблю руками з сертифікатами, і від кількості пов’язаних файлів можна потірятись.
Отже, у нас будуть дві пари ключ+сертифікат.
Пара 1 – наш Certificate Authority:
ca-private.key: приватний ключ CA- використовується виключно для підпису інших сертифікатів, зберігається окремо від сертифікатів NGINX
ca-public.crt: публічний сертифікат CA- підписується
ca-private.key– власне – тому ця схема і є “self-signed” – ми самі собі підписуємо публічний сертифікат, який потім додаємо в trust store хостів
- підписується
Пара 2 – для NGINX:
wildcard.net.setevoy.key: приватний ключ NGINX- лежить на сервері і нікому не передається
- під час TLS handshake NGINX ним підписує challenge від клієнта, чим доводить що володіє ключем (сам ключ по мережі не йде)
wildcard.net.setevoy.crt: фінальний публічний сертифікат веб-сервера, підписаний нашим CA- це CSR + підпис від
ca-private.key - саме цей файл NGINX і віддає браузеру
- це CSR + підпис від
Окремо будемо створювати файл Certificate Signing Request (CSR) – wildcard.net.setevoy.csr і який буде використовуватися для створення підпису публічного сертифікату wildcard.net.setevoy.crt.
Процес валідації сертифікатів
Тепер розберемо як саме CA використовується для перевірки сертифіката від NGINX.
Тут приклади на вже готових файлах.
У нас буде файл wildcard.net.setevoy.crt, який NGINX передає клієнту під час підключення і який підписаний ca-private.key – приватним ключем CA.
Клієнт має у своєму trust store публічний сертифікат CA – ca-public.crt, використовуючи який він має впевнитись, що wildcard.net.setevoy.crt був підписаний саме ca-private.key.
Файл wildcard.net.setevoy.crt містить в собі набір полів:
# openssl x509 -in wildcard.net.setevoy.crt -noout -text
Certificate:
Data:
...
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = UA, ST = Kyiv, O = Setevoy Home NAS, CN = Setevoy CA
Validity
Not Before: Apr 18 11:35:59 2026 GMT
Not After : Jul 21 11:35:59 2028 GMT
Subject: C = UA, ST = Kyiv, O = Setevoy Home NAS, CN = *.net.setevoy
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:dd:c6:f7:e1:13:1c:dd:91:44:37:d5:75:09:ca:
fb:16:a5:80:22:23:42:6e:6b:7c:1f:08:dd:25:f3:
7f:bd:05:13:74:79:76:de:d7:2b:f8:4c:bd:4c:a5:
...
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
3d:24:95:55:cd:fb:c6:af:35:59:bc:dd:f6:05:fb:da:c9:51:
f1:37:38:79:f0:e8:62:4a:5c:bc:f3:da:4b:45:8c:39:75:f4:
3c:e5:3f:73:89:e6:8a:93:79:52:d7:8e:08:b0:50:02:ce:e9:
18:63:4d:cd:ef:be:fa:78:f2:ed:01:db:77:e8:30:d7:b6:27:
...
Для підписання цього сертифіката CA бере хеш (SHA-256) від усієї секції Data (subject, issuer, public key, validity, SAN, …), шифрує цей хеш своїм приватним ключем ca-private.key і прикріплює результат до сертифіката як поле “Signature Value“.
Коли клієнт отримує wildcard.net.setevoy.crt від NGINX – він перевіряє значення issuer, бачить там “Setevoy CA“, і шукає у своєму trust store сертифікат CA з таким subject – це буде наш ca-public.crt.
Тепер у клієнта є wildcard.net.setevoy.crt з Signature Value, і є ca-public.crt, після чого:
- клієнт бере секцію
Dataз сертифіката і сам обчислює її SHA-256 хеш – назвемо цей хеш “H1“ - бере значення
Signature Valueі розшифровує його публічним ключем CA (який лежить всерединіca-public.crt) – це буде хеш “H2“
Якщо хеши співпадають – то підпис дійсно був зроблений парним приватним ключем до публічного ключа CA, ca-private.key.
“H1” та “H2” тут – чисто умовні позначення, аби простіше було розібратись з тим, що будемо робити нижче.
Demo: перевірка підпису сертифікату
Виглядає класно в теорії – але давайте глянемо на практиці, як цей механізм працює.
Створимо файл data.txt – це буде наш умовний блок Data із сертифікату wildcard.net.setevoy.crt:
$ echo "Hello, this is our Data block" > data.txt
Створимо приватний ключ – це наш умовний ca-private.key, приватний ключ CA:
$ openssl genrsa -out demo.key 2048
З ca-private.key ми будемо підписувати хеш від data.txt.
Отримуємо з demo.key публічну частину – це буде наш умовний ca-public.crt, публічний сертифікат CA:
$ openssl rsa -in demo.key -pubout -out demo.pub
З ca-public.crt ми повинні мати змогу перевірити підпис від ca-private.key та отримати оригінальні дані.
А тепер сама цікава частина.
Отримуємо хеш даних в data.txt – це буде наш умовний “H1“:
$ openssl dgst -sha256 data.txt SHA2-256(data.txt)= 959af28af72380bb03c44bf734d886a4ee3302d83a6edb0283a428e9850b9b68
Це той самий хеш, який клієнт буде обчислювати самостійно з блоку Data сертифіката від NGINX.
Підписуємо дані використовуючи приватний ключ CA:
$ openssl dgst -sha256 -sign demo.key -out signature.bin data.txt
Тепер у файлі signature.bin маємо 256 байт – це, власне – той самий “Signature Value” із сертифіката від NGINX, тільки у нас цей Value лежить окремим файлом, а не полем в сертифікаті:
$ od -An -tx1 signature.bin | tr -d ' \n' | head -c 200 203f65033571f3c7...d51b
Далі нам треба розшифрувати цей хеш, використовуючи публічний сертифікат CA:
$ openssl pkeyutl -verifyrecover -pubin -inkey demo.pub -in signature.bin -out decrypted.bin
Перевіряємо його зміст:
$ openssl asn1parse -inform DER -in decrypted.bin
...
4:d=2 hl=2 l= 9 prim: OBJECT :sha256
...
17:d=1 hl=2 l= 32 prim: OCTET STRING [HEX DUMP]:959AF28AF72380BB03C44BF734D886A4EE3302D83A6EDB0283A428E9850B9B68
Бачимо той самий хеш “959AF28AF…850B9B68” – це наш умовний H2, і він точно дорівнює H1, який ми отримали кілька кроків тому.
Підпис валідний – значить його зробив той, хто має приватний ключ, парний до demo.pub.
Те ж саме клієнт робить кожного разу при підключенні до NGINX – тільки замість demo.pub використовує публічний ключ з ca-public.crt у своєму trust store.
Все – досить теорії.
Давайте тепер створювати ключі і сертифікати.
План дій – Certificate Authority та NGINX
Нам треба буде створити файли нашого CA, а потім – файли для NGINX:
- створимо приватний ключ, ним підпишемо сертифікат для CA – отримаємо self-signed Public CA certificate
- створимо приватний ключ для NGINX – він буде використовуватись під час TLS Handshake для встановлення безпечного з’єднання
- створимо CSR з потрібними CN та SAN – доменами, для яких буде валідним публічний сертифікат NGINX
- з цим CSR та нашим приватним ключем CA отримаємо сертифікат для NGINX
- налаштуємо virtualhost в NGINX з приватним ключем та сертифікатом
- додамо публічний сертифікат CA в trusted store на FreeBSD і Linux
Створення власного Certificate Authority
На FreeBSD (в моєму випадку, але процес ідентичний на будь-якому Linux) створюємо каталог:
# mkdir -p /usr/local/etc/ssl/setevoy/NasCA/ # cd /usr/local/etc/ssl/setevoy/NasCA/
Генеруємо приватний ключ CA на 4096 біт:
# openssl genrsa -out ca-private.key 4096
З цим ключем генеруємо публічний self-signed сертифікат нашого CA:
# openssl req -new -x509 -days 3650 -key ca-private.key -out ca-public.crt -subj "/C=UA/ST=Kyiv/O=Setevoy Home NAS/CN=Setevoy CA"
Тут:
-new -x509: генеруємо новий self-signed сертифікат (а не CSR)-days 3650: сертифікат валідний 10 років (для root CA норм)-key ca-private.key: підписуємо приватним ключем CA, який створили вище-out ca-public.crt: куди зберегти публічний сертифікат-subj: метадані сертифіката – поле CN потім будемо бачити в браузері
Перевіряємо:
# openssl x509 -in ca-public.crt -noout -issuer -subject issuer=C = UA, ST = Kyiv, O = Setevoy Home NAS, CN = Setevoy CA subject=C = UA, ST = Kyiv, O = Setevoy Home NAS, CN = Setevoy CA
Власне, issuer і subject однакові: це і є self-signed сертифікат – бо виданий від “Setevoy CA” для “Setevoy CA“.
Сертифікат для *.net.setevoy
В NGINX можна було б використати ключ ca-private.key напряму – але це наш “рутовий” ключ, і якщо NGINX зламають – атакуючий зможе підписувати ним будь-що, тому для NGINX робимо окремий ключ.
Створюємо приватний ключ для NGINX – тут вже можна зробити 2048 біт, а не 4096, як для рутового ключа CA:
# openssl genrsa -out wildcard.net.setevoy.key 2048
Тепер генеруємо CSR – Certificate Signing Request:
# openssl req -new -key wildcard.net.setevoy.key -out wildcard.net.setevoy.csr -subj "/C=UA/ST=Kyiv/O=Setevoy Home NAS/CN=*.net.setevoy"
Тут:
req -new: генеруємо новий CSR (без -x509, бо це не сертифікат)-key wildcard.net.setevoy.key: використовуємо приватний ключ, який створили вище – його публічна частина піде в CSR-out wildcard.net.setevoy.csr: куди зберегти сам Certificate Signing RequestCN=*.net.setevoy: wildcard, покриває всі сабдомени*.net.setevoy(хоча CN ролі не грає, див. далі)
Перевіряємо що в CSR:
# openssl req -in wildcard.net.setevoy.csr -noout -subject subject=C = UA, ST = Kyiv, O = Setevoy Home NAS, CN = *.net.setevoy
Common Name та Subject Alternative Name
Тут є нюанс, на якому я сам спіткнувся: сучасні браузери і клієнти ігнорують Common Name (CN) і дивляться тільки на поле Subject Alternative Name (SAN), тому значення в полі CN недостатньо – треба додати SAN з усіма іменами, які покриває сертифікат.
Історія цього питання довга. RFC 2818 задепрікейтив CN на користь SAN ще у 2000, але залишив fallback:
If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used. Although the use of the Common Name is existing practice, it is deprecated and Certification Authorities are encouraged to use the dNSName instead.
Google Chrome повністю прибрав підтримку CN-matching у 2017, див. Remove support for commonName matching in certificates, Firefox і всі сучасні TLS-клієнти зробили те саме.
А вже RFC 9525 у 2023 офіційно прибрав CN-перевірку з самого стандарту – див. Identifying Application Services:
The Common Name RDN MUST NOT be used to identify a service because it is not strongly typed (it is essentially free-form text) and therefore suffers from ambiguities in interpretation.
Тобто сертифікат без SAN сьогодні – це гарантована помилка валідації, незалежно від того що в CN.
Окремий момент: wildcard *.net.setevoy покриває рівно один рівень піддомену, тобто test-ssl.net.setevoy – так, але саме net.setevoy (без префіксу) – ні. Тому в SAN додаємо обидва записи.
Створюємо файл san.cnf – можна мінімальний:
[v3_req] subjectAltName = DNS:*.net.setevoy, DNS:net.setevoy
Або, якщо робити більше кошерно і по шаблону OpenSSL (див. x509v3_config) – то файл буде таким:
[req] req_extensions = v3_req distinguished_name = req_distinguished_name [req_distinguished_name] [v3_req] subjectAltName = @alt_names [alt_names] DNS.1 = *.net.setevoy DNS.2 = net.setevoy
Тут:
[req]: секція для командиopenssl req(генерація CSR)req_extensions = v3_req: підтягувати розширення з секції[v3_req]distinguished_name = req_distinguished_name: поляsubject(CN, O, C) брати з секції[req_distinguished_name][req_distinguished_name]: порожня, боsubjectми передаємо через-subjпрямо в команді[v3_req]: секція з розширеннями, які підуть у сертифікатsubjectAltName = @alt_names: значення SAN брати зі секції[alt_names],@означає “посилання на секцію”[alt_names]: власне список DNS-іменDNS.1,DNS.2: workaround обмеження OpenSSL – ключ DNS в одній секції може зустрічатись тільки раз, тому додають .1, .2
Створюємо сам публічний сертифікат для NGINX, який підписується нашим приватним ключем CA:
# openssl x509 -req -days 825 -in wildcard.net.setevoy.csr -CA ca-public.crt -CAkey ca-private.key -CAcreateserial -out wildcard.net.setevoy.crt -extensions v3_req -extfile san.cnf Certificate request self-signature ok subject=C = UA, ST = Kyiv, O = Setevoy Home NAS, CN = *.net.setevoy
Опції тут:
x509 -req: підписуємо CSR і робимо з нього сертифікат-CA ca-public.crt -CAkey ca-private.key: підписуємо нашим CA-CAcreateserial: генерує серійний номер сертифіката (буде створений файлca.srl)-days 825: максимум, який приймає Chrome/Safari без скарг (це обмеження Apple з 2020 року, див. Apple Cuts SSL Validity Period to 13 Months Effective September 1)-extfile san.cnf -extensions v3_req: додаємо список SAN з конфіга (без цього SAN не запишеться в сертифікат, навіть якщо він був у CSR)
Сама послідовність створення сертифікату із CSR така:
- на вході у нас CSR (
wildcard.net.setevoy.csr) – заявка з полямиsubject,public key, SAN - OpenSSL бере дані з CSR (
subject,public key), додає від себе кілька полів (issuer = Setevoy CA,validity,serial number,extensionsзsan.cnf), збирає це все в нову структуруData - хешує цю структуру
Data, шифрує хеш приватним ключем CA – отримує Signature Value - на виході збирає
Data+Signature Valueв один файл – це і єwildcard.net.setevoy.crt
Перевіряємо що у нас в новому сертифікаті:
# openssl x509 -in wildcard.net.setevoy.crt -noout -issuer -subject -dates issuer=C = UA, ST = Kyiv, O = Setevoy Home NAS, CN = Setevoy CA subject=C = UA, ST = Kyiv, O = Setevoy Home NAS, CN = *.net.setevoy notBefore=Apr 18 11:35:59 2026 GMT notAfter=Jul 21 11:35:59 2028 GMT
Тепер issuer – це Setevoy CA, а subject – наш wildcard. Це означає, що сертифікат підписаний саме нашим CA, а не сам собою.
Перевіряємо що SAN на місці:
# openssl x509 -in wildcard.net.setevoy.crt -noout -ext subjectAltName
X509v3 Subject Alternative Name:
DNS:*.net.setevoy, DNS:net.setevoy
В результаті маємо три файли:
wildcard.net.setevoy.key: приватний ключ для NGINXwildcard.net.setevoy.csr: Certificate Signing Request, який використовували для створення сертифікатуwildcard.net.setevoy.crt: власне – сам сертифікат, який буде віддаватись клієнтам
Налаштування SSL в NGINX
Створюємо каталог для сертифікатів:
# mkdir -p /usr/local/etc/nginx/ssl
Копіюємо сертифікат і ключ:
# cp /usr/local/etc/ssl/setevoy/NasCA/wildcard.net.setevoy.crt /usr/local/etc/nginx/ssl # cp /usr/local/etc/ssl/setevoy/NasCA/wildcard.net.setevoy.key /usr/local/etc/nginx/ssl
Доступ до приватного ключа залишаємо тільки root:
# chmod 600 /usr/local/etc/nginx/ssl/wildcard.net.setevoy.key
Загальні параметри SSL виносимо в окремий файл /usr/local/etc/nginx/conf.d/ssl.conf, щоб не дублювати в кожному віртуалхості:
ssl_certificate /usr/local/etc/nginx/ssl/wildcard.net.setevoy.crt; ssl_certificate_key /usr/local/etc/nginx/ssl/wildcard.net.setevoy.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
І сам віртуалхост, наприклад /usr/local/etc/nginx/conf.d/test-ssl.net.setevoy.conf:
server {
listen 80;
server_name test-ssl.net.setevoy;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name test-ssl.net.setevoy;
include /usr/local/etc/nginx/conf.d/ssl.conf;
location / {
root /usr/local/www/nginx;
index index.html;
}
}
Хоча include /usr/local/etc/nginx/conf.d/ssl.conf можна взагалі винести в nginx.conf в секцію http{}.
Перевіряємо конфіг і ребутаємо:
# nginx -t && service nginx reload nginx: the configuration file /usr/local/etc/nginx/nginx.conf syntax is ok nginx: configuration file /usr/local/etc/nginx/nginx.conf test is successful
Спробуємо curl:
# curl https://test-ssl.net.setevoy curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20) More details here: https://curl.se/docs/sslcerts.html curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it. To learn more about this situation and how to fix it, please visit the webpage mentioned above.
Це очікувано: curl не знає нашого CA, бо ми його ще нікуди не додали.
Додавання CA в локальні trusted store
Аби перевірка проходила без помилок – треба публічний сертифікат CA додати на всі хости в їхні trust store.
FreeBSD та certctl
Копіюємо CA-сертифікат у системний каталог:
# cp /usr/local/etc/ssl/setevoy/NasCA/ca-public.crt /usr/local/share/certs/setevoy-nas-ca.crt
Оновлюємо trusted store (займе пару хвилин):
# certctl rehash
Перевіряємо:
# certctl list | grep -i setevoy certctl: Listing Trusted Certificates: f6c33121.0 Setevoy CA
І тепер curl з хоста FreeBSD працює без помилок:
# curl https://test-ssl.net.setevoy <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
Arch Linux та trust
Копіюємо ca-public.crt з FreeBSD на ноутбук з Arch Linux:
[setevoy@setevoy-work ~] $ scp [email protected]:/usr/local/etc/ssl/setevoy/NasCA/ca-public.crt setevoy-nas-ca.crt ca-public.crt
Кладемо в системний trust source:
[setevoy@setevoy-work ~] $ sudo cp setevoy-nas-ca.crt /etc/ca-certificates/trust-source/anchors/
Оновлюємо:
[setevoy@setevoy-work ~] $ sudo update-ca-trust
Тепер маємо сімлінк в /etc/ssl/certs/:
[setevoy@setevoy-work ~] $ ll /etc/ssl/certs/ | grep Sete lrwxrwxrwx 1 root root 52 Apr 18 15:02 Setevoy_CA.pem -> ../../ca-certificates/extracted/cadir/Setevoy_CA.pem
І бачимо сертифікат в trust list:
[setevoy@setevoy-work ~] $ trust list | grep -i "setevoy"
label: Setevoy CA
Перевіряємо з curl:
[setevoy@setevoy-work /tmp] $ curl https://test-ssl.net.setevoy <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
Bonus: швидкий debug сертифікатів
Якщо браузер чи curl все ще скаржиться на сертифікат – корисні команди для перевірки.
Подивитись, що саме віддає NGINX:
$ openssl s_client -connect test-ssl.net.setevoy:443 </dev/null 2>/dev/null | openssl x509 -noout -issuer -subject -ext subjectAltName
issuer=C=UA, ST=Kyiv, O=Setevoy Home NAS, CN=Setevoy CA
subject=C=UA, ST=Kyiv, O=Setevoy Home NAS, CN=*.net.setevoy
X509v3 Subject Alternative Name:
DNS:*.net.setevoy, DNS:net.setevoy
Тут перевіряємо: правильний issuer (наш CA), правильний subject, і головне – Subject Alternative Name з потрібним хостом.
Перевірити підключення з конкретним CA, не додаючи його в систему:
[setevoy@setevoy-work /tmp] $ curl --cacert ./setevoy-nas-ca.crt https://test-ssl.net.setevoy <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
Якщо з --cacert працює, а без нього – ні, значить CA не доїхав до системного trust store – перевіряємо update-ca-trust / certctl rehash.
Перевірити повний chain:
$ openssl s_client -connect test-ssl.net.setevoy:443 -showcerts Connecting to 192.168.0.2 CONNECTED(00000003) depth=0 C=UA, ST=Kyiv, O=Setevoy Home NAS, CN=*.net.setevoy verify error:num=20:unable to get local issuer certificate verify return:1 depth=0 C=UA, ST=Kyiv, O=Setevoy Home NAS, CN=*.net.setevoy verify error:num=21:unable to verify the first certificate verify return:1 depth=0 C=UA, ST=Kyiv, O=Setevoy Home NAS, CN=*.net.setevoy verify return:1 --- Certificate chain 0 s:C=UA, ST=Kyiv, O=Setevoy Home NAS, CN=*.net.setevoy i:C=UA, ST=Kyiv, O=Setevoy Home NAS, CN=Setevoy CA a:PKEY: RSA, 2048 (bit); sigalg: sha256WithRSAEncryption v:NotBefore: Apr 18 11:35:59 2026 GMT; NotAfter: Jul 21 11:35:59 2028 GMT ...
Браузери та сертифікати CA
Окремий момент щодо браузерів: Firefox має власний trust store і не дивиться в системний тому для Firefox власний сертифікат CA треба додавати окремо через about:preferences#privacy > “View Certificates”:
Далі Import:
І тепер працює без помилок:
Google Chrome, Brave, Vivaldi і решта на Linux зазвичай використовують системний trust store, але можна імпортувати вручну на сторінці chrome://certificate-manager/localcerts:
![]()






