Worklet: (macOS) enforce password policy for all users and not for exempt users

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
4 Likes