Автор Тема: OpenLDAP - Второй фактор  (Прочитано 2573 раз)

earyutin

  • Новичок
  • *
  • Сообщений: 6
    • Просмотр профиля
OpenLDAP - Второй фактор
« : 01 Ноябрь 2022, 00:54:30 »
Всем привет, Коллеги!

Дошли наконец до задачи настройки централизованной авторизации.
Были развернуты freeradius для сетевых устройств и Open LDAP Proxy в связке с Active Directory для порталов.

Возник вопрос: возможно ли выполнять запуск внешних скриптов на Open LDAP, когда есть совпадение пары логина и пароля, до установления привязки?

На freeradius вроде как можно, но пока не вникал. https://networkradius.com/doc/3.0.10/raddb/mods-available/exec.html
Идея в том, чтобы OpenLDAP запускал php скрипт, после получения от AD сообщения о совпадение пары логина и пароля, который в свою очередь отправлял сообщение сотруднику и ждал бы ответ от него.
Предполагаемая схема во вложении.

Нашел обсуждения https://pro-ldap.ru/forum/index.php?topic=1039.msg2149#msg2149 и https://pro-ldap.ru/forum/index.php?topic=433.msg1169#msg1169. Подскажите, возможно ли использовать database shell для реализации моей задачи ?

С php я думаю разберусь. Мне бы понять, можно ли организовать запуск и ожидание выполнение скрипта в средствами OpenLDAP, случае положительного ответа от Active Directory.

Спасибо )!
« Последнее редактирование: 01 Ноябрь 2022, 01:08:38 от earyutin »

egor

  • Администратор
  • Старожил
  • *****
  • Сообщений: 486
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #1 : 01 Ноябрь 2022, 18:40:57 »
Здравствуйте! Правильно ли я понимаю, что Вы хотите после прохождения двухфакторной аутентификации, первым фактором которой будет совпадение логина и пароля пользователя AD, получить какие-то данные из самого AD?

Если сами данные из AD не нужны, а нужна только аутетификация, то, наверное, нет смысла вообще городить огороды с OpenLDAP, а сразу разбираться с Radius. Я в нём не специалист, но наверняка многофакторную аутентификацию он делать умеет.

В общем, опишите задачу поподробнее, если это деликатная информация, можно в личку.

Егор
« Последнее редактирование: 01 Ноябрь 2022, 18:52:09 от egor »

earyutin

  • Новичок
  • *
  • Сообщений: 6
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #2 : 01 Ноябрь 2022, 22:02:57 »
Добрый вечер, Егор !

Да, верно. Данные из AD необходимо получить.
Идея такова, чтобы перед авторизацией php проверил наличие ID чата, принадлежность к определенной группе и т.д.

Схема, как я вижу следующая:

1. Сотрудник проходит авторизацию на портале
2. Портал отправляет запрос на OpenLDAP Proxy -> далее, идет запрос в Active Directory
3. Active Directory отвечает -> OpenLDAP Proxy
4. OpenLDAP Proxy (если логин/пароль верны)-> Запуск PHP скрипта
5. PHP Ищет атрибут "TelegramID" у пользователя в каталоге AD (Схему изменил в AD, атрибут есть. PHP обрабатывает прекрасно) и отправляет в чат телеграм сообщение-вопрос с кнопками Это вы ? "Это я" и "Нет, не я".
Если пользователь нажимает кнопку "Это я", соответственно выполняется привязка.. Если пользователь не чего не отвечает, то есть срабатывает тайм аут или же отвечает отрицательно - привязка не выполняется, происходит ошибка авторизации.

6. Далее уже работа Webhook telegram bot

На текущий момент, главная задача заставить OpenLDAP запускать скрипт, не выполняя привязку, пока скрипт не отработает.

Radius не подходит для таких порталов, как битрикс.
Рассматриваю Radius для VPN и сетевых устройств и прочего, что не поддерживает LDAP )

egor

  • Администратор
  • Старожил
  • *****
  • Сообщений: 486
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #3 : 02 Ноябрь 2022, 20:45:14 »
Пока не могу взять в толк, зачем Вам нужен OpenLDAP в качестве proxy? Если php-скрипт берёт что-то из AD, значит он, как минимум, подключается к AD и проходит там аутентификацию от имени какой-то учётки. А значит могут быть 2 пути:

1. Если атрибут TelegramID находится в LDAP-записи пользователя в AD, то логично в php-скрипте сразу проходить аутентификацию для подключения к AD от имени этого пользователя и извлекать атрибут. Если аутентификация при подключении к AD не выполнилась, значит был введён неверный пароль.

2. Если атрибут TelegramID находится в какой-то другой записи, к которой у пользователя нет прав доступа, и в php-скрипте подключение к AD происходит от некого "привилегированного" пользователя, то можно сделать 2 последовательных подключения к AD: сначала от имени пользователя, проверить, что аутентификацию с введённым паролем он проходит успешно (например, получить свою учётку из AD), а затем уже второй раз подключиться от "привилегированного" пользователя и выполнять то, что Вам нужно.

Надеюсь, не сильно запутанно объяснил. Не то, чтобы я против OpenLDAP-proxy, просто в любом случае это будет костыльно, скорее всего с дырами в безопасности, а в случае с database shell ещё и медленно.

К тому же, без установления привязки выяснить совпадение пары логина и пароля не получится в принципе: SimpleBind-аутентификация в LDAP основана как раз на привязке, то есть смог пользователь аутентифицироваться в каталоге, значит пароль был верный. В открытом виде пароли в каталоге не хранятся, в AD вообще невозможно прочитать атрибут с паролем.
« Последнее редактирование: 02 Ноябрь 2022, 21:52:57 от egor »

earyutin

  • Новичок
  • *
  • Сообщений: 6
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #4 : 03 Ноябрь 2022, 03:27:14 »
Доброй ночи, Егор !

Под php я имею ввиду отдельный скрипт.
К порталам, на которых должна проходить авторизация, этот скрипт отношения не имеет
Скрипт php изначально написан для отправки сообщений в телеграм чат и расположен он на машинке с Open ldap proxy

Я понимаю, о чем Вы, но ковыряться в исходном коде порталов мы не можем.

Open ldap Proxy я рассматриваю в качестве узла в DMZ и в качестве сервиса второго фактора,
который бы смог после установления привязки, не авторизуя пользователя запустить еще один процесс проверки за счет запуска php скрипта.
Скрипту же нужно передать логин, под которым сотрудник пытается войти.

Был еще вариант заюзать модуль pw-radius и настроить запуск скрипта модулем exec на freeradius, но вот команда gcc -shared -I ../../../include -Wall -g -o pw-radius.so radius.c -lradius не срабатывает, информации по модулю pw-radius найти не удалось.
Отправил письмо в поддержку, пока молчат.

Ребята с чата подсказали, что можно рассмотреть slapd-sock.
Не могу пока понять, как я могу использовать slapd-sock или slapd-shell.

egor

  • Администратор
  • Старожил
  • *****
  • Сообщений: 486
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #5 : 03 Ноябрь 2022, 20:34:32 »
Здравствуйте! В рабочие дни нереально заниматься посторонними делами, попробую найти время на выходных.

Вопросы:
1. У Вас уже есть настроенный Openldap-proxy? Если да, пришлите конфигурацию.
2. php-скрипт принимает логин пользователя просто в качестве первого аргумента командной строки при запуске?
3. Что должен выдать php-скрипт при успешном и неуспешном завершении проверки?

Цитировать
Ребята с чата подсказали, что можно рассмотреть slapd-sock.
У ребят из чата есть рабочий пример настройки slapd на взаимодействие с сокетом и исходники сокета? Любопытно было бы посмотреть.

Егор

earyutin

  • Новичок
  • *
  • Сообщений: 6
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #6 : 05 Ноябрь 2022, 15:30:42 »

Добрый день, Егор !

Вопросы:
Цитировать
1. У Вас уже есть настроенный Openldap-proxy? Если да, пришлите конфигурацию.

cat /etc/ldap/ldap.conf
### Schema includes ###########################################################
include         /etc/ldap/schema/core.schema
include         /etc/ldap/schema/cosine.schema
include         /etc/ldap/schema/nis.schema
include         /etc/ldap/schema/inetorgperson.schema
include         /etc/ldap/schema/microsoftattributetype.schema
include         /etc/ldap/schema/microsoftattributetypestd.schema
#include         /etc/ldap/schema/microsoftobjectclass.schema

## Module paths ##############################################################
modulepath              /usr/lib/ldap
moduleload              back_ldap.la
moduleload              back_ldap
moduleload                  back_hdb.la
moduleload              back_meta
moduleload              rwm
moduleload                  pcache.la
moduleload              memberof.la
moduleload              rwm.la
moduleload              dynlist.la

# Main settings ###############################################################
pidfile                 /var/run/slapd/slapd.pid
argsfile                /var/run/slapd/slapd.args

### Database definition (Proxy to AD) #########################################
database                ldap
readonly                yes
protocol-version        3
rebind-as-user          yes
uri                     "ldap://ldap.example.ru"
suffix                  "dc=example,dc=ru"
overlay                 rwm
rwm-map                 attribute       uid     sAMAccountName
rwm-map                 attribute       mail    proxyAddresses
rebind-as-user          yes
### Logging ###################################################################
loglevel 9

Цитировать
2. php-скрипт принимает логин пользователя просто в качестве первого аргумента командной строки при запуске?
Ну да, идея была такова, что то вроде
php /var/www/html/2fa.php username или /var/www/html/2fa.sh username

<?php ?> или #!/bin/php
$username = $1;
...
Цитировать
3. Что должен выдать php-скрипт при успешном и неуспешном завершении проверки?

При успешном выполнении пользователь должен пройти авторизацию на портале, при неуспешном портал должен сообщить об ошибке авторизации

Цитировать
У ребят из чата есть рабочий пример настройки slapd на взаимодействие с сокетом и исходники сокета? Любопытно было бы посмотреть.

Задал им вопрос, пока молчат

В данный момент имею два тестовых скрипта, пока не нашел возможность запуска скрипта для дополнительной проверки авторизации писать оконченный вариант не стал.
Возможно кустарно, так как тему изучать начал относительно не давно.
Большинство кода взял из интернет

Скрипт проверки поиска в LDAP
<?php
header
('Content-type: text/html; charset=utf-8');
extension_loaded('ldap');

//$ldap_user = "cn=ldap_view,ou=it отдел,ou=users,ou=general,dc=example,dc=ru";
$ldap_user "ldap_view@example.ru";
$ldap_pass "********";
$dn "ou=general,dc=example,dc=ru";
$attrs = array("displayname""telegramid""mobile""mail");
$filter "samaccountname=ivan.petrov";
$ldapuri "ldap://ХХХ.ХХХ.ХХХ.ХХХ";

    
$ldap_con=ldap_connect($ldapuri);
    
//ldap_set_option($ldap_con, LDAP_OPT_PROTOCOL_VERSION, 3)
    //or die ("Could not set ldap protocol");

if ($ldap_con) {


    
$bind ldap_bind($ldap_con$ldap_user$ldap_pass);


    if (
$bind) {
        echo 
"LDAP-OK!\n$ldap_user";

        
//$search = ldap_search($ldap_con, $dn, $filter, $attrs)
        
$search ldap_search($ldap_con$dn$filter)
          or die (
"ldap search failed");

        
$entries ldap_get_entries($ldap_con$search);
        
//print_r($entries);
        
if ($entries["count"] > 0) {

          for (
$i=0$i<$entries["count"]; $i++) {
            echo 
"\n\n";
            echo 
"Сотрудник: ".$entries[$i]["displayname"][0]."\n";
            echo 
"ID чата телеграмм: ".$entries[$i]["telegramid"][0]."\n";
            echo 
"Phone: +".$entries[$i]["mobile"][0]."\n";
            echo 
"Email: ".$entries[$i]["mail"][0]."\n";
          }

        } else {
          echo 
"<p>No results found!</p>";
        }

        
//ldap_unbind($ad);

    
} else {
        echo 
"LDAP-NO!\n$ldap_user";
    }

}


?>

Скрипт отправки сообщения
<?php
header
('Content-Type: text/html; charset=utf-8');
// подключаемся к API
require_once("vendor/autoload.php");
$site_dir dirname(dirname(__FILE__)).'/'// корень сайта
$bot_token 'ТОКЕН БОТА'// токен вашего бота
$data file_get_contents('php://input'); // весь ввод перенаправляем в $data
$data json_decode($datatrue); // декодируем json-закодированные-текстовые данные в PHP-массив
$date date('d.m.Y H:i:s ');

    
$inline_button1 = array("text"=>"Да, это я","callback_data"=>"1");
    
$inline_button2 = array("text"=>"Нет, это не я","callback_data"=>'0');
    
$inline_keyboard = [[$inline_button1,$inline_button2]];
    
$keyboard=array("inline_keyboard"=>$inline_keyboard);
    
$replyMarkup json_encode($keyboard);


    
file_put_contents(__DIR__ '/telegram_users/users.txt'$date print_r($datatrue) . PHP_EOLFILE_APPEND LOCK_EX);
// Для отладки, добавим запись полученных декодированных данных в файл message.txt,
// который можно смотреть и понимать, что происходит при запросе к боту
// Позже, когда все будет работать закомментируйте эту строку:


// Основной код: получаем сообщение, что юзер отправил боту и
// заполняем переменные для дальнейшего использования
 
if (!empty($data['message']['text'])) {
    
$chat_id $data['message']['from']['id'];
    
$user_name $data['message']['from']['username'];
    
$first_name $data['message']['from']['first_name'];
    
$last_name $data['message']['from']['last_name'];
    
$text trim($data['message']['text']);
    
$text_array explode(" "$text);





//file_put_contents($path.'/'.$data['message']['from']['id'].'.txt', $date . print_r($data, true) . PHP_EOL, FILE_APPEND | LOCK_EX);

    //if ($text == '/help') {
        
$text_return "Привет, $first_name $last_name. \nСегодня, $date была выполнена попытка подключения к ВПН. Это Вы ?";
        
message_to_telegram($bot_token$chat_id$text_return$replyMarkup);
    
//}
    // elseif ($text == '/about') {
    //    $text_return = "verysimple_bot:Я пример самого простого бота для телеграм, написанного на простом PHP";
    //    message_to_telegram($bot_token, $chat_id, $text_return, $replyMarkup);
    //}

 
}

// функция отправки сообщени от бота в диалог с юзером
function message_to_telegram($bot_token$chat_id$text$reply_markup '')
{
    
$ch curl_init();
    
$ch_post = [
        
CURLOPT_URL => 'https://api.telegram.org/bot' $bot_token '/sendMessage',
        
CURLOPT_POST => TRUE,
        
CURLOPT_RETURNTRANSFER => TRUE,
        
CURLOPT_TIMEOUT => 10,
        
CURLOPT_POSTFIELDS => [
            
'chat_id' => $chat_id,
            
'parse_mode' => 'HTML',
            
'text' => $text,
            
'reply_markup' => $reply_markup,
        ]
    ];

    
curl_setopt_array($ch$ch_post);
    
curl_exec($ch);
}

?>

egor

  • Администратор
  • Старожил
  • *****
  • Сообщений: 486
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #7 : 07 Ноябрь 2022, 10:47:00 »
Здравствуйте! Опишу результаты проведённых на выходных экспериментов.

Давно не занимался OpenLDAP, так много нового =). Во первых, в современных версиях (2.5, 2.6) отказались от механизма slapd-shell, поэтому пришлось собрать OpenLDAP 2.4.59. Думаю, всё то же самое можно сделать и на slapd-sock, но, повторюсь, никто не знает как =) .

Во-вторых, пути к slapd, ldapadd, ldapsearch, файлам конфигурации, файлам схем, хранилищу БД, а также ip-адреса и порты в LDAP-URI я использовал нестандартные (для экспериментов от имени непривилегированного пользователя), будьте внимательны, не забудьте всё поменять и проверить. Если что, все файлы продублированы в архиве во вложении.

Собственно, что сделано. Поскольку у меня нет AD, то я запустил первый процесс slapd для его эмуляции на порту 9000. Файл конфигурации slapd_fake_ad.conf:
include /opt/openldap-2.4-lastest/etc/openldap/schema/core.schema
include /opt/openldap-2.4-lastest/etc/openldap/schema/cosine.schema
include /opt/openldap-2.4-lastest/etc/openldap/schema/nis.schema
include /opt/openldap-2.4-lastest/etc/openldap/schema/inetorgperson.schema
include /data/openldap-experiments/2022-11-05_slapd-shell/additional_attrs.schema

modulepath  /opt/openldap-2.4-lastest/libexec/openldap/
moduleload back_mdb.so


database mdb
directory /data/openldap-experiments/2022-11-05_slapd-shell/db/
suffix cn=users,dc=mydomain,dc=com
rootDN cn=Administrator,cn=users,dc=mydomain,dc=com
rootPW secret123

access to attrs=userPassword
    by self write
    by anonymous auth
    by * none

access to *
    by self write
    by * none

Пришлось добавить несколько атрибутов в отдельном файле схемы additional_attrs.schema:
attributetype ( 1.2.840.113556.1.4.221
NAME 'sAMAccountName'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX '1.3.6.1.4.1.1466.115.121.1.15'
SINGLE-VALUE )

attributetype ( 1.2.840.113556.1.2.210
NAME 'proxyAddresses'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX '1.3.6.1.4.1.1466.115.121.1.15' )

attributetype ( 1.2.840.113556.1.4.284
NAME 'telegramId'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX '1.3.6.1.4.1.1466.115.121.1.15'
SINGLE-VALUE )

Команда для запуска slapd:
/opt/openldap-2.4-lastest/libexec/slapd -u egor -g egor -h ldap://127.1:9000 -f /data/openldap-experiments/2022-11-05_slapd-shell/slapd_fake_ad.conf -d 256

Проинициализировал каталог таким файлом init.ldif:
dn: cn=Users,dc=mydomain,dc=com
objectClass: applicationProcess
cn: Users

dn: cn=Иванов Иван,cn=Users,dc=mydomain,dc=com
objectClass: inetOrgPerson
objectClass: extensibleObject
cn: Иванов Иван
sn: Иванов
displayName: Иванов Иван
userPassword: ivanovPassword
mobile: +79111111111
mail: ivan.ivanov@mydomain.com
saMAccountName: ivan.ivanov
telegramId: ivan.ivanov.telegramId


dn: cn=Петров Петр,cn=Users,dc=mydomain,dc=com
objectClass: inetOrgPerson
objectClass: extensibleObject
cn: Петров Петр
sn: Петров
displayName: Петров Петр
userPassword: petrovPassword
mobile: +79222222222
mail: petr.petrov@mydomain.com
saMAccountName: petr.petrov
telegramId: petr.petrov.telegramId

dn: cn=Сидоров Сидор,cn=Users,dc=mydomain,dc=com
objectClass: inetOrgPerson
objectClass: extensibleObject
cn: Сидоров Сидор
sn: Сидоров
displayName: Сидоров Сидор
userPassword: sidorovPassword
mobile: +79333333333
mail: sidor.sidorov@mydomain.com
saMAccountName: sidor.sidorov
telegramId: sidor.sidorov.telegramId

Команда для инициализации каталога:
/opt/openldap-2.4-lastest/bin/ldapadd -H ldap://127.1:9000 -xD cn=Administrator,cn=users,dc=mydomain,dc=com -w secret123 -f /data/openldap-experiments/2022-11-05_slapd-shell/init.ldif

В Вашем случае это всё не нужно, поскольку у Вас есть AD.

Далее идут файлы настройки slapd в режиме прокси на основе slapd-shell slapd_shell_proxy.conf:
include /opt/openldap-2.4-lastest/etc/openldap/schema/core.schema
include /opt/openldap-2.4-lastest/etc/openldap/schema/cosine.schema
include /opt/openldap-2.4-lastest/etc/openldap/schema/nis.schema
include /opt/openldap-2.4-lastest/etc/openldap/schema/inetorgperson.schema
include /data/openldap-experiments/2022-11-05_slapd-shell/additional_attrs.schema

modulepath  /opt/openldap-2.4-lastest/libexec/openldap/
moduleload back_shell.so



database  shell
suffix    cn=users,dc=mydomain,dc=com
bind      /data/openldap-experiments/2022-11-05_slapd-shell/bind.sh
search    /data/openldap-experiments/2022-11-05_slapd-shell/search.sh

Не забудьте скорректировать суффикс под свой AD. Скрипт для операции bind:
#!/bin/bash

while [ 1 ]; do
    read TAG VALUE
    if [ $? -ne 0 ]; then
        break
    fi
    case "$TAG" in
        dn:)
        FIRST_QUERY_FILTER=`echo $VALUE | cut -d ',' -f 1`
        USER_ACCOUNT_NAME=`echo $FIRST_QUERY_FILTER | cut -d '=' -f 2`
        ;;
        cred:)
        CRED=$VALUE
        ;;
        # include other parameters here
    esac
done

# Получение реального DN пользователя с аутентификацией от RootDN
FIRST_QUERY_RESULT=`/opt/openldap-2.4-lastest/bin/ldapsearch -x -LLL -H ldap://127.0.0.1:9000 -b cn=users,dc=mydomain,dc=com -D cn=Administrator,cn=users,dc=mydomain,dc=com -w secret123 $FIRST_QUERY_FILTER 1.1`

if [[ -z $FIRST_QUERY_RESULT ]]; then
  echo "RESULT"
  echo "code: 49"
  echo "info: Unknown user: $USER_ACCOUNT_NAME"
  exit 49
fi


# Получение любого атрибута от DN пользователя
USER_DN=`echo $FIRST_QUERY_RESULT | head -n 1 | perl -MMIME::Base64 -wpe 's/^([^:]+):: (.+)$/"$1: " . decode_base64($2)/e'`
USER_DN=${USER_DN:3}

