Обоснование
Ознакомился со всеми предлагаемые в интернете скриптами об уведомлении пользователей о скором истечении срока действия пароля учетной записи в Active Directory. Они все либо через чур сложные и объемные, имея от 70 строк и более, либо недоработанные, что не могут нормально отослать почту. И плюс ко всему, сильно не хотелось разбираться в особенностях скриптования под Power Shell. Поэтому было решено писать на богоспасаемом bash'е с использованием линуксовых утилит для работы с LDAP. Это же гораздо проще, чем PS!
Правда, тут есть ограничение, нужно либо иметь в сети Linux машину, либо ставить WSL (Windows Subsystem for Linux), что делается элементарно. Скажу сразу, что на WSL скрипт не тестировался, но в теории должен работать.
Принцип работы
Каждый день по крону на Linux машине стартует скрипт, который логинится на контроллер домена и проверяет пароли пользователей на истечение их срока действия. Чекаются только активные пользователи с истекающими паролями. Если пароль истекает менее, чем через 15 дней включительно, то шлется письмо на ящик этого пользователя, указанный в AD. Письма будут отсылаться каждый день, пока пароль не будет изменен или не истечет. Когда пароль истек, то по тому же принципу отсылается письмо о просрочке.
Просто
Зависимости
Утилита ldapsearch
из набора инструментовldap-utils
для сбора информации с AD сервера, калькулятор bc
для арифметических вычислений и консольный почтовый клиент mutt
для отсылки почты. Который, кстати, можно настроить разными способами и самый распространенный (в моей голове) представлен в статье.
apt install ldap-utils bc mutt
Скрипт
Собственно, сам скрипт, вычисляющий окончание срока действия пароля ADшного пользователя и отсылающий ему по почте оповещение, если пароль скоро будет просрочен:
#!/bin/bash AD_USER='ad_user' # Имя пользователя для коннекта к AD AD_USER_PASS='pass_for_ad_user' # Пароль от пользователя ad_user DC_ADDRESS='192.168.0.1' # IP адрес или DNS имя контроллера домена SLD='example' # Второй уровень имени AD'шного домена, например домена example.com TLD='com' # Первый уровень имени AD'шного домена, например домена example.com # Получить список пользователей с истекающими паролями USERS_LIST=$(ldapsearch -h $DC_ADDRESS -D "$AD_USER@$SLD" -x -w "$AD_USER_PASS" -b "DC=$SLD,DC=$TLD" -s sub "(&(userAccountControl=512))" | grep -e sAMAccountName | cut -d " " -f 2) # Получить срок жизни пароля из групповых политик AD MAX_PWD_AGE=$(ldapsearch -s base -h $DC_ADDRESS -D "$AD_USER@$SLD" -x -w "$AD_USER_PASS" -b "DC=$SLD,DC=$TLD" maxPwdAge | grep maxPwdAge: | cut -d "-" -f 2) for USER in $USERS_LIST do # Получить время последней смены пароля пользователя PWD_LAST_SET=$(ldapsearch -h $DC_ADDRESS -D "$AD_USER@$SLD" -x -w "$AD_USER_PASS" -b "DC=$SLD,DC=$TLD" -s sub "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$USER))" | grep -e pwdLastSet | cut -d " " -f 2) # Рассчитать количество дней до истечения пароля пользователя TIME_TO_EXPIRATION=$(bc <<< "(($PWD_LAST_SET+$MAX_PWD_AGE)/10000000-11644473600-$(date +%s))/3600/24") # Получить e-mail пользователя MAIL=$(ldapsearch -h $DC_ADDRESS -D "$AD_USER@$SLD" -x -w "$AD_USER_PASS" -b "DC=$SLD,DC=$TLD" -s sub "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$USER))" | grep mail | cut -d " " -f 2) # Если количество дней до истечения пароля меньше или равно 0, то отослать письмо, что пароль истек if [ "$TIME_TO_EXPIRATION" -le "0" ] then echo "Время действия пароля для пользователя $USER истекло! Обратитесь к системному администратору для восстановления доступа." | mutt -F /root/.muttrc_pass_exp -s "Время действия пароля истекло!!!" $MAIL # Если количество дней до истечения пароля меньше или равно 15, то отослать письмо о скором истечении пароля elif [ "$TIME_TO_EXPIRATION" -le "15" ] then echo "Время действия пароля для пользователя $USER истечет через $TIME_TO_EXPIRATION дня\дней! Письма перестанут приходить когда пароль будет изменен." | mutt -F /root/.muttrc_pass_exp -s "Необходимо сменить пароль!" $MAIL fi done
15 строк, не считая переменных, комментов и шебанга. 15!
Сложно
Немного теории
Для AD в частности и LDAP в общем, время записывается в формате "Active Directory timestamps" или "Windows NT time format", или "Win32 FILETIME\SYSTEMTIME", или "NTFS file time" и представляет собой количество 100 наносекундных интервалов, начиная с полуночи 1 января 1601 года, представленная в виде 18-и значного численного значения. Такой формат был введен для удобства вычисления интервалов времени или сравнения его промежутков. Другими словами, это как Unix Time, только для Windows. Дальше буду называть просто Windows Time. Существует несколько типов представления Windows Time, в контексте статьи нужны только два - это DateTime и TimeSpan. Удовлетворять любопытство, почему именно 1 января 1601 года, можно начать отсюда.
TimeSpans
Это интервал времени, который можно представить как разность двух DateTime атрибутов. TimeSpans атрибуты имеют особенность, они могут являться как положительными, так и отрицательными. В контексте AD некоторые атрибуты всегда имеют отрицательное значение, а именно maxPwdAge (максимальный срок действия пароля), который по дефолту равен -36288000000000
. Чтобы перевести в cекунды, его требуется умножить на сто наносекунд == 100*10^-9 (100 умножить на десять в минус девятой степени). Или же разделить на 10^7, что проще, поскольку bc не умеет отрицательные степени.
Упомяну, что сто наносекундных интервалов получило название "тик" (tick). Количество тиков в человеческих единицах времени, скопированные с сайта Майкрософт:
Ticks per day 864,000,000,000 Ticks per hour 36,000,000,000 Ticks per minute 600,000,000 Ticks per second 10,000,000 Ticks per millisecond 10,000
Итак, полученное значение TimeSpans, взятое по модулю, делится на 10.000.000, чтобы перевести его в секунды, затем на количество секунд в часе и на количество часов в сутках. На выходе получается количество дней, пока все просто:
$ bc <<< "36288000000000/10000000/3600/24"
42
Забавно, что maxPwdAge равен -36288000000000
только в том случае, когда его не трогали. Если атрибут настраивался, то значение становится равным -37108517437440
и остается таким всегда, даже если вернуть все настройки в "как было". При этом, появляется два maxPwdAge, что надо учесть в скрипте для получения верного значения:
example.com dn: DC=example,DC=com maxPwdAge: -36288000000000 Builtin, example.com dn: CN=Builtin,DC=example,DC=com maxPwdAge: -37108517437440
В первом блоке или текущее значение атрибута, или последнее заданное, если в текущий момент оно не определено. Во втором дефолтное значение. Это даже логично и можно объяснить, но не буду публиковать догадки.
DateTime
Представляет собой мгновенное время и используется для вычисления даты. Например, дефолтное значение атрибута pwdLastSet (дата последней смены пароля пользователем) записывается как 132178176000000000
в Windows Time формате, что соответствует времени 10 Ноября 2019, 21:00:00
или 1573419600
в Unix Time.
132178176000000000 Windows Time 1573419600 Unix Time Sun Nov 10 21:00:00 UTC 2019 Human Time
Т.е. записанный в виде DateTime атрибут, указывает на конкретную дату и время.
Самый простой способ перевести в человеческий вид можно при помощи конвертера. Кому интересно, подробное о типах представления времени можно прочесть в документации.
Описание работы
Список всех пользователей с истекающими паролями
USERS_LIST=$(ldapsearch -h $DC_ADDRESS -D "$AD_USER@$SLD" -x -w "$AD_USER_PASS" -b "DC=$SLD,DC=$TLD" -s sub "(&(userAccountControl=512))" | grep -e sAMAccountName | cut -d " " -f 2)
userAccountControl=512
- фильтр для поиска пользователей. Значение 512 соответствует активному (не отключенному) пользователю с ограниченным сроком действия пароля. Подробно описывал в заметке про виндовые пользовательские атрибуты. Подразумевается, что sAMAccountName содержит имя пользователя, что и должно быть в норме.
Срок жизни пароля
MAX_PWD_AGE=$(ldapsearch -s base -h $DC_ADDRESS -D "$AD_USER@$SLD" -x -w "$AD_USER_PASS" -b "DC=$SLD,DC=$TLD" maxPwdAge | grep maxPwdAge: | cut -d "-" -f 2)
Запрос берет значение атрибута maxPwdAge из групповых политик домена. Для удобства вычислений берется значение по модулю, отсекая минус. Параметр -s base
говорит взять первый параметр политик, который и является нужным. Не исключено, что при использовании нескольких политик, надо брать разные значения атрибута из других мест, но для подавляющего большинства случаев, с единой политикой, этого достаточно.
Время последней смены пароля пользователем
PWD_LAST_SET=$(ldapsearch -h $DC_ADDRESS -D "$AD_USER@$SLD" -x -w "$AD_USER_PASS" -b "DC=$SLD,DC=$TLD" -s sub "(&(objectCategory=person)(objectClass=user)(sAMAccountName=$USER))" | grep -e pwdLastSet | cut -d " " -f 2)
Возвращает время последней смены пароля пользователя в Windows time формате. Пока берется как есть, в следующей команде будет переводится в удобочитаемый формат.
Время до истечения пароля пользователя
Вычисляется время в днях до истечения пароля пользователя:
TIME_TO_EXPIRATION=$(bc <<< "(($PWD_LAST_SET+$MAX_PWD_AGE)/10000000-11644473600-$(date +%s))/3600/24")
Тут есть две не совсем понятных штуки, это $(date +%s)
и 11644473600
. Поскольку скрипт выполняется в Linux среде, надо перевести формат даты из Windows time в Unix time. 11644473600
- разница между Windows time и Unix time в секундах. Т.е. -11644473600
в Unix time формате это Monday, 1 January 1601
.date +%s
- текущее время в Unix time формате.
$PWD_LAST_SET+$MAX_PWD_AGE
- дата исчения пароля в тиках. $PWD_LAST_SET+$MAX_PWD_AGE)/10000000
тоже самое, но в секундах. Вычитая 11644473600
- получается дата исчения пароля в Unix time формате. Вычитая из получившегося числа текущую дату - количество секунд до истечения пароля, которая преобразуется в количество дней.
Mutt
Самое простое. Этот почтовый клиент был выбран и-за простоты настройки и легкости смены конфигураций. А еще он умеет отсылать почту от любого почтового сервиса по имеющимся кредам. Как видно из скрипта, используется параметр -F /root/.muttrc_pass_exp
, указывающий на файл конфигов, который выглядит так:
set realname = "Пришло время сменить пароль" set from = [email protected] set use_from = yes set ssl_starttls = yes set ssl_force_tls = yes set smtp_url = 'smtps://[email protected]@smtp.yandex.com:465' set smtp_pass = "[email protected]_user"
В конфиге приведены настройки для отсылки с Яндекса. Уверен, что настроить на любой другой почтовый сервис не составит труда.
Вы бы проверили сперва свой скрипт. Строка MAIL=$(ldapsearch -h $DC_ADDRESS -D «$AD_USER@$SLD» -x -w «$AD_USER_PASS» -b «DC=$SLD,DC=$TLD» -s sub «(&(objectCategory=person)(objectClass=user)(sAMAccountName=$USER))» | grep mail | cut -d » » -f 2) — выведет все поля в которых есть намек на слово mail.
Да и это нормально, поскольку слово mail содержит только одно поле. Если же расширяли схему, то знаете какое поле надо взять. Если же слово содержится в именах ну да, надо грепать не
mail
, аmail:
. А так же можно дополнительно грепнуть и по символу@
, чтобы наверняка. И не зачем грубить вот так сразу ;)А каким образом осуществляется отправка сразу на несколько адресов и сопоставление?
У меня почему то происходит ошибка (syntax error), если пользователей больше чем один.
Спасибо за подсказку (mail: ). От себя бы еще добавил переменные конкретной OU, бывает что лес в домене очень большой. Прошу прощения, если грубо зашел)
А каким образом осуществляется отправка сразу на несколько адресов и сопоставление?
У меня почему то происходит ошибка (syntax error), если пользователей больше чем один.
Читайте мой коммент выше, не хватает «:» после слова mail. Для проверки можете вывести командой echo список адресов.