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

Установка Tessera на Astra Linux SE

Этот документ — пошаговый сценарий установки и базовой настройки tessera на чистой машине Astra Linux SE 1.7+. В конце — рабочий вход по удостоверению, проверенный через pamtester (§10). Каждый раздел заканчивается командой проверки; если проверка не прошла — см. §11 «Troubleshooting» и troubleshooting.md.

Все команды выполняются от имени root или с sudo. На время правки PAM-стека держите открытый рут-shell в другом терминале. Если PAM-стек собьёт авторизацию, второй терминал — единственный способ откатить изменения.

Окно терминала
cat /etc/astra_version 2>/dev/null || cat /etc/os-release

Ожидаемый вывод: версия 1.7.5 или новее. На других редакциях Astra Linux (Орёл, Воронеж, Смоленск 1.7+) сценарий идентичен. На Ubuntu/Debian — best-effort.

Окно терминала
uname -r

Ожидание: 5.15.0-93-generic или новее (необходимо для корректной доставки udev-событий извлечения USB).

Окно терминала
sudo apt update
sudo apt install -y \
libpam0g \
libssl3 \
libudev1 \
libdbus-1-3 \
libsystemd0 \
pcscd \
opensc-pkcs11 \
pamtester

Если apt install не находит pamtester (или другие пакеты) на Ubuntu/Debian — проверьте, включён ли компонент universe/multiverse (sudo add-apt-repository universe && sudo apt update на Ubuntu): pamtester — реальная зависимость .deb (см. debian/control), но живёт не в main. На Astra SE pamtester не пакетирован вообще — ни в одном компоненте (main/contrib/non-free/non-free-firmware) ни repository-main, ни repository-extended; apt-cache search pamtester там всегда пуст, никакой репозиторий не поможет. Собирается из исходников за минуту (upstream не развивается с 2005 года, версия всегда 0.1.2, зависимость только libpam0g-dev):

Окно терминала
sudo apt install -y build-essential libpam0g-dev
cd /tmp
wget -L "https://sourceforge.net/projects/pamtester/files/pamtester/0.1.2/pamtester-0.1.2.tar.gz/download" -O pamtester-0.1.2.tar.gz
# запасной источник того же тарбола, если SourceForge отдаёт
# промежуточную страницу вместо файла:
# wget http://deb.debian.org/debian/pool/main/p/pamtester/pamtester_0.1.2.orig.tar.gz -O pamtester-0.1.2.tar.gz
tar xzf pamtester-0.1.2.tar.gz
cd pamtester-0.1.2
./configure && make
sudo make install

Ставится в /usr/local/bin/pamtester — в PATH попадает автоматически, дальше используется как в §10.

Разделы 3–4 (выпуск тестового CA и удостоверений) — обычные вызовы openssl без какого-либо движка (ECDSA P-256 поддерживается встроенным default-provider’ом OpenSSL везде, включая macOS), не обязаны выполняться на целевой Astra-машине и вынесены в отдельный документ — cert-issuance-lab.md.

Перед установкой полезно убедиться, что окружение не заблокирует ни сам токен на USB-шине, ни запуск pam_tessera.so / tessera через ЭЦП-контроль.

Если на хосте установлен USBGuard в режиме block, USB-токен должен быть в allowlist — иначе ядро не отдаст устройство udev’у, и tessera не увидит его.

Окно терминала
sudo systemctl is-active usbguard # active / inactive / not-found
sudo usbguard list-devices 2>/dev/null # столбец "block" → токен заблокирован

Разрешить конкретный токен (по vid:pid или по hash) — отдельным правилом в /etc/usbguard/rules.conf:

allow id 0aca:0030 name "Rutoken ECP" hash "ABC..."

После правки правил — sudo systemctl reload usbguard. Подробности по runtime-аспекту (порядок старта monitord относительно USBGuard) — в docs/operations.md §3.5.

В production-развёртывании на Astra SE требуется одно из двух:

  1. astra-digsig-control переведён в logging-only-режим (модуль не блокирует выполнение неподписанных ELF, но шумит в /var/log/syslog сообщениями DIGSIG: NOT_ELF_SIGNED); либо
  2. бинари pam_tessera.so и tessera подписаны через сервис подписи Astra-партнёра (bsign GPG-ключом из доверенной связки в /etc/digsig/keys/) — обычно это шаг сборки .deb в Astra-CI.
Окно терминала
sudo astra-digsig-control status # ВКЛЮЧЕНО / НЕАКТИВНО / logging-only
sudo dmesg | grep -i digsig | tail # видны ли отказы по подписи

В режиме enforce без валидной подписи PAM-аутентификация не проходит — pam_tessera.so просто не загружается. См. также docs/threat-model.md §3.7.

Релизы публикуются на GitHub: github.com/TesseraLabs/tessera/releases. В релизе — только .deb в двух вариантах (…-astra.deb и …-ubuntu.deb), плюс .changes и .buildinfo для аудита; готовых файлов контрольных сумм там нет, их считает оператор на доверенной машине (см. §2.2). Скачать нужный вариант — через gh CLI или вручную из браузера:

Окно терминала
gh release download v0.5.0 --repo TesseraLabs/tessera --pattern '*-astra.deb'
# или для Ubuntu-цели:
# gh release download v0.5.0 --repo TesseraLabs/tessera --pattern '*-ubuntu.deb'

2.2 Генерация контрольных сумм (доверенная машина)

Заголовок раздела «2.2 Генерация контрольных сумм (доверенная машина)»

Контрольные суммы считает сборщик или оператор на доверенной машине (не на целевой) скриптом generate-checksums.sh. Скрипт живёт в репозитории (scripts/generate-checksums.sh), в .deb он не упаковывается — доверенная машина должна иметь клон tessera нужного тега (git clone --branch v0.5.0 --depth 1 https://github.com/TesseraLabs/tessera.git) либо скачать сам файл напрямую:

Окно терминала
curl -fsSL -o generate-checksums.sh \
https://raw.githubusercontent.com/TesseraLabs/tessera/v0.5.0/scripts/generate-checksums.sh
chmod +x generate-checksums.sh
./generate-checksums.sh tessera_0.5.0-1_amd64.deb checksums

Скрипт кладёт в каталог checksums/ файл checksums.txt — отчёт по SHA-256 для самого .deb и каждого файла внутри пакета — плюс standalone-файлы *.sha256. (Скрипт умеет также писать секцию Streebog-256 при наличии gost-engine, но в этом сценарии ECDSA-only она не используется.)

На целевую машину доставляются три вещи: сам .deb, checksums.txt (генерируется на доверенной машине в §2.2, переносится вручную — scp, USB и т.п.) и verify-checksums.sh (скрипт самодостаточен — из внешнего ему нужен только openssl). Сам скрипт, в отличие от checksums.txt, — статичный файл репозитория, его проще скачать напрямую на целевой машине, а не копировать с доверенной:

Окно терминала
curl -fsSL -o verify-checksums.sh \
https://raw.githubusercontent.com/TesseraLabs/tessera/v0.5.0/scripts/verify-checksums.sh
chmod +x verify-checksums.sh
Окно терминала
./verify-checksums.sh tessera_0.5.0-1_amd64.deb checksums.txt

Ожидание: OK: N checksum(s) verified. Скрипт (описан в scripts/verify-checksums.sh) сверяет SHA-256. Ненулевой код выхода — несовпадение суммы (код 1) либо проблема запуска: неверные аргументы (2). В любом из этих случаев пакет не устанавливать, пока причина не разобрана.

Окно терминала
sudo apt install ./tessera_0.5.0-1_amd64.deb

apt подтянет недостающие зависимости (libpkcs11-helper1, librtpkcs11ecp).

Перед systemctl restart tessera или при первой установке прогоните preflight: он валидирует config.toml и сообщает обо всех ошибках конфигурации за один проход — без открытия сокета и без рестарта демона.

Окно терминала
sudo tessera check

Что проверяется:

  • PAM-стек. Сканирует /etc/pam.d/{login,fly-dm,fly-dm-np,sshd,sudo,su} и валит ERROR в трёх случаях:

    1. @include tessera-* стоит ПЕРЕД auth required pam_parsec_mac.so (на Astra SE это убивает account-фазу с «Can’t obtain required data»). Check id: pam_stack_misorder.
    2. session required pam_tessera.so стоит ПЕРЕД pam_systemd.so / @include common-sessionXDG_SESSION_ID ещё не доступен на момент pam_sm_open_session, UpdateSessionTarget не отправляется, monitord не умеет вызвать logind Logout/Lock при извлечении USB. Check id: pam_stack_session_misorder.
    3. Нет литеральной строки auth ... pam_parsec_mac.so (стек без МКЦ или auth-фаза целиком через @include common-auth, как в стоковых sudo/sshd на Debian/Astra), но @include tessera-* стоит ПОСЛЕ @include common-authpam_unix.so из common-auth успевает аутентифицировать по паролю раньше, чем вообще выполнится снипет tessera, что срывает cert-only/optional. Чинится через --unintegrate и повторную интеграцию. Check id: pam_stack_common_auth_misorder, INFO-парой pam_stack_common_auth_ok при корректном порядке.

    Все ERROR подсказывают команду фикса через integrate-pam.sh. Хелсчек для session-фазы дополнительно пишет pam_stack_session_ok (INFO) при корректном порядке или pam_stack_session_no_systemd (INFO) если в стеке вообще нет pam_systemd — типично для sysvinit/OpenRC хостов.

  • [mac].runtime vs ядро. runtime=required без активного parsec_strict_mode()=1 — ERROR (required в strict-mode без МКЦ ядра делает демона бесполезным). auto + отсутствующее ядро — WARN (тихий fallback на StubBackend, MAC НЕ enforced). disabled — INFO.

  • Trust anchors / intermediates. Каждый путь из [trust].anchors и [trust].intermediates должен существовать, быть непустым и содержать хотя бы один -----BEGIN CERTIFICATE----- маркер. Иначе ERROR — демон не может валидировать ни одной цепочки.

  • /etc/tessera/ca/. WARN, если world-writable (mode & 0o002 != 0).

  • PARSEC_CAP_CHMAC. Если МКЦ ядро активно и [mac].runtime ≠ disabled, но у процесса нет capability — WARN: метки на sessions.json не лягут.

  • host_identity-источники. По одной INFO/WARN строке на каждый настроенный источник (machine_id, dmi_*, hostname, custom_command) — видно сразу, что резолвится и что падает.

Exit-код: 0 — только INFO/WARN; 1 — есть хотя бы один ERROR. Тот же check выполняется демоном на старте: при наличии ERROR загрузка прерывается, в journalctl -u tessera останутся структурные сообщения с target=tessera.startup_check для каждой проверки.

2.4¾ Сценарий клонированного образа (golden image → устройство)

Заголовок раздела «2.4¾ Сценарий клонированного образа (golden image → устройство)»

Если устанавливаете на множество устройств через клон одного образа, полный end-to-end workflow вынесен в отдельный документ: docs/clone-image.md — bootstrap-cert на эталоне, finish-bootstrap.sh на каждом клоне, dump-host-id для CA-админа, выпуск per-host сертификата, troubleshooting, Ansible-выкатка.

Tldr — два инструмента, поставляемых в .deb:

  • tessera dump-host-id [--output FILE | --usb] — пробует все известные host_identity-источники, пишет TSV-отчёт. Столбец active_under_current_config=yes отмечает источник, который демон реально использует сейчас. --usb автоматически монтирует первую USB-флешку r/w и пишет host-ids-<hostname>-<UTC>.tsv.
  • /usr/share/tessera/finish-bootstrap.sh — переход за один проход с bootstrap-state на production: перезапись config.toml (sources = ["override"]["dmi_board_serial", "machine_id"]), tessera check, рестарт демона, дамп host_id’ов на USB. Идемпотент. Флаги — см. clone-image.md §4.2.
Окно терминала
systemctl status tessera

Ожидание: Active: active (running). Если inactive (dead) — запустить вручную:

Окно терминала
sudo systemctl enable --now tessera
Окно терминала
tessera --version
test -d /run/tessera && echo "runtime dir OK"
test -S /run/tessera/monitord.sock && echo "socket OK"

Ожидание: версия 0.5.0. Обе остальные строки на свежей машине, где ещё нет /etc/tessera/config.toml (см. постинсталл-подсказку .debcp config.toml.example config.toml), закономерно пустые: демон стартует, но валидный config.toml требует материал из §3 (CA), §7 (расширения удостоверения) и §8 (ролевое хранилище), которого на этом этапе walkthrough’а ещё нет. Обе строки должны появиться только после systemctl restart tessera в конце §8 — полная проверка демона/сокета там же, в Verification (раздел 8) и smoke-тесте §10.

Вынесено в отдельный документ — cert-issuance-lab.md §«Создание тестового CA»: каталог, ключ CA, самоподписанный сертификат, verification. Всё — обычные вызовы openssl без движка (ECDSA P-256), можно выполнять на рабочей станции администратора, не на целевой Astra-машине.

Результат раздела — ca.pem и ca.key в /tmp/ca/, нужны дальше в §4 и §5.

4. Создание тестовой ролевой учётной записи

Заголовок раздела «4. Создание тестовой ролевой учётной записи»

Роль на логине — это имя учётной записи входа: инженер входит в ролевую учётную запись, названную по роли (ssh serv@device), и запрошенная роль равна имени этой УЗ. Отдельного запроса роли нет.

Дальше по сценарию используется роль serv («сервисный инженер») — для неё в пакете есть готовый срез dist/roles/serv.toml, он понадобится в §8. Учётная запись входа тоже называется serv; заводим её в §8, вместе с ролевым хранилищем.

Ключ, CSR, оба обязательных расширения (pam_cert_host_binding, pam_cert_allowed_roles) и упаковка в P12 — в отдельном документе: cert-issuance-lab.md §«Создание удостоверения ролевой учётной записи». Там же — таблица OID, fail-closed-поведение при отсутствии расширений (см. §7 и §10 этого документа) и получение host_id_hash через sudo tessera dump-host-id с целевой машины.

Результат раздела — serv.p12 в /tmp/ca/, готов к переносу на USB-носитель (§5).

Mode A: ключ хранится в .p12 на USB-носителе, защищён парольной фразой. Для production выбирать Mode B (PKCS#11-токен).

tessera ищет .p12 на любой партиции с FS из allowlist (vfat, exfat, ext4, ntfs). Метка партиции значения не имеет — защита обеспечивается на уровне расшифровки .p12 пользовательским паролем и валидации цепочки сертификатов модулем доверия. Лимит на число перебираемых партиций задаётся параметром max_usb_partitions в config.toml (по умолчанию 8, диапазон 1..=64).

Если на USB-флешке несколько разделов и часть содержит посторонние файлы с именем, совпадающим с pkcs12_path_pattern (типично для Apple-форматированных носителей и USB с несколькими партициями), tessera распознаёт их как «не PKCS#12» по ASN.1-конверту (без запроса PIN) и продолжает искать настоящий .p12 на следующих разделах. Ошибки, требующие пароля (неверный PIN / MAC verify / decrypt / chain), остаются fail-closed без перебора.

Типовой рецепт (sdX1 — раздел USB-носителя из вывода lsblk | grep -i usb):

Окно терминала
# ВНИМАНИЕ: команда УНИЧТОЖАЕТ данные на устройстве /dev/sdX1.
# Поддерживаемые FS: vfat, exfat, ext4, ntfs.
sudo mkfs.ext4 /dev/sdX1
sudo mount /dev/sdX1 /mnt/usb
sudo install -m 0600 service.p12 /mnt/usb/service.p12
sudo umount /mnt/usb

Если флешка отформатирована без таблицы разделов (FS лежит прямо на whole-device), это тоже работает: tessera читает ID_FS_TYPE udev и монтирует whole-device напрямую.

/mnt/usb/
├─ certs/
│ ├─ user.p12
│ └─ chain.pem
└─ tessera.marker
Окно терминала
sudo mkdir -p /mnt/usb/certs
sudo cp /tmp/ca/serv.p12 /mnt/usb/certs/user.p12
sudo cp /tmp/ca/ca.pem /mnt/usb/certs/chain.pem
sudo touch /mnt/usb/tessera.marker
sudo umount /mnt/usb
Окно терминала
sudo mount /dev/sdX1 /mnt/usb
ls -la /mnt/usb/certs/
sudo umount /mnt/usb

Ожидание: оба файла присутствуют, размер > 0.

6. Подготовка Рутокен ЭЦП 2.0 (режим pkcs11 / Mode B)

Заголовок раздела «6. Подготовка Рутокен ЭЦП 2.0 (режим pkcs11 / Mode B)»
Окно терминала
sudo apt install librtpkcs11ecp
Окно терминала
pkcs11-tool --module /usr/lib/librtpkcs11ecp.so -L

Ожидание: вывод вида Slot 0 (0x...): ... с моделью токена.

6.3 Инициализация (только для нового, неинициализированного токена)

Заголовок раздела «6.3 Инициализация (только для нового, неинициализированного токена)»
Окно терминала
pkcs11-tool --module /usr/lib/librtpkcs11ecp.so \
--init-token --label "serv-token" \
--so-pin '12345678'
pkcs11-tool --module /usr/lib/librtpkcs11ecp.so \
--init-pin --so-pin '12345678' --pin '1234567890'

pkcs11-tool ждёт ключ и сертификат отдельными объектами в DER/PEM, а не PKCS#12-контейнером. Сначала извлекаем их из serv.p12 (пароль — из cert-issuance-lab.md §«Упаковка в P12»):

Окно терминала
openssl pkcs12 -in serv.p12 -nocerts -nodes -passin pass:test \
-out serv.token.key # приватный ключ, PEM, без пароля
openssl pkcs12 -in serv.p12 -clcerts -nokeys -passin pass:test \
-out serv.token.crt # удостоверение, PEM
# Часть токенов принимает только DER — при необходимости конвертируем:
# openssl pkey -in serv.token.key -outform DER -out serv.token.key.der
# openssl x509 -in serv.token.crt -outform DER -out serv.token.crt.der

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

Окно терминала
pkcs11-tool --module /usr/lib/librtpkcs11ecp.so \
--login --pin '1234567890' \
--write-object serv.token.crt --type cert --label serv --id 01
pkcs11-tool --module /usr/lib/librtpkcs11ecp.so \
--login --pin '1234567890' \
--write-object serv.token.key --type privkey --label serv --id 01

Стираем временный приватный ключ, лежавший открытым на диске:

Окно терминала
shred -u serv.token.key serv.token.key.der 2>/dev/null || shred -u serv.token.key
Окно терминала
pkcs11-tool --module /usr/lib/librtpkcs11ecp.so \
--pin '1234567890' -O

Ожидание: в выводе присутствуют Private Key Object и Certificate Object с label=serv.

Привязка «на каком устройстве и в какой роли» живёт в самом удостоверении. PAM-модуль читает два X.509 v3 расширения листа:

  • pam_cert_host_binding (OID 2.25.183976554325829274683049824615098) — список разрешённых устройств;
  • pam_cert_allowed_roles (OID 2.25.185305973969816596290730578528098241367, некритичное) — список ролей, которые удостоверение вправе активировать.

Это две независимые оси: первая отвечает «где», вторая — «кем». Списка разрешённых учётных записей отдельно нет: имя учётной записи входа и есть роль, поэтому pam_cert_allowed_roles заодно отвечает и на вопрос допуска к УЗ. Допуск решается только удостоверением — механизма, которым устройство разрешало бы вход по своим правилам, в конфигурации не существует. Отказ по любому из двух — fail-closed.

Готовые рецепты openssl.cnf для выдачи удостоверений с правильными расширениями приведены в cert-issuance.md.

Окно терминала
openssl x509 -in /tmp/ca/serv.pem -noout -text \
| grep -E '2\.25\.(183976554325829274683049824615098|185305973969816596290730578528098241367)'

Ожидание: обе строки с дотированными OID присутствуют в выводе.

Роль требуется на каждом входе, и её описание модуль берёт из ролевого хранилища — каталога со срезами <role>.toml на самом устройстве. Пока хранилище пусто или недоступно, устройство не пускает никого: это намеренное fail-closed-поведение, а не ошибка настройки.

Готовый образец среза serv лежит в репозитории — dist/roles/serv.toml; в .deb образцы ролей не входят, поэтому на чистой машине срез проще создать на месте:

Окно терминала
sudo install -d -m 0755 -o root -g root /var/lib/tessera/roles
sudo tee /var/lib/tessera/roles/serv.toml >/dev/null <<'EOF'
role = "serv"
version = 1
os = "linux"
name = "Service Engineer"
level = 5
description = "Service engineer with sudo and higher resource limits."
[payload]
groups = ["service", "wheel"]
sudo_role = "service"
[payload.limits]
nofile = 4096
[session]
max_ttl_seconds = 14400
memory_max = "2G"
tasks_max = 512
EOF
sudo chown root:root /var/lib/tessera/roles/serv.toml
sudo chmod 0644 /var/lib/tessera/roles/serv.toml

Если репозиторий доступен на этой машине, то же самое одной командой:

Окно терминала
sudo install -m 0644 -o root -g root \
dist/roles/serv.toml /var/lib/tessera/roles/serv.toml

Доверие к хранилищу держится на правах файловой системы — той же модели, что у sudoers.d: каталог, все срезы и все родительские каталоги обязаны принадлежать root:root и не быть доступны на запись группе или всем. Срез, лежащий в каталоге непривилегированного пользователя, продукт отвергает: иначе роль (а с ней группы, sudo и лимиты сессии) мог бы переопределить тот, кого она ограничивает.

[roles]
dir = "/var/lib/tessera/roles"
# Потолок сессии, когда его не задают ни удостоверение, ни роль.
# default_session_ttl_seconds = 43200 # 12 ч, значение по умолчанию

Полное описание секции — configuration.md.

Имя учётной записи и есть роль, поэтому на устройстве должна существовать учётная запись serv. В парке ролевые учётные записи провижинятся отдельно; для лабораторного стенда достаточно:

Окно терминала
sudo useradd --create-home --shell /bin/bash serv

Ролевая учётная запись обязана быть обычной, а не системной. Флага --system здесь нет намеренно: он выдаёт uid вне диапазона обычных пользователей, а продукт отвергает вход в учётную запись с таким uid. Такая учётная запись просто не пустит инженера.

Диапазон обычных пользователей задан двумя границами: 1000 и 61183 включительно. Всё, что ниже, — учётные записи, которые дистрибутив и пакеты завели для себя; нижняя граница совпадает с UID_MIN из /etc/login.defs в Debian, Ubuntu и Astra. Всё, что выше, — тоже не обычные пользователи: с 61184 начинается блок, из которого systemd раздаёт uid юнитам с DynamicUser=yes, а дальше лежат nobody (65534) и nogroup (65535). Проверка опирается именно на эти две границы, а не на список зарезервированных имён; uid выше верхней границы — не арифметическая экзотика, а штатное состояние любой системы с systemd.

Верхняя граница намеренно не совпадает с UID_MAX (60000 в Debian): UID_MAX ограничивает лишь то, что useradd раздаёт сам, а ролевая учётная запись, заведённая провижинингом выше этого значения, обязана уметь войти. Значение UID_MAX остаётся разумной рекомендацией провижинингу, но границей отказа не является.

Причина отказа — в том, что пространство имён ролей совпало с пространством имён учётных записей Unix. root, daemon, bin, sys, mail, nobody подходят под формат role_id (^[a-z][a-z0-9-]{0,15}$), и один срез root.toml — опечатка провижининга или скопированный образец — превратил бы ssh root@device в штатный ролевой вход с правами, которых ролевая модель никогда не выдавала. Поэтому продукт смотрит не на имя, а на uid учётной записи на самом устройстве: опасно не имя, а то, что под ним уже существует учётная запись с чужими привилегиями. Список имён кодировал бы догадку о том, какие это имена, расходился бы с дистрибутивом и отвергал бы законные роли — mail в Debian одновременно системная учётная запись и осмысленное имя роли.

Границы продукт несёт в себе, а не читает из /etc/login.defs в момент входа: правка этого файла на устройстве не должна расширять допуск. Поэтому и приёмочная проверка ниже сверяется с границами продукта, а не с содержимым login.defs, — иначе она отвечала бы на другой вопрос, чем модуль в момент входа.

Если учётная запись уже была заведена с --system, менять её uid у живой записи не стоит — файлы останутся за старым владельцем. Проще удалить и завести заново:

Окно терминала
id -u serv # вне диапазона → так входить нельзя
sudo userdel -r serv
sudo useradd --create-home --shell /bin/bash serv

При провижининге через Census этот шаг уже сделан: Census заводит ролевую учётную запись командой useradd -u <uid> … с uid из объявленного в декларации диапазона uid_range, который целиком лежит внутри диапазона обычных пользователей, и проверяет попадание в него для каждой роли.

8.4 Закрытие остальных путей входа в ролевую учётную запись

Заголовок раздела «8.4 Закрытие остальных путей входа в ролевую учётную запись»

Учётная запись из §8.3 создана с домашним каталогом и логин-шеллом. Отсутствие пароля закрывает ровно один путь — парольный вход; открытыми остаются ~/.ssh/authorized_keys, su serv, sudo -u serv -i и любой PAM-стек, в котором нет pam_tessera. Продукт эти пути не закрывает — он не управляет ни sshd_config, ни sudoers, ни чужими PAM-стеками. Их закрывают провижининг и администратор устройства.

Цена пропущенного шага выросла вместе с моделью. Раньше обход давал доступ к личной учётной записи одного инженера; теперь serv — общая ролевая учётная запись с правами роли, и вход в обход pam_tessera не оставляет следа ни в role.audit, ни в журнале выдачи: некому записать, кто именно вошёл.

Ниже — минимум, который нужно выполнить на каждом устройстве. Всё, что касается sshd, выполнять при открытом втором рут-shell (см. pam-integration.md §8).

Окно терминала
sudo passwd -l serv
sudo passwd -S serv # ожидание: во второй колонке L

passwd -S для чужой учётной записи читает /etc/shadow, поэтому выполняется от root; без sudo команда откажет.

«Пароль не задан» и «пароль заблокирован» — разные состояния. useradd оставляет в поле пароля !, и по паролю такая запись действительно не пускает, но это состояние по умолчанию, а не объявленное намерение: один passwd serv, набранный при отладке, молча открывает парольный вход, и в конфигурации устройства это никак не видно. passwd -l делает блокировку явной, а passwd -S — видимой: запись L во второй колонке проверяется одной командой и на приёмке, и позже.

Отдельно: passwd -l не закрывает непарольные методы. Ключ в authorized_keys, su от root, sudo -u продолжают работать — именно поэтому дальше идут остальные шаги.

При провижининге через Census passwd -l уже выполнен: он входит в последовательность заведения ролевой учётной записи. Повторный вызов безвреден, и на устройстве, заведённом руками, он обязателен.

authorized_keys: не должно быть и не должно появиться

Заголовок раздела «authorized_keys: не должно быть и не должно появиться»

Домашний каталог берём из базы, а не угадываем — при провижининге он задаётся декларацией и не обязан быть /home/serv:

Окно терминала
HOME_SERV=$(getent passwd serv | cut -d: -f6)
sudo test -e "$HOME_SERV/.ssh/authorized_keys" \
&& echo "authorized_keys существует — разобраться, откуда"

Ожидание: команда ничего не печатает. Census этот файл не создаёт никогда; если он есть — его создали руками или принесли образом.

Чтобы файл не появился впредь, каталог .ssh отдают root и снимают с ролевой учётной записи право записи в него:

Окно терминала
sudo mkdir -p "$HOME_SERV/.ssh"
sudo chown root:root "$HOME_SERV/.ssh"
sudo chmod 0500 "$HOME_SERV/.ssh"

Честная граница этой меры: она закрывает добавление ключа от имени самого serv (в том числе изнутри уже открытой ролевой сессии), но не от root. Более того, sshd со StrictModes yes принимает authorized_keys, принадлежащий владельцу каталога или root, — то есть root-owned ключ, положенный сюда, был бы принят. Полностью путь публичных ключей для этой учётной записи закрывает только настройка sshd из следующего шага; права на каталог — второй рубеж, а не первый.

sshd: оставить единственный метод аутентификации

Заголовок раздела «sshd: оставить единственный метод аутентификации»

Cert-аутентификация Tessera приходит в sshd через keyboard-interactive и PAM, поэтому закрывать нужно всё остальное, а UsePAM yes и KbdInteractiveAuthentication yes — сохранить:

Окно терминала
sudo tee /etc/ssh/sshd_config.d/50-tessera-roles.conf >/dev/null <<'EOF'
Match User serv
PubkeyAuthentication no
PasswordAuthentication no
KbdInteractiveAuthentication yes
HostbasedAuthentication no
GSSAPIAuthentication no
PermitEmptyPasswords no
Match all
EOF
sudo sshd -t
sudo sshd -T -C user=serv,host=localhost,addr=127.0.0.1 \
| grep -E '^(usepam|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|hostbasedauthentication)'
sudo sshd -T -C user=<обычная-учётная-запись>,host=localhost,addr=127.0.0.1 \
| grep -E '^(usepam|pubkeyauthentication)'

Ожидание первой проверки: usepam yes, kbdinteractiveauthentication yes, остальные три — no. Ожидание второй: usepam yes и pubkeyauthentication yes — то есть на другого пользователя блок не подействовал. Именно sshd -T -C показывает эффективную конфигурацию для конкретной учётной записи, с учётом всех Match-блоков и порядка включений; чтение конфигов глазами этого не заменяет.

Match all в конце — не украшение, не удалять. Область действия Match-блока тянется до следующего Match или до конца всего конфига, а не до конца файла, в котором блок написан. Debian и Ubuntu ставят Include /etc/ssh/sshd_config.d/*.conf первой строкой sshd_config — то есть без завершающего Match all условие User serv накрыло бы весь оставшийся конфиг: и другие включаемые файлы, и всё тело родительского sshd_config. Глобальные директивы, объявленные ниже, перестали бы применяться ко всем прочим пользователям — включая UsePAM yes, который в Debian объявлен именно там. Настройка, закрывающая пути входа в одну учётную запись, отключила бы PAM на всём устройстве. Match all возвращает разбор в глобальный контекст, и вторая проверка sshd -T -C выше — та самая, что ловит эту ошибку.

Два места, где дистрибутивы расходятся, и универсальной команды нет:

  • Каталог sshd_config.d/. Строка Include /etc/ssh/sshd_config.d/*.conf есть в поставке Debian 11+, Ubuntu 20.04+ и Astra с OpenSSH 8.2+. Проверить: grep -n '^Include' /etc/ssh/sshd_config. Если строки нет, файл создавать бессмысленно — блок дописывают в конец /etc/ssh/sshd_config. Завершающий Match all нужен и там: трейлинг-блок безопасен ровно до первой правки после него, а добавить глобальную директиву в конец sshd_config — обычное дело, и она молча попала бы внутрь условия User serv.
  • Имя юнита. sudo systemctl reload ssh в Debian/Ubuntu, sudo systemctl reload sshd в Astra и части сборок. Проверить — systemctl list-units 'ssh*'; перезагружать только после успешного sshd -t.

Если поставленный OpenSSH понимает AuthorizedKeysFile none (8.2+), директиву стоит добавить в тот же Match-блок как третий рубеж поверх PubkeyAuthentication no. На более старых сборках она не поддерживается и ломает старт sshd; проверять — тем же sshd -t сразу после правки.

su и sudo -u: закрыть переход в ролевую учётную запись

Заголовок раздела «su и sudo -u: закрыть переход в ролевую учётную запись»

Единой команды, одинаково работающей на Debian, Ubuntu и Astra, здесь нет: su и sudo — разные механизмы с разными файлами настройки, и закрывать их приходится по отдельности.

Для su работает pam_succeed_if. Модуль входит в пакет libpam-modules, который на всех трёх целевых дистрибутивах стоит из базовой поставки и не выносится в отдельную зависимость. Убедиться, что он на месте, стоит до правки стека — строка ниже написана как requisite, и если модуля нет, su закроется для всех, включая root:

Окно терминала
ls /lib/*/security/pam_succeed_if.so /lib/security/pam_succeed_if.so 2>/dev/null

Ролевые учётные записи собирают в отдельную группу, и переход в члена этой группы запрещают:

Окно терминала
sudo groupadd -f tessera-roles
sudo usermod -aG tessera-roles serv

В /etc/pam.d/su, выше строки auth sufficient pam_rootok.so:

auth requisite pam_succeed_if.so quiet user notingroup tessera-roles

user здесь — целевая учётная запись su, поэтому правило читается как «переход разрешён, только если цель не является ролевой». Выше pam_rootok.so — потому что иначе root минует проверку. Закрыть root полностью всё равно нельзя: у него есть chsh, прямая правка shadow и десяток других путей. Смысл строки в том, чтобы su serv перестал быть будничной командой для всех остальных.

Проверять нужно обе стороны — и что запрет сработал, и что он не задел остальных. От имени непривилегированной учётной записи:

Окно терминала
su - serv # ожидание: отказ
su - <обычная-учётная-запись> # ожидание: запрос пароля и вход

Вторая команда обязательна. Если она тоже отказывает, дело не в правиле, а в самой строке: requisite при отсутствующем или неверно записанном модуле закрывает su целиком. Разбираться с этим следует при открытом втором рут-shell — восстановление правкой /etc/pam.d/su.

Для sudo то же самое делается запретом в runas-списке:

Окно терминала
sudo visudo -cf /etc/sudoers.d/40-tessera-roles # после создания файла

Содержимое (создавать через visudo -f, не tee):

%engineers ALL = (ALL, !%tessera-roles) ALL

Здесь важна оговорка. Отрицание в runas-списке действует только на то правило, где оно написано. Если инженеру где-то ещё выдано (serv) ALL или более широкое правило ниже по файлу — выиграет последнее совпавшее, и запрет не сработает. Поэтому проверять нужно не файл, а результат:

Окно терминала
sudo -l -U <учётная-запись-инженера>

Ожидание: в выводе нет строки, разрешающей запуск от имени serv.

Любой сервис, который аутентифицирует и открывает сессию, — это вход. Стек, в котором нет pam_tessera, пустит в ролевую учётную запись по своим правилам, мимо роли и мимо role.audit. Проверять нужно все стеки, через которые на устройство вообще заходят: sshd, login, fly-dm (и экранная блокировка), su, sudo. Что и куда добавлять — pam-integration.md; tessera check ловит ошибки порядка модулей в тех стеках, где модуль уже стоит, но не подскажет о стеке, куда его не добавили.

Окно терминала
ls -la /var/lib/tessera/roles/
id serv
# uid в диапазоне обычных пользователей (границы продукта, см. §8.3)
u=$(id -u serv)
[ "$u" -ge 1000 ] && [ "$u" -le 61183 ] \
&& echo "uid в диапазоне обычных пользователей: ок"
sudo passwd -S serv
sudo test -e "$(getent passwd serv | cut -d: -f6)/.ssh/authorized_keys" \
&& echo "authorized_keys существует — разобраться, откуда"
sudo sshd -T -C user=serv,host=localhost,addr=127.0.0.1 \
| grep -E '^(usepam|pubkeyauthentication|passwordauthentication)'
sudo sshd -T -C user=<обычная-учётная-запись>,host=localhost,addr=127.0.0.1 \
| grep -E '^(usepam|pubkeyauthentication)'
sudo tessera check

Ожидание: serv.toml присутствует с правами -rw-r--r-- root root, id serv печатает uid/gid, uid попадает в диапазон обычных пользователей, passwd -S показывает L во второй колонке, про authorized_keys не печатается ничего, для servusepam yes при pubkeyauthentication no и passwordauthentication no, для другой учётной записи — usepam yes и pubkeyauthentication yes (блок не протёк за свои границы), tessera check завершается без ERROR.

Правка PAM-стека вынесена в отдельный документ — docs/pam-integration.md:

  • integrate-pam.sh и поставочный snippet
  • Two-include pattern (0.3.12+) и порядок pam_systemd.so
  • fly-dm (зачем + применение + screen-locker)
  • Три режима: 2fa / optional / cert-only с lockout-warning
  • sudo, login, sshd
  • PAM-стек с учётом МКЦ → pam-integration.md §7
  • Безопасность правки + recovery

ВАЖНО. Перед правкой PAM открыть второй рут-shell. Подробности — pam-integration.md §8 «Безопасность правки».

Окно терминала
sudo tessera check

tessera check ловит ошибки порядка PAM-стека (например pam_stack_session_misorder). Полный smoke-тест аутентификации через pamtester — в разделе 10.

Имя, передаваемое pamtester, — это имя ролевой учётной записи, оно же запрошенная роль.

AuthContext, который pam_sm_authenticate кладёт через pam_set_data, живёт только в рамках одного pam_start()/pam_end() — то есть одного процесса, читающего/пишущего один и тот же PAM-хендл. Три отдельных вызова pamtester — это три независимых хендла, и account/session-фазы такого прогона ничего не скажут о реальной работе pam_tessera в проверяемой службе: модуль просто не найдёт контекст, оставленный предыдущим вызовом. Поэтому все операции, которые должны пройти в одном логине, передаются pamtester одним списком — тогда используется один pam_start() на всё:

Окно терминала
pamtester sudo serv authenticate acct_mgmt open_session close_session

Положительный результат: pamtester печатает successfully на каждую операцию по очереди (четыре строки).

pamtester — не полноценный логин-стек (нет привилегированного разделения процессов, как у sshd или login), поэтому такой smoke-тест подтверждает корректность самого PAM-стека и pam_tessera, но не заменяет проверку через реальный сервис. Разница и порядок дальнейшей проверки — в pam-integration.md §9.

В одном терминале запустить:

Окно терминала
pamtester sudo serv authenticate

Сразу после ввода извлечь USB. Ожидание: monitord пишет в журнал:

Окно терминала
sudo journalctl -u tessera -n 20 -g 'medium absent'

Полный справочник по диагностике — docs/troubleshooting.md:

  • Cert/auth-ошибки (host_binding mismatch, роль вне allowed_roles, общий чек-лист)
  • USB и токены (pcscd, Token PIN locked, USBGuard, ЗПС)
  • monitord и демон (monitord not reachable, failed-старт)
  • PAM-стек и lockout (Logout requested but session has no logind id, recovery из rescue.target)
  • МКЦ (pam_parsec_mac: Can't obtain required data, parsec.mac=0, mac_caps_missing, dmi_board_serial = 0)
  • fly-dm и greeter (wallpaper не виден) — см. также fly-dm-greeter.md
  • Clone-image / golden image (dump-host-id пуст, повторный flip) — см. также clone-image.md
  • Инциденты безопасности (компрометация cert, потеря токена, CA worst-case, DIGSIG)

Пакет ставит оба init-варианта: tessera.service (systemd) и /etc/init.d/tessera (SysV). На systemd-хостах SysV-скрипт трогать не нужно. На non-systemd:

Окно терминала
sudo update-rc.d tessera defaults
sudo service tessera start

Подробности (caveats, отсутствие logind logout) — pam-integration.md §10.

  • docs/configuration.md — справочник по всем параметрам config.toml.
  • docs/cert-issuance.md — выдача удостоверений с расширениями pam_cert_host_binding и pam_cert_allowed_roles.
  • docs/operations.md — runbook эксплуатации и процедуры incident response.
  • docs/threat-model.md — модель угроз и какие атаки модуль защищает.

Полная активация МКЦ (capability демону, поставляемый PAM-стек, systemd drop-in, per-user MNKC, защита config.toml через ilevel=63, verify, откат) — отдельный документ: docs/mac-integrity.md.

Краткий путь:

  1. astra-strictmode-control enable + reboot.
  2. usercaps -m "+3" tessera + pdpl-user --ilevel 63 tessera.
  3. Скопировать tessera.example и mac-integrity.conf.example из /usr/share/tessera/ в /etc/pam.d/ и /etc/systemd/system/tessera.service.d/.
  4. pdpl-user --ilevel 63 <pam_user> для каждого end-user.
  5. [mac].cert_integrity = "required" + runtime = "required", рестарт демона.

Default (cert_integrity = "ignore", runtime = "disabled") — production-готов без активации МКЦ. Ничего настраивать не надо.