MAATRIX / Блог / Ключ подрядчика остался в authorized_keys: аудит доступов за час

Ключ подрядчика остался в authorized_keys: аудит доступов за час

MAATRIX

Фрилансера позвали на три дня — починить упавший деплой или разобраться с багом в проде. Выдали SSH-доступ, он сделал работу, получил оплату, все довольны. Проблема в том, что доступ никто не отозвал: строка в authorized_keys осталась на месте, и через полгода-год об этом человеке уже никто не помнит, а его ключ по-прежнему открывает дверь на боевой сервер. Ниже — не теория про «важность безопасности», а конкретная методика: как за час пройтись по всем серверам, найти чужие ключи, понять, у кого есть sudo, и закрыть находки без хаоса.

Почему так происходит почти в каждой команде

Дело не в разгильдяйстве, а в том, что выдача доступа — разовое действие с понятной ответственностью (кто-то один открыл SSH), а отзыв — действие без владельца. Задача подрядчика закрыта, счёт оплачен, проект переключился на следующую задачу — и никто формально не обязан вспомнить про доступ через месяц.

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

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

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

Час на аудит: реалистичный тайминг

Задача ниже рассчитана на инфраструктуру среднего размера — от пяти до пятидесяти серверов, без централизованного IdP и SSH-сертификатов. Если у вас уже есть bastion с Keycloak и короткоживущими сертификатами, эта проблема вас касается меньше — но за легаси-хостами, оставшимися на классических ключах, всё равно стоит присматривать.

ШагВремяРезультат
Собрать список серверов5-10 минФайл с адресами всех хостов
Слить authorized_keys со всех серверов15-20 минОдин файл со всеми ключами и их источниками
Сопоставить ключи с людьми15-20 минТаблица: ключ → человек → статус (действующий/непонятно/бывший)
Проверить sudo и группы10 минСписок, у кого из найденных есть повышенные права
Принять решение по находкам5-10 минЧто удаляем сразу, что уточняем

Итого — час с небольшим запасом. Уточнение находок может занять больше времени, если комментариев в ключах нет или они бессмысленные, но сам сбор данных укладывается в 40 минут даже на полусотне серверов.

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

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

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

Шаг 1: собрать authorized_keys со всех серверов

Первая проблема часто не в аудите, а в том, что нет актуального списка серверов вообще. Соберите его из того, что реально есть под рукой:

  • панель хостинг-провайдера / облака — экспорт списка VPS через API или веб-интерфейс;
  • Terraform state (terraform state list покажет все управляемые ресурсы, включая инстансы);
  • Ansible-инвентарь, если он используется;
  • список целей мониторинга (Zabbix, Prometheus targets, Uptime Kuma) — если сервер мониторится, он почти наверняка боевой;
  • DNS-записи A/AAAA на поддомены вида *.example.com.

Дальше — собрать сами ключи. Если серверов немного и Ansible нет, хватит простого цикла:

#!/usr/bin/env bash
# collect_keys.sh — собрать все authorized_keys с инвентаря
while read -r host; do
  echo "=== $host ==="
  ssh -o BatchMode=yes -o ConnectTimeout=5 "$host" \
    "sudo find /root /home -maxdepth 3 -name authorized_keys -exec sh -c 'echo \"--- {} ---\"; cat {}' \;" \
    2>/dev/null
done < servers.txt > all_keys_raw.txt

Если Ansible уже настроен — быстрее и надёжнее ad-hoc модулем, потому что он сам параллелит подключения:

ansible all -i inventory.ini -b -m shell \
  -a "for f in /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys; do [ -f \"\$f\" ] && echo \"--- \$f ---\" && cat \"\$f\"; done" \
  --forks 20

Для десятков хостов без Ansible можно распараллелить тот же цикл через xargs -P 10 — просто оберните тело ssh-команды в вызов через xargs, чтобы не ждать серверы по очереди, а не гонять их последовательно.

Отдельно проверьте нестандартные пути: если в sshd_config задан AuthorizedKeysFile с нестандартным значением (например, ключи хранятся в LDAP или в централизованном хранилище, а не в домашней директории), обычный find их не найдёт — сначала посмотрите на конфиг:

grep -i AuthorizedKeysFile /etc/ssh/sshd_config

Шаг 2: сопоставить ключи с людьми

Голый публичный ключ ничего не говорит о владельце. Смотреть нужно на два поля: fingerprint (уникальный идентификатор ключа) и комментарий (последнее поле в строке, обычно вида user@hostname, но зависит от того, что человек ввёл при генерации).

