Password policies are a powerful way to ensure that your users have appropriate strong passwords and that they update their passwords regularly. If you'd like to enforce password policies on your devices, we recommend using MDM Configurations. You can find more information on our MDM Configurations in our article Addigy Mobile Device Management (MDM).
If your organization has decided that the MDM Passcode payload is not compatible or desired, then you can still manage password policies through scripted solutions.
The pwpolicy utility, native to the Mac, allows for configuration of a policy for local passwords. Using pwpolicy, here are some settings we can configure:
1. Maximum attempts for a user to input wrong passwords until the computer locks up (MAX_FAILED).
2. Amount of time the computer will be locked after X amount of wrong password inputs (LOCKOUT)
3. Amount of time a password is valid before it expires (PW_EXPIRE)
4. Minimum length of password in characters (MIN_LENGTH)
5. Minimum amount of numbers in the password (MIN_NUMERIC)
6. Minimum amount of lowercase letters in password (MIN_ALPHA_LOWER)
7. Minimum amount of uppercase letters in password (MIN_UPPER_ALPHA)
8. Minimum amount of special characters in password (MIN_SPECIAL_CHAR)
9. Number of passwords to remember that cannot be reused (PW_HISTORY)
The following script allows you to customize each of these settings for a local password policy.
This script is built to handle only one exempt user. If more exempt users are needed, it is possible to adjust the script to account for the multiple exempt users on line 28.
Notes:
- Please test this script on a test device/VM before deploying it to production devices. Addigy is not responsible for any personal misconfiguration or misuse of the script.
- Editing lines 12-22 should fit most use cases of this script.
#!/bin/bash ############################# # Password Policy Settings ## ############################# MAX_FAILED=5 # 5 max failed logins before locking LOCKOUT=120 # 2min lockout PW_EXPIRE=90 # 90 days password expiration MIN_LENGTH=16 # at least 16 chars for password MIN_NUMERIC=0 # at least 0 number in password MIN_ALPHA_LOWER=0 # at least 0 lower case letter in password MIN_UPPER_ALPHA=0 # at least 0 upper case letter in password MIN_SPECIAL_CHAR=0 # at least 0 special character in password PW_HISTORY=3 # remember last 3 passwords exemptAccount1="ENTER_EXEMPT_ACCOUNT" #Exempt account used for remote management. CHANGE THIS TO YOUR EXEMPT ACCOUNT if [ $PW_EXPIRE -lt "1" ]; then echo "PW EXPIRE TIME CAN NOT BE 0 or less." exit 1 fi for user in $(dscl . list /Users UniqueID | awk '$2 >= 500 {print $1}'); do if [ "$user" != "$exemptAccount1" ]; then #Check if current plist is installed by comparing the current variables to the new ones #PW_History currentPwHistory=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>Does not match any of last $PW_HISTORY passwords</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newPwHistory="<string>Does not match any of last $PW_HISTORY passwords</string>" #MIN_SPECIAL_CHAR currentMinSpecialChar=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '(.*[^a-zA-Z0-9].*){$MIN_SPECIAL_CHAR,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newMinSpecialChar="<string>policyAttributePassword matches '(.*[^a-zA-Z0-9].*){$MIN_SPECIAL_CHAR,}+'</string>" #MIN_UPPER_ALPHA currentUpperLimit=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '(.*[A-Z].*){$MIN_UPPER_ALPHA,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newUpperLimit="<string>policyAttributePassword matches '(.*[A-Z].*){$MIN_UPPER_ALPHA,}+'</string>" #MIN_ALPHA_LOWER currentLowerLimit=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '(.*[a-z].*){$MIN_ALPHA_LOWER,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newLowerLimit="<string>policyAttributePassword matches '(.*[a-z].*){$MIN_ALPHA_LOWER,}+'</string>" #MIN_NUMERIC currentNumLimit=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '(.*[0-9].*){$MIN_NUMERIC,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newNumLimit="<string>policyAttributePassword matches '(.*[0-9].*){$MIN_NUMERIC,}+'</string>" #MIN_LENGTH currentMinLength=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '.{$MIN_LENGTH,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newMinLength="<string>policyAttributePassword matches '.{$MIN_LENGTH,}+'</string>" #PW_EXPIRE currentPwExpire=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>Change every $PW_EXPIRE days</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newPwExpire="<string>Change every $PW_EXPIRE days</string>" #LOCKOUT currentLockOut=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<integer>$LOCKOUT</integer>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newLockOut="<integer>$LOCKOUT</integer>" #MAX_FAILED currentMaxFailed=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<integer>$MAX_FAILED</integer>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' ) newMaxFailed="<integer>$MAX_FAILED</integer>" isPlistNew=0 if [ "$currentPwHistory" == "$newPwHistory" ]; then echo "PW_History is the same" else echo "PW_History is NOT the same" echo "current: $currentPwHistory" echo "new: $newPwHistory" isPlistNew=1 fi if [ "$currentMinSpecialChar" == "$newMinSpecialChar" ]; then echo "MIN_SPECIAL_CHAR is the same" else echo "MIN_SPECIAL_CHAR is NOT the same" echo "current: $currentMinSpecialChar" echo "new: $newMinSpecialChar" isPlistNew=1 fi if [ "$currentUpperLimit" == "$newUpperLimit" ]; then echo "MIN_UPPER_ALPHA is the same" else echo "MIN_UPPER_ALPHA is NOT the same" echo "current: $currentUpperLimit" echo "new: $newUpperLimit" isPlistNew=1 fi if [ "$currentLowerLimit" == "$newLowerLimit" ]; then echo "MIN_ALPHA_LOWER is the same" else echo "MIN_ALPHA_LOWER is NOT the same" echo "current: $currentLowerLimit" echo "new: $newLowerLimit" isPlistNew=1 fi if [ "$currentNumLimit" == "$newNumLimit" ]; then echo "MIN_NUMERIC is the same" else echo "MIN_NUMERIC is NOT the same" echo "current: $currentNumLimit" echo "new: $newNumLimit" isPlistNew=1 fi if [ "$currentMinLength" == "$newMinLength" ]; then echo "MIN_LENGTH is the same" else echo "MIN_LENGTH is NOT the same" echo "current: $currentMinLength" echo "new: $newMinLength" isPlistNew=1 fi if [ "$currentPwExpire" == "$newPwExpire" ]; then echo "PW_Expire is the same" else echo "PW_Expire is NOT the same" echo "current: $currentPwExpire" echo "new: $newPwExpire" isPlistNew=1 fi if [ "$currentLockOut" == "$newLockOut" ]; then echo "LOCKOUT is the same" else echo "LOCKOUT is NOT the same" echo "current: $currentLockOut" echo "new: $newLockOut" isPlistNew=1 fi if [ "$currentMaxFailed" == "$newMaxFailed" ]; then echo "MAX_FAILED is the same" else echo "MAX_FAILED is NOT the same" echo "current: $currentMaxFailed" echo "new: $newMaxFailed" isPlistNew=1 fi if [ "$isPlistNew" -eq "1" ]; then # Creates plist using variables above echo "<dict> <key>policyCategoryAuthentication</key> <array> <dict> <key>policyContent</key> <string>(policyAttributeFailedAuthentications < policyAttributeMaximumFailedAuthentications) OR (policyAttributeCurrentTime > (policyAttributeLastFailedAuthenticationTime + autoEnableInSeconds))</string> <key>policyIdentifier</key> <string>Authentication Lockout</string> <key>policyParameters</key> <dict> <key>autoEnableInSeconds</key> <integer>$LOCKOUT</integer> <key>policyAttributeMaximumFailedAuthentications</key> <integer>$MAX_FAILED</integer> </dict> </dict> </array> <key>policyCategoryPasswordChange</key> <array> <dict> <key>policyContent</key> <string>policyAttributeCurrentTime > policyAttributeLastPasswordChangeTime + (policyAttributeExpiresEveryNDays * 24 * 60 * 60)</string> <key>policyIdentifier</key> <string>Change every $PW_EXPIRE days</string> <key>policyParameters</key> <dict> <key>policyAttributeExpiresEveryNDays</key> <integer>$PW_EXPIRE</integer> </dict> </dict> </array> <key>policyCategoryPasswordContent</key> <array> <dict> <key>policyContent</key> <string>policyAttributePassword matches '.{$MIN_LENGTH,}+'</string> <key>policyIdentifier</key> <string>Has at least $MIN_LENGTH characters</string> <key>policyParameters</key> <dict> <key>minimumLength</key> <integer>$MIN_LENGTH</integer> </dict> </dict> <dict> <key>policyContent</key> <string>policyAttributePassword matches '(.*[0-9].*){$MIN_NUMERIC,}+'</string> <key>policyIdentifier</key> <string>Has a number</string> <key>policyParameters</key> <dict> <key>minimumNumericCharacters</key> <integer>$MIN_NUMERIC</integer> </dict> </dict> <dict> <key>policyContent</key> <string>policyAttributePassword matches '(.*[a-z].*){$MIN_ALPHA_LOWER,}+'</string> <key>policyIdentifier</key> <string>Has a lower case letter</string> <key>policyParameters</key> <dict> <key>minimumAlphaCharactersLowerCase</key> <integer>$MIN_ALPHA_LOWER</integer> </dict> </dict> <dict> <key>policyContent</key> <string>policyAttributePassword matches '(.*[A-Z].*){$MIN_UPPER_ALPHA,}+'</string> <key>policyIdentifier</key> <string>Has an upper case letter</string> <key>policyParameters</key> <dict> <key>minimumAlphaCharacters</key> <integer>$MIN_UPPER_ALPHA</integer> </dict> </dict> <dict> <key>policyContent</key> <string>policyAttributePassword matches '(.*[^a-zA-Z0-9].*){$MIN_SPECIAL_CHAR,}+'</string> <key>policyIdentifier</key> <string>Has a special character</string> <key>policyParameters</key> <dict> <key>minimumSymbols</key> <integer>$MIN_SPECIAL_CHAR</integer> </dict> </dict> <dict> <key>policyContent</key> <string>none policyAttributePasswordHashes in policyAttributePasswordHistory</string> <key>policyIdentifier</key> <string>Does not match any of last $PW_HISTORY passwords</string> <key>policyParameters</key> <dict> <key>policyAttributePasswordHistoryDepth</key> <integer>$PW_HISTORY</integer> </dict> </dict> </array> </dict>" > /private/var/tmp/pwpolicy.plist #save the plist temp chmod 777 /private/var/tmp/pwpolicy.plist pwpolicy -u "$user" -clearaccountpolicies pwpolicy -u "$user" -setaccountpolicies /private/var/tmp/pwpolicy.plist fi fi done rm /private/var/tmp/pwpolicy.plist echo "Password policy successfully applied. Run \"sudo pwpolicy -u <user> -getaccountpolicies\" to see it." exit 0
If you experience any issues with the script or have additional questions, please reach out to Addigy support (support@addigy.com).