SSH в деталях или разгребаем кучи ключей

Решил я недавно разобраться в подробностях работы SSH. Использовал его для удалённого запуска команд давно, но, будучи не слишком опытным в системном администрировании, очень размыто представлял, зачем админы просят им отправить какой-то ключ, что с этим ключом происходит при подключении, зачем при запуске ssh периодически орёт на меня какими-то предупреждениями, и прочие прелести. К своему удивлению, не смог найти ресурсов с описанием протокола, после которых у меня не осталось бы только больше вопросов. Поэтому, после прочтения спецификаций и разборок с OpenSSH, хочу разложить всё по полочкам здесь.

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

Что нужно знать

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

Терминология

Далее везде

  • Пользователь — системный пользователь, то есть который создаётся на линуксе через adduser или на винде в настройках.

  • Клиент — программа, реализующая клиентскую часть протокола SSH: отправляющая запросы к SSH-серверу, пишущая вам в консоль гневные тирады из-за смены ключей, и прочее.

  • Сервер — соответственно программа, реализующая серверную часть SSH.

  • Стороны — клиент и сервер.

  • Вы — вы, то есть человек, использующий клиент.

  • Админ(истратор) — человек, ответственный за сервер. Возможно совпадает с "вами". Также это может быть некоторая автоматическая система, например, если вы используете облачные сервисы. В этом случае взаимодействие с сервером происходит через панель управления облаком.

Что такое SSH?

SSH — это сетевой протокол для защищённого управления удалёнными устройствами по незащищённому сетевому соединению. Самый примечательный сценарий использования: удалённый вход в систему и выполнение команд.

Это вольный перевод первого абзаца статьи про SSH на английской википедии. Думаю, каждый, кто открывал хоть одну ссылку про SSH в гугле, знает об этом.

Чуть менее тривиально (написано во втором абзаце) то, что на самом деле SSH состоит из трёх более или менее независимых протоколов: протокол транспортного уровня, протокол аутентификации, и протокол соединения. Возможно, называть их прямо отдельными протоколами — слишком громкие слова, и можно было бы считать, что это просто три стадии налаживания соединения, но так их обзывают в спецификации, а чем я хуже. Кстати, спецификация у каждого из них своя: RFC 4253, RFC 4252 и RFC 4254 соответственно, за подробностями можно и нужно обращаться к этим ссылкам.

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

Акт 1. Протокол транспортного уровня

Сцена 1. Установка защищённого соединения

Первый этап в работе SSH — наладить защищённый канал связи. Внимание, спойлер: позднее, например если используется аутентификация по паролю, мы будем отправлять по сети пароль голым текстом (а как иначе сервер сможет сравнить пароль с хешем, который хранит у себя?), либо не пароль так ещё кучу всякой конфиденциальной информации. Поэтому нужно осуществить некоторые танцы с бубном, чтобы кто попало эти данные не прочитал. Это, я считаю, самый сложный из трёх протоколов, так как содержит множество хитрых фикусов и рассуждений о безопасности.

Итак, первое, что делают стороны (клиент и сервер) после того, как TCP соединение установлено (а SSH обычно работает поверх TCP) — отправляют друг другу версию своего ПО и самого протокола SSH, которую хотят использовать (на момент написания статьи актуальна версия 2.0). Далее они обмениваются списками поддерживаемых алгоритмов шифрования (и не только).

Разработчики SSH предусмотрели, что, с одной стороны, алгоритмы шифрования, как и всё в жизни, со временем выходят из моды (точнее в них находят уязвимости), а с другой, не все разработчики клиентов/серверов SSH способны поддерживать все существующие алгоритмы. Поэтому и нужен шаг, на котором стороны договариваются об используемых алгоритмах. Сколько же алгоритмов и для каких конкретных целей нам понадобится? Не один и даже не два.

В SSH слова "соединение защищено" значат, что все отправляемые сообщения подвергаются симметричному шифрованию. То есть и у сервера, и у клиента есть (обычно) одинаковый ключ симметричного шифрования, который может и зашифровать, и расшифровать любое сообщение. Симметричные шифры бывают разные, поэтому первый алгоритм, о котором договариваются стороны: алгоритм симметричного шифрования (encryption algorithm).

Уточнение 1

На самом деле стороны договариваются одновременно обо всех алгоритмах, а "первым" я его назвал для удобства. Кроме того, алгоритмов симметричного шифрования может быть выбрано два: для сообщений от сервера к клиенту один, а от клиента к серверу другой. Но спецификация рекомендует использовать один в обе стороны, и для простоты далее я буду считать, что он один.

