← все статьи

NAS, день восемнадцатый: https для ящика — свой центр сертификатов, который не подпишет банк

Восемнадцатый день NAS. Хотелось поставить панель ящика на экран телефона приложением — чтобы пуш о сбоящем диске приходил сразу. Всё для этого уже было, не хватало одной буквы: https. Эта запись — как мы дали домашнему ящику свой центр сертификатов и сделали так, чтобы он не мог подписать ничего, кроме самого ящика.

Всё было готово, кроме одной буквы

От проекта-донора панели достались манифест, service worker, значки под iPhone и Android, плашка «Установить» и пошаговая инструкция. Но панель открывалась по http.

А браузеры ставят сайт приложением и пускают его воркер только по https или на localhost. На iPhone по http выходит лишь значок без пушей, у Chrome кнопки «Установить» нет вовсе.

Let's Encrypt не поможет: ящик живёт в домашней сети по адресу вроде 192.168.1.20 и наружу не смотрит. Доказать публичному центру, что этот адрес наш, нельзя. Значит, нужен свой центр, которому телефоны и ноутбуки один раз доверят.

Почему не взяли готовый

Свой центр у нас уже был — у Сторожа, нашего внешнего мониторинга. Соблазн взять его был большой.

Из Сторожа мы взяли рецепт, а не сам центр.

Центр, который умеет подписать только ящик

У ящика появился свой центр. Главное в нём — одно поле сертификата, про которое редко вспоминают: nameConstraints, ограничение имён. В нём перечислено, для каких адресов центр вообще имеет право подписывать: адрес ящика, его имя, если оно есть, и localhost. Ничего больше.

openssl x509 -in ca.crt -noout -subject -ext basicConstraints,nameConstraints
subject=CN=NAS localhost, O=NAS
X509v3 Basic Constraints: critical
CA:TRUE, pathlen:0
X509v3 Name Constraints: critical
Permitted:
DNS:localhost
IP:127.0.0.1/255.255.255.255
Сертификат центра на стенде: подписывать можно только localhost и 127.0.0.1 — на ящике там его адрес

Поле помечено как критичное: браузер, который его не понимает, обязан отвергнуть сертификат, а не пропустить молча. Даже если ключ центра утечёт, им не выпустить сертификат ни для банка, ни для роутера на соседнем адресе. Утечка ключа становится бедой одного ящика, а не всего интернета на телефоне.

Ключ центра лежит только на самом ящике, с правами 600, и видит его только API. В базу и в ночные копии он не попадает.

Как устроено

Центр и сертификат выпускает сам API ящика на Python библиотекой cryptography, а не скрипт на хосте. Так одинаково работают все три формы установки: свой ящик, ящик рядом с TrueNAS и стенд разработчика.

  1. Выпуск. Раз в 6 часов API смотрит, свеж ли сертификат. Он выдаётся на 397 дней, новый выпускается за 60 дней* до конца — или сразу, если у ящика поменялось имя.

  2. Подхват. nginx берёт сертификат не при старте, а по переменной: читает файл на каждом рукопожатии. Перевыпуск не требует перезапуска, а без сертификата nginx всё равно поднимается — http и вход S3* работают как раньше.

  3. Устройство. В панели — карточка «Защищённый адрес»: профиль .mobileconfig для iPhone и Mac, файл .crt для Android*, шаги и отпечаток центра. Оба файла отдаются без входа: их ставят до входа, и секрета в них нет.

Попробовали обмануть сами себя

Ограничение, которое никто не проверил, — это надежда. Поэтому мы взяли ключ своего же центра и сделали то, чего боимся: подписали сертификаты для чужих адресов. OpenSSL подписал всё без вопросов — центр подписать может что угодно. Вопрос в том, что скажет проверяющая сторона.

Этот прогон лёг в репозиторий скриптом tls-check.sh вместе с проверками профиля и самого https: 15 проверок, все зелёные. Теперь его гоняют после любой правки этой части.

