Перейти к содержимому

Собственный web-УЦ: выпуск сертификатов в браузере, роли и аудит в nginx и apache

От Root CA до User Authorization в nginx и Apache. Часть 4. Собственный web-УЦ: выпуск сертификатов из браузера, роли и аудит

В первой части цикла была собрана двухуровневая инфраструктура открытых ключей: корневой Root CA и три промежуточных центра сертификации. Во второй части появились механизмы отзыва, публикация CRL и OCSP-responder. В третьей была настроена аутентификация пользователей по клиентским сертификатам в nginx и Apache.

Но после успешной настройки входа возникает практическая проблема: где пользователи будут получать сертификаты? Пока инфраструктурой пользуется один администратор, достаточно команд OpenSSL в терминале. Однако при появлении новых сотрудников требуются уже не отдельные команды, а полноценный процесс:

- создание заявки;
- проверка личности и полномочий;
- выпуск сертификата;
- назначение роли;
- фиксация всех действий в журнале;
- отзыв и повторный выпуск;
- понимание того, кто именно подписал документ и на каком основании.

Для решения этой задачи создаётся PKIDesk - веб-движок управления сертификатами. Он объединяет браузерный выпуск ключевой пары, обработку PKCS#10, управление ролями и аудит операций.

Стенд проверялся с OpenSSL 3.3.7, nginx 1.29.8 и Go 1.24. Браузерная часть использует Web Crypto API. Основной принцип проекта - ключ пользователя никогда не покидает браузер, а ключ удостоверяющего центра не должен находиться в одном процессе с веб-приложением.

Шаг 1. Выпуск сертификата в браузере

Ключевая пара должна создаваться у владельца

Наиболее простой с точки зрения реализации сценарий выглядит так: сервер генерирует закрытый ключ, формирует PKCS#12 и передаёт файл пользователю. Но именно эта простота создаёт серьёзную угрозу.

Секретный ключ в таком случае может оказаться:

- в памяти серверного процесса;
- в теле HTTP-ответа;
- в логах прокси или балансировщика;
- во временном файле;
- в резервной копии;
- у администратора системы.

Для обычного внутреннего сертификата это уже нежелательно, а для сертификата, используемого в целях неотказуемости, принципиально неприемлемо. Нельзя доказать, что подпись выполнил именно владелец ключа, если его копия когда-либо находилась на сервере.

Корректная схема обратная: браузер самостоятельно создаёт ключевую пару, закрытый ключ остаётся в хранилище пользователя, а на сервер отправляется только запрос на сертификацию - PKCS#10, или CSR.

Как формируется PKCS#10

Запрос PKCS#10 представляет собой ASN.1-структуру, включающую:

1. версию;
2. данные субъекта;
3. открытый ключ;
4. набор атрибутов;
5. цифровую подпись закрытым ключом.

Web Crypto API умеет генерировать ключи и выполнять операции подписи. Поэтому для создания CSR не обязательно подключать тяжёлую стороннюю библиотеку. Достаточно корректно реализовать DER-кодирование и структуру ASN.1.

Есть два момента, на которых чаще всего возникают ошибки.

Во-первых, `exportKey('spki')` возвращает уже готовую структуру `SubjectPublicKeyInfo`. Её следует вставлять в CertificationRequestInfo целиком. Повторный разбор и ручная сборка могут привести к нарушению кодирования.

Во-вторых, поле `attributes [0] IMPLICIT` не исчезает, если атрибутов нет. Пустой набор должен быть закодирован как контейнер с нулевой длиной. Если требуется передать `subjectAltName`, он помещается внутрь атрибута `extensionRequest` с OID `1.2.840.113549.1.9.14`.

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

Почему нельзя доверять Distinguished Name

Кнопка "Получить сертификат" не должна позволять пользователю самостоятельно назначать себе произвольное имя субъекта. Если браузер отправляет на сервер поле `CN=admin`, это ещё не означает, что перед системой действительно находится администратор.

Поле `subject` - не механизм идентификации. Оно является утверждением, которое удостоверяющий центр должен сформировать после проверки личности и роли. Иначе пользователь сможет запросить сертификат с именем другого сотрудника, системной учётной записи или привилегированной группы.

