# 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 < policyAttributeMaximumFailedAuthentications) OR (policyAttributeCurrentTime > (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 > 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
```