The Microsoft Management Console provides plugins for managing many aspects of Active Directory, including user accounts. The Active Directory Users and Computers MMC plugin allows you to view and manage user accounts, but there are some things you cannot discover, such as last logon time or when a user’s password will expire (if at all). Password expiration can be particularly vexing for road warriors or those who use non-Windows platforms but still rely on ADS for authentication to numerous corporate resources. Windows users have two possible means of being warned, and non-Windows users are just out of luck!
If you are connected to the domain with a Windows computer, you will be warned of an impending password expiration and be able to easily change your password. If you are remote, you can probably use a VPN client to connect to the domain before you logon to Windows.
If you use Exchange, then you will probably also have the ability to change your password using Outlook Web Access (OWA). Laptop users may already know that besides the credentials in Active Directory, there also exists a locally cached copy so that users can logon while disconnected from the domain. Changes through OWA only reset the Active Directory credentials, not the locally-cached copy, so the two can become out of synch. Users then have to change the password on the laptop separately.
Danger, Will Robinson!
When a remote user has no clue that his or her password is about to expire, certain productivity loss looms large on the horizon. It may happen at a very inopportune moment when remote access to some resource is vitally important. It may take some time before the user has an idea of what is wrong and contacts support for remote assistance. Warnings are essential to eliminating such wasteful and unnecessary events. Larger organizations may have identity access and management systems or even a self-service mechanism that reduce some of the cost of this kind of event, but the goal should always be prevention first, and cost reduction second. Aside from that, thinking of your next good password may take some time, and being forced to come up with one quickly may make it easier to forget, which may again require intervention from your support desk.
Active Directory stores the last password reset time for each user and also contains the domain policy for the maximum password lifetime. Given these two pieces of information, it is possible to compute when any particular user’s password will expire. There are commericial tools available (e.g. Password Reminder Pro), but a little perl scripting and the LDAP protocol is all you need! We now have what we need to provide e-mail notification to every user before passwords actually expire.
Script Configuration
You find the script here, and the configuration file here. It’s good practice to separate configuration from code, even with scripts. It will make your scripts more portable, and therefore more easily consumed by others. As a bonus, without the need to edit the script, it is less likely that changes you make will cause execution to fail completely. I like to use the Config::IniFiles
package. From it’s description…
Config::IniFiles provides a way to have readable configuration files outside your Perl script. Configurations can be imported (inherited, stacked,…), sections can be grouped, and settings can be accessed from a tied hash.
The script assumes the initialization file will be found at /usr/local/etc/pwdexpire.ini
— if this is not where you want to place the initialization file, you will have to modify the script. The initialization file is divided into sections; each section name is found on a single line between square brackets. After that, each parameter is one per line in ‘name=value’ pairs.
A few of the most important parameters to set are below:
# One server per line; use a backslash to continue to next line # Use server:port to specify a non-standard or SSL port DCs = dc1.example.com \ dc2.example.com
List each of your domain controllers here, or at least the nearby ones that can be used for LDAP queries. These will be tried in some random order.
# Root DN for domain rootDN = dc=example,dc=com # Base DN for search of all users baseDN = ou=employees,dc=example,dc=com
These two may or may not be different, but your ADS should be structured so that users are in a separate subtree. The root is where the maximum password age for the domain will be found. This will be a base tree search only.
# Bind anonymously (0|1); if set then user/passwd are ignored bindAnon = 0 # User may be a DN or a UPN name, e.g. user\@domain.com user = cn=adsquery,ou=Service,dc=cigital,dc=com passwd = secret
Depending on your Active Directory security, you may not be able to bind anonymously to ADS. If you’re not sure, try it anonymously first, and then with some set of credentials.
In the “Expire” section, check the following parameters:
# Number of days before expiration to begin warnings warnDays = 14
You will want to set this to some reasonable value. What that means will depend on a number of factors, including how long someone might be away without e-mail access and how often you require password changes. If you require password changes every month, warning someone two weeks in advance will start to get rather tedious; someone who goes on vacation for two weeks will expect to have to deal with an expired password, so it’s probably not a good idea to issue e-mail warnings to everyone halfway through the normal cycle. One the otherhand, for a 90-day password reset cycle, this might be tolerable.
# The e-mail address for from/reply-to fromAddr = "HelpDesk" <helpdesk@example.com> # The organization signature for the e-mail warning message orgSig = <<EOT Example Information Technology helpdesk@example.com EOT # A brief description of the organizational services that # use ADS authentication orgSvcs = Example's e-mail, VPN, and other internal # The URL for additional help to change a password helpURL = http://intranet.example.com/pwdexpire.html
These parameters affect the content of the e-mail message. These can customized per your requirements. See the script for the fixed content of the e-mail message. The idea is to keep the message short, but include a URL to a help page that can walk someone through the password change process and provide them with additional means of getting help. The e-mail “from” and “reply-to” address might be your helpdesk, so that a reply generates a helpdesk ticket.
# If you wish to test prior to enabling for production, set a test # e-mail recipient for all warnings (rather than end users). # Defaults to root@localhost testAddr = hostmaster\@example.com # Enable test mode (0=production, 1=test) testMode = 1
These last two parameters are for testing, and enable you to direct all warning messages to a single e-mail address. You can verify that the script is functioning as you expect, and then set testMode
to 0 (zero).
Automation: The final step
While this script should execute (with minor modifications) on a Windows host, I’m assuming this will run on some Un*x host.
Now create a crontab entry for the perl script on a host with access to your domain controllers. Add the entry for a user that has access to the both the script and the initialization file. This entry might look like:
30 0 * * * /usr/local/bin/pwdexpire.pl
This will run the script at 00:30 (or 12:30am) every day. Often you will see the output of cron jobs sent to /dev/null
by adding the following after the script:
>/dev/null 2>&1
This sends standard output and standard error to the null device; the script may write an output log of its own. In this case, we do not want this behavior. The script will not have any output if no users have passwords that will expire within the warning timeframe. When there is output, this will normally be mailed to the owner of the crontab.
Your operating system may provide another means for jobs to execute on a regular basis, e.g. Debian Linux (and derived version like Ubuntu) provide directories in which all scripts will be executed, e.g. /etc/cron.daily
. Use whatever method you’re comfortable with — it’s only important that the script execute regularly.
That’s it — enjoy!
May 8, 2009 at 16:23
Great script. I had one question though.
Is it possible to have it check for the mail attribute in LDAP, then email to that email address. Maybe also if mail in LDAP doesn’t exist, send to the test email account?
I cannot program in perl. I was trying to disassemble what you’ve done here.
Thanks.
May 11, 2009 at 10:42
You’re welcome to substitute whatever you like for the “userPrincipalName” if that does not correspond to your standard e-mail address format. That would be a bit unusual, but not without precedent. If you mean LDAP as something separate from ADS, then you have other work to do. I will look into your other suggestion when I have time — thanks!
-Robert
December 28, 2009 at 10:43
Nice script but when I try to run I got:
Can’t call method “get_value” on an undefined value at ./pwdexpire.pl line 134.
Do you have any idea, thanks for your time to reply.
Happy new year
December 28, 2009 at 17:30
Hi Luigi!
This is where the maximum password age is retrieved from the root DSA. My guess… either you didn’t specify the root DN correctly (rootDN in pwdexpire.ini) or perhaps you tried to bind anonymously, rather than specifying credentials for binding. By default, I do not believe this attribute (maxPwdAge) is visible if you bind anonymously. You can test this yourself using Microsoft’s LDAP tool (LDP.exe) — simply connect the first time, then bind and refresh the entry. You may want to establish a “service” account for querying AD/LDAP. Either way, LDP.exe can help you find the proper settings, if you’re unsure. Good luck, and Happy New Year to you too!!!
-Robert
March 30, 2010 at 17:55
Hi Rob I’ve configured the script but I don’t know which mail server is used to send e-mail there is any way to configure it.
Thanks for your precious work.
March 30, 2010 at 22:48
Hi Luigi! The script uses Mail::Send, a simple mail interface. It uses the the localhost to actually process the message, i.e. whatever routing that host is configured for. For example, if the localhost is configured to relay all mail to a “smart host”, that’s what will happen. There’s nothing to be configured in the script. Simplicity is a beautiful thing!
May 7, 2010 at 16:25
Robert,
Very nice script. One of the things I really love about Perl is the cross platform compatibility. With almost no effort I was able to make this script run under 64-bit Windows 7 (I’m sure XP would would be the same, if not easier). I was actually looking for something to pull out the last update/change time for a password, so this had all the code I needed. Very nice indeed…
January 13, 2011 at 15:24
Good post, but really, this is nothing new. There are a LOT of VB scripts on the net for creating emailed password expiration reminders. VB is a lot easier to use by non-programmers than perl for doing something like this, and hey, keep in mind we’re Windows admins not ‘nix admins ;p.
If all you need to “solve” your user community problem is a simple email reminder script, then IMHO you really don’t have much of a problem. Reminders sent by themselves to users are still a “reactionary” solution, and unfortunately, many users do not log in to their email often enough to see the reminder before their password expires- or- even worse (and common) users will ignore emails sent from IT (Execs are the biggest offenders). What ends up happening is the hours you just spent crafting your perl / vb reminder script only bought you about a %30 reduction in help requests, at best, and you still have no auditing capability or guarantee it is running every day. Plus, depending on how large your AD is, your script might not even work! Are you going to test / QA this script on your user community? Careful there… Think about it, is this worth your valuable time when a good pre-made solution exists?
You mentioned the Password Reminder PRO product from sysop tools- now that is an EXCELLENT solution, because, it is designed not to just send reminders but to give IT staff daily proactive insight to upcoming password related and account logon related issues, so these issues can be handled in advance- Basically eliminating loss of productivity to the end user by giving IT a chance to stay ahead of the game. Plus, you can use the added reporting feature to make sure all of your domain user accounts are organized to a level of PCI / SOX compliance without messing with scripts / spreadsheets etc. – Something that used to take a few hours now takes 10 minutes.
My point is, for the small amount of money that software actually costs, and the large benefit it provides to both the end user and the IT team, a solution like this is in a vastly different world that using a basic script to generate email reminders- it is not entirely correct to compare it to a basic email script, because the end results are different beasts.
For any admin reading this who does indeed have a support issue on his hands with password expiring users, I would strongly recommend using their free trial for a month and comparing it to some of the free VB (and perl here, by author) email reminder scripts. See if the free script solution will suffice vs. paying a reasonable cost for a proven out-of-the-box solution- That is what sold me on it, and I actually know how to write email reminder types of scripts very well- Plus, you can safely test with it domain-wide and quantify results without bothering users- can’t do that with a script- well, you can, but it takes a lot more scripting effort.
While I am still a firm believer in free things for certain uses (I love Nagios and Cacti!), in come cases it is “penny wise and pound foolish” to try and find / create a free solution just for the sake of being free, when a resonable purchase gets you what you need quickly / no time wasted. My 2c from an admin whos’s been there, many times (15+ years as an AD admin / security admin).
January 13, 2011 at 21:38
Mike, thanks for the lengthy comment. I think you’ve missed a few things though.
This script does a lot more than send e-mail reminders to users. First, it also provides IT with a nightly report about users whose passwords are about to expire. This report also serves as confirmation that the script did indeed run and provides an opportunity for proactive intervention.
Second, a newer version of the script, which I’ve not yet posted, provides complete flexibility in the days prior to password expiration to deliver warnings. For example, I might configure to deliver the first warning 30 days in advance, then next 14 days in advance, etc. I’ll release that sometime soon.
Third, if someone has the expertise to deploy what I’ve written, there’s no time lost — certainly not much more, if any at all versus deploying a commercial solution.
Fourth, this script can be deployed in “test” mode so there is no danger of disrupting operations until you are ready. This script was run in production for months before it was published here. It’s been well-tested. While it may not provide everything a commercial solution might, I certainly wouldn’t classify it as a radically different “beast” (as you put it). The problem of users ignoring e-mail alerts from IT is a matter of education — and some restraint on the part of IT. But, each to his or her own. Do what works for you!
August 23, 2011 at 16:19
Thanks for this script and I’d like to note that I’m running this script on a Unix host 🙂
March 24, 2011 at 16:39
Very cool script
I’m getting: Could not retrieve configuration:
although the .ini file exists.
I’m using perl 5.8.8
any idea?
March 24, 2011 at 20:23
If the configuration file exists in the location specified by the script (the default is /etc/local/etc, but you can change this if you like), then the problem can only be one of two things: 1) a permissions problem, or 2) an error in the file. You can test the first case by accessing the file from the command line, e.g. using ‘cat’, under the same account you use to run the script. For the second case, you can make a copy of the original configuration file and then start deleting sections until the error disappears — of course the script will then complain about missing configuration data, but at least you’ll have an idea of where to look for the [syntax] error. Hope this helps!
March 24, 2011 at 20:32
Hi Robert,
thanks for the quick reply.
i managed to fix the ini file issue, apperently it was missing sections such as [AD]
i executed it with perl -d for debugging and seems like it breaks at:
foreach $upn (sort sortExpAsc keys(%expiry)) {
any idea?
thanks!
March 25, 2011 at 20:28
You should have downloaded the ini file and modified the appropriate sections. Check the post for the link. There are only a few items that you might need to change and they are all documented in the comments of the ini file itself. If you were missing an entire section, then you’re probably missing other important items. If you’re still having trouble, double check the credentials and perhaps add a couple of diagnostic print statements to ensure that you are probably binding to AD and retrieving account information.
April 19, 2011 at 13:00
I am getting the error
Can’t use an undefined value as a symbol reference at /usr/share/perl5/Config/IniFiles.pm line 676, line 113.
Any ideas?
Thanks!
May 18, 2011 at 14:13
HI
very useful script, thank you very much
running the script I am getting
Use of uninitialized value in hash element at ./pwdexpire.pl line 192
any idea
thanks
May 18, 2011 at 19:46
Did you download and modify the ini file? Not supplying all of the configuration items, or the presence of errors in the configuration file, could lead to the problem you’re seeing. Hope this helps!
May 20, 2011 at 13:59
Robert,
Thank you very much
I found the issue; my sendmail on the server was not configured properly
fixed, and your script is working fine
September 1, 2011 at 12:30
I would love this, except I also get this strange error:
Can’t use an undefined value as a symbol reference at /usr/share/perl5/Config/IniFiles.pm line 682, line 139.
I am using perl 5.10.1 under debian squeeze. As far as I can tell it does find and open the ini file, put as I am not a perl expert at all, I am struggling to figure out exactly where it fails. The INI file does have all values filled in.
September 2, 2011 at 07:17
Found the reason: the INI file was in DOS format. Converted the file to unix format (using dos2unix) and now it works (the parsing, that is).
September 6, 2011 at 00:37
Hi Robert, very nice post. I have never used Perl or UNIX before. How can I run this on a Windows 2003 server? Please advise.
September 6, 2011 at 19:57
Ken, I’ve never had reason to try but it should work if you get all of the necessary modules installed from CPAN. You might want to give it a go with Strawberry Perl for Windows (http://strawberryperl.com/).
September 12, 2011 at 10:23
directory…
[…]Active Directory: Automated password expiration warnings « The Lowe Down[…]…
February 8, 2012 at 00:15
Great script, thanks!
February 8, 2013 at 09:55
This is a wonderful script! Thank you very much! We have been using it about a year then did a bunch of updates on our server. We noticed the script has not been running lately and so I ran it manually and got this error: Can’t use string (“maxPwdAge”) as an ARRAY ref while “strict refs” in use at /usr/local/share/perl/5.14.2/Convert/ASN1/_encode.pm line 269. Any ideas what this could mean?
Thanks!
February 12, 2013 at 08:54
Yes! I will post an updated version later today. It will address this and add a bit of new functionality.
March 12, 2013 at 18:36
I’d like to give this a try, but I’m running into the same issue as the poster above w/ the strict ref issue.
Thanks
Rob
May 30, 2014 at 11:58
Specials Thanks and BIG THANKS.
The script has been test on CentOS 6.4 64 bits.
Thanks again.
Azzeddine
December 22, 2014 at 22:09
with error
Can’t use string (“maxPwdAge”) as an ARRAY ref while “strict refs” in use at /usr/local/share/perl/5.14.2/Convert/ASN1/_encode.pm line 269
I disabled strict mode inside _encode.pm like
#use strict;
at
/usr/local/share/perl5/Convert/ASN1/_encode.pm
and work like a charm.
Thanks Robert!