Безопаснее использовать один из вариантов:

- брать идентификатор из уже подтверждённой учётной записи;
- сопоставлять запрос с корпоративным логином;
- назначать Subject на стороне УЦ;
- разрешать пользователю выбирать только ограниченный набор атрибутов;
- проверять каждое значение по политике выпуска.

То же касается `subjectAltName`. Имя, адрес электронной почты или DNS-имя нельзя безоговорочно переносить из CSR в готовый сертификат.

Вариант для терминала

Для ручной генерации ключевой пары применяется команда:

```bash
openssl genpkey
-algorithm RSA
-pkeyopt rsa_keygen_bits:3072
-out user.key
```

Закрытый ключ следует хранить с ограниченными правами доступа:

```bash
chmod 600 user.key
```

После этого формируется запрос:

```bash
openssl req
-new
-key user.key
-out user.csr
-subj "/CN=User"
```

На практике параметры субъекта и расширения лучше задавать через конфигурационный файл. Это позволяет явно контролировать `keyUsage`, `extendedKeyUsage`, `subjectAltName` и другие поля.

Терминальный сценарий удобен администраторам, но плохо масштабируется. Он не решает вопросы разграничения полномочий, подтверждения заявок и аудита.

Шаг 2. PKIDesk как слой управления сертификатами

PKIDesk должен быть не просто веб-формой, запускающей `openssl ca`. Его задача - контролировать жизненный цикл сертификата.

Первая архитектурная развилка: собственная реализация или OpenSSL

Можно написать собственный X.509-движок либо использовать OpenSSL как криптографический backend.

Полная самостоятельная реализация даёт контроль над всеми деталями, но резко увеличивает площадь риска. Ошибки в ASN.1, правилах подписи, обработке расширений и проверке цепочки могут поставить под угрозу всю инфраструктуру.

Обёртка над OpenSSL обычно практичнее. В этом случае приложение отвечает за:

- авторизацию;
- проверку заявки;
- выбор политики;
- аудит;
- очередь операций;
- хранение метаданных.

А криптографическая часть остаётся у проверенного инструмента.

Вторая развилка: где хранить ключ УЦ

Самый опасный вариант - поместить закрытый ключ промежуточного центра непосредственно в веб-приложение. Если веб-процесс будет взломан, злоумышленник получит возможность выпускать сертификаты от имени УЦ.

Лучше разделить систему на компоненты:

- веб-интерфейс принимает заявку;
- сервис авторизации определяет роль пользователя;
- отдельный worker формирует команду на выпуск;
- криптографический сервис подписывает сертификат;
- журнал фиксирует результат.

Для более строгой модели ключ УЦ должен находиться в отдельном контейнере, на изолированном сервере или в HSM. Веб-приложение в таком случае не получает прямого доступа к секретному ключу.

Третья развилка: где определяется роль

Роль не должна выводиться только из имени сертификата. Она должна храниться в собственной модели доступа и подтверждаться сервером.

Например, можно разделить роли на:

- `user` - обычный пользователь;
- `operator` - сотрудник, имеющий право обрабатывать заявки;
- `auditor` - просмотр журнала без возможности выпуска;
- `admin` - управление политиками и учётными записями.

В сертификат роль может попадать через отдельное расширение, `OU` или иной атрибут. Однако источник истины должен находиться в серверной политике, а не в произвольном поле CSR.

Четвёртая развилка: что делать с subjectAltName

Есть два безопасных подхода. Первый - полностью игнорировать значения из запроса и генерировать расширения самостоятельно. Второй - разрешать ограниченный набор значений, после чего проверять их по белому списку.

Например, пользователь может запросить только адрес из домена организации, но не произвольное DNS-имя. Для клиентской аутентификации часто достаточно URI, email или внутреннего идентификатора. Избыточные значения увеличивают риск неправильного сопоставления сертификата с учётной записью.

Пятая развилка: первый вход

Если сертификат нужен для первого входа, возникает циклическая зависимость: сертификата ещё нет, а получить его может только пользователь, который уже вошёл.

Разрыв этой зависимости возможен через временную идентификацию:

- корпоративный логин и пароль;
- одноразовый код;
- приглашение администратора;
- подтверждённый внешний провайдер;
- ручное одобрение заявки.

После первичного входа браузер создаёт ключ, отправляет CSR, а сервер связывает запрос с уже известной учётной записью.

Что должен уметь движок

Минимальный набор функций PKIDesk включает:

- создание заявки;
- генерацию ключевой пары в браузере;
- передачу CSR по защищённому каналу;
- проверку субъекта и расширений;
- выпуск сертификата по выбранной политике;
- скачивание только открытого сертификата;
- отображение цепочки доверия;
- продление;
- отзыв;
- просмотр статуса;
- аудит всех операций.

Каждое действие должно иметь идентификатор операции, время, инициатора, результат и причину отказа. В журнале желательно фиксировать хеш CSR, серийный номер сертификата, применённую политику и идентификатор одобрившего сотрудника.

Шаг 3. Проблемы, обнаруженные на рабочем стенде

`subjectAltName = supplied`

Одна из типичных ошибок связана с политикой OpenSSL, разрешающей перенос расширений из CSR. Если в конфигурации указано `subjectAltName = supplied`, удостоверяющий центр фактически доверяет содержимому заявки.

Это допустимо только при строгой предварительной проверке. В противном случае пользователь сможет добавить чужое DNS-имя, адрес электронной почты или иной идентификатор.

Безопаснее задавать SAN сервером либо пропускать его через явную валидацию.

Просроченный сертификат остаётся действительным в интерфейсе

Истечение срока действия сертификата не означает автоматическое изменение статуса в базе данных. Если система хранит состояние как `valid`, пользователь может видеть устаревшие сведения.

Статус следует вычислять по двум источникам:

- состоянию записи в базе;
- фактическим датам `notBefore` и `notAfter`.

Кроме того, нужен фоновый процесс, который регулярно переводит истёкшие сертификаты в состояние `expired`. Это улучшает отображение, отчётность и работу механизмов продления.

Гонка "выпустил - сразу вошёл"

После выпуска сертификата пользователь может немедленно попытаться подключиться к nginx или Apache. Но веб-сервер, OCSP-responder и кэш доверенных данных могут ещё не увидеть новый сертификат.

Причины задержки:

- сертификат ещё не попал в хранилище;
- не обновился CRL;
- OCSP-responder не загрузил новые сведения;
- прокси удерживает старый ответ;
- сервер использует закэшированную цепочку.

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

Полный жизненный цикл

Рабочая последовательность выглядит так:

1. Пользователь проходит первичную аутентификацию.
2. Браузер создаёт ключевую пару через Web Crypto API.
3. Закрытый ключ сохраняется локально и не отправляется на сервер.
4. Формируется PKCS#10.
5. Сервер проверяет связанную с заявкой учётную запись.
6. CSR проходит валидацию.
7. Оператор или политика подтверждает выпуск.
8. Изолированный криптографический компонент подписывает сертификат.
9. Сертификат и цепочка публикуются.
10. Событие записывается в аудит.
11. Пользователь устанавливает сертификат в браузер.
12. nginx или Apache проверяет цепочку, назначение, срок действия и статус отзыва.
13. При компрометации сертификат отзывается, а информация распространяется через CRL или OCSP.

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

Итог

Веб-УЦ - это не кнопка, запускающая OpenSSL. Его основная ценность состоит в управлении доверием: кто запросил сертификат, кто его подтвердил, по какой политике он выпущен и что произошло после выдачи.

Браузерный выпуск позволяет сохранить закрытый ключ у пользователя и избавляет от небезопасной передачи PKCS#12 с сервера. Но сама генерация CSR не решает проблему авторизации: имя субъекта, SAN и роль должны определяться подтверждённой политикой.

Надёжная архитектура строится вокруг разделения обязанностей. Веб-приложение управляет заявками, оператор принимает решения, отдельный криптографический сервис выполняет подпись, а аудит сохраняет доказательства каждой операции. Только при таком подходе инфраструктура сертификатов становится управляемой системой, а не набором ручных команд и разрозненных конфигураций.

Прокрутить вверх