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 устройства (тот же общий секрет, что и у
других видов заявок) и ограничен по частоте запросов. Перед созданием
строки эндпоинт последовательно проверяет:
- Allowlist принципала —
principalдолжен точно совпадать с одним из значений в администрируемом списке разрешённых принципалов для purposessh. Это тот же список, которым пользуется сертификатный/enroll/ssh: второй администрируемый список ради одного и того же смысла («кому вообще разрешён SSH-доступ») не потребовался. - Существование устройства + enrollment_token — устройство должно уже быть заведено (сама регистрация ключа новое устройство не создаёт), токен сверяется constant-time-сравнением.
- Доказательство обладания ключом (если платформа это поддерживает) — для 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-хостов, целевых серверов), а не для людей. Порядок
проверок:
- IP/CIDR allowlist (обязателен, «fail closed» по умолчанию). Список CIDR редактируется администратором (вкладка «SSH-ключи» в настройках). Пустой список = эндпоинт полностью закрыт — оператор обязан явно перечислить хотя бы одну подсеть, прежде чем API станет доступен вообще. Список читается заново на каждый запрос — правка в настройках применяется без перезапуска сервера. Некорректная запись CIDR пропускается с предупреждением в лог, а не роняет всю проверку.
- Rate limit, привязанный к паре (IP клиента, principal), а не просто к IP — потому что один легитимный bastion-хост обслуживает логины множества разных пользователей, и лимит по одному IP душил бы легитимный трафик.
- Опциональный 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} |
Отозвать токен — сразу исключается из проверки |