Простейшая SSO для SSH

Левинца Егор

Обсуждение статьи на форуме проекта Pro-LDAP.ru.

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

Другое дело, когда серверов и пользователей в сети больше. В этом случае узким местом становится стандартная для openssh инфраструктура хранения и работы с авторизованными открытыми ключами пользователей. Если коротко, то для того, чтобы пользователь успешно прошёл аутентификацию по паре ключей, открытый ключ из этой пары должен входить в некий набор известных серверу openssh "авторизованных" для данного пользователя открытых ключей. По умолчанию этот набор представляет собой обычный текстовый файл ~/.ssh/authorized_keys в домашней директории пользователя (расположение задаётся директивой AuthorizedKeysFile в sshd_config). Если пользователь заходит по ssh с аутентификацией по ключам на несколько серверов, то на каждом из них в файле authorized_keys должен присутствовать его открытый ключ (ключи). Когда серверов много, поддерживать такую инфраструктуру становится по крайней мере неудобно, а то и небезопасно (представьте, например, что Вам нужно оперативно отозвать открытые ключи какого-нибудь пользователя со всех серверов при компрометации его закрытого ключа).

С другой стороны, если сеть более-менее организована, то в ней наверняка имеется некое централизованное хранилище данных о пользователях и компьютерах. Почему бы не хранить открытые ключи пользователей для прохождения аутентификации на серверах openssh именно в нём? Со временем это осознали и разработчики openssh, и в версии 6.2 появились директивы настройки, позволяющие черпать информацию об авторизованных ключах пользователя не только из файла authorized_keys, но и путём запуска сторонней программы, получающей на вход имя пользователя и выдающей на выход ключи этого пользователя. Как именно программа будет получать эти ключи — остаётся на усмотрение разработчика этой программы.

Вот такой простейший, но одновременно и гениальный подход: храни ключи где хочешь в любой точке сети, отдай их по запросу серверу openssh, и пользователь сможет на основании этого ключа пройти аутентификацию.

В современных условиях одним из основных вариантов подобного централизованного хранилища является каталог LDAP, ведь в том или ином виде он присутствует практически в любой организации (тот же Active Directory, к примеру). Имея каталог, для организации инфраструктуры централизованного хранения и выдачи авторизованных для openssh открытых ключей пользователей остаётся сделать три простых шага:

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

Расскажем обо всём этом немного подробнее.

Шаг 1. Модификация учётной записи

Для определённости, будем работать на Linux (дистрибутив значения не имеет) с сервером openssh и его утилитами командной строки. В качестве сервера каталогов будем использовать OpenLDAP и его клиенты командной строки.

У нас есть каталог, где хранятся учётные записи пользователей, например, такая:

dn: uid=ivanov,ou=People,dc=mycompany,dc=ru
objectClass: inetOrgPerson
objectClass: posixAccount
uid: ivanov
cn: Ivan Ivanov
sn: Ivanov
userPassword:: aXZhbm92UGFzc3dvcmQ=
uidNumber: 1001
gidNumber: 1001
homeDirectory: /home/ivanov
loginShell: /bin/bash
gecos: ivanov

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

/home/ivanov/.ssh/
              |
              |\ id_rsa     # Закрытый ключ RSA
               \ id_rsa.pub # Открытый ключ RSA

Если же у пользователя ещё нет пары ключей, то не составляет труда её сгенерировать:

ivanov@host ~ $ ssh-keygen -t rsa -f ~/.ssh/id_rsa -q -N ''

Итак, у нас есть пара ключей для работы по алгоритму RSA с именами id_rsa и id_rsa.pub, закрытый ключ не защищён парольной фразой (дискуссию о рисках при отказе от использования парольной фразы оставим за скобками). Поместим открытый ключ в учётную запись пользователя в каталоге LDAP. Поскольку открытый ключ — это просто строка символов (убедитесь сами, заглянув в файл id_rsa.pub), то для его хранения подойдёт любой из доступных (и неиспользуемых) текстовых атрибутов, например, description, roomNumber или favouriteDrink. Однако, общепринятой практикой является использование вспомогательного объектного класса ldapPublicKey и его атрибута sshPublicKey из схемы данных проекта openssh-ldappubkey, решавшего задачу получения открытого ключа из каталога до выхода openssh 6.2 (естественно, этот набор схемы данных нужно предварительно добавить в конфигурацию сервера OpenLDAP).

Пришла пора сформировать LDIF-файл для модификации учётной записи пользователя. Поскольку открытый ключ находится в отдельном файле, существует соблазн использовать URL-нотацию:

dn: uid=ivanov,ou=People,dc=mycompany,dc=ru
changetype: modify
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey:< file:///home/ivanov/.ssh/id_rsa.pub

Ключ будет добавлен, но, поскольку в конце файла id_rsa.pub присутствует символ перевода строки, то перед помещением в атрибут ключ будет закодирован в Base64 (к слову, значимая часть ключа для представления в текстовом виде непечатных символов сама по себе закодирована в Base64). В общем-то ничего страшного, но перед выдачей ключа для обработки в openssh его придётся декодировать обратно. Лучше не полениться и скопировать строку с ключом прямо в LDIF-файл:

