📜 ⬆️ ⬇️

Automatic password management in Active Directory

Once I got tired of it all ...
Probably, in most cases, the creativity of system administrators begins with this phrase. As a result, we see (although, more correctly, we don’t even notice) the emergence of many small programs that perform their exact and well-defined tasks in one large system.

A similar story happened to me (and it happens regularly). Not to say that I invented something new and outstanding. Rather, on the contrary, he took advantage of the work of his colleagues found in the Internet and in the storehouses of the wisdom of Habr. But I managed to combine them to solve a very specific and quite interesting task. Next, I will describe a specific solution to the specific task of managing user passwords in Active Directory. More precisely, the automation of checking the expiration of these passwords and the generation of new passwords. As a gratitude to my colleagues, I found it necessary to publish this decision here, in the hope that it will be useful to someone or serve as a source of new ideas.

So, there is a certain organization with a powerful and extensive branch network. There are a lot of branches all over our immense Motherland and they are all different-sized. Most of them are included in the corporate network with a domain structure, but many are connected according to the home-office principle. In addition, many employees are constantly on long trips without the ability to connect to a domain network and to the Internet in general.

As a result, the problem of expired passwords often occurs. The company's policy prohibits perpetual passwords, and the requirements with stringent passwords are rather severe, which makes it difficult for users to invent and replace them. Accordingly, they happily put their headache on IT support, calling and demanding to change their already invalid password. Regularly. I'm tired.
')
So what did I want to do? I need a tool that:
• itself checked the expiration of the user's password;
• previously warned him about the date of the password change by e-mail;
• offered the user a new password option;
• if the user did not have time to change the password, automatically replaced it with a new one;
• Notify the user about the new password via SMS.

The interest was to solve this problem with the best available means, without attracting third-party services and services. Well, there was no desire to choose tariffs and packages. But there was a free GSM modem. And Almighty PowerShell.

The result is a script, or rather two scripts. Why is it so explained simply - it happened historically. The fact is that password checking is performed by a script on a virtual machine located in one branch office, and another machine located in the opposite part of the country is sending SMS notifications. Because of the conditions of the mobile operator, otherwise it was unprofitable.

Further I cite both scripts entirely, which I commented as much as possible. They look a little curly. I didn’t have a particular need to comb them, because they work well and in this form:

#    ,     , #      email, #   ,     . # #   . $dt=Get-Date -Format "dd-MM-yyyy" $setupFolder = "c:\Active_Directory\Log" New-Item -ItemType directory -Path $setupFolder -Force | out-null #    $global:logfilename="C:\Active_Directory\Log\"+$dt+"_LOG.log" [int]$global:errorcount=0 #   [int]$global:warningcount=0 #   function global:Write-log #     -    . {param($message,[string]$type="info",[string]$logfile=$global:logfilename,[switch]$silent) $dt=Get-Date -Format "dd.MM.yyyy HH:mm:ss" $msg=$dt + "`t" + $type + "`t" + $message #: 01.01.2001 01:01:01 [tab] error [tab]  Out-File -FilePath $logfile -InputObject $msg -Append -encoding unicode if (-not $silent.IsPresent) { switch ( $type.toLower() ) { "error" { $global:errorcount++ write-host $msg -ForegroundColor red } "warning" { $global:warningcount++ write-host $msg -ForegroundColor yellow } "completed" { write-host $msg -ForegroundColor green } "info" { write-host $msg } default { write-host $msg } } } } #    function global:Get-RandomPassword { <#    PasswordLength -   #> [CmdletBinding()] param( [Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)] [ValidateRange(4,15)] [Int] $PasswordLength ) Begin{} Process{ $numberchars=0..9 | % {$_.ToString()} $lochars = [char]'a' .. [char]'z' | % {[char]$_} $hichars = [char]'A' .. [char]'Z' | % {[char]$_} $punctchars = [char[]](33..47) $PasswordArray = Get-Random -InputObject @($hichars + $lochars + $numberchars + $punctchars) -Count $PasswordLength $char1 = Get-Random -InputObject $hichars $char2 = Get-Random -InputObject $lochars $char3 = Get-Random -InputObject $numberchars $char4 = Get-Random -InputObject $punctchars $RndIndexArray = Get-Random (0..($PasswordLength-1)) -Count 4 $PasswordArray[$RndIndexArray[0]] = $char1 $PasswordArray[$RndIndexArray[1]] = $char2 $PasswordArray[$RndIndexArray[2]] = $char3 $PasswordArray[$RndIndexArray[3]] = $char4 return [system.string]::Join('', $PasswordArray) } End{} } #SMTP    $smtpServer = "mail.domain.local" #   $msg = new-object Net.Mail.MailMessage $msgr = new-object Net.Mail.MailMessage #    $smtp = new-object Net.Mail.SmtpClient($smtpServer) #     Function EmailStructure($to,$expiryDate,$upn) { $msg.IsBodyHtml = $true $msg.From = "ITHelpDesk@domain.local" $msg.To.Clear() $msg.To.Add($to) $msg.Subject = "Password expiration notice" $msg.Body = "<html><body><font face='Arial'>This is an automatically generated message from Company IT Service.<br><br> <b>Please note that the password for your account <i><u>domain\$upn</u></i> will expire on $expiryDate.</b><br><br> System automatically generated a new password for you. <br> You can use password - <b>$generated_password</b><br> Please change your password immediately or at least before this date as you will be unable to access the service without contacting your administrator.<br> If you will not change your password, System set it automatically.<br> </font></body></html>"} #     Function EmailStructureReport($to) { $msgr.IsBodyHtml = $true $msgr.From = "PasswordChecker@domain.local" $msgr.To.Add($to) $msgr.Subject = "Script running report" $msgr.Body = "<html><body><font face='Arial'><b>This is a daily report.<br> <br>Script for check expiried passwords has successfully completed its work. <br>$NotificationCounter users have recieved notifications:<br> <br>$ListOfAccounts<br><br></b></font></body></html>"} #      Active Directory Import-Module activedirectory #      ,       $NotificationCounter = 0 $OU = "OU=Russia,DC=local,DC=domain" $ADAccounts = Get-ADUser -LDAPFilter "(objectClass=user)" -searchbase $OU -properties PasswordExpired, employeeNumber, PasswordNeverExpires, PasswordLastSet, Mail, mobile, Enabled | Where-object {$_.Enabled -eq $true -and $_.PasswordNeverExpires -eq $false} #    foreach ($ADAccount in $ADAccounts) #    { $accountFGPP = Get-ADUserResultantPasswordPolicy $ADAccount if ($accountFGPP -ne $null) { $maxPasswordAgeTimeSpan = $accountFGPP.MaxPasswordAge } else { $maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge } #    $samAccountName = $ADAccount.samAccountName $userEmailAddress = $ADAccount.mail $userPrincipalName = $ADAccount.UserPrincipalName $userStorePassword = $ADAccount.employeeNumber $usermobile = $ADAccount.mobile #    ,     if ($ADAccount.PasswordExpired) { #      AD #     ,     - Pa$$w0rd if ($userStorePassword -eq $NULL -or $useStorePassword -eq " ") { $userStorePassword = "Pa$$w0rd" } #     $newpwd = ConvertTo-SecureString -String $userStorePassword -AsPlainText –Force Set-ADAccountPassword -Identity $samAccountName -NewPassword $newpwd –Reset #         TXT  if ($usermobile -ne $NULL) { $SMSfile="C:\ActiveDirectory\SMS_notice.txt" $SMSMessage=$usermobile + "," + $userStorePassword Out-File -FilePath $SMSfile -InputObject $SMSMessage -Append -encoding unicode } #     write-log "for $samAccountName will set a stored password - $userStorePassword. Message send to mobile - $usermobile" write-log "---------------------------------------------------------------------------------------------------------" #    AD Set-ADUser $samAccountName -employeeNumber $null } else #   ,     ,   $DaysToExpireDD  2 { $ExpiryDate = $ADAccount.PasswordLastSet + $maxPasswordAgeTimeSpan $TodaysDate = Get-Date $DaysToExpire = $ExpiryDate - $TodaysDate #     DaysToExpireDD    $DaysToExpireDD = $DaysToExpire.ToString() -Split ("\S{17}$") if (($DaysToExpire.Days -le 2)) { Write-log "The password for account $samAccountName expires on: $ExpiryDate. Days left: $DaysToExpireDD #      $generated_password $generated_password = Get-RandomPassword 10 write-log "Generated password: $samAccountName - $generated_password" write-log "-----------------------------------------------------------------------------------------" #      e AD.    employeeNumber Set-ADUser $samAccountName -employeeNumber $generated_password #      if ($userEmailAddress) #      . { EmailStructure $userEmailAddress $expiryDate $samAccountName $smtp.Send($msg) write-log "NOTIFICATION - $samAccountName :: e-mail was sent to $userEmailAddress" $NotificationCounter = $NotificationCounter + 1 $ListOfAccounts = $ListOfAccounts + $samAccountName + " - $DaysToExpireDD days left. Sent to $userEmailAddress<br>" } } } } #     ,    SMS #    If (Test-Path $SMSfile) { Copy-Item -Path $SMSfile -Destination \\SMS-Send-Server.domain.local\C$\ActiveDirectory\SMS_notice.txt #       Remove-Item $SMSfile } #     Write-log "SENDING REPORT TO IT DEPARTMENT" EmailStructureReport("ITHelpdesk@domain.local") $smtp.Send($msgr) 

We will add this script to the Windows Task Scheduler, setting it up for execution at the right time. For example, at night.

Unfortunately, the script checks expired passwords at the time of its execution. So if the password expires in the afternoon, it will not take it into account. But we don’t need it, because during working hours the employee can change the password on his own.

As a result, we get a list of mobile numbers of users who have a new password. We will send this list to the server to which the GSM modem is connected. And there the following script will deal with this list.

 # #            # # ,      $sms_text_filename = "SMS_notice.txt" $PathToSmsPrepareToSend = "C:\ActiveDirectory" + "\" + $sms_text_filename $dt=Get-Date -Format "dd.MM.yyyy" # ,       $of="C:\ActiveDirectory\Log\"+$dt+"_LOG.log" #     If (Test-Path $PathToSmsPrepareToSend) { $SMS = Import-Csv $PathToSmsPrepareToSend -Header mobile, newpassword #       foreach ($SM in $SMS) { # $mobileForSMS = $SM.mobile # $passwordFroSMS = $SM.newpassword # echo $mobileForSMS #    SerialPort $serialPort = new-Object System.IO.Ports.SerialPort #    ,     <# !!!!!! USB-   COM .   ,        .   GSM-   USB ,   COM  . #> $serialPort.PortName = "COM3" $serialPort.BaudRate = 115200 $serialPort.WriteTimeout = 500 $serialPort.ReadTimeout = 3000 $serialPort.DtrEnable = "true" #   # $serialPort.Open() #        #       $phoneNumber = [Regex]::replace($SM.mobile,'\s','') $textMessage = "Your new password - " + $SM.newpassword try { $serialPort.Open() } catch { #  5     Sleep -Milliseconds 500 $serialPort.Open() } If ($serialPort.IsOpen -eq $true) { #  ,     AT- $serialPort.Write("AT+CMGF=1`r`n") Sleep -Milliseconds 500 #     #       #   <CL>   $serialPort.Write("AT+CMGS=`"$phoneNumber`"`r`n") #      Sleep -Milliseconds 500 #      $serialPort.Write("$textMessage`r`n") Sleep -Milliseconds 500 #    Ctrl+Z    . $serialPort.Write($([char] 26)) # ,     Sleep -Milliseconds 500 } #   $serialPort.Close() if ($serialPort.IsOpen -eq $false) { #     $dts=Get-Date -Format "dd.MM.yyyy HH:mm:ss" $msg=$dts+" :Message "+$textMessage+" send to "+ $phoneNumber Out-File -FilePath $of -InputObject $msg -Append -encoding unicode } Sleep -Milliseconds 1000 } #      #         $newname =$dt+"_"+$sms_text_filename rename-item -path $PathToSmsPrepareToSend -newname $newname } #    Else #      { #    ,       $dts=Get-Date -Format "dd.MM.yyyy HH:mm:ss" $msg=$dts + " :No data to send SMS" Out-File -FilePath $of -InputObject $msg -Append -encoding unicode } 

Scripts tested in combat conditions and showed their best side.

I will not explain why I did this, because the task was quite specific. And the decision was quite specific.

But I will welcome any advice on how to improve or optimize the scripts.

UPD:
I got an interesting error.
Importing passwords from a text file is based on the Import-Csv function, which rightly considers the comma as a field separator.
And the password generator quite actively uses the comma as one of the special characters.
As a result, the password was set to normal length, as expected, but in the SMS the user got a “cropped” password.
The solution is simple: if you cannot use a comma, we will use an asterisk (they love it more than punctuation)
Add a line immediately after generating the password:
 $generated_password = $generated_password_comma -replace ",","*" 


An annoying trifle, unreasoned in advance ( mea culpa ), and the trust of users has shattered great.

Source: https://habr.com/ru/post/255197/


All Articles