bash platform/tls-check.sh
── Центр и профиль
✓ центр по http
✓ ограничение имён, critical
✓ CA:TRUE, pathlen:0
✓ профиль: 200 application/x-apple-aspen-config
✓ в профиле — тот же центр
── Панель по https
✓ https://localhost:8443/admin/
✓ https://localhost:8443/api/admin-app/manifest
✓ по адресу 127.0.0.1
✓ http остался
── Ограничение имён: чужое центр подписать может, но браузер не примет
✓ OpenSSL, example.com: принят=no
✓ macOS, example.com: принят=no
✓ OpenSSL, 192.168.1.1: принят=no
✓ macOS, 192.168.1.1: принят=no
✓ OpenSSL, localhost: принят=yes
✓ macOS, localhost: принят=yes
✓ tls-check: всё сходится
tls-check.sh на стенде: чужое подписано, но не принято

Отдельно проверили перевыпуск. «Перемотали» часы на 340 дней вперёд — выпустился новый сертификат ящика, а центр остался прежним. Добавили ящику новое имя — центр выпустился заново: иначе браузер не принял бы и наш собственный сертификат.

Встреча с настоящим Mac

На стенде всё сходилось. Выпустили версию, ящик обновился кнопкой — и началась часть, которую стенд не покажет.

Первый сюрприз: macOS пишет про профиль красным Unsigned. Выглядит тревожно, но так и должно быть: «Verified» профиль становится, только если подписан сертификатом, которому Apple уже доверяет. У домашнего ящика такого нет.

Are you sure you want to install this profile?
NAS 192.168.1.20 — защищённый адрес панели
NAS Unsigned
Description
Чтобы панель ящика открывалась по https и ставилась приложением. Центр подписывает только адреса самого ящика.
Received
24 Oct 2026 at 11:44
Settings
CertificateNAS 192.168.1.20
Details
Certificate
Description
NAS 192.168.1.20
Certificate
NAS 192.168.1.20
Expires
21 Oct 2036 at 11:43
Issuer
NAS 192.168.1.20
CancelContinue
Unsigned — это нормально: вместо подписи сверяем отпечаток

Замена подписи — отпечаток SHA-256. Панель показывает его в карточке, его сверяют с тем, что легло в «Связку ключей». Мы сверили: совпал до знака.

Второй сюрприз серьёзнее. Профиль стоит, а Safari пишет «This Connection Is Not Private».

This Connection Is Not Private

This website may be impersonating “192.168.1.20” to steal your personal or financial information. You should go back to the previous page.

Show DetailsGo Back
Профиль стоит, а Safari всё равно не верит

Разбирались с того же Mac двумя командами. openssl s_client показал, что ящик отдаёт правильную цепочку. security verify-cert назвал причину: корень цепочки не доверен.

NAS 192.168.1.20
NAS 192.168.1.20
Root certificate authority
Expires: Tuesday, 21 October 2036 at 11:43:14
This certificate is marked as trusted for this account
Trust
When using this certificate:
Use Custom Settings
?
Secure Sockets Layer (SSL)
no value specified
Secure Mail (S/MIME)
Always Trust
Extensible Authentication (EAP)
Always Trust
IP Security (IPsec)
Always Trust
Code Signing
Always Trust
Time Stamping
Always Trust
X.509 Basic Policy
Always Trust
Так профиль оставляет центр: всё доверено, кроме сайтов

Этот шаг теперь есть в карточке панели. Стенд его не поймал бы никогда: там центру доверяли напрямую, мимо профиля.

Что осталось в панели

  • «Защищённый адрес» в «Настройки → Ящик»: профиль для iPhone и Mac, сертификат для Android, шаги для того устройства, с которого открыт экран, и отпечаток центра.
  • Плашка «Установить» по http ведёт на эту карточку — сначала защищённый адрес, потом значок на экране.
  • Сертификат ящика перевыпускается сам, а tls-check.sh проверяет ограничение имён атакой на самих себя.

Ещё не проверено: профиль на iPhone, сертификат на Android и пуши в установленном приложении — об этом в следующих записях.

Своё железо — тоже система
Начните с брифа: четыре вопроса и честный ответ, подходим ли мы друг другу.
Пройти бриф