---
# System prepended metadata

title: MacOS 使用Script設定登入密碼原則 (Password Policy)

---

# 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
```