The Ubuntu 20.04 operating system must lock an account after - at most -
consecutive invalid access attempts.
Rationale
By limiting the number of failed logon attempts, the risk of unauthorized
system access via user password guessing, otherwise known as brute-force
attacks, is reduced. Limits are imposed by locking the account.
To configure the operating system to lock an account after three
unsuccessful consecutive access attempts using pam_tally2.so,
modify the content of both /etc/pam.d/common-auth and
/etc/pam.d/common-account as follows:
add or modify the pam_tally2.so module line in
/etc/pam.d/common-auth to ensure both onerr=fail and
deny= are present. For example:
- name: Gather the package facts
package_facts:
manager: auto
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- 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: Set Deny For Failed Password Attempts - Check if expected PAM module line
is present in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
regexp: ^\s*auth\s+{{ 'required' | regex_escape() }}\s+pam_tally2.so\s*.*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_present
when: '"libpam-runtime" in ansible_facts.packages'
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Include or update the PAM module line
in /etc/pam.d/common-auth
block:
- name: Set Deny For Failed Password Attempts - Check if required PAM module line
is present in /etc/pam.d/common-auth with different control
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
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: Set Deny For Failed Password Attempts - Ensure the correct control for the
required PAM module line in /etc/pam.d/common-auth
ansible.builtin.replace:
dest: /etc/pam.d/common-auth
regexp: ^(\s*auth\s+).*(\bpam_tally2.so.*)
replace: \1required \2
register: result_pam_module_edit
when:
- result_pam_line_other_control_present.found == 1
- name: Set Deny For Failed Password Attempts - Ensure the required PAM module line
is included in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
dest: /etc/pam.d/common-auth
line: auth required 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: Set Deny For Failed Password Attempts - 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:
- '"libpam-runtime" in ansible_facts.packages'
- result_pam_line_present.found is defined
- result_pam_line_present.found == 0
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Check if the required PAM module option
is present in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
regexp: ^\s*auth\s+{{ 'required' | regex_escape() }}\s+pam_tally2.so\s*.*\sdeny\b
state: absent
check_mode: true
changed_when: false
register: result_pam_module_deny_option_present
when: '"libpam-runtime" in ansible_facts.packages'
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Ensure the "deny" PAM option for "pam_tally2.so"
is included in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
backrefs: true
regexp: ^(\s*auth\s+{{ 'required' | regex_escape() }}\s+pam_tally2.so.*)
line: \1 deny={{ var_password_pam_tally2 }}
state: present
register: result_pam_deny_add
when:
- '"libpam-runtime" in ansible_facts.packages'
- result_pam_module_deny_option_present.found == 0
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Ensure the required value for "deny"
PAM option from "pam_tally2.so" in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
backrefs: true
regexp: ^(\s*auth\s+{{ 'required' | regex_escape() }}\s+pam_tally2.so\s+.*)(deny)=[0-9a-zA-Z]+\s*(.*)
line: \1\2={{ var_password_pam_tally2 }} \3
register: result_pam_deny_edit
when:
- '"libpam-runtime" in ansible_facts.packages'
- result_pam_module_deny_option_present.found > 0
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Check if expected PAM module line
is present in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
regexp: ^\s*auth\s+{{ 'required' | regex_escape() }}\s+pam_tally2.so\s*.*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_present
when: '"libpam-runtime" in ansible_facts.packages'
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Include or update the PAM module line
in /etc/pam.d/common-auth
block:
- name: Set Deny For Failed Password Attempts - Check if required PAM module line
is present in /etc/pam.d/common-auth with different control
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
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: Set Deny For Failed Password Attempts - Ensure the correct control for the
required PAM module line in /etc/pam.d/common-auth
ansible.builtin.replace:
dest: /etc/pam.d/common-auth
regexp: ^(\s*auth\s+).*(\bpam_tally2.so.*)
replace: \1required \2
register: result_pam_module_edit
when:
- result_pam_line_other_control_present.found == 1
- name: Set Deny For Failed Password Attempts - Ensure the required PAM module line
is included in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
dest: /etc/pam.d/common-auth
insertafter: (fail)
line: auth required 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: Set Deny For Failed Password Attempts - 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:
- '"libpam-runtime" in ansible_facts.packages'
- result_pam_line_present.found is defined
- result_pam_line_present.found == 0
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Check if the required PAM module option
is present in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
regexp: ^\s*auth\s+{{ 'required' | regex_escape() }}\s+pam_tally2.so\s*.*\sonerr\b
state: absent
check_mode: true
changed_when: false
register: result_pam_module_onerr_option_present
when: '"libpam-runtime" in ansible_facts.packages'
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Ensure the "onerr" PAM option for
"pam_tally2.so" is included in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
backrefs: true
regexp: ^(\s*auth\s+{{ 'required' | regex_escape() }}\s+pam_tally2.so.*)
line: \1 onerr=fail
state: present
register: result_pam_onerr_add
when:
- '"libpam-runtime" in ansible_facts.packages'
- result_pam_module_onerr_option_present.found == 0
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Ensure the required value for "onerr"
PAM option from "pam_tally2.so" in /etc/pam.d/common-auth
ansible.builtin.lineinfile:
path: /etc/pam.d/common-auth
backrefs: true
regexp: ^(\s*auth\s+{{ 'required' | regex_escape() }}\s+pam_tally2.so\s+.*)(onerr)=[0-9a-zA-Z]+\s*(.*)
line: \1\2=fail \3
register: result_pam_onerr_edit
when:
- '"libpam-runtime" in ansible_facts.packages'
- result_pam_module_onerr_option_present.found > 0
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - 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+{{ 'required' | regex_escape() }}\s+pam_tally2.so\s*.*
state: absent
check_mode: true
changed_when: false
register: result_pam_line_present
when: '"libpam-runtime" in ansible_facts.packages'
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - Include or update the PAM module line
in /etc/pam.d/common-account
block:
- name: Set Deny For Failed Password Attempts - 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: Set Deny For Failed Password Attempts - 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: \1required \2
register: result_pam_module_edit
when:
- result_pam_line_other_control_present.found == 1
- name: Set Deny For Failed Password Attempts - 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 required 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: Set Deny For Failed Password Attempts - 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:
- '"libpam-runtime" in ansible_facts.packages'
- result_pam_line_present.found is defined
- result_pam_line_present.found == 0
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - 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+{{ 'required' | regex_escape() }}\s+pam_tally2.so\s*.*\s\b
state: absent
check_mode: true
changed_when: false
register: result_pam_module__option_present
when: '"libpam-runtime" in ansible_facts.packages'
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- name: Set Deny For Failed Password Attempts - 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+{{ 'required' | regex_escape() }}\s+pam_tally2.so.*)
line: \1
state: present
register: result_pam__add
when:
- '"libpam-runtime" in ansible_facts.packages'
- result_pam_module__option_present.found == 0
tags:
- DISA-STIG-UBTU-20-010072
- PCI-DSS-Req-8.1.6
- PCI-DSSv4-8.3.4
- accounts_passwords_pam_tally2
- configure_strategy
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
Remediation - Shell Script
# Remediation is applicable only in certain platforms
if dpkg-query --show --showformat='${db:Status-Status}\n' 'libpam-runtime' 2>/dev/null | grep -q installed; 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"/>'
# Use a non-number regexp to force update of the value of the deny option
if ! grep -qP '^\s*auth\s+'"required"'\s+pam_tally2.so\s*.*' "/etc/pam.d/common-auth"; 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/common-auth")" -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.*)/\1'"required"' \2/' "/etc/pam.d/common-auth"
else
echo 'auth '"required"' pam_tally2.so' >> "/etc/pam.d/common-auth"
fi
fi
# Check the option
if ! grep -qP '^\s*auth\s+'"required"'\s+pam_tally2.so\s*.*\sdeny\b' "/etc/pam.d/common-auth"; then
sed -i -E --follow-symlinks '/\s*auth\s+'"required"'\s+pam_tally2.so.*/ s/$/ deny='"${var_password_pam_tally2}"'/' "/etc/pam.d/common-auth"
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/common-auth"
fi
if ! grep -qP '^\s*auth\s+'"required"'\s+pam_tally2.so\s*.*' "/etc/pam.d/common-auth"; 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/common-auth")" -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.*)/\1'"required"' \2/' "/etc/pam.d/common-auth"
else
LAST_MATCH_LINE=$(grep -nP "(fail)" "/etc/pam.d/common-auth" | tail -n 1 | cut -d: -f 1)
if [ ! -z $LAST_MATCH_LINE ]; then
sed -i --follow-symlinks $LAST_MATCH_LINE' a auth '"required"' pam_tally2.so' "/etc/pam.d/common-auth"
else
echo 'auth '"required"' pam_tally2.so' >> "/etc/pam.d/common-auth"
fi
fi
fi
# Check the option
if ! grep -qP '^\s*auth\s+'"required"'\s+pam_tally2.so\s*.*\sonerr\b' "/etc/pam.d/common-auth"; then
sed -i -E --follow-symlinks '/\s*auth\s+'"required"'\s+pam_tally2.so.*/ s/$/ onerr='"fail"'/' "/etc/pam.d/common-auth"
else
sed -i -E --follow-symlinks 's/(\s*auth\s+'"required"'\s+pam_tally2.so\s+.*)('"onerr"'=)[[:alnum:]]+\s*(.*)/\1\2'"fail"' \3/' "/etc/pam.d/common-auth"
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.*)/\1'"required"' \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