Перейти к содержанию

SSH Key Registry и Keyholder API

Не путать с сертификатным SSH (/enroll/ssh)

Alatyr поддерживает два независимых механизма SSH-доступа. Первый — описанный в «Архитектура и PKI» выпуск короткоживущих OpenSSH-сертификатов через Vault SSH secrets engine (/enroll/ssh, purpose ssh, отзыв через KRL). Второй — SSH Key Registry, тема этой страницы: реестр «сырых» hardware-backed публичных ключей без какого-либо сертификата и без CA. Это две разные, параллельные подсистемы: у них независимое хранение данных, разные эндпоинты регистрации и разный жизненный цикл — сертификат истекает и переиздаётся, а зарегистрированный сырой ключ живёт, пока его явно не отзовут.

Модель: raw-ключ вместо сертификата

На устройстве в TPM 2.0 или Secure Enclave (в зависимости от платформы) уже существует аппаратный ключ пользовательского присутствия — тот же самый, которым агент пользуется для аутентификации по mTLS. Реестр не создаёт для него отдельный ключ — он просто регистрирует публичную половину этого ключа на сервере как самостоятельную SSH-идентичность:

  • Приватный ключ никогда не покидает TPM/Secure Enclave.
  • Сервер хранит только публичный ключ в формате authorized_keys и его SHA-256-отпечаток — никакого сертификата, CA или срока действия.
  • Один и тот же принципал (логин пользователя) может иметь несколько зарегистрированных ключей — с разных устройств, а при необходимости и несколько с одного.

Регистрация

Агент отправляет POST /api/v1/enroll/ssh-key:

{
  "device_serial": "...",
  "enrollment_token": "...",
  "principal": "ivan.petrov",
  "ssh_public_key": "ssh-ed25519 AAAA... alatyr-agent",
  "proof": { "...": "TPM2 AK-to-EK proof-of-possession, если применимо" }
}