dn: uid=ivanov,ou=People,dc=mycompany,dc=ru
changetype: modify
add: objectClass
objectClass: ldapPublicKey
-
add: sshPublicKey
sshPublicKey: ssh-rsa XXXXXXXXXX...XXXXXXXXXX ivanov@host

(для удобства представления ключ был сокращён).

Применим изменения учётной записи в каталоге:

$ ldapmodify -H ldap://10.0.0.10 -x -D uid=ivanov,ou=People,dc=mycompany,dc=ru -W -f ./add_pub_key.ldif
Enter LDAP Password: 
modifying entry "uid=ivanov,ou=People,dc=mycompany,dc=ru"

Проверим, всё ли в порядке:

$ ldapsearch -x -LLL -o ldif-wrap=no -H ldap://10.0.0.10 -b 'dc=mycompany,dc=ru' 'uid=ivanov' sshPublicKey
dn: uid=ivanov,ou=People,dc=mycompany,dc=ru
sshPublicKey: ssh-rsa XXXXXXXXXX...XXXXXXXXXX ivanov@host

Ключ на месте, можно переходить ко второму шагу. Но, прежде чем мы это сделаем, позвольте привести ещё несколько замечаний.

Во-первых, необходимо отметить, что данный (первый) шаг, по-хорошему, должен выполняться самим пользователем. Прекрасно понимая, что далеко не все пользователи запускают ssh-клиент из Unix-подобных операционок, в конце статьи мы приводим материал о том, как создать пару ключей, поместить открытый ключ в каталог и устанавливать ssh-соединение с аутентификацией по ключам в MS Windows.

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

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

Шаг 2. Bash-скрипт: всё гениальное просто

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

В сердце будущего скрипта у нас будет поисковый запрос в каталог LDAP. Вполне подойдёт тот, который мы использовали для проверки наличия ключа в учётной записи пользователя в самом конце предыдущего шага. Для большей определённости немного усовершенствуем поисковый фильтр LDAP (учётка будет выбираться лишь тогда, когда в ней есть открытый ключ):

$ ldapsearch -x -LLL -o ldif-wrap=no -H ldap://10.0.0.10 -b 'dc=mycompany,dc=ru' '(&(uid=ivanov)(sshPublicKey=*))' sshPublicKey
dn: uid=ivanov,ou=People,dc=mycompany,dc=ru
sshPublicKey: ssh-rsa XXXXXXXXXX...XXXXXXXXXX ivanov@host

Упростим вывод команды, оставив только интересующие нас строки (с помощью grep):

$ ldapsearch -x -LLL -o ldif-wrap=no -H ldap://10.0.0.10 -b 'dc=mycompany,dc=ru' '(&(uid=ivanov)(sshPublicKey=*))' sshPublicKey | \
> grep sshPublicKey
sshPublicKey: ssh-rsa XXXXXXXXXX...XXXXXXXXXX ivanov@host

Наконец, отрежем всё лишнее, оставив только открытый ключ пользователя (с помощью perl):

$ ldapsearch -x -LLL -o ldif-wrap=no -H ldap://10.0.0.10 -b 'dc=mycompany,dc=ru' '(&(uid=ivanov)(sshPublicKey=*))' sshPublicKey | \
> grep sshPublicKey | \
> perl -wpe 's/sshPublicKey: //'
ssh-rsa XXXXXXXXXX...XXXXXXXXXX ivanov@host

На этом можно было бы остановиться, но хочется всё же предусмотреть ситуацию, когда какой-нибудь пользователь по недосмотру внесёт в атрибут sshPublicKey повторно закодированное в Base64 значение. Усовершенствуем наш однострочник на perl:

$ ldapsearch -x -LLL -o ldif-wrap=no -H ldap://10.0.0.10 -b 'dc=mycompany,dc=ru' '(&(uid=ivanov)(sshPublicKey=*))' sshPublicKey | \
> grep sshPublicKey | \
> perl -MMIME::Base64 -wpe 's/^sshPublicKey(:{1,2}) (.+)$/$1 eq "::" ? decode_base64($2) : $2/e'
ssh-rsa XXXXXXXXXX...XXXXXXXXXX ivanov@host

Вот и всё. Оформим наш bash-скрипт /usr/bin/get_ldap_ssh_key.sh:

#!/bin/bash

# Параметры поиска в LDAP-каталоге
LDAP_URI="ldap://10.0.0.10"
BASE_DN="dc=mycompany,dc=ru"

# Имя пользователя из первого аргумента вызова скрипта
SSH_USER=$1

# Получаем ключ (ключи)
KEY=$(ldapsearch -x -LLL -o ldif-wrap=no -H "${LDAP_URI}" -b "${BASE_DN}" "(&(uid=${SSH_USER})(sshPublicKey=*))" sshPublicKey | \
    grep sshPublicKey | \
    perl -MMIME::Base64 -wpe 's/^sshPublicKey(:{1,2}) (.+)$/$1 eq "::" ? decode_base64($2) : $2/e')

