Set Deny For Failed Password Attempts

Classification:

compliance

Framework:

Control:

Description

The Ubuntu 20.04 operating system must lock an account after - at most - 5 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=5 are present. For example:
auth required pam_tally2.so onerr=fail silent audit deny=5
  • add or modify the following line in /etc/pam.d/common-account:
account required pam_tally2.so

Remediation

Shell script

The following script can be run on the host to remediate the issue.

# 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='5'

# 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

Ansible playbook

The following playbook can be run with Ansible to remediate the issue.

- 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 5
 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\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\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\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\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\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\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\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\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\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\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\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