Скрипт оповещения пользователей об окончании срока действия ADшного пароля по почте

Автор: Admin | 14.11.2019

Обоснование

Ознакомился со всеми предлагаемые в интернете скриптами об уведомлении пользователей о скором истечении срока действия пароля учетной записи в 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"

В конфиге приведены настройки для отсылки с Яндекса. Уверен, что настроить на любой другой почтовый сервис не составит труда.

Комментарии к посту “Скрипт оповещения пользователей об окончании срока действия ADшного пароля по почте

  1. Ру

    Вы бы проверили сперва свой скрипт. Строка 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.

    1. admin Автор записи

      Да и это нормально, поскольку слово mail содержит только одно поле. Если же расширяли схему, то знаете какое поле надо взять. Если же слово содержится в именах ну да, надо грепать не mail, а mail:. А так же можно дополнительно грепнуть и по символу @, чтобы наверняка. И не зачем грубить вот так сразу ;)

      1. V

        А каким образом осуществляется отправка сразу на несколько адресов и сопоставление?
        У меня почему то происходит ошибка (syntax error), если пользователей больше чем один.

  2. Ру

    Спасибо за подсказку (mail: ). От себя бы еще добавил переменные конкретной OU, бывает что лес в домене очень большой. Прошу прощения, если грубо зашел)

    1. V

      А каким образом осуществляется отправка сразу на несколько адресов и сопоставление?
      У меня почему то происходит ошибка (syntax error), если пользователей больше чем один.

      1. Ру

        Читайте мой коммент выше, не хватает «:» после слова mail. Для проверки можете вывести командой echo список адресов.

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

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