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

Changelog

Переименование проекта (2026-06-04): проект pam_certauth переименован в Tessera (CLI/пакет tessera, модуль pam_tessera.so, пути /etc/tessera и т.д.). Записи ниже относятся к релизам под прежним именем pam_certauth и сохранены как есть — упомянутые в них команды, бинари и пути соответствуют тому, что поставлялось на момент релиза.

  • Роль на логине — это имя учётной записи входа; суффикс <user>+<role> и PAM-prompt удалены. Инженер входит в ролевую учётную запись, названную по роли (ssh serv@device), и запрошенная роль равна PAM_USER. Других источников роли не осталось. Вместе с суффиксом ушло и переписывание PAM_USER: пока модуль подменял имя, прочитанное стеком, разница между «до» и «после» подмены была доступна другим модулям стека — тот же класс, что вскрыл CVE-2021-3560 в polkit. Теперь подменять нечего. Имя учётной записи, не удовлетворяющее формату role_id (^[a-z][a-z0-9-]{0,15}$), отвергает вход до обращения к носителю. Личность инженера не теряется — она в удостоверении и журнале выпусков, а не в имени учётной записи. Миграция: вход ivanov+serv@device заменяется на serv@device с удостоверением, чей pam_cert_allowed_roles содержит роль serv (см. запись про единственный список допуска ниже). Ролевые учётные записи должны быть заведены на устройствах до обновления (провижининг — Census) как обычные, а не системные: вход в учётную запись, чей uid вне диапазона обычных пользователей, продукт отвергает. Остальные пути входа в них (authorized_keys, su, sudo -u, парольный вход, PAM-стеки без модуля) закрывают провижининг и администратор — см. install.md §8.4. Руководство по установке (docs/{ru,en}/install.md) переработано на ролевую учётную запись: выдача тестового удостоверения ведётся на роль serv, и добавлен раздел о ролевом хранилище устройства — без заполненного хранилища вход не проходит.

  • Допуск к учётной записи описывает один список — pam_cert_allowed_roles; расширение pam_cert_user_binding, аргумент issuer issue-leaf --user и секция [[user_mapping]] удалены. Раз имя учётной записи входа и есть роль, user_binding («в какие учётные записи пущен предъявитель») и allowed_roles («какие роли вправе активировать») проверялись против одной и той же строки и утверждали одно и то же; различие, которое они сохраняли, описывало нереализуемое состояние «пущен в serv, но не вправе быть serv». Секция [[user_mapping]] удалена по другой причине: она была утверждением устройства о допуске («пущу любого, чей CN совпал»), а не выпускающего, — параллельный путь, где рамки назначает ограничиваемая сторона. Удостоверения чужой корпоративной PKI не поддерживаются, и обслуживать этот откат нечего. Конфиг, всё ещё содержащий [[user_mapping]], отвергается при старте с диагностикой об удалении, а не общей ошибкой «неизвестное поле». Миграция: уберите секцию из config.toml; в командах выпуска замените --user <учётная запись> на --role <роль> — это одно и то же имя. Ранее выпущенные штатным путём удостоверения продолжают работать: pam_cert_user_binding игнорируется как любое неизвестное non-critical расширение, а allowed_roles они уже несут (без него вход не работал и раньше). Перевыпуск нужен только там, где вход держался на [[user_mapping]], — но такое удостоверение и до этого изменения не проходило проверку роли. OID 2.25.215438916728501023845629178354627 считается отозванным и не будет переиспользован под другую семантику.

  • issuer issue-root и issuer issue-ca требуют --allow-role и не принимают --max-ttl 0. Список ролей в рамках делегирования — закрытый белый список, поэтому пустой список разрешал не «любую роль», а ни одной; нулевой потолок TTL требовал от дочернего звена нулевого срока действия. Прежние умолчания (--allow-role необязателен, --max-ttl 0) выпускали CA, через который аутентификация не проходила никогда, — при этом сертификат выглядел валидным и попадал в журнал выдачи, а расхождение обнаруживалось только на устройстве, с диагностикой, указывающей на цепочку доверия, а не на аргументы команды выпуска. --max-ttl теперь имеет ненулевые умолчания по тому, что рамка ограничивает: 31536000 (год, срок CA организации) у issue-root и 14400 (4 часа, срок листа смены) у issue-ca. Миграция: добавьте --allow-role с именами ролей вашего внедрения; документированные примеры выпуска уже задают оба аргумента явно.

  • Ключ [roles].enforce удалён; роль проверяется всегда. Прежде это был миграционный тумблер ("false" | "warn" | "require") с умолчанием "false" — «роли не проверяются». Умолчание оказалось несовместимо с собственным выпуском: удостоверения штатного issuer всегда несут рамки делегирования, а вход без выбранной роли отвергает любую цепочку с рамками, то есть парк на умолчаниях не пускал никого по своим же удостоверениям. Промежуточное "warn" (проверить, записать, пропустить) убрано вместе с остальным: положения «проверить и пропустить» в модели доверия быть не должно. Конфиг, содержащий ключ в любом значении, отвергается при старте с диагностикой об удалении — намеренно громко, а не тихим ужесточением. Миграция: удалить ключ и убедиться, что база ролей под [roles].dir заполнена, иначе входы отклоняются fail-closed (это поведение не менялось). Парк, чьи удостоверения выпущены без pam_cert_allowed_roles, теряет возможность входа — альтернатива только перевыпуск.

  • [trust.revocation].mode теперь обязателен. Пропуск секции [trust.revocation] целиком или ключа mode внутри неё — ошибка валидации конфига на старте. Раньше отсутствие режима молча означало mode = "none" (отзыв не проверяется), и отозванное удостоверение проходило аутентификацию. Миграция: чтобы сохранить прежнее поведение, добавьте mode = "none" явно; лучше — включите реальную проверку (mode = "crl" / "ocsp" / "crl_then_ocsp").

  • ГОСТ через OpenSSL теперь требует явный gost_engine_path. Если allow-list разрешает ГОСТ-подписи, неявный поиск native-модуля через унаследованный OPENSSL_ENGINES отклоняется при загрузке конфига.

  • Persistent state демона перенесён в /var/lib/tessera/daemon/. Родитель /var/lib/tessera/ теперь 0750 root:tessera, потому что в нём лежат доверенные роли, теги и enrollment material. Старый wallpaper.orig.jpg безопасно мигрируется postinst-скриптом.

  • pkcs11_locking_mode по умолчанию — mutex, а не os. Раньше по умолчанию вызовы PKCS#11-модуля шли без сериализации, потому что модуль просят про CKF_OS_LOCKING_OK. Заявленная поддержка этого флага потокобезопасности не гарантирует: rtpkcs11ecp 2.14.1 при одновременном C_Initialize из двух нитей роняет процесс необработанным C++-исключением. Модуль — cdylib внутри sshd, login и fly-dm, поэтому падает процесс аутентификации. Теперь по умолчанию каждый вызов сериализуется процесс-глобальным мьютексом; цена — около 20 нс на вызов, что несопоставимо со временем C_Sign на устройстве. Миграция: инсталляциям, которым нужна параллельность и чей поставщик модуля её подтверждает, добавить pkcs11_locking_mode = "os" явно; режим никуда не делся. Явно заданное значение и раньше, и теперь имеет приоритет.

  • Команда issuer serve удалена; открытый issuer — инструмент выпуска в командной строке. Веб-кабинет и агент выпуска поставляются отдельно, и вместе с командой ушли её feature и зависимость tiny_http. Модуль подтверждения оператором остался как библиотечный API за фичей native — внешние фронтенды подписи продолжают им пользоваться. Релизные бинари собираются с cli,pkcs11,vault,file. Миграция: браузерный выпуск — через коммерческий инструментарий, выпуск из командной строки не изменился.

  • Кабинет выпуска вынесен из репозитория; реестр подписывается на стороне агента. Для этого у агента появился выделенный ключ подписи реестра (P-256, проверяется при старте) и конечная точка /sign-registry. Спецификация кабинета уехала вслед за кодом.

  • Токен, не сообщивший CKA_EXTRACTABLE, больше не считается подтвердившим неизвлекаемость ключа. Инвариант режима B проверялся чтением атрибута, но cryptoki молча выбрасывает из результата всё, что токен отказался отдать: записи с CK_UNAVAILABLE_INFORMATION пропускаются, а CKR_ATTRIBUTE_SENSITIVE и CKR_ATTRIBUTE_TYPE_INVALID ошибкой не считаются. Парсер начинал с «неизвлекаем» и переписывал значение только при наличии атрибута, поэтому молчание провайдера было неотличимо от подтверждения, и ключ принимался. Теперь состояний три: подтверждённое FALSE, подтверждённое TRUE и «не сообщено»; последнее отклоняется отдельной ошибкой ExtractableAttributeUnavailable с указанием причины отказа. Если уточняющий запрос сообщает, что атрибут читаем, выполняется одиночное перечитывание — провайдеры, ломающиеся на смешанных шаблонах, известны, а прямое чтение FALSE и есть требуемое доказательство. Миграция: развёртывания, где токен атрибут не отдаёт, до обновления входили молча, после обновления получают отказ; вход восстанавливается ключом pkcs11_allow_unreported_extractable = true. Обходные ключи независимы: pkcs11_allow_extractable_keys разрешает только подтверждённое TRUE и неотвеченный атрибут больше не покрывает, чтобы оператору, которому нужна совместимость с молчащим модулем, не приходилось заодно соглашаться на экспортируемые ключи. Измерено: на Рутокен ЭЦП 3.0 поведение не меняется — токен отдаёт CKA_EXTRACTABLE штатно. От остального парка (SoftHSM2, JaCarta-2 GOST, Рутокен Lite) того же ожидаем, но по этому атрибуту не проверяли: если ваш модуль его не отдаёт, вход после обновления отклонится до установки pkcs11_allow_unreported_extractable = true.

  • Публичный API tessera_core: поле FoundPrivateKey::extractable меняет тип с bool на перечисление ExtractableState — булево не может выразить «токен отказался отвечать», не соврав. Модуль token::pkcs11 экспортирует новые типы ExtractableState, ExtractableProbe и ExtractableKeyPolicy.

  • Движок ГОСТ загружается только по заданному пути — во всех путях кода, а не только при открытии контейнера. Раньше поиск движка по унаследованной OPENSSL_ENGINES оставался достижим ещё из четырёх мест: проверки цепочки, проверки подписи CRL, challenge-response и проверки OCSP-ответа. Во всех четырёх признаком «нужен ГОСТ» служит содержимое предъявленного удостоверения, то есть данные, которые контролирует предъявитель: достаточно было принести самоподписанный сертификат с ГОСТ-OID подписи, чтобы процесс аутентификации нашёл по OPENSSL_ENGINES произвольную библиотеку, загрузил её и сделал реализацией по умолчанию для RSA, DSA, DH, RAND и всех хешей — на устройстве, которое ни на какой движок не соглашалось. Теперь загрузчик отвергает вызов без пути, и поиск по идентификатору удалён из кода целиком. Устройства с заданным gost_engine_path работают как прежде; на устройствах без него ГОСТ-удостоверение, ГОСТ-CRL и ГОСТ-ответ OCSP честно отвергаются (fail-closed), а не проверяются наполовину. Валидация конфига и раньше требовала gost_engine_path везде, где allow-list разрешает ГОСТ-подписи, так что рабочие ГОСТ-развёртывания это не затрагивает.
  • Фазы сессии и account management действуют только от имени, под которым прошёл допуск. PAM_USER принадлежит приложению, а не модулю, и между pam_sm_authenticate и pam_sm_open_session его значение никем не закреплено. Раньше сессионная фаза перечитывала имя и отдавала его как есть в MAC-метку сессии и в привилегированные хуки, а при ошибке чтения подставляла CN удостоверения — то есть личность инженера вместо имени ролевой учётной записи. Теперь прочитанное имя сверяется с ролью, зафиксированной в контексте аутентификации (имя ролевой учётной записи и есть роль), и расхождение отклоняется fail-closed: PAM_SESSION_ERR в open_session, PAM_PERM_DENIED в acct_mgmt, пропуск хуков в close_session (выход из системы блокировать нельзя). Запасного имени нет ни при каком исходе.
  • PAM проверяет конфиг, trust anchors/intermediates, CRL, PKCS#11-модуль, GOST engine, standalone-роли и device tags по полной root-controlled цепочке пути. Symlink, non-root owner или group/world write приводят к fail-closed отказу.
  • Битое присутствующее расширение MAX_INTEGRITY больше не считается отсутствующим: обе credential-схемы отклоняют его до применения optional fallback.
  • Unmaintained-цепочка ab_glyph -> owned_ttf_parser -> ttf-parser удалена. Wallpaper writer переведён на maintained skrifa и ab_glyph_rasterizer; размеры шрифта, шаблона и raster allocation ограничены.
  • Вход по сертификату на Windows. Провайдер учётных данных на экране входа и служба движка под LocalSystem; ядро проверки переиспользовано без изменений — всё, что понадобилось в нём поправить, это платформенные ограждения, логика проверки не двигалась. Инженер предъявляет съёмный носитель, выбирает роль, вводит PIN и попадает на рабочий стол. Роль пока не даёт прав: она выбирается, сверяется с тем, что допускает удостоверение, и попадает в журнал — но выдача прав остаётся за срезом, вместе с самым дорогим нерешённым вопросом дизайна. Доверие локальным файлам получило настоящую реализацию под Windows: конфигурация и ролевое хранилище читаются через него, а POSIX-политика вне Unix отказывает целиком.

  • Один открытый host вместо сборок под feature-флаги: платформенные enforcement-бэкенды поставляются подписанными плагинами и выбираются в runtime секцией [mac] backend. Подпись проверяется до загрузки .so; любой отказ — деградация в stub-бэкенд.

  • PKCS#11-библиотека инициализируется один раз на процесс, а не на каждую загрузку бэкенда. Pkcs11Backend::load звал C_Initialize при каждом вызове, хотя это процесс-глобальное состояние провайдера. Повторный вызов в лучшем случае возвращал CKR_CRYPTOKI_ALREADY_INITIALIZED, а одновременный — на rtpkcs11ecp 2.14.1 убивал процесс. Теперь контекст живёт в процесс-глобальном реестре по канонизированному пути модуля: сколько бы ни было загрузок, инициализация одна. Разные модули в одном процессе по-прежнему получают независимые контексты. Создание контекста сериализовано независимо от pkcs11_locking_mode — два бэкенда могут быть настроены по-разному, а дефект провайдера процесс-глобален; сам режим теперь закрепляется за модулем при первой загрузке, расхождение логируется как pkcs11_locking_mode_conflict. C_Finalize не вызывается вообще: это процесс-глобальная операция над общей загруженной библиотекой, и она деинициализировала бы провайдера у постороннего потребителя PKCS#11 в том же процессе (pam_pkcs11, sshd с PKCS11Provider), который не может об этом узнать. Цена — модуль остаётся загруженным до завершения процесса. Дефект не ловился три года, потому что весь PKCS#11-контур проверялся на SoftHSM2, который гонку переживает.

  • Выпущенный лист смены снова пригоден для аутентификации. issuer issue-leaf не клал в удостоверение ни keyUsage (digitalSignature), ни extendedKeyUsage (clientAuth), а проверка листа в модуле требует оба. Любой выпуск штатным путём отвергался на первом же гейте с диагностикой trust: key usage missing required bit — доступ не выдавался (fail-closed), но и выдаться не мог. Теперь лист несёт keyUsage с digitalSignature (critical) и extendedKeyUsage с clientAuth; самопроверка после подписи отказывается отдать артефакт, в котором любого из них нет. Дефект пережил обе половины тестов, потому что выпуск сверялся со своими ожиданиями, а модуль — со своими фикстурами: добавлена перекрёстная проверка, прогоняющая выпущенный лист через pre_validate_end_entity — ту же функцию, которой пользуется модуль.

  • Исчерпание попыток PIN возвращает PAM_MAXTRIES (11), а не 8. FlowError::pam_code для класса MaxTries / PinLocked / MaxAttemptsExceeded отдавал 8 под именем PAM_MAXTRIES, но в <security/_pam_types.h> 8 — это PAM_CRED_INSUFFICIENT. Приложение вместо «попытки исчерпаны» получало «не удаётся получить данные аутентификации»: login и sshd при PAM_MAXTRIES прекращают дальнейшие запросы, при PAM_CRED_INSUFFICIENT такого сигнала нет, а администратор по коду возврата искал проблему с носителем вместо заблокированного PIN. Доступ при этом не выдавался — fail-closed не нарушался. Значение в документации (architecture.md, таблица кодов возврата) исправлено там же.

  • issuer по умолчанию ставит profile_version = 0, а не 1. Три подкоманды выпуска (issue-root, issue-ca, issue-leaf) без явного --profile-version записывали в удостоверение версию формата 1, тогда как Engine без ключа [trust].max_supported_profile_version принимает максимум 0. Любое удостоверение штатной выдачи отвергалось на устройстве с ошибкой «profile_version 1 exceeds supported maximum 0» — fail-closed сработал как задумано, но на паре «выпуск ↔ проверка» это означало неработоспособную конфигурацию по умолчанию. Дефолт выдачи опущен до 0 (текущая версия формата); потолок Engine не менялся. Кому нужен прежний вывод — передавайте --profile-version 1 явно и поднимайте max_supported_profile_version на устройствах.

  • allowed_signature_algorithms сверяется по алгоритму, а не по имени, которым его называет libcrypto. Раньше запись конфига сравнивалась посимвольно с той строкой, которую OpenSSL печатает для OID сертификата, — а печатает он длинное имя, если знает OID, и точечный OID, если не знает. Знает он разное на разных сборках и меняет ответ прямо в процессе, как только загружен движок: тот же ГОСТ-сертификат читался то как 1.2.643.7.1.1.3.2, то как GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit). Проверка алгоритма зависела от того, что libcrypto успел зарегистрировать, — на ГОСТ-развёртывании это давало отказ входа, не воспроизводимый на соседнем устройстве. Теперь сертификат читается как точечный OID (всегда, из его собственных байтов), запись конфига распознаётся по таблице имён, и сравниваются распознанные алгоритмы. Одну подпись можно записать любым её именем: "sha256WithRSAEncryption" и "1.2.840.113549.1.1.11" равнозначны. Оба написания года в ГОСТ-именах (-2012- и -12-, второе — из нашей же документации) распознаются. Кого затронет: тех, у кого в списке стоит имя вне таблицы — "sha1WithRSAEncryption", "RSASSA-PSS", "ED25519" и подобные. Такое имя теперь не совпадёт ни с одним сертификатом, и удостоверения с этим алгоритмом перестанут проходить — деградация в отказ входа (fail-closed), без выдачи лишнего доступа. Лечится записью точечного OID алгоритма; таблица имён — в configuration.md, секция [trust].

  • ГОСТ-контейнер открывается на устройстве, где gost-engine не был загружен заранее. Расшифровать ключ ГОСТ-контейнера libcrypto своими силами не может, а движок, прописанный хостом в openssl.cnf, в процессе модуля не регистрируется: этот файл читается только при инициализации с загрузкой конфига, которую биндинг не запрашивает. Контейнер выглядел контейнером с неподдерживаемым алгоритмом ключа, и вход не проходил. Теперь неудачная попытка открыть контейнер догружает движок по настроенному пути и повторяется один раз. Повтор происходит только там, где gost_engine_path задан. Неверный PIN не повторяется никогда.

    Заодно исправлена классификация самой ошибки. Когда MAC контейнера назван хешем, которого в процессе нет, MAC не вычисляется вовсе, и OpenSSL может положить mac verify failure поверх настоящей причины. Модуль читал только эту строку и объявлял происходящее неверным PIN: инженеру предлагали ввести PIN заново до исчерпания попыток, а движок так и не загружался. Теперь строки о ненайденном алгоритме разбираются раньше строки о MAC. Набор этих строк зависит от версии OpenSSL и от того, почему именно не нашёлся алгоритм (unknown digest algorithm, key gen error, mac generation error, inner_evp_generic_fetch: unsupported; строки mac verify failure в стеке может и не быть вовсе) — поэтому разбирается набор признаков, а не одна фиксированная форма. Настоящий неверный PIN ни одного из них не порождает.

    Признаки при этом рассматриваются, только пока движок не загружен. Как только он загружен, отказ MAC действительно означает неверный PIN, а записи о неудачном поиске алгоритма могут остаться в очереди ошибок OpenSSL и после того, как поиск успешно завершился запасным путём.

    Следствие, которое стоит знать: на ГОСТ-контейнере верный и неверный PIN до загрузки движка неотличимы — MAC не считается, PIN в это ещё не участвует. Поэтому на устройстве, где gost_engine_path не задан, ГОСТ-контейнер даёт «контейнер не читается», а не «неверный PIN»: он действительно не читается, и повторный ввод этого не изменит. Меняется и код возврата PAM: раньше такой вход заканчивался PAM_MAXTRIES (11) после исчерпания попыток, теперь — PAM_PERM_DENIED (6) на первой же. Стеки, которые различают эти коды (pam_faillock, разборы журнала в fail2ban), увидят другое событие: не «исчерпан лимит попыток», а «отказано». Число попыток загрузки движка при этом ограничено одной на процесс — после первой попытки движок либо загружен, либо его неудача запомнена.

    Неудачная загрузка движка теперь пишется в журнал (tessera.gost, уровень warn) с вариантом ошибки и настроенным путём — на каждой попытке входа, а не однократно. Инженеру по-прежнему показывается ошибка контейнера: путь к движку — забота администратора, и из приглашения на ввод PIN он не виден. Запись о неудаче кэшируется на всё время жизни процесса: sshd форкается на каждое соединение и этого не замечает, а дисплейный менеджер живёт одним процессом весь сеанс — там сломанный движок останется сломанным до перезапуска службы, даже если файл починили. Это осознанный размен: повторная загрузка подозрительной библиотеки на каждой попытке трогала бы глобальную таблицу ENGINE в libcrypto по инициативе того, кто стоит у приглашения, и всё равно ничего бы не исправила.

  • OCSP-режимы отвергаются валидатором конфига. [revocation].mode = "ocsp" / "crl_then_ocsp" и любые ocsp_* ключи теперь дают ошибку валидации на старте. Раньше ключи принимались, но OCSP-проверка не выполнялась — конфиг молча обещал то, чего нет. OCSP не реализован; план — в openspec/changes/ocsp-support.
  • crl_max_age_hours = 0 невалиден. Допустимый диапазон — 1..=8760. Ноль раньше означал «каждая CRL мгновенно stale» — бессмысленная конфигурация, которую принимали молча. Чтобы отключить bound по возрасту — опускайте ключ.
  • Extractable-ключ на PKCS#11-токене отклоняется по умолчанию. Ключ с CKA_EXTRACTABLE = TRUE противоречит смыслу аппаратного токена (приватный ключ можно выгрузить). Opt-out для legacy-токенов: pkcs11_allow_extractable_keys = true.
  • Пустой [trust].anchors — ошибка валидации. Раньше пустой список принимался, и любая валидация цепочки гарантированно падала на первой auth-сессии вместо старта демона.
  • Malformed pam_cert_user_binding — отказ аутентификации. Расширение, которое есть в сертификате, но не парсится, раньше приводило к fallback на [[user_mapping]] — повреждённое (или подделанное) binding-расширение тихо понижало источник авторизации. Теперь fail-closed.
  • syslog_facility / journald_priority deprecated. Ключи игнорируются (WARN в лог при наличии в конфиге); удалите их из config.toml на следующем переоформлении.
  • Подпись CRL верифицируется fail-closed. Подпись каждой применимой CRL проверяется ключом её issuer’а из verified-цепочки; CRL, подписанные не issuer’ом (или с невалидной подписью), перестанут приниматься — раньше revoked-список такой CRL применялся без проверки подписи.
  • pam-certauth dump-host-id CLI subcommand. Пробует ВСЕ канонические host_identity-источники (не только подмножество в [host_identity].sources) и пишет TSV-отчёт. Колонки: source, status, hash_hex, hash_prefix, raw, normalized, active_under_current_config, reason. Строка с active_under_current_config=yes отмечает источник, который daemon сейчас реально использует. Запись атомарная через --output FILE или --usb (автоматический mount первой USB r/w, имя host-ids-<hostname>-<UTC>.tsv). Stdout без флагов. Exit ≠ 0, если ни один источник не отдал непустое значение — сигнал «не выписывать сертификат, пока не починён вход».
  • /usr/share/pam-certauth/finish-bootstrap.sh — single-pass переход с clone-image bootstrap state на production. Атомарный rewrite config.toml (sources = ["override"] → реальные источники) с backup’ом .bak.<UTC>, валидация через pam-certauth check, рестарт daemon’а, dump-host-id --usb. Флаги: --non-interactive (Ansible), --sources "A,B" (или env POST_INSTALL_SOURCES), --no-restart, --no-dump. Идемпотент: повторный запуск без sources = ["override"] exit 0 без изменений.
  • fly-dm greeter pivot #2: wallpaper writer. GreetString.desktop writer из 0.3.16 оказался no-op на production МКЦ-3 АРМ (fly-modern hardcoded’но рендерит «Усиленный уровень защищенности» в headline place). Замена: впечатать host_id в JPG, на который смотрит [background].path в /etc/X11/fly-dm/fly-modern/settings.ini. Opt-in: [fly_dm_greeter].update_wallpaper = true. Pure Rust (image + ab_glyph), без native deps. Templates template_ru / template_en (по locale), one-time backup оригинала. Atomic save.
  • CA-инструменты (выпуск per-host / wildcard / bootstrap сертификатов, настройка PKI, подготовка USB) вынесены из .deb: не должны лежать на боевых АРМ, поставляются отдельно и хранятся на CA-машине.
  • Новый документ docs/clone-image.md — end-to-end runbook раскатки парка АРМ через клонированный образ (bootstrap-эталон, finish-bootstrap.sh, CA-сторона, troubleshooting, Ansible).
  • docs/install.md §2.4¾, docs/operations.md §2.4, docs/cert-issuance.md cross-linked на новый документ.
  • Drop debconf prompt в debian/postinst (введён в 0.3.16). Операторы раскатывают через готовые config.toml (Ansible / centralized rollout) — debconf-шаг при dpkg install только переключал флаг в config.toml и добавлял moving parts (templates, debconf dep, postinst branching) без изменения production-поведения.
  • Удалены debian/templates, debian/config, debconf-branch в debian/postinst, debconf из debian/control Depends, confmodule sourcing.
  • Daemon-side feature не менялась: [fly_dm_greeter].update_greet_string = true в config.toml по-прежнему заставляет демон переписывать /etc/X11/fly-dm/override/GreetString.desktop на старте.
  • Per-build pipeline time снижено:
    • astra job теперь cargo nextest run --workspace --features astra-mac в debug profile. Прежний release-run занимал 510s vs ubuntu 56s (9.1×) из-за workflow-level CARGO_PROFILE_RELEASE_CODEGEN_UNITS=1
      • LTO=thin (нужны для production .deb), которые делают release test compilation катастрофически медленным. Тесты по-прежнему проходят astra-mac feature-gate.
    • scripts/build-deb.sh больше не делает cargo clean && cargo build --release перед dpkg-buildpackage. debian/rules уже чистит target/ через override_dh_auto_clean и пересобирает через override_dh_auto_build — pre-build был чистым дублированием работы (~60s/pipeline).
  • Source-code изменений vs 0.3.16 нет. Тот же .deb, быстрее CI.
  • fly-dm greeter pivot. Revert fly-dmrc greeter-show-messages (cargo-cult из 0.3.15 — таргетил KDM/LightDM legacy key, который fly-qdm не парсит). Переключение на механизм, который реально работает на Astra fly-qdm 2.15+: /etc/X11/fly-dm/override/GreetString.desktop (freedesktop i18n .desktop файл). Live-testing на Astra VM подтвердил рендер host_id в headline login-form’ы.
  • Opt-in: [fly_dm_greeter].update_greet_string = true. На старте daemon резолвит host identity и переписывает GreetString.desktop с short hash + source:
    Устройство astra184 · host_id=abc12345 (dmi_board_serial)
    Original сохраняется как .orig при первом запуске. Atomic write (tmpfile + rename). Silent skip на хостах без /etc/X11/fly-dm/. Log-and-continue — auth никогда не блокируется ошибкой greeter writer.
  • Debconf prompt в debian/postinst (en + ru) спрашивает оператора про включение banner’а; default off. Удалён в 0.3.18 (см. выше).
  • pam-certauth check validator: fly_dm_greeter_greet_string_customized, _default, _override_missing, _read_failed — Info-записи.
  • integrate-pam.sh: убраны --enable-greeter-messages / --no-greeter-messages флаги и fly-dmrc patcher из 0.3.15. Скрипт снова занимается только PAM-stack editing.
  • Попытка №1 заставить fly-dm показать PAM_TEXT_INFO с host_id: патч fly-dmrc через integrate-pam.sh (greeter-show-messages = true под [greeter]). Идемпотент, backup fly-dmrc.bak.<UTC>. Флаги --enable-greeter-messages (default ON для fly-dm*), --no-greeter-messages (CI). --unintegrate fly-dmrc не трогает.
  • Startup-check fly_dm_greeter читает fly-dmrc и эмитит Info-запись: messages_on / _off / _default / _read_failed. Silent skip при отсутствии файла (sshd-only хосты, серверы).
  • Не сработало на fly-qdm 2.15+ — таргетили KDM legacy key. Revert в 0.3.16, окончательный workaround через wallpaper writer в 0.3.19.
  • Build hotfix. Timestamp в debian/changelog для 0.3.13 (12:00 +0300) был раньше чем 0.3.12 (18:00 +0300) — Ubuntu lintian latest-changelog-entry-without-new-date валил .deb build. Astra .deb в 0.3.13 release собрался (Astra pipeline пропускает lintian). Этот релиз бампает версию + timestamp, оба .deb строятся.
  • Функциональных изменений vs 0.3.13 нет.
  • Hotfix: ACTUALLY implement XDG_SESSION_ID capture in pam_sm_open_session. Версии 0.3.10/0.3.11/0.3.12 в changelog’е заявили, что капчурят XDG_SESSION_ID и отправляют UpdateSessionTarget в monitord, но в cdylib код этого не было — pam_sm_open_session не вызывал pam_getenv, не открывал IPC и не слал UpdateSessionTarget. На production-терминалах USB-removal Logout/Lock молча не работал три релиза подряд: action handler не имел logind id и падал на terminate_session.
  • Добавлены: crate::pam_helpers::pam_get_env_string (thin обёртка над pam_getenv) и новый модуль crate::xdg_capture с чистой функцией capture_xdg (unit-тесты на три ветки: XDG отсутствует / присутствует и IPC ok / IPC fail). Production cdylib зовёт capture_xdg после MAC pipeline и до session_open hooks; любая IPC-ошибка логируется WARN и НЕ ломает session-open (best-effort semantics).
  • Wire-протокол не меняется — переиспользуется UpdateSessionTarget фрейм, отправленный с daemon-side ещё в 0.3.10.
  • Startup validation pipeline. Daemon теперь на старте, сразу после load_validated_config, прогоняет run_startup_checks — единый sweep из шести проверок, который ловит мисконфиги, не видимые TOML-валидатором:

    1. PAM stack ordering. Сканирует /etc/pam.d/{login,fly-dm, fly-dm-np,sshd,sudo,su}. Если @include certauth-* стоит ПЕРЕД auth required pam_parsec_mac.so — ERROR с подсказкой про integrate-pam.sh (регрессия фикса из 0.3.8: на Astra SE неправильный порядок убивает account-фазу pam_parsec_mac).
    2. [mac].runtime vs ядро. required без активного parsec_strict_mode()=1 — ERROR + fail-fast (раньше demon стартовал, и фейл случался на каждой auth). Остальные комбинации — INFO/WARN с явным текстом.
    3. Trust anchors / intermediates. Существование + не-ноль байт + счётчик BEGIN CERTIFICATE маркеров. Defense-in-depth: TOML валидатор отвергает невалидные пути на загрузке, startup-check ловит сценарий «файл удалили/обрезали между провизионингом и рестартом».
    4. /etc/pam_certauth/ca/ permissions. WARN, если каталог world-writable (mode & 0o002 != 0).
    5. PARSEC_CAP_CHMAC. WARN, если МКЦ-ядро активно и [mac].runtime ≠ disabled, но у процесса нет capability — метки на sessions.json не лягут.
    6. host_identity probe. По одной INFO/WARN-строке на каждый настроенный источник — admin сразу видит резолв, не дожидаясь первой auth-сессии.

    Каждая запись помечена стабильным check ID (pam_stack_misorder, mac_runtime_required_missing_kernel, trust_anchor_missing, …) — grep’абельно через journalctl -u pam-certauth -g startup_check. При наличии хотя бы одного ERROR boot обрывается с явным сообщением — заметно в systemctl status, а не в первой неудачной auth-сессии.

  • pam-certauth check subcommand. Standalone preflight: загружает config + прогоняет тот же pipeline без открытия socket’а. Печатает [INFO ]/[WARN ]/[ERROR] лог + summary. Exit 0 — чисто, exit 1 — есть ERROR. Можно навесить на ExecStartPre= в systemd-unit, чтобы превратить preflight в hard gate. Документация в docs/install.md §2.4½.

  • integrate-pam.sh placement fix для Astra SE. Скрипт теперь вставляет @include certauth* ПОСЛЕ существующей строки auth ... pam_parsec_mac.so, если она есть, а не перед первой auth-строкой. Боевой кейс: на Astra SE 1.8.3 /etc/pam.d/login и /etc/pam.d/fly-dm штатно начинаются с auth required pam_parsec_mac.so; placement до неё приводил к тому, что success=done jump из certauth-only snippet’а обходил auth-инстанс pam_parsec_mac, и account/session-инстансы валились "Can't obtain required data" → login deny несмотря на успешную cert-аутентификацию. Подтверждено на проде (Astra SE 1.8.3, kernel 6.1.141): после reordering login проходит до конца, fly-dm greeter banner и MAC integrity level 63 — всё работает.
  • Test harness: tests/scripts/test_integrate_pam.sh проверяет инвариант «@include certauth-only строго после pam_parsec_mac.so».
  • docs/install.md §8 — описана логика placement anchor.
  • docs/install.md §10 «Что делать, если…» расширен на 8 новых troubleshooting-кейсов из боевой отладки:
    • pam_parsec_mac: Can't obtain required data (три причины + фиксы)
    • parsec.mac=0 + pam_parsec_mac в стеке (включить kernel МКЦ vs убрать из стека)
    • Legacy [mac].enabled = true (TOML parse error → миграция на [mac].runtime)
    • WARN mac_caps_missing / pdp_set_fd rc=-1 — не блокеры, как выдать PARSEC_CAP_CHMAC если нужна метка на session-файле
    • 14-секундная тишина после trying USB candidate на 0.3.5 (фикс через апгрейд на 0.3.6+)
    • dmi_board_serial = 0 в виртуалках, drift host_id при пересборке
    • fly-dm не показывает greeter banner (greeter-show-messages = true)
    • DIGSIG enforce без подписи (подписание или logging-only)
    • pam-certauth модуль не загружается (ldd missing libparsec-mic.so.3)
  • [mac].runtime runtime-переключатель Parsec backend. Новое поле [mac].runtime (required | auto | disabled, default auto) разводит compile-time feature astra-mac от runtime-выбора backend’а. Боевой кейс: один .deb (собранный с astra-mac) ставится на терминалы с МКЦ и без — поведение управляется через config.toml.
    • disabled — гарантированный StubBackend, никаких pdp_* вызовов даже на сборке с astra-mac (фиксирует событие mac_runtime_disabled в syslog).
    • required — fail-closed: если parsec_strict_mode() ядра вернул не «активно», аутентификация отклоняется с событием mac_runtime_required (вместо тихой деградации).
    • auto (default) — probe ядра на старте сессии; настоящий ParsecBackend при активном МКЦ, fallback на StubBackend с одноразовым mac_runtime_fallback (WARN) иначе.
    • Валидация: disabled + cert_integrity=required и required без astra-mac отвергаются на старте.
    • Снимает блокер на терминале МКЦ: pam_parsec_mac: Can't obtain required data теперь решается выставлением runtime = "disabled"
      • удалением pam_parsec_mac из стека, а не пересборкой .deb.
  • HostIdentityResolver::probe_all() — публичный API, возвращающий одно ProbeResult на каждый сконфигурированный источник ([host_identity].sources) без влияния на политику выбора (resolve() остаётся first-working-wins). cdylib теперь на старте каждой auth-сессии логирует по строке INFO на источник в pam_certauth.host_identity (probe ok / probe error + probe selected). Источник истины для регистрации терминала в реестре — этот лог; sha256sum /etc/machine-id вручную больше не нужен и даёт расхождение, если [host_identity].sources содержит не только machine_id.
  • ResolvedHostId::hash_prefix() — первые 8 hex символов sha256 для on-screen диагностик. Сообщение host_binding mismatch на лок-скрине терминала теперь показывает короткий host_id=a1b2c3d4 (source=…) вместо нечитаемых 64 hex. Полный hash остаётся в syslog.
  • fly-dm greeter baseline: в начале pam_sm_authenticate модуль отправляет PAM_TEXT_INFO с короткой идентификацией машины («Это устройство: host_id=… (source=…)»). fly-dm показывает её в greeter UI при greeter-show-messages = true в /etc/X11/fly-dm/fly-dmrc — инженер у терминала мгновенно сверяет hash с реестром, не заходя в shell.
  • configuration.md §«MAC integrity» — новая подсекция «Семантика runtime» с матрицей runtime × cert_integrity × astra-mac, таблица полей дополнена runtime и warn_on_homedir_label_mismatch.
  • install.md §8.5 переписан под runtime-переключатель: один и тот же .deb для трёх сценариев (МКЦ выключен / включён / смешанный парк). Подсекция §8.5.1 — baseline для fly-dm greeter.
  • install.md Troubleshooting — команда journalctl … 'host_identity: probe' теперь источник истины для регистрации терминала.
  • Новые audit-события mac_runtime_fallback (WARN) и mac_runtime_disabled (INFO) в target mac.audit.
  • MacRuntimeMode (validated layer) и RawMacRuntimeMode (raw config) — pub re-exports через pam_certauth_core::config::validated и ::config::raw.
  • build_backend(MacRuntimeMode) в pam_certauth::session — единственная точка решения «Parsec vs Stub», вместо двух compile-time ветвей.
  • host_id логируется при каждом resolve() с указанием source, raw и полного host_id_hash (target pam_certauth.host_identity). Fallback на unknown тоже логируется. Регистрация терминала в реестре теперь по факту resolved hash из syslog, не ручное вычисление sha256(/etc/machine-id) — устраняет drift между скриптом выпуска cert’а и развёрнутыми [host_identity].sources.
  • PAM_TEXT_INFO на экране при host_binding mismatch: показывает host_id_hash этой машины + тип источника + просьбу передать админу. Текст дублируется в syslog (warn).
  • PAM_TEXT_INFO на экране при wrong .p12 PIN (MAC verify): если cert лежит в незашифрованном SafeBag (новый issuance-скрипт), модуль читает его без пароля и показывает host/user, для которых cert выпущен — инженер сразу видит «вставлена не та флешка». Для legacy-формата (cert тоже зашифрован) — обычное «пароль неверный».
  • Per-candidate USB-iteration логирование на уровне info: mount succeeded → discovery → envelope parsed → chain validated → final outcome. Был «провал тишины» 14 секунд между «trying USB candidate» и concluding модулем; теперь каждый шаг видим в journalctl -t pam_certauth.
  • Fail-closed на неверный PIN: не перебираем USB-партиции (lock-test wrong_pin_does_not_fall_back_to_next_partition). Multi-partition fallback остаётся ограничен pre-password failures (ASN.1 envelope), иначе создаётся PIN-oracle / chain-probing по сменным носителям.
  • Multi-source matching по [host_identity].sources намеренно НЕ делается (weakest-link bypass: атакующий с root спуфит самый писабельный источник → байпасит host-binding). Это зафиксировано в threat-model.md §4.10.
  • install.md — новая секция «Сертификат не принимается на терминале» (чек-лист: host_id из syslog → сверка с реестром → перевыпуск или чтение cert plaintext из .p12). Обновлён раздел host_binding mismatch (cert в новом формате читается без PIN).
  • install.md §8.5 — два сценария PAM-стека (с/без МКЦ PARSEC MAC) с явной инструкцией где pam_parsec_mac.so нужен, а где он завалит account-фазу с Can't obtain required data.
  • threat-model.md §4.10 — multi-source iteration по host_identity явно отмечена как НЕ выполняемая по причине weakest-link bypass.
  • Новый pub helper pam_certauth_core::pkcs12::try_extract_cert_without_pin — best-effort чтение leaf-cert из PKCS#12 без пароля. Возвращает None для legacy-формата. Используется wrong-PIN диагностикой.
  • Новый pub метод FlowIo::show_info(&str) (default no-op) — путь доставки PAM_TEXT_INFO на экран. RealFlowIo::with_pamh() привязывает live PAM-handle для cdylib; тест-фейки остаются без изменений.
  • [mac].runtime = "auto" | "required" | "disabled" — runtime- переключатель Parsec backend без пересборки (сейчас compile-time feature astra-mac решает однозначно). Боевой кейс: бинарь собран с astra-mac, но на конкретной машине МКЦ-ядро выключено — нужен fallback на StubBackend без пересборки .deb.
  • HostIdentityResolver::probe_all() — вернёт значения всех сконфигурированных источников (а не только первого работающего) для startup-логирования и admin-troubleshooting’а.
  • host_id_hash_prefix (первые 8 hex) в PAM_TEXT_INFO — полный 64-char hash на экране нечитаем.
  • Baseline-строка «Это устройство: source=… hash_prefix=…» для fly-dm greeter (до prompt’а PIN).
  • USB partition iteration теперь делает fallback на следующий раздел при ASN.1-ошибке парсинга PKCS#12 (т.е. «файл по нашему пути есть, но это не P12»). Раньше такая коллизия имён — типичная для USB-устройств с несколькими разделами и Apple-форматированных носителей — мгновенно роняла auth с asn1_check_tlen: wrong tag, Type=PKCS12, не пробуя оставшиеся партиции.
  • Fallback срабатывает ТОЛЬКО на ASN.1-fail (pre-parse БЕЗ пароля). Ошибки MAC verify / decrypt / chain validation (всё, что требует пароля или валидации сертификата) остаются fail-closed без перебора — не создаёт PIN-oracle и не позволяет chain-probing по разделам.
  • pam_certauth_core::pkcs12::validate_p12_envelope(&[u8]) — pure-функция, проверяющая ASN.1-конверт PKCS#12 без обращения к паролю. Используется в flow.rs::authenticate_pkcs12 как граница между «файл на USB не P12 → пробуем следующий раздел» и «файл — валидный P12, но не расшифровывается → fail-closed».
  • FlowError::P12Envelope (мапится на PAM_AUTHINFO_UNAVAIL (9)) для случая «ни одна партиция не дала валидного P12-конверта».
  • pkcs12_path_pattern теперь реально применяется при discovery credentials с USB-носителя. До этого параметр декларировался в конфиге, но игнорировался — discovery всегда искал <mountpoint>/certs/user.p12. Default остался прежним (certs/user.p12) для backwards compat. Поддержан плейсхолдер ${user}, добавлена защита от path-traversal в валидаторе (отклоняются абсолютные пути, пустая строка, сегменты .. и .).
  • Снято требование LABEL=PAMCERT на партиции USB-носителя. pam_certauth теперь перебирает все партиции с FS из allowlist (vfat, exfat, ext4, ntfs) и останавливается на первой, где найден .p12. Реальная граница доверия — расшифровка .p12 паролем пользователя и валидация цепочки сертификатов; label-фильтр ничего не добавлял к безопасности, только UX-friction.
  • Удалена ошибка UsbError::AmbiguousPartition (несколько партиций с меткой PAMCERT) — она теряет смысл без обязательной метки.
  • Новый конфиг-параметр max_usb_partitions (default 8, range 1..=64) ограничивает число перебираемых партиций. Анти-DoS guard против атакующего с физическим доступом, который мог бы подсунуть устройство с огромным числом разделов и заставить модуль крутить бесконечный цикл mount/umount.
  • Новая ошибка UsbError::TooManyPartitions { devnode, count, limit } (fail-closed при превышении лимита).
  • Конфиги 0.3.2 совместимы как есть: max_usb_partitions опционален, default 8 достаточен для всех реалистичных USB-носителей.
  • Раздел LABEL=PAMCERT продолжает работать, но метка больше не обязательна — можно оставить как есть или убрать на следующем переоформлении флешки.
  • Поддержка USB-флешек с partition table: если на whole-device нет FS, pam_certauth ищет среди разделов один с label=PAMCERT и FS из allowlist. Несколько подходящих разделов → отказ (fail-closed). Обратная совместимость: установки с FS на whole-device работают как раньше.
  • MAC integrity (МКЦ) integration for Astra SE strict-mode. Сессия теперь получает метку (level, categories), выбранную как пересечение расширения MAX_INTEGRITY сертификата (OID 2.25.273824307386008814506455310913083078403) с потолком рантайма от libpdp/libparsec. Новая секция [mac] в config.toml c полями cert_integrity (required / optional / ignore) и fallback_max_integrity.
  • Feature-флаг astra-mac (включается на сборке для Astra SE); stub-бэкенд используется на не-Astra хостах и отвергает cert_integrity = "required" на этапе загрузки конфига.
  • DER-кодек IntegrityLabel со строгим парсером и компонентным strictly_below для сравнения меток.
  • Метки pdpl-file :::iinh накладываются на /etc/pam_certauth/, /var/lib/pam_certauth/, /var/cache/pam_certauth/ через postinst при astra-strictmode-control is-enabled. host_id получает chattr +i после первой записи.
  • Атомарная запись sessions.json теперь использует fd-based labeling через pdp_set_fd (метка накладывается до публикации имени файла, закрывает TOCTOU-окно). irelax через fd-API ядро не принимает (EINVAL) — relax-семантика для sessions.json обеспечивается iinh-наследованием от parent dir.
  • E2E-сценарии T1-T12 (vagrant/scripts/test-mac.sh) и perf-bench (vagrant/scripts/bench-mac.sh) для Astra VM.
  • Документация: docs/install.md, docs/cert-issuance.md, docs/configuration.md, docs/threat-model.md пополнены секциями по МКЦ.
  • debian/control: добавлен Recommends: libpdp3 (>= 3.11+ci97~) и libparsec-base3 (>= 3.11+ci97~) (оба runtime-dep при сборке с astra-mac).
  • Linker fix: parsec_capget оказался экспортируемым из libparsec-base.so, а не libpdp.so — Astra CI build падал с undefined symbol: parsec_capget (verified run 25903325006, 2026-05-15). build.rs теперь emits и -lpdp, и -lparsec-base; extern-блок с parsec_capget помечен #[link(name = "parsec-base")].
  • Linker fix: getmicnam / freemicent_r живут в libparsec-mic.so.3, а не в libpdp.so (комментарий в build.rs, утверждавший обратное, исправлен). build.rs / Dockerfile теперь линкуют -lparsec-mic.
  • libpdp text-codec grammar. Кодировщик encode_label_text раньше формировал строку "0:0:cat:flags:level" (пять сегментов, пятый = линейный ilevel) — это была устаревшая интерпретация заголовков. Реальное strict-mode-ядро Astra 1.8.4 принимает четырёхсегментную грамматику level:ilevel:cat[:flags]. Кодек переписан, e2e-применение метки на sessions.json теперь отображается pdpl-file как Уровень_0:Сетевые_сервисы:Нет:0x0!.
  • pdp_set_fd + irelax несовместимы. Ядро возвращает EINVAL, если irelax передан через fd-based API. Демон теперь вызывает set_fd_label(.., irelax=false). Path-based pdp_set_path irelax по-прежнему принимает (используется postinst через pdpl-file).
  • getmicnam возвращает library-private static memory (per man getmicnam на Astra 1.8.4), а не heap-аллоцированную структуру. Прежний код звал freemicent_r на результат и падал в pam_sm_open_session с free(): invalid pointer → SIGABRT. Указатель больше не освобождается.
  • Daemon под User=pamcertauth (не root) при опциональной активации МКЦ. Шипованный drop-in mac-integrity.conf.example использует PAMName=pam-certauth + парный PAM-стек dist/pam.d/pam-certauth.example (pam_parsec_cap.so + pam_parsec_mac.so) для подъёма ilevel=63 и PARSEC_CAP_CHMAC на процессе демона. Ранее обсуждавшийся execaps -c 0x8 -- ...-обход не используется — execaps сам требует PARSEC_CAP_CAP у запускающего процесса, которой у pamcertauth нет.
  • Sessions registry на tmpfs. Переехал из /var/lib/pam_certauth/sessions.json (persistent) в /run/pam_certauth/sessions.json (volatile, RuntimeDirectory=). Снимает stale-state-after-reboot foot-gun и MAC-labelling churn на каталоге. daemon.lock и кэши остаются в /var/lib/.
  • Откат 0.2.x-набора pam_cert_scopes / CMS M-of-N work-order / approver-EKU / external policy TOML / pam-certauth execute|policy|gc. Бинарь оставляет только pam-certauth daemon. IPC v2 retains engineer_ski + engineer_cert_sha256 (МКЦ-audit), scopes убран.
  • Cert-binding extensions take precedence over the legacy [[user_mapping]] TOML list. pam_cert_user_binding / pam_cert_host_binding are the sole source of authorisation when present; [[user_mapping]] is consulted only for certificates without pam_cert_user_binding.
  • PAM cdylib syslog backend wired into the tracing subscriber: every error! / warn! emitted from libpam_certauth.so lands in /var/log/auth.log (LOG_AUTH facility, ident pam_certauth, pam_certauth[<pid>]: prefix). Production diagnosis no longer blind.
  • Three PAM-stack snippets shipped alongside the module: /etc/pam.d/certauth (2FA, default), /etc/pam.d/certauth-optional (phased rollout), /etc/pam.d/certauth-only (cert-only, lockout-strict). integrate-pam.sh --mode=2fa|optional|cert-only selects which one to wire in. The deprecated --strict / --optional flags still work as aliases.
  • SysV init script (/etc/init.d/pam-certauth) shipped for hosts without systemd; adds lsb-base dependency to the .deb.
  • Manpage pam-certauth(8) shipped.
  • Docs: USBGuard interop, Astra ЗПС (DIGSIG) caveat, USB-lockout pre-deploy checklist, full on_usb_removed mode reference.

Initial public release.

  • PAM module for X.509 certificate authentication on Astra Linux SE 1.7+.
  • USB token support: PKCS#11 (Rutoken/JaCarta/ESMART), PKCS#12 file.
  • GOST cryptography (Р 34.10-2012, Р 34.11-2012) via openssl + gost-engine.
  • Cert-driven authorisation: per-cert host_binding and user_binding X.509 v3 extensions; no central ACL.
  • Host-removal monitor daemon (pam-certauth) with udev + logind integration: lock/logout/shutdown on USB unplug.
  • Configurable hook execution (pre_auth/post_auth_success/session_open/ session_close) via fork+execve with full sandboxing.
  • Debian package for Astra Linux SE.