# И выводим его (их)
echo "${KEY}"

Опробуем скрипт (предварительно не забыв дать ему права на исполнение):

$ /usr/bin/get_ldap_ssh_key.sh ivanov
ssh-rsa XXXXXXXXXX...XXXXXXXXXX ivanov@host

Вот и всё. Напоследок следует отметить, что этот скрипт можно совершенствовать как угодно, добавляя необходимые проверки как на уровне каталога (в виде дополнительных поисковых фильтров LDAP), так и на уровне команд оболочки (например, предварительно проверить, принадлежит ли пользователь нужной группе).

Шаг 3. Настройка сервера openssh

Тут всё совсем просто. На сервере, где работает openssh, в файле /etc/ssh/sshd_config нужно раскомментировать (или добавить) строки:

AuthorizedKeysCommand /usr/bin/get_ldap_ssh_key.sh
AuthorizedKeysCommandUser nobody

В первой из них, понятно, указывается имя нашего скрипта извлечения ключа, во второй — имя пользователя, с правами которого этот скрипт будет выполняться (в данном случае бесправный nobody). Маленький нюанс: sshd отказывается использовать скрипт, если у него "слишком мягкие" ограничения. Таких оказалось достаточно:

$ ls -l /usr/bin/get_ldap_ssh_key.sh
-rwxr-xr-x 1 root root 590 дек.  25 21:00 /usr/bin/get_ldap_ssh_key.sh

Остаётся только перезапустить openssh и попробовать пройти аутентификацию с ssh-клиента по паре ключей.

Да, скрипт получения ключа и данную настройку sshd нужно будет распространить на все обслуживаемые openssh сервера. Но эту задачу несложно автоматизировать, если в сети используется какая-либо система распространения конфигураций (например, chef или puppet).

Бонус для пользователей Windows

Пользователи MS Windows также могут самостоятельно сгенерировать ключи для аутентификации в openssh, сохранить открытый ключ в каталог LDAP и использовать созданную нами инфраструктуру работы с открытыми ключами для подключения ко многим ssh-серверам. В данном примере мы будем использовать ssh-клиент PuTTY и его инструменты, а для работы с каталогом — LDAP-клиент LdapAdmin (но можно и любой другой, по своему вкусу). Чтобы не перегружать материал весёлыми картинками, приведём только значимые скриншоты.

Генерация пары ключей и запись открытого ключа в каталог LDAP

Для создания пары ключей откроем программу puttygen.exe и нажмём кнопку "Генерировать" (1):

В процессе генерации потребуется немного подвигать мышкой:

В итоге получим нашу пару ключей:

Для дальнейшего прохождения аутентификации сохраним закрытый ключ в файл, нажав кнопку "Личный ключ" (1) и пройдя соответствующие диалоги сохранения:

Открытый ключ сохранять не надо, просто скопируем его из поля "Открытый ключ для вставки в файл..." (1) в буфер обмена:

Далее откроем LDAP-клиент, подключимся к нашему LDAP-серверу, найдём запись интересующего нас пользователя и откроем её для редактирования. В окне редактирования добавим в запись вспомогательный объектный класс ldapPublicKey (1), атрибут sshPublicKey (2), в качестве значения этого атрибута вставим содержимое буфера обмена (3), и сохраним запись (4):

LDAP-клиент и puttygen.exe можно закрыть.

Подключение к ssh-серверам с аутентификацией по паре ключей

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

Если закрытый ключ защищён парольной фразой, или для работы нам требуется несколько пар ключей, то целесообразнее использовать агент pagent.exe. Рекомендую в таком случае ознакомиться с этой статьёй.

В нашем примере мы используем одну пару ключей, поэтому укажем ссылку на файл закрытого ключа прямо в настройках PuTTY. В разделе "Соединение" -> "SSH" -> "Аутентификация" (1) с помощью кнопки "Обзор..." (2) укажем местоположение закрытого ключа в поле "Файл с личным ключом для аутентификации" (3):

Для удобства мы также можем указать имя пользователя, которое будет передаваться ssh-серверу автоматически. В разделе "Соединение" -> "Данные" (1) можно либо выбрать пункт "Использовать системное" (2) (если оно соответствует нужному нам), либо задать имя вручную в поле "Имя пользователя для автовхода" (3):

Осталось сохранить наши настройки в качестве настроек по умолчанию. В разделе "Сеанс" (1), выбрать "Default Settings" (2) и нажать кнопку "Сохранить" (3):

Теперь можно попасть на любой сервер, настроенный на нашу инфраструктуру, введя только имя (или ip-адрес) этого сервера.

Ссылки

Материалы, которые помогли мне разобраться в теме:

Последнее изменение страницы — 29 декабря 2016 года.

Обсуждение статьи на форуме проекта Pro-LDAP.ru.