Типичная заявка: «на прод с ноутбука не пускает, только через bastion, я скопировал свой id_ed25519 на прыжок, теперь захожу в два ssh». Вариант «культурнее»: «включили ForwardAgent, чтобы не вводить пароль от ключа дважды, и Ansible сам прыгает». Через месяц на джампе кто-то с root снимает сокет агента и ходит на прод вашим ключом. Ключ при этом «не крали» — его и не надо красть, агент сам подписывает.
Я не таскаю закрытый ключ на промежуточную машину. Прыжок для меня — дырка в TCP, не второй дом для ssh-agent. Ниже — чем ProxyJump отличается от проброса агента, когда агент всё-таки оставляю, и чем проверяю, что ключ с ноутбука никуда не уехал.
Хосты в примерах вымышленные: jump.example.net (документационный 203.0.113.40), внутренний prod-web-01.internal, пользователь ops. Свои имена и ключи подставляйте, чужие из чатов не копируйте.
Две привычки, которые путают с «удобным прыжком»
Первая: положить тот же закрытый ключ на bastion. «Ну я же сам туда захожу, чего его беречь». Bastion — это машина, на которую смотрит интернет. На ней чаще стоят общие учётки, старый sudo, чужой tmux. Если ключ от прода лежит файлом в ~/.ssh на такой коробке, компрометация прыжка равна компрометации всего, куда этот ключ пускают. Пароль на ключ тут слабое утешение: его снимут вместе с файлом, из памяти агента или из вашей же сессии.
Вторая: ForwardAgent yes глобально в ~/.ssh/config или в системном ssh_config. Так делают, чтобы со прыжка открыть следующий ssh «как с ноутбука». Удобно. И ровно поэтому опасно. На время сессии на джампе появляется unix-сокет агента. Любой, кто может читать этот сокет — root, ваша скомпрометированная учётка, чужой процесс под тем же uid — может пользоваться ключами, пока вы онлайн. Ключ с диска не копировали. Подпись от вашего агента уже достаточно.
Есть ещё гибрид: ключ на ноутбуке, а на прыжок его один раз залили «на всякий случай» и забыли. Потом ротировали ключ дома, на джампе старый остался. Я такие находки встречаю чаще, чем хотелось бы: ls -l ~/.ssh на bastion и два id_*, один из которых уже «не наш».
Что именно делает агент, когда его пробросили
ssh-agent на ноутбуке — нормальная вещь. Он держит расшифрованные ключи в памяти, чтобы не тыкать passphrase на каждый git fetch. Плохо не агент. Плохо отдавать его сокет чужой машине.
При ForwardAgent yes клиент поднимает на удалённой стороне канал и выставляет SSH_AUTH_SOCK. Дальше ssh prod-web-01, запущенный уже на прыжке, ходит в этот сокет. Подпись делает ваш ноутбук. Выглядит безопасно: «ключ же дома». Но решение «подписать вот этот challenge» принимает процесс на bastion. Если bastion врет, вы подпишете не то соединение, о котором думаете.
Практическая картинка, которую я показываю заказчику:
- Вы в сессии на
jump.example.net. echo $SSH_AUTH_SOCKдаёт путь вроде/tmp/ssh-XXXX/agent.1234.- С правами на этот сокет можно сделать
ssh-add -Lи увидеть ваши ключи. - Дальше — обычный
ssh ops@prod-web-01.internal. Прод видит ваш ключ. В authorized_keys это тот же самый ключ, которым вы заходите «сами».
Root на джампе для этого не всегда нужен. Достаточно вашей же утёкшей сессии, crон-скрипта под тем же uid или «временного» бинарника в $PATH. Поэтому фраза «ну у нас же на bastion только свои» меня не греет: свои тоже ошибаются, а агент не спрашивает, кто именно попросил подпись.
Отдельно: ssh-add -c на ноутбуке чуть лучше, чем голый агент. Перед каждой подписью всплывает confirm. Это не броня, но если сокет увели тихо, вы хотя бы увидите лишний запрос в момент, когда сами ничего не открывали.
ProxyJump: прыжок без вашего ключа на той стороне
ProxyJump (ключ -J) делает другое. Клиент на ноутбуке открывает ssh на bastion и просит его прокинуть TCP до цели. Аутентификация на prod-web-01 идёт с ноутбука, напрямую в этом тоннеле. На прыжке нет вашего закрытого ключа, нет сокета агента, нет второго ssh от вашего имени.
Грубо: bastion видит, что с вашего IP пришёл ssh и что вы попросили соединиться с prod-web-01:22. Bastion не видит, каким ключом вы представились проду, и не может этот ключ переиспользовать через минуту после того, как вы отключились.
Минимальный ~/.ssh/config, с которого я начинаю:
Host jump
HostName jump.example.net
User ops
IdentityFile ~/.ssh/id_ed25519_jump
IdentitiesOnly yes
ForwardAgent no
Host prod-web-01
HostName prod-web-01.internal
User ops
ProxyJump jump
IdentityFile ~/.ssh/id_ed25519_prod
IdentitiesOnly yes
ForwardAgent no
Два ключа — не паранойя. Ключ на интернет-джамп я считаю расходным: его проще отозвать. Ключ на прод на прыжок не кладу и в агент для сессии на jump не пихаю, если можно не пихать. IdentitiesOnly yes обязателен: иначе клиент переберёт все ключи из агента, и вы сами не вспомните, чем открыли хост. Плюс лишние попытки ключей иногда заканчиваются Too many authentication failures.
Одноразово без конфига:
ssh -J ops@jump.example.net ops@prod-web-01.internal
Два и больше прыжка — список через запятую:
ssh -J jump.example.net,dmz-gw.example.net ops@prod-web-01.internal
В конфиге то же самое: ProxyJump jump,dmz-gw. Старый ProxyCommand ssh -W %h:%p jump я оставляю только там, где клиент древний и ProxyJump ещё нет. Смысл тот же: проброс TCP, не проброс агента.
Как я проверяю, что агент не уехал
После захода на цель смотрю три вещи, не «ну вроде работает».
На ноутбуке, до входа:
ssh -G prod-web-01 | egrep -i 'proxyjump|proxycommand|forwardagent|identityfile|identitiesonly'
Хочу увидеть свой ProxyJump, forwardagent no, нужные IdentityFile и identitiesonly yes. Если forwardagent yes приехало из /etc/ssh/ssh_config или из Host * в конце файла — чиню там, а не добавляю ещё один Host выше, который потом перебьют.
Вход с отладкой:
ssh -v prod-web-01 2>&1 | egrep -i 'proxy|jump|forwarding|agent|Offering|Authentication succeeded'
Ищу, что клиент идёт через jump и что нет строк про agent forwarding. «Offering public key» должно относиться к ключу прода, не к ключу джампа. Если клиент предлагает пять ключей подряд — IdentitiesOnly не сработал.
На самой цели и на прыжке, уже внутри сессии:
echo "SSH_AUTH_SOCK=${SSH_AUTH_SOCK:-}"
ssh-add -l
На prod-web-01 при чистом ProxyJump сокет агента обычно пустой, ssh-add -l отвечает, что агента нет. На прыжке при обычном заходе на сам jump — то же самое, если я не просил проброс. Если сокет вдруг есть, а я его не включал, ищу кто: Host *, Include-info в конфиге, обёртка вроде assh/sshpass, корпоративный «улучшатель» ssh.
Ещё одна проверка, которую люди пропускают: ключи на диске прыжка.
ls -l ~/.ssh find ~/.ssh -type f \( -name 'id_*' -o -name '*.pem' \) ! -name '*.pub'
Закрытых ключей там быть не должно. Если есть — снимаю с машины, смотрю authorized_keys на проде, не торчит ли тот же pubkey, и ротирую. Pubkey на прыжке (чтобы вы сами на него заходили) — нормально. Приватник от прода — нет.
Когда агент всё-таки нужен — и как его не размазать
Иногда без проброса не обойтись. Типичные случаи, которые я оставляю точечно:
- С удалённой машины нужно сходить в git по SSH вашим ключом, а ключ на эту машину класть нельзя. Тогда агент — меньшее зло, чем файл в
~/.ssh. - Старый сценарий, который сам делает
sshсо прыжка на пачку хостов и не умеетProxyJump. Это чинят, но не в пять минут посреди инцидента. - Промежуточный хост должен подписать что-то ещё вашим ключом (редко, и я сначала пытаюсь перенести это действие на ноутбук).
Правило одно: агент включаю на конкретный Host, не на *. И только на время задачи.
Host git-build.internal
HostName git-build.internal
User ops
ProxyJump jump
IdentityFile ~/.ssh/id_ed25519_prod
IdentitiesOnly yes
ForwardAgent yes
На ноутбуке ключ в агент кладу с подтверждением:
ssh-add -c ~/.ssh/id_ed25519_prod
Сессию короче держу. После работы ssh-add -d этот ключ из агента. На прыжке в sshd_config я по возможности ставлю AllowAgentForwarding no — тогда даже ошибочный ForwardAgent yes у клиента не поднимет сокет. Если агент нужен на одну машину за прыжком, форвардинг агента разрешаю только там, не на bastion.
Ansible отдельным абзацем. Плейбук, который заходит на прод через bastion, не обязан пробрасывать агент. В инвентаре достаточно:
ansible_ssh_common_args: '-o ProxyJump=ops@jump.example.net'
или ansible_ssh_extra_args с тем же. Ключ остаётся на контрольной машине. Если вам «без ForwardAgent плейбук не едет», чаще виноват не Ansible, а то, что роли сами делают ssh/rsync/git уже на целевом хосте вашим ключом. Это другая задача: либо ключ деплоя на целевой машине, либо отдельный deploy key в git, либо вынести шаг на ноутбук.
Что должно быть на самом прыжке
Bastion я собираю скучно. Чем меньше на нём живёт, тем меньше соблазна «раз уж зашли, давайте тут ключи».
- Отдельный ключ только для входа на jump. Этот ключ не пускает на прод.
PermitRootLogin no, вход по паролю выключен, по возможности отдельный порт или хотя бы geo/fail2ban — как принято у заказчика, не догма.AllowTcpForwarding yes(иначе ProxyJump не взлетит),AllowAgentForwarding no,X11Forwarding no.- Если sshd умеет
PermitOpen— ограничиваю, куда вообще можно прыгать: только рабочие подсети, не «любой порт мира через нас». - Нет общих
opsс одним ключом на пятерых. Если ключ общий, вы не отзовёте одного человека. - На диске нет
id_*без.pub. Нетauthorized_keysс ключами прода «для удобства».
Кусок sshd_config на прыжке, от которого я отталкиваюсь:
AllowAgentForwarding no AllowTcpForwarding yes X11Forwarding no PermitTunnel no GatewayPorts no # если политика позволяет — режем направления: # PermitOpen prod-web-01.internal:22 prod-db-01.internal:22
PermitOpen легко забыть и потом час ловить administratively prohibited. Сначала проверяю, что ProxyJump жив без него, потом сужаю. Логи sshd на прыжке смотрю не «красиво ли», а кто и куда просил forward: если из сессии ops вдруг полезли на адрес, которого в заявке не было, это уже не удобство, а инцидент.
Мелочи, из-за которых «прыжок не работает»
Частая жалоба: «с -J меня не пускает, а два ручных ssh работают». Почти всегда одно из трёх.
Первое — разные пользователи. На jump вы ops, на проде deploy, а в команде указали одного. В конфиге это два User. В -J — явно ops@jump и deploy@prod.
Второе — DNS. Ноутбук не резолвит prod-web-01.internal, а bastion резолвит. ProxyJump соединяет туда, куда сказал клиент. Если клиент не знает имя, пишу в HostName адрес, который виден с прыжка, либо завожу имя в локальный /etc/hosts только как метку. Обратная ловушка: в HostName поставили внешний IP прода, которого с прыжка нет — ручной второй ssh с jump шёл на внутреннее имя, а ProxyJump пытается в белый адрес.
Третье — ключи и агент. Клиент предлагает ключ джампа на прод, прод отвечает «не тот» несколько раз и отваливается. Лечится IdentitiesOnly и отдельным IdentityFile. Если ключ в агенте один, а файлов много, агент всё равно может предложить не то: поэтому для прод-хостов я указываю файл явно.
Четвёртое, реже: ControlMaster. Мультиплекс на jump удобен, но старый сокет иногда держит чужие опции. Если после правки конфига поведение «как раньше», сношу сокеты ~/.ssh/cm-* и проверяю ещё раз.
Что я отказываюсь считать решением
Не считаю решением скопировать закрытый ключ на bastion «только на время работ». Файл переживает работы. Его найдёт следующий, кто зайдёт под той же учёткой.
Не считаю решением глобальный ForwardAgent yes «потому что так в wiki». Wiki часто писали, когда ProxyJump ещё не было в клиенте по умолчанию. Сейчас клиент это умеет давно.
Не считаю решением один ключ на всё: ноутбук, jump, прод, git, CI. Отзыв после утечки тогда равен «перевыпустить жизнь». Два-три ключа с понятной ролью дешевле нервов.
Не считаю решением «у нас на джампе свои люди, нам не надо». Свои люди оставляют tmux, ставят тестировать бинарник, дают sudo на час. Агент в этот час работает на всех.
Считаю решением короткую схему, которую можно проговорить вслух: ключ прода живёт на ноутбуке, прыжок только прокидывает TCP, агент выключен, кроме одной явно названной машины, и на диске bastion нет ни одного id_* без суффикса .pub. Это скучно. Зато когда bastion однажды вскроют — а интернет-машину вскрывают — прод не откроется тем же ключом из чужой сессии.
Нужна профессиональная удалённая помощь с сервером, сайтом, компьютером или ноутбуком?
Свяжитесь со мной любым удобным для вас способом, и получите её быстро и не дорого.
Обсудить задачуПомогла статья? Поблагодари автора!
Остались вопросы, или есть что добавить? Добро пожаловать в комментарии.
Угостить автора чашечкой кофе