Получить fingerprint для каждой строки:

while read -r line; do
  [ -z "$line" ] && continue
  fp=$(echo "$line" | ssh-keygen -lf /dev/stdin 2>/dev/null)
  comment=$(echo "$line" | awk '{print $NF}')
  echo -e "$fp\t$comment"
done < all_keys_raw.txt | sort -u

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

  • переписка/тикеты, где выдавался доступ («дай Диме ключ на бэкенд-3») — обычно можно найти по поиску в почте или таск-трекере за последний год;
  • договоры с подрядчиками — там часто фигурирует имя и период работ;
  • сам комментарий в ключе — если человек генерировал ключ стандартной командой ssh-keygen, комментарий обычно имя@имя-ноутбука, что уже сильно сужает круг.

Ключи без осмысленного комментария (ssh-keygen -N "" без указания -C) — отдельная категория риска: их сложнее атрибутировать, и по опыту именно такие чаще всего оказываются «ничьими» — либо очень старыми, либо сервисными, скопированными вручную мимо секрет-менеджера. Тип ключа тоже подсказка: свежие ssh-ed25519 обычно генерировались недавно, ssh-rsa без комментария — почти всегда наследие пяти-семилетней давности.

Соберите результат в простую таблицу:

FingerprintКомментарийСервер(ы)СтатусДействие
SHA256:k9F3...dmitry@laptopweb-1, web-2, db-1Действующий сотрудникОставить
SHA256:a71c...freelancer-may2025web-3Подрядчик, задача закрыта в мае 2025Удалить
SHA256:7e2b...(пусто)db-2Не опознанУдалить, если за 48 часов никто не откликнется
SHA256:9d0e...deploy-bot@ciвсеСервисный, деплой-пайплайнОставить, проверить, что ротируется отдельно

Ключи без атрибуции — не повод паниковать, но и не повод оставлять «на всякий случай». Практика, которая работает лучше долгих расследований: объявить в общем канале команды («нашли неопознанный ключ SHA256:7e2b... на db-2, отзовитесь, если ваш, иначе удаляем в пятницу») и удалить по истечении срока, если никто не откликнулся. Если это был кому-то нужный доступ — попросят восстановить, это займёт пять минут; если нет — вы только что закрыли дыру, о которой никто не помнил.

Шаг 3: проверить sudo, а не только факт ключа

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

Проверить, кто в sudo-группе:

getent group sudo
getent group wheel   # на RHEL/CentOS/AlmaLinux группа обычно называется иначе

Посмотреть содержимое sudoers — как основной файл, так и всё, что подключено через /etc/sudoers.d/:

sudo cat /etc/sudoers
sudo ls -la /etc/sudoers.d/
sudo cat /etc/sudoers.d/*

Отдельно поищите записи NOPASSWD — они позволяют выполнить sudo без пароля, а у временного подрядчика пароля в системе часто и нет вовсе (только ключ), так что это мгновенный root:

sudo grep -r NOPASSWD /etc/sudoers /etc/sudoers.d/ 2>/dev/null

Проверьте также группы конкретного пользователя, если аккаунт найденного ключа не только про sudo, но и, например, про доступ в docker (что тоже эквивалентно root — контейнер с смонтированным / в привилегированном режиме даёт полный доступ к хосту):

id dmitry

Если находите ключ подрядчика с sudo-правами без NOPASSWD — это чуть менее срочно (нужен ещё и пароль), но всё равно стоит закрыть в тот же час, а не откладывать на «после аудита». Приоритет: сначала NOPASSWD-sudo, потом обычный sudo, потом группа docker/adm, потом обычный пользовательский доступ без повышенных прав.

Закрыть находки: удалить и проверить, что это не восстановится

Само удаление строки из authorized_keys простое:

# на конкретном сервере — удалить строку с известным fingerprint
sudo sed -i '/freelancer-may2025/d' /home/deploy/.ssh/authorized_keys

Но здесь есть та же грабля, что и с офбордингом сотрудников: если сервер под конфигурационным менеджером (Ansible, Puppet, Chef, Salt) и список ключей описан в репозитории, ручное удаление на сервере откатится обратно при следующем прогоне плейбука. Проверьте, есть ли автоматика, до того как удалять руками:

# если используется ansible-pull, посмотрите таймер
systemctl list-timers | grep -i ansible

Если находите совпадение — правьте источник правды (файл в репозитории с описанием пользователей), а не только сервер, и только потом прогоняйте плейбук повторно, чтобы убедиться, что изменение применилось на всех хостах, а не откатилось.

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

Как не повторять эту ситуацию: регулярный access review

Разовый аудит закрывает текущую проблему, но без изменения процесса она вернётся — просто с другим именем в комментарии ключа через год. Три вещи, которые реально снижают вероятность повтора:

Фиксированное расписание пересмотра доступов. Раз в квартал — календарное событие, не «когда вспомним». На пересмотр закладывайте тот же час: собрать ключи, сверить с текущим составом команды и списком подрядчиков, чьи задачи завершены. Это дешевле, чем кажется, если процесс сбора уже отработан на первом аудите — второй и третий раз займут меньше времени, потому что скрипты уже написаны.

Доступ подрядчика — с датой окончания с самого начала. Выдавая SSH-доступ на конкретную задачу, сразу фиксируйте (в тикете, в календаре, где угодно) дату, к которой доступ нужно отозвать — «до 15 сентября» вместо «пока не понадобится». Даже простое напоминание в календаре с чётким получателем закрывает большую часть случаев «забыли отозвать», потому что переносит ответственность с памяти на систему. Юридическую сторону выдачи доступа подрядчику — NDA, что именно фиксировать в договоре до открытия SSH — разбирали отдельно в статье оформление NDA для доступа подрядчика к серверу.

Доступ подрядчику не должен требовать root по умолчанию. Большинство задач, которые делает временный исполнитель — деплой конкретного сервиса, просмотр логов, рестарт процесса — не требуют полного root. Если вместо личного sudo-доступа выдавать доступ через отдельного пользователя с ограниченными правами (конкретные sudo-команды в sudoers.d, а не весь ALL=(ALL) ALL), забытый доступ подрядчика остаётся неприятностью, а не катастрофой. Как выстроить такую модель для команды в целом — в статье как раздать доступ команде без выдачи root.

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

Отдельно заведите правило для будущих подрядчиков: временный доступ выдаётся с узнаваемым комментарием в ключе — например contractor-<имя>-<дата-окончания> вместо стандартного user@laptop. Это упрощает следующий аудит: такой ключ виден с первого взгляда, и не нужно гадать, кому он принадлежит.

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

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

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

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

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

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

С чего начать, если инвентаря серверов вообще нет?

Соберите список из того, что доступно прямо сейчас: панель облачного провайдера (список инстансов через API или веб-консоль), DNS-записи на поддомены, список целей мониторинга. Даже неполный список лучше, чем ничего — начните аудит с того, что нашли, и дополняйте по мере обнаружения «пропущенных» хостов через ссылки в конфигах (nginx upstream, переменные окружения с адресами других серверов).

Как быть с сервисными ключами (деплой-боты, CI/CD), которые тоже лежат в authorized_keys?

Проверьте, что они действительно используются — по логам сессий видно, batch-подключение это или интерактивное, и совпадает ли время входов с расписанием деплоев. Если сервисный ключ используется — оставьте, но убедитесь, что он хранится в секрет-менеджере (Vault, зашифрованный CI-secret), а не просто лежит приватной частью на чьём-то ноутбуке.

Сколько времени в среднем занимает такой аудит на полусотне серверов?

Сбор данных укладывается в 40-50 минут при параллельном подключении. Основная переменная — не количество серверов, а качество комментариев в ключах: осмысленные сопоставляются за минуты, пустые требуют переписки и уточнений.

Нужно ли сразу переходить на SSH-сертификаты, чтобы проблема не повторялась?

Не обязательно как первый шаг. Для команды среднего размера часто достаточно регулярного пересмотра доступов и дисциплины «доступ с датой окончания». Сертификаты с коротким TTL через bastion оправданы, когда серверов много и состав команды меняется часто.

Что делать, если найденный ключ явно активно используется прямо сейчас, а не просто лежит без дела?

Прежде чем удалять, проверьте историю входов по этому fingerprint (journalctl -u sshd | grep -F "<fingerprint>" или содержимое /var/log/auth.log*) и историю команд, если сохранилась. Если использование не согласовано ни с кем в команде — это уже не вопрос гигиены доступов, а потенциальный инцидент: зафиксируйте таймлайн, ротируйте секреты, к которым мог быть доступ, и только потом разбирайтесь, кто и почему продолжал пользоваться доступом.

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

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

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