0055. Встроенный HTTPS с перезагрузкой сертификата
Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-02. Дополняет ADR 0048 и ADR 0052.
Контекст
Первый промышленный профиль — Standalone: один сервер с PostgreSQL и диском, клиенты (CI, агенты развёртывания, консоль) работают с других машин. До этого решения API слушал только HTTP, и для удалённого доступа требовался отдельный TLS-терминатор (nginx). На Windows-серверах это лишний компонент со своим обновлением и мониторингом, а неправильно настроенный proxy легко оставляет backend-порт открытым по HTTP. Сертификаты ACME и корпоративного УЦ обновляются каждые 60–90 дней; перезапуск API ради нового сертификата обрывает долгие передачи.
Решение
- Встроенный HTTPS включается двумя переменными:
ARKVORY_TLS_CERT_FILE(PEM, лист и при необходимости цепочка) иARKVORY_TLS_KEY_FILE(PEM-ключ без пароля). Одна переменная без другой — ошибка конфигурации, а не тихий переход на HTTP. Минимальная версия —TLSv1.2по умолчанию, допускаетсяTLSv1.3. - При запуске сертификат и ключ проверяются: файлы не больше 1 MiB, PEM разбирается, ключ подходит к сертификату, срок не истёк. Проверка выполняется до открытия БД и других ресурсов, поэтому плохой сертификат останавливает запуск.
- Fastify получает HTTPS-сервер через
serverFactory. Тип listener остаётсяhttp.Server, поэтому маршруты, hooks и тесты не различают транспорт. - Перезагрузка: файлы опрашиваются раз в
ARKVORY_TLS_RELOAD_SECONDS(по умолчанию 300 с;0отключает). Опрос, а неfs.watch: события файловой системы ненадёжны на сетевых томах и при замене файла через rename. Новый материал проходит ту же проверку и устанавливается черезsetSecureContext. Новые соединения получают новый сертификат, открытые передачи продолжаются со старым. Ошибка обновления оставляет рабочий сертификат и пишетtls.reload_failedс константной причиной, без путей. - Наблюдаемость:
api.listeningсодержитtls;- за 14 дней до окончания срока раз в сутки пишется
tls.expiringсnotAfterиdaysLeft; - метрика
arkvory_tls_certificate_expiry_timestamp_secondsдля алертов; - не-loopback HTTP без доверенного proxy даёт предупреждение
http.plaintext_exposed.
- Встроенный listener отправляет
Strict-Transport-Security: max-age=31536000. За TLS-терминирующим proxy этот заголовок выставляет proxy. - Reverse proxy остаётся поддержанным вариантом; поведение
ARKVORY_TRUSTED_PROXIESне меняется.
Альтернативы
- Только reverse proxy: лишний компонент на Windows и риск открытого backend-порта; оставлен как вариант, но не как единственный путь.
- Перезапуск API при обновлении сертификата: обрывает многочасовые передачи.
- Встроенный ACME-клиент: тянет сетевую зависимость, хранение аккаунта и выбор challenge внутрь хранилища. Выпуск сертификата остаётся задачей внешнего инструмента (certbot, win-acme, корпоративный УЦ), Arkvory только подхватывает файлы.
- mTLS для клиентов: сервисная авторизация уже построена на ключах с политиками; клиентские сертификаты потребуют отдельной модели отзыва. Не входит в это решение.
Последствия
Оператор отвечает за выпуск и продление сертификата и за права на ключ: доступ только у учётной записи службы. Зашифрованный паролем ключ не поддерживается: секрет для его расшифровки пришлось бы хранить рядом. HTTP/2 не включён. На нативной установке HTTPS включается командой arkvory configure --root <root> --tls-cert <pem> --tls-key <pem> [--listen-host <addr>] и выключается --tls-off. Команда проверяет файлы так же, как API, записывает config/runtime.json, перезапускает службы и требует проверки готовности по новому транспорту. При любой ошибке она возвращает прежнюю конфигурацию и перезапускает службы с ней. Пути не копируются, а указываются: обновления, записанные на месте, API подхватывает сам. Учётная запись службы должна иметь право читать оба файла.
Инструменты установки (проверка готовности после установки и обновления, создание владельца, healthcheck контейнера) обращаются к своему API через apps/deploy/src/local-api.ts. При встроенном TLS соединение проверяется по самому настроенному сертификату: файл из ARKVORY_TLS_CERT_FILE служит доверенным корнем (allowPartialTrustChain), а SHA-256 листа должен совпасть. Имя хоста при этом не проверяется, потому что инструменты подключаются к 127.0.0.1, а сертификат выписан на публичное имя. Проверка TLS не отключается. Режим Compose со встроенным TLS отклоняется с подсказкой использовать reverse proxy: путь к сертификату внутри контейнера недоступен инструментам на хосте.
Проверка
tests/tls.test.mjs:
- проверка настроек и признака открытого HTTP;
- отказ при несовпадающем ключе, истёкшем сертификате и не-PEM;
- обслуживание по HTTPS с HSTS;
- замена сертификата без перезапуска и сохранение рабочего сертификата при испорченном обновлении;
- суточное предупреждение об окончании срока.
tests/metrics.test.mjs проверяет метрику срока действия. tests/local-api.test.mjs и tests/integration/deployment-health.test.mjs проверяют инструменты установки: HTTP, закреплённый сертификат, отказ на чужом сертификате и на HTTP к HTTPS-порту, отказ Compose с TLS. tests/tls-setup.test.mjs проверяет команду установщика: запись настроек, перезапуск с проверкой готовности, откат при сбое и отказ до изменений при неверных файлах. Тестовые сертификаты создаются в памяти (tests/tls-certificate.mjs) и не хранятся в репозитории.