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

от автора

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

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

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

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

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

Далее везде

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

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

  • Сервер — соответственно программа, реализующая серверную часть SSH. Не менее часто в дистрибутивах линукса предустановлен и сервер OpenSSH, выполненный в виде демона sshd. О запуске и настройке этого демона речи не пойдёт, как это делается читателю предлагается найти на других ресурсах.

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

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

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

Что такое SSH?

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

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

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

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

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

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

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

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

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

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

Уточнение 1

На самом деле стороны договариваются одновременно обо всех алгоритмах, а «первым» я его назвал для удобства. Вообще всё, что описано здесь про протокол транспортного уровня, происходит всего за 3-4 обмена сообщениями между клиентом и сервером (то бишь всего за 6-8 отправленных сообщений). Так что по длине текста статьи не стоит думать, что все проверки и обмены ключами занимают большое количество шагов, это не так. В разработке протокола старались минимизировать количество необходимых сетевых взаимодействий.

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

Вопрос: как так сделать, чтобы клиент и сервер, изначально общаясь по незащищённому соединению, смогли договориться о ключе симметричного шифрования, и при этом никто кроме клиента и сервера не узнал этот ключ? Ответ: использовать хитроумный алгоритм обмена ключами (англ. 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, при каждом подключении к новому серверу ходили просить админа отправить ключ. Обычно при первом подключении к новому серверу клиенты 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) — это, грубо говоря, тоже хеш публичного ключа, который можно спросить у админа или скопировать из авторитетного источника, тогда клиент сам проверит, что отпечаток ключа, отправленного сервером, совпадает со введённым вами, и, если это так, добавит его в 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».

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

Итого

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

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

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

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

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

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

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

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

Акт 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.

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


ссылка на оригинал статьи https://habr.com/ru/articles/747080/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *