MAATRIX / Блог / SSH-ключи после увольнения: как убедиться, что их правда отозвали

SSH-ключи после увольнения: как убедиться, что их правда отозвали

MAATRIX

Сотрудник уволился, в тикете стоит статус «доступы отозваны», и на этом обычно все успокаиваются. Проблема в том, что «отозвали» — это чьи-то слова, а не проверенный факт: SSH-ключ может десять раз значиться удалённым в чек-листе и при этом продолжать спокойно открывать доступ на одном из серверов, до которого просто не дошли руки. Ниже — не про то, что отзывать при увольнении (это разобрано отдельно), а про то, как убедиться, что отзыв действительно произошёл, а не остался галочкой в задаче.

Разница между «отозвали» и «проверили»

Когда администратор отчитывается «ключи отозваны», за этим обычно стоит конкретное, но ограниченное действие: он открыл список серверов, до которых сам вспомнил, зашёл на каждый, удалил строку с известным ему ключом из ~/.ssh/authorized_keys основного пользователя и закрыл тикет. Это честная работа, но она опирается на память конкретного человека о том, где вообще есть серверы и куда этот ключ мог попасть за годы работы сотрудника. Память — ненадёжный источник истины для security-процесса, и это не претензия к администратору, а свойство любого ручного процесса без контрольного списка.

Разница между «я отозвал» и «я проверил, что он отозван» в том, что первое — отчёт о выполненном действии, а второе — независимое подтверждение результата. В идеале подтверждение получает не тот же человек, который проводил отзыв, а кто-то другой — или хотя бы тот же, но по отдельному чек-листу, не полагаясь на память «вроде всё сделал». Ниже — где именно ключ может остаться незамеченным и как получить это подтверждение.

Первый шаг — не проверка, а полный список серверов

Прежде чем проверять хоть один сервер, нужно ответить на вопрос, который на практике оказывается сложнее, чем кажется: сколько вообще серверов есть в инфраструктуре и с каким доступом. Если такого списка нет или он не обновлялся полгода, любая проверка ключей будет проверкой по неполному множеству — вы аккуратно убедитесь, что ключа нет там, где вы посмотрели, и ничего не скажете про сервер, который команда подняла для теста три месяца назад и забыла внести в документацию. Методология сборки такого реестра с нуля — платежи, панели провайдеров, домены, опрос команды — разобрана в статье «Инфраструктура выросла стихийно: собираем реестр серверов, доменов и доступов».

Если реестр уже есть — хорошо, но список «известных серверов» и список «серверов, куда сотрудник реально имел доступ» не всегда совпадают: человек мог за годы работы получить доступ к серверу подрядчика, тестовому стенду партнёра, машине, которую поднимали для миграции и должны были снести. О том, что отзывать в первую очередь и в каком порядке в первые два часа после увольнения, — отдельная статья «Уволили разработчика в пятницу: что отозвать в первые два часа». Здесь же — про следующий шаг: как убедиться, что список из неё реально закрыт, а не просто пройден по пунктам.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Почему это легко упустить: authorized_keys — файл на каждом сервере, а не общая база

Ключевая техническая причина, по которой отзыв SSH-ключей систематически даёт сбои: в классической схеме без централизованного управления ключами нет единой точки, где ключ можно отозвать один раз и он перестанет работать везде. Файл ~/.ssh/authorized_keys существует локально на каждом сервере, для каждого пользователя отдельно. Добавление ключа на сервер А никак не связано с сервером Б — это два независимых файла на двух независимых машинах.

Это отличается от того, как интуитивно работает отзыв доступа в большинстве других систем. Когда вы блокируете учётную запись сотрудника в Google Workspace или в корпоративном каталоге — это одно действие в одном месте, которое сразу действует везде, где эта запись использовалась. С разрозненными authorized_keys интуиция подсказывает то же самое, но на практике «отозвать доступ» — это N действий, где N равно числу серверов, на которых у сотрудника когда-либо был ключ. Если по факту отозвали M из N, а N никто точно не считал, разница между «мы всё отозвали» и «мы отозвали то, что вспомнили» остаётся невидимой до момента, пока кто-то не подключится этим ключом к серверу номер M+1.

Усугубляет это то, что ключ попадает на сервер не только через штатное «добавили нового администратора»: его добавляли вручную при разовой отладке инцидента ночью, копировали в authorized_keys сервиса деплоя, чтобы CI заходил на прод под тем же пользователем, что и человек, он оказывался на резервном сервере, клонированном из образа основного вместе со всей домашней директорией. Каждый такой случай — легитимная причина в моменте и забытая точка входа через год.

Ключ живёт не только в основном аккаунте пользователя

Стандартная проверка при увольнении почти всегда сводится к одному: зашли под личным аккаунтом сотрудника (или под общим deploy/admin) и убрали его ключ из authorized_keys этого аккаунта. Этого недостаточно, если тот же публичный ключ когда-либо добавляли в authorized_keys другого пользователя на той же или другой машине — а это случается чаще, чем принято думать.

Список мест, которые стоит явно проверить помимо основного личного аккаунта:

  • root. Классическая ситуация «нужно было один раз быстро что-то поправить от root, добавили ключ туда же, забыли снять» — /root/.ssh/authorized_keys.
  • Сервисные и системные учётные записи, под которыми обычно ходит автоматика, но иногда заходят и люди: deploy, www-data, nginx, postgres, git, ci, jenkins, gitlab-runner, backup. У любой может быть свой ~/.ssh/authorized_keys, о котором не вспоминают при офбординге, потому что «это же не пользовательский аккаунт».
  • Общие/групповые аккаунты, если в инфраструктуре такие остались — например, старый общий admin, которым исторически пользовалась вся команда.
  • Резервные и staging-копии продовых серверов — если их поднимали клонированием диска или образа, скопировался и authorized_keys со всеми ключами на момент клонирования, включая уже отозванные на проде.
  • Bastion/jump-хосты, если через них организован доступ в закрытый контур. Ключ, снятый с целевых серверов, но оставшийся на бастионе, всё ещё даёт первый шаг внутрь периметра.

Практический метод — не полагаться на память о том, «какие вообще есть пользователи», а получить список явно: cut -d: -f1 /etc/passwd покажет всех пользователей сервера, и для каждого с домашней директорией стоит проверить ~/.ssh/authorized_keys. Именно такой пропущенный аккаунт стоит за историей в статье «SSH-ключ уволенного сотрудника работал ещё восемь месяцев» — там ключ нашли по логам уже постфактум, а не в результате проверки при увольнении.

Если в организации есть централизованное управление ключами — не забудьте проверить и его

Часть организаций, особенно с числом серверов от пары десятков, уходит от схемы «разрозненные authorized_keys на каждой машине» к более управляемым моделям. Общая идея в разных вариантах одна: единая точка выдачи и отзыва доступа вместо ручного редактирования файлов на каждом сервере. Это может быть система выдачи коротко живущих SSH-сертификатов вместо статических ключей (сертификат сам истекает через несколько часов, и «отзыв» происходит автоматически при следующем запросе нового), интеграция SSH-доступа с корпоративным каталогом или SSO, где список допущенных ключей формируется централизованно и раздаётся на серверы агентом, либо push authorized_keys через систему управления конфигурацией — тогда файл на серверах генерируется автоматически из центрального источника при каждом прогоне.

Если у вас такая схема есть — это хорошая новость с точки зрения масштабирования, но она не отменяет проверку, а меняет её характер. Стоит убедиться в двух вещах:

  1. Запись о сотруднике действительно удалена или деактивирована в центральном источнике (каталоге, системе выдачи сертификатов, репозитории конфигурации) — а не помечена на удаление в интерфейсе, где изменения применяются по расписанию раз в сутки.
  2. Изменение реально долетело до каждого сервера. Push-модели иногда обрываются на части серверов из-за сетевых проблем или ошибки в плейбуке — и тогда центральный источник уже «чист», а конкретный сервер живёт со старой версией authorized_keys, потому что последний успешный прогон на нём был неделю назад.

Другими словами, централизация снимает проблему «забыли один сервер из списка», потому что список ведётся автоматически, но не снимает проблему «изменение не применилось» — просто переносит точку проверки с ручного грепа по файлам на проверку статуса последнего прогона синхронизации на каждой машине. Если в инфраструктуре до сих пор нет ни того, ни другого и authorized_keys правится вручную — стоит иметь в виду, что сама по себе ручная модель перестаёт нормально администрироваться на команде из 15-20 с лишним человек; где именно это ломается и что ставить вместо, разобрано в статье «Предел числа пользователей и ключей SSH».

Практическая процедура: ищем ключ по всем серверам и всем пользователям

Для проверки нужен не сам ключ целиком, а его отпечаток — так надёжнее сравнивать, чем построчно сверять base64-строки на глаз. Если публичный ключ сотрудника сохранён (например, в реестре доступов или в исходном тикете на выдачу доступа), отпечаток получаем так:

ssh-keygen -lf sotrudnik_id_ed25519.pub
# 256 SHA256:AbCdEf... comment (ED25519)

Дальше на каждом сервере из полного списка (не из «списка серверов, куда я точно давал доступ этому человеку», а из полного реестра инфраструктуры) выполняем поиск по всем пользователям, а не только по основному аккаунту:

# на одном сервере — найти все authorized_keys и показать их содержимое построчно с указанием файла
sudo find / -xdev -name authorized_keys -exec grep -H . {} \;

Флаг -xdev ограничивает поиск текущей файловой системой и не даёт find уйти в примонтированные сетевые диски или другие разделы — на многодисковых серверах его стоит убрать и явно указать нужные точки монтирования. Дальше строки из вывода сверяются с известным публичным ключом сотрудника — совпадение ищем по значимой части (base64-блоку), а не по комментарию в конце строки, потому что комментарий редактируется свободно и ни на что не влияет.

Если серверов много и заходить на каждый вручную нецелесообразно, тот же поиск делается пакетно через инструмент оркестрации, которым уже пользуется команда (Ansible, SSH for-loop по списку хостов из реестра и т.п.), например:

ansible all -m shell -a \
  "find / -xdev -name authorized_keys -exec grep -l 'AAAAC3NzaC1lZDI1NTE5...' {} \;" \
  --become

Здесь AAAAC3NzaC1lZDI1NTE5... — уникальный фрагмент публичного ключа сотрудника (без комментария), а --become нужен, чтобы прочитать authorized_keys пользователей, домашние директории которых недоступны без повышения прав, включая root и сервисные аккаунты. Результат такой команды — список файлов на всех серверах сразу, где ключ ещё присутствует. В норме после отзыва этот список должен быть пустым; если он не пустой — вот он, тот самый M+1-й сервер, о котором никто не вспомнил.

Отдельно стоит проверить облачные механизмы доступа: некоторые провайдеры хранят SSH-ключи не только в authorized_keys на машине, но и в метаданных инстанса или отдельном разделе панели, откуда ключ впрыскивается заново при каждой перезагрузке или пересоздании сервера. Если ключ отозвали только редактированием файла, а в метаданных облака он остался, — при следующем пересоздании или восстановлении из снапшота ключ вернётся сам.

Финальное подтверждение: попытка подключения должна закончиться отказом

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

Проверка делается напрямую тем самым ключом, который был у сотрудника (или его копией, если ключ передавали при выдаче доступа и сохранили в защищённом хранилище):

ssh -i sotrudnik_id_ed25519 \
    -o PreferredAuthentications=publickey \
    -o PasswordAuthentication=no \
    -o BatchMode=yes \
    user@server "echo test"

Ожидаемый результат — явный отказ на этапе аутентификации:

user@server: Permission denied (publickey).

Если вместо этого echo test выполнилась и вернула test — отзыв не сработал, и это нужно закрывать немедленно, а не разбираться, почему проверка «должна была» отработать иначе. Флаги PreferredAuthentications=publickey, PasswordAuthentication=no и BatchMode=yes не дают SSH-клиенту откатиться на другой метод аутентификации и тем самым создать ложное впечатление, что «подключение не удалось», хотя на деле просто не был опробован именно тот метод, который проверяется.

Эту проверку стоит держать в процессе офбординга как обязательный последний шаг — до того, как задача на отзыв доступов будет закрыта. Идея простая и она же главная мысль этой статьи: не полагаться на предположение, что процесс сработал правильно, а получить прямое доказательство. Отчёт «мы всё отозвали» и Permission denied (publickey) при реальной попытке подключения — два разных уровня уверенности, и второй надёжнее первого.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Сколько времени занимает полная проверка на инфраструктуре из 20-30 серверов?

Зависит от того, есть ли готовый реестр и инструмент для пакетного выполнения команд. С готовым списком хостов и Ansible-инвентарём поиск по authorized_keys занимает от нескольких минут до получаса; основное время уходит на составление полного списка серверов, если его не было заранее.

Что делать, если ключ нашёлся на сервере, который не входил в первоначальный список для отзыва?

Немедленно удалить строку из authorized_keys, провести финальное подтверждение отказом в подключении именно на этом сервере и отдельно разобраться, почему он не попал в исходный список — обычно это сигнал, что реестр серверов неполный.

Нужно ли проверять сервисные аккаунты, если сотрудник специально доступ туда не получал?

Да: вопрос не в том, знал ли сотрудник о существовании аккаунта, а в том, был ли туда физически добавлен его ключ — часто это делают кем-то другим ради удобства («чтобы CI и человек заходили одинаково»), без ведома самого сотрудника.

Что делать, если сервер недоступен по сети во время финальной проверки?

Это не равнозначно «ключ отозван». Пока сервер недоступен, статус его authorized_keys неизвестен, а не подтверждён как безопасный — пометьте его как непроверенный и вернитесь, как только он снова в сети, не закрывая задачу.

Есть ли смысл сразу менять ключи всей команде вместо точечного отзыва одного сотрудника?

Обычно нет, если процесс отзыва и проверки выше реально соблюдается — точечный отзыв дешевле и не создаёт лишних неудобств остальным. Массовая ротация оправдана отдельно, например при подозрении, что приватный ключ мог быть скопирован или доступ скомпрометирован шире одного увольнения.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →