Выпуск сертификатов: host_binding и allowed_roles
Введение
Заголовок раздела «Введение»Документ адресован CA-администратору: он объясняет, какие
X.509-расширения зашивать в leaf-сертификат Tessera, и приводит готовые
фрагменты openssl.cnf, по которым сертификат выпускается штатным
openssl x509 -req.
Расширений пять. Два — обязательные: они кодируют авторизацию «в какой роли на каком хосте» и проверяются PAM-модулем на этапе аутентификации:
pam_cert_host_binding— на каких хостах сертификат действует;pam_cert_allowed_roles— какие роли сертификат вправе активировать.
Эти два расширения и только они определяют область действия
сертификата. Отдельного списка разрешённых учётных записей нет: имя
учётной записи входа и есть роль, поэтому pam_cert_allowed_roles
отвечает и на вопрос «в какую учётную запись пущен предъявитель».
Отката на стороне устройства — механизма, которым устройство разрешало
бы вход по своим правилам, — не существует: рамки назначает
выпускающий, а не ограничиваемая сторона.
Остальные три — опциональные, под конкретные возможности; каждое разобрано в своём разделе ниже:
pam_cert_max_integrity— потолок метки целостности на МКЦ-хостах Astra;pam_cert_profile_version— версия формата сертификата (version-gate);pam_cert_delegation_constraints— рамки делегирования для промежуточного CA.
OID-таблица
Заголовок раздела «OID-таблица»| Имя расширения | Дотированный OID | ASN.1 синтаксис | Критичность |
|---|---|---|---|
pam_cert_host_binding | 2.25.183976554325829274683049824615098 | extnValue ::= SEQUENCE OF UTF8String | non-critical |
pam_cert_allowed_roles | 2.25.185305973969816596290730578528098241367 | extnValue ::= SEQUENCE OF UTF8String | non-critical |
pam_cert_max_integrity | 2.25.273824307386008814506455310913083078403 | extnValue ::= SEQUENCE { level INTEGER, categories BIT STRING } | non-critical |
pam_cert_profile_version | 2.25.107983357797077476746994938370032043240 | extnValue ::= INTEGER | critical |
pam_cert_delegation_constraints | 2.25.242193075883906031821745064285793775511 | SEQUENCE { requireTags, allowRoles, maxLevel, maxTtl } | critical, только CA=TRUE |
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 совпадает с указанным 64-символьным lowercase-hex (case-insensitive) |
| Любая другая UTF-8 строка | строка интерпретируется как «сырое» machine_id и сравнение идёт через SHA-256 от строки |
В pam_cert_allowed_roles запись — это role_id, обязанный матчить
^[a-z][a-z0-9-]{0,15}$. Подстановка * здесь не поддерживается:
список ролей всегда перечисляется явно. Подробнее — раздел
«Расширение allowed_roles».
Для авторизации сертификата на конкретном хосте и в конкретной ролевой учётной записи нужна хотя бы одна совпавшая запись в каждом из двух расширений.
Сценарий 1 — рабочая станция: один хост, одна роль
Заголовок раздела «Сценарий 1 — рабочая станция: один хост, одна роль»Рабочее место конкретного оператора. Сертификат можно использовать
только на машине с известным machine_id и только для входа в ролевую
учётную запись oper.
# 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.185305973969816596290730578528098241367 = ASN1:SEQUENCE:ar_one
[ hb_one ]e0 = UTF8String:sha256:a1b2c3d4e5f6...64charsTotal...
[ ar_one ]e0 = UTF8String:operЛичность инженера (CN, subjectAltName) на допуск не влияет: она
нужна журналу выдачи и разбору инцидентов, а решение принимается по
двум расширениям выше.
Команда выпуска:
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_hosts ]e0 = UTF8String:sha256:1111111111111111111111111111111111111111111111111111111111111111e1 = UTF8String:sha256:2222222222222222222222222222222222222222222222222222222222222222e2 = UTF8String:sha256:3333333333333333333333333333333333333333333333333333333333333333
[ ar_operator ]e0 = UTF8String:operСценарий 3 — мобильный администратор: любой хост, точная роль
Заголовок раздела «Сценарий 3 — мобильный администратор: любой хост, точная роль»[ hb_any ]e0 = UTF8String:*
[ ar_admin ]e0 = UTF8String:admin* в host_binding позволяет сертификату работать на любой машине; в
allowed_roles по-прежнему остаётся жёсткое ограничение на роль, то есть
на имя учётной записи входа.
Проверка выпущенного сертификата
Заголовок раздела «Проверка выпущенного сертификата»openssl x509 -in user.pem -noout -textВ выводе должны присутствовать обе строки с дотированными OID:
2.25.183976554325829274683049824615098: 0...sha256:a1b2c3d4...2.25.185305973969816596290730578528098241367: 0...operТаблица проверки
Заголовок раздела «Таблица проверки»| Запись | Совпадает с… |
|---|---|
* (host_binding) | любым хостом |
sha256:<HEX> | хостом, чей host_id_hash равен HEX (без учёта регистра) |
<raw> (host_binding) | хостом, чей host_id_hash равен sha256(raw) |
<role> (allowed_roles) | входом в ролевую учётную запись с точным именем <role> |
| Расширение отсутствует | отказ (HostExtensionMissing; для allowed_roles — роль не покрыта) |
| Расширение пустое или DER-битое | отказ (*ExtensionMalformed) |
| Записи есть, но ни одна не совпала | отказ (HostNotAllowed / роль не покрыта) |
См. также 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 = DER:30:07:02:01:02:03:02:00:01DER здесь — три TLV: SEQUENCE (длина 7), INTEGER 2,
BIT STRING '01'B. Расширение объявлено non-critical (см. выше);
парсер терпим к critical-флагу, но выпускать следует non-critical.
Расширение allowed_roles (выбор роли на логине)
Заголовок раздела «Расширение allowed_roles (выбор роли на логине)»pam_cert_allowed_roles — non-critical X.509 v3-расширение, перечисляющее
role_id, которые leaf-сертификат имеет право активировать на логине.
Запрошенная роль — это имя учётной записи входа (ssh serv@device).
Семантика авторизационная: роль покрыта, если её role_id присутствует
в списке.
Это единственный список допуска. На вопрос «в какую учётную запись пущен
предъявитель» он отвечает тем же перечислением, потому что имя учётной
записи входа и есть роль: удостоверение с allowed_roles = [oper] во вход
в serv не пускает. Отдельного списка учётных записей нет — два списка над
одной строкой описывали бы нереализуемое состояние «пущен в serv, но не
вправе быть serv».
OID: 2.25.185305973969816596290730578528098241367
Структура (DER) — та же, что у host_binding:
extnValue ::= SEQUENCE OF UTF8StringКаждая UTF8String — это role_id, обязан матчить ^[a-z][a-z0-9-]{0,15}$.
Список разбирается строго fail-closed: при некорректном DER или любой
строке, не проходящей regex role_id, всё расширение считается malformed
(не пропуск одной строки), список ролей пуст → запрошенная роль не покрыта
→ отказ (audit cert_allowed_roles_parse_failed). Отсутствие расширения =
сертификат не даёт ролей, то есть по нему нельзя войти: роль требуется всегда
и покрытие доказывается именно этим расширением.
Семантика на сервере: см. docs/configuration.md §«roles» и дельта-спеку
role-selection. Извлечение — только из верифицированного серта
(VerifiedX509), как у max_integrity.
Фрагмент openssl.cnf через ASN1:SEQUENCE (две роли — oper, serv):
# Роли, которые серт может активировать на логине2.25.185305973969816596290730578528098241367 = ASN1:SEQUENCE:allowed_roles
[ allowed_roles ]e0 = UTF8String:opere1 = UTF8String:servЭквивалент одной DER-строкой (SEQUENCE { UTF8String "oper", UTF8String "serv" }):
2.25.185305973969816596290730578528098241367 = DER:30:0c:0c:04:6f:70:65:72:0c:04:73:65:72:76DER здесь: SEQUENCE (30 0c) → UTF8String "oper" (0c 04 6f 70 65 72) →
UTF8String "serv" (0c 04 73 65 72 76). Расширение non-critical (без
префикса critical,).
Расширение profile_version (version-gate)
Заголовок раздела «Расширение profile_version (version-gate)»pam_cert_profile_version — critical X.509 v3-расширение, несущее
целочисленную версию формата серта. Engine знает max_supported_profile_version
(конфиг [trust].max_supported_profile_version, дефолт 0); серт любого
звена цепи с версией выше → reject всей цепи (fail-closed version-gate). Это
второй слой защиты от эволюции формата: непонятый critical-OID отвергается по
RFC, а понятый, но более новый профиль — version-gate’ом.
OID: 2.25.107983357797077476746994938370032043240
Структура (DER):
extnValue ::= INTEGERИзвлечение — только из верифицированного серта (VerifiedX509). Malformed (не
INTEGER) или отрицательное значение → reject (audit profile_version_rejected).
Отсутствие расширения = baseline (версия 0), допускается.
Текущая версия формата — 0. Других версий пока не существует: issuer без
явного --profile-version выпускает удостоверения именно с версией 0, и это
единственная версия, которую Engine принимает без правки конфига. Значение выше
нуля имеет смысл выставлять только для парка, где
[trust].max_supported_profile_version уже поднят на всех устройствах, — иначе
выданное удостоверение будет отвергнуто на первой же аутентификации.
Фрагмент openssl.cnf для текущей версии (0):
2.25.107983357797077476746994938370032043240 = critical,ASN1:INTEGER:0Эквивалент DER-строкой (INTEGER 0): critical,DER:02:01:00.
Расширение delegation_constraints (рамки делегирования)
Заголовок раздела «Расширение delegation_constraints (рамки делегирования)»pam_cert_delegation_constraints — critical X.509 v3-расширение, валидное
только на серте с basicConstraints CA=TRUE (на листе → malformed → reject).
Объявляет конверт делегирования выпускающего CA: на какую группу устройств (по
тегам), какие роли, потолок уровня и TTL он вправе выпускать. Гарантия
проверяется на устройстве офлайн против собственных подписанных тегов, по
логическому И/MIN ко всем CA-звеньям цепи (misissued дочерний CA не
вырывается из родительского конверта).
OID: 2.25.242193075883906031821745064285793775511
Структура (DER):
DelegationConstraints ::= SEQUENCE { requireTags SEQUENCE OF SEQUENCE { key UTF8String, value UTF8String }, allowRoles SEQUENCE OF UTF8String, -- каждый — валидный role_id maxLevel INTEGER, -- потолок МКЦ-уровня (-128..127) maxTtl INTEGER -- потолок срока звена, секунды}Семантика на устройстве: device.tags ⊇ requireTags (generic-сравнение пар,
без хардкода имён ключей); запрошенная роль ∈ allowRoles; запрошенный уровень
≤ maxLevel; срок дочернего звена ≤ maxTtl. Любое нарушение → reject (audit
delegation_denied; инженеру — обобщённая причина). Извлечение — только из
VerifiedX509; malformed или невалидный role_id → reject.
Фрагмент openssl.cnf через ASN1:SEQUENCE (CA для region=north, роли
oper/serv, уровень ≤ 5, TTL ≤ 14400 с):
# Только на CA-серте (basicConstraints CA:TRUE)2.25.242193075883906031821745064285793775511 = critical,ASN1:SEQUENCE:deleg
[ deleg ]field1 = SEQUENCE:require_tagsfield2 = SEQUENCE:allow_rolesfield3 = INTEGER:5 # maxLevelfield4 = INTEGER:14400 # maxTtl
[ require_tags ]t0 = SEQUENCE:tag_region
[ tag_region ]key = UTF8String:regionval = UTF8String:north
[ allow_roles ]r0 = UTF8String:operr1 = UTF8String:servЭквивалент DER-строкой:
2.25.242193075883906031821745064285793775511 = critical,DER:30:28:30:11:30:0f:0c:06:72:65:67:69:6f:6e:0c:05:6e:6f:72:74:68:30:0c:0c:04:6f:70:65:72:0c:04:73:65:72:76:02:01:05:02:02:38:40DER: SEQUENCE(30 28){ SEQUENCE(30 11) requireTags { SEQUENCE(30 0f){
UTF8String "region", UTF8String "north" } }, SEQUENCE(30 0c) allowRoles {
UTF8String "oper", UTF8String "serv" }, INTEGER 5(02 01 05),
INTEGER 14400(02 02 38 40) }.
Монотонное сужение. Дочерний CA ДОЛЖЕН выпускать конверт ⊆ родительского
(больше requireTags, подмножество allowRoles, не больший maxLevel/maxTtl)
— ранний отказ и ясность; но безопасность не зависит от честности звеньев:
Engine применяет рамки каждого CA по И, так что более широкий дочерний конверт
не расширяет права. Пример сужения: родитель requireTags{region:north},
allowRoles{oper,serv,admin}, maxLevel:7; дочерний регион-CA
requireTags{region:north,site:hq}, allowRoles{oper,serv}, maxLevel:5.
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_allowed_roles = <role> и стандартный
extendedKeyUsage = clientAuth, emailProtection (emailProtection
требует штатный валидатор Astra — openssl CMS_verify; сам
tessera этот EKU не проверяет). На МКЦ-АРМ
дополнительно pam_cert_max_integrity (см. §«Расширение MAX_INTEGRITY»).
Готовый .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.