CIS Benchmarks - 5.2 Password Management

cainehorr
Contributor III

Every organization has different security requirements and depending on those requirements, you may find certain methods work better or worse than others.

One of the things I've been tasked with is enforcing strong passwords on our macOS endpoints.

I am currently required to adhere to CIS benchmarks as well as FedRAMP guidelines.

The out of the box Jamf Pro MobileConfig does almost everything I need it to do - sans the ability to use mixed case (ie upper and lower case) values (unless I want to hack away on some plist files - which I don't want to do).

The Jamf CIS scripts as hosted at https://github.com/jamf don't address the password enforcement - because Jamf Pro does the basics (CIS Layer 1) natively per the MobileConfig method previously mentioned.

But what if you need something a bit more robust than just the basics? Something that uses Level 1, possibly Level 2, both scored and unscored?

Well, I've seen a script floating about here in Jamf Nation and I tracked down the original source on GitHub: https://github.com/jhollandcivis/Mac-PWPolicy-Deployment-Script - unfortunately, the script seems to be missing from this location. I did however find other sources of the original script elsewhere - both on GitHub and here on Jamf Nation.

So why re-post? Because the other posts were not so much about CIS Benchmarks as they were/are just about enforcing a "password policy" to varying degrees.

That being said, the original script was tested on macOS 10.10.x and 10.11.x. and I have tested it on 10.12.x, 10.13.x, and yes, 10.14.x - all with success! Thank you Apple for not changing the commands for this particular workflow!

Without further ado, I give you my barely modified version of this script that addresses the following CIS Benchmarks:

CIS Benchmarks: 5.2 Password Management
- 5.2.1 Configure account lockout threshold (Scored), Level 1
- 5.2.2 Set a minimum password length (Scored), Level 1
- 5.2.3 Complex passwords must contain an Alphabetic Character (Not Scored), Level 2
- 5.2.4 Complex passwords must contain a Numeric Character (Not Scored), Level 2
- 5.2.5 Complex passwords must contain a Special Character (Not Scored), Level 2
- 5.2.6 Complex passwords must uppercase and lowercase letters (Not Scored), Level 2
- 5.2.7 Password Age (Scored), Level 1
- 5.2.8 Password History (Scored), Level 1

DISCLAIMER
I take very little credit for anything in this script other than identifying and calling out the various CIS Benchmarks that are applied and testing it on macOS 10.10.x through 10.14.x.

#!/bin/bash

##########################################################################################################
## Pupose:  Create a pwpolicy XML file based upon variables and options included below.
##          Policy is applied and then file gets deleted. Use "sudo pwpolicy -u <user> -getaccountpolicies"
##          to see it, and "sudo pwpolicy -u <user> -clearaccountpolicies" to clear it.
## 
## Usage:   Edit variables in Variable flowerbox below.
##          Then run as a policy from Casper, or standalone as root.
##
## Tested on: OS X 10.10 and 10.11
##
## Authors: Danny Friedman, Civis Analytics IT Manager, CCA, civisanalytics.com
##          Jeff Holland, Civis Analytics Sr. Security Engineer, CISSP/GCUX, civisanalytics.com
##
## CHANGE CONTROL LOG
##      Modified on 2019-06-25
##      Modified by Caine Hörr
##      Tested on macOS 10.13.x, 10.14.x
##      Modifications: Set values to CIS Benchmarks for macOS 10.12.x, 10.13.x, 10.14.x
##
##      Modified on 2017-11-17
##      Modified by Caine Hörr
##      Tested on macOS 10.13.x
##      Modifications: Set values to CIS Benchmarks for macOS 10.12.x
##
#########################################################################################################

# Get logged-in user and assign it to a variable
LOGGEDINUSER=$(ls -l /dev/console | awk '{print $3}')

echo "LOGGEDINUSER is: $LOGGEDINUSER"

#########################################################################################################
#
# Variables for script and commands generated below.
#
# EDIT AS NECESSARY FOR YOUR OWN PASSWORD POLICY AND COMPANY INFORMATION
#
COMPANY_NAME="Company"            # CHANGE THIS TO YOUR COMPANY NAME
MAX_FAILED=3                    # Number of max failed logins before account lockout - CIS Benchmark 5.2.1
LOCKOUT=300                     # 5 min lockout (in seconds)
MIN_LENGTH=15                   # Required password length - CIS Benchmark 5.2.2
MIN_ALPHA_LOWER=1               # Number of required lower case letters in password - CIS Benchmark 5.2.3 & 5.2.6
MIN_ALPHA_UPPER=1               # Number of required upper case letters in password - CIS Benchmark 5.2.3 & 5.2.6
MIN_NUMERIC=1                   # Number of numeric characters in password - CIS Benchmark 5.2.4
MIN_SPECIAL_CHAR=1              # Number of non-alphanumeric characters in password - CIS Benchmark 5.2.5
PW_EXPIRE=60                    # Number of days until password expiration - CIS Benchmark 5.2.7
PW_HISTORY=24                   # Remember last used passwords - CIS Benchmark 5.2.8
# exemptAccount1="admin"      # Exempt account used for remote management. CHANGE THIS TO YOUR EXEMPT ACCOUNT
#
#########################################################################################################

#########################################################################################################
#
# Create pwpolicy.plist in /private/var/tmp
# Password policy using variables above is:
# Change as necessary in variable flowerbox above
#
#########################################################################################################

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_ALPHA_UPPER,}+'</string>
   <key>policyIdentifier</key>
    <string>Has an upper case letter</string>
   <key>policyParameters</key>
   <dict>
   <key>minimumAlphaCharacters</key>
    <integer>$MIN_ALPHA_UPPER</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

