This rule configures the system to lock out the root account after a number of
incorrect login attempts using pam_tally2.so.
Rationale
By limiting the number of failed logon attempts, the risk of unauthorized system access via
user password guessing, also known as brute-forcing, is reduced. Limits are imposed by locking
the account.
ISA-62443-2-1-2009, Security for Industrial Automation and Control Systems Part 2-1: Establishing an Industrial Automation and Control Systems Security Program
# Remediation is applicable only in certain platforms
if rpm --quiet -q pam; then
var_password_pam_tally2='<xccdf-1.2:sub xmlns:xccdf-1.2="http://checklists.nist.gov/xccdf/1.2" idref="xccdf_org.ssgproject.content_value_var_password_pam_tally2" use="legacy"/>'
if grep -qP "^\s*auth\s+required\s+pam_tally2.so\s.*\bonerr=fail\b" "/etc/pam.d/login"; then
sed -i -E --follow-symlinks "s/(.*auth.*required.*pam_tally2.so.*)\sonerr=fail=?[[:alnum:]]*(.*)/\1\2/g" "/etc/pam.d/login"
fi
if ! grep -qP "^\s*auth\s+required\s+pam_tally2.so\s*.*" "/etc/pam.d/login"; then
# Line matching group + control + module was not found. Check group + module.
if [ "$(grep -cP '^\s*auth\s+.*\s+pam_tally2.so\s*' "/etc/pam.d/login")" -eq 1 ]; then
# The control is updated only if one single line matches.
sed -i -E --follow-symlinks "s/^(\s*auth\s+).*(\bpam_tally2.so.*)/\1required \2/" "/etc/pam.d/login"
else
echo "auth required pam_tally2.so" >> "/etc/pam.d/login"
fi
fi
# Check the option
if ! grep -qP "^\s*auth\s+required\s+pam_tally2.so\s*.*\sdeny\b" "/etc/pam.d/login"; then
sed -i -E --follow-symlinks "/\s*auth\s+required\s+pam_tally2.so.*/ s/$/ deny=${var_password_pam_tally2}/" "/etc/pam.d/login"
else
sed -i -E --follow-symlinks "s/(\s*auth\s+required\s+pam_tally2.so\s+.*)(deny=)[[:alnum:]]+\s*(.*)/\1\2${var_password_pam_tally2} \3/" "/etc/pam.d/login"
fi
if ! grep -qP "^\s*auth\s+required\s+pam_tally2.so\s*.*" "/etc/pam.d/login"; then
# Line matching group + control + module was not found. Check group + module.
if [ "$(grep -cP '^\s*auth\s+.*\s+pam_tally2.so\s*' "/etc/pam.d/login")" -eq 1 ]; then
# The control is updated only if one single line matches.
sed -i -E --follow-symlinks "s/^(\s*auth\s+).*(\bpam_tally2.so.*)/\1required \2/" "/etc/pam.d/login"
else
echo "auth required pam_tally2.so" >> "/etc/pam.d/login"
fi
fi
# Check the option
if ! grep -qP "^\s*auth\s+required\s+pam_tally2.so\s*.*\seven_deny_root\b" "/etc/pam.d/login"; then
sed -i -E --follow-symlinks "/\s*auth\s+required\s+pam_tally2.so.*/ s/$/ even_deny_root/" "/etc/pam.d/login"
fi
if ! grep -qP "^\s*account\s+required\s+pam_tally2.so\s*.*" "/etc/pam.d/common-account"; then
# Line matching group + control + module was not found. Check group + module.
if [ "$(grep -cP '^\s*account\s+.*\s+pam_tally2.so\s*' "/etc/pam.d/common-account")" -eq 1 ]; then
# The control is updated only if one single line matches.
sed -i -E --follow-symlinks "s/^(\s*account\s+).*(\bpam_tally2.so.*)/\1required \2/" "/etc/pam.d/common-account"
else
echo "account required pam_tally2.so" >> "/etc/pam.d/common-account"
fi
fi
# Check the option
if ! grep -qP "^\s*account\s+required\s+pam_tally2.so\s*.*\s\b" "/etc/pam.d/common-account"; then
sed -i -E --follow-symlinks "/\s*account\s+required\s+pam_tally2.so.*/ s/$/ /" "/etc/pam.d/common-account"
fi
else
>&2 echo 'Remediation is not applicable, nothing was done'
fi
Remediation - Ansible
- name: Gather the package facts
package_facts:
manager: auto
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: XCCDF Value var_password_pam_tally2 # promote to variable
set_fact:
var_password_pam_tally2: !!str <xccdf-1.2:sub xmlns:xccdf-1.2="http://checklists.nist.gov/xccdf/1.2" idref="xccdf_org.ssgproject.content_value_var_password_pam_tally2" use="legacy"/>
tags:
- always
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Define a fact for control already filtered in case filters are used
ansible.builtin.set_fact:
pam_module_control: required
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the "onerr=fail" option from "pam_tally2.so" is not present in /etc/pam.d/login
ansible.builtin.replace:
dest: /etc/pam.d/login
regexp: (.*auth.*{{ pam_module_control | regex_escape() }}.*pam_tally2.so.*)\bonerr=fail\b=?[0-9a-zA-Z]*(.*)
replace: \1\2
register: result_pam_option_removal
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Define a fact for control already filtered in case filters are used
ansible.builtin.set_fact:
pam_module_control: required
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if expected PAM module line is present in /etc/pam.d/login
ansible.builtin.lineinfile:
path: /etc/pam.d/login
regexp: ^\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so\s*.*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_present
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Include or update the PAM module line in /etc/pam.d/login
block:
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if required PAM module line is present in /etc/pam.d/login with different
control
ansible.builtin.lineinfile:
path: /etc/pam.d/login
regexp: ^\s*auth\s+.*\s+pam_tally2.so\s*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_other_control_present
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the correct control for the required PAM module line in /etc/pam.d/login
ansible.builtin.replace:
dest: /etc/pam.d/login
regexp: ^(\s*auth\s+).*(\bpam_tally2.so.*)
replace: \1{{ pam_module_control }} \2
register: result_pam_module_edit
when:
- result_pam_line_other_control_present.found == 1
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the required PAM module line is included in /etc/pam.d/login
ansible.builtin.lineinfile:
dest: /etc/pam.d/login
line: auth {{ pam_module_control }} pam_tally2.so
register: result_pam_module_add
when:
- result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
> 1
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b
when:
- result_authselect_present is defined
- result_authselect_present.stat.exists
- |-
(result_pam_module_add is defined and result_pam_module_add.changed)
or (result_pam_module_edit is defined and result_pam_module_edit.changed)
when:
- '"pam" in ansible_facts.packages'
- result_pam_line_present.found is defined
- result_pam_line_present.found == 0
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Define a fact for control already filtered in case filters are used
ansible.builtin.set_fact:
pam_module_control: required
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if the required PAM module option is present in /etc/pam.d/login
ansible.builtin.lineinfile:
path: /etc/pam.d/login
regexp: ^\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so\s*.*\sdeny\b
state: absent
check_mode: true
changed_when: false
register: result_pam_module_accounts_passwords_pam_tally2_deny_root_option_present
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the "deny" PAM option for "pam_tally2.so" is included in /etc/pam.d/login
ansible.builtin.lineinfile:
path: /etc/pam.d/login
backrefs: true
regexp: ^(\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so.*)
line: \1 deny={{ var_password_pam_tally2 }}
state: present
register: result_pam_accounts_passwords_pam_tally2_deny_root_add
when:
- '"pam" in ansible_facts.packages'
- result_pam_module_accounts_passwords_pam_tally2_deny_root_option_present.found
== 0
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the required value for "deny" PAM option from "pam_tally2.so" in /etc/pam.d/login
ansible.builtin.lineinfile:
path: /etc/pam.d/login
backrefs: true
regexp: ^(\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so\s+.*)(deny)=[0-9a-zA-Z]+\s*(.*)
line: \1\2={{ var_password_pam_tally2 }} \3
register: result_pam_accounts_passwords_pam_tally2_deny_root_edit
when:
- '"pam" in ansible_facts.packages'
- result_pam_module_accounts_passwords_pam_tally2_deny_root_option_present.found
> 0
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Define a fact for control already filtered in case filters are used
ansible.builtin.set_fact:
pam_module_control: required
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if expected PAM module line is present in /etc/pam.d/login
ansible.builtin.lineinfile:
path: /etc/pam.d/login
regexp: ^\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so\s*.*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_present
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Include or update the PAM module line in /etc/pam.d/login
block:
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if required PAM module line is present in /etc/pam.d/login with different
control
ansible.builtin.lineinfile:
path: /etc/pam.d/login
regexp: ^\s*auth\s+.*\s+pam_tally2.so\s*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_other_control_present
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the correct control for the required PAM module line in /etc/pam.d/login
ansible.builtin.replace:
dest: /etc/pam.d/login
regexp: ^(\s*auth\s+).*(\bpam_tally2.so.*)
replace: \1{{ pam_module_control }} \2
register: result_pam_module_edit
when:
- result_pam_line_other_control_present.found == 1
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the required PAM module line is included in /etc/pam.d/login
ansible.builtin.lineinfile:
dest: /etc/pam.d/login
line: auth {{ pam_module_control }} pam_tally2.so
register: result_pam_module_add
when:
- result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
> 1
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b
when:
- result_authselect_present is defined
- result_authselect_present.stat.exists
- |-
(result_pam_module_add is defined and result_pam_module_add.changed)
or (result_pam_module_edit is defined and result_pam_module_edit.changed)
when:
- '"pam" in ansible_facts.packages'
- result_pam_line_present.found is defined
- result_pam_line_present.found == 0
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Define a fact for control already filtered in case filters are used
ansible.builtin.set_fact:
pam_module_control: required
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if the required PAM module option is present in /etc/pam.d/login
ansible.builtin.lineinfile:
path: /etc/pam.d/login
regexp: ^\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so\s*.*\seven_deny_root\b
state: absent
check_mode: true
changed_when: false
register: result_pam_module_accounts_passwords_pam_tally2_deny_root_option_present
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the "even_deny_root" PAM option for "pam_tally2.so" is included in /etc/pam.d/login
ansible.builtin.lineinfile:
path: /etc/pam.d/login
backrefs: true
regexp: ^(\s*auth\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so.*)
line: \1 even_deny_root
state: present
register: result_pam_accounts_passwords_pam_tally2_deny_root_add
when:
- '"pam" in ansible_facts.packages'
- result_pam_module_accounts_passwords_pam_tally2_deny_root_option_present.found
== 0
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Define a fact for control already filtered in case filters are used
ansible.builtin.set_fact:
pam_module_control: required
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if expected PAM module line is present in /etc/pam.d/common-account
ansible.builtin.lineinfile:
path: /etc/pam.d/common-account
regexp: ^\s*account\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so\s*.*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_present
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Include or update the PAM module line in /etc/pam.d/common-account
block:
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if required PAM module line is present in /etc/pam.d/common-account
with different control
ansible.builtin.lineinfile:
path: /etc/pam.d/common-account
regexp: ^\s*account\s+.*\s+pam_tally2.so\s*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_other_control_present
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the correct control for the required PAM module line in /etc/pam.d/common-account
ansible.builtin.replace:
dest: /etc/pam.d/common-account
regexp: ^(\s*account\s+).*(\bpam_tally2.so.*)
replace: \1{{ pam_module_control }} \2
register: result_pam_module_edit
when:
- result_pam_line_other_control_present.found == 1
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the required PAM module line is included in /etc/pam.d/common-account
ansible.builtin.lineinfile:
dest: /etc/pam.d/common-account
line: account {{ pam_module_control }} pam_tally2.so
register: result_pam_module_add
when:
- result_pam_line_other_control_present.found == 0 or result_pam_line_other_control_present.found
> 1
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b
when:
- result_authselect_present is defined
- result_authselect_present.stat.exists
- |-
(result_pam_module_add is defined and result_pam_module_add.changed)
or (result_pam_module_edit is defined and result_pam_module_edit.changed)
when:
- '"pam" in ansible_facts.packages'
- result_pam_line_present.found is defined
- result_pam_line_present.found == 0
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Define a fact for control already filtered in case filters are used
ansible.builtin.set_fact:
pam_module_control: required
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Check if the required PAM module option is present in /etc/pam.d/common-account
ansible.builtin.lineinfile:
path: /etc/pam.d/common-account
regexp: ^\s*account\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so\s*.*\s\b
state: absent
check_mode: true
changed_when: false
register: result_pam_module_accounts_passwords_pam_tally2_deny_root_option_present
when: '"pam" in ansible_facts.packages'
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Configure the root Account lock for Failed Password Attempts via pam_tally2
- Ensure the "" PAM option for "pam_tally2.so" is included in /etc/pam.d/common-account
ansible.builtin.lineinfile:
path: /etc/pam.d/common-account
backrefs: true
regexp: ^(\s*account\s+{{ pam_module_control | regex_escape() }}\s+pam_tally2.so.*)
line: \1
state: present
register: result_pam_accounts_passwords_pam_tally2_deny_root_add
when:
- '"pam" in ansible_facts.packages'
- result_pam_module_accounts_passwords_pam_tally2_deny_root_option_present.found
== 0
tags:
- CCE-91546-2
- NIST-800-53-AC-7(b)
- NIST-800-53-CM-6(a)
- NIST-800-53-IA-5(c)
- accounts_passwords_pam_tally2_deny_root
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy