Решил я недавно разобраться в подробностях работы SSH. Использовал его для удалённого запуска команд давно, но, будучи не слишком опытным в системном администрировании, очень размыто представлял, зачем админы просят им отправить какой-то ключ, что с этим ключом происходит при подключении, зачем при запуске ssh периодически орёт на меня какими-то предупреждениями, и прочие прелести. К своему удивлению, не смог найти ресурсов с описанием протокола, после которых у меня не осталось бы только больше вопросов. Поэтому, после прочтения спецификаций и разборок с OpenSSH, хочу разложить всё по полочкам здесь. Статья рассчитана на тех, кто поверхностно знаком с SSH, возможно, использовали на практике, но не осознали его сакральных смыслов и глубоких тайн. Попытаюсь описать основные аспекты безопасности протокола: какие ключи и алгоритмы используются, в какой момент и зачем. Статья призвана дать базу в работе с самим протоколом SSH, зная которую можно разбираться в его более продвинутых возможностях. Также будут замечания, как некоторые компоненты протокола претворяются в жизнь в OpenSSH на линуксе (конкретной программе, наверное, самой популярной, реализующей протокол SSH). Единственное требование: знать об (а)симметричном шифровании и цифровой подписи. Что такое криптографический ключ, чем открытый отличается от закрытого, зачем они нужны и что умеют — на эти и связанные вопросы здесь ответа нет. Кому надо, тот загуглит про (а)симметричное шифрование, об этом есть множество доступных ресурсов. Далее везде  Пользователь — системный пользователь, то есть который создаётся на линуксе через adduser или на винде в настройках. Клиент — программа, реализующая клиентскую часть протокола SSH: отправляющая запросы к SSH-серверу, пишущая вам в консоль гневные тирады из-за смены ключей, и прочее. В дистрибутивах линукса часто предустановлен клиент OpenSSH, который запускается командой ssh. Сервер — соответственно программа, реализующая серверную часть SSH. Не менее часто в дистрибутивах линукса предустановлен и сервер OpenSSH, выполненный в виде демона sshd. О запуске и настройке этого демона речи не пойдёт, как это делается, читателю предлагается найти на других ресурсах. Стороны — клиент и сервер. Вы — вы, то есть человек, использующий клиент. Админ(истратор) — человек, ответственный за сервер. Возможно, совпадает с "вами". Также это может быть некоторая автоматическая система, например, если вы используете облачные сервисы. В этом случае взаимодействие с сервером происходит через панель управления облаком. SSH — это сетевой протокол для защищённого управления удалёнными устройствами по незащищённому сетевому соединению. Самый примечательный сценарий использования: удалённый вход в систему и выполнение команд. Это вольный перевод первого абзаца статьи про SSH на английской википедии. Думаю, каждый, кто открывал хоть одну ссылку про SSH в гугле, знает об этом. Чуть менее тривиально (написано во втором абзаце) то, что на самом деле SSH состоит из трёх более или менее независимых протоколов: протокол транспортного уровня, протокол аутентификации, и протокол соединения. Возможно, называть их прямо отдельными протоколами — слишком громкие слова, и можно было бы считать, что это просто три стадии работы протокола. Но так их обзывают в спецификации, а чем я хуже непонятно. Кстати, спецификация у каждого из них своя: RFC 4253, RFC 4252 и RFC 4254 соответственно, за подробностями можно и нужно обращаться к этим ссылкам. Обычно на практике разделение на три протокола нечасто упоминается, но, чтобы понять, когда и зачем какой ключ используется, это довольно важно. Поэтому далее каждый из этих протоколов разберём отдельно. Первый этап в работе SSH — наладить защищённый канал связи. Внимание, спойлер: позднее, например если используется аутентификация по паролю, мы будем отправлять по сети пароль голым текстом (а как иначе сервер сможет сравнить пароль с хешем, который хранит у себя?), либо не пароль так ещё кучу всякой конфиденциальной информации. Поэтому нужно осуществить некоторые танцы с бубном, чтобы кто попало эти данные не прочитал. Это, по-моему, самый сложный из трёх протоколов, так как содержит множество хитрых фикусов и рассуждений о безопасности.  Итак, первое, что делают стороны после того, как между устройствами установлено сетевое соединение (а SSH обычно работает поверх TCP, так что обычно после того, как налажено соединение TCP) — отправляют друг другу версию своего ПО и самого протокола SSH, которую хотят использовать (на момент написания статьи актуальна версия 2.0). Далее они обмениваются списками поддерживаемых алгоритмов шифрования (и не только). Разработчики SSH предусмотрели, что, с одной стороны, алгоритмы шифрования, как и всё в жизни, со временем выходят из моды (точнее в них находят уязвимости), а с другой, не все разработчики клиентов/серверов SSH способны поддерживать все существующие алгоритмы. Поэтому и нужен шаг, на котором стороны договариваются об используемых алгоритмах. Сколько же алгоритмов и для каких конкретных целей нам понадобится? Не один и даже не два.  Первый алгоритм — алгоритм сжатия. Самый скучный, им просто сжимается каждое отправленное сообщение ради экономии трафика.  Во-первых, алгоритмов сжатия может быть выбрано два: один для сообщений от клиента к серверу, второй от сервера к клиенту. Конкретно для сжатия возможность выбрать два алгоритма не особо важна, всё равно почти всегда используется gzip в обе стороны. Но такая же возможность есть для некоторых других алгоритмов, для которых она может быть более актуальной (далее указано, для каких). Во-вторых, на самом деле стороны договариваются одновременно обо всех алгоритмах, а "первым" я его назвал в целях красоты повествования. Вообще всё, что описано далее про протокол транспортного уровня, происходит всего за 3-4 обмена сообщениями между клиентом и сервером (то бишь всего за 6-8 отправленных сообщений). Так что по длине текста статьи не стоит делать вывод, что все проверки и обмены ключами и алгоритмами занимают большое количество шагов, это не так. В разработке протокола старались минимизировать количество необходимых сетевых взаимодействий. В SSH слова "соединение защищено", помимо прочего, значат, что все отправляемые сообщения подвергаются симметричному шифрованию. То есть и у сервера, и у клиента есть (обычно) одинаковый ключ симметричного шифрования, который может и зашифровать, и расшифровать любое сообщение. Симметричные шифры бывают разные, поэтому второй алгоритм, о котором договариваются стороны: алгоритм симметричного шифрования (encryption algorithm). Как и со сжатием, алгоритмов симметричного шифрования, может быть выбрано два: для сообщений от сервера к клиенту один, а от клиента к серверу другой. Спецификация рекомендует использовать одинаковый в обе стороны, и чаще всего так и происходит, поэтому я не буду зацикливаться. Но вообще говоря надо иметь это в виду: алгоритмов симметричного шифрования (а, следовательно, и ключей симметричного шифрования) две штуки на соединение. Вопрос: как так сделать, чтобы клиент и сервер, изначально общаясь по незащищённому соединению, смогли договориться о ключе симметричного шифрования, и при этом никто кроме клиента и сервера не узнал этот ключ? Ответ: использовать хитроумный алгоритм обмена ключами (англ. key exchange algorithm, он же kex algorithm). Это третий алгоритм, о котором договариваются стороны. Конкретные алгоритмы обмена ключами — это вариации на тему алгоритма Диффи-Хеллмана. На английской википедии есть очень наглядная картинка (приведена ниже), где в роли ключей выступают цвета. Сначала стороны договариваются о некотором открытом ключе (обычно клиент его генерирует и отправляет серверу по незащищённому соединению, на картинке общим ключом является жёлтый цвет). Затем каждая сторона создаёт свой закрытый ключ (на картинке цвет заката и морской волны в моей интерпретации 💅). Он каким-то образом объединяется с открытым ключом (в примере с цветами — смешивается, а вообще, то, как ключи объединяются, и как в принципе генерируются, зависит от конкретного выбранного алгоритма обмена ключами), и результат отправляется другой стороне.  Затем каждая сторона объединяет уже полученный чужой результат со своим закрытым ключом, и в итоге, благодаря магии математики, оказывается, что у обеих сторон вышло одно и то же — общий секретный ключ. Далее я не буду выделять его как один из используемых ключей, потому что это всего лишь промежуточное значение, которое используется для генерации ключей, делающих более осмысленные вещи. Тут вопрос терминологии: в английском этот секретный ключ даже ключом не называется, там это просто "shared secret". Это одно и то же можно использовать, чтобы сгенерировать ключ симметричного шифрования. Он считается как хеш секретного ключа, конкатенированного с некоторыми другими данными, также известными и клиенту, и серверу. Сама функция хеширования привязана к выбранному алгоритму обмена ключами, так что у обеих сторон она одинаковая. Однако только симметричного шифрования недостаточно для защищённого соединения. Нужен ещё уже четвёртый алгоритм имитовставки (mac algorithm), которому нужен ключ имитовставки (integrity key). Он настолько же важный и базовый, как алгоритм симметричного шифрования, и введение имитовставки, так же, как и шифрование, обычно происходит под капотом у программ-реализаций, а рядовой разработчик о них не задумывается. Но если, говоря о шифровании, я могу рассчитывать, что большинство знакомы с его принципами, то имитовставка менее общеизвестна. Поэтому позволю себе вынести подробности о ней в спойлер для любопытного читателя. Почему же шифра недостаточно? Шифр защищает от возможности прочитать содержимое сообщения, но не защищает от того, чтобы изменить его. Да, злоумышленник не знает ключ шифрования, так что скорее всего не сможет подменить сообщение на что-либо осмысленное, то есть, например, не сможет заставить сервер выполнить конкретную команду (и то не факт, если очень умный, может и сможет). Но он точно может намусорить: поменять какой-нибудь байт, и на сервер придёт мусорное сообщение, а сервер подумает, что клиент это и имел в виду. Последствия у такого тоже могут быть самые разные, в лучшем случае просто команда клиента не выполнится, и клиенту вернётся сообщение с ошибкой.  Но SSH же работает поверх TCP, который гарантирует целостность данных? Он включает контрольную сумму, которая не позволит просто так изменить сообщение? Не совсем. TCP защищает от случайных изменений пакета. Злоумышленник же, поменяв содержимое сообщения, может запросто пересчитать контрольную сумму, и подменить и её: ведь шифруются только данные, на уровне протокола SSH, а данные всех протоколов, поверх которых SSH работает, не шифруются. Поэтому для предотвращения таких вмешательств в ход истории существует имитовставка (англ. MAC — message authentication code). Это набор байтов, который генерируется из содержимого сообщения и ключа имитовставки (integrity key), и дописывается к концу сообщения. Используя этот ключ и содержимое сообщения, каждая сторона может проверить, что оно дошло без изменений. Ключ хранится в секрете и (обычно) одинаковый у сервера и клиента, всё как с симметричным шифрованием. Таким образом, если злоумышленник подменит какие-то байты, сервер заметит подвих и не станет обрабатывать изменённое сообщение. Сам ключ имитовставки генерируется аналогично ключу симметричного шифрования — как хеш секретного ключа, полученного из алгоритма обмена ключами, и некоторой информации, известной обеим сторонам. Только эта дополнительная информация разная для генерации ключа имитовставки и симметричного шифрования, поэтому и ключи получаются разные. От каких именно данных считается хеш для всех ключей, написано в RFC.  Алгоритмов имитовставки (а, следовательно, и ключей имитовставки) также может быть два на соединение: один от клиента к серверу, другой от сервера к клиенту. Этих трёх алгоритмов достаточно, чтобы наладить защищённое соединение: стороны перед отправлением шифруют каждое сообщение, при получении дешифруют, а имитовставка гарантирует, что сообщение дошло в неизменном виде. Но это не конец. На этапе обмена ключами происходит ещё одно важное действие: клиент, так сказать, проверяет сервер на вшивость. Привычно думать, что в аутентификации нуждается клиент, то есть клиент должен подтвердить свою личность, чтобы злоумышленник не проник внутрь сервера, и не натворил делов. О предотвращении делов речь пойдёт позже. Но в SSH аутентификация сервера не менее важна, ведь клиент (опять спойлер) будет отправлять ему, например, свои пароли. А все описанные ранее шаги не гарантируют, что сервер не подставной: с таким же успехом клиент мог произвести обмен ключами со злоумышленником. Чтобы бороться с недобросовестностью сервера, используется очередной алгоритм, о котором стороны также договариваются перед обменом ключами — алгоритм цифровой подписи сервера. Это не официальное название, это я так обозвал в попытках отразить суть, в спецификации зовётся "server host key algorithm". Пока что все описанные действия — базовые меры безопасности, которые происходят за кулисами, и с которыми редко приходится иметь дело, если вообще приходится. Теперь же речь пойдёт о вещах более приземлённых, и знание о которых важно при работе с протоколом. У сервера изначально есть пара ключей: закрытый и открытый, они называются ключами сервера (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, при каждом подключении к новому серверу ходили просить админа отправить ключ. Обычно при первом подключении к новому серверу клиенты SSH показывают сообщение такого характера: Таким образом клиент говорит, что не знает, можно ли доверять этому серверу, и оставляет это на ваше усмотрение. Как можно действовать: Набрать no и отменить подключение. Набрать yes: тогда клиент автоматически добавит полученный открытый ключ сервера в known_hosts, сопоставив его домену, к которому пытались подключиться (обычно добавляется и сопоставление к IP адресу, соответствующему на данный момент этому домену). Перед тем, как бездумно набирать yes, можно обратить внимание, что выше выведен хеш открытого ключа. Его можно сравнить с ожидаемым хешем ключа (например, опять же, спросить у админа, какой должен быть хеш, или, если подключаетесь к машине в облаке, в панели управления облака он может быть указан). Также можно ввести отпечаток (fingerprint) — это, грубо говоря, тоже хеш публичного ключа, который можно спросить у админа или скопировать из авторитетного источника, тогда клиент сам проверит, что отпечаток ключа, отправленного сервером, совпадает со введённым вами, и, если это так, добавит его в known_hosts. Стоит отметить, что так как ключ сервера используется клиентом для идентификации, если он в какой-то момент изменится (например, если на сервере переустановили SSH или операционку, либо может перегенерировали ключи, из-за того, что закрытый оказался слит, а, может, просто в целях профилактики), то сервер не получится идентифицировать. С точки зрения клиента смена ключа неотличима от попытки злоумышленника подсунуть липовый ключ. Поэтому в таких случаях клиент при подключении на вас наорёт, например, как орёт OpenSSH: Собственно русским по белому написано: возможно, вас пытаются злоумыслить, а, возможно, просто ключ поменялся. Узнать это можно только обратившись к администратору. Есть способы, как организовать регулярную смену ключей, но я недостаточно квалифицирован, чтобы их описывать. Любопытный читатель загуглит "ssh key rotation". Хранить публичный ключ вкупе с доменом — не единственный способ идентификации сервера, ещё можно, например, использовать сертификаты: тогда сервер перед обменом ключами отправит свой сертификат, который клиент сможет проверить на достоверность. Но о них, возможно, в другой раз. После всех проделанных действий мы имеем: возможность защищённым образом передавать пакеты по незащищённой сети, и уверенность в том, что сервер не подставной. Для этого у нас есть Ключ сервера: закрытый хранится только на сервере, открытый стороны используют в начале соединения для проверки личности сервера, и после этого больше не используют. Ключ симметричного шифрования и ключ имитовставки: есть у обеих сторон, хранятся только в памяти и генерируются заново при каждом подключении (и, вообще говоря, могут меняться в течение соединения. Для этого существует процедура повторного обмена ключами — key re-exchange — но это детали). Нужны для работы защищённого соединения. Разработчик обычно с ними дела не имеет, работа с ними происходит под капотом. Обращу внимание, что пока что речь ни о каком ключе клиента не шла. Тот самый id_rsa.pub, который вечно нужно генерировать и кому-то отправлять, на текущий момент ещё не использовался. До этого сервер распинался перед клиентом, как бы подтвердить свою личность, пора бы и клиенту что-нибудь подтвердить. Всю описанную выше процедуру вполне мог провернуть злоумышленник, пытающийся получить доступ к серверу, поэтому теперь сервер должен проверить клиента. Простейший способ аутентифицировать клиента: не аутентифицировать клиента. Да, можно настроить сервер для некоторых пользователей так, что как только соединение установлено, считается, что клиент молодец, и сразу может от имени этих пользователей выполнять команды/делать что там SSH ещё умеет делать. Под каким пользователем нужно подключиться, вы выбираете сами при подключении (пишете ssh myuser@example.com), если для него авторизация выключена, то и ни паролей, ни ключей не понадобится. Естественно, это дико небезопасно, но в некоторых случаях можно оправдать такую схему. Но мы не будем её оправдывать. У нас же теперь есть защищённое соединение, по которому можно передавать что угодно без страха, что кто-то левый прочитает. Поэтому чуть менее простейший способ аутентификации клиента: по паролю. Клиент просто отправляет имя пользователя, под которым хочет зайти, и пароль. Сервер где-то у себя хранит хеш пароля для каждого пользователя, под которым можно зайти (на линуксе обычно пользователи 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. Дальше всё как было при идентификации сервера: клиент составляет строку (из данных, которые сервер тоже знает, конкретная строка описана в спецификации), подписывает закрытым ключом и отправляет подпись серверу вместе с открытым ключом, а также именем пользователя, под которым хочет войти (допустим, myuser). Сервер проверяет, что открытый ключ ему известен (OpenSSH на линуксе смотрит, что он указан в файле /home/myuser/.ssh/authorized_keys), составляет такую же строку, и проверяет достоверность подписи. Если всё совпало — клиент идентифицирован, и можно принимать от него команды/файлы/что угодно. Важно! Ни в коем случае нельзя никому сообщать свой закрытый ключ (по умолчанию ~/.ssh/id_rsa без расширения .pub). Это всё равно, что рассказать свой пароль: сервер не может отличить вас от злоумышленника, имеющего ваш закрытый ключ. На этом этапе, защищённое соединение установлено, клиент подтвердил личность сервера, а сервер — клиента. Протокол соединения работает уже на прикладном уровне: если SSH используется для запуска команд в терминале, он описывает, как передавать сами команды и настройки терминала, если для передачи файлов — процесс передачи файлов. Это наименее интересная для меня часть 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 — список открытых ключей клиентов, которым разрешено входить под пользователем . /etc/ssh/sshd_config — настройки сервера, подробнее в man sshd_config. Естественно, здесь опущены разные мелочи, не расписаны принципы ротации ключей, использования сертификатов, и прочее прочее. Но, надеюсь, статья будет полезной для начинающих рыбок в море сетевых протоколов.