Password policies are a fundamental component of system security, especially on enterprise-managed Linux devices. For Managed Service Providers (MSPs) and IT professionals overseeing fleets of Linux endpoints, enforcing consistent password policies is essential to reducing attack surfaces and ensuring regulatory compliance. This post explores a comprehensive shell script that configures critical password controls across various Linux distributions using authconfig, authselect, or manual PAM modifications. The ability to set the password policy for Linux devices with shell scripting adds automation and reliability to your endpoint management strategy.
Background
Many Linux environments, particularly those in enterprise or MSP-managed infrastructures, rely on decentralized systems, making it difficult to enforce uniform password requirements. Manual configuration is error-prone and inefficient at scale. While tools like Ansible and Puppet can help, they may be overkill for teams using RMM platforms like NinjaOne.
This shell script bridges the gap: it targets Debian 11+ and RHEL 8+ systems and dynamically applies password policies based on the availability of authconfig, authselect, or direct PAM configuration files. IT professionals can use shell scripting to set the password policy for Linux devices, ensuring better security hygiene without requiring heavy orchestration.
The Script
#!/usr/bin/env bash # # Description: Sets the password policy for linux devices. # By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. # Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. # Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. # Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. # Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. # Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. # Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. # EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). # # Preset Parameter: --maxLoginAttempts "replaceMeWithANumber" # Define how many incorrect password attempts are allowed before the device locks. # # Preset Parameter: --loginAttemptLockTime "replaceMeWithANumber" # Set the lock duration (in minutes) after the maximum login attempts is reached. Max Login Attempts is required (if not previously set). # # Preset Parameter: --daysUntilPasswordExpiration "replaceMeWithANumber" # Specify how many days before a password expires. # # Preset Parameter: --minimumPasswordLength "replaceMeWithANumber" # Define the minimum number of characters required for a password. # # Preset Parameter: --passwordHistory "replaceMeWithANumber" # Define how many previous passwords must be remembered before reuse. # This option is only available on Debian-based or RHEL 8+-based distributions. # https://bugzilla.redhat.com/show_bug.cgi?id=1271804 # # Preset Parameter: --help # Displays some help text. # # Minimum OS Architecture Supported: Debian 11 (Bullseye)+, Red Hat Enterprise Linux (RHEL) 8+ # # Release Notes: Initial Release # Initialize variables for various password policy parameters with default values _arg_maxLoginAttempts= _arg_loginAttemptLockTime= _arg_daysUntilPasswordExpiration= _arg_minimumPasswordLength= _arg_passwordHistory= print_help() { printf '\n\n%s\n\n' 'Usage: [--maxLoginAttempts|-a <arg>] [--loginAttemptLockTime|-t <arg>] [--daysUntilPasswordExpiration|-e <arg>] [--minimumPasswordLength|-l <arg>] [--passwordHistory|-ph <arg>] [--help|-h]' printf '%s\n' 'Preset Parameter: --maxLoginAttempts "replaceMeWithANumber"' printf '\t%s\n' "Define how many incorrect password attempts are allowed before the device locks." printf '%s\n' 'Preset Parameter: --loginAttemptLockTime "replaceMeWithANumber"' printf '\t%s\n' "Set the lock duration (in minutes) after the maximum login attempts is reached. Max Login Attempts is required (if not previously set)." printf '%s\n' 'Preset Parameter: --daysUntilPasswordExpiration "replaceMeWithANumber"' printf '\t%s\n' "Specify how many days before a password expires." printf '%s\n' 'Preset Parameter: --minimumPasswordLength "replaceMeWithANumber"' printf '\t%s\n' "Define the minimum number of characters required for a password." printf '%s\n' 'Preset Parameter: --passwordHistory "replaceMeWithANumber"' printf '\t%s\n' "Define how many previous passwords must be remembered before reuse." printf '\t%s\n' "This option is only available on Debian-based or RHEL 8+-based distributions." printf '\t%s\n' "https://bugzilla.redhat.com/show_bug.cgi?id=1271804" printf '%s\n' 'Preset Parameter: --help' printf '\t%s\n' "Displays this help menu." } die() { local _ret="${2:-1}" echo "$1" >&2 test "${_PRINT_HELP:-no}" = yes && print_help >&2 exit "${_ret}" } parse_commandline() { while test $# -gt 0; do _key="$1" case "$_key" in --maxLoginAttempts | --maxloginattempts | --attempts | -a) test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1 _arg_maxLoginAttempts=$2 shift ;; --maxLoginAttempts=*) _arg_maxLoginAttempts="${_key##--maxLoginAttempts=}" ;; --loginAttemptLockTime | --loginattemptlocktime | --locktime | -t) test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1 _arg_loginAttemptLockTime=$2 shift ;; --loginAttemptLockTime=*) _arg_loginAttemptLockTime="${_key##--loginAttemptLockTime=}" ;; --daysUntilPasswordExpiration | --daysuntilpasswordexpiration | --expiration | -e) test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1 _arg_daysUntilPasswordExpiration=$2 shift ;; --daysUntilPasswordExpiration=*) _arg_daysUntilPasswordExpiration="${_key##--daysUntilPasswordExpiration=}" ;; --minimumPasswordLength | --minimumpasswordlength | --length | -l) test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1 _arg_minimumPasswordLength=$2 shift ;; --minimumPasswordLength=*) _arg_minimumPasswordLength="${_key##--minimumPasswordLength=}" ;; --passwordHistory | --passwordhistory | --history | -ph) test $# -lt 2 && die "[Error] Missing value for the optional argument '$_key'." 1 _arg_passwordHistory=$2 shift ;; --passwordHistory=*) _arg_passwordHistory="${_key##--passwordHistory=}" ;; --help | -h) _PRINT_HELP=yes die ;; *) _PRINT_HELP=yes die "[Error] Got an unexpected argument '$1'" 1 ;; esac shift done } echo "" parse_commandline "$@" # If environment variables for the script parameters are set, override the command line argument values if [[ -n "$maxLoginAttempts" ]]; then _arg_maxLoginAttempts="$maxLoginAttempts" fi if [[ -n "$loginAttemptLockTime" ]]; then _arg_loginAttemptLockTime="$loginAttemptLockTime" fi if [[ -n "$daysUntilPasswordExpiration" ]]; then _arg_daysUntilPasswordExpiration="$daysUntilPasswordExpiration" fi if [[ -n "$minimumPasswordLength" ]]; then _arg_minimumPasswordLength="$minimumPasswordLength" fi if [[ -n "$passwordHistory" ]]; then _arg_passwordHistory="$passwordHistory" fi # Check if the script is being run as root. If not, exit with an error message. if [[ $(id -u) -ne 0 ]]; then _PRINT_HELP=no die "[Error] This script must be run with root permissions. Try running it with sudo or as the system/root user." 1 fi # Validate max login attempts. Ensure it is a positive integer greater than zero. if [[ -n "$_arg_maxLoginAttempts" ]]; then _arg_maxLoginAttempts=$(echo "$_arg_maxLoginAttempts" | xargs) if [[ -z "$_arg_maxLoginAttempts" ]]; then _PRINT_HELP=yes die "[Error] An invalid number of max login attempts was given. Please specify a positive whole number that is greater than 0." 1 fi fi if [[ "$_arg_maxLoginAttempts" =~ [^0-9] ]]; then _PRINT_HELP=yes die "[Error] An invalid value for max login attempts was given: '$_arg_maxLoginAttempts'. Please specify a positive whole number that is greater than 0." 1 fi if [[ "$_arg_maxLoginAttempts" == 0 ]]; then _PRINT_HELP=yes die "[Error] An invalid value for max login attempts was given: '$_arg_maxLoginAttempts'. Please specify a positive whole number that is greater than 0." 1 fi # Validate login attempt lock time. Ensure it is a positive integer greater than zero. if [[ -n "$_arg_loginAttemptLockTime" ]]; then _arg_loginAttemptLockTime=$(echo "$_arg_loginAttemptLockTime" | xargs) if [[ -z "$_arg_loginAttemptLockTime" ]]; then _PRINT_HELP=yes die "[Error] An invalid lock time was given. Please specify a positive whole number that is greater than 0." 1 fi fi if [[ "$_arg_loginAttemptLockTime" =~ [^0-9] ]]; then _PRINT_HELP=yes die "[Error] An invalid value for lock time was given: '$_arg_loginAttemptLockTime'. Please specify a positive whole number that is greater than 0." 1 fi if [[ "$_arg_loginAttemptLockTime" == 0 ]]; then _PRINT_HELP=yes die "[Error] An invalid value for lock time was given: '$_arg_loginAttemptLockTime'. Please specify a positive whole number that is greater than 0." 1 fi # Validate password expiration time. Ensure it is a positive whole number or 99999 for no expiration. if [[ -n "$_arg_daysUntilPasswordExpiration" ]]; then _arg_daysUntilPasswordExpiration=$(echo "$_arg_daysUntilPasswordExpiration" | xargs) if [[ -z "$_arg_daysUntilPasswordExpiration" ]]; then _PRINT_HELP=yes die "[Error] An invalid expiration time was given. Please specify a positive whole number or '99999' for no expiration." 1 fi if [[ "$_arg_daysUntilPasswordExpiration" =~ [^0-9] ]]; then _PRINT_HELP=yes die "[Error] An invalid value for password expiration was given: '$_arg_daysUntilPasswordExpiration'. Please specify a positive whole number that is greater than 0 and less than or equal to 99999." 1 fi if [[ "$_arg_daysUntilPasswordExpiration" == 0 || "$_arg_daysUntilPasswordExpiration" -gt 99999 ]]; then _PRINT_HELP=yes die "[Error] An invalid value for password expiration was given: '$_arg_daysUntilPasswordExpiration'. Please specify a positive whole number that is greater than 0 and less than or equal to 99999." 1 fi fi # Validate minimum password length. Ensure it is a positive whole number greater than or equal to 8. if [[ -n "$_arg_minimumPasswordLength" ]]; then _arg_minimumPasswordLength=$(echo "$_arg_minimumPasswordLength" | xargs) if [[ -z "$_arg_minimumPasswordLength" ]]; then _PRINT_HELP=yes die "[Error] An invalid minimum password length was given. Please specify a positive whole number that is greater than or equal to '8'." 1 fi fi if [[ "$_arg_minimumPasswordLength" =~ [^0-9] ]]; then _PRINT_HELP=yes die "[Error] An invalid value for minimum password length was given: '$_arg_minimumPasswordLength'. Please specify a positive whole number that is greater than or equal to 8." 1 fi if [[ -n "$_arg_minimumPasswordLength" && "$_arg_minimumPasswordLength" -lt 8 ]]; then _PRINT_HELP=yes die "[Error] An invalid value for minimum password length was given: '$_arg_minimumPasswordLength'. Please specify a positive whole number that is greater than or equal to 8." 1 fi # Validate password history setting. Ensure it is a positive whole number greater than zero. if [[ -n "$_arg_passwordHistory" ]]; then _arg_passwordHistory=$(echo "$_arg_passwordHistory" | xargs) if [[ -z "$_arg_passwordHistory" ]]; then _PRINT_HELP=yes die "[Error] An invalid number of passwords to remember was given. Please specify a positive whole number that is greater than 0." 1 fi fi if [[ "$_arg_passwordHistory" =~ [^0-9] ]]; then _PRINT_HELP=yes die "[Error] An invalid value for the number of passwords to remember was given: '$_arg_passwordHistory'. Please specify a positive whole number that is greater than 0." 1 fi if [[ "$_arg_passwordHistory" == 0 ]]; then _PRINT_HELP=yes die "[Error] An invalid value for the number of passwords to remember was given: '$_arg_passwordHistory'. Please specify a positive whole number that is greater than 0." 1 fi today=$(date "+%s") # Ensure that at least one password policy is being set. If none are provided and the reset flag is off, throw an error. if [[ -z "$_arg_maxLoginAttempts" && -z "$_arg_loginAttemptLockTime" && -z "$_arg_daysUntilPasswordExpiration" && -z "$_arg_minimumPasswordLength" && -z "$_arg_passwordHistory" ]]; then _PRINT_HELP=yes die "[Error] You must specify the password policy you are trying to set." 1 fi # Check if authselect and authconfig commands are available authselectAvailable=$(command -v authselect) authconfigAvailable=$(command -v authconfig) # Proceed only if authconfig is available but authselect is not if [[ -n "$authconfigAvailable" && -z "$authselectAvailable" ]]; then echo "Detected the command authconfig. Setting the password policy using authconfig." # If maximum login attempts or login attempt lock time arguments are provided if [[ -n "$_arg_maxLoginAttempts" || -n "$_arg_loginAttemptLockTime" ]]; then echo "" echo "Checking the current status of the login attempt lock and lock time." # Check current status of faillock settings failLockStatus=$(authconfig --test | grep "pam_faillock" | grep -v "disabled") # Extract current maximum login attempts and lock time values currentAttempts=$(authconfig --test | grep "pam_faillock" | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*" | xargs) currentLockTime=$(authconfig --test | grep "pam_faillock" | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*" | xargs) # Convert lock time from seconds to minutes if available if [[ -n "$currentLockTime" ]]; then currentLockTimeMinutes=$((currentLockTime / 60)) fi # If login attempt lock time is provided, convert to seconds if [[ -n "$_arg_loginAttemptLockTime" ]]; then loginAttemptLockTimeMinutes="$_arg_loginAttemptLockTime" _arg_loginAttemptLockTime=$((_arg_loginAttemptLockTime * 60)) fi echo "Successfully retrieved the current lock status and lock time." # If faillock module is not enabled, enable it if [[ -z "$failLockStatus" ]]; then echo "Enabling faillock PAM module." if ! authconfig --enablefaillock --update; then _PRINT_HELP=no die "[Error] Failed to enable faillock. This is required to set the max login attempts allowed as well as the lock time." 1 fi # Verify that faillock has been enabled successfully failLockStatus=$(authconfig --test | grep "pam_faillock" | grep -v "disabled") if [[ -z "$failLockStatus" ]]; then _PRINT_HELP=no die "[Error] Failed to enable faillock. This is required to set the max login attempts allowed as well as the lock time." 1 fi fi # Check if max login attempts value is already set correctly if [[ -n "$_arg_maxLoginAttempts" && -n "$currentAttempts" && "$_arg_maxLoginAttempts" == "$currentAttempts" ]]; then echo "The maximum login attempts is already set to '$_arg_maxLoginAttempts'. Skipping." _arg_maxLoginAttempts= fi # Check if lock time value is already set correctly if [[ -n "$_arg_loginAttemptLockTime" && -n "$currentLockTime" && "$_arg_loginAttemptLockTime" == "$currentLockTime" ]]; then echo "The lock time is already set to '$loginAttemptLockTimeMinutes' minutes. Skipping." _arg_loginAttemptLockTime= fi fi # Exit with error if only max login attempts or lock time is provided without the other if [[ -n "$_arg_maxLoginAttempts" && -z "$_arg_loginAttemptLockTime" && -z "$currentLockTime" ]]; then _PRINT_HELP=yes die "[Error] When specifying the maximum number of login attempts, you must also specify the login attempt lock time." 1 fi if [[ -n "$_arg_loginAttemptLockTime" && -z "$_arg_maxLoginAttempts" && -z "$currentAttempts" ]]; then _PRINT_HELP=yes die "[Error] When specifying a login attempt lock time you must also specify the maximum login attempts." 1 fi # If both maximum login attempts and lock time arguments are provided, update authconfig policy if [[ -n "$_arg_maxLoginAttempts" && -n "$_arg_loginAttemptLockTime" ]]; then echo "Setting the maximum login attempts and lock time in the authconfig policy." # Update authconfig with max login attempts and lock time if ! authconfig --faillockargs="deny=$_arg_maxLoginAttempts unlock_time=$_arg_loginAttemptLockTime" --update; then _PRINT_HELP=no die "[Error] Failed to set the maximum login attempts and lock time in the authconfig policy." 1 fi # Verify the updated values newAttempts=$(authconfig --test | grep "pam_faillock" | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*" | xargs) newLockTime=$(authconfig --test | grep "pam_faillock" | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*" | xargs) if [[ -n "$newAttempts" && "$newAttempts" == "$_arg_maxLoginAttempts" ]]; then echo "Successfully added the max login attempts to the authconfig policy." else _PRINT_HELP=no die "[Error] Failed to add the max login attempts to the authconfig policy." 1 fi if [[ -n "$newLockTime" && "$newLockTime" == "$_arg_loginAttemptLockTime" ]]; then echo "Successfully set the lock time in the authconfig policy." else _PRINT_HELP=no die "[Error] Failed to set the lock time in the authconfig policy." 1 fi fi # Update only maximum login attempts if lock time is not provided if [[ -n "$_arg_maxLoginAttempts" && -z "$_arg_loginAttemptLockTime" ]]; then echo "Modifying the maximum login attempts from '$currentAttempts' to '$_arg_maxLoginAttempts' in the authconfig policy." # Update max login attempts in authconfig if ! authconfig --faillockargs="deny=$_arg_maxLoginAttempts unlock_time=$currentLockTime" --update; then _PRINT_HELP=no die "[Error] Failed to modify the max login attempts in the authconfig policy." 1 fi # Verify the new maximum login attempts value newAttempts=$(authconfig --test | grep "pam_faillock" | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*" | xargs) if [[ -n "$newAttempts" && "$newAttempts" == "$_arg_maxLoginAttempts" ]]; then echo "Successfully modified the max login attempts in the authconfig policy." else _PRINT_HELP=no die "[Error] Failed to modify the max login attempts in the authconfig policy." 1 fi fi # Update only lock time if max login attempts is not provided if [[ -n "$_arg_loginAttemptLockTime" && -z "$_arg_maxLoginAttempts" ]]; then echo "Modifying the current lock time from '$currentLockTimeMinutes' minutes to '$loginAttemptLockTimeMinutes' minutes in the authconfig policy." # Update lock time in authconfig if ! authconfig --faillockargs="deny=$currentAttempts unlock_time=$_arg_loginAttemptLockTime" --update; then _PRINT_HELP=no die "[Error] Failed to modify the lock time in the authconfig policy." 1 fi # Verify the new lock time value newLockTime=$(authconfig --test | grep "pam_faillock" | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*" | xargs) if [[ -n "$newLockTime" && "$newLockTime" == "$_arg_loginAttemptLockTime" ]]; then echo "Successfully modified the lock time of the authconfig policy." else _PRINT_HELP=no die "[Error] Failed to set the lock time in the authconfig policy." 1 fi fi # If minimum password length is specified, retrieve and update the policy if [[ -n "$_arg_minimumPasswordLength" ]]; then echo "" echo "Retrieving the current password quality policy." # Check if the password quality configuration file exists if [[ ! -f "/etc/security/pwquality.conf" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/security/pwquality.conf' does not exist. Unable to read the current password quality policy." 1 fi # Get the current minimum password length value currentMinimumLength=$(grep -v "^#" /etc/security/pwquality.conf | grep -v '^\s*$' | grep -o -e "minlen[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*" | xargs) echo "Successfully retrieved the current password quality policy." # If the minimum password length is already set correctly, skip the update if [[ -n "$currentMinimumLength" && "$currentMinimumLength" == "$_arg_minimumPasswordLength" ]]; then echo "The minimum password length is already '$_arg_minimumPasswordLength'. Skipping." _arg_minimumPasswordLength= fi fi # Set or update the minimum password length in the policy if [[ -n "$_arg_minimumPasswordLength" ]]; then if [[ -n "$currentMinimumLength" ]]; then echo "Updating the minimum password length from '$currentMinimumLength' to '$_arg_minimumPasswordLength'." else echo "Setting the minimum password length to '$_arg_minimumPasswordLength'." fi # Update the password quality policy with the new minimum length if ! authconfig --passminlen="$_arg_minimumPasswordLength" --update; then _PRINT_HELP=no die "[Error] Failed to modify the password length in the authconfig policy." 1 fi # Verify the updated minimum password length value newMinimumLength=$(grep -v "^#" /etc/security/pwquality.conf | grep -v '^\s*$' | grep -o -e "minlen[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*" | xargs) if [[ -n "$newMinimumLength" && "$newMinimumLength" == "$_arg_minimumPasswordLength" ]]; then echo "Successfully set the minimum password length." else _PRINT_HELP=no die "[Error] Failed to modify the minimum password length in '/etc/security/pwquality.conf'." 1 fi fi fi # Check if authselect is available if [[ -n "$authselectAvailable" ]]; then echo "The command authselect was detected. Setting the password policy using authselect." # Check if max login attempts or lock time arguments are provided if [[ -n "$_arg_maxLoginAttempts" || -n "$_arg_loginAttemptLockTime" ]]; then echo "" echo "Checking the current status of the login attempt lock and lock time." # Check if the faillock module is enabled in authselect failLockStatus=$(authselect current | grep "with-faillock") # Exit if faillock.conf file does not exist if [[ ! -f "/etc/security/faillock.conf" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/security/faillock.conf' does not exist. Unable to read the current faillock policy." 1 fi # Retrieve current maximum login attempts and lock time from faillock.conf currentAttempts=$(grep -v "^#" /etc/security/faillock.conf | grep -v '^\s*$' | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") currentLockTime=$(grep -v "^#" /etc/security/faillock.conf | grep -v '^\s*$' | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") # Convert lock time to minutes if it exists if [[ -n "$currentLockTime" ]]; then currentLockTimeMinutes=$((currentLockTime / 60)) fi # Convert provided lock time to seconds for internal use if [[ -n "$_arg_loginAttemptLockTime" ]]; then loginAttemptLockTimeMinutes="$_arg_loginAttemptLockTime" _arg_loginAttemptLockTime=$((_arg_loginAttemptLockTime * 60)) fi echo "Successfully retrieved the current lock status and lock time." # Enable faillock if not currently enabled if [[ -z "$failLockStatus" ]]; then echo "Enabling faillock PAM module." if ! authselect enable-feature with-faillock; then _PRINT_HELP=no die "[Error] Failed to enable faillock. This is required to set the max login attempts allowed as well as the lock time." 1 fi if ! authselect apply-changes; then _PRINT_HELP=no die "[Error] Failed to enable faillock. This is required to set the max login attempts allowed as well as the lock time." 1 fi # Confirm if faillock has been enabled successfully failLockStatus=$(authselect current | grep "with-faillock") if [[ -z "$failLockStatus" ]]; then _PRINT_HELP=no die "[Error] Failed to enable faillock. This is required to set the max login attempts allowed as well as the lock time." 1 fi fi # Skip updating if max login attempts or lock time are already set to the desired values if [[ -n "$_arg_maxLoginAttempts" && -n "$currentAttempts" && "$_arg_maxLoginAttempts" == "$currentAttempts" ]]; then echo "The maximum login attempts is already set to '$_arg_maxLoginAttempts'. Skipping." _arg_maxLoginAttempts= fi if [[ -n "$_arg_loginAttemptLockTime" && -n "$currentLockTime" && "$_arg_loginAttemptLockTime" == "$currentLockTime" ]]; then echo "The lock time is already set to '$loginAttemptLockTimeMinutes' minutes. Skipping." _arg_loginAttemptLockTime= fi fi # Exit with error if only max login attempts or lock time is provided without the other if [[ -n "$_arg_maxLoginAttempts" && -z "$_arg_loginAttemptLockTime" && -z "$currentLockTime" ]]; then _PRINT_HELP=yes die "[Error] When specifying the maximum number of login attempts, you must also specify the login attempt lock time." 1 fi if [[ -n "$_arg_loginAttemptLockTime" && -z "$_arg_maxLoginAttempts" && -z "$currentAttempts" ]]; then _PRINT_HELP=yes die "[Error] When specifying a login attempt lock time you must also specify the maximum login attempts." 1 fi # Backup faillock.conf if changes are to be made if [[ -n "$_arg_maxLoginAttempts" || -n "$_arg_loginAttemptLockTime" ]]; then faillockBackup="/etc/security/$today-faillock.conf.backup" echo "Backing up '/etc/security/faillock.conf' to '$faillockBackup'" # Exit if backup file already exists to avoid overwriting if [[ -f "$faillockBackup" ]]; then _PRINT_HELP=no die "[Error] The backup file '$faillockBackup' already exists. Unable to backup faillock policy." 1 fi # Attempt to create backup of faillock.conf, exit if it fails if ! cp /etc/security/faillock.conf "$faillockBackup"; then _PRINT_HELP=no die "[Error] Unable to backup faillock policy. Failed to save file to '$faillockBackup'" 1 fi # Confirm if the backup was successful if [[ -f "$faillockBackup" ]]; then echo "Backup created successfully." else _PRINT_HELP=no die "[Error] Unable to backup faillock. Failed to save the file to '$faillockBackup'" 1 fi fi # Configure maximum login attempts and lock time in faillock policy if they are not set already if [[ -n "$_arg_maxLoginAttempts" && -n "$_arg_loginAttemptLockTime" && -z "$currentAttempts" && -z "$currentLockTime" ]]; then echo "Configuring the maximum login attempts and lock time in the faillock policy." # Define the new faillock policy values faillockPolicy="silent deny = $_arg_maxLoginAttempts unlock_time = $_arg_loginAttemptLockTime" # Append new policy to faillock.conf, exit if it fails if ! echo "$faillockPolicy" >>"/etc/security/faillock.conf"; then _PRINT_HELP=no die "[Error] Failed to configure the maximum login attempts and lock time in the faillock policy." 1 fi # Confirm if the values were correctly set in faillock.conf newAttempts=$(grep -v "^#" /etc/security/faillock.conf | grep -v '^\s*$' | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") newLockTime=$(grep -v "^#" /etc/security/faillock.conf | grep -v '^\s*$' | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") # Verify if new max login attempts were set correctly if [[ -n "$newAttempts" && "$newAttempts" == "$_arg_maxLoginAttempts" ]]; then echo "Successfully set the maximum login attempts in the faillock policy." else _PRINT_HELP=no die "[Error] Unable to configure the maximum login attempts in the faillock policy." 1 fi if [[ -n "$newLockTime" && "$newLockTime" == "$_arg_loginAttemptLockTime" ]]; then echo "Successfully added the lock time to the faillock policy." else _PRINT_HELP=no die "[Error] Failed to add the lock time to the faillock policy." 1 fi fi # Update max login attempts in faillock.conf if the current attempts differ from desired setting if [[ -n "$_arg_maxLoginAttempts" && -n "$currentAttempts" ]]; then echo "Updating the current maximum login attempts from '$currentAttempts' to '$_arg_maxLoginAttempts' in the faillock policy." # Update deny value in faillock.conf using sed, exit if it fails if ! sed -i "s/^[^#]*deny[[:space:]]*=[[:space:]]*[0-9]*/deny = $_arg_maxLoginAttempts/g" "/etc/security/faillock.conf"; then _PRINT_HELP=no die "[Error] Failed to modify the max login attempts in the faillock policy." 1 fi # Confirm if the updated max login attempts value was set correctly newAttempts=$(grep -v "^#" /etc/security/faillock.conf | grep -v '^\s*$' | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newAttempts" && "$newAttempts" == "$_arg_maxLoginAttempts" ]]; then echo "Successfully updated the maximum login attempts in the faillock policy." else _PRINT_HELP=no die "[Error] Failed to modify the max login attempts in the faillock policy." 1 fi fi # Check if a new lock time argument is provided and differs from the current setting if [[ -n "$_arg_loginAttemptLockTime" && -n "$currentLockTime" ]]; then echo "Updating the lock time from '$currentLockTimeMinutes' minutes to '$loginAttemptLockTimeMinutes' minutes in the faillock policy." # Modify the unlock_time value in faillock.conf, exiting with error if the operation fails if ! sed -i "s/^[^#]*unlock_time[[:space:]]*=[[:space:]]*[0-9]*/unlock_time = $_arg_loginAttemptLockTime/g" "/etc/security/faillock.conf"; then _PRINT_HELP=no die "[Error] Failed to modify the lock time in the faillock policy." 1 fi # Retrieve and verify the updated lock time from faillock.conf newLockTime=$(grep -v "^#" /etc/security/faillock.conf | grep -v '^\s*$' | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newLockTime" && "$newLockTime" == "$_arg_loginAttemptLockTime" ]]; then echo "Successfully modified the lock time in the faillock policy." else _PRINT_HELP=no die "[Error] Failed to modify the lock time in the faillock policy." 1 fi fi # Check if a minimum password length argument is provided if [[ -n "$_arg_minimumPasswordLength" ]]; then echo "" echo "Retrieving the current password quality policy." # Exit with error if pwquality.conf does not exist if [[ ! -f "/etc/security/pwquality.conf" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/security/pwquality.conf' does not exist. Unable to read the current password quality policy." 1 fi # Retrieve the current minimum password length setting from pwquality.conf currentMinimumLength=$(grep -v "^#" /etc/security/pwquality.conf | grep -v '^\s*$' | grep -o -e "minlen[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") echo "Successfully retrieved the current password quality policy." # Skip updating if the current minimum length matches the desired setting if [[ -n "$currentMinimumLength" && "$currentMinimumLength" == "$_arg_minimumPasswordLength" ]]; then echo "The minimum password length is already '$_arg_minimumPasswordLength'. Skipping." _arg_minimumPasswordLength= fi fi # Back up pwquality.conf if changes are to be made if [[ -n "$_arg_minimumPasswordLength" ]]; then passwordQualityBackup="/etc/security/$today-pwquality.conf.backup" echo "Backing up '/etc/security/pwquality.conf' to '$passwordQualityBackup'" # Exit if backup file already exists to prevent overwriting if [[ -f "$passwordQualityBackup" ]]; then _PRINT_HELP=no die "[Error] The backup file '$passwordQualityBackup' already exists. Unable to backup password quality policy." 1 fi # Attempt to create the backup, exiting if it fails if ! cp "/etc/security/pwquality.conf" "$passwordQualityBackup"; then _PRINT_HELP=no die "[Error] Failed to back up the password quality policy to '$passwordQualityBackup'." 1 fi # Confirm if the backup was successful if [[ -f "$passwordQualityBackup" ]]; then echo "Backup created successfully." else _PRINT_HELP=no die "[Error] Failed to back up the password quality policy to '$passwordQualityBackup'." 1 fi fi # Set minimum password length if no current setting exists if [[ -n "$_arg_minimumPasswordLength" && -z "$currentMinimumLength" ]]; then echo "Setting the minimum password length to '$_arg_minimumPasswordLength'." # Append new minlen value to pwquality.conf, exiting with error if the operation fails if ! echo "minlen = $_arg_minimumPasswordLength" >>"/etc/security/pwquality.conf"; then _PRINT_HELP=no die "[Error] Unable to set the minimum password length in '/etc/security/pwquality.conf'." 1 fi # Confirm the updated minimum password length value newMinimumLength=$(grep -v "^#" /etc/security/pwquality.conf | grep -v '^\s*$' | grep -o -e "minlen[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newMinimumLength" && "$newMinimumLength" == "$_arg_minimumPasswordLength" ]]; then echo "Successfully set the minimum password length." else _PRINT_HELP=no die "[Error] Unable to set the minimum password length in '/etc/security/pwquality.conf'." 1 fi fi # Update the minimum password length if a current setting already exists if [[ -n "$_arg_minimumPasswordLength" && -n "$currentMinimumLength" ]]; then echo "Updating the minimum password length from '$currentMinimumLength' to '$_arg_minimumPasswordLength'." # Modify minlen value in pwquality.conf using sed, exit if the operation fails if ! sed -i "s/^[^#]*minlen[[:space:]]*=[[:space:]]*[0-9]*/minlen = $_arg_minimumPasswordLength/g" "/etc/security/pwquality.conf"; then _PRINT_HELP=no die "[Error] Failed to update the password length policy." 1 fi # Verify if the update was successful newMinimumLength=$(grep -v "^#" /etc/security/pwquality.conf | grep -v '^\s*$' | grep -o -e "minlen[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newMinimumLength" && "$newMinimumLength" == "$_arg_minimumPasswordLength" ]]; then echo "Successfully updated the minimum password length." else _PRINT_HELP=no die "[Error] Failed to update the minimum password length in '/etc/security/pwquality.conf'." 1 fi fi # Check if a password history argument is provided if [[ -n "$_arg_passwordHistory" ]]; then echo "" echo "Retrieving the current password history policy." # Exit with error if pwhistory.conf does not exist if [[ ! -f "/etc/security/pwhistory.conf" ]]; then errorMessage="[Error] The file '/etc/security/pwhistory.conf' does not exist. [Error] This system may not support changing the password history. [Error] https://bugzilla.redhat.com/show_bug.cgi?id=2063379 [Error] Unable to read the current password history policy. " _PRINT_HELP=no die "$errorMessage" 1 fi # Retrieve current password history setting from pwhistory.conf currentPasswordHistory=$(grep -v "^#" /etc/security/pwhistory.conf | grep -v '^\s*$' | grep -o -e "remember[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") echo "Successfully retrieved the current password history policy." # Skip updating if current password history matches desired setting if [[ -n "$currentPasswordHistory" && "$currentPasswordHistory" == "$_arg_passwordHistory" ]]; then echo "The minimum password history is already set to remember the past '$_arg_passwordHistory' passwords. Skipping." _arg_passwordHistory= fi fi # Back up pwhistory.conf if changes are to be made if [[ -n "$_arg_passwordHistory" ]]; then passwordHistoryBackup="/etc/security/$today-pwhistory.conf.backup" echo "Backing up '/etc/security/pwhistory.conf' to '$passwordHistoryBackup'" # Exit if backup file already exists if [[ -f "$passwordHistoryBackup" ]]; then _PRINT_HELP=no die "[Error] The backup file '$passwordHistoryBackup' already exists. Unable to backup the password history policy." 1 fi # Attempt to create backup, exiting if it fails if ! cp "/etc/security/pwhistory.conf" "$passwordHistoryBackup"; then _PRINT_HELP=no die "[Error] Failed to back up the password history policy to '$passwordHistoryBackup'." 1 fi # Confirm if the backup was successful if [[ -f "$passwordHistoryBackup" ]]; then echo "Backup created successfully." else _PRINT_HELP=no die "[Error] Failed to back up the password history policy to '$passwordHistoryBackup'." 1 fi fi # Set password history requirement if no current setting exists if [[ -n "$_arg_passwordHistory" && -z "$currentPasswordHistory" ]]; then echo "Setting the password history requirement." # Append remember value to pwhistory.conf, exit if operation fails if ! echo "remember = $_arg_passwordHistory" >>"/etc/security/pwhistory.conf"; then _PRINT_HELP=no die "[Error] Failed to configure the password history requirement in '/etc/security/pwhistory.conf'." 1 fi # Verify if the password history setting was added successfully newPasswordHistory=$(grep -v "^#" /etc/security/pwhistory.conf | grep -v '^\s*$' | grep -o -e "remember[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newPasswordHistory" && "$newPasswordHistory" == "$_arg_passwordHistory" ]]; then echo "Successfully set the password history requirement." else _PRINT_HELP=no die "[Error] Failed to set the password history requirement in the file '/etc/security/pwhistory.conf'." 1 fi fi # Update password history requirement if current setting already exists if [[ -n "$_arg_passwordHistory" && -n "$currentPasswordHistory" ]]; then echo "Updating the password history requirement from remembering the last '$currentPasswordHistory' passwords to the last '$_arg_passwordHistory' passwords." # Modify remember value in pwhistory.conf, exit if operation fails if ! sed -i "s/^[^#]*remember[[:space:]]*=[[:space:]]*[0-9]*/remember = $_arg_passwordHistory/g" "/etc/security/pwhistory.conf"; then _PRINT_HELP=no die "[Error] Failed to update the password history policy." 1 fi # Verify if the update was successful newPasswordHistory=$(grep -v "^#" /etc/security/pwhistory.conf | grep -v '^\s*$' | grep -o -e "remember[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newPasswordHistory" && "$newPasswordHistory" == "$_arg_passwordHistory" ]]; then echo "Successfully updated the password history requirement." else _PRINT_HELP=no die "[Error] Failed to update the password history requirement in the file '/etc/security/pwhistory.conf'." 1 fi fi fi # Check if a days until password expiration argument is provided if [[ -n "$_arg_daysUntilPasswordExpiration" ]]; then echo "" echo "Retrieving the current password expiration policy." # Exit with error if login.defs file does not exist if [[ ! -f "/etc/login.defs" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/login.defs' does not exist. Unable to read the current password expiration policy." 1 fi # Exit with error if login.defs file is empty if [[ ! -s "/etc/login.defs" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/login.defs' is empty. Unable to read the current password expiration policy." 1 fi # Retrieve current maximum password age setting from login.defs currentMaxAge=$(grep "^PASS_MAX_DAYS" "/etc/login.defs" | grep -o -e "[0-9]*") echo "Successfully retrieved the current password expiration policy." # Skip updating if current max age matches desired setting if [[ -n "$currentMaxAge" && "$currentMaxAge" == "$_arg_daysUntilPasswordExpiration" ]]; then echo "The current password expiration policy is already set to a maximum of '$_arg_daysUntilPasswordExpiration' days. Skipping." skipMaxAge="on" fi fi # Back up login.defs if changes to password expiration are needed if [[ -n "$_arg_daysUntilPasswordExpiration" && "$skipMaxAge" != "on" ]]; then loginDefsBackup="/etc/$today-login.defs.backup" echo "Backing up '/etc/login.defs' to '$loginDefsBackup'" # Exit if backup file already exists to prevent overwriting if [[ -f "$loginDefsBackup" ]]; then _PRINT_HELP=no die "[Error] The backup file '$loginDefsBackup' already exists. Unable to backup login.defs." 1 fi # Attempt to create backup, exiting if it fails if ! cp /etc/login.defs "$loginDefsBackup"; then _PRINT_HELP=no die "[Error] Unable to backup login.defs. Failed to save file to '$loginDefsBackup'" 1 fi # Confirm if the backup was successful if [[ -f "$loginDefsBackup" ]]; then echo "Backup created successfully." else _PRINT_HELP=no die "[Error] Unable to backup login.defs. Failed to save file to '$loginDefsBackup'" 1 fi fi # Set maximum password age if there is no existing setting if [[ -n "$_arg_daysUntilPasswordExpiration" && -z "$currentMaxAge" && "$skipMaxAge" != "on" ]]; then echo "Configuring the maximum password age in '/etc/login.defs'." # Append new PASS_MAX_DAYS value to login.defs, exiting if the operation fails if ! echo "PASS_MAX_DAYS $_arg_daysUntilPasswordExpiration" >>"/etc/login.defs"; then _PRINT_HELP=no die "[Error] Failed to configure the password expiration policy in '/etc/login.defs'." 1 fi # Verify if the maximum age setting was added successfully newMaxAge=$(grep "^PASS_MAX_DAYS" "/etc/login.defs" | grep -o -e "[0-9]*") if [[ -n "$newMaxAge" && "$newMaxAge" == "$_arg_daysUntilPasswordExpiration" ]]; then echo "Successfully added a password expiration of '$_arg_daysUntilPasswordExpiration' days for new users." else _PRINT_HELP=no die "[Error] Failed to configure the password expiration policy in '/etc/login.defs'." 1 fi fi # Update existing maximum password age if a current setting is already present if [[ -n "$_arg_daysUntilPasswordExpiration" && -n "$currentMaxAge" && "$skipMaxAge" != "on" ]]; then echo "Updating the maximum password age from '$currentMaxAge' to '$_arg_daysUntilPasswordExpiration'." # Modify PASS_MAX_DAYS value in login.defs using sed, exiting if the operation fails if ! sed -i "s/^PASS_MAX_DAYS[[:space:]]*[0-9]*/PASS_MAX_DAYS $_arg_daysUntilPasswordExpiration/g" "/etc/login.defs"; then _PRINT_HELP=no die "[Error] Failed to set the maximum password age in '/etc/login.defs'." 1 fi # Confirm the update was successful newMaxAge=$(grep "^PASS_MAX_DAYS" "/etc/login.defs" | grep -o -e "[0-9]*") if [[ -n "$newMaxAge" && "$newMaxAge" == "$_arg_daysUntilPasswordExpiration" ]]; then echo "Successfully modified the maximum password age for new users." else _PRINT_HELP=no die "[Error] Failed to configure the password expiration policy in '/etc/login.defs'." 1 fi fi # Apply password expiration policy to existing accounts if a days until expiration argument is provided if [[ -n "$_arg_daysUntilPasswordExpiration" ]]; then echo "Retrieving list of existing accounts." # Retrieve user information, exiting if the operation fails if ! allUsers=$(cut -f 1,3 -d ':' "/etc/passwd"); then _PRINT_HELP=no die "[Error] Failed to retrieve existing users from '/etc/passwd'." 1 fi # Retrieve minimum and maximum user IDs from login.defs, exiting if retrieval fails if ! minId=$(grep "^UID_MIN" "/etc/login.defs" | grep -o -e "[0-9]*"); then _PRINT_HELP=no die "[Error] Failed to find the minimum user ID in '/etc/login.defs'." 1 fi if ! maxId=$(grep "^UID_MAX" "/etc/login.defs" | grep -o -e "[0-9]*"); then _PRINT_HELP=no die "[Error] Failed to find the minimum user ID in '/etc/login.defs'." 1 fi # Ensure both minimum and maximum IDs were found, otherwise exit with error if [[ -z "$minId" || -z "$maxId" ]]; then _PRINT_HELP=no die "[Error] Failed to find the minimum or maximum user ID in '/etc/login.defs'." 1 fi echo "Setting the maximum password age for existing accounts." # Loop through each user to apply the expiration policy if the user ID is within the range for user in $allUsers; do uid=$(echo "$user" | cut -f 2 -d ":") # Check if the user ID is within the defined range if [[ "$uid" -ge "$minId" && "$uid" -le "$maxId" ]]; then usernameToModify=$(echo "$user" | cut -f 1 -d ":") currentMaxAge=$(chage -l "$usernameToModify" | grep Max | grep -o -e "[0-9]*" | xargs) # Skip updating if the user's maximum password age already matches the desired setting if [[ -n "$currentMaxAge" && "$currentMaxAge" == "$_arg_daysUntilPasswordExpiration" ]]; then echo "The user '$usernameToModify' already has a maximum password age of '$_arg_daysUntilPasswordExpiration'. Skipping." continue fi echo "Updating the maximum password age for the user '$usernameToModify'." # Attempt to apply the expiration policy, exiting with error if it fails if ! chage --maxdays "$_arg_daysUntilPasswordExpiration" "$usernameToModify"; then _PRINT_HELP=no die "[Error] Failed to update the maximum password age for the user '$usernameToModify'." 1 fi # Confirm the update was successful newMaxAge=$(chage -l "$usernameToModify" | grep Max | grep -o -e "[0-9]*" | xargs) if [[ -z "$newMaxAge" || "$newMaxAge" != "$_arg_daysUntilPasswordExpiration" ]]; then _PRINT_HELP=no die "[Error] Failed to update the maximum password age for the user '$usernameToModify'." 1 else echo "Successfully updated the maximum password age for the user '$usernameToModify'." fi fi done fi # Check if authselect or authconfig is available if [[ -n "$authselectAvailable" || -n "$authconfigAvailable" ]]; then # If password history is specified and only authconfig is available, exit with error if [[ -n "$_arg_passwordHistory" && -n "$authconfigAvailable" && -z "$authselectAvailable" ]]; then errorMessage=" [Error] This system uses authconfig to modify the PAM configuration files. [Error] Unfortunately, authconfig does not provide an option to implement a password history requirement and will overwrite all manual changes every time it is run. [Error] Please upgrade your system to a distribution that supports authselect or allows manual edits of PAM configuration files. [Error] https://bugzilla.redhat.com/show_bug.cgi?id=1271804" _PRINT_HELP=no die "$errorMessage" 1 fi exit fi # Check if max login attempts or lock time is provided if [[ -n "$_arg_maxLoginAttempts" || -n "$_arg_loginAttemptLockTime" ]]; then echo "" echo "Attempting to retrieve the current PAM authentication policy." # Check if common-auth file exists and is not empty if [[ ! -f "/etc/pam.d/common-auth" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/pam.d/common-auth' does not exist. Cannot read the current PAM authentication policy." 1 fi if [[ ! -s "/etc/pam.d/common-auth" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/pam.d/common-auth' is empty. Cannot read the current PAM authentication policy." 1 fi # Retrieve current max login attempts and lock time from common-auth currentAttempts=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep "pam_faillock.so" | grep "preauth" | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") currentLockTime=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep "pam_faillock.so" | grep "preauth" | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") # Convert lock time to minutes if it exists if [[ -n "$currentLockTime" ]]; then currentLockTimeMinutes=$((currentLockTime / 60)) fi # Convert provided lock time to seconds if specified if [[ -n "$_arg_loginAttemptLockTime" ]]; then loginAttemptLockTimeMinutes="$_arg_loginAttemptLockTime" _arg_loginAttemptLockTime=$((_arg_loginAttemptLockTime * 60)) fi echo "Current PAM authentication policy retrieved successfully." # Skip max login attempts setting if it matches the current configuration if [[ -n "$_arg_maxLoginAttempts" && -n "$currentAttempts" && "$_arg_maxLoginAttempts" == "$currentAttempts" ]]; then echo "The maximum login attempts is already set to '$_arg_maxLoginAttempts'. Skipping." _arg_maxLoginAttempts= fi # Skip lock time setting if it matches the current configuration if [[ -n "$_arg_loginAttemptLockTime" && -n "$currentLockTime" && "$_arg_loginAttemptLockTime" == "$currentLockTime" ]]; then echo "The lock time is already set to '$loginAttemptLockTimeMinutes' minutes. Skipping." _arg_loginAttemptLockTime= fi fi # Check for missing lock time or max attempts arguments if [[ -n "$_arg_maxLoginAttempts" && -z "$_arg_loginAttemptLockTime" && -z "$currentLockTime" ]]; then _PRINT_HELP=yes die "[Error] When specifying the maximum number of login attempts, you must also specify the login attempt lock time." 1 fi if [[ -n "$_arg_loginAttemptLockTime" && -z "$_arg_maxLoginAttempts" && -z "$currentAttempts" ]]; then _PRINT_HELP=yes die "[Error] When specifying a login attempt lock time you must also specify the maximum login attempts." 1 fi # Create a backup of the common-auth file if settings need to be applied if [[ -n "$_arg_maxLoginAttempts" || -n "$_arg_loginAttemptLockTime" ]]; then commonAuthBackup="/etc/pam.d/$today-common-auth.backup" echo "Creating a backup of '/etc/pam.d/common-auth' to '$commonAuthBackup'." # Exit if a backup file already exists if [[ -f "$commonAuthBackup" ]]; then _PRINT_HELP=no die "[Error] Backup file '$commonAuthBackup' already exists. Cannot create a backup of the common-auth policy." 1 fi # Attempt to create a backup, exiting if it fails if ! cp /etc/pam.d/common-auth "$commonAuthBackup"; then _PRINT_HELP=no die "[Error] Failed to create a backup of the common-auth policy to '$commonAuthBackup'." 1 fi # Confirm successful backup creation if [[ -f "$commonAuthBackup" ]]; then echo "Backup created successfully." else _PRINT_HELP=no die "[Error] Unable to backup common-auth. Failed to save file to '$commonAuthBackup'" 1 fi fi # Apply max login attempts and lock time settings if none exist in the current policy if [[ -n "$_arg_maxLoginAttempts" && -n "$_arg_loginAttemptLockTime" && -z "$currentAttempts" && -z "$currentLockTime" ]]; then echo "Configuring the maximum login attempts and lock time in the PAM authentication policy." # Prepend authfail and preauth lines to common-auth for faillock, exiting if any operation fails if ! sed -i "1i auth required pam_faillock.so authfail deny=$_arg_maxLoginAttempts unlock_time=$_arg_loginAttemptLockTime" "/etc/pam.d/common-auth"; then _PRINT_HELP=no die "[Error] Failed to configure the maximum login attempts and lock time in the PAM authentication policy." 1 fi if ! sed -i "1i auth required pam_faillock.so preauth silent deny=$_arg_maxLoginAttempts unlock_time=$_arg_loginAttemptLockTime" "/etc/pam.d/common-auth"; then _PRINT_HELP=no die "[Error] Failed to configure the maximum login attempts and lock time in the PAM authentication policy." 1 fi # Verify if the settings were applied successfully newAttempts=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep "pam_faillock.so" | grep "preauth" | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") newLockTime=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep "pam_faillock.so" | grep "preauth" | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newAttempts" && "$newAttempts" == "$_arg_maxLoginAttempts" ]]; then echo "Successfully configured the maximum login attempts in the PAM authentication policy." else _PRINT_HELP=no die "[Error] Failed to add the max login attempts to the PAM authentication policy." 1 fi if [[ -n "$newLockTime" && "$newLockTime" == "$_arg_loginAttemptLockTime" ]]; then echo "Successfully added the lock time to the PAM authentication policy." else _PRINT_HELP=no die "[Error] Failed to configure the lock time in the PAM authentication policy." 1 fi fi # Modify existing max login attempts setting if it doesn't match the desired setting if [[ -n "$_arg_maxLoginAttempts" && -n "$currentAttempts" ]]; then echo "Modifying the current max login attempts from '$currentAttempts' to '$_arg_maxLoginAttempts' in the PAM authentication policy." # Update max login attempts in preauth and authfail lines, exiting if any operation fails existingPreAuthLine=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep -e "pam_faillock\.so [[:space:]]*preauth") newAttemptPreAuthLine=$(echo "$existingPreAuthLine" | sed "s/deny[[:space:]]*=[[:space:]]*[0-9]*/deny=$_arg_maxLoginAttempts/g") if ! sed -i "s/$existingPreAuthLine/$newAttemptPreAuthLine/g" "/etc/pam.d/common-auth"; then _PRINT_HELP=no die "[Error] Failed to modify the max login attempts in the PAM authentication policy 1." 1 fi existingAuthFailLine=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep -e "pam_faillock\.so [[:space:]]*authfail") newAttemptAuthFailLine=$(echo "$existingAuthFailLine" | sed "s/deny[[:space:]]*=[[:space:]]*[0-9]*/deny=$_arg_maxLoginAttempts/g") if ! sed -i "s/$existingAuthFailLine/$newAttemptAuthFailLine/g" "/etc/pam.d/common-auth"; then _PRINT_HELP=no die "[Error] Failed to modify the max login attempts in the PAM authentication policy 2." 1 fi # Confirm the new max attempts setting was applied newAttempts=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep "pam_faillock.so" | grep "preauth" | grep -o -e "deny[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newAttempts" && "$newAttempts" == "$_arg_maxLoginAttempts" ]]; then echo "Successfully modified the max login attempts." else _PRINT_HELP=no die "[Error] Failed to modify the max login attempts in the PAM authentication policy." 1 fi fi # Check if a new lock time is specified and differs from the current one if [[ -n "$_arg_loginAttemptLockTime" && -n "$currentLockTime" ]]; then echo "Modifying the current lock time from '$currentLockTimeMinutes' minutes to '$loginAttemptLockTimeMinutes' minutes in the PAM authentication policy template." # Find and replace the unlock_time in the preauth line existingPreAuthLine=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep -e "pam_faillock\.so [[:space:]]*preauth") newLockTimePreAuthLine=$(echo "$existingPreAuthLine" | sed "s/unlock_time[[:space:]]*=[[:space:]]*[0-9]*/unlock_time=$_arg_loginAttemptLockTime/g") if ! sed -i "s/$existingPreAuthLine/$newLockTimePreAuthLine/g" "/etc/pam.d/common-auth"; then _PRINT_HELP=no die "[Error] Failed to modify the lock time in the PAM authentication policy." 1 fi # Find and replace the unlock_time in the authfail line existingAuthFailLine=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep -e "pam_faillock\.so [[:space:]]*authfail") newLockTimeAuthFailLine=$(echo "$existingAuthFailLine" | sed "s/unlock_time[[:space:]]*=[[:space:]]*[0-9]*/unlock_time=$_arg_loginAttemptLockTime/g") if ! sed -i "s/$existingAuthFailLine/$newLockTimeAuthFailLine/g" "/etc/pam.d/common-auth"; then _PRINT_HELP=no die "[Error] Failed to modify the lock time in the PAM authentication policy." 1 fi # Confirm the lock time modification echo "Successfully modified the current lock time." newLockTime=$(grep -v "^#" /etc/pam.d/common-auth | grep -v '^\s*$' | grep "pam_faillock.so" | grep "preauth" | grep -o -e "unlock_time[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newLockTime" && "$newLockTime" == "$_arg_loginAttemptLockTime" ]]; then echo "Successfully added the lock time to the PAM authentication policy." else _PRINT_HELP=no die "[Error] Failed to configure the lock time in the PAM authentication policy." 1 fi fi # Check if minimum password length or password history arguments are specified if [[ -n "$_arg_minimumPasswordLength" || -n "$_arg_passwordHistory" ]]; then echo "" echo "Retrieving the current PAM common password policy." # Check if common-password file exists and is not empty if [[ ! -f "/etc/pam.d/common-password" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/pam.d/common-password' does not exist. Cannot read the current PAM common password policy." 1 fi if [[ ! -s "/etc/pam.d/common-password" ]]; then _PRINT_HELP=no die "[Error] The file '/etc/pam.d/common-password' is empty. Cannot read the current PAM common password policy." 1 fi # Check if pam_unix.so module is present in the file if ! grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" 1>/dev/null; then _PRINT_HELP=no die "[Error] The 'pam_unix.so' module is missing. Cannot append the minimum password length." 1 fi # Retrieve current settings for minimum length and password history currentMinimumLength=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" | grep -o -e "minlen[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") currentPasswordHistory=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" | grep -o -e "remember[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") echo "Successfully retrieved the current PAM common password policy." # Skip minimum length setting if it matches the current configuration if [[ -n "$_arg_minimumPasswordLength" && -n $currentMinimumLength && "$_arg_minimumPasswordLength" == "$currentMinimumLength" ]]; then echo "The minimum password length is already '$_arg_minimumPasswordLength'. Skipping." _arg_minimumPasswordLength= fi # Skip password history setting if it matches the current configuration if [[ -n "$_arg_passwordHistory" && -n $currentPasswordHistory && "$_arg_passwordHistory" == "$currentPasswordHistory" ]]; then echo "The password history requirement is already set to remember the past '$_arg_passwordHistory' passwords. Skipping." _arg_passwordHistory= fi fi # Create a backup of common-password file if either minimum length or password history needs to be set if [[ -n "$_arg_minimumPasswordLength" || -n "$_arg_passwordHistory" ]]; then commonPasswordBackup="/etc/pam.d/$today-common-password.backup" echo "Creating a backup of '/etc/pam.d/common-password' to '$commonPasswordBackup'." # Exit if a backup file already exists if [[ -f "$commonPasswordBackup" ]]; then _PRINT_HELP=no die "[Error] The backup file '$commonPasswordBackup' already exists. Unable to backup common-password." 1 fi # Attempt to create a backup, exiting if it fails if ! cp /etc/pam.d/common-password "$commonPasswordBackup"; then _PRINT_HELP=no die "[Error] Failed to create a backup of 'common-password' to '$commonPasswordBackup'." 1 fi # Confirm successful backup creation if [[ -f "$commonPasswordBackup" ]]; then echo "Backup created successfully." else _PRINT_HELP=no die "[Error] Failed to create a backup of 'common-password' to '$commonPasswordBackup'." 1 fi fi # Set the minimum password length if it does not currently exist in the file if [[ -n "$_arg_minimumPasswordLength" && -z "$currentMinimumLength" ]]; then echo "Setting the minimum password length to '$_arg_minimumPasswordLength'." # Find the line with pam_unix.so and append minlen setting currentPasswordLine=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so") currentPasswordLine=$(echo "$currentPasswordLine" | sed 's/\[/\\[/g' | sed 's/\]/\\]/g') newMinLengthLine="$currentPasswordLine minlen=$_arg_minimumPasswordLength" newMinLengthLine=${newMinLengthLine//\\/} if ! sed -i "s/$currentPasswordLine/$newMinLengthLine/g" "/etc/pam.d/common-password"; then _PRINT_HELP=no die "[Error] Unable to set the minimum password length in '/etc/pam.d/common-password'." 1 fi # Verify the setting was applied successfully newMinimumLength=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" | grep -o -e "minlen[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newMinimumLength" && "$newMinimumLength" == "$_arg_minimumPasswordLength" ]]; then echo "Successfully set the minimum password length." else _PRINT_HELP=no die "[Error] Unable to set the minimum password length in '/etc/pam.d/common-password'." 1 fi fi # Update minimum password length if it already exists but differs from desired value if [[ -n "$_arg_minimumPasswordLength" && -n "$currentMinimumLength" ]]; then echo "Updating the minimum password length from '$currentMinimumLength' to '$_arg_minimumPasswordLength'." # Modify the existing minlen setting currentMinLengthLine=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" | grep "minlen") currentMinLengthLine=$(echo "$currentMinLengthLine" | sed 's/\[/\\[/g' | sed 's/\]/\\]/g') newMinLengthLine=$(echo "$currentMinLengthLine" | sed "s/minlen[[:space:]]*=[[:space:]]*[0-9]*/minlen=$_arg_minimumPasswordLength/g") newMinLengthLine=${newMinLengthLine//\\/} if ! sed -i "s/$currentMinLengthLine/$newMinLengthLine/g" "/etc/pam.d/common-password"; then _PRINT_HELP=no die "[Error] Unable to update the minimum password length in '/etc/pam.d/common-password'." 1 fi # Verify the updated setting newMinimumLength=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" | grep -o -e "minlen[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newMinimumLength" && "$newMinimumLength" == "$_arg_minimumPasswordLength" ]]; then echo "Successfully modified the minimum password length required." else _PRINT_HELP=no die "[Error] Unable to update the minimum password length in '/etc/pam.d/common-password'." 1 fi fi # Set or modify the password history requirement if [[ -n "$_arg_passwordHistory" && -z "$currentPasswordHistory" ]]; then echo "Setting the password history requirement." currentPasswordLine=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so") currentPasswordLine=$(echo "$currentPasswordLine" | sed 's/\[/\\[/g' | sed 's/\]/\\]/g') newPasswordHistoryLine="$currentPasswordLine remember=$_arg_passwordHistory" newPasswordHistoryLine=${newPasswordHistoryLine//\\/} if ! sed -i "s/$currentPasswordLine/$newPasswordHistoryLine/g" "/etc/pam.d/common-password"; then _PRINT_HELP=no die "[Error] Failed to set the password history requirement in '/etc/pam.d/common-password'." 1 fi # Confirm the new setting newPasswordHistory=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" | grep -o -e "remember[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newPasswordHistory" && "$newPasswordHistory" == "$_arg_passwordHistory" ]]; then echo "Successfully set the password history requirement." else _PRINT_HELP=no die "[Error] Failed to set the password history requirement in '/etc/pam.d/common-password'." 1 fi fi # Modify the password history setting if it already exists but differs from the new desired value if [[ -n "$_arg_passwordHistory" && -n "$currentPasswordHistory" && "$_arg_passwordHistory" != "$currentPasswordHistory" ]]; then echo "Updating the password history requirement from remembering the last '$currentPasswordHistory' passwords to the last '$_arg_passwordHistory' passwords." currentPasswordLine=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" | grep "minlen") currentPasswordLine=$(echo "$currentPasswordLine" | sed 's/\[/\\[/g' | sed 's/\]/\\]/g') newPasswordHistoryLine=$(echo "$currentPasswordLine" | sed "s/remember[[:space:]]*=[[:space:]]*[0-9]*/remember=$_arg_passwordHistory/g") newPasswordHistoryLine=${newPasswordHistoryLine//\\/} if ! sed -i "s/$currentPasswordLine/$newPasswordHistoryLine/g" "/etc/pam.d/common-password"; then _PRINT_HELP=no die "[Error] Failed to change the password history requirement in '/etc/pam.d/common-password'." 1 fi # Confirm the updated setting newPasswordHistory=$(grep -v "^#" /etc/pam.d/common-password | grep -v '^\s*$' | grep "pam_unix.so" | grep -o -e "remember[[:space:]]*=[[:space:]]*[0-9]*" | grep -o -e "[0-9]*") if [[ -n "$newPasswordHistory" && "$newPasswordHistory" == "$_arg_passwordHistory" ]]; then echo "Successfully modified the password history requirement." else _PRINT_HELP=no die "[Error] Failed to change the password history requirement in '/etc/pam.d/common-password'." 1 fi fi
Detailed Breakdown
The script performs the following functions:
1. Argument Parsing and Validation
It accepts parameters like:
- –maxLoginAttempts: Number of allowed failed attempts before lockout.
- –loginAttemptLockTime: Duration (in minutes) for account lockout.
- –daysUntilPasswordExpiration: Password expiration policy.
- –minimumPasswordLength: Required password length.
- –passwordHistory: Number of previous passwords remembered.
Each value is strictly validated to prevent misconfiguration.
2. Permission Check
The script must be run with root privileges. It checks id -u and exits otherwise.
3. Platform Detection
It detects whether to use:
- authselect (modern RHEL-based systems),
- authconfig (legacy RHEL-based systems),
- Or directly edit PAM files for Debian-based systems.
4. Policy Application
Based on the available configuration system, the script:
- Enables faillock if necessary.
- Updates /etc/security/faillock.conf, pwquality.conf, and pwhistory.conf.
- Applies password expiration rules via /etc/login.defs and chage.
- Backs up config files before changes to ensure rollback capability.
5. Fallback to Manual Configuration
If neither authconfig nor authselect is available, the script modifies PAM modules like /etc/pam.d/common-auth and /etc/pam.d/common-password.
Potential Use Cases
Scenario:
An MSP managing 1,000+ mixed-distribution Linux servers wants to enforce:
- Password expiration every 90 days,
- Lock accounts for 10 minutes after 5 failed login attempts,
- Prevent users from reusing the last 5 passwords.
Solution:
They deploy this script via NinjaOne’s scripting engine, passing arguments like:
bash
CopyEdit
–maxLoginAttempts 5 –loginAttemptLockTime 10 –daysUntilPasswordExpiration 90 –minimumPasswordLength 12 –passwordHistory 5
With one policy execution, password standards are enforced consistently across Debian and RHEL systems.
Comparisons
Method | Pros | Cons |
Manual Configuration | Full control | Time-consuming, error-prone |
PAM GUI tools | User-friendly | Limited in headless/server environments |
Configuration Management (e.g., Ansible) | Scalable | Requires infrastructure & scripting |
This Shell Script | Lightweight, cross-platform, backup-friendly | Requires root and basic Bash knowledge |
This Linux password policy shell script offers a balance between flexibility and simplicity, making it ideal for IT teams without a full DevOps stack.
FAQs
Q1: Will this script work on all Linux distros?
It supports Debian 11+ and RHEL 8+ specifically. Older distributions may lack necessary modules like authselect.
Q2: Can I use this in a cron job for periodic enforcement?
Yes, but be cautious. Overwriting password policies repeatedly can lead to unexpected lockouts if not tested properly.
Q3: What if I don’t have authconfig or authselect?
The script gracefully falls back to direct PAM edits using /etc/pam.d/common-auth and related files.
Q4: Is a reboot required after running this?
No. Changes take effect immediately for new sessions.
Implications
Misconfigured password policies can either weaken security (if too lenient) or impede user productivity (if too strict). By automating these settings, IT administrators ensure a secure baseline across their environment. Moreover, maintaining password history and enforcing expiration policies helps reduce vulnerabilities like password reuse and brute-force attacks.
From a compliance perspective, the script aids adherence to CIS benchmarks, PCI-DSS, and HIPAA, which often require password aging and complexity controls.
Recommendations
- Test in a staging environment before deploying to production.
- Use NinjaOne’s scheduling engine to automate periodic audits.
- Keep backups of configuration files as the script does.
- Combine with account monitoring tools for a holistic security approach.
- Ensure time synchronization across devices for accurate policy enforcement.
Final Thoughts
Managing password policies at scale doesn’t have to be a pain point. With this Linux password policy shell script, IT teams and MSPs gain a reliable, version-controlled way to enforce critical security standards across distributed Linux systems. Integrated with NinjaOne, this script becomes even more powerful. NinjaOne enables seamless script deployment, centralized policy enforcement, and real-time compliance reporting—empowering IT professionals to stay ahead of evolving security demands.
By using shell scripting to set the password policy for Linux devices, you reduce human error, ensure consistency, and significantly harden your endpoints. This approach is a must-have in any modern IT toolkit.