Чужие ключи в authorized_keys: чистим список, не потеряв доступ
authorized_keys на унаследованном сервере обычно выглядит как археологический разрез: десяток строк без единой подписи о том, кто их туда добавил и зачем. Стереть всё разом и попросить всех перевыпустить ключи — самый простой путь, но не всегда доступный: на сервере крутится боевой сервис, часть строк наверняка принадлежит деплой-ботам, которых легко забыть переподключить, а среди безымянных строк вполне может обнаружиться и тот единственный ключ, которым вы сами сейчас зашли. Ниже — как разобрать файл построчно, понять, чей каждый ключ, и вычистить лишнее в порядке, который не оставит вас без доступа на середине процесса.
Если у вас вообще нет надёжного входа — только один чужой ключ, а root-пароль неизвестен — это отдельная, более острая ситуация: сначала нужно закрепиться, а не разбирать файл построчно, и она разобрана в статье «Пароля root нет, есть только чужой ключ: как вернуть себе сервер». Здесь предполагается, что рабочий доступ у вас уже есть — вопрос в том, что делать с десятком чужих строк в файле.
Содержание
- Смотрим на файл как есть: формат строки и что из него уже видно
- Комментарий в конце строки: полезная подсказка, не доказательство
- Дата добавления: git-история, если она есть, и почему дата файла не поможет
- Логи подключений: используется ключ прямо сейчас или нет
- Сводим находки в таблицу, прежде чем что-то удалять
- Порядок чистки: сначала свой ключ, потом чужие — и не сразу удалением
Смотрим на файл как есть: формат строки и что из него уже видно
Прежде чем что-то удалять, разложите каждую строку на составляющие. Формат authorized_keys простой: необязательные опции, тип ключа, сам ключ в base64 и необязательный комментарий в конце — четыре поля через пробел, последнее из которых человек мог вписать что угодно или не вписать вовсе.
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... deploy@ci-runner
command="/usr/local/bin/backup-only.sh",no-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB... old-backup-script
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB...
Первым делом — просто посчитайте, сколько строк вообще есть, и посмотрите на них глазами:
cat -n ~/.ssh/authorized_keys
grep -vc '^\s*$\|^\s*#' ~/.ssh/authorized_keys # число реально активных строк, без пустых и закомментированных
Файл на пользователе, под которым вы зашли, — не единственное место. Проверьте root и всех остальных пользователей с шеллом, а заодно нестандартный путь, если он задан отдельно в конфиге sshd:
grep -i AuthorizedKeysFile /etc/ssh/sshd_config
for u in $(awk -F: '$7 !~ /nologin|false/ {print $1}' /etc/passwd); do
f=$(getent passwd "$u" | cut -d: -f6)/.ssh/authorized_keys
[ -f "$f" ] && echo "=== $u ($f) ===" && cat -n "$f"
done
sudo cat -n /root/.ssh/authorized_keys 2>/dev/null
Дальше получите отпечаток каждого ключа — дальше он понадобится для сверки с логами, а по base64-блоку строки на глаз не сравнить:
while IFS= read -r line; do
case "$line" in ''|'#'*) continue ;; esac
echo "$line" | ssh-keygen -lf /dev/stdin 2>/dev/null
done < ~/.ssh/authorized_keys
Команда выведет отпечаток вида 256 SHA256:AbCdEf... comment (ED25519) для каждой валидной строки. Если для какой-то строки ssh-keygen ничего не вернул — либо она обрезана (частая находка при копипасте из чата, где длинная строка перенеслась на два ряда и превратилась в два невалидных ключа), либо это действительно мусор, который можно смело чистить в первую очередь: сломанный ключ никого не пускает, риска потерять доступ через его удаление нет.
Комментарий в конце строки: полезная подсказка, не доказательство
Комментарий — самое соблазнительное поле для атрибуции, потому что читается сразу, но полагаться на него как на факт нельзя. ssh-keygen без флага -C подставляет логин@хост-где-сгенерирован, что уже сужает круг поиска. Но комментарий — обычный текст, который никак не проверяется системой: его можно вписать любой, отредактировать в любой момент без последствий для работы ключа, и он свободно кочует вместе с ключом при копировании на другой сервер — тот же deploy@ci-runner может стоять на пяти машинах, из которых деплой реально ходит только через одну, а на остальных строка просто скопирована «для единообразия» и давно не используется.
Пустой комментарий или что-то формальное вроде imported-key — отдельный сигнал, но тоже слабый: часто это ключи, сгенерированные без -C намеренно, либо добавленные через веб-панель хостинга, где поле комментария — это подпись в интерфейсе провайдера, которая в файл на сервере не попадает вовсе.
Тип ключа даёт слабый, но иногда полезный намёк на возраст: старый ssh-rsa без комментария почти всегда наследие прошлых лет, потому что современные клиенты и гайды по умолчанию давно предлагают ed25519. Это не доказательство даты, а ориентир — свежий ed25519 с осмысленным комментарием скорее добавлен недавно осознанным человеком, чем безымянный ssh-rsa, который мог пролежать там сколько угодно.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверДата добавления: git-история, если она есть, и почему дата файла не поможет
Соблазн посмотреть stat ~/.ssh/authorized_keys и решить, что дата изменения файла — это дата добавления последнего ключа, обманчив: mtime файла обновляется при любой правке любой строки, а не построчно. Если файл редактировали десять раз за пять лет, stat покажет только последнюю правку и ничего не скажет про остальные девять.
Единственный источник, который реально даёт даты и авторов по каждой строке, — версионный контроль, если файл управляется не вручную на сервере, а из репозитория конфигурации (роль Ansible, манифест Puppet/Chef, дотфайлы, куда authorized_keys коммитится как обычный файл). Проверьте, есть ли такой источник, прежде чем считать, что его нет:
# ищем, не приезжает ли файл из системы управления конфигурацией
grep -rl "authorized_keys" /etc/ansible /opt/*/roles /etc/puppet /etc/salt 2>/dev/null
systemctl list-timers | grep -i "ansible\|puppet\|salt\|chef"
Если источник нашёлся и это git-репозиторий, git log по конкретному файлу и git blame дадут именно то, что нужно: кто и когда добавил каждую строку.
cd /path/to/config-repo
git log -p -- roles/common/files/authorized_keys
git blame roles/common/files/authorized_keys
Честно: на большинстве унаследованных одиночных серверов такого источника нет вообще — файл правили руками через vim или echo >> годами, и версионной истории попросту не существует. Обходной путь на этот случай — если на сервере делались резервные копии (Borg, restic, обычные тарболлы по расписанию), можно достать authorized_keys из старых архивов и через diff найти, в каком по счёту бэкапе конкретная строка появляется впервые. Это работает только если бэкапы существуют и уходят достаточно далеко назад — считайте это бонусом, если повезло, а не обязательным шагом. Когда ни git, ни бэкапов нет, точную дату добавления восстановить обычно нельзя, и это нормально: следующий раздел про логи подключений даёт более практичный ответ на вопрос «используется ли ключ сейчас», который важнее даты его появления.
Логи подключений: используется ключ прямо сейчас или нет
Это самый надёжный источник из всех — не про то, когда ключ добавили, а про то, заходят ли им до сих пор. Современный sshd пишет в лог отпечаток ключа при каждой успешной аутентификации, и этот отпечаток можно напрямую сверить с тем, что вы получили на первом шаге через ssh-keygen -lf.
# systemd-дистрибутивы — имя юнита зависит от системы, sshd или ssh
journalctl -u sshd --since "-180 days" | grep "Accepted publickey"
journalctl -u ssh --since "-180 days" | grep "Accepted publickey"
# классический syslog без journald
grep "Accepted publickey" /var/log/auth.log* 2>/dev/null
grep "Accepted publickey" /var/log/secure* 2>/dev/null # RHEL-семейство
Строка в логе выглядит примерно так — обратите внимание, отпечаток в самом конце совпадает по формату с выводом ssh-keygen -lf:
Accepted publickey for deploy from 203.0.113.10 port 51244 ssh2: ED25519 SHA256:AbCdEf1234...
Дальше — прямая сверка конкретного отпечатка из файла с логами:
FP="SHA256:AbCdEf1234..."
journalctl -u sshd --since "-180 days" | grep -F "$FP"
Если совпадение есть — ключ живой, трогать его нельзя без выяснения, кто за ним стоит. Если совпадений нет за весь доступный период логов — это сильный аргумент в пользу того, что ключ мёртв, но не стопроцентная гарантия: у логов есть горизонт хранения (у journald — настройки vacuum, у файлового auth.log — logrotate), и то, что ключ не всплыл за последние полгода, не исключает, что им пользуются раз в год для сезонной задачи. Проверьте глубину ротации, прежде чем делать вывод: ls -la /var/log/auth.log* покажет дату самого старого файла.
Как дополнительный, более грубый сигнал на уровне аккаунта (а не конкретного ключа — если под одним пользователем заходят двумя разными ключами, не различит их) пригодятся last -a и lastlog.
Сводим находки в таблицу, прежде чем что-то удалять
Три источника — комментарий, версионная история (если есть) и логи — редко дают однозначный вердикт по отдельности, но вместе почти всегда достаточно уверенности для решения. Соберите результат по каждому ключу в одну таблицу, прежде чем переходить к чистке:
| Fingerprint | Комментарий | Аккаунт | В логах за 180 дней | sudo | Вердикт |
|---|---|---|---|---|---|
| SHA256:k9F3... | dmitry@laptop | deploy | Да, регулярно | Нет | Живой, оставить |
| SHA256:a71c... | freelancer-2023 | www-data | Нет ни разу | Нет | Мёртвый, низкий риск, удалить первым |
| SHA256:9d0e... | deploy-bot@ci | root | Да, ежедневно | root напрямую | Живой сервисный, оставить, но взять на карандаш ротацию |
| SHA256:7e2b... | (пусто) | root | Нет ни разу | root напрямую | Мёртвый, но с root — приоритетнее в очереди |
Отдельно проверьте sudo-права по каждому найденному аккаунту — ключ без пометки о повышенных правах и ключ с root-доступом требуют разного уровня спешки при удалении:
sudo -l -U username 2>/dev/null
getent group sudo wheel
sudo grep -r NOPASSWD /etc/sudoers /etc/sudoers.d/ 2>/dev/null
Порядок чистки: сначала свой ключ, потом чужие — и не сразу удалением
Это единственная часть процедуры, где ошибка в последовательности действительно дорого стоит: не в атрибуции (её можно доуточнить), а в том, что можно случайно выпилить единственный работающий вход раньше, чем закрепился новый. Порядок ниже написан так, чтобы эта ошибка была структурно исключена.
Шаг 1. Заведите свой собственный новый ключ, не трогая старые. Даже если вы уже как-то вошли на сервер — сгенерируйте отдельную пару специально для этой процедуры и добавьте её как новую строку, а не поверх существующих:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_cleanup -C "cleanup-$(date +%Y%m%d)"
cat ~/.ssh/id_ed25519_cleanup.pub >> ~/.ssh/authorized_keys
Шаг 2. Проверьте вход новым ключом в отдельной сессии, не закрывая текущую. Это единственный момент во всей процедуре, где нельзя срезать угол:
ssh -i ~/.ssh/id_ed25519_cleanup user@server "echo OK, новый ключ работает"
Только когда эта команда реально отработала — двигайтесь дальше. Если что-то не так, у вас всё ещё есть исходная рабочая сессия, чтобы разобраться, не оставшись снаружи.
Шаг 3. Снимите резервную копию файла до первой же правки.
cp -a ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak-$(date +%Y%m%d)
Шаг 4. Сначала не удаляйте, а «глушите» подозрительные строки комментированием. sshd игнорирует строки, начинающиеся с #, — это значит, что ключ можно отключить обратимо за секунду, вместо того чтобы сразу стирать строку и надеяться, что бэкап под рукой, если окажется, что кому-то ключ всё же нужен:
sed -i '/freelancer-2023/s/^/#DISABLED-2026-09-08 /' ~/.ssh/authorized_keys
Шаг 5. Дайте выдержку и понаблюдайте за логами, прежде чем удалять окончательно. Несколько дней для явно мёртвых ключей, дольше для тех, что могли использоваться редко (сезонные скрипты, ежеквартальные задачи):
journalctl -u sshd --since "-7 days" | grep "Failed publickey\|Connection closed by authenticating user"
Если за это время никто не пожаловался на отказ в доступе и в логах не всплыли неудачные попытки характерного клиента — можно удалять закомментированные строки насовсем, оставив датированный бэкап файла ещё на какое-то время на всякий случай.
Очерёдность, в которой стоит проходить находки. Технический риск удаления одинаков для любой строки, если вы уже прошли шаги 1-3, — очерёдность здесь про цену ошибки в атрибуции:
- Явно битые или обрезанные строки (не проходят
ssh-keygen -lf) — удаляются без риска, они и так никого не пускают. - Ключи с чёткой атрибуцией и нулевой активностью в логах — низкий риск, можно удалять первыми среди «живых» строк.
- Мёртвые ключи с root или sudo-доступом — риск удаления тот же, что у пункта 2, но цена того, что их оставили, выше, поэтому по приоритету они идут раньше, даже при менее уверенной атрибуции.
- Безымянные ключи без комментария и без совпадений в логах — самая неопределённая категория; для них выдержка подольше и, если есть с кем сверяться, короткое объявление в команде перед окончательным удалением.
- Всё, что засветилось в логах хотя бы раз, — не трогать до выяснения, кто или что за этим стоит: проверьте cron, systemd-таймеры и запущенные процессы на предмет автоматики, завязанной на этот аккаунт.
Если параллельно с чисткой authorized_keys вы разбираете и остальные унаследованные пароли — от базы данных, от панели хостинга, от SMTP — общий порядок замены секретов на живом сервере без простоя, включая тот же принцип «сначала новое, потом старое», разобран в статье «Меняем пароли и ключи на живом сервере: порядок, при котором ничего не отвалится» — там же более широкий взгляд на синхронную замену для секретов, у которых несколько потребителей одновременно.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
ssh-keygen -lf не даёт отпечаток для какой-то строки — это ошибка или битый ключ?
Чаще всего строка обрезана или перенесена на два ряда при копипасте из чата или тикета. Такие строки никого не пускают, их можно удалять сразу, без выдержки и атрибуции.
Нужно ли сразу удалять ключи без комментария?
Нет, отсутствие комментария — это просто более слабая атрибуция, а не признак того, что ключ чужой или вредоносный. Разбирайтесь через логи так же, как с подписанными строками: нет совпадений за весь доступный период — значит мёртвый, вне зависимости от комментария.
После удаления строки ключ через какое-то время появился снова — как так?
Файл генерируется откуда-то заново: системой управления конфигурацией при следующем прогоне, AuthorizedKeysCommand из внешнего источника, или метаданными облачного провайдера, которые переинжектируются при пересоздании сервера. Править нужно источник, а не файл на сервере, иначе изменение откатится.
Сколько держать закомментированные строки перед окончательным удалением?
Единого числа нет: неделя-две достаточно для явно мёртвых находок, месяц разумен для ключей, которые теоретически использовались нерегулярно — например, раз в квартал.
Ключ явно используется, но по логам и комментарию непонятно, чей он?
Не отключайте до выяснения. Посмотрите процессы, cron и systemd-юниты под этим аккаунтом и с каких IP реально приходят подключения. Если объяснения не находится, это уже не гигиена доступов, а потенциальный инцидент: зафиксируйте таймлайн и подумайте о ротации секретов, до которых у ключа мог быть доступ.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →