Выпуск сертификатов: host_binding и user_binding
Введение
Заголовок раздела «Введение»Авторизация «какой пользователь на каком хосте» закодирована в двух X.509-расширениях leaf-сертификата, которые PAM-модуль проверяет на этапе аутентификации:
pam_cert_host_bindingpam_cert_user_binding
Когда оба расширения присутствуют — они и только они определяют
область действия сертификата. Список [[user_mapping]] в
config.toml остался как legacy fallback для сертификатов,
выпущенных без расширения pam_cert_user_binding; на новые выпуски
расширения должны проставляться УЦ всегда (см. docs/threat-model.md,
mandatory-extension policy).
Этот документ описывает синтаксис расширений и приводит готовые рецепты
для openssl.cnf, по которым сертификат можно выпустить штатным
openssl x509 -req.
OID-таблица
Заголовок раздела «OID-таблица»| Имя расширения | Дотированный OID | ASN.1 синтаксис |
|---|---|---|
pam_cert_host_binding | 2.25.183976554325829274683049824615098 | extnValue ::= SEQUENCE OF UTF8String |
pam_cert_user_binding | 2.25.215438916728501023845629178354627 | extnValue ::= SEQUENCE OF UTF8String |
OID размещены в нерегистрируемой ветке 2.25.<UUID> (RFC 4530), что
гарантирует уникальность без обращения к внешнему реестру. Эти значения
зафиксированы в коде (tessera_core::x509::oids) и являются частью
on-the-wire X.509-контракта — менять их нельзя.
Семантика
Заголовок раздела «Семантика»Каждая запись UTF8String в pam_cert_host_binding интерпретируется
так:
| Запись | Значение |
|---|---|
* | разрешено на любом хосте |
sha256:<HEX> | разрешено только на хосте, чей host_id_hash совпадает с указанным шестидесятичетырёхсимвольным lowercase-hex (case-insensitive) |
| Любая другая UTF-8 строка | строка интерпретируется как «сырое» machine_id и сравнение идёт через SHA-256 от строки |
В pam_cert_user_binding запись либо * (любой PAM-пользователь), либо
точное имя пользователя (case-sensitive — Linux usernames регистрозависимы).
Для авторизации сертификата на конкретном хосте/пользователе нужна хотя бы одна совпавшая запись в каждом из двух расширений.
Сценарий 1 — рабочая станция: один хост, один пользователь
Заголовок раздела «Сценарий 1 — рабочая станция: один хост, один пользователь»Рабочее место конкретного оператора. Сертификат можно использовать
только на машине с известным machine_id и только для конкретного
PAM-пользователя.
# openssl.cnf — фрагмент[ user_exts ]basicConstraints = critical,CA:FALSEkeyUsage = critical,digitalSignatureextendedKeyUsage = clientAuthsubjectAltName = email:[email protected]
# Хост: SHA-256 от machine-id операторской АРМ2.25.183976554325829274683049824615098 = ASN1:SEQUENCE:hb_one# Пользователь: единственное имя2.25.215438916728501023845629178354627 = ASN1:SEQUENCE:ub_one
[ hb_one ]e0 = UTF8String:sha256:a1b2c3d4e5f6...64charsTotal...
[ ub_one ]e0 = UTF8String:ivanovКоманда выпуска:
openssl req -new -key user.key -subj "/CN=Иванов" \ -reqexts user_exts -config openssl.cnf -out user.csropenssl x509 -req -in user.csr -CA int.pem -CAkey int.key \ -CAcreateserial -days 365 -sha256 \ -extfile openssl.cnf -extensions user_exts -out user.pemСценарий 2 — оператор банкоматов: несколько хостов, один пользователь
Заголовок раздела «Сценарий 2 — оператор банкоматов: несколько хостов, один пользователь»[ hb_three_atms ]e0 = UTF8String:sha256:1111111111111111111111111111111111111111111111111111111111111111e1 = UTF8String:sha256:2222222222222222222222222222222222222222222222222222222222222222e2 = UTF8String:sha256:3333333333333333333333333333333333333333333333333333333333333333
[ ub_operator ]e0 = UTF8String:operatorСценарий 3 — мобильный администратор: любой хост, точный пользователь
Заголовок раздела «Сценарий 3 — мобильный администратор: любой хост, точный пользователь»[ hb_any ]e0 = UTF8String:*
[ ub_admin ]e0 = UTF8String:admin* в host_binding позволяет сертификату работать на любой машине; в
user_binding по-прежнему остаётся жёсткое ограничение на имя
пользователя.
Проверка выпущенного сертификата
Заголовок раздела «Проверка выпущенного сертификата»openssl x509 -in user.pem -noout -textВ выводе должны присутствовать обе строки с дотированными OID:
2.25.183976554325829274683049824615098: 0...sha256:a1b2c3d4...2.25.215438916728501023845629178354627: 0...ivanovТаблица проверки
Заголовок раздела «Таблица проверки»| Запись | Совпадает с… |
|---|---|
* | любым хостом / любым пользователем |
sha256:<HEX> | хостом, чей host_id_hash равен HEX (без учёта регистра) |
<raw> (host_binding) | хостом, чей host_id_hash равен sha256(raw) |
<name> (user_binding) | PAM-пользователем с точным именем <name> |
| Расширение отсутствует | отказ (HostExtensionMissing / UserExtensionMissing) |
| Расширение пустое или DER-битое | отказ (*ExtensionMalformed) |
| Записи есть, но ни одна не совпала | отказ (HostNotAllowed / UserNotAllowed) |
См. также docs/configuration.md.
Расширение MAX_INTEGRITY (МКЦ Astra, 0.3.0+)
Заголовок раздела «Расширение MAX_INTEGRITY (МКЦ Astra, 0.3.0+)»MAX_INTEGRITY — non-critical X.509 v3-расширение, кодирующее
максимальную метку целостности (level, categories), до которой
сертификат может быть допущен на хосте Astra SE с включённым
strict-mode.
OID: 2.25.273824307386008814506455310913083078403
Структура (DER):
IntegrityLabel ::= SEQUENCE { level INTEGER (-128..127), categories BIT STRING DEFAULT ''B}Семантика на сервере:
- При
open_sessionPAM-модуль выбирает эффективную метку какintersect(cert, runtime_caps, fallback?). cert_integrity = "required"→ сертификат без расширения отвергается.cert_integrity = "optional"→ отсутствие расширения допускается; если задан[mac.fallback_max_integrity], применяется он.cert_integrity = "ignore"→ расширение игнорируется.
См. docs/configuration.md §«MAC integrity» и docs/threat-model.md
§«Privilege-escalation via MAC label».
Готовые шаблоны openssl.cnf для тестовых сертификатов:
tests/fixtures/leaf-{l2-c01,l1-empty,no-ext,l3,malformed,l0-fullcats}.cnf.
Генерация — tests/fixtures/setup-mac-fixtures.sh.
Пример строки в openssl.cnf для level=2, categories={0}:
2.25.273824307386008814506455310913083078403 = critical,DER:30:06:02:01:02:03:02:00:01DER здесь — три TLV: SEQUENCE, INTEGER 2, BIT STRING '01'B.
Workflow для клонированных образов
Заголовок раздела «Workflow для клонированных образов»Полный end-to-end runbook (эталон → клон → flip → выпуск per-host) — в docs/clone-image.md. Здесь — только CA-сторона: как читать TSV-дамп и что попадает в выпускаемый сертификат.
TSV-дамп от оператора
Заголовок раздела «TSV-дамп от оператора»Оператор после finish-bootstrap.sh присылает CA-админу файл
host-ids-<hostname>-<UTC>.tsv (с USB или через защищённый канал).
Колонки:
source status hash_hex hash_prefix raw normalized active_under_current_config reasonОдна строка на каждый известный источник (не только настроенные
в [host_identity].sources): machine_id, dmi_board_serial,
dmi_system_uuid, dmi_system_serial, hostname, плюс
custom_command (если configured).
Строка с active_under_current_config=yes — это тот источник,
который daemon сейчас использует. Только её hash_hex идёт
в сертификат.
Выпуск per-host сертификата
Заголовок раздела «Выпуск per-host сертификата»hash_hex подаётся в CA-инструмент выпуска (см.
clone-image.md §6.1 — CA-инструменты поставляются
отдельно, не в этом репозитории).
Cert получает pam_cert_host_binding = <hash_hex>,
pam_cert_user_binding = <service_user> и стандартный
extendedKeyUsage = clientAuth, emailProtection (emailProtection
требует штатный валидатор Astra — openssl CMS_verify; сам
tessera этот EKU не проверяет). На МКЦ-АРМ
дополнительно pam_cert_max_integrity (см. §«Поле MaxIntegrity»).
Готовый .p12 упаковывается на ту же флешку CA-инструментом
и возвращается на АРМ.
Pre-flight checks
Заголовок раздела «Pre-flight checks»tessera dump-host-id (вызываемый внутри finish-bootstrap.sh
или вручную) выходит с ненулевым кодом, если ни один источник
не отдал непустое значение. Это однозначный сигнал «не выписывайте
сертификат, пока не починён вход» — типичные причины: пустые
DMI-поля в VM, очищенный machine_id, неработающий custom_command.
См. clone-image.md §8 — troubleshooting.
Ручной дамп (без скрипта)
Заголовок раздела «Ручной дамп (без скрипта)»После уже состоявшегося flip-а:
tessera dump-host-id --usb— на USB-флешку;tessera dump-host-id --output /tmp/host.tsv— в файл;tessera dump-host-id(без флагов) — в stdout.