SECOND_QUERY_RESULT=`/opt/openldap-2.4-lastest/bin/ldapsearch -x -LLL -H ldap://127.0.0.1:9000 -b "$USER_DN" -D "$USER_DN" -w $CRED cn`

if [[ -z $SECOND_QUERY_RESULT ]]; then
  echo "RESULT"
  echo "code: 49"
  echo "info: Wrong password for $USER_ACCOUNT_NAME"
  exit 49
fi

# Второй фактор
SECOND_FACTOR_TEST=`php /data/openldap-experiments/2022-11-05_slapd-shell/2fa.php $USER_ACCOUNT_NAME`

if [[ $SECOND_FACTOR_TEST != "OK" ]]; then
  echo "RESULT"
  echo "code: 49"
  echo "info: Second factor auth failed for $USER_ACCOUNT_NAME"
  exit 49
fi

# Успешное завершение
echo "RESULT"
echo "code: 0"
exit 0

Как Вы понимаете, это главная часть. Поскольку в AD мудрёная схема именования записей, приходится делать два запроса: один для извлечения DN записи пользователя (от имени администратора), а второй для собственно прохождения аутентификации от имени самого пользователя. Если SimpleBIND-аутентификация пройдена, выполняется проверка второго фактора вызовом php-скрипта.

php-скрипт-заглушка для эмуляции второго фактора 2fa.php:
<?php
$username 
$argv[1];

if(
$username === 'ivan.ivanov') {
  echo 
'OK';
} else if(
$username === 'petr.petrov') {
  echo 
'ER';
} else {
  
sleep(30);
  echo 
'ER';
}

Для ivan.ivanov всегда возвращается OK, для petr.petrov -- ER, для остальных эмулируется таймаут. Надеюсь, принцип понятен.

Скрипт-заглушка для операции search:
#!/bin/bash

echo "RESULT"
echo "code: 0"

exit 0

Он просто всегда возвращает успех.

Команда для запуска slapd в режиме shell-прокси на порту 9001:
/opt/openldap-2.4-lastest/libexec/slapd -u egor -g egor -h ldap://127.1:9001 -f /data/openldap-experiments/2022-11-05_slapd-shell/slapd_shell_proxy.conf -d 256

Ну а дальше проверяем аутентификацию. Тест 1, неверное имя пользователя:
$ /opt/openldap-2.4-lastest/bin/ldapsearch -H ldap://127.1:9001 -LLLxb cn=Users,dc=mydomain,dc=com -D sAMAccountName=ivan.ivanov1,cn=Users,dc=mydomain,dc=com -w ivanovPassword
ldap_bind: Invalid credentials (49)
additional info:  Unknown user: ivan.ivanov1

Тест 2, неверный пароль пользователя:
$ /opt/openldap-2.4-lastest/bin/ldapsearch -H ldap://127.1:9001 -LLLxb cn=Users,dc=mydomain,dc=com -D sAMAccountName=ivan.ivanov,cn=Users,dc=mydomain,dc=com -w ivanovPassword1
ldap_bind: Invalid credentials (49)
additional info:  Wrong password for ivan.ivanov

Тест 3, правильные имя пользователя и пароль, успешно пройденный второй фактор:
$ /opt/openldap-2.4-lastest/bin/ldapsearch -H ldap://127.1:9001 -LLLxb cn=Users,dc=mydomain,dc=com -D sAMAccountName=ivan.ivanov,cn=Users,dc=mydomain,dc=com -w ivanovPassword

Тест 4, пользователь не прошёл второй фактор:
$ /opt/openldap-2.4-lastest/bin/ldapsearch -H ldap://127.1:9001 -LLLxb cn=Users,dc=mydomain,dc=com -D sAMAccountName=petr.petrov,cn=Users,dc=mydomain,dc=com -w petrovPassword
ldap_bind: Invalid credentials (49)
additional info:  Second factor auth failed for petr.petrov

Тест 5, пользователь не прошёл второй фактор по таймауту:
$ LDAPTIMEOUT=10 /opt/openldap-2.4-lastest/bin/ldapsearch -H ldap://127.1:9001 -LLLxb cn=Users,dc=mydomain,dc=com -D sAMAccountName=sidor.sidorov,cn=Users,dc=mydomain,dc=com -w sidorovPassword
ldap_result: Timed out (-5)

Надеюсь, идея понятна, сможете подкорректировать и развить. Прокси, который Вы настраивали (slapd-ldap), не понадобился.

Егор

egor

  • Администратор
  • Старожил
  • *****
  • Сообщений: 486
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #8 : 07 Ноябрь 2022, 11:00:04 »
Да, поскольку slapd я запускаю в режиме дебага (-d 256), то использую 3 терминала: в одном крутится slapd эмуляции AD, во втором slapd-shell-proxy, а в третьем, собственно, выполняются пользовательские команды ldapadd, ldapsearch. Надеюсь, понятно.

Егор

earyutin

  • Новичок
  • *
  • Сообщений: 6
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #9 : 14 Ноябрь 2022, 17:50:55 »
Всем привет !

Егор, спасибо Вам большое, очень помогли ) !

В общем то, авторизация работает как и задумывалось, но не без проблем конечно )

В данный момент имею проблему авторизации учетных записей порталов Bitrix и pfsense.
При тестировании выходила ошибка "ldap_search_ext: Bad search filter (-7)".

Если я подключаю портал на порт 389 openldap proxy, минуя 9000 порт с backend Shell, то авторизация проходит нормально

Для работы авторизации на портале Bitrix был дописан файл search.sh и добавлены условия, при которых удаляются лишние символы из поисковой строки, так как в строке filter было замечены лишние символы, которые генерирует портал. С pfsense пока что не удалось решить. На первый взгляд, данные в строке filter указаны корректно.

#!/bin/bash

while [ 1 ]; do
    read TAG VALUE
    if [ $? -ne 0 ]; then
        break
    fi
    case "$TAG" in
        base:)
        BASE=$VALUE
        ;;
      filter:)
FILTER=$VALUE
        ;;
        #include other parameters here
    esac
done


if [[ "$FILTER " == *"sAMAccountName"* ]]; then
 
FILTER_NEW=`echo $FILTER | sed 's/(&(?/(/g' | sed 's/user))/user)/g' | sed 's/?//g'`
echo "It's there - sAMAccountName !!!" $FILTER_NEW >> "/home/test/FILTER.txt"

elif [[ "$FILTER " == *"(|(ou=*)(cn=users))"*  ]]; then

FILTER_NEW=$FILTER
echo "It's there - (|(ou=*)(cn=users)) !!!" $FILTER_NEW >> "/home/test/FILTER.txt"

else

FILTER_NEW=`echo $FILTER | sed 's/(&(?/(/g' | sed 's/))/)/g' | sed 's/?//g'`

echo "It's NOT there - sAMAccountName !!!" $FILTER_NEW >> "/home/test/FILTER.txt"

fi

# Search
/usr/bin/ldapsearch -x -LLL -H ldap://127.0.0.1 -b dc=example,dc=ru -D "cn=ldap_view,ou=IT отдел,ou=Users,ou=General,dc=example,dc=ru" -w "Secret123" $FILTER_NEW

echo "RESULT"
echo "code: 0"

exit 0

Файл bind.sh почти не трогал, единственное сделал вывод в файл всех переменных, чтобы понимать, что внутри. Переменную $FIRST_QUERY_FILTER заключил в кавычки при задании переменной FIRST_QUERY_RESULT, так как в случае портала Bitrix передаются данные с пробелом. В моем случае учетная запись "cn=Тест Тестов 1" и выполнение скрипта рушится.

#!/bin/bash

while [ 1 ]; do
    read TAG VALUE
    if [ $? -ne 0 ]; then
        break
    fi
    case "$TAG" in
        dn:)
        FIRST_QUERY_FILTER=`echo $VALUE | cut -d ',' -f 1`
        USER_ACCOUNT_NAME=`echo $FIRST_QUERY_FILTER | cut -d '=' -f 2`
        ;;
        cred:)
        CRED=$VALUE
        ;;
        # include other parameters here
    esac
done


# START Тест - вывод значения переменных в файл
echo "FIRST_QUERY_FILTER - $FIRST_QUERY_FILTER" | perl -MMIME::Base64 -MEncode=decode -n -00 -e 's/\n +//g;s/(?<=:: )(\S+)/decode("UTF-8",decode_base64($1))/eg;print' >> /home/test/2fa.php.txt
echo "USER_ACCOUNT_NAME - $USER_ACCOUNT_NAME" | perl -MMIME::Base64 -MEncode=decode -n -00 -e 's/\n +//g;s/(?<=:: )(\S+)/decode("UTF-8",decode_base64($1))/eg;print' >> /home/test/2fa.php.txt
echo "FIRST_QUERY_FILTER_NEW_NEW - $FIRST_QUERY_FILTER_NEW_NEW" | perl -MMIME::Base64 -MEncode=decode -n -00 -e 's/\n +//g;s/(?<=:: )(\S+)/decode("UTF-8",decode_base64($1))/eg;print' >> /home/test/2fa.php.txt
# END Тест - вывод значения переменных в файл

# Получение реального DN пользователя с аутентификацией от RootDN
FIRST_QUERY_RESULT=`/usr/bin/ldapsearch -x -LLL -H ldap://127.0.0.1 -b ou=General,dc=example,dc=ru -D "cn=ldap_view,ou=IT отдел,ou=Users,ou=General,dc=example,dc=ru" -w Secret123 "$FIRST_QUERY_FILTER" 1.1`
# START Тест - вывод значения переменных в файл
echo "FIRST_QUERY_RESULT - $FIRST_QUERY_RESULT" | perl -MMIME::Base64 -MEncode=decode -n -00 -e 's/\n +//g;s/(?<=:: )(\S+)/decode("UTF-8",decode_base64($1))/eg;print' >> /home/test/2fa.php.txt
# END Тест - вывод значения переменных в файл

if [[ -z $FIRST_QUERY_RESULT ]]; then
  echo "RESULT"
  echo "code: 49"
  echo "info: Unknown user: $USER_ACCOUNT_NAME"
  exit 49
fi


# Получение любого атрибута от DN пользователя
USER_DN=`echo $FIRST_QUERY_RESULT | head -n 1 | perl -MMIME::Base64 -wpe 's/^([^:]+):: (.+)$/"$1: " . decode_base64($2)/e'`
USER_DN=${USER_DN:3}

SECOND_QUERY_RESULT=`/usr/bin/ldapsearch -x -LLL -H ldap://127.0.0.1 -b "$USER_DN" -D "$USER_DN" -w $CRED cn`
#SECOND_QUERY_RESULT=`/usr/bin/ldapsearch -x -LLL -H ldap://127.0.0.1 -b ou=General,dc=example,dc=ru -D "$USER_DN" -w $CRED cn`