Учётной записью эндпоинт не аутентифицируется. Как и остальные /enroll/*, он проверяет enrollment_token устройства (тот же общий секрет, что и у других видов заявок) и ограничен по частоте запросов. Перед созданием строки эндпоинт последовательно проверяет:

  1. Allowlist принципалаprincipal должен точно совпадать с одним из значений в администрируемом списке разрешённых принципалов для purpose ssh. Это тот же список, которым пользуется сертификатный /enroll/ssh: второй администрируемый список ради одного и того же смысла («кому вообще разрешён SSH-доступ») не потребовался.
  2. Существование устройства + enrollment_token — устройство должно уже быть заведено (сама регистрация ключа новое устройство не создаёт), токен сверяется constant-time-сравнением.
  3. Доказательство обладания ключом (если платформа это поддерживает) — для TPM-хранилищ проверяется криптографическое доказательство владения ключом; для Secure Enclave отдельное доказательство не требуется — у него нет внешне проверяемой цепочки аттестации в принципе.

Публичный ключ, приложенный к регистрации, не превращается в X.509-CSR и никуда не подписывается — сервер лишь сохраняет его как есть и вычисляет отпечаток для дедупликации по паре (device_id, fingerprint). Повторная отправка уже известного (device, ключ) — идемпотентна: существующая строка возвращается без изменений, в том числе если она уже revoked/rejected — повторная регистрация никогда не «оживляет» отозванный или отклонённый ключ молча.

Жизненный цикл ключа

Статус Значение
pending Зарегистрирован, ждёт решения администратора
active Одобрен (вручную или автоматически) — ключ реально обслуживается Keyholder API
rejected Отклонён администратором — можно повторно одобрить (approve) позже
revoked Отозван — терминальное состояние, approve больше не проходит (409, не 404)

По умолчанию каждая новая регистрация уходит в pending и требует ручного подтверждения POST /api/v1/admin/ssh-keys/{id}/approve (роль cert-admin, и только она — в отличие от обычных заявок на сертификат, где approve доступен ещё и cert-approver). Отдельная настройка автоодобрения (по умолчанию выключена) переводит прошедшую проверку регистрацию сразу в active, без участия человека — сама криптографическая проверка при этом не ослабляется, настройка убирает только шаг человеческого ревью.

Автоодобрение доверяет принципалу, который называет сам агент: principal — это поле, которое присылает клиент, а сервер не связывает устройство с конкретным человеком — такой привязки в модели данных нет. Единственная проверка — allowlist принципалов. При включённом автоодобрении любое устройство с валидным enrollment_token получит рабочий SSH-доступ под любым принципалом из allowlist — без единого человеческого решения. Поэтому ручное одобрение — поведение по умолчанию, а не автоодобрение.

Одобрение (approve) при каждом вызове заново проверяет, не заблокировано ли владеющее устройство, и отказывает (403), если это так — даже если на момент регистрации оно заблокировано не было. Отклонённая (rejected) регистрация ключа может быть повторно одобрена тем же эндпоинтом — это единственный предусмотренный путь «реабилитации»; отозванную (revoked) регистрацию ключа одобрить нельзя никогда.

Отзыв устройства НЕ отзывает его SSH-ключи автоматически

Как и в остальной части системы (см. документацию по устройствам), здесь нет автоматического каскадного отзыва. Отзыв устройства блокирует новые регистрации и одобрения его ключей — но выдача уже одобренных ключей через Keyholder API этим не затрагивается: она проверяет только статус самого ключа и не смотрит, заблокировано ли устройство-владелец. Уже одобренный ключ заблокированного устройства продолжит открывать SSH-доступ, пока администратор не отзовёт его отдельно. Это сознательный компромисс архитектуры: каскадный отзыв — отдельное решение, которое пока не принято.

Чтобы отозвать SSH-доступ вместе с устройством, отзовите его ключи явно: получите список ключей устройства — GET /api/v1/devices/{serial}/ssh-keys — и отзовите каждый через POST /api/v1/admin/ssh-keys/{id}/revoke.

Admin UI

Зарегистрированные ключи видны в двух местах:

  • На карточке устройства — при разворачивании строки устройства в списке появляется вторая панель (рядом с панелью сертификатов) с ключами именно этого устройства: принципал, отпечаток, статус, дата.
  • На отдельной странице «SSH Keys» (только для ролей с доступом к Admin UI) — общий список по всему флоту: устройство (ссылка на карточку), принципал, отпечаток, статус, уровень аттестации, даты создания/одобрения/отзыва. Здесь же выполняются approve/reject/revoke.

Интерфейс показывает фактический уровень аттестации: для ключа без аппаратного доказательства обладания в списке так и указано, что ключ не аттестован.

Keyholder API

Keyholder API — не самостоятельный сервис, а группа маршрутов того же сервера, вынесенная из-под админской авторизации: доступ к ней определяется allowlist'ом по IP, а не ролями. Эндпоинт GET /api/v1/keyholder/keys?principal=<login> определяет по login список активных публичных ключей — предназначен для использования в AuthorizedKeysCommand целевых серверов (sshd на каждой машине, куда пускают по этим ключам, вызывает внешнюю команду, которая обращается сюда).

GET /api/v1/keyholder/keys?principal=ivan.petrov
→ {"keys": ["ssh-ed25519 AAAA...", "ssh-ed25519 BBBB..."]}

Возвращаются только ключи в статусе active. Для неизвестного принципала эндпоинт отвечает 200 и пустым массивом, никогда не 404 — иначе сам факт ответа позволил бы перебором узнавать, какие логины вообще существуют во флоте (перебор принципалов), даже через фильтр по IP.

Доступность

AuthorizedKeysCommand вызывается при каждой попытке SSH-логина, поэтому доступность Keyholder API напрямую влияет на доступность SSH во флоте. Рекомендуемая эксплуатационная конфигурация: обёртка вокруг AuthorizedKeysCommand, которая кеширует последний успешный ответ на диск и использует кеш, если сервер недоступен, плюс сохранённый локально break-glass-ключ администратора в обычном authorized_keys.

Доступ: IP-allowlist + rate limit + опциональный токен

В отличие от остального Admin API, /keyholder/keys не защищён обычным Bearer-токеном администратора — это service-to-service эндпоинт для инфраструктуры (bastion-хостов, целевых серверов), а не для людей. Порядок проверок:

  1. IP/CIDR allowlist (обязателен, «fail closed» по умолчанию). Список CIDR редактируется администратором (вкладка «SSH-ключи» в настройках). Пустой список = эндпоинт полностью закрыт — оператор обязан явно перечислить хотя бы одну подсеть, прежде чем API станет доступен вообще. Список читается заново на каждый запрос — правка в настройках применяется без перезапуска сервера. Некорректная запись CIDR пропускается с предупреждением в лог, а не роняет всю проверку.
  2. Rate limit, привязанный к паре (IP клиента, principal), а не просто к IP — потому что один легитимный bastion-хост обслуживает логины множества разных пользователей, и лимит по одному IP душил бы легитимный трафик.
  3. Опциональный bearer-токен сервера (по умолчанию выключен, включается в настройках). Когда включён, каждый запрос дополнительно должен нести Authorization: Bearer <token>, сверяемый по SHA-256-хешу с активными токенами. Хранится только хеш; сырое значение токена показывается администратору один раз — в момент создания — и больше никогда не восстанавливается.

IP-allowlist — не барьер между разными принципалами

Даже с allowlist один и тот же хост внутри разрешённой подсети может запросить ключи любого принципала, а не только «своего» — allowlist ограничивает, откуда можно спрашивать, но не что именно спрашивать. Это задокументированный остаточный риск: опциональный bearer-токен существует именно для тех, кому этого мало.

Администрирование токенов Keyholder

Управление серверными токенами — обычные админ-эндпоинты (роль cert-admin), доступные в той же вкладке настроек, где включается опциональный bearer-токен сервера:

Метод и путь Назначение
GET /api/v1/admin/keyholder-tokens Список токенов (label, даты — без значения/хеша)
POST /api/v1/admin/keyholder-tokens Создать токен по метке; сырое значение возвращается один раз в ответе
DELETE /api/v1/admin/keyholder-tokens/{id} Отозвать токен — сразу исключается из проверки