Вопрос: как так сделать, чтобы клиент и сервер, изначально общаясь по незащищённому соединению, смогли договориться о ключе симметричного шифрования, и при этом никто кроме клиента и сервера не узнал этот ключ? Ответ: использовать хитроумный алгоритм обмена ключами (англ. key exchange algorithm, он же kex algorithm). Это второй алгоритм, о котором договариваются стороны.

Конкретные алгоритмы для обмена ключами — это обычно вариации на тему алгоритма Диффи-Хеллмана. На английской википедии есть очень наглядная картинка (приведена ниже), где в роли ключей выступают цвета. Сначала стороны договариваются о некотором открытом ключе (обычно клиент его генерирует и отправляет серверу по незащищённому соединению, на картинке общим ключом является жёлтый цвет). Затем каждая сторона создаёт свой закрытый ключ (цвет заката и морской волны в моей интерпретации 💅). Он каким-то образом объединяется с открытым ключом (в примере с цветами — смешивается, а вообще то, как ключи объединяются, и как в принципе генерируются, зависит от конкретного выбранного алгоритма обмена ключами), и результат отправляется другой стороне. Затем каждая сторона объединяет уже полученный чужой результат со своим закрытым ключом, и в итоге, благодаря магии математики, оказывается, что у обеих сторон вышло одно и то же. Это одно и то же можно использовать в качестве ключа симметричного шифрования (либо чтобы его сгенерировать).

Иллюстрация работы алгоритма Диффи-Хеллмана.

Этих двух алгоритмов достаточно, чтобы установить защищённое соединение: у обеих сторон есть ключ симметричного шифрования, поэтому они просто перед отправлением шифруют каждое сообщение, а при получении дешифруют.

Уточнение 2

На самом деле алгоритмов больше: есть ещё алгоритм сжатия данных (compression algorithm), и алгоритм имитовставки (mac algorithm), причём они, как и алгоритм симметричного шифрования, могут быть разные для сообщений от клиента к серверу и от сервера к клиенту. Но их подробности нам сейчас не слишком интересны. Со сжатием данных всё просто: содержимое сообщений сжимается с помощью выбранного алгоритма, чтобы экономить трафик. А с MAC всё наоборот чуть сложнее, но при этом знание о нём не имеет большой ценности при работе с SSH, поэтому позволю себе отправить любопытного читателя ознакомиться со спецификацией SSH.

Но это не конец. На этапе обмена ключами происходит ещё одно важное действие: клиент, так сказать, проверяет сервер на вшивость. Привычно думать, что в аутентификации нуждается клиент, то есть клиент должен подтвердить свою личность, чтобы злоумышленник не проник внутрь сервера, и не натворил делов. О предотвращении делов речь пойдёт позже. Но в SSH аутентификация сервера не менее важна: ведь клиент (опять спойлер) будет отправлять ему, например, свои пароли. Поэтому клиенту также нужно убедиться, что сервер не подставной. Для этого используется ещё один алгоритм, о котором стороны также договариваются перед обменом ключами — алгоритм цифровой подписи сервера. Это не официальное название, это я так обозвал в попытках отразить суть, в спецификации зовётся "server host key algorithm".

Сцена 2. Идентификация сервера

У сервера изначально есть пара ключей: закрытый и открытый, они называются ключами сервера (host keys). На самом деле такая пара не одна: у сервера по паре ключей на каждый алгоритм цифровой подписи, который он поддерживает (ведь разные алгоритмы генерируют ключи по-разному). У сервера OpenSSH на линуксе по умолчанию ключи сервера хранятся в папке /etc/ssh в файлах с названиями ssh_host_*_key (закрытый ключ) и ssh_host_*_key.pub (соответствующий открытый ключ), вместо звёздочки пишется название алгоритма, например rsa или ecdsa. Эти ключи перед запуском SSH-сервера кладёт туда администратор: либо генерирует с нуля (например, с помощью ssh-keygen -A), либо устанавливает из каких-то других соображений, мало ли в кармане завалялись.

Во время обмена ключами, сервер шифрует некоторую строку своим закрытым ключом (на этот момент они с клиентом уже договорились, какой алгоритм цифровой подписи использовать, поэтому ключ выбираются соответствующий), и отправляет результат клиенту впридачу с открытым ключом. Тот с помощью открытого ключа её расшифровывает. Изначальная строка составлена из некоторого набора данных, которые обеим сторонам на этот момент уже известны, вроде названия используемого протокола, версии ПО и т.п. (конкретно из каких описано в RFC), поэтому клиент может сравнить расшифрованную строку с оригинальной. Если они совпадают, значит сервер правда владеет закрытым ключом, соответствующим заявленному открытому.

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

