Восемнадцатый день NAS. Хотелось поставить панель ящика на экран телефона приложением — чтобы пуш о сбоящем диске приходил сразу. Всё для этого уже было, не хватало одной буквы: https. Эта запись — как мы дали домашнему ящику свой центр сертификатов и сделали так, чтобы он не мог подписать ничего, кроме самого ящика.
Всё было готово, кроме одной буквы
От проекта-донора панели достались манифест, service worker, значки под iPhone и Android, плашка «Установить» и пошаговая инструкция. Но панель открывалась по http.
А браузеры ставят сайт приложением и пускают его воркер только по https или на localhost. На iPhone по http выходит лишь значок без пушей, у Chrome кнопки «Установить» нет вовсе.
Let's Encrypt не поможет: ящик живёт в домашней сети по адресу вроде 192.168.1.20 и наружу не смотрит. Доказать публичному центру, что этот адрес наш, нельзя. Значит, нужен свой центр, которому телефоны и ноутбуки один раз доверят.
Почему не взяли готовый
Свой центр у нас уже был — у Сторожа, нашего внешнего мониторинга. Соблазн взять его был большой.
Из Сторожа мы взяли рецепт, а не сам центр.
Центр, который умеет подписать только ящик
У ящика появился свой центр. Главное в нём — одно поле сертификата, про которое редко вспоминают: nameConstraints, ограничение имён. В нём перечислено, для каких адресов центр вообще имеет право подписывать: адрес ящика, его имя, если оно есть, и localhost. Ничего больше.
Поле помечено как критичное: браузер, который его не понимает, обязан отвергнуть сертификат, а не пропустить молча. Даже если ключ центра утечёт, им не выпустить сертификат ни для банка, ни для роутера на соседнем адресе. Утечка ключа становится бедой одного ящика, а не всего интернета на телефоне.
Ключ центра лежит только на самом ящике, с правами 600, и видит его только API. В базу и в ночные копии он не попадает.
Как устроено
Центр и сертификат выпускает сам API ящика на Python библиотекой cryptography, а не скрипт на хосте. Так одинаково работают все три формы установки: свой ящик, ящик рядом с TrueNAS и стенд разработчика.
Выпуск. Раз в 6 часов API смотрит, свеж ли сертификат. Он выдаётся на 397 дней, новый выпускается за 60 дней* до конца — или сразу, если у ящика поменялось имя.
Подхват. nginx берёт сертификат не при старте, а по переменной: читает файл на каждом рукопожатии. Перевыпуск не требует перезапуска, а без сертификата nginx всё равно поднимается — http и вход S3* работают как раньше.
Устройство. В панели — карточка «Защищённый адрес»: профиль .mobileconfig для iPhone и Mac, файл .crt для Android*, шаги и отпечаток центра. Оба файла отдаются без входа: их ставят до входа, и секрета в них нет.
Попробовали обмануть сами себя
Ограничение, которое никто не проверил, — это надежда. Поэтому мы взяли ключ своего же центра и сделали то, чего боимся: подписали сертификаты для чужих адресов. OpenSSL подписал всё без вопросов — центр подписать может что угодно. Вопрос в том, что скажет проверяющая сторона.
| Сертификат для | Кто это | OpenSSL | macOS |
|---|---|---|---|
| example.com | чужой сайт | отверг | отвергла |
| 192.168.1.1 | роутер в той же сети | отверг | отвергла |
| localhost | сам ящик | принял | приняла |
Этот прогон лёг в репозиторий скриптом tls-check.sh вместе с проверками профиля и самого https: 15 проверок, все зелёные. Теперь его гоняют после любой правки этой части.
Отдельно проверили перевыпуск. «Перемотали» часы на 340 дней вперёд — выпустился новый сертификат ящика, а центр остался прежним. Добавили ящику новое имя — центр выпустился заново: иначе браузер не принял бы и наш собственный сертификат.
Встреча с настоящим Mac
На стенде всё сходилось. Выпустили версию, ящик обновился кнопкой — и началась часть, которую стенд не покажет.
Первый сюрприз: macOS пишет про профиль красным Unsigned. Выглядит тревожно, но так и должно быть: «Verified» профиль становится, только если подписан сертификатом, которому Apple уже доверяет. У домашнего ящика такого нет.
Замена подписи — отпечаток SHA-256. Панель показывает его в карточке, его сверяют с тем, что легло в «Связку ключей». Мы сверили: совпал до знака.
Второй сюрприз серьёзнее. Профиль стоит, а Safari пишет «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.
Разбирались с того же Mac двумя командами. openssl s_client показал, что ящик отдаёт правильную цепочку. security verify-cert назвал причину: корень цепочки не доверен.
Этот шаг теперь есть в карточке панели. Стенд его не поймал бы никогда: там центру доверяли напрямую, мимо профиля.
Что осталось в панели
- «Защищённый адрес» в «Настройки → Ящик»: профиль для iPhone и Mac, сертификат для Android, шаги для того устройства, с которого открыт экран, и отпечаток центра.
- Плашка «Установить» по http ведёт на эту карточку — сначала защищённый адрес, потом значок на экране.
- Сертификат ящика перевыпускается сам, а tls-check.sh проверяет ограничение имён атакой на самих себя.
Ещё не проверено: профиль на iPhone, сертификат на Android и пуши в установленном приложении — об этом в следующих записях.
