Skip to content

How to enforce a password policy

This guide makes local accounts choose passwords of at least 12 characters that use at least three character classes (lower case, upper case, digits, other characters), and sets password-ageing defaults for new accounts. It then proves the policy works by trying a weak and a strong password on a test account.

The policy applies when a user changes their own password with passwd. When root sets a password (with passwd <USER> or chpasswd), the quality modules only print a warning; that's deliberate and this guide keeps it that way, so root can always reset a password.

Before you begin

  • You have a shell with sudo on the machine.
  • Open a second session and start a root shell in it with sudo -i. Keep it open until you have finished the Verify section.

Changing PAM can lock you out

The steps below change only the password part of the PAM configuration, which is used when a password is changed, not when someone logs in. A mistake there stops users changing passwords, but doesn't stop logins. Edit only the files named here, in the way shown: a typo in the auth or account lines of the same files can refuse every login, including yours through sudo. The root shell in your second session is your way back; each step says how to undo it.

Steps

  1. Make sure a password quality module is in the password stack.

    Install pam_pwquality. Its package adds it to /etc/pam.d/common-password through pam-auth-update:

    sudo apt-get update
    sudo apt-get install -y libpam-pwquality
    

    Check that the module is now the first password line:

    $ grep pam_pwquality /etc/pam.d/common-password
    password    requisite    pam_pwquality.so retry=3
    

    Don't edit common-password by hand: pam-auth-update regenerates it when PAM-related packages are installed or removed. To undo this step, run sudo apt-get remove libpam-pwquality.

    pam_pwquality is enabled by default. Check that it's in the stacks that authselect generates:

    $ grep pam_pwquality /etc/pam.d/system-auth /etc/pam.d/password-auth
    /etc/pam.d/system-auth:password    requisite                                    pam_pwquality.so
    /etc/pam.d/password-auth:password    requisite                                    pam_pwquality.so
    

    Don't edit these files by hand: they belong to authselect, which overwrites them. The policy goes in pwquality.conf.d in the next step.

    Arch doesn't check password quality by default. Install libpwquality, which provides the module:

    sudo pacman -S --needed --noconfirm libpwquality
    

    Keep a copy of the PAM file for the passwd command, then add the module before the line that includes system-auth:

    sudo cp -p /etc/pam.d/passwd /root/pam.d-passwd.bak
    sudo sed -i '/^password[[:space:]]\+include[[:space:]]\+system-auth/i password\trequisite\tpam_pwquality.so retry=3' /etc/pam.d/passwd
    

    The file should now read:

    $ cat /etc/pam.d/passwd
    #%PAM-1.0
    auth    include    system-auth
    account    include    system-auth
    password    requisite    pam_pwquality.so retry=3
    password    include    system-auth
    

    The control must be requisite, so that a refused password stops the stack. With required, pam_unix in system-auth still runs after the refusal, asks for a new password itself, and stores it.

    Changing /etc/pam.d/passwd affects only the passwd command; logins don't use it. To undo this step, run sudo cp -p /root/pam.d-passwd.bak /etc/pam.d/passwd.

    Gentoo's pambase checks passwords with pam_passwdqc by default (USE flag passwdqc). Check that it's in system-auth:

    $ grep pam_passwdqc /etc/pam.d/system-auth
    password    required    pam_passwdqc.so config=/etc/security/passwdqc.conf
    

    If the line is missing, sys-auth/pambase was built without the passwdqc USE flag; enable it and rebuild pambase rather than editing system-auth, which is a package-managed file.

  2. Set the policy.

    Create the file /etc/security/pwquality.conf.d/50-local.conf with this content (the directory doesn't exist by default, but the module reads it):

    # Local password policy: at least 12 characters, from at least 3 classes.
    minlen = 12
    minclass = 3
    retry = 3
    

    Create the file /etc/security/pwquality.conf.d/50-local.conf with this content:

    # Local password policy: at least 12 characters, from at least 3 classes.
    minlen = 12
    minclass = 3
    retry = 3
    

    Create the file /etc/security/pwquality.conf.d/50-local.conf with this content:

    # Local password policy: at least 12 characters, from at least 3 classes.
    minlen = 12
    minclass = 3
    retry = 3
    

    pam_passwdqc reads /etc/security/passwdqc.conf. Keep a copy, then set the minimum lengths and make the policy refuse weak passwords for ordinary users:

    sudo cp -p /etc/security/passwdqc.conf /root/passwdqc.conf.bak
    sudo sed -i -e 's/^min=.*/min=disabled,disabled,12,12,12/' -e 's/^enforce=.*/enforce=users/' /etc/security/passwdqc.conf
    

    The five values of min= are the minimum lengths for passwords made of one character class, two classes, passphrases of several words, three classes and four classes; disabled refuses that kind outright. Gentoo's default is enforce=none, which only warns; enforce=users refuses weak passwords chosen by users and still lets root set any password. To undo this step, run sudo cp -p /root/passwdqc.conf.bak /etc/security/passwdqc.conf.

    In pwquality.conf terms, minlen is the minimum length (exactly, as long as the *credit settings keep their default of 0), minclass the number of classes required, and retry the number of attempts passwd allows. The module also refuses dictionary words and passwords containing the user name by default. To undo this step on Debian, Ubuntu, the RHEL family and Arch, delete /etc/security/pwquality.conf.d/50-local.conf.

  3. Set password-ageing defaults for accounts created from now on, in /etc/login.defs: a password must be changed after 365 days, and its owner is warned 14 days before it expires.

    sudo cp -p /etc/login.defs /root/login.defs.bak
    for setting in 'PASS_MAX_DAYS 365' 'PASS_WARN_AGE 14'; do
        key=${setting%% *}
        if sudo grep -q "^${key}[[:space:]]" /etc/login.defs; then
            sudo sed -i "s/^${key}[[:space:]].*/${setting}/" /etc/login.defs
        else
            echo "$setting" | sudo tee -a /etc/login.defs >/dev/null
        fi
    done
    grep -E '^PASS_(MAX_DAYS|WARN_AGE)' /etc/login.defs
    

    Debian, Ubuntu and the RHEL family have these settings in login.defs already, and the loop changes them; on Arch and Gentoo they are missing, and it adds them. useradd copies them into each new account; existing accounts keep their current values, which you change per account, for example with sudo chage --maxdays 365 --warndays 14 <USER>, where <USER> is the account's name. PASS_MIN_LEN in login.defs isn't used for this policy: on these systems, password length is checked by the PAM module configured above. To undo this step, run sudo cp -p /root/login.defs.bak /etc/login.defs; accounts created in the meantime keep the maximum age they were given, so check them with sudo chage -l <USER>.

    Don't expire accounts that have no password

    When an account's password expires, PAM demands a new password before the account can log in or use sudo, even when it logs in with an SSH key. An account without a usable password, such as an administrator who only logs in with a key and uses passwordless sudo, can then no longer get in. The new default applies to every account created from now on, including key-only administrators and the accounts that cloud-init creates, and the lockout only shows a year later. Check an account with sudo chage -l <USER> before giving it a maximum age, and remove the maximum from each new account that won't use a password, as below.

  4. For every new account that won't use a password, remove the maximum age right after creating it. For example, for a key-only account called deploy:

    $ sudo useradd -m <USER>
    $ sudo chage -M -1 <USER>
    $ sudo chage -l <USER>
    Last password change                                    : Nov 07, 2026
    Password expires                                        : never
    Password inactive                                       : never
    Account expires                                         : never
    Minimum number of days between password change          : 0
    Maximum number of days between password change          : -1
    Number of days of warning before password expires       : 14
    

    Password expires: never confirms that the account can't be locked out by ageing. Accounts created by automation (cloud-init, configuration management) need the same treatment, in the automation itself.

Verify

Test the policy on a throwaway account, never on your own.

  1. Create a test account and give it a starting password, as root:

    sudo useradd -m pwtest
    echo 'pwtest:Start-Pass-2026-xyz' | sudo chpasswd
    
  2. Check that the new account got the ageing defaults from step 3:

    $ sudo passwd -S pwtest
    pwtest P 2026-11-07 0 365 14 -1
    

    The fields are the account, its password status (P for a usable password), the date of the last change, and the minimum age, maximum age, warning period and inactivity period in days. The maximum age should be 365 and the warning period 14. On Arch and Gentoo the minimum age is shown as -1 (not set) instead of 0; both mean that the password can be changed at any time.

  3. As pwtest, try to change the password to Kx9#mPq2, which is eight characters long. It's long enough for the distributions' defaults, but not for the new policy:

    $ printf '%s\n' 'Start-Pass-2026-xyz' 'Kx9#mPq2' 'Kx9#mPq2' | sudo su -s /bin/sh pwtest -c passwd
    Changing password for pwtest.
    Current password: New password: BAD PASSWORD: The password is shorter than 12 characters
    New password: BAD PASSWORD: The password is shorter than 12 characters
    New password: Password change has been aborted.
    passwd: Authentication token manipulation error
    passwd: password unchanged
    

    passwd reads the current password, then the new one twice, from its standard input here, so the test runs without typing. The policy refuses the password, so passwd takes the repeated line as a second attempt, refuses that too, gives up at the end of its input and exits with an error. On Gentoo, pam_passwdqc prints its own explanation of the rules and Weak password: too short. instead.

  4. Try a strong one, which the policy accepts:

    $ printf '%s\n' 'Start-Pass-2026-xyz' 'Correct-Horse-Battery-42' 'Correct-Horse-Battery-42' | sudo su -s /bin/sh pwtest -c passwd
    Changing password for pwtest.
    Current password: New password: Retype new password: passwd: password updated successfully
    
  5. Delete the test account:

    sudo userdel -r pwtest
    

You can now close the root shell in your second session.

Troubleshooting

A weak password set by root is accepted, with only a BAD PASSWORD warning. That's the default: root is warned, not refused, so it can always reset a password. Test as the user, as in the Verify section. If you need root to be held to the policy too, add enforce_for_root to the pwquality.conf.d file, or use enforce=everyone in passwdqc.conf on Gentoo; scripts that set passwords as root then fail on weak ones.

The password is changed even though the policy refused it. The quality module's line has required instead of requisite, or the pam_unix line after it lacks use_authtok or try_first_pass, so pam_unix asks for a password itself and stores it. On Arch, check the line added to /etc/pam.d/passwd in step 1.

The policy file seems to be ignored. Check the file name ends in .conf, and that no argument on the PAM line overrides it: options given in /etc/pam.d/ win over the configuration files. On Gentoo, check that system-auth passes config=/etc/security/passwdqc.conf.

passwd: Authentication token manipulation error without any explanation. A module in the password stack failed or couldn't be loaded; the reason is in the system log. Use sudo journalctl -t passwd on systemd distributions, or, on Gentoo with OpenRC, in the files of the system logger you installed (with sysklogd, /var/log/auth.log).