Active Directory

PowerShell: Sending password expiration notices via GMail – Part 1

In a perfect world, users would never forget their password, and never forget to change it before the expiration date. But we don't live in that perfect world. I covered how to unlock AD accounts earlier in this post, but now I'd like to talk about how to first find the users whose accounts are about to expire, and then email a warning to them. It turns out to be a fairly big script, so I'm going to break it up into a couple of posts. In this first post, I'll cover how to identify the users whose password will expire in the next n days. Then in the next post, we'll send them an email via the company's GMail account. Finally, in the third post in this series, I'll pull the whole thing together into a complete script, with comment-based help.

 

The process to find the users breaks down into several component parts:

  • Get domain credentials
  • Connect to the AD DS domain
  • Do some Date arithmatic
  • Query Active Directory for the users whose passwords will expire

The first part, getting domain credentials, you can use a simple Get-Credential if you want to be prompted every time, or store the credentials securely in a file, as I described here. Notice that I don't suggest that you should simply log in as a Domain Admin and run the script with your domain credentials - that's because I much prefer to always run as a limited user.

 

After we have those credentials, we connect to the Active Directory domain and query it for the MaxPasswordAge property of the domain.

$ADCred = Get-Credential -UserName TreyResearch\Domain.Admin `
                         -Message "Enter the domain admin's password"
$PDC = (Get-Domain -Identity 'TreyResearch.net' `
                   -Credential $ADCred).PDCEmulator
$maxPasswordAge = (Get-ADDefaultDomainPasswordPolicy `
                         -Server $PDC `
                         -Credential $ADCred).MaxPasswordAge

Next, we're going to have to do some Date arithmetic. Rarely fun, but needs must. We'll start by getting some initial values. Our final script will assume a week of warning, but we'll want to be able to change that with a DaysWarning parameter:

[CmdletBinding()]
Param([Parameter(Mandatory=$False,Position=0)]
      [int]$DaysWarning = 7)

And we'll need to know today's date, that's easy:

$today = (Get-Date)

The other bit of information we need is the number of days since the user last set their password. We query for all users (-Filter *), but then discard all the ones we don't need, storing (in $TreyUsers) only those whose password will expire between now and the DaysWarning value. We don't need to do anything with those users whose password is set to never expire, nor do we care about users whose password has already expired. They won't be able to read any emails we send them anyway. :) While we're getting a list of these users, we need to get some properties that aren't returned by default when we use Get-ADUser. We'll use the -Properties parameter to specify those.

$TreyUsers = Get-ADUser -Filter * `
                        -Server $PDC `
                        -Credential $ADCred `
                        -Properties Name,`
                                    PasswordNeverExpires,`
                                    PasswordExpired,`
                                    PasswordLastSet,`
                                    EmailAddress `
      | Where-Object {$_.Enabled -eq $True `
          -AND $_.PasswordNeverExpires -eq $False `
          -AND $_.passwordexpired -eq $False `
          -AND $_.EMailAddress `
          -AND (($today-$_.PasswordLastSet).Days -ge ($MaxPasswordAge.Days-$DaysWarning))
      }

Whew, that looks a right mess. But it's not as bad as it looks. I've tried to make it as efficient as I could, taking advantage of PowerShell's 'short-cut' processing. Yes, I have to query AD DS for all the users, but I quickly stop processing the user if their account is disabled, their password never expires, or their password has already expired. That gets us down to only those users who are actually active. On those, we do a bit of date math. This is slightly complicated by the PasswordLastSet and MaxPasswordAge properties which return an object with more information than we need or want. All we really want is the Days property of those values.

 

The math here is a bit convoluted, so let's work through it. First ($today-$_.PasswordLastSet) is the number of days since the user set their password. The users we want to send an email to are those for whom that is greater than, or equal to,  MaxPasswordAge-DaysWarning.

 

How does that work? Let's assume we have a policy that says you need to change your password at least every 90 days, and I want to start warning users a week ahead of time.  Therefore, MaxPasswordAge-DaysWarning is equal to 83 days. So we only want to send warnings to those users who set their password more than 83 days ago.

 

Next time, we'll send each of those users an email, warning them that they need to change their password.

Importing users into Active Directory

When you need to create a single user in Active Directory Domain Services (AD DS), the tendency is to just "do it" in the GUI - either Active Directory Users and Computers (ADUC) or Active Directory Administrative Center (ADAC). But if you've got 25 users to add, or even 5 users to add, that's just painful. Plus, having this scripted ensures that each user is correctly entered following the same format.

For me, the easiest way to do this is by putting the users in a spreadsheet and then leveraging PowerShell's Import-CSV command. So, for the sake of argument, let's assume I have 8 new users to add to my TreyResearch.net domain. I open Excel, and create a spreadsheet with the following information for each user: Name, Given Name, Surname, Display Name, SamAccountName, and Description. The spreadsheet would look like this:blog_01

We'll save that as "TreyUsers.csv", and ignore any warnings about formatting, etc. This gives us a file with:

Name,GivenName,Surname,DisplayName,SAMAccountName,Description
David Guy,David,Guy,Dave R. Guy,Dave,Customer Appreciation Manager
Alfredo Fettucine,Alfredo,Fettuccine,Alfie NoNose,Alfie,Shop Foreman
Stanley Behr,Stanley,Behr,Stanley T. Behr, Stanley,Webmaster
Priscilla Catz,Priscilla,Catz,Dame Priscilla,Priscilla,Shop Steward
Harold Catz,Harold,Catz,Harold S. Catz,Harold,Engineering Manager
William Wallace,William,Wallace,Sir William Wallace,Wally,Marketing Manager
Trey Barksdale,Trey,Barksdale,Lord Barksalot,Trey,Sales Manager
Charlie Derby,Charles,Derby,Sparky,Sparky,Chief Security Officer

Now, we use PowerShell's Import-CSV to import this set of users into a variable $TreyUsers.

$TreyUsers = Import-CSV TreyUsers.csv

To create the users, we'll use a simple ForEach loop:

foreach ($user in $TreyUsers ) {
   New-ADUser -DisplayName $User.DisplayName `
              -Description $user.Description `
              -GivenName $user.GivenName `
              -Name $User.Name `
              -SurName $User.SurName `
              -SAMAccountName $User.SAMAccountName `
              -Enabled $True `
              -ChangePasswordAtLogon $True `
              -PasswordNeverExpires $False `
              -UserPrincipalName $user.SAMAccountName `
              -AccountPassword (ConvertTo-SecureString `
                   -AsPlainText `
                   -Force `
                   -String 'P@ssw0rd!' ) 2>$NULL
}

(If you find that plain text password a bit problematic, use a Read-Line -AsSecureString early in the script to prompt you for an initial password. )

Finally, if you want to copy the security groups of an existing template user, stay tuned -- I'll cover that shortly

 

ETA: To correct ill-advised use of double-quotes in ConvertTo-SecureString.

Starting a PowerShell window as a Domain Admin

If you run as a limited user on your own desktop, as you should, it's useful to keep a separate PowerShell window open as the Domain Administrator. I give that window a nice dark red background so I know instantly that I'm in a powerful window and to be appropriately careful. But how can I actually start a window as a different account, and using Run As Administrator? Ah, so glad you asked.

$AdminCred = Get-Credential -UserName "TreyResearch\domainadmin" `
                            -Message "Enter your password for the DomainAdmin account:" 
Start-Process PowerShell.exe -Credential $AdminCred `
                             -ArgumentList "Start-Process PowerShell.exe -Verb RunAs" `
                             -NoNewWindow

Save this as "Start-myAdmin.ps1" or equivalent and it's always available. The nice thing about using Start-Process with the NoNewWindow parameter is that it doesn't leave a spare window open. Try it. It will make it just that much easier to run as a limited user with no administrative rights, even on your own workstation. And really, when you have the power to be a domain admin, you really, really, really shouldn't run any other way.

Active Directory — Unlocking a User Account with PowerShell

As any SysAdmin knows, users periodically lock themselves out of their accounts, usually because they forgot a password or somehow mistyped it too many times. And after all, if it failed once, why not keep trying it? Unlocking that account is NOT something you do with Set-ADUser, unfortunately, because the PowerShell ActiveDirectory module has a special, single-purpose cmdlet - Unlock-ADAccount. Now, it doesn't take a whole script to run a single cmdlet, but sometimes there are good reasons to wrap a command in a more complicated script, and this is one I've had to. Primarily because I work on at least three different domains that are unrelated - my home domain, my "work" domain, and my testlab domain. With this script, I can also take advantage of an encrypted password for one of those domains, stored securely on my main desktop's hard disk. Plus, I've added in a bit of code to go grab the current holder of the FSMO PDCEmulator role, since I'd prefer to unlock the account directly against that PDC.

Also, to make life easier, and allow us to stick this in the middle of a pipeline when we need to, we'll add the ability to handle parameters from the pipeline. We do that in the Param section:

[CmdletBinding()]
Param(
     [Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)]
     [alias("user","account")]
     [string[]]
     $Identity,
     [Parameter(Mandatory=$False,ValueFromPipeline=$True,Position=1)]
     [string]
     $Domain = "DOMAIN",
     [Parameter(Mandatory=$False,ValueFromPipeline=$True)]
     [PSCredential]
     $Credential = $NULL
     )

You'll notice here that we've allowed for each of our parameters to accept pipeline input. And they'll accept that "ByValue", meaning that the piped objects must have the same .NET type, or must be able to be converted to that type. So only an actual PSCredential object can be accepted for the -Credential parameter.

 

Next, we want to be able to handle credentials in any of several ways. So we use:

if ( $Credential ) {
   $myCred = $Credential
} else {
   # Test if there is a stored, encrypted, password we can use
   $pwPath = "$home\Documents\WindowsPowerShell\$domainPW.txt"
   if (Test-Path $pwPath ) {
      $mypw = Get-Content $pwPath | ConvertTo-SecureString
      $myCred = New-Object -TypeName System.Management.Automation.PSCredential `
                           -ArgumentList "$DOMAIN\Charlie",$mypw
   } else {
      # Prompt for a credential, since we don't seem to have one here. 
      $myCred = Get-Credential 
   }
}

Let's look at this for a moment. First, we know from the Param section that the default value of $Credential is $NULL, so our first test is whether that's been overridden at the command line with either a pipelined parameter, or a specifically entered credential object. If so, we're good, and we use that. But failing that, we'll check if there's one stored securely on disk. (More on storing credentials on disks in another post soon.) If we've stored one on disk that matches the domain we're going against, we'll use that. If not, and we still don't have a PSCredential object we can use, we'll simply prompt for one.

 

Next up, we want to run this against the domain controller that hosts the PDCEmulator role. Not a big deal in simplified domain environment, but really a good idea in a complicated, multi-site environment where site-to-site propagation is a bit slow.

$PDC = (Get-ADDomain -Identity $DOMAIN -Credential $myCred).PDCEmulator

We'll plug the value of $PDC into the -Server parameter of our AD commands.

Finally, the meat of the whole thing. I'm not assuming you're only unlocking a single account, so I've designed this to take a list of strings ([string[]]). (A list can, of course, be a list of length one.)

foreach ($usr in $Identity) {
   Unlock-ADAccount -Identity $usr `
                    -Credential $myCred `
                    -Server $PDC `
                    -PassThru `
       | Get-ADUser -Properties LockedOut

 

That last line ensures that we report back in a way that shows the account no longer locked out. But for Get-ADUser to actually do anything, you need to include the -PassThru parameter in the Unlock-ADAccount command. Otherwise, nothing at all gets passed to the Get-ADUser command.

So, here's the whole thing:

<#
.Synopsis
Unlocks a  domain account
.Description
Unlock-myUser accepts an array of DOMAIN account names and unlocks the accounts 

This script accepts pipeline input for the credential. If no credential is supplied, 
it will attempt to use one you have stored on disk. Failing that, it will prompt 
you for credentials. 
.Example
Unlock-myUser -Identity Charlie
Unlocks the account of Charlie 
.Example
Unlock-myUser -Identity Charlie, Sharon
Unlocks the accounts of Charlie and Sharon 
.Example
Unlock-myUser -Identity Charlie -Domain TREYRESEARCH `
              -Credential (Get-Credential `
                               -username "TREYRESEARCH\Alfie"  `
                               -Message "Enter your Domain PW")

Unlocks the account of TreyResearch\Charlie, prompting Alfie for credentials. 
.Parameter Identity
The AD identity of the user or users whose acounts are to be reset.
.Parameter Domain
The Active Directory domain of the user or users whose accounts are to be reset
.Parameter Credential
The user credentials to run the script under. 
.Inputs
[string[]]
[string]
[PSCredential]
.Notes
    Author: Charlie Russel
 Copyright: 2016 by Charlie Russel
          : Permission to use is granted but attribution is appreciated
   Initial: 07 Sept, 2016 (cpr)
   ModHist: 09 Sept, 2016 (cpr) - added PDC test and Domain parameter 
          :
#>
[CmdletBinding()]
Param(
     [Parameter(Mandatory=$True,ValueFromPipeline=$true,Position=0)]
     [alias("user","account")]
     [string[]]
     $Identity,
     [Parameter(Mandatory=$False,ValueFromPipeline=$True,Position=1)]
     [string]
     $Domain = "DOMAIN",
     [Parameter(Mandatory=$False,ValueFromPipeline=$True)]
     [PSCredential]
     $Credential = $NULL
     )

if ( $Credential ) {
   $myCred = $Credential
} else {
   # Test if there is a stored, encrypted, password we can use
   $pwPath = "$home\Documents\WindowsPowerShell\$domainPW.txt"
   if (Test-Path $pwPath ) {
      $mypw = Get-Content $pwPath | ConvertTo-SecureString
      $myCred = New-Object -TypeName System.Management.Automation.PSCredential `
                           -ArgumentList "DOMAIN\Charlie",$mypw
   } else {
      # Prompt for a credential, since we don't seem to have one here. 
      $myCred = Get-Credential 
   }
}

# Find out which server holds the PDC role. 
# Useful in complicated, multi-site environments where 
# Domain changes might not propagate quickly. 

$PDC = (Get-ADDomain -Identity $DOMAIN -Credential $myCred).PDCEmulator

foreach ($usr in $Identity) {
   Unlock-ADAccount -Identity $usr `
                    -Credential $myCred `
                    -Server $PDC `
                    -PassThru `
       | Get-ADUser -Properties LockedOut
}

As always, feel free to use this script or any portion of it that you find useful. However, if you do, I'd appreciate attribution and a pointer back to my blog.

Promoting a new domain controller

I’ve been working with Windows Server 2016 CTP5 recently, and because I installed it without the Desktop Experience (what we used to call a Server Core installation), I’m having to do everything in Windows PowerShell. No complaints, I enjoy it, but it does force me to think about things a bit sometimes.

One of the tasks I needed to do was promote a new server to be a secondary domain controller. The PowerShell command for this is: Install-ADDSDomainController. But before you start promoting a new DC, it’s a really good idea to test that the promotion will succeed by using the Test-ADDSDomainControllerInstallation cmdlet. So, I combined the two steps into a simple script that allows you to run the test, and if the output looks clean, finish the installation and initiate a reboot.

The script is smart enough to realize you haven’t installed the ActiveDirectory feature yet, and goes ahead and installs it for you.

<# 
.Synopsis 
Tests a candidate domain controller, and then promotes it to DC.

.Description 
Promote-myDC first tests if a domain controller can be successfully promoted, and, 
if the user confirms that the test was successful, completes the promotion and 
restarts the new domain controller. 

.Example 
Promote-myDC -Domain TreyResearch.net

Tests if the local server can be promoted to domain controller for the 
domain TreyResearch.net. The user is prompted after the test completes 
and must press the Y key to continue the promotion. 

.Parameter Domain 
The domain to which the server will be promoted to domain controller. 

.Inputs 
[string] 

.Notes 
    Author: Charlie Russel 
 Copyright: 2016 by Charlie Russel 
          : Permission to use is granted but attribution is appreciated 
   Initial: 05/14/2016 (cpr) 
   ModHist: 
          : 
#> 
[CmdletBinding()] 
Param( 
     [Parameter(Mandatory=$True,Position=0)] 
     [string] 
     $Domain 
     )

Write-Verbose "Testing if ADDSDeployment module is available" 
If ( ! (Get-Module ADDSDeployment )) { 
   Write-Verbose "Installing the ActiveDirectory Windows Feature, since you seem to have forgotten that." 
   Install-WindowsFeature -Name ActiveDirectory -IncludeManagementTools 
   Write-Host "" 
}

If ( ! (Get-Module ADDSDeployment )) { 
   throw "Failed to install the ActiveDirectory Windows Feature." 
}

Write-Verbose "Testing if server $env:computername can be promoted to DC in the $Domain domain" 
Write-Host "" 
Test-ADDSDomainControllerInstallation ` 
      -NoGlobalCatalog:$false ` 
      -CreateDnsDelegation:$false ` 
      -CriticalReplicationOnly:$false ` 
      -DatabasePath "C:\Windows\NTDS" ` 
      -DomainName $Domain ` 
      -LogPath "C:\Windows\NTDS" ` 
      -NoRebootOnCompletion:$false ` 
      -SiteName "Default-First-Site-Name" ` 
      -SysvolPath "C:\Windows\SYSVOL" ` 
      -InstallDns:$true ` 
      -Force 
Write-Host "" 
Write-Host "" 
Write-Host ""

Write-Host -NoNewLine "If the above looks correct, press Y to continue...  " 
$Key = [console]::ReadKey($true) 
$sKey = $key.key

Write-Verbose "The $sKey key was pressed." 
Write-Host "" 
Write-Host "" 
If ( $sKey -eq "Y" ) { 
   Write-Host "The $sKey key was pressed, so proceeding with promotion of $env:computername to domain controller." 
   Write-Host "" 
   sleep 5 
   Install-ADDSDomainController ` 
      -SkipPreChecks ` 
      -NoGlobalCatalog:$false ` 
      -CreateDnsDelegation:$false ` 
      -CriticalReplicationOnly:$false ` 
      -DatabasePath "C:\Windows\NTDS" ` 
      -DomainName $Domain ` 
      -InstallDns:$true ` 
      -LogPath "C:\Windows\NTDS" ` 
      -NoRebootOnCompletion:$false ` 
      -SiteName "Default-First-Site-Name" ` 
      -SysvolPath "C:\Windows\SYSVOL" ` 
      -Force:$true 
} else { 
   Write-Host "The $sKey key was pressed, exiting to allow you to fix the problem." 
   Write-Host "" 
   Write-Host "" 
}

	                    
	                

Shutting Down Running VMs

A quick Hyper-V PowerShell one-liner today. This one will gracefully stop all virtual machines with RODC in their name.

 Get-VM -Name *rodc* | Where-Object {$_.State -eq "Running" } | Foreach-Object { Stop-VM $_.Name }

I needed this because I was cloning an RODC that I had virtualized, and I wanted a quick way to shut it down gracefully without actually logging on to it. (It's on a private subnet that is a nuisance for some things.) Then I simply extended it a bit to get all my RODC's and to make sure that I didn't try to stop a VM that was already stopped.

Charlie.

And, because I'm lazy and don't want to bother logging in to my VM host server, I modified this to get all the running VMs on the server and then stop them.

Get-VM -ComputerName Trey-VMHost `
          | Where-Object {$_.State -eq "Running" } `
          | Foreach-Object { Stop-VM $_.Name -ComputerName $_.ComputerName }

Notice that by the time I get to the end of the pipeline, I need to tell Stop-VM what computer these are running on. No problem, that's part of the object I'm passing in to it anyway. And, you could easily extend this by providing an array of host names to Get-VM to stop multiple VMs on multiple hosts.