# START Тест - вывод значения переменных в файл
echo "USER_DN - $USER_DN\n" | perl -MMIME::Base64 -MEncode=decode -n -00 -e 's/\n +//g;s/(?<=:: )(\S+)/decode("UTF-8",decode_base64($1))/eg;print' >> /home/test/2fa.php.txt
echo "SECOND_QUERY_RESULT - $SECOND_QUERY_RESULT" | perl -MMIME::Base64 -MEncode=decode -n -00 -e 's/\n +//g;s/(?<=:: )(\S+)/decode("UTF-8",decode_base64($1))/eg;print' >> /home/test/2fa.php.txt
# END Тест - вывод значения переменных в файл

if [[ -z $SECOND_QUERY_RESULT ]]; then
  echo "RESULT"
  echo "code: 49"
  echo "info: Wrong password for $USER_ACCOUNT_NAME"
  exit 49
fi
VARUSER_DN=`echo $USER_DN | perl -MMIME::Base64 -MEncode=decode -n -00 -e 's/\n +//g;s/(?<=:: )(\S+)/decode("UTF-8",decode_base64($1))/eg;print'`
# Второй фактор
SECOND_FACTOR_TEST=`/usr/bin/php /var/www/html/php_ldap_test_2.php "$USER_ACCOUNT_NAME" "$VARUSER_DN"`

# START Тест - вывод значения переменных в файл
echo "SECOND_FACTOR_TEST - $SECOND_FACTOR_TEST" | perl -MMIME::Base64 -MEncode=decode -n -00 -e 's/\n +//g;s/(?<=:: )(\S+)/decode("UTF-8",decode_base64($1))/eg;print' >> /home/test/2fa.php.txt
# END Тест - вывод значения переменных в файл

echo "*****************************************************" >> /home/test/2fa.php.txt
if [[ $SECOND_FACTOR_TEST != "OK" ]]; then
  echo "RESULT"
  echo "code: 49"
  echo "info: Second factor auth failed for $USER_ACCOUNT_NAME"
  exit 49
fi

# Успешное завершение
echo "RESULT"
echo "code: 0"
exit 0



Вот в таком виде пока что собран файл php

<?php
header
(&#39;Content-type: text/html; charset=utf-8&#39;);
require("/var/www/html/inc/ldap_functions.php");

$ldap_user "cn=ldap_view,ou=it отдел,ou=users,ou=general,dc=example,dc=ru";
$user $argv[1];
$userdn $argv[2];
$password = &#39;Secret123&#39;;
$ldapuri = &#39;ldap://192.168.210.254&#39;;
$basedn = &#39;ou=General,dc=example,dc=ru&#39;;
//$filter = "sAMAccountName={$user}";
$filter "cn=".$user;
$group_2fa = &#39;a-s-2fa_telegram&#39;;
$group_2fa_service = &#39;a-s-2fa_services&#39;;
$startTime time();
$timeout 61;

file_put_contents("/home/test/var_user.txt"$userFILE_APPEND LOCK_EX);
file_put_contents("/home/test/var_filter.txt"$filterFILE_APPEND LOCK_EX);

// Подключение к LDAP-серверу
$ad ldap_connect($ldapuri) or die(&#39;Could not connect to LDAP server.&#39;);

// Выбор протокола подключения
ldap_set_option($adLDAP_OPT_PROTOCOL_VERSION3) or die ("Could not set ldap protocol");
//ldap_set_option($ad, LDAP_OPT_REFERRALS, 0);

// Прохождение авторизации в каталоге от сервисной учетной записи
@ldap_bind($ad,$ldap_user,$password) or die(&#39;Could not bind to AD.&#39;);

// Поиск значение TelegramID у пользователя
$search ldap_search($ad$basedn$filter) or die ("ldap search failed");

// Создание массива с найденными данными
$entr ldap_get_entries($ad$search);
// Определение полного пути для пользователя
//$userdn = getDN($ad, $user, $basedn);

file_put_contents("/home/test/var_userdn.txt"$userdnFILE_APPEND LOCK_EX);

// Авторизовать сервисные учетные записи без требования второго фактора
if (checkGroupEx($ad$userdngetDN($ad$group_2fa_service$basedn)))
{
echo "OK";

// Проверка вхождения пользователя в группу проверки второго фактора
} else if (checkGroupEx($ad$userdngetDN($ad$group_2fa$basedn))) {

  if (
$entr["count"] > 0) {
    
    
$out_array print_r($entr,true);
    
file_put_contents("/home/test/var_entr.txt"$out_arrayFILE_APPEND LOCK_EX);

    
// Задание переменной со значением TelegramID
$user_chat_id $entr[0]["telegramid"][0];
$FirstName $entr[0]["givenname"][0];
$LastName $entr[0]["sn"][0];
//$teluser = "test";
   
file_put_contents("/home/test/var_FirstName.txt"$FirstNameFILE_APPEND LOCK_EX);
file_put_contents("/home/test/var_LastName.txt"$LastNameFILE_APPEND LOCK_EX);
file_put_contents("/home/test/var_user_chat_id.txt"$user_chat_idFILE_APPEND LOCK_EX);
      
      if (
$user_chat_id != NULL) {
        
$callback_data_file "/home/test/{$user_chat_id}.txt";
        if (
file_exists($callback_data_file)) {unlink($callback_data_file);}

        
// Отправка сообщения в телеграм чат 
exec("/usr/bin/php -f /var/www/html/SendMessage_authbot.php {$FirstName} {$LastName} {$user_chat_id} > /home/test/exec_php.txt 2>&1 &");
        
        while (
true) {
          
          
// Обработка ответа в чате
$callback_data file_get_contents($callback_data_file);
          
          if ((
$callback_data != NULL) or (time() > $startTime $timeout)) {
        
break;
    

        }

        if (
$callback_data != "YES") {echo "ERR";} else {echo "OK";}
      }
  }

} else {echo 
"ER";}

//unlink("/home/test/{$user_chat_id}.txt");

ldap_unbind($ad);

?>


Позднее, как приведу скрипты в нормальный вид, могу скинуть сюда. Может кому то пригодится.

Конфиг ldap.conf, работает на порту 389. На нем настроен Proxy в MS AD

cat /etc/ldap/ldap.conf
### Schema includes ###########################################################
include         /etc/ldap/schema/core.schema
include         /etc/ldap/schema/cosine.schema
include         /etc/ldap/schema/nis.schema
include         /etc/ldap/schema/inetorgperson.schema
include         /etc/ldap/schema/microsoftattributetype.schema
include         /etc/ldap/schema/microsoftattributetypestd.schema
#include         /etc/ldap/schema/microsoftobjectclass.schema
#include         /etc/ldap/schema/additional_attrs.schema

## Module paths ##############################################################
modulepath              /usr/lib/ldap
moduleload              back_ldap.la
moduleload              rwm.la

# Main settings ###############################################################
pidfile                 /var/run/slapd/slapd.pid
argsfile                /var/run/slapd/slapd.args

### Database definition (Proxy to AD) #########################################
database                ldap
readonly                yes
protocol-version        3
rebind-as-user          yes
uri                     "ldap://ldap.example.ru"
suffix                  "dc=example,dc=ru"
overlay                 rwm
rwm-map                 attribute       uid     sAMAccountName
rwm-map                 attribute       mail    proxyAddresses
rwm-map                 objectClass     posixGroup      group
rwm-map                 objectClass     posixAccount    person
rwm-map                 objectClass     memberUid       member
rebind-as-user          yes

#rootDN         cn=ldap_view,ou=IT отдел,ou=Users,ou=General,dc=example,dc=ru
#rootPW         Secret123

access to attrs=userPassword
    by self write
    by anonymous auth
    by * none

access to *
    by self write
    by * none

### Logging ###################################################################
loglevel 256

Конфиг ldap_2fa.cong, работает на порту 9000

cat /etc/ldap/ldap_2fa.conf
include         /etc/ldap/schema/core.schema
include         /etc/ldap/schema/cosine.schema
include         /etc/ldap/schema/nis.schema
include         /etc/ldap/schema/inetorgperson.schema
include         /etc/ldap/schema/additional_attrs.schema

modulepath  /usr/lib/ldap
moduleload      back_shell.so

database  shell
suffix    "dc=example,dc=ru"
bind      /etc/ldap/shell/bind.sh
search    /etc/ldap/shell/search.sh

Егор, атрибуты написанные Вами внес в файлик /etc/ldap/schema/microsoftattributetype.schema.
Используемые схемы прикрепляю во вложении.

Пока не понял/не дошли руки понять, для чего используется следующее:
rootDN         cn=ldap_view,ou=IT отдел,ou=Users,ou=General,dc=example,dc=ru
rootPW         Secret123

Егор, возможно ли дописать схемы или правила, чтобы запросы прилетающие на порт :9000 корректно и одинаково обрабатывались в файлике search.sh для всех используемых порталов ?
Следующим сообщением выложу debug сообщения

Спасибо ! 

earyutin

  • Новичок
  • *
  • Сообщений: 6
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #10 : 14 Ноябрь 2022, 17:54:37 »
Егор, во вложении debug сообщения для двух порталов

В случае с pfsense я могу из админки просмотреть каталог Active Directory, но вот пройти тест авторизации учетки не могу. Вероятно, не срабатывает filter на этапе проверки принадлежности учетной записи группе a-s-ovpn_users

egor

  • Администратор
  • Старожил
  • *****
  • Сообщений: 486
    • Просмотр профиля
Re: OpenLDAP - Второй фактор
« Ответ #11 : 15 Ноябрь 2022, 10:11:26 »
Здравствуйте!

Наличие вопросительных знаков в фильтрах -- признак того, что OpenLDAP не может найти что-то (атрибут, объектный класс) в своей схеме данных. Нужно прописать в схему данных все недостающие атрибуты и объектные классы, тогда не нужно будет городить костыли с sed.

Наличие в логах кучи мусорных строк типа
UNKNOWN attributeDescription "PRIMARYGROUPID" inserted.говорит о том же. Если Вам для аутентификации не нужны все эти атрибуты (подозреваю, что это так), то нужно перестроить LDAP-запрос, чтобы он их не просил у LDAP-сервера.

Я никак не могу взять в толк, зачем Вы упорно используете прокси на slapd-ldap? У Вас же и так есть AD. Получается, что между клиентом (Bitrix) и источником данных (AD) у Вас две прослойки, и обе -- сплошные костыли, а значит в два раза больше источников потенциальных ошибок. Предлагаю совсем отказать от прокси на slapd-ldap, отладить аутентификацию Bitrix сначала без второго фактора непосредственно в AD, а потом уже цеплять к AD прокси на slapd-shell для реализации второго фактора.

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

По поводу rootDN и rootPw -- это учётка с правами суперпользователя на уровне каталога (то есть того фрагмента настроек, которые следуют за директивой database и корень которого определяется директивой suffix). В этом каталоге у суперпользователя полные права, все накладываемые ограничения (явные и дефолтные), в том числе ACL, лимиты и прочее, игнорируются.

Егор