Как убедиться, что сервер настоящий — это вопрос менее тривиальный, чем применить цифровую подпись. Есть разные варианты, одни более безопасные, другие менее. Чаще и проще всего это делается так: клиент хранит у себя небольшую базу данных (обычно просто в виде файла в определённом формате), которые сопоставляют каждому серверу его открытый ключ. Например, клиент OpenSSH на линуксе смотрит в файл ~/.ssh/known_hosts.

Теоретическая ситуация: мне нужно ssh-нуться к машинке по адресу example.com. Для этого (будучи ответственным и волнующимся о безопасности человеком), я пойду к администратору, и узнаю у него публичный ключ сервера, запущенного на этой машинке (соответствующий алгоритму, который буду использовать для проверки сервера). Затем добавлю в known_hosts запись о том, что публичный ключ сервера example.com такой-то (формат файла подскажет гугл). Затем при каждом подключении к example.com клиент будет проверять, что отправленный сервером открытый ключ совпадает с тем, что лежит в known_hosts. Если совпадает, клиент проверит, что сервер правда владеет соответствующим закрытым ключо как описано выше. Если владеет — значит сервер точно не подставной. Небольшой нюанс: так как в known_hosts ключ сопоставляется домену, то если я хочу подключаться к этой машинке не только по домену, но и по IP адресу, этому IP адресу тоже нужно отдельной строчкой сопоставить тот же ключ.

Однако вряд ли вы при каждом подключении к новому серверу идете просить админа отправить ключ. Обычно при первом подключении к новому серверу клиенты SSH показывают сообщение такого характера:

The authenticity of host '...' can't be established.
ED25519 key fingerprint is SHA256:noP2S4gaQmKTIO8cHHg4ju3QptLSo6MEgCw2AiLwOJM.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?

Таким образом клиент говорит, что не знает, можно ли доверять этому серверу, и оставляет это на ваше усмотрение. Как можно действовать:

  • Набрать no и отменить подключение.

  • Набрать yes: тогда клиент автоматически добавит полученный открытый ключ сервера в known_hosts, сопоставив его домену, к которому пытались подключиться (обычно добавляется и сопоставление к IP адресу, соответствующему на данный момент этому домену). Перед тем, как бездумно набирать yes, можно обратить внимание, что выше выведен хеш открытого ключа. Его можно сравнить с ожидаемого хешем ключа (например, опять же, спросить у админа, какой должен быть хеш, или, если подключаетесь к машине в облаке, в панели управления облака он может быть указан).

  • Также можно ввести "fingerprint" — это, грубо говоря, тоже хеш публичного ключа, который можно спросить у админа или скопировать из авторитетного источника, тогда клиент сам проверит, что ключ подходит под этот fingerprint, и если так, добавит его в known_hosts.

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

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
6e:45:f9:a8:af:38:3d:a1:a5:c7:76:1d:02:f8:77:00.
Please contact your system administrator.
Add correct host key in /home/hostname /.ssh/known_hosts to get rid of this message.

Собственно русским по белому написано: возможно вас пытаются злоумыслить, а возможно просто ключ поменялся. Узнать это можно только обратившись к ответственным за сервер. Есть способы, как организовать регулярную смену ключей, но я не достаточно квалифицирован, чтобы их описывать. Любопытый читатель загуглит "ssh key rotation".

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

Итого

После всех проделанных действий (которые, кстати, обычно укладываются в 6-8 пакетов данных, переданных между клиентом и сервером) мы имеем: возможность защищённым образом передавать пакеты по незащищённой сети, и уверенность в том, что сервер не подставной. Для этого у нас есть

  1. Ключ симметричного шифрования, который есть у обеих сторон, и который нигде не сохраняется, а генерируется заново при каждом подключении (и, вообще говоря, может меняться в течение соединения, для этого существует процедура повторного обмена ключами — key re-exchange — но это детали).

  2. Ключ сервера: закрытый только у сервера, открытый у обеих сторон.

Обращу внимание, что пока что речь ни о каком ключе клиента не шла. Тот самый id_rsa.pub, который вечно нужно генерировать и кому-то отправлять, на текущий момент ещё не использовался.

Акт 2. Протокол аутентификации

