Skip to main content

Here is a worklet for enforcing password policies for macOS for all users (as well as setting exempt users).



This code is test on macOS mojave. The defaults are provided for example only.



Check script.



#!/usr/bin/env bash

set -e -o pipefail



######################################################################

# Enforcing a password policy is good practice for all users

# Using the variables below you can check the password policy for all

# logged in users.

# the default variables provided in the main clause are provided for EXAMPLE ONLY

######################################################################



######################################################################

# get_logged_in_users

# Retrieves the logged in users. Does deduplication to make sure that we only

# apply policies to

#

# Note: if there are service users it will find them too

#

# Globals:

#

# Arguments:

# None

# Returns:

# A space delimited string of unique users logged in

######################################################################



function get_logged_in_users() {

who -q | grep -v "#" | xargs -n1 | sort -u | xargs

}



######################################################################

# get_pwpolicy_for_user

# Retrieves the password policies to the given user using the given identifier

#

# Note: if there are service users it will find them too

#

# Globals:

# None

# Arguments:

# $1 = the username to get password policies for

# $2 = the identifier used to find the policy

# Returns:

# 0 - if the password policy is found for the given user

# 1 - if the password policy is NOT found for the given user

######################################################################



function get_pwpolicy_for_user() {

local user=$1

local policy_identifier=$2

local pw_policy_search=$(pwpolicy -u $user -getaccountpolicies | grep -o "$policy_identifier" | sed -e 's/ //g')

if [[ "$pw_policy_search" == "$policy_identifier" ]]; then

return 0

fi

return 1

}



######################################################################

# remove_exempt_users

# Remove exempt users from the given array

#

# Globals:

# exempt_users

# Arguments:

# $1 = users array

# Returns:

# None

######################################################################



function remove_exempt_users() {

local users=("$@")

# separate the users by line

# use grep with extended regex user1|user2|user3 to match a line and remove it

echo "${users[@]}" | xargs -n1 | grep -Ev "$(join_by "|" "${exempt_users[@]}")"

}



######################################################################

# join_by

# Joins a string given a separator

#

# Globals:

# None

# Arguments:

# $1 = the separator

# Returns:

# None

######################################################################

function join_by {

local IFS="$1"; shift; echo "$*";

}



######################################################################

# main

# Variables to set:

# users - the users to set the policy for. Default: all logged in users

# policy_identifier - used to identify a policy

# exempt_users - users that should be exempt from this password policy

users=($(get_logged_in_users))

policy_identifier="Does not match any of last 5 passwords" # provided as a default

exempt_users=(

"admin" # need remote management

)

# exit 0: Everything is a-ok

# exit 1: We did not find the password policy that we expected

######################################################################

users=($(remove_exempt_users "${users[@]}"))

non_compliant=0

for user in "${users[@]}"; do

get_pwpolicy_for_user "$user" "${policy_identifier}"

# shellcheck disable=SC2181

if [[ $? != 0 ]]; then

non_compliant=1 && echo "$user does not have password policy set"

else

echo "$user has compliant password policy set"

fi

done



# compliant: 1 == bad; 0 == good

exit $non_compliant



Here is the remediation script:



#!/usr/bin/env bash

set -o pipefail



######################################################################

# Enforcing a password policy is considered a good practice for all users

# Using the variables below you can configure the password policy to your liking

# to set it for all users.

# The default variables provided in the main clause are provided for EXAMPLE ONLY

######################################################################



######################################################################

# get_logged_in_users

# Retrieves the logged in users. Does deduplication to make sure that we only

# apply policies to

#

# Note: if there are service users it will find them too

#

# Globals:

#

# Arguments:

# None

# Returns:

# A space delimited string of unique users logged in

######################################################################



function get_logged_in_users() {

who -q | grep -v "#" | xargs -n1 | sort -u | xargs

}



######################################################################

# remove_exempt_users

# Remove exempt users from the given array

#

# Globals:

# exempt_users

# Arguments:

# $1 = users array

# Returns:

# None

######################################################################



function remove_exempt_users() {

local users=("$@")

# separate the users by line

# use grep with extended regex user1|user2|user3 to match a line and remove it

echo "${users[@]}" | xargs -n1 | grep -Ev $(join_by "|" "${exempt_users[@]}")

}



######################################################################

# join_by

# Joins a string given a separator

#

# Globals:

# None

# Arguments:

# $1 = the separator

# Returns:

# None

######################################################################

function join_by {

local IFS="$1"; shift; echo "$*";

}



# temporary file

temp_pw_policy_path="$(mktemp "${TMPDIR:-/tmp/}"$(uuidgen).plist)"



######################################################################

# Password Policy

######################################################################

# Variables to set:

# pw_policy_xml - used to identify a policy

# company_name - company identifier

# lockedout - the lockout timer in seconds

# max_failed - the number of logins before locking

# pw_expre - the days to expiration for a password

# min_length - the minimum length for a password

# min_numeric - the minimum number of numeric characters required

# min_alpha_lower - the minimum number of lower case characters required

# min_special_char - the minimum number of special characters

# pw_history - the number of passwords to remember

# clear_existing_password_policies - clear policies before setting a new one

company_name="your_cool_company.com" # CHANGE THIS TO YOUR COMPANY NAME

lockout=300 # 5 min lockout

max_failed=10 # 10 max failed logins before locking

pw_expire=90 # 90 days password expiration

min_length=8 # at least 8 chars for password

min_numeric=1 # at least 1 number in password

min_alpha_lower=1 # at least 1 lower case letter in password

min_upper_alpha=1 # at least 1 upper case letter in password

min_special_char=1 # at least one special character in password

pw_history=5 # remember last 10 passwords

# create the password policy file

echo "<dict>

<key>policyCategoryAuthentication</key>

<array>

<dict>

<key>policyContent</key>

<string>(policyAttributeFailedAuthentications &lt; policyAttributeMaximumFailedAuthentications) OR (policyAttributeCurrentTime &gt; (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 &gt; 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>" > $temp_pw_policy_path





######################################################################

# main

# Variables to set:

# users - the users to set the policy for. Default: all logged in users

# exempt_users - users that should be exempt from this password policy

# clear_existing_policies - 1 for clearing policies before setting a new one or 0 for not

# shellcheck disable=SC2207

users=($(get_logged_in_users))

exempt_users=(

"admin" # need remote management

)

clear_existing_password_policies=0 # Let's not clear the existing password policies by default

# exit 0: Everything is a-ok

# exit 1: We did not find the password policy that we expected

######################################################################

users=($(remove_exempt_users "${users[@]}"))

non_compliant=0 # by default assume we are compliant



# setup trap to make sure before we exit we cleanup the password

# policy file.

function cleanup {

rm -f "$temp_pw_policy_path"

}

trap cleanup EXIT



# set the password policy for all logged in users

for user in "${users[@]}"; do

if [[ $clear_existing_password_policies -eq 1 ]]; then

pwpolicy -u $user -clearaccountpolicies

fi



pwpolicy -u $user -setaccountpolicies "$temp_pw_policy_path"

# shellcheck disable=SC2181

if [[ $? -ne 0 ]]; then

non_compliant=1 && echo "Could not set password policy for user $user" >&2

fi

done



exit $non_compliant