#########################################################################################################
#
# End of pwpolicy.plist generation script
#
#########################################################################################################

# Check for non-admin account before deploying policy
# if [ "$LOGGEDINUSER" != "$exemptAccount1" ]; then
  chown $LOGGEDINUSER:staff /private/var/tmp/pwpolicy.plist
  chmod 644 /private/var/tmp/pwpolicy.plist

# Clear account policy before loading a new one
  pwpolicy -u $LOGGEDINUSER -clearaccountpolicies 
  pwpolicy -u $LOGGEDINUSER -setaccountpolicies /private/var/tmp/pwpolicy.plist

# elif [ "$LOGGEDINUSER" == "$exemptAccount1" ]; then
#    echo "Currently $exemptAccount1 is logged in and the password policy was NOT set. This script can only be run if the standard computer user is logged in."
#    rm -f /private/var/tmp/pwpolicy.plist
#    exit 1
# fi

# Delete staged pwploicy.plist
rm -f /private/var/tmp/pwpolicy.plist

echo "Password policy successfully applied. Run "sudo pwpolicy -u <user> -getaccountpolicies" to see it."

exit 0

You may have noticed that the script does not use the following (as recommended by CIS):

pwpolicy -getaccountpolicies | egrep com.apple.uppercaseAndLowercase

By not using that line, you are given the flexibility to use and control both uppercase and lowercase independently. With both values set, you still achieve the same functionality as with the singular "uppercaseAndLowercase" value.

Also as you can see, my personal requirements for CIS Benchmarks 5.2.7 and 5.2.8 are more stringent than those as recommended by CIS - and that's OK!

The only real change I made to the script was to remove (remark out) the use of the "exemptAccount1" account as in my environment as I don't require that feature.

So if you're looking for something that handles "All The Things" for CIS Benchmarks 5.2, this is the script that gets the job done!

Kind regards,

Caine Hörr

A reboot a day keeps the admin away!

3 REPLIES 3

gachowski
Valued Contributor II

Nice post !! Thank you!!

C

dmw3
Contributor III

@caine.horr Nice post.

We are in the process of moving away from complex passwords to a pass-phrase with a minimum length of 14 characters, no need for upper case, numerics or special characters. The phrase has to be random words joined as one string.

We have just been asked to try and adhere to the CIS guidelines except for the password section, so will be interesting to see what score we get when finished.

Vincenthesse
New Contributor III

Hello everyone, hope you’re well... I’ve almost finished the CIS Benchmark compliance but there is a little recalcitrant (CIS 5.9) that resets every time you change the value: sudo pmset -g | grep -e standby do you know if there is something to change in the Jamf or Mac configuration to maintain the values you want?