Левинца Егор
Обсуждение статьи на форуме проекта 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 открытых ключей пользователей остаётся сделать три простых шага:
Расскажем обо всём этом немного подробнее.
Для определённости, будем работать на 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-серверы, настроенные на использование описываемой нами инфраструктуры, перестанут считать этот ключ авторизованным для прохождения аутентификации.
Можно, конечно, ничего не сочинять, а скачать готовый скрипт из сети, там их большой выбор. Но! В большинстве своём они избыточны: вместо того, чтобы сделать быстро и качественно своё дело, они начинают зачем-то копаться в конфигурационных файлах других проектов, выполнять кучу ненужных проверок, тормозить и т.п. В общем, зная, что мы хотим получить и как это сделать, можно за несколько минут самим составить отличный скрипт.
В сердце будущего скрипта у нас будет поисковый запрос в каталог 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), так и на уровне команд оболочки (например, предварительно проверить, принадлежит ли пользователь нужной группе).
Тут всё совсем просто. На сервере, где работает 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).
Пользователи MS Windows также могут самостоятельно сгенерировать ключи для аутентификации в openssh, сохранить открытый ключ в каталог LDAP и использовать созданную нами инфраструктуру работы с открытыми ключами для подключения ко многим ssh-серверам. В данном примере мы будем использовать ssh-клиент PuTTY и его инструменты, а для работы с каталогом — LDAP-клиент LdapAdmin (но можно и любой другой, по своему вкусу). Чтобы не перегружать материал весёлыми картинками, приведём только значимые скриншоты.
Для создания пары ключей откроем программу puttygen.exe
и нажмём кнопку "Генерировать" (1):
В процессе генерации потребуется немного подвигать мышкой:
В итоге получим нашу пару ключей:
Для дальнейшего прохождения аутентификации сохраним закрытый ключ в файл, нажав кнопку "Личный ключ" (1) и пройдя соответствующие диалоги сохранения:
Открытый ключ сохранять не надо, просто скопируем его из поля "Открытый ключ для вставки в файл..." (1) в буфер обмена:
Далее откроем LDAP-клиент, подключимся к нашему LDAP-серверу, найдём запись интересующего нас пользователя и откроем её для редактирования. В окне редактирования добавим в запись вспомогательный объектный класс ldapPublicKey (1), атрибут sshPublicKey (2), в качестве значения этого атрибута вставим содержимое буфера обмена (3), и сохраним запись (4):
LDAP-клиент и puttygen.exe
можно закрыть.
Теперь мы, используя сгенерированную пару ключей, можем подключаться к любым серверам 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.