# MacOS 使用Script設定登入密碼原則 (Password Policy) ## 參考 本文參考[Applying a Password Policy to macOS Devices Using a Script](https://support.addigy.com/hc/en-us/articles/4403542684307)以及[man pwpolicy(8)](https://www.manpagez.com/man/8/pwpolicy/osx-10.10.php) ## pwpolicy Mac 上原生的 pwpolicy 工具允許配置本地密碼策略。 使用 pwpolicy,我們可以配置以下一些設定: 使用者輸入錯誤密碼的最大嘗試次數,直到電腦鎖定(MAX_FAILED)。 在輸入 ${MAX_FAILED} 次錯誤密碼後,電腦鎖定的時間(LOCKOUT)。 密碼在到期之前有效的時間(PW_EXPIRE)。 密碼的最小長度(MIN_LENGTH)。 密碼中要包含的數字的最小數量(MIN_NUMERIC)。 密碼中要包含的小寫字母的最小數量(MIN_ALPHA_LOWER)。 密碼中要包含的大寫字母的最小數量(MIN_UPPER_ALPHA)。 密碼中要包含的特殊字符的最小數量(MIN_SPECIAL_CHAR)。 不允許重複使用的記憶的密碼數量(PW_HISTORY)。 下面的腳本允許您為本地密碼策略自定義每個設定。 此腳本僅支持一個例外使用者。如果需要更多例外使用者,可以在第28行調整腳本以符合多個例外使用者的需求。 注意:編輯第7至17行應適用於大多數此腳本的使用案例。 ```bash= #!/bin/bash ############################# # Password Policy Settings ## ############################# MAX_FAILED=5 # Maximum attempts for a user to input wrong passwords until the computer locks up LOCKOUT=120 # Amount of time the computer will be locked after ${MAX_FAILED} amount of wrong password inputs PW_EXPIRE=90 # Amount of time a password is valid before it expires MIN_LENGTH=8 # Minimum length of password in characters MIN_DIGITS=1 # Minimum amount of digits in the password MIN_LOWER_LETTERS=1 # Minimum amount of lowercase letters in password MIN_UPPER_LETTERS=1 # Minimum amount of uppercase letters in password MIN_SPECIAL_CHAR=0 # Minimum amount of special characters in password PW_HISTORY=5 # Number of passwords to remember that cannot be reused exemptAccount="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" != "$exemptAccount" ]; then # Check if current plist is installed by comparing the current variables to the new ones echo "========== $user ========== " # 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:]]*$//') [[ $MIN_SPECIAL_CHAR -gt 0 ]] && newMinSpecialChar="<string>policyAttributePassword matches '(.*[^a-zA-Z0-9].*){$MIN_SPECIAL_CHAR,}+'</string>" || newMinSpecialChar="" # MIN_UPPER_LETTERS currentUpperLimit=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '(.*[A-Z].*){$MIN_UPPER_LETTERS,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') [[ $MIN_UPPER_LETTERS -gt 0 ]] && newUpperLimit="<string>policyAttributePassword matches '(.*[A-Z].*){$MIN_UPPER_LETTERS,}+'</string>" || newUpperLimit="" # MIN_LOWER_LETTERS currentLowerLimit=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '(.*[a-z].*){$MIN_LOWER_LETTERS,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') [[ $MIN_LOWER_LETTERS -gt 0 ]] && newLowerLimit="<string>policyAttributePassword matches '(.*[a-z].*){$MIN_LOWER_LETTERS,}+'</string>" || newLowerLimit="" # MIN_DIGITS currentNumLimit=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '(.*[0-9].*){$MIN_DIGITS,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') [[ $MIN_DIGITS -gt 0 ]] && newNumLimit="<string>policyAttributePassword matches '(.*[0-9].*){$MIN_DIGITS,}+'</string>" || newNumLimit="" # MIN_LENGTH currentMinLength=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>policyAttributePassword matches '.{$MIN_LENGTH,}+'</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') [[ $MIN_LENGTH -gt 0 ]] && newMinLength="<string>policyAttributePassword matches '.{$MIN_LENGTH,}+'</string>" || newMinLength="" # PW_EXPIRE currentPwExpire=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "<string>Change the password every $PW_EXPIRE days.</string>" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') newPwExpire="<string>Change the password every $PW_EXPIRE days.</string>" # LOCKOUT currentLockOut=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "Lock the account for" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') newLockOut="Lock the account for $LOCKOUT seconds" # MAX_FAILED currentMaxFailed=$(sudo pwpolicy -u "$user" -getaccountpolicies | grep "failed password attempts" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') newMaxFailed="$MAX_FAILED failed password attempts" 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_CHAR is the same" else echo "MIN_UPPER_ALPHA_CHAR is NOT the same" echo " current: $currentUpperLimit" echo " new: $newUpperLimit" isPlistNew=1 fi if [ "$currentLowerLimit" == "$newLowerLimit" ]; then echo "MIN_LOWER_ALPHA_CHAR is the same" else echo "MIN_LOWER_ALPHA_CHAR is NOT the same" echo " current: $currentLowerLimit" echo " new: $newLowerLimit" isPlistNew=1 fi if [ "$currentNumLimit" == "$newNumLimit" ]; then echo "MIN_DIGITS is the same" else echo "MIN_DIGITS 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 if [ $MIN_LENGTH -gt 0 ]; then minimumLength=" <dict> <key>policyContent</key> <string>policyAttributePassword matches '.{$MIN_LENGTH,}+'</string> <key>policyContentDescription</key> <dict> <key>en</key> <string>Must be a minimum of $MIN_LENGTH characters in length</string> <key>zh-Hant</key> <string>密碼長度最短必須為 $MIN_LENGTH 個字元</string> </dict> <key>policyIdentifier</key> <string>tw.neko.policy.legacy.minimumLength</string> </dict> " fi if [ $MIN_DIGITS -gt 0 ]; then minimumDigits=" <dict> <key>policyContent</key> <string>policyAttributePassword matches '(.*[0-9].*){$MIN_DIGITS,}+'</string> <key>policyContentDescription</key> <dict> <key>en</key> <string>The password must contain at least $MIN_DIGITS digits</string> <key>zh-Hant</key> <string>密碼最少包含 $MIN_DIGITS 個數字</string> </dict> <key>policyIdentifier</key> <string>tw.neko.policy.legacy.minimumDigits</string> </dict> " fi if [ $MIN_LOWER_LETTERS -gt 0 ]; then minimumLowerCaseLetters=" <dict> <key>policyContent</key> <string>policyAttributePassword matches '(.*[a-z].*){$MIN_LOWER_LETTERS,}+'</string> <key>policyContentDescription</key> <dict> <key>en</key> <string>The password must contain at least $MIN_LOWER_LETTERS lowercase letters</string> <key>zh-Hant</key> <string>密碼最少包含 $MIN_LOWER_LETTERS 個小寫字母</string> </dict> <key>policyIdentifier</key> <string>tw.neko.policy.legacy.minLowercaseLetters</string> </dict> " fi if [ $MIN_UPPER_LETTERS -gt 0 ]; then minimumUpperCaseLetters=" <dict> <key>policyContent</key> <string>policyAttributePassword matches '(.*[A-Z].*){$MIN_UPPER_LETTERS,}+'</string> <key>policyContentDescription</key> <dict> <key>en</key> <string>The password must contain at least $MIN_UPPER_LETTERS uppercase letters</string> <key>zh-Hant</key> <string>密碼最少包含 $MIN_UPPER_LETTERS 個大寫字母</string> </dict> <key>policyIdentifier</key> <string>tw.neko.policy.legacy.minimumUpperCaseLetters</string> </dict> " fi if [ $MIN_SPECIAL_CHAR -gt 0 ]; then minimumSpecialCharacters=" <dict> <key>policyContent</key> <string>policyAttributePassword matches '(.*[^a-zA-Z0-9].*){$MIN_SPECIAL_CHAR,}+'</string> <key>policyContentDescription</key> <dict> <key>en</key> <string>The password must contain at least $MIN_SPECIAL_CHAR special characters</string> <key>zh-Hant</key> <string>密碼最少包含 $MIN_SPECIAL_CHAR 個特殊符號</string> </dict> <key>policyIdentifier</key> <string>tw.neko.policy.legacy.minimumSpecialCharacters</string> </dict> " fi if [ $PW_HISTORY -gt 0 ]; then policyAttributePasswordHistoryDepth=" <dict> <key>policyContent</key> <string>none policyAttributePasswordHashes in policyAttributePasswordHistory</string> <key>policyContentDescription</key> <dict> <key>en</key> <string>Does not match any of last $PW_HISTORY passwords</string> <key>zh-Hant</key> <string>不得與最近的 $PW_HISTORY 個密碼中的任何一個相同</string> </dict> <key>policyIdentifier</key> <string>tw.neko.policy.legacy.policyAttributePasswordHistoryDepth</string> <key>policyParameters</key> <dict> <key>policyAttributePasswordHistoryDepth</key> <integer>$PW_HISTORY</integer> </dict> </dict> " fi # Creates plist using variables above echo " <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>policyCategoryAuthentication</key> <array> <dict> <key>policyContent</key> <string>(policyAttributeFailedAuthentications &lt; policyAttributeMaximumFailedAuthentications) OR (policyAttributeCurrentTime &gt; (policyAttributeLastFailedAuthenticationTime + autoEnableInSeconds))</string> <key>policyContentDescription</key> <dict> <key>en</key> <string>Lock the account for $LOCKOUT seconds after $MAX_FAILED failed password attempts</string> <key>zh-Hant</key> <string>嘗試密碼錯誤 $MAX_FAILED 次,鎖定帳號 $LOCKOUT 秒</string> </dict> <key>policyIdentifier</key> <string>tw.neko.policy.legacy.policyCategoryAuthentication</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>policyContentDescription</key> <dict> <key>en</key> <string>Change the password every $PW_EXPIRE days.</string> <key>zh-Hant</key> <string>每 $PW_EXPIRE 天更換一次密碼</string> </dict> <key>policyIdentifier</key> <string>tw.neko.policy.legacy.policyCategoryPasswordChange</string> <key>policyParameters</key> <dict> <key>policyAttributeExpiresEveryNDays</key> <integer>$PW_EXPIRE</integer> </dict> </dict> </array> <key>policyCategoryPasswordContent</key> <array> $minimumLength $minimumDigits $minimumLowerCaseLetters $minimumUpperCaseLetters $minimumSpecialCharacters $policyAttributePasswordHistoryDepth </array> </dict> </plist>" >/private/var/tmp/pwpolicy.plist # save the plist temp pwpolicy -u "$user" -clearaccountpolicies pwpolicy -u "$user" -setaccountpolicies /private/var/tmp/pwpolicy.plist rm /private/var/tmp/pwpolicy.plist echo "Password policy successfully applied. Run \"sudo pwpolicy -u $user -getaccountpolicies\" to see it." fi fi done exit 0 ``` 將以上Script存成檔案(ex:pwpolicy.sh),開啟終端機執行 ```bash sudo bash pwpolicy.sh ```