У меня в проекте было четырнадцать копий одного IP‑адреса. По одной в каждом скрипте, который ходит на боевой сервер: деплой, перезапуск сервиса, правка DNS, диагностика почты. Классический копипаст, который живёт до первого переезда.
Переезд случился. Адрес поменялся. Четырнадцать скриптов стали указывать в никуда.
Дальше начинается то, ради чего я это пишу.
Скрипт не упал
Я ожидал четырнадцать таймаутов. Вместо этого скрипт подключился.
Прежний адрес провайдер уже отдал другому клиенту — так работают облачные и VPS‑провайдеры, освободившийся IP не лежит в резерве, он уходит в пул и выдаётся следующему. На том конце был живой SSH‑сервер. Мой скрипт установил соединение и отправил туда рутовый пароль из переменной окружения VPS_PASS.
Не человеку в чёрной шапке. Просто какому‑то клиенту того же провайдера, которому достался мой бывший адрес.
Виновата была одна строка:
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
Что именно делает эта строка
Когда SSH‑клиент подключается к серверу, тот представляется своим ключом хоста. Клиент сверяет ключ с тем, что записан в known_hosts. Совпал — продолжаем. Не совпал — останавливаемся и кричим.
Этот крик все видели:
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Он выглядит как назойливость, потому что в 99% случаев ключ сменился по скучной причине: сервер переустановили. Но проверка существует ради оставшегося процента — она отвечает на вопрос «я разговариваю с той же машиной, что и в прошлый раз, или с другой, которая притворяется ею».
AutoAddPolicy отвечает на этот вопрос «неважно» и молча принимает любой ключ.
Обычно её объясняют защитой от MITM — и поэтому она кажется теорией: «кто будет перехватывать мой трафик до моего же сервера». Но перехват тут не нужен. Достаточно, чтобы адрес перестал принадлежать вам. Провайдер сделает это сам, бесплатно и без злого умысла.

Отдельно неприятно, что в логах это выглядит как успех. Соединение установлено, команда отправлена, код возврата нулевой. Ничего не сломалось — просто пароль теперь есть у постороннего.
Как чинил
Три правки, и все три обязательные — по отдельности каждая закрывает только часть.
Первое. Строгая политика и явная загрузка known_hosts:
ssh = paramiko.SSHClient() ssh.load_system_host_keys() ssh.load_host_keys(str(Path.home() / ".ssh" / "known_hosts")) ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
RejectPolicy не «строже». Она возвращает поведение по умолчанию: незнакомый или сменившийся ключ — исключение до того, как в сокет уйдёт хоть один байт учётных данных. Ключ нового сервера добавляется в known_hosts один раз, руками, когда вы точно знаете, что это ваша машина.
Второе. Адрес живёт в одном месте. У меня это модуль vps.py, из которого его импортируют все скрипты. Протухнуть в одном файле из четырнадцати он больше не может, а переезд правится одной строкой.
Третье. Ключ вместо пароля:
key_path = os.environ.get("VPS_KEY", str(Path.home() / ".ssh" / "id_ed25519")) if Path(key_path).exists(): ssh.connect(host, username=user, key_filename=key_path) else: ssh.connect(host, username=user, password=os.environ["VPS_PASS"])
Приватный ключ не уходит на сервер — уходит подпись. Даже если вы всё‑таки подключились не туда, красть у вас нечего. Пароль оставлен запасным путём, но он именно запасной.
Тот же вопрос в другом месте
Через неделю после этой истории я делал на сайте инструмент: пользователь вводит адрес, сервер загружает страницу и показывает её вес, блокирующие скрипты и платформу.
Это ровно тот же класс проблемы, только с другой стороны. Там я не доверял чужому серверу, здесь сервер по моей команде идёт по чужому адресу. Оба раза вопрос один: «а точно ли на том конце то, что я думаю».
Что попробует ввести первый же любопытный:
http://127.0.0.1/ — то, что крутится на самом хосте http://169.254.169.254/ — метаданные облака, в них бывают токены file:///etc/passwd — а вдруг клиент проглотит схему
Поэтому в чекере: только http и https, резолв имени до запроса и проверка полученного IP по приватным диапазонам, петле и link‑local, и повторная проверка после каждого редиректа — иначе внешний хост отдаст 302 на 127.0.0.1 и обойдёт весь входной контроль.
На это у меня стоят смоук‑тесты, которые гоняются против боевого сервера, а не против моков:
ok blocked http://127.0.0.1/ ok blocked http://169.254.169.254/ ok blocked file:///etc/passwd
Вывод
Я не открыл ничего нового: не отключайте проверку ключа хоста — это написано в любом гайде. Мне было полезно понять, почему совет не работает.
Он звучит как защита от гипотетического злоумышленника, а выглядит как лишнее окно, которое мешает автоматизации. И его отключают не по глупости, а потому что риск кажется абстрактным.
Он не абстрактный. Достаточно, чтобы у вас поменялся сервер.
Проверьте свои деплой‑скрипты на AutoAddPolicy. Их обычно писали давно, второпях, и с тех пор никто в них не заглядывал.
Dzzzen
Нейрослоп прямо в заголовке(( Может проще на хабре промпт публиковать, а кого заинтересует, сам его отправит ИИ?