Account Lockouts Must Be Logged
An XCCDF Rule
Description
PAM faillock locks an account due to excessive password failures, this event must be logged.
Rationale
Without auditing of these events it may be harder or impossible to identify what an attacker did after an attack.
- ID
- xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_audit
- Severity
- Medium
- References
- CCI: Control Correlation IdentifierNIST Special Publication 800-53 (Revision 4): Security and Privacy Controls for Federal Information Systems and OrganizationsGPOS SRG: General Purpose Operating System Security Requirements GuideSTIG: Security Technical Implementation Guides for UNIX/Linux Operating SystemsSTIG References, Finding IDs and Rule IDs
- Updated
Remediation - Ansible
- name: Account Lockouts Must Be Logged - Check if system relies on authselect tool
ansible.builtin.stat:
path: /usr/bin/authselect
register: result_authselect_present
tags:
- DISA-STIG-UBTU-20-010072 - NIST-800-53-AC-7 (a)
- accounts_passwords_pam_faillock_audit
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Account Lockouts Must Be Logged - Remediation where authselect tool is present
block:
- name: Account Lockouts Must Be Logged - Check integrity of authselect current
profile
ansible.builtin.command:
cmd: authselect check
register: result_authselect_check_cmd
changed_when: false
failed_when: false
- name: Account Lockouts Must Be Logged - Informative message based on the authselect
integrity check result
ansible.builtin.assert:
that:
- result_authselect_check_cmd.rc == 0
fail_msg:
- authselect integrity check failed. Remediation aborted!
- This remediation could not be applied because an authselect profile was not
selected or the selected profile is not intact.
- It is not recommended to manually edit the PAM files when authselect tool
is available.
- In cases where the default authselect profile does not cover a specific demand,
a custom authselect profile is recommended.
success_msg:
- authselect integrity check passed
- name: Account Lockouts Must Be Logged - Get authselect current features
ansible.builtin.shell:
cmd: authselect current | tail -n+3 | awk '{ print $2 }'
register: result_authselect_features
changed_when: false
when:
- result_authselect_check_cmd is success
- name: Account Lockouts Must Be Logged - Ensure "with-faillock" feature is enabled
using authselect tool
ansible.builtin.command:
cmd: authselect enable-feature with-faillock
register: result_authselect_enable_feature_cmd
when:
- result_authselect_check_cmd is success
- result_authselect_features.stdout is not search("with-faillock")
- name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b
when:
- result_authselect_enable_feature_cmd is not skipped
- result_authselect_enable_feature_cmd is success
when: result_authselect_present.stat.exists
tags:
- DISA-STIG-UBTU-20-010072
- NIST-800-53-AC-7 (a)
- accounts_passwords_pam_faillock_audit
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Account Lockouts Must Be Logged - Remediation where authselect tool is not
present
block:
- name: Account Lockouts Must Be Logged - Check if pam_faillock.so is already enabled
ansible.builtin.lineinfile:
path: /etc/pam.d/system-auth
regexp: .*auth.*pam_faillock\.so (preauth|authfail)
state: absent
check_mode: true
changed_when: false
register: result_pam_faillock_is_enabled
- name: Account Lockouts Must Be Logged - Enable pam_faillock.so preauth editing
PAM files
ansible.builtin.lineinfile:
path: '{{ item }}'
line: auth required pam_faillock.so preauth
insertbefore: ^auth.*sufficient.*pam_unix\.so.*
state: present
loop:
- /etc/pam.d/system-auth
- /etc/pam.d/password-auth
when:
- result_pam_faillock_is_enabled.found == 0
- name: Account Lockouts Must Be Logged - Enable pam_faillock.so authfail editing
PAM files
ansible.builtin.lineinfile:
path: '{{ item }}'
line: auth required pam_faillock.so authfail
insertbefore: ^auth.*required.*pam_deny\.so.*
state: present
loop:
- /etc/pam.d/system-auth
- /etc/pam.d/password-auth
when:
- result_pam_faillock_is_enabled.found == 0
- name: Account Lockouts Must Be Logged - Enable pam_faillock.so account section
editing PAM files
ansible.builtin.lineinfile:
path: '{{ item }}'
line: account required pam_faillock.so
insertbefore: ^account.*required.*pam_unix\.so.*
state: present
loop:
- /etc/pam.d/system-auth
- /etc/pam.d/password-auth
when:
- result_pam_faillock_is_enabled.found == 0
when: not result_authselect_present.stat.exists
tags:
- DISA-STIG-UBTU-20-010072
- NIST-800-53-AC-7 (a)
- accounts_passwords_pam_faillock_audit
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Account Lockouts Must Be Logged - Check the presence of /etc/security/faillock.conf
file
ansible.builtin.stat:
path: /etc/security/faillock.conf
register: result_faillock_conf_check
tags:
- DISA-STIG-UBTU-20-010072
- NIST-800-53-AC-7 (a)
- accounts_passwords_pam_faillock_audit
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter
in /etc/security/faillock.conf
ansible.builtin.lineinfile:
path: /etc/security/faillock.conf
regexp: ^\s*audit
line: audit
state: present
when: result_faillock_conf_check.stat.exists
tags:
- DISA-STIG-UBTU-20-010072
- NIST-800-53-AC-7 (a)
- accounts_passwords_pam_faillock_audit
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter
not in PAM files
block:
- name: Account Lockouts Must Be Logged - Check if /etc/pam.d/system-auth file is
present
ansible.builtin.stat:
path: /etc/pam.d/system-auth
register: result_pam_file_present
- name: Account Lockouts Must Be Logged - Check the proper remediation for the system
block:
- name: Account Lockouts Must Be Logged - Define the PAM file to be edited as
a local fact
ansible.builtin.set_fact:
pam_file_path: /etc/pam.d/system-auth
- name: Account Lockouts Must Be Logged - Check if system relies on authselect
tool
ansible.builtin.stat:
path: /usr/bin/authselect
register: result_authselect_present
- name: Account Lockouts Must Be Logged - Ensure authselect custom profile is
used if authselect is present
block:
- name: Account Lockouts Must Be Logged - Check integrity of authselect current
profile
ansible.builtin.command:
cmd: authselect check
register: result_authselect_check_cmd
changed_when: false
failed_when: false
- name: Account Lockouts Must Be Logged - Informative message based on the authselect
integrity check result
ansible.builtin.assert:
that:
- result_authselect_check_cmd.rc == 0
fail_msg:
- authselect integrity check failed. Remediation aborted!
- This remediation could not be applied because an authselect profile was
not selected or the selected profile is not intact.
- It is not recommended to manually edit the PAM files when authselect tool
is available.
- In cases where the default authselect profile does not cover a specific
demand, a custom authselect profile is recommended.
success_msg:
- authselect integrity check passed
- name: Account Lockouts Must Be Logged - Get authselect current profile
ansible.builtin.shell:
cmd: authselect current -r | awk '{ print $1 }'
register: result_authselect_profile
changed_when: false
when:
- result_authselect_check_cmd is success
- name: Account Lockouts Must Be Logged - Define the current authselect profile
as a local fact
ansible.builtin.set_fact:
authselect_current_profile: '{{ result_authselect_profile.stdout }}'
authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
when:
- result_authselect_profile is not skipped
- result_authselect_profile.stdout is match("custom/")
- name: Account Lockouts Must Be Logged - Define the new authselect custom profile
as a local fact
ansible.builtin.set_fact:
authselect_current_profile: '{{ result_authselect_profile.stdout }}'
authselect_custom_profile: custom/hardening
when:
- result_authselect_profile is not skipped
- result_authselect_profile.stdout is not match("custom/")
- name: Account Lockouts Must Be Logged - Get authselect current features to
also enable them in the custom profile
ansible.builtin.shell:
cmd: authselect current | tail -n+3 | awk '{ print $2 }'
register: result_authselect_features
changed_when: false
when:
- result_authselect_profile is not skipped
- authselect_current_profile is not match("custom/")
- name: Account Lockouts Must Be Logged - Check if any custom profile with the
same name was already created
ansible.builtin.stat:
path: /etc/authselect/{{ authselect_custom_profile }}
register: result_authselect_custom_profile_present
changed_when: false
when:
- authselect_current_profile is not match("custom/")
- name: Account Lockouts Must Be Logged - Create an authselect custom profile
based on the current profile
ansible.builtin.command:
cmd: authselect create-profile hardening -b {{ authselect_current_profile
}}
when:
- result_authselect_check_cmd is success
- authselect_current_profile is not match("custom/")
- not result_authselect_custom_profile_present.stat.exists
- name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
when:
- result_authselect_check_cmd is success
- result_authselect_profile is not skipped
- authselect_current_profile is not match("custom/")
- authselect_custom_profile is not match(authselect_current_profile)
- name: Account Lockouts Must Be Logged - Ensure the authselect custom profile
is selected
ansible.builtin.command:
cmd: authselect select {{ authselect_custom_profile }}
register: result_pam_authselect_select_profile
when:
- result_authselect_check_cmd is success
- result_authselect_profile is not skipped
- authselect_current_profile is not match("custom/")
- authselect_custom_profile is not match(authselect_current_profile)
- name: Account Lockouts Must Be Logged - Restore the authselect features in
the custom profile
ansible.builtin.command:
cmd: authselect enable-feature {{ item }}
loop: '{{ result_authselect_features.stdout_lines }}'
register: result_pam_authselect_restore_features
when:
- result_authselect_profile is not skipped
- result_authselect_features is not skipped
- result_pam_authselect_select_profile is not skipped
- name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
when:
- result_authselect_check_cmd is success
- result_authselect_profile is not skipped
- result_pam_authselect_restore_features is not skipped
- name: Account Lockouts Must Be Logged - Change the PAM file to be edited according
to the custom authselect profile
ansible.builtin.set_fact:
pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
| basename }}
when:
- result_authselect_present.stat.exists
- name: Account Lockouts Must Be Logged - Ensure the "audit" option from "pam_faillock.so"
is not present in {{ pam_file_path }}
ansible.builtin.replace:
dest: '{{ pam_file_path }}'
regexp: (.*auth.*pam_faillock.so.*)\baudit\b=?[0-9a-zA-Z]*(.*)
replace: \1\2
register: result_pam_option_removal
- name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b
when:
- result_authselect_present.stat.exists
- result_pam_option_removal is changed
when:
- result_pam_file_present.stat.exists
- name: Account Lockouts Must Be Logged - Check if /etc/pam.d/password-auth file
is present
ansible.builtin.stat:
path: /etc/pam.d/password-auth
register: result_pam_file_present
- name: Account Lockouts Must Be Logged - Check the proper remediation for the system
block:
- name: Account Lockouts Must Be Logged - Define the PAM file to be edited as
a local fact
ansible.builtin.set_fact:
pam_file_path: /etc/pam.d/password-auth
- name: Account Lockouts Must Be Logged - Check if system relies on authselect
tool
ansible.builtin.stat:
path: /usr/bin/authselect
register: result_authselect_present
- name: Account Lockouts Must Be Logged - Ensure authselect custom profile is
used if authselect is present
block:
- name: Account Lockouts Must Be Logged - Check integrity of authselect current
profile
ansible.builtin.command:
cmd: authselect check
register: result_authselect_check_cmd
changed_when: false
failed_when: false
- name: Account Lockouts Must Be Logged - Informative message based on the authselect
integrity check result
ansible.builtin.assert:
that:
- result_authselect_check_cmd.rc == 0
fail_msg:
- authselect integrity check failed. Remediation aborted!
- This remediation could not be applied because an authselect profile was
not selected or the selected profile is not intact.
- It is not recommended to manually edit the PAM files when authselect tool
is available.
- In cases where the default authselect profile does not cover a specific
demand, a custom authselect profile is recommended.
success_msg:
- authselect integrity check passed
- name: Account Lockouts Must Be Logged - Get authselect current profile
ansible.builtin.shell:
cmd: authselect current -r | awk '{ print $1 }'
register: result_authselect_profile
changed_when: false
when:
- result_authselect_check_cmd is success
- name: Account Lockouts Must Be Logged - Define the current authselect profile
as a local fact
ansible.builtin.set_fact:
authselect_current_profile: '{{ result_authselect_profile.stdout }}'
authselect_custom_profile: '{{ result_authselect_profile.stdout }}'
when:
- result_authselect_profile is not skipped
- result_authselect_profile.stdout is match("custom/")
- name: Account Lockouts Must Be Logged - Define the new authselect custom profile
as a local fact
ansible.builtin.set_fact:
authselect_current_profile: '{{ result_authselect_profile.stdout }}'
authselect_custom_profile: custom/hardening
when:
- result_authselect_profile is not skipped
- result_authselect_profile.stdout is not match("custom/")
- name: Account Lockouts Must Be Logged - Get authselect current features to
also enable them in the custom profile
ansible.builtin.shell:
cmd: authselect current | tail -n+3 | awk '{ print $2 }'
register: result_authselect_features
changed_when: false
when:
- result_authselect_profile is not skipped
- authselect_current_profile is not match("custom/")
- name: Account Lockouts Must Be Logged - Check if any custom profile with the
same name was already created
ansible.builtin.stat:
path: /etc/authselect/{{ authselect_custom_profile }}
register: result_authselect_custom_profile_present
changed_when: false
when:
- authselect_current_profile is not match("custom/")
- name: Account Lockouts Must Be Logged - Create an authselect custom profile
based on the current profile
ansible.builtin.command:
cmd: authselect create-profile hardening -b {{ authselect_current_profile
}}
when:
- result_authselect_check_cmd is success
- authselect_current_profile is not match("custom/")
- not result_authselect_custom_profile_present.stat.exists
- name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b --backup=before-hardening-custom-profile
when:
- result_authselect_check_cmd is success
- result_authselect_profile is not skipped
- authselect_current_profile is not match("custom/")
- authselect_custom_profile is not match(authselect_current_profile)
- name: Account Lockouts Must Be Logged - Ensure the authselect custom profile
is selected
ansible.builtin.command:
cmd: authselect select {{ authselect_custom_profile }}
register: result_pam_authselect_select_profile
when:
- result_authselect_check_cmd is success
- result_authselect_profile is not skipped
- authselect_current_profile is not match("custom/")
- authselect_custom_profile is not match(authselect_current_profile)
- name: Account Lockouts Must Be Logged - Restore the authselect features in
the custom profile
ansible.builtin.command:
cmd: authselect enable-feature {{ item }}
loop: '{{ result_authselect_features.stdout_lines }}'
register: result_pam_authselect_restore_features
when:
- result_authselect_profile is not skipped
- result_authselect_features is not skipped
- result_pam_authselect_select_profile is not skipped
- name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b --backup=after-hardening-custom-profile
when:
- result_authselect_check_cmd is success
- result_authselect_profile is not skipped
- result_pam_authselect_restore_features is not skipped
- name: Account Lockouts Must Be Logged - Change the PAM file to be edited according
to the custom authselect profile
ansible.builtin.set_fact:
pam_file_path: /etc/authselect/{{ authselect_custom_profile }}/{{ pam_file_path
| basename }}
when:
- result_authselect_present.stat.exists
- name: Account Lockouts Must Be Logged - Ensure the "audit" option from "pam_faillock.so"
is not present in {{ pam_file_path }}
ansible.builtin.replace:
dest: '{{ pam_file_path }}'
regexp: (.*auth.*pam_faillock.so.*)\baudit\b=?[0-9a-zA-Z]*(.*)
replace: \1\2
register: result_pam_option_removal
- name: Account Lockouts Must Be Logged - Ensure authselect changes are applied
ansible.builtin.command:
cmd: authselect apply-changes -b
when:
- result_authselect_present.stat.exists
- result_pam_option_removal is changed
when:
- result_pam_file_present.stat.exists
when: result_faillock_conf_check.stat.exists
tags:
- DISA-STIG-UBTU-20-010072
- NIST-800-53-AC-7 (a)
- accounts_passwords_pam_faillock_audit
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
- name: Account Lockouts Must Be Logged - Ensure the pam_faillock.so audit parameter
in PAM files
block:
- name: Account Lockouts Must Be Logged - Check if pam_faillock.so audit parameter
is already enabled in pam files
ansible.builtin.lineinfile:
path: /etc/pam.d/system-auth
regexp: .*auth.*pam_faillock\.so (preauth|authfail).*audit
state: absent
check_mode: true
changed_when: false
register: result_pam_faillock_audit_parameter_is_present
- name: Account Lockouts Must Be Logged - Ensure the inclusion of pam_faillock.so
preauth audit parameter in auth section
ansible.builtin.lineinfile:
path: '{{ item }}'
backrefs: true
regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)
line: \1required\3 audit
state: present
loop:
- /etc/pam.d/system-auth
- /etc/pam.d/password-auth
when:
- result_pam_faillock_audit_parameter_is_present.found == 0
when: not result_faillock_conf_check.stat.exists
tags:
- DISA-STIG-UBTU-20-010072
- NIST-800-53-AC-7 (a)
- accounts_passwords_pam_faillock_audit
- low_complexity
- low_disruption
- medium_severity
- no_reboot_needed
- restrict_strategy
Remediation - Shell Script
if [ -f /usr/bin/authselect ]; then
if ! authselect check; then
echo "
authselect integrity check failed. Remediation aborted!
This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.It is not recommended to manually edit the PAM files when authselect tool is available.
In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
exit 1
fi
authselect enable-feature with-faillock
authselect apply-changes -b
else
pam_file="/etc/pam.d/common-auth"
if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+preauth.*$' "$pam_file" ; then
sed -i --follow-symlinks '/^auth.*pam_unix\.so.*/i auth required pam_faillock.so preauth' "$pam_file"
fi
if ! grep -qE '^\s*auth\s+\[default=die\]\s+pam_faillock\.so\s+authfail.*$' "$pam_file" ; then
num_lines=$(sed -n 's/^auth.*success=\([0-9]\).*pam_unix\.so.*/\1/p' "$pam_file")
if [ ! -z "$num_lines" ]; then
echo "$num_lines"
pattern=""
for ((i=1; i <= num_lines; i++)); do
pattern="${pattern}n;"
done;
sed -i --follow-symlinks '/^auth.*pam_unix\.so.*/{'$pattern'a auth [default=die] pam_faillock.so authfail
}' "$pam_file"
else
sed -i --follow-symlinks '/^auth.*pam_unix\.so.*/a auth [default=die] pam_faillock.so authfail' "$pam_file"
fi
fi
if ! grep -qE '^\s*auth\s+sufficient\s+pam_faillock\.so\s+authsucc.*$' "$pam_file" ; then
sed -i --follow-symlinks '/^auth.*pam_faillock\.so.*authfail.*/a auth sufficient pam_faillock.so authsucc' "$pam_file"
fi
pam_file="/etc/pam.d/common-account"
if ! grep -qE '^\s*account\s+required\s+pam_faillock\.so.*$' "$pam_file" ; then
echo 'account required pam_faillock.so' >> "$pam_file"
fi
fi
AUTH_FILES=("/etc/pam.d/common-auth" "/etc/pam.d/password-auth")
FAILLOCK_CONF="/etc/security/faillock.conf"
if [ -f $FAILLOCK_CONF ]; then
regex="^\s*audit"
line="audit"
if ! grep -q $regex $FAILLOCK_CONF; then
echo $line >> $FAILLOCK_CONF
fi
for pam_file in "${AUTH_FILES[@]}"
do
if [ -e "$pam_file" ] ; then
PAM_FILE_PATH="$pam_file"
if [ -f /usr/bin/authselect ]; then
if ! authselect check; then
echo "
authselect integrity check failed. Remediation aborted!
This remediation could not be applied because an authselect profile was not selected or the selected profile is not intact.
It is not recommended to manually edit the PAM files when authselect tool is available.
In cases where the default authselect profile does not cover a specific demand, a custom authselect profile is recommended."
exit 1
fi
CURRENT_PROFILE=$(authselect current -r | awk '{ print $1 }')
# If not already in use, a custom profile is created preserving the enabled features.
if [[ ! $CURRENT_PROFILE == custom/* ]]; then
ENABLED_FEATURES=$(authselect current | tail -n+3 | awk '{ print $2 }')
authselect create-profile hardening -b $CURRENT_PROFILE
CURRENT_PROFILE="custom/hardening"
authselect apply-changes -b --backup=before-hardening-custom-profile
authselect select $CURRENT_PROFILE
for feature in $ENABLED_FEATURES; do
authselect enable-feature $feature;
done
authselect apply-changes -b --backup=after-hardening-custom-profile
fi
PAM_FILE_NAME=$(basename "$pam_file")
PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"
authselect apply-changes -b
fi
if grep -qP '^\s*auth\s.*\bpam_faillock.so\s.*\baudit\b' "$PAM_FILE_PATH"; then
sed -i -E --follow-symlinks 's/(.*auth.*pam_faillock.so.*)\baudit\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
fi
if [ -f /usr/bin/authselect ]; then
authselect apply-changes -b
fi
else
echo "$pam_file was not found" >&2
fi
done
else
for pam_file in "${AUTH_FILES[@]}"
do
if ! grep -qE '^\s*auth.*pam_faillock\.so (preauth|authfail).*audit' "$pam_file"; then
sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ audit/' "$pam_file"
fi
done
fi