До этого сервер распинался перед клиентом, как бы подтвердить свою личность, пора бы и клиенту что-нибудь подтвердить. Всю описанную выше процедуру вполне мог провернуть злоумышленник, пытающийся получить доступ к серверу, поэтому теперь сервер должен проверить клиента.

Простейший способ аутентифицировать клиента: не аутентифицировать клиента. Да, можно настроить сервер так, что как только соединение установлено, считается, что клиент молодец, и сразу может выполнять команды/делать что там SSH ещё умеет делать. Естественно, это дико небезопасно, но в некоторых случаях можно оправдать такую схему.

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

А вот обязательна возможность входа ещё менее простейшим, но более безопасным способом — по ключу. Здесь уже вы сами создаёте пару ключей клиента (например, с помощью утилиты ssh-keygen, по умолчанию закрытый ключ она записывает в файл ~/.ssh/id_rsa, а открытый — в ~/.ssh/id_rsa.pub), сообщаете администратору открытый ключ (при использовании ssh-keygen это id_rsa.pub), тот добавляет его на сервер. Если запущен сервер OpenSSH на линуксе, то у каждого пользователя на сервере в домашней папке есть свой файл ~/.ssh/auhorized_keys: администратор добавляет ключ клиента в authorized_keys того пользователя, под которым клиент сможет входить. Можно добавить один ключ в authorized_keys нескольких пользователей, тогда вы сможете входить под всеми ними. Под каким конкретно вы пытаетесь войти — указываете при подключении, например ssh myuser@example.com.

Дальше всё как было при идентификации сервера: клиент закрытым ключом шифрует некоторую строку (которую и клиент, и сервер могут составить из известных им данных, конкретная строка описана в спецификации), и отправляет шифр серверу вместе с открытым ключом, а так же отправляет имя пользователя, под которым хочет войти (например foo). Сервер проверяет, что открытый ключ ему известен (OpenSSH на линуксе смотрит, что он указан в файле /home/foo/.ssh/authorized_keys), расшифровывает строку открытым ключом, сравнивает её с оригиналом. Если всё совпало: клиент идентифицирован, и можно принимать от него команды/файлы/что угодно.

Важно! Ни в коем случае нельзя никому сообщать свой закрытый ключ (по умолчанию ~/.ssh/id_rsa без расширения .pub). Это всё равно, что рассказать свой пароль, только пароль можно сменить самому, а чтобы сменить ключ, надо дёргать администратора.

Акт 3. Протокол соединения

На этом этапе, защищённое соединение уже установлено, клиент подтвердил личность сервера, а сервер — клиента. Протокол соединения работает на прикладном уровне: если SSH используется для запуска команд в терминале он описывает, как передавать сами команды и настройки терминала, если для передачи файлов — процесс передачи файлов. Здесь не буду вдаваться в подробности, это для меня наименее интересная часть SSH, знать подробности которой не особо важно при работе с протоколом.

Резюме

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

  • Ключ сервера — концептуально это ключ цифровой подписи, нужен для проверки личности сервера клиентом. Не используется для шифрования.

  • Ключ клиента — концептуально это ключ цифровой подписи, нужен для проверки личности клиента сервером. Не используется для шифрования.

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

Если вы используете OpenSSH на линуксе, то вот список основных файлов, которые используются. На клиенте:

  • ~/.ssh/id_rsa — закрытый ключ клиента, который никому нельзя сообщать.

  • ~/.ssh/id_rsa.pub — открытый ключ клиента, который администратор добавляет на сервер, чтобы вы получили доступ.

  • ~/.ssh/known_hosts — список, сопоставляющий доменам/IP адресам серверов их открытые ключи. Используется чтобы удостовериться, что сервер не подставной.

  • ~/.ssh/config, /etc/ssh/ssh_config — в статье не упоминался, но это настройки клиента SSH, подробнее что настраивается расскажет man ssh_config.

На сервере:

  • /etc/ssh/ssh_host_*_key — закрытые ключи сервера, по ключу на каждый поддерживаемый алгоритм цифровой подписи.

  • /etc/ssh/ssh_host_*_key.pub — открытые ключи сервера.

  • /home/[user]/.ssh/authorized_keys — список открытых ключей клиентов, которым разрешено входить под пользователем [user].

  • /etc/ssh/sshd_config — настройки сервера, подробнее в man sshd_config.

Естественно, здесь опущены разные мелочи, не расписаны принципы ротации ключей, использования сертификатов, и прочее прочее. Но, надеюсь, статья будет полезной для начинающих рыбок в море сетевых протоколов.