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

Архитектура Tessera

Этот документ — единая ссылочная архитектура версии 0.4.0. После прочтения инженер должен корректно отвечать на вопросы:

  • что происходит при вызове pam_sm_authenticate?
  • что лежит в /run/tessera/?
  • что делает monitord при udev REMOVE-событии?
  • как сериализуется IPC и какие сообщения проходят между PAM-модулем и monitord?
  • Аутентифицирует локального UNIX-пользователя по X.509-сертификату на USB-носителе или PKCS#11-токене.
  • Привязывает предъявителя к машине и к ролевой учётной записи через X.509 v3 расширения pam_cert_host_binding и pam_cert_allowed_roles, встроенные в сам leaf-сертификат.
  • Мониторит состояние USB-носителя в течение сессии и реагирует на его извлечение (lock / logout / hook / shutdown).
  • Корректно обрабатывает suspend/resume.
  • Делегирует ГОСТ-криптографию сертифицированному gost-engine.
  • Не реализует свою криптографию (всё через OpenSSL и gost-engine).
  • Не управляет жизненным циклом CA (выпуск/отзыв сертификатов — задача внешнего УЦ).
  • Не управляет PIN-кодами токенов (это задача администратора и пользователя).
  • Не защищает от компрометации учётной записи root или ядра ОС (вне TOE).
  • Сетевые запросы выполняет только на revocation-пути в OCSP-режимах (mode ∈ {ocsp, crl_then_ocsp}): синхронный HTTP POST на заданный конфигом ocsp_responder_url, с жёстким таймаутом и дисковым кэшем. В режимах none/crl сети нет вовсе (offline CRL). Zero-egress контуры (терминалы) остаются на none/crl.

Полное описание границ TOE — в docs/threat-model.md.

tessera — это workspace из шести крейтов и одна ОС-интеграция (systemd, udev, logind). Ниже — четыре runtime-крейта, работающие на устройстве. Остальные два относятся к выпуску сертификатов: tessera_ext — общие определения X.509-расширений Tessera (OID, DER-кодеки), используется и ядром, и инструментами выпуска; tessera_issuer — инструменты выпуска; его ядро pure-Rust собирается и под wasm32 (внешний веб-кабинет линкует его как WASM-ядро), описано в issuer.md.

Синхронное ядро. Содержит:

  • Загрузку и валидацию конфигурации (config::raw::RawConfigconfig::validated::ValidatedConfig).
  • Парсинг и проверку X.509 (x509/).
  • Цепочки доверия и проверку CRL (trust/, crl/).
  • Challenge-response (challenge/).
  • ГОСТ-делегацию через gost-engine (gost/).
  • PKCS#12 и PKCS#11 (pkcs12/, token/).
  • USB-mount и MountGuard RAII (usb/, mount/).
  • Хуки (hooks/).
  • Host identity chain (host_identity/).
  • Cert-scope verification — парсинг расширений pam_cert_host_binding / pam_cert_allowed_roles и сверка их записей с host_id_hash и запрошенной ролью, она же pam_user (x509/, verify_cert_scope).
  • IPC client side (ipc/).

Без tokio, без асинхронности. Все операции блокирующие — это оправдано: PAM-модуль вызывается из синхронного контекста libpam.

Wire-протокол IPC между PAM-модулем и демоном. Содержит:

  • ClientMessage и ServerMessage — варианты сообщений (crates/tessera_proto/src/client.rs, .../server.rs).
  • WireError и encode/decode-функции (wire.rs).
  • framing::FramingError — кадрирование NDJSON.
  • SessionTarget — кодирует tty/display/logind-id для конкретной сессии.
  • PROTOCOL_VERSION — текущее значение 2.

#![forbid(unsafe_code)] — крейт чисто-safe.

PAM service module. Содержит:

  • PAM entry points: pam_sm_authenticate, pam_sm_setcred, pam_sm_acct_mgmt, pam_sm_open_session, pam_sm_close_session (см. crates/pam_tessera/src/entry.rs).
  • Panic guard (panic_guard.rs) — каждая C-граница защищена catch_unwind, panic → PAM_AUTHINFO_UNAVAIL.
  • DI wiring (di.rs) — собирает зависимости ядра из конфига.
  • Flow orchestrator (flow.rs) — основной авторизационный пайплайн.
  • PAM conversation helpers (pam_conv.rs).
  • Persistent data между pam_sm_* вызовами (data_handle.rs).

Билдится в /lib/security/pam_tessera.so (см. debian/rules).

Мульти-командный CLI: tessera daemon (долгоживущий демон, юнит tessera.service со строкой запуска /usr/bin/tessera daemon --config /etc/tessera/config.toml), tessera check, tessera dump-host-id, tessera role, tessera tags, tessera enroll. Демон владеет:

  • Сокетом IPC (/run/tessera/monitord.sock).
  • udev-мониторингом USB-устройств (udev_monitor.rs).
  • D-Bus подключением к systemd-logind (logind.rs).
  • Реестром активных сессий (registry.rs, state.rs).

Основан на tokio multi-thread (см. main.rs). Использует sd_notify для интеграции с systemd Type=notify. Билдится в /usr/bin/tessera и поставляется юнитом tessera.service.

КомпонентИсточникДоверие
libpam0gсистемный, Astra/Debian repoда
libssl3системный, Astra/Debian repoда
gost-engineAstra SE 1.7+ (СКЗИ ФСБ)да (в составе сертифицированной ОС)
librtpkcs11ecp.soРутокен, поставляется отдельнода (СКЗИ ФСБ)
libjcPKCS11.soJaCarta, поставляется отдельнода (СКЗИ ФСБ)
libudev1системный, Astra/Debian repoда
libdbus-1-3системный, Astra/Debian repoда
libsystemd0системный, Astra/Debian repoда

ГОСТ и PKCS#11. Подпись ГОСТ-алгоритмами работает только на PKCS#12-пути — через gost-engine в OpenSSL. На PKCS#11-пути (librtpkcs11ecp.so, libjcPKCS11.so) ГОСТ-механизмы не поддержаны (crate cryptoki не покрывает GOST mechanisms); Рутокен/JaCarta на PKCS#11 применимы для RSA/ECDSA-сертификатов. Поддержка GOST-на-PKCS#11 — proposal openspec/changes/gost-pkcs11.

flowchart TD
libpam[libpam.so] --> cdylib[libpam_tessera.so]
cdylib --> core[tessera_core]
cdylib --> proto[tessera_proto]
monitord[tessera] --> proto
monitord --> core
cdylib -. "AF_UNIX SOCK_STREAM NDJSON" .-> monitord
core --> openssl[libssl3 + gost-engine]
core --> pkcs11[PKCS#11 module]
monitord --> udev[libudev]
monitord --> dbus[libdbus / logind]

PAM-стек делает несколько вызовов в порядке auth → account → session. tessera обрабатывает их все, но реальная работа — в pam_sm_authenticate. Остальные читают сохранённый AuthContext из PAM data.

  1. Распаковать аргументы модуля (config=...).
  2. Загрузить и валидировать config.toml (через tessera_core::config::load_validated_config). При ошибке — PAM_AUTHINFO_UNAVAIL.
  3. Запустить self_check (engine, paths, hooks placeholders). При ошибке — PAM_AUTHINFO_UNAVAIL.
  4. Прочитать PAM_USER, PAM_SERVICE, PAM_TTY из libpam. Запрошенная роль выводится из PAM_USER: имя учётной записи входа И ЕСТЬ роль. Иных источников роли нет, и PAM_USER не переписывается. Имя, не удовлетворяющее формату role_id, отвергает вход до обращения к носителю.
  5. Через di::wire собрать DI-граф (mount, trust, token).
  6. Резолвить host_id через цепочку источников из конфига и вычислить host_id_hash = sha256(host_id).
  7. Запустить flow::authenticate(ctx):
    • смонтировать USB или открыть PKCS#11-сессию;
    • найти сертификат, проверить цепь и revocation;
    • challenge-response с приватным ключом;
    • извлечь расширения pam_cert_host_binding и pam_cert_allowed_roles из leaf-сертификата и сверить их с host_id_hash и запрошенной ролью через verify_cert_scope. pam_cert_allowed_roles — единственный источник допуска к учётной записи: PAM_USER и есть роль, поэтому один список отвечает и на вопрос «в какую учётную запись пущен предъявитель». Отката к конфигурации устройства не существует; отсутствие расширения — отказ, а не переход на иной механизм (коды отказов — в §13).
  8. При успехе — построить AuthContext и сохранить через pam_set_data.
  9. Отправить Hello + SessionOpen в monitord (получить Ack).
  10. Вернуть PAM_SUCCESS. Любая ошибка маппится в PAM_AUTHINFO_UNAVAIL / PAM_PERM_DENIED / PAM_MAXTRIES / PAM_AUTH_ERR / PAM_SYSTEM_ERR по семантике flow::FlowError::pam_code (таблица — в §13).

Не делает ничего сверх PAM_SUCCESS. Сертификаты не размещаются в keyring пользователя.

Читает AuthContext, проверяет, что:

  • notAfter сертификата ещё не истёк (с допуском clock_skew_seconds; значение берётся из конфига в момент pam_sm_authenticate и сохраняется в AuthContext).

При несоответствии возвращает PAM_ACCT_EXPIRED.

Читает AuthContext. Отправляет в monitord SessionOpen с полным payload’ом (см. client.rs::SessionOpenPayload):

  • session_id (UUID);
  • pam_user, pam_service;
  • target (Tty / Display / LogindSession);
  • usb_serial — серийник носителя, авторизовавшего сессию;
  • host_id_hash — hex SHA-256 от host_id;
  • opened_at — wall-clock unix-время;
  • cert_cn, cert_serial;
  • engineer_ski — lowercase-hex SubjectKeyIdentifier сертификата инженера (v2; пустая строка на кадрах v1-клиента);
  • engineer_cert_sha256 — lowercase-hex SHA-256(cert DER) leaf’а инженера (v2);
  • uid — Unix-uid, который аутентифицировал PAM-модуль (v2; 0 при отсутствии в кадре v1-клиента);
  • role, role_version — id роли и версия среза роли, с которыми открыта сессия (опциональные NDJSON-поля v2 — опциональны ради совместимости с кадрами прежних версий; в текущей версии роль есть на каждом входе).

Monitord добавляет сессию в реестр и начинает мониторинг USB.

Отправляет SessionClose { session_id, closed_at }. Monitord удаляет сессию из реестра и не триггерит on_usb_removed — пользователь явно завершил сессию.

Что tessera держит на диске во время работы, кто пишет и кто читает каждый путь. Схема ниже — общая карта, таблица — точные владельцы и права доступа.

flowchart LR
etc["/etc/tessera/"] --> cfg[config.toml]
etc --> ca["ca/bundle.pem"]
etc --> crl["crl/*.pem"]
run["/run/tessera/"] --> sock[monitord.sock]
run --> sessions["sessions/sid/"]
run --> state[sessions.json]
run --> lock[daemon.lock]
var["/var/lib/tessera/ (root-owned policy)"] --> daemon["daemon/ (tessera-owned state)"]
daemon --> wp["wallpaper.orig.jpg"]
ПутьКто пишетКто читаетПрава
/etc/tessera/config.tomlадминистраторcdylib + monitord0640 root:root
/etc/tessera/ca/bundle.pemадминистраторcdylib + monitord0640 root:root
/run/tessera/monitord.sockmonitordcdylib0660 tessera:tessera
/run/tessera/sessions/<sid>/cdylibудаляет MountGuard на drop0700 root:root
/run/tessera/sessions.jsonmonitordmonitord (между перезапусками демона в пределах boot; tmpfs, эфемерный)0600 tessera:tessera
/run/tessera/daemon.lockmonitord (flock-singleton; рядом с sessions.json, fallback /var/lib/tessera/daemon/)monitord
/var/lib/tessera/roles/*.toml, /var/lib/tessera/tags.tomlадминистраторcdylibroot-owned; весь путь без group/world write
/var/lib/tessera/daemon/wallpaper.orig.jpgmonitordmonitordсостояние демона под 0750 tessera:tessera
/var/cache/tessera/ocsp/*.dercdylib (auth-путь, OCSP-кэш)cdylib (с ре-верификацией перед использованием)0640 root:root (каталог 0750 root:root)

/run/tessera/ и /var/lib/tessera/daemon/ создаются systemd через директивы RuntimeDirectory и вложенную StateDirectory юнита. Сам /var/lib/tessera/ пакет оставляет 0750 root:tessera, поэтому непривилегированный демон не может подменить доверенные роли и теги. (см. tessera.service и dist/tmpfiles/tessera.conf). /var/cache/tessera/ocsp/ создаёт postinst пакета (debian/postinst).

6. Диаграмма последовательности — pam_sm_authenticate, успешный путь с PKCS#11

Заголовок раздела «6. Диаграмма последовательности — pam_sm_authenticate, успешный путь с PKCS#11»
sequenceDiagram
participant U as User
participant L as libpam
participant P as cdylib
participant C as core
participant T as PKCS#11 module
participant M as monitord
U->>L: попытка входа
L->>P: pam_sm_authenticate
P->>C: load_validated_config
P->>C: self_check
P->>C: resolve_host_identity
C->>T: C_OpenSession
P->>U: запрос PIN
U->>P: PIN
P->>T: C_Login
T-->>P: cert + privkey handles
C->>C: build_chain + verify_chain
C->>C: revocation check
P->>U: запрос challenge
P->>T: C_Sign(challenge)
T-->>P: signature
C->>C: verify(signature, pubkey, challenge)
C->>C: parse_cert_extensions + verify_cert_scope
P->>M: Hello(protocol_version=2)
M-->>P: HelloAck
P->>L: PAM_SUCCESS

7. Диаграмма последовательности — pam_sm_open_session + IPC SessionOpen

Заголовок раздела «7. Диаграмма последовательности — pam_sm_open_session + IPC SessionOpen»
sequenceDiagram
participant L as libpam
participant P as cdylib
participant M as monitord
L->>P: pam_sm_open_session
P->>P: read AuthContext from pam_data
P->>M: SessionOpen payload
M->>M: registry.insert(session_id)
M->>M: start udev watch for usb_serial
M-->>P: Ack
P->>L: PAM_SUCCESS

8. Диаграмма последовательности — извлечение USB → grace → lock

Заголовок раздела «8. Диаграмма последовательности — извлечение USB → grace → lock»
sequenceDiagram
participant K as kernel/udev
participant M as monitord
participant L as logind
participant U as User session
K->>M: udev REMOVE event
M->>M: lookup session by serial
M->>M: start grace timer
alt USB не вернулся за grace
M->>L: LockSession(id)
L->>U: lock screen
else USB вернулся
K->>M: udev ADD event
M->>M: cancel grace timer
M->>M: log removal cancelled
end

Поведение on_usb_removed:

  • "lock"LockSession (по умолчанию).
  • "logout"TerminateSession.
  • "hook" — выполняется хук usb_removed.
  • "shutdown"PowerOff через D-Bus к logind.

9. Диаграмма последовательности — suspend / resume

Заголовок раздела «9. Диаграмма последовательности — suspend / resume»
sequenceDiagram
participant L as logind
participant M as monitord
L->>M: PrepareForSleep(true)
M->>M: snapshot active sessions
Note right of M: udev REMOVE-события<br/>в течение suspend<br/>будут отмечены transient
L->>M: PrepareForSleep(false)
M->>M: arm suspend_grace timer
Note right of M: пока таймер активен,<br/>любые udev REMOVE<br/>с reinsertion в grace<br/>игнорируются

При monitor_fail_mode = "strict" cdylib ожидает Ack от monitord по таймауту; при "permissive" — переживает кратковременную недоступность.

  • AF_UNIX SOCK_STREAM.
  • Путь сокета: /run/tessera/monitord.sock.
  • Права: 0660 tessera:tessera (см. tmpfiles + systemd RuntimeDirectory).
  • Аутентификация peer’а: SO_PEERCRED — monitord проверяет, что uid == 0. Любой иной peer закрывается.
  • Реализация: crates/tessera_cli/src/peercred.rs.

Newline-delimited JSON (NDJSON):

  • каждый кадр — единственная строка UTF-8 JSON;
  • терминатор — единственный байт \n;
  • максимальный размер кадра — MAX_FRAME_BYTES = 64 KiB (см. crates/tessera_proto/src/wire.rs).

Обоснование выбора NDJSON:

  • стандартные tools (jq, journalctl-форматер) умеют обрабатывать его без специальной поддержки;
  • кадрирование тривиально — \n-разделитель;
  • расходы на парсинг JSON оправданы низкой частотой сообщений (≤ 10 в секунду в типовом дне).
  • PROTOCOL_VERSION: u32 = 2 (см. crates/tessera_proto/src/version.rs). Версия 2 добавила GetActiveSessionByUid / ActiveSession, дополнительные поля SessionOpen (engineer_ski, engineer_cert_sha256, uid, а также опциональные role / role_version) и код ошибки NO_ACTIVE_SESSION (1200); кадры v1-клиента без новых полей десериализуются.
  • Первый кадр на любом соединении — Hello { protocol_version }.
  • Если protocol_version не равен серверному, monitord отвечает Error { code: 1000 (PROTOCOL_MISMATCH) } и закрывает соединение.
  • Семантика версий: MAJOR-mismatch → разрыв; MINOR (если появятся) — best-effort обратная совместимость.

Из crates/tessera_proto/src/client.rs:

{"type": "hello", "protocol_version": 2, "agent": "libpam_tessera/0.4.0"}
{"type": "session_open", "session_id": "1c5e8a90-3b6f-4a1d-9c2e-77f0b1c2d3e4", "pam_user": "alice", "pam_service": "sudo", "target": {"kind": "logind_session", "id": "12"}, "usb_serial": "RUTOKEN-001", "host_id_hash": "ee0bd4f3a3c8e21d4a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f", "opened_at": 1735689600, "cert_cn": "Alice", "cert_serial": "01a2b3c4d5e6f70809"}
{"type": "session_close", "session_id": "1c5e8a90-3b6f-4a1d-9c2e-77f0b1c2d3e4", "closed_at": 1735689700}
{"type": "ping"}

Из crates/tessera_proto/src/server.rs:

{"type": "hello_ack", "server_version": "0.4.0", "protocol_version": 2}
{"type": "ack"}
{"type": "pong"}
{"type": "error", "code": 1000, "message": "protocol version mismatch"}

Раздельных пофреймовых таймаутов нет. Клиент применяет единый конфигурируемый monitor.timeout_ms (дефолт 2000 мс, диапазон 100..=60000 мс) ко всему соединению целиком — значение ставится через set_read_timeout / set_write_timeout на сокете в момент MonitordClient::connect (см. crates/tessera_core/src/ipc/client.rs) и покрывает и хендшейк, и все последующие RPC на этом соединении.

ИнициаторСообщениеПолучательОжидаемый ответДействие при timeout
clientHelloserverHelloAck или Errorразрыв соединения
clientSessionOpenserverAck или Errorсогласно monitor_fail_mode
clientSessionCloseserverAcklog + продолжить
clientPingserverPonglog + продолжить

Из crates/tessera_proto/src/server.rs:

КодИмяСемантикаДействие cdylib
1000PROTOCOL_MISMATCHВерсии протокола не совпали.fail-closed
1001DEVICE_GONEUSB-устройство по usb_serial отсутствует.fail-closed
1003UNAUTHORIZEDPeer не uid=0 (по SO_PEERCRED).разрыв
1100BAD_REQUESTНевалидный кадр (нарушение схемы).разрыв + log
1101PROTOCOL_VIOLATIONНарушение wire-протокола: oversize-кадр, idle-таймаут и т.п. Сервер закрывает соединение после отправки.разрыв + log
1200NO_ACTIVE_SESSIONНет активной сессии для запрошенного uid (ответ v2-демона на GetActiveSessionByUid).штатный «не найдено»
1500INTERNALВнутренняя ошибка демона.по monitor_fail_mode

DEVICE_GONE и UNAUTHORIZED фатальны всегда — они меняют вердикт аутентификации и пробрасываются даже в permissive (ipc/failmode.rs). Остальные ошибки — по политике monitor_fail_mode на конкретном call-site; на auth-пути сбой уведомления monitord не отменяет уже состоявшийся успех аутентификации (см. §13).

{
"title": "SessionOpenPayload",
"type": "object",
"properties": {
"session_id": {"type": "string", "format": "uuid"},
"pam_user": {"type": "string"},
"pam_service": {"type": "string"},
"target": {"type": "object"},
"usb_serial": {"type": ["string", "null"]},
"host_id_hash": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
"opened_at": {"type": "integer"},
"cert_cn": {"type": "string"},
"cert_serial": {"type": "string", "pattern": "^[0-9a-f]+$"},
"engineer_ski": {"type": "string", "pattern": "^[0-9a-f]*$"},
"engineer_cert_sha256": {"type": "string", "pattern": "^[0-9a-f]*$"},
"uid": {"type": "integer"},
"role": {"type": ["string", "null"]},
"role_version": {"type": ["integer", "null"]}
},
"required": ["session_id", "pam_user", "pam_service", "target", "host_id_hash", "opened_at", "cert_cn", "cert_serial"]
}

Поля engineer_ski, engineer_cert_sha256, uid — из версии 2: v2-клиент их всегда сериализует, а кадр v1-клиента без них десериализуется в значения по умолчанию (пустая строка / 0), поэтому в required они не вынесены. role / role_version присутствуют только когда сессия открыта с ролью.

  • Полностью синхронный, без tokio.
  • Соединение с monitord — единственное per pam_sm_* вызов; закрывается после ответа.
  • Без shared mutable state: каждый PAM-вызов имеет собственный flow::Context.
  • tokio multi-thread runtime (количество worker threads — по умолчанию системный default tokio).
  • На каждое входящее соединение — отдельная задача (server.rs::handle_connection).
  • Реестр сессий — Mutex<RegistryStore> (см. registry.rs).
  • udev и logind — свои dedicated long-running tasks.
  • Запись /run/tessera/sessions.json — atomic-rename через tempfile (без flock; от двойного запуска демона защищает отдельный singleton-замок daemon.lock). Файл лежит на tmpfs (RuntimeDirectory=tessera), намеренно эфемерный: реестр нужен только между перезапусками демона в пределах одного boot — все процессы, держащие эти сессии (sshd/login/ sudo), всё равно умирают на reboot.
  • cdylib создаёт каталог <sid> через MountGuard::new (RAII).
  • Удаляет каталог в Drop (или в pam_sm_close_session).
  • monitord не пишет в этот каталог напрямую — только читает при диагностике.

host_id вычисляется в момент pam_sm_authenticate через цепочку источников из секции [host_identity]. Реализация — crates/tessera_core/src/host_identity/chain.rs.

Источники в порядке предпочтения:

  1. machine_id/etc/machine-id (стабилен между перезагрузками, меняется при переустановке).
  2. dmi_board_serial/sys/class/dmi/id/board_serial (стабилен на уровне железа, меняется при замене материнской платы).
  3. dmi_system_uuid / dmi_system_serial — DMI-идентификаторы системы (стабильность зависит от вендора/гипервизора).
  4. hostname/etc/hostname (нестабилен, легко подменяется; OK для тестов).
  5. custom_command — администраторский скрипт.
  6. override — фиксированное значение из конфига (bootstrap/тесты).

Цепочка обходится в указанном sources порядке. Первый непустой результат — победитель. Если все источники пустые:

  • fallback = "deny"PAM_AUTH_ERR (production по умолчанию);
  • fallback = "warn"PAM_SUCCESS с warning-логом (тестовое окружение);
  • fallback = "allow"PAM_SUCCESS молча (опасно, не использовать).

Модуль спроектирован fail-closed: отказ любой проверки сертификата (цепочка доверия, отзыв, challenge-response, привязка host/user) ведёт к отказу во входе, а не к пропуску. Таблица ниже перечисляет условия и PAM-коды, которые получает стек; единственное осознанное исключение — недоступность monitord не отменяет уже состоявшийся успех аутентификации — вынесено в «Принципы» под таблицей.

#УсловиеВозврат
1panic в любом pam_sm_*PAM_AUTHINFO_UNAVAIL (9)
2загрузка config.toml упалаPAM_AUTHINFO_UNAVAIL (9)
3self_check упал (engine, paths, hooks)PAM_AUTHINFO_UNAVAIL (9)
4USB/mount/discovery не дали носителя, PKCS#11-модуль не загрузилсяPAM_AUTHINFO_UNAVAIL (9)
5сертификат не проходит chain verificationPAM_PERM_DENIED (6)
6revocation check провалился (crl: серийник в CRL, CRL отсутствует/несвежа; ocsp/crl_then_ocsp: responder недоступен, таймаут, статус unknown/revoked, подпись ответа невалидна)PAM_PERM_DENIED (6)
7challenge-response не сошёлсяPAM_PERM_DENIED (6)
8запрошенная роль не покрыта pam_cert_allowed_roles — расширение отсутствует, невалидно (список считается пустым) либо роли в нём нетPAM_PERM_DENIED (6)
9исчерпан лимит попыток PIN (MaxTries, PinLocked)PAM_MAXTRIES (11)
10расширение pam_cert_host_binding отсутствует или невалидноPAM_AUTH_ERR (7)
11host_id_hash не входит в записи pam_cert_host_bindingPAM_AUTH_ERR (7)
12единичная ошибка PIN, ошибка PAM conversation, отказ хукаPAM_AUTH_ERR (7)
13нарушение внутренних инвариантов (Internal)PAM_SYSTEM_ERR (4)
14Error из monitord с code = DEVICE_GONE / UNAUTHORIZEDпробрасывается всегда, даже в permissive

Полная таблица соответствия FlowError → PAM-код — doc-comment FlowError::pam_code в crates/pam_tessera/src/flow.rs.

Принципы:

  • panic’и и инфраструктурные ошибки → PAM_AUTHINFO_UNAVAIL (сообщает PAM-стеку: «следующий модуль может попробовать»).
  • Отказы криптографической проверки (цепь, revocation, challenge-response) и отказ по роли → PAM_PERM_DENIED; отказы host-scope (pam_cert_host_binding) и прочие auth-ошибки → PAM_AUTH_ERR; исчерпанный PIN-бюджет → PAM_MAXTRIES.
  • Регистрация в monitord входит в вердикт аутентификации. При monitor_fail_mode = "strict" транспортная ошибка IPC или отказ регистрации закрывает вход: сессию не удалось поставить под enforcement извлечения. При permissive обёртка FailModeWrapper поглощает транспортные ошибки, а DEVICE_GONE и UNAUTHORIZED остаются фатальными в любом режиме (crates/tessera_core/src/ipc/failmode.rs).

cdylib pam_tessera.so логирует в syslog, а не в stderr: libpam отбрасывает stderr модуля, поэтому на stderr production-диагностика недоступна. tracing-подписчик строится в момент первого вызова pam_sm_* и шлёт записи в syslog через LOG_AUTH facility с ident pam_tessera. На системах с journald эти строки видны через journalctl -t pam_tessera и попадают в /var/log/auth.log (на обычном syslog-стеке) с префиксом pam_tessera[<pid>]:. (Переход с stderr на syslog — 0.1.1, см. changelog.md.)

tessera использует tracing-journald и пишет в journald через нативный Type=notify-канал. На SysV-init хостах без journald записи tracing уходят в stderr демона; куда они попадут дальше — определяется тем, как init-скрипт перенаправляет stderr (в стандартной поставке start-stop-daemon отдаёт stderr системному syslog’у через logger).

Полная семантика того, что и на каком уровне логируется, — в docs/operations.md §6.

  • docs/threat-model.md — какие угрозы покрывает каждый из этих fail-closed правил.
  • docs/configuration.md — какие поля влияют на поведение, описанное здесь.
  • docs/operations.md — как читать журнал и диагностировать аномалии.