## Guide to the Secure Configuration of Red Hat Enterprise Linux 8

> with profile ANSSI-BP-028 (minimal)
> This profile contains configurations that align to ANSSI-BP-028 v2.0 at the minimal hardening level. ANSSI is the French National Information Security Agency, and stands for Agence nationale de la sécurité des systèmes d'information. ANSSI-BP-028 is a configuration recommendation for GNU/Linux systems. A copy of the ANSSI-BP-028 can be found at the ANSSI website: https://www.ssi.gouv.fr/administration/guide/recommandations-de-securite-relatives-a-un-systeme-gnulinux/

The SCAP Security Guide Project
https://www.open-scap.org/security-policies/scap-security-guide
This guide presents a catalog of security-relevant configuration settings for Red Hat Enterprise Linux 8. It is a rendering of content structured in the eXtensible Configuration Checklist Description Format (XCCDF) in order to support security automation. The SCAP content is is available in the `scap-security-guide` package which is developed at https://www.open-scap.org/security-policies/scap-security-guide.
Providing system administrators with such guidance informs them how to securely configure systems under their control in a variety of network roles. Policy makers and baseline creators can use this catalog of settings, with its associated references to higher-level security control catalogs, in order to assist them in security baseline creation. This guide is a catalog, not a checklist, and satisfaction of every item is not likely to be possible or sensible in many operational scenarios. However, the XCCDF format enables granular selection and adjustment of settings, and their association with OVAL and OCIL content provides an automated checking capability. Transformations of this document, and its associated automated checking content, are capable of providing baselines that meet a diverse set of policy objectives. Some example XCCDF Profiles, which are selections of items that form checklists and can be used as baselines, are available with this guide. They can be processed, in an automated fashion, with tools that support the Security Content Automation Protocol (SCAP). The DISA STIG, which provides required settings for US Department of Defense systems, is one example of a baseline created from this guidance.

This benchmark is a direct port of a SCAP Security Guide benchmark developed for Red Hat Enterprise Linux. It has been modified through an automated process to remove specific dependencies on Red Hat Enterprise Linux and to function with CentOS. The result is a generally useful SCAP Security Guide benchmark with the following caveats:

- CentOS is not an exact copy of Red Hat Enterprise Linux. There may be configuration differences that produce false positives and/or false negatives. If this occurs please file a bug report.
- CentOS has its own build system, compiler options, patchsets, and is a community supported, non-commercial operating system. CentOS does not inherit certifications or evaluations from Red Hat Enterprise Linux. As such, some configuration rules (such as those requiring FIPS 140-2 encryption) will continue to fail on CentOS.

Members of the CentOS community are invited to participate in OpenSCAP and SCAP Security Guide development. Bug reports and patches can be sent to GitHub: https://github.com/ComplianceAsCode/content. The mailing list is at https://fedorahosted.org/mailman/listinfo/scap-security-guide.

Do not attempt to implement any of the settings in this guide without first testing them in a non-operational environment. The creators of this guidance assume no responsibility whatsoever for its use by other parties, and makes no guarantees, expressed or implied, about its quality, reliability, or any other characteristic.

## Profile Information

Profile Title | ANSSI-BP-028 (minimal)
Profile ID | xccdf_org.ssgproject.content_profile_anssi_bp28_minimal

#### CPE Platforms

- cpe:/o:redhat:enterprise_linux:8.0
- cpe:/o:redhat:enterprise_linux:8.1
- cpe:/o:redhat:enterprise_linux:8.10
- cpe:/o:redhat:enterprise_linux:8.2
- cpe:/o:redhat:enterprise_linux:8.3
- cpe:/o:redhat:enterprise_linux:8.4
- cpe:/o:redhat:enterprise_linux:8.5
- cpe:/o:redhat:enterprise_linux:8.6
- cpe:/o:redhat:enterprise_linux:8.7
- cpe:/o:redhat:enterprise_linux:8.8
- cpe:/o:redhat:enterprise_linux:8.9
- cpe:/o:redhat:enterprise_linux:8
- cpe:/o:centos:centos:8

## Revision History

Current version: 0.1.71

- draft (as of 2023-12-16)

## Table of Contents

1. System Settings
   1. Installing and Maintaining Software
   2. Account and Access Control
   3. File Permissions and Masks
2. Services
   1. DHCP
   2. Mail Server Software
   3. Obsolete Services

## Checklist

Group Guide to the Secure Configuration of Red Hat Enterprise Linux 8 Group contains 26 groups and 47 rules
Group System Settings Group contains 14 groups and 34 rules
[ref] Contains rules that check correct system settings.
Group Installing and Maintaining Software Group contains 2 groups and 11 rules
[ref] The following sections contain information on security-relevant choices during the initial operating system installation process and the setup of software updates.
Group Sudo Group contains 2 rules

[ref] `Sudo`, which stands for "su 'do'", provides the ability to delegate authority to certain users, groups of users, or system administrators. When configured for system users and/or groups, `Sudo` can allow a user or group to execute privileged commands that normally only `root` is allowed to execute.
For more information on `Sudo` and addition `Sudo` configuration options, see https://www.sudo.ws.

#### Rule Ensure Users Re-Authenticate for Privilege Escalation - sudo !authenticate [ref]

The sudo `!authenticate` option, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that the `!authenticate` option does not exist in `/etc/sudoers` configuration file or any sudo configuration snippets in `/etc/sudoers.d/`.
Rationale: | Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sudo_remove_no_authenticate

Identifiers and References

References: BP28(R5), BP28(R59), 1, 12, 15, 16, 5, DSS05.04, DSS05.10, DSS06.03, DSS06.10, CCI-002038, 4.3.3.5.1, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-11, CM-6(a), PR.AC-1, PR.AC-7, SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158, SV-230272r854027_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    for f in /etc/sudoers /etc/sudoers.d/* ; do
      if [ ! -e "$f" ] ; then
        continue
      fi
      matching_list=$(grep -P '^(?!#).*[\s]+\!authenticate.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          # comment out "!authenticate" matches to preserve user data
          sed -i "s/^${entry}$/# &/g" $f
        done <<< "$matching_list"

        /usr/sbin/visudo -cf $f &> /dev/null || echo "Fail to validate $f with visudo"
      fi
    done

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Find /etc/sudoers.d/ files
      ansible.builtin.find:
        paths:
        - /etc/sudoers.d/
      register: sudoers
      tags:
      - DISA-STIG-RHEL-08-010381
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_remove_no_authenticate

    - name: Remove lines containing !authenticate from sudoers files
      ansible.builtin.replace:
        regexp: (^(?!#).*[\s]+\!authenticate.*$)
        replace: '# \g<1>'
        path: '{{ item.path }}'
        validate: /usr/sbin/visudo -cf %s
      with_items:
      - path: /etc/sudoers
      - '{{ sudoers.files }}'
      tags:
      - DISA-STIG-RHEL-08-010381
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_remove_no_authenticate

#### Rule Ensure Users Re-Authenticate for Privilege Escalation - sudo NOPASSWD [ref]

The sudo `NOPASSWD` tag, when specified, allows a user to execute commands using sudo without having to authenticate. This should be disabled by making sure that the `NOPASSWD` tag does not exist in `/etc/sudoers` configuration file or any sudo configuration snippets in `/etc/sudoers.d/`.
Warning: This rule is disabled on Red Hat Virtualization Hosts and Managers, it will report not applicable. RHV requires to perform operations as root without being asked for password.

Rationale: | Without re-authentication, users may access resources or perform tasks for which they do not have authorization. When operating systems provide the capability to escalate a functional capability, it is critical that the user re-authenticate.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_sudo_remove_nopasswd

Identifiers and References

References: BP28(R5), BP28(R59), 1, 12, 15, 16, 5, DSS05.04, DSS05.10, DSS06.03, DSS06.10, CCI-002038, 4.3.3.5.1, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, A.18.1.4, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-11, CM-6(a), PR.AC-1, PR.AC-7, SRG-OS-000373-GPOS-00156, SRG-OS-000373-GPOS-00157, SRG-OS-000373-GPOS-00158, SV-230271r854026_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    for f in /etc/sudoers /etc/sudoers.d/* ; do
      if [ ! -e "$f" ] ; then
        continue
      fi
      matching_list=$(grep -P '^(?!#).*[\s]+NOPASSWD[\s]*\:.*$' $f | uniq )
      if ! test -z "$matching_list"; then
        while IFS= read -r entry; do
          # comment out "NOPASSWD" matches to preserve user data
          sed -i "s/^${entry}$/# &/g" $f
        done <<< "$matching_list"

        /usr/sbin/visudo -cf $f &> /dev/null || echo "Fail to validate $f with visudo"
      fi
    done

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Find /etc/sudoers.d/ files
      ansible.builtin.find:
        paths:
        - /etc/sudoers.d/
      register: sudoers
      tags:
      - DISA-STIG-RHEL-08-010380
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_remove_nopasswd

    - name: Remove lines containing NOPASSWD from sudoers files
      ansible.builtin.replace:
        regexp: (^(?!#).*[\s]+NOPASSWD[\s]*\:.*$)
        replace: '# \g<1>'
        path: '{{ item.path }}'
        validate: /usr/sbin/visudo -cf %s
      with_items:
      - path: /etc/sudoers
      - '{{ sudoers.files }}'
      tags:
      - DISA-STIG-RHEL-08-010380
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-11
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
      - sudo_remove_nopasswd

Group Updating Software Group contains 9 rules

[ref] The `yum` command line tool is used to install and update software packages. The system also provides a graphical software update tool in the System menu, in the Administration submenu, called Software Update.
Red Hat Enterprise Linux 8 systems contain an installed software catalog called the RPM database, which records metadata of installed packages. Consistently using `yum` or the graphical Software Update for all software installation allows for insight into the current inventory of installed software on the system.

#### Rule Install dnf-automatic Package [ref]

The `dnf-automatic` package can be installed with the following command:

    $ sudo yum install dnf-automatic

Rationale: | `dnf-automatic` is an alternative command line interface (CLI) to `dnf upgrade` suitable for automatic, regular execution.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_dnf-automatic_installed
Identifiers and References | References: BP28(R8), SRG-OS-000191-GPOS-00080

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    if ! rpm -q --quiet "dnf-automatic" ; then
        yum install -y "dnf-automatic"
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    package --add=dnf-automatic

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Ensure dnf-automatic is installed
      package:
        name: dnf-automatic
        state: present
      tags:
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_dnf-automatic_installed

Remediation OSBuild Blueprint snippet ⇲

    [[packages]]
    name = "dnf-automatic"
    version = "*"

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    include install_dnf-automatic

    class install_dnf-automatic {
      package { 'dnf-automatic':
        ensure => 'installed',
      }
    }

#### Rule Configure dnf-automatic to Install Available Updates Automatically [ref]

To ensure that the packages comprising the available updates will be automatically installed by `dnf-automatic`, set `apply_updates` to `yes` under `[commands]` section in `/etc/dnf/automatic.conf`.

Rationale:

Installing software updates is a fundamental mitigation against the exploitation of publicly-known vulnerabilities. If the most recent security patches and updates are not installed, unauthorized users may take advantage of weaknesses in the unpatched software. The lack of prompt attention to patching could result in a system compromise. The automated installation of updates ensures that recent security patches are applied in a timely manner.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_dnf-automatic_apply_updates
Identifiers and References | References: BP28(R8), 0940, 1144, 1467, 1472, 1483, 1493, 1494, 1495, SI-2(5), CM-6(a), SI-2(c), FMT_SMF_EXT.1, SRG-OS-000191-GPOS-00080

Remediation Shell script ⇲

    found=false

    # set value in all files if they contain section or key
    for f in $(echo -n "/etc/dnf/automatic.conf"); do
        if [ ! -e "$f" ]; then
            continue
        fi

        # find key in section and change value
        if grep -qzosP "[[:space:]]*\[commands\]([^\n\[]*\n+)+?[[:space:]]*apply_updates" "$f"; then
                sed -i "s/apply_updates[^(\n)]*/apply_updates = yes/" "$f"
                found=true

        # find section and add key = value to it
        elif grep -qs "[[:space:]]*\[commands\]" "$f"; then
                sed -i "/[[:space:]]*\[commands\]/a apply_updates = yes" "$f"
                found=true
        fi
    done

    # if section not in any file, append section with key = value to FIRST file in files parameter
    if ! $found ; then
        file=$(echo "/etc/dnf/automatic.conf" | cut -f1 -d ' ')
        mkdir -p "$(dirname "$file")"
        echo -e "[commands]\napply_updates = yes" >> "$file"
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown

    - name: Configure dnf-automatic to Install Available Updates Automatically
      ini_file:
        dest: /etc/dnf/automatic.conf
        section: commands
        option: apply_updates
        value: 'yes'
        create: true
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-2(5)
      - NIST-800-53-SI-2(c)
      - dnf-automatic_apply_updates
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - unknown_strategy

#### Rule Configure dnf-automatic to Install Only Security Updates [ref]

To configure `dnf-automatic` to install only security updates automatically, set `upgrade_type` to `security` under `[commands]` section in `/etc/dnf/automatic.conf`.
Rationale: | By default, `dnf-automatic` installs all available updates. Reducing the amount of updated packages only to updates that were issued as a part of a security advisory increases the system stability.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_dnf-automatic_security_updates_only
Identifiers and References | References: BP28(R8), SI-2(5), CM-6(a), SI-2(c), FMT_SMF_EXT.1, SRG-OS-000191-GPOS-00080

Remediation Shell script ⇲

    found=false

    # set value in all files if they contain section or key
    for f in $(echo -n "/etc/dnf/automatic.conf"); do
        if [ ! -e "$f" ]; then
            continue
        fi

        # find key in section and change value
        if grep -qzosP "[[:space:]]*\[commands\]([^\n\[]*\n+)+?[[:space:]]*upgrade_type" "$f"; then
                sed -i "s/upgrade_type[^(\n)]*/upgrade_type = security/" "$f"
                found=true

        # find section and add key = value to it
        elif grep -qs "[[:space:]]*\[commands\]" "$f"; then
                sed -i "/[[:space:]]*\[commands\]/a upgrade_type = security" "$f"
                found=true
        fi
    done

    # if section not in any file, append section with key = value to FIRST file in files parameter
    if ! $found ; then
        file=$(echo "/etc/dnf/automatic.conf" | cut -f1 -d ' ')
        mkdir -p "$(dirname "$file")"
        echo -e "[commands]\nupgrade_type = security" >> "$file"
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown

    - name: Configure dnf-automatic to Install Only Security Updates
      ini_file:
        dest: /etc/dnf/automatic.conf
        section: commands
        option: upgrade_type
        value: security
        create: true
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-2(5)
      - NIST-800-53-SI-2(c)
      - dnf-automatic_security_updates_only
      - low_complexity
      - low_severity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

#### Rule Ensure gpgcheck Enabled In Main yum Configuration [ref]

The `gpgcheck` option controls whether RPM packages' signatures are always checked prior to installation. To configure yum to check package signatures before installing them, ensure the following line appears in `/etc/yum.conf` in the `[main]` section:

    gpgcheck=1

Rationale:

Changes to any software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor.
Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization.
Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA).

Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_gpgcheck_globally_activated

Identifiers and References

References: BP28(R15), 11, 2, 3, 9, 5.10.4.1, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), SA-12, SA-12(10), CM-11(a), CM-11(b), PR.DS-6, PR.DS-8, PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, Req-6.2, 6.3.3, SRG-OS-000366-GPOS-00153, 1.2.3, SV-230264r880711_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q yum; then

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^gpgcheck")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "1"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^gpgcheck\\>" "/etc/yum.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^gpgcheck\\>.*/$escaped_formatted_output/gi" "/etc/yum.conf"
    else
        if [[ -s "/etc/yum.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/yum.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/yum.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/yum.conf" >> "/etc/yum.conf"
        printf '%s\n' "$formatted_output" >> "/etc/yum.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010370
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-11(a)
      - NIST-800-53-CM-11(b)
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SA-12
      - NIST-800-53-SA-12(10)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - configure_strategy
      - ensure_gpgcheck_globally_activated
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed

    - name: Ensure GPG check is globally activated
      ini_file:
        dest: /etc/yum.conf
        section: main
        option: gpgcheck
        value: 1
        no_extra_spaces: true
        create: false
      when: '"yum" in ansible_facts.packages'
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010370
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-11(a)
      - NIST-800-53-CM-11(b)
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SA-12
      - NIST-800-53-SA-12(10)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - configure_strategy
      - ensure_gpgcheck_globally_activated
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed

#### Rule Ensure gpgcheck Enabled for Local Packages [ref]

`yum` should be configured to verify the signature(s) of local packages prior to installation. To configure `yum` to verify signatures of local packages, set the `localpkg_gpgcheck` to `1` in `/etc/yum.conf`.
Rationale: | Changes to any software components can have significant effects to the overall security of the operating system. This requirement ensures the software has not been tampered and has been provided by a trusted vendor. Accordingly, patches, service packs, device drivers, or operating system components must be signed with a certificate recognized and approved by the organization.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_gpgcheck_local_packages

Identifiers and References

References: BP28(R15), 11, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, CM-11(a), CM-11(b), CM-6(a), CM-5(3), SA-12, SA-12(10), PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, SRG-OS-000366-GPOS-00153, SV-230265r877463_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q yum; then

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^localpkg_gpgcheck")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "1"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^localpkg_gpgcheck\\>" "/etc/yum.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^localpkg_gpgcheck\\>.*/$escaped_formatted_output/gi" "/etc/yum.conf"
    else
        if [[ -s "/etc/yum.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/yum.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/yum.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/yum.conf" >> "/etc/yum.conf"
        printf '%s\n' "$formatted_output" >> "/etc/yum.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | unknown

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-010371
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-11(a)
      - NIST-800-53-CM-11(b)
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SA-12
      - NIST-800-53-SA-12(10)
      - ensure_gpgcheck_local_packages
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

    - name: Ensure GPG check Enabled for Local Packages (yum)
      block:

      - name: Check stats of yum
        stat:
          path: /etc/yum.conf
        register: pkg

      - name: Check if config file of yum is a symlink
        ansible.builtin.set_fact:
          pkg_config_file_symlink: '{{ pkg.stat.lnk_target if pkg.stat.lnk_target is match("^/.*")
            else "/etc/yum.conf" | dirname ~ "/" ~ pkg.stat.lnk_target }}'
        when: pkg.stat.lnk_target is defined

      - name: Ensure GPG check Enabled for Local Packages (yum)
        ini_file:
          dest: '{{ pkg_config_file_symlink |  default("/etc/yum.conf") }}'
          section: main
          option: localpkg_gpgcheck
          value: 1
          no_extra_spaces: true
          create: true
      when: '"yum" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-010371
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-11(a)
      - NIST-800-53-CM-11(b)
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SA-12
      - NIST-800-53-SA-12(10)
      - ensure_gpgcheck_local_packages
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed
      - unknown_strategy

#### Rule Ensure gpgcheck Enabled for All yum Package Repositories [ref]

To ensure signature checking is not disabled for any repos, remove any lines from files in `/etc/yum.repos.d` of the form:

    gpgcheck=0

Rationale:

Verifying the authenticity of the software prior to installation validates the integrity of the patch or upgrade received from a vendor. This ensures the software has not been tampered with and that it has been provided by a trusted vendor. Self-signed certificates are disallowed by this requirement. Certificates used to verify the software must be from an approved Certificate Authority (CA)."

Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_gpgcheck_never_disabled

Identifiers and References

References: BP28(R15), 11, 2, 3, 9, 5.10.4.1, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), SA-12, SA-12(10), CM-11(a), CM-11(b), PR.DS-6, PR.DS-8, PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, Req-6.2, 6.3.3, SRG-OS-000366-GPOS-00153, SV-230264r880711_rule

Remediation Shell script ⇲

    sed -i 's/gpgcheck\s*=.*/gpgcheck=1/g' /etc/yum.repos.d/*

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | enable

    - name: Grep for yum repo section names
      shell: |
        set -o pipefail
        grep -HEr '^\[.+\]' -r /etc/yum.repos.d/
      register: repo_grep_results
      failed_when: repo_grep_results.rc not in [0, 1]
      changed_when: false
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010370
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-11(a)
      - NIST-800-53-CM-11(b)
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SA-12
      - NIST-800-53-SA-12(10)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - enable_strategy
      - ensure_gpgcheck_never_disabled
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed

    - name: Set gpgcheck=1 for each yum repo
      ini_file:
        path: '{{ item[0] }}'
        section: '{{ item[1] }}'
        option: gpgcheck
        value: '1'
        no_extra_spaces: true
      loop: '{{ repo_grep_results.stdout | regex_findall( ''(.+\.repo):\[(.+)\]\n?'' )
        }}'
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010370
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-11(a)
      - NIST-800-53-CM-11(b)
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SA-12
      - NIST-800-53-SA-12(10)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - enable_strategy
      - ensure_gpgcheck_never_disabled
      - high_severity
      - low_complexity
      - medium_disruption
      - no_reboot_needed

#### Rule Ensure Red Hat GPG Key Installed [ref]

To ensure the system can cryptographically verify base software packages come from Red Hat (and to connect to the Red Hat Network to receive them), the Red Hat GPG key must properly be installed. To install the Red Hat GPG key, run:

    $ sudo subscription-manager register

If the system is not connected to the Internet or an RHN Satellite, then install the Red Hat GPG key from trusted media such as the Red Hat installation CD-ROM or DVD. Assuming the disc is mounted in `/media/cdrom`, use the following command as the root user to import it into the keyring:

    $ sudo rpm --import /media/cdrom/RPM-GPG-KEY

Alternatively, the key may be pre-loaded during the RHEL installation. In such cases, the key can be installed by running the following command:

    sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

Rationale: | Changes to software components can have significant effects on the overall security of the operating system. This requirement ensures the software has not been tampered with and that it has been provided by a trusted vendor. The Red Hat GPG key is necessary to cryptographically verify packages are from Red Hat.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_ensure_redhat_gpgkey_installed

Identifiers and References

References: BP28(R15), 11, 2, 3, 9, 5.10.4.1, APO01.06, BAI03.05, BAI06.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS06.02, 3.4.8, CCI-001749, 164.308(a)(1)(ii)(D), 164.312(b), 164.312(c)(1), 164.312(c)(2), 164.312(e)(2)(i), 4.3.4.3.2, 4.3.4.3.3, 4.3.4.4.4, SR 3.1, SR 3.3, SR 3.4, SR 3.8, SR 7.6, A.11.2.4, A.12.1.2, A.12.2.1, A.12.5.1, A.12.6.2, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, CIP-003-8 R4.2, CIP-003-8 R6, CIP-007-3 R4, CIP-007-3 R4.1, CIP-007-3 R4.2, CIP-007-3 R5.1, CM-5(3), SI-7, SC-12, SC-12(3), CM-6(a), PR.DS-6, PR.DS-8, PR.IP-1, FPT_TUD_EXT.1, FPT_TUD_EXT.2, Req-6.2, 6.3.3, SRG-OS-000366-GPOS-00153, 1.2.2, SV-256973r902752_rule

Remediation Shell script ⇲

    # The two fingerprints below are retrieved from https://access.redhat.com/security/team/key
    readonly REDHAT_RELEASE_FINGERPRINT="567E347AD0044ADE55BA8A5F199E2F91FD431D51"
    readonly REDHAT_AUXILIARY_FINGERPRINT="6A6AA7C97C8890AEC6AEBFE2F76F66C3D4082792"

    # Location of the key we would like to import (once it's integrity verified)
    readonly REDHAT_RELEASE_KEY="/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"

    RPM_GPG_DIR_PERMS=$(stat -c %a "$(dirname "$REDHAT_RELEASE_KEY")")

    # Verify /etc/pki/rpm-gpg directory permissions are safe
    if [ "${RPM_GPG_DIR_PERMS}" -le "755" ]
    then
      # If they are safe, try to obtain fingerprints from the key file
      # (to ensure there won't be e.g. CRC error).

      readarray -t GPG_OUT < <(gpg --show-keys --with-fingerprint --with-colons "$REDHAT_RELEASE_KEY" | grep -A1 "^pub" | grep "^fpr" | cut -d ":" -f 10)

      GPG_RESULT=$?
      # No CRC error, safe to proceed
      if [ "${GPG_RESULT}" -eq "0" ]
      then
        echo "${GPG_OUT[*]}" | grep -vE "${REDHAT_RELEASE_FINGERPRINT}|${REDHAT_AUXILIARY_FINGERPRINT}" || {
          # If $REDHAT_RELEASE_KEY file doesn't contain any keys with unknown fingerprint, import it
          rpm --import "${REDHAT_RELEASE_KEY}"
        }
      fi
    fi

Remediation Ansible snippet ⇲

Complexity: | medium
Disruption: | medium
Reboot: | false
Strategy: | restrict

    - name: Read permission of GPG key directory
      stat:
        path: /etc/pki/rpm-gpg/
      register: gpg_key_directory_permission
      check_mode: false
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010019
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - ensure_redhat_gpgkey_installed
      - high_severity
      - medium_complexity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Read signatures in GPG key
      command: gpg --show-keys --with-fingerprint --with-colons "/etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release"
      changed_when: false
      register: gpg_fingerprints
      check_mode: false
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010019
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - ensure_redhat_gpgkey_installed
      - high_severity
      - medium_complexity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Set Fact - Installed GPG Fingerprints
      set_fact:
        gpg_installed_fingerprints: |-
          {{ gpg_fingerprints.stdout | regex_findall('^pub.*
          (?:^fpr[:]*)([0-9A-Fa-f]*)', '\1') | list }}
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010019
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - ensure_redhat_gpgkey_installed
      - high_severity
      - medium_complexity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Set Fact - Valid fingerprints
      set_fact:
        gpg_valid_fingerprints: ("567E347AD0044ADE55BA8A5F199E2F91FD431D51" "6A6AA7C97C8890AEC6AEBFE2F76F66C3D4082792")
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010019
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - ensure_redhat_gpgkey_installed
      - high_severity
      - medium_complexity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy

    - name: Import RedHat GPG key
      rpm_key:
        state: present
        key: /etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
      when:
      - gpg_key_directory_permission.stat.mode <= '0755'
      - (gpg_installed_fingerprints | difference(gpg_valid_fingerprints)) | length ==
        0
      - gpg_installed_fingerprints | length > 0
      - ansible_distribution == "RedHat"
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010019
      - NIST-800-171-3.4.8
      - NIST-800-53-CM-5(3)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SC-12
      - NIST-800-53-SC-12(3)
      - NIST-800-53-SI-7
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - ensure_redhat_gpgkey_installed
      - high_severity
      - medium_complexity
      - medium_disruption
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure Software Patches Installed [ref]

If the system is joined to the Red Hat Network, a Red Hat Satellite Server, or a yum server, run the following command to install updates:

    $ sudo yum update

If the system is not configured to use one of these sources, updates (in the form of RPM packages) can be manually downloaded from the Red Hat Network and installed using `rpm`.
NOTE: U.S. Defense systems are required to be patched within 30 days or sooner as local policy dictates.
Warning: The OVAL feed of Red Hat Enterprise Linux 8 is not a XML file, which may not be understood by all scanners.

Rationale: | Installing software updates is a fundamental mitigation against the exploitation of publicly-known vulnerabilities. If the most recent security patches and updates are not installed, unauthorized users may take advantage of weaknesses in the unpatched software. The lack of prompt attention to patching could result in a system compromise.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_security_patches_up_to_date
Identifiers and References | References: BP28(R08), 18, 20, 4, 5.10.4.1, APO12.01, APO12.02, APO12.03, APO12.04, BAI03.10, DSS05.01, DSS05.02, CCI-000366, CCI-001227, 4.2.3, 4.2.3.12, 4.2.3.7, 4.2.3.9, A.12.6.1, A.14.2.3, A.16.1.3, A.18.2.2, A.18.2.3, SI-2(5), SI-2(c), CM-6(a), ID.RA-1, PR.IP-12, FMT_MOF_EXT.1, Req-6.2, 6.3.3, SRG-OS-000480-GPOS-00227, 1.9, SV-230222r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | high
Reboot: | true
Strategy: | patch

    yum -y update

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | high
Reboot: | true
Strategy: | patch

    - name: Security patches are up to date
      package:
        name: '*'
        state: latest
      tags:
      - CJIS-5.10.4.1
      - DISA-STIG-RHEL-08-010010
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-2(5)
      - NIST-800-53-SI-2(c)
      - PCI-DSS-Req-6.2
      - PCI-DSSv4-6.3.3
      - high_disruption
      - low_complexity
      - medium_severity
      - patch_strategy
      - reboot_required
      - security_patches_up_to_date
      - skip_ansible_lint

#### Rule Enable dnf-automatic Timer [ref]

The `dnf-automatic` timer can be enabled with the following command:

    $ sudo systemctl enable dnf-automatic.timer

Rationale: | The `dnf-automatic` is an alternative command line interface (CLI) to `dnf upgrade` with specific facilities to make it suitable to be executed automatically and regularly from systemd timers, cron jobs and similar. The tool is controlled by `dnf-automatic.timer` SystemD timer.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_timer_dnf-automatic_enabled
Identifiers and References | References: BP28(R8), SI-2(5), CM-6(a), SI-2(c), FMT_SMF_EXT.1, SRG-OS-000191-GPOS-00080

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    SYSTEMCTL_EXEC='/usr/bin/systemctl'
    "$SYSTEMCTL_EXEC" start 'dnf-automatic.timer'
    "$SYSTEMCTL_EXEC" enable 'dnf-automatic.timer'

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | enable

    - name: Enable timer dnf-automatic
      block:

      - name: Gather the package facts
        package_facts:
          manager: auto

      - name: Enable timer dnf-automatic
        systemd:
          name: dnf-automatic.timer
          enabled: 'yes'
          state: started
        when:
        - '"dnf-automatic" in ansible_facts.packages'
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-SI-2(5)
      - NIST-800-53-SI-2(c)
      - enable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - timer_dnf-automatic_enabled

Group Account and Access Control Group contains 8 groups and 16 rules

[ref] In traditional Unix security, if an attacker gains shell access to a certain login account, they can perform any action or access any file to which that account has access. Therefore, making it more difficult for unauthorized people to gain shell access to accounts, particularly to privileged accounts, is a necessary part of securing a system. This section introduces mechanisms for restricting access to accounts under Red Hat Enterprise Linux 8.

Group Protect Accounts by Configuring PAM Group contains 4 groups and 11 rules

[ref] PAM, or Pluggable Authentication Modules, is a system which implements modular authentication for Linux programs. PAM provides a flexible and configurable architecture for authentication, and it should be configured to minimize exposure to unnecessary risk. This section contains guidance on how to accomplish that.
PAM is implemented as a set of shared objects which are loaded and invoked whenever an application wishes to authenticate a user. Typically, the application must be running as root in order to take advantage of PAM, because PAM's modules often need to be able to access sensitive stores of account information, such as /etc/shadow. Traditional privileged network listeners (e.g. sshd) or SUID programs (e.g. sudo) already meet this requirement. An SUID root application, userhelper, is provided so that programs which are not SUID or privileged themselves can still take advantage of PAM.
PAM looks in the directory `/etc/pam.d` for application-specific configuration information. For instance, if the program login attempts to authenticate a user, then PAM's libraries follow the instructions in the file `/etc/pam.d/login` to determine what actions should be taken.
One very important file in `/etc/pam.d` is `/etc/pam.d/system-auth`. This file, which is included by many other PAM configuration files, defines 'default' system authentication measures. Modifying this file is a good way to make far-reaching authentication changes, for instance when implementing a centralized authentication service.
Warning: Be careful when making changes to PAM's configuration files. The syntax for these files is complex, and modifications can have unexpected consequences. The default configurations shipped with applications should be sufficient for most users.
Warning: Running `authconfig` or `system-config-authentication` will re-write the PAM configuration files, destroying any manually made changes and replacing them with a series of system defaults. One reference to the configuration file syntax can be found at https://fossies.org/linux/Linux-PAM-docs/doc/sag/Linux-PAM_SAG.pdf.

Group Set Lockouts for Failed Password Attempts Group contains 5 rules

[ref] The `pam_faillock` PAM module provides the capability to lock out user accounts after a number of failed login attempts. Its documentation is available in `/usr/share/doc/pam-VERSION/txts/README.pam_faillock`.
Warning: Locking out user accounts presents the risk of a denial-of-service attack. The lockout policy must weigh whether the risk of such a denial-of-service attack outweighs the benefits of thwarting password guessing attacks.

#### Rule Limit Password Reuse [ref]

Do not allow users to reuse recent passwords. This can be accomplished by using the `remember` option for the `pam_unix` or `pam_pwhistory` PAM modules.
Warning: If the system relies on `authselect` tool to manage PAM settings, the remediation will also use `authselect` tool. However, if any manual modification was made in PAM files, the `authselect` integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report.
Warning: Newer versions of `authselect` contain an authselect feature to easily and properly enable `pam_pwhistory.so` module. If this feature is not yet available in your system, an authselect custom profile must be used to avoid integrity issues in PAM files.

Rationale: | Preventing re-use of previous passwords helps ensure that a compromised password is not re-used by a user.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_remember

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, 5.6.2.1.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.8, CCI-000200, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(f), IA-5(1)(e), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.5, 8.3.7, SRG-OS-000077-GPOS-00045, 5.4.3

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_unix_remember='2'

    if [ -f /usr/bin/authselect ]; then
        if authselect list-features minimal | grep -q with-pwhistory; 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-pwhistory

            authselect apply-changes -b
        else

            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 "/etc/pam.d/system-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
            if ! grep -qP '^\s*password\s+'"requisite"'\s+pam_pwhistory.so\s*.*' "$PAM_FILE_PATH"; then
                # Line matching group + control + module was not found. Check group + module.
                if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                    # The control is updated only if one single line matches.
                    sed -i -E --follow-symlinks 's/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1'"requisite"' \2/' "$PAM_FILE_PATH"
                else
                    LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "$PAM_FILE_PATH" | tail -n 1 | cut -d: -f 1)
                    if [ ! -z $LAST_MATCH_LINE ]; then
                        sed -i --follow-symlinks $LAST_MATCH_LINE' a password     '"requisite"'    pam_pwhistory.so' "$PAM_FILE_PATH"
                    else
                        echo 'password    '"requisite"'    pam_pwhistory.so' >> "$PAM_FILE_PATH"
                    fi
                fi
            fi
        fi
    else
        if ! grep -qP '^\s*password\s+'"requisite"'\s+pam_pwhistory.so\s*.*' "/etc/pam.d/system-auth"; then
            # Line matching group + control + module was not found. Check group + module.
            if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "/etc/pam.d/system-auth")" -eq 1 ]; then
                # The control is updated only if one single line matches.
                sed -i -E --follow-symlinks 's/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1'"requisite"' \2/' "/etc/pam.d/system-auth"
            else
                LAST_MATCH_LINE=$(grep -nP "^password.*requisite.*pam_pwquality\.so" "/etc/pam.d/system-auth" | tail -n 1 | cut -d: -f 1)
                if [ ! -z $LAST_MATCH_LINE ]; then
                    sed -i --follow-symlinks $LAST_MATCH_LINE' a password     '"requisite"'    pam_pwhistory.so' "/etc/pam.d/system-auth"
                else
                    echo 'password    '"requisite"'    pam_pwhistory.so' >> "/etc/pam.d/system-auth"
                fi
            fi
        fi
    fi

    PWHISTORY_CONF="/etc/security/pwhistory.conf"
    if [ -f $PWHISTORY_CONF ]; then
        regex="^\s*remember\s*="
        line="remember = $var_password_pam_unix_remember"
        if ! grep -q $regex $PWHISTORY_CONF; then
            echo $line >> $PWHISTORY_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(remember\s*=\s*\)\(\S\+\)|\1'"$var_password_pam_unix_remember"'|g' $PWHISTORY_CONF
        fi
        if [ -e "/etc/pam.d/system-auth" ] ; then
            PAM_FILE_PATH="/etc/pam.d/system-auth"
            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 "/etc/pam.d/system-auth")
                PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

                authselect apply-changes -b
            fi

        if grep -qP '^\s*password\s.*\bpam_pwhistory.so\s.*\bremember\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks 's/(.*password.*pam_pwhistory.so.*)\bremember\b=?[[:alnum:]]*(.*)/\1\2/g' "$PAM_FILE_PATH"
        fi
            if [ -f /usr/bin/authselect ]; then

                authselect apply-changes -b
            fi
        else
            echo "/etc/pam.d/system-auth was not found" >&2
        fi
    else
        PAM_FILE_PATH="/etc/pam.d/system-auth"
        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 "/etc/pam.d/system-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi
        if ! grep -qP '^\s*password\s+'"requisite"'\s+pam_pwhistory.so\s*.*' "$PAM_FILE_PATH"; then
            # Line matching group + control + module was not found. Check group + module.
            if [ "$(grep -cP '^\s*password\s+.*\s+pam_pwhistory.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                # The control is updated only if one single line matches.
                sed -i -E --follow-symlinks 's/^(\s*password\s+).*(\bpam_pwhistory.so.*)/\1'"requisite"' \2/' "$PAM_FILE_PATH"
            else
                echo 'password    '"requisite"'    pam_pwhistory.so' >> "$PAM_FILE_PATH"
            fi
        fi
        # Check the option
        if ! grep -qP '^\s*password\s+'"requisite"'\s+pam_pwhistory.so\s*.*\sremember\b' "$PAM_FILE_PATH"; then
            sed -i -E --follow-symlinks '/\s*password\s+'"requisite"'\s+pam_pwhistory.so.*/ s/$/ remember='"$var_password_pam_unix_remember"'/' "$PAM_FILE_PATH"
        else
            sed -i -E --follow-symlinks 's/(\s*password\s+'"requisite"'\s+pam_pwhistory.so\s+.*)('"remember"'=)[[:alnum:]]+\s*(.*)/\1\2'"$var_password_pam_unix_remember"' \3/' "$PAM_FILE_PATH"
        fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_unix_remember
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_password_pam_unix_remember # promote to variable
      set_fact:
        var_password_pam_unix_remember: !!str 2
      tags:
        - always

    - name: Limit Password Reuse - Check if system relies on authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_unix_remember
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Limit Password Reuse - Collect the available authselect features
      ansible.builtin.command:
        cmd: authselect list-features minimal
      register: result_authselect_available_features
      changed_when: false
      when:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_unix_remember
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Limit Password Reuse - Enable pam_pwhistory.so using authselect feature
      block:

      - name: Limit Password Reuse - Check integrity of authselect current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

      - name: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - Ensure "with-pwhistory" feature is enabled using
          authselect tool
        ansible.builtin.command:
          cmd: authselect enable-feature with-pwhistory
        register: result_authselect_enable_feature_cmd
        when:
        - result_authselect_check_cmd is success
        - result_authselect_features.stdout is not search("with-pwhistory")

      - name: Limit Password Reuse - 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:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      - result_authselect_available_features.stdout is search("with-pwhistory")
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_unix_remember
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Limit Password Reuse - Enable pam_pwhistory.so in appropriate PAM files
      block:

      - name: Limit Password Reuse - Define the PAM file to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: Limit Password Reuse - Check if system relies on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Limit Password Reuse - Ensure authselect custom profile is used if authselect
          is present
        block:

        - name: Limit Password Reuse - Check integrity of authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          failed_when: false

        - name: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - Check if expected PAM module line is present in {{
          pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwhistory.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: Limit Password Reuse - Include or update the PAM module line in {{ pam_file_path
          }}
        block:

        - name: Limit Password Reuse - Check if required PAM module line is present in
            {{ pam_file_path }} with different control
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: Limit Password Reuse - Ensure the correct control for the required PAM
            module line in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*)
            replace: \1requisite \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: Limit Password Reuse - Ensure the required PAM module line is included
            in {{ pam_file_path }}
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            insertafter: ^password.*requisite.*pam_pwquality\.so
            line: password    requisite    pam_pwhistory.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: Limit Password Reuse - 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:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0
      when:
      - '"pam" in ansible_facts.packages'
      - |
        (result_authselect_available_features.stdout is defined and result_authselect_available_features.stdout is not search("with-pwhistory")) or result_authselect_available_features is not defined
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_unix_remember
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Limit Password Reuse - Check the presence of /etc/security/pwhistory.conf
        file
      ansible.builtin.stat:
        path: /etc/security/pwhistory.conf
      register: result_pwhistory_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_unix_remember
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Limit Password Reuse - pam_pwhistory.so parameters are configured in /etc/security/pwhistory.conf
        file
      block:

      - name: Limit Password Reuse - Ensure the pam_pwhistory.so remember parameter in
          /etc/security/pwhistory.conf
        ansible.builtin.lineinfile:
          path: /etc/security/pwhistory.conf
          regexp: ^\s*remember\s*=
          line: remember = {{ var_password_pam_unix_remember }}
          state: present

      - name: Limit Password Reuse - Ensure the pam_pwhistory.so remember parameter is
          removed from PAM files
        block:

        - name: Limit Password Reuse - 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: Limit Password Reuse - Check the proper remediation for the system
          block:

          - name: Limit Password Reuse - Define the PAM file to be edited as a local fact
            ansible.builtin.set_fact:
              pam_file_path: /etc/pam.d/system-auth

          - name: Limit Password Reuse - Check if system relies on authselect tool
            ansible.builtin.stat:
              path: /usr/bin/authselect
            register: result_authselect_present

          - name: Limit Password Reuse - Ensure authselect custom profile is used if authselect
              is present
            block:

            - name: Limit Password Reuse - Check integrity of authselect current profile
              ansible.builtin.command:
                cmd: authselect check
              register: result_authselect_check_cmd
              changed_when: false
              failed_when: false

            - name: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - Ensure the "remember" option from "pam_pwhistory.so"
              is not present in {{ pam_file_path }}
            ansible.builtin.replace:
              dest: '{{ pam_file_path }}'
              regexp: (.*password.*pam_pwhistory.so.*)\bremember\b=?[0-9a-zA-Z]*(.*)
              replace: \1\2
            register: result_pam_option_removal

          - name: Limit Password Reuse - 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:
      - '"pam" in ansible_facts.packages'
      - result_pwhistory_conf_check.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_unix_remember
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Limit Password Reuse - pam_pwhistory.so parameters are configured in PAM files
      block:

      - name: Limit Password Reuse - Define the PAM file to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: Limit Password Reuse - Check if system relies on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Limit Password Reuse - Ensure authselect custom profile is used if authselect
          is present
        block:

        - name: Limit Password Reuse - Check integrity of authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          failed_when: false

        - name: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - 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: Limit Password Reuse - Check if expected PAM module line is present in {{
          pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwhistory.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: Limit Password Reuse - Include or update the PAM module line in {{ pam_file_path
          }}
        block:

        - name: Limit Password Reuse - Check if required PAM module line is present in
            {{ pam_file_path }} with different control
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_pwhistory.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: Limit Password Reuse - Ensure the correct control for the required PAM
            module line in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_pwhistory.so.*)
            replace: \1requisite \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: Limit Password Reuse - Ensure the required PAM module line is included
            in {{ pam_file_path }}
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            line: password    requisite    pam_pwhistory.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: Limit Password Reuse - 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:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: Limit Password Reuse - Check if the required PAM module option is present
          in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwhistory.so\s*.*\sremember\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_remember_option_present

      - name: Limit Password Reuse - Ensure the "remember" PAM option for "pam_pwhistory.so"
          is included in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwhistory.so.*)
          line: \1 remember={{ var_password_pam_unix_remember }}
          state: present
        register: result_pam_remember_add
        when:
        - result_pam_module_remember_option_present.found == 0

      - name: Limit Password Reuse - Ensure the required value for "remember" PAM option
          from "pam_pwhistory.so" in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ 'requisite' | regex_escape() }}\s+pam_pwhistory.so\s+.*)(remember)=[0-9a-zA-Z]+\s*(.*)
          line: \1\2={{ var_password_pam_unix_remember }} \3
        register: result_pam_remember_edit
        when:
        - result_pam_module_remember_option_present.found > 0

      - name: Limit Password Reuse - Ensure authselect changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - (result_pam_remember_add is defined and result_pam_remember_add.changed) or
          (result_pam_remember_edit is defined and result_pam_remember_edit.changed)
      when:
      - '"pam" in ansible_facts.packages'
      - not result_pwhistory_conf_check.stat.exists
      tags:
      - CJIS-5.6.2.1.1
      - NIST-800-171-3.5.8
      - NIST-800-53-IA-5(1)(e)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.5
      - PCI-DSSv4-8.3.7
      - accounts_password_pam_unix_remember
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

#### Rule Lock Accounts After Failed Password Attempts [ref]

This rule configures the system to lock out accounts after a number of incorrect login attempts using `pam_faillock.so`. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. Ensure that the file `/etc/security/faillock.conf` contains the following entry: `deny = <count>` Where count should be less than or equal to 3 and greater than 0. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as `authselect` or `authconfig`, depending on the OS version.
Warning: If the system relies on `authselect` tool to manage PAM settings, the remediation will also use `authselect` tool. However, if any manual modification was made in PAM files, the `authselect` integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the `/etc/security/faillock.conf` file, the pam_faillock parameters should be defined in `faillock.conf` file.

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.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5.5.3, DSS05.04, DSS05.10, DSS06.10, 3.1.8, CCI-000044, CCI-002236, CCI-002237, CCI-002238, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(a), PR.AC-7, FIA_AFL.1, Req-8.1.6, 8.3.4, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, 5.4.2, 5.5.2, SV-230333r743966_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_accounts_passwords_pam_faillock_deny='3'

    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

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then
            sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth        required      pam_faillock.so preauth silent' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth        required      pam_faillock.so authfail' "$pam_file"
            sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account     required      pam_faillock.so' "$pam_file"
        fi
        sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required     \3/g' "$pam_file"
    done

    fi

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

    FAILLOCK_CONF="/etc/security/faillock.conf"
    if [ -f $FAILLOCK_CONF ]; then
        regex="^\s*deny\s*="
        line="deny = $var_accounts_passwords_pam_faillock_deny"
        if ! grep -q $regex $FAILLOCK_CONF; then
            echo $line >> $FAILLOCK_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(deny\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_deny"'|g' $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.*\bdeny\b' "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks 's/(.*auth.*pam_faillock.so.*)\bdeny\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).*deny' "$pam_file"; then
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ deny='"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file"
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ deny='"$var_accounts_passwords_pam_faillock_deny"'/' "$pam_file"
            else
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*silent.*\)\('"deny"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_deny"'\3/' "$pam_file"
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"deny"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_deny"'\3/' "$pam_file"
            fi
        done
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Check if system relies on authselect
        tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Remediation where authselect
        tool is present
      block:

      - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect
          current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

      - name: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Remediation where authselect
        tool is not present
      block:

      - name: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - not result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_passwords_pam_faillock_deny # promote to variable
      set_fact:
        var_accounts_passwords_pam_faillock_deny: !!str 3
      tags:
        - always

    - name: Lock Accounts After Failed Password Attempts - Check the presence of /etc/security/faillock.conf
        file
      ansible.builtin.stat:
        path: /etc/security/faillock.conf
      register: result_faillock_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so
        deny parameter in /etc/security/faillock.conf
      ansible.builtin.lineinfile:
        path: /etc/security/faillock.conf
        regexp: ^\s*deny\s*=
        line: deny = {{ var_accounts_passwords_pam_faillock_deny }}
        state: present
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so
        deny parameter not in PAM files
      block:

      - name: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Lock Accounts After Failed Password Attempts - Define the PAM file to
            be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/system-auth

        - name: Lock Accounts After Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Lock Accounts After Failed Password Attempts - Ensure authselect custom
            profile is used if authselect is present
          block:

          - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect
              current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

          - name: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - Ensure the "deny" option
            from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bdeny\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Lock Accounts After Failed Password Attempts - Define the PAM file to
            be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/password-auth

        - name: Lock Accounts After Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Lock Accounts After Failed Password Attempts - Ensure authselect custom
            profile is used if authselect is present
          block:

          - name: Lock Accounts After Failed Password Attempts - Check integrity of authselect
              current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

          - name: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - 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: Lock Accounts After Failed Password Attempts - Ensure the "deny" option
            from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bdeny\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Lock Accounts After Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Lock Accounts After Failed Password Attempts - Ensure the pam_faillock.so
        deny parameter in PAM files
      block:

      - name: Lock Accounts After Failed Password Attempts - Check if pam_faillock.so
          deny parameter is already enabled in pam files
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail).*deny
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_deny_parameter_is_present

      - name: Lock Accounts After Failed Password Attempts - Ensure the inclusion of pam_faillock.so
          preauth deny 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 deny={{ var_accounts_passwords_pam_faillock_deny }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_deny_parameter_is_present.found == 0

      - name: Lock Accounts After Failed Password Attempts - Ensure the inclusion of pam_faillock.so
          authfail deny parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
          line: \1required\3 deny={{ var_accounts_passwords_pam_faillock_deny }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_deny_parameter_is_present.found == 0

      - name: Lock Accounts After Failed Password Attempts - Ensure the desired value
          for pam_faillock.so preauth deny parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(deny)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_deny }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_deny_parameter_is_present.found > 0

      - name: Lock Accounts After Failed Password Attempts - Ensure the desired value
          for pam_faillock.so authfail deny parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(deny)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_deny }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_deny_parameter_is_present.found > 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020011
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.6
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_deny
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Configure the root Account for Failed Password Attempts [ref]

This rule configures the system to lock out the `root` account after a number of incorrect login attempts using `pam_faillock.so`. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as `authselect` or `authconfig`, depending on the OS version.
Warning: If the system relies on `authselect` tool to manage PAM settings, the remediation will also use `authselect` tool. However, if any manual modification was made in PAM files, the `authselect` integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the `/etc/security/faillock.conf` file, the pam_faillock parameters should be defined in `faillock.conf` file.

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.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_deny_root

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, CCI-002238, CCI-000044, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(b), IA-5(c), PR.AC-7, FMT_MOF_EXT.1, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, SV-230345r743984_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    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

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then
            sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth        required      pam_faillock.so preauth silent' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth        required      pam_faillock.so authfail' "$pam_file"
            sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account     required      pam_faillock.so' "$pam_file"
        fi
        sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required     \3/g' "$pam_file"
    done

    fi

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

    FAILLOCK_CONF="/etc/security/faillock.conf"
    if [ -f $FAILLOCK_CONF ]; then
        regex="^\s*even_deny_root"
        line="even_deny_root"
        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.*\beven_deny_root\b' "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks 's/(.*auth.*pam_faillock.so.*)\beven_deny_root\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).*even_deny_root' "$pam_file"; then
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ even_deny_root/' "$pam_file"
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ even_deny_root/' "$pam_file"
            fi
        done
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Check if system
        relies on authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Remediation where
        authselect tool is present
      block:

      - name: Configure the root Account for Failed Password Attempts - Check integrity
          of authselect current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

      - name: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Remediation where
        authselect tool is not present
      block:

      - name: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - not result_authselect_present.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Check the presence
        of /etc/security/faillock.conf file
      ansible.builtin.stat:
        path: /etc/security/faillock.conf
      register: result_faillock_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so
        even_deny_root parameter in /etc/security/faillock.conf
      ansible.builtin.lineinfile:
        path: /etc/security/faillock.conf
        regexp: ^\s*even_deny_root
        line: even_deny_root
        state: present
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so
        even_deny_root parameter not in PAM files
      block:

      - name: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - Check the proper
          remediation for the system
        block:

        - name: Configure the root Account for Failed Password Attempts - Define the PAM
            file to be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/system-auth

        - name: Configure the root Account for Failed Password Attempts - Check if system
            relies on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Configure the root Account for Failed Password Attempts - Ensure authselect
            custom profile is used if authselect is present
          block:

          - name: Configure the root Account for Failed Password Attempts - Check integrity
              of authselect current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

          - name: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - Ensure the "even_deny_root"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\beven_deny_root\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - Check the proper
          remediation for the system
        block:

        - name: Configure the root Account for Failed Password Attempts - Define the PAM
            file to be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/password-auth

        - name: Configure the root Account for Failed Password Attempts - Check if system
            relies on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Configure the root Account for Failed Password Attempts - Ensure authselect
            custom profile is used if authselect is present
          block:

          - name: Configure the root Account for Failed Password Attempts - Check integrity
              of authselect current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

          - name: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - 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: Configure the root Account for Failed Password Attempts - Ensure the "even_deny_root"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\beven_deny_root\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Configure the root Account for Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Configure the root Account for Failed Password Attempts - Ensure the pam_faillock.so
        even_deny_root parameter in PAM files
      block:

      - name: Configure the root Account for Failed Password Attempts - Check if pam_faillock.so
          even_deny_root parameter is already enabled in pam files
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail).*even_deny_root
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_even_deny_root_parameter_is_present

      - name: Configure the root Account for Failed Password Attempts - Ensure the inclusion
          of pam_faillock.so preauth even_deny_root 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 even_deny_root
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_even_deny_root_parameter_is_present.found == 0

      - name: Configure the root Account for Failed Password Attempts - Ensure the inclusion
          of pam_faillock.so authfail even_deny_root parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
          line: \1required\3 even_deny_root
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_even_deny_root_parameter_is_present.found == 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020023
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(c)
      - accounts_passwords_pam_faillock_deny_root
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Set Interval For Counting Failed Password Attempts [ref]

Utilizing `pam_faillock.so`, the `fail_interval` directive configures the system to lock out an account after a number of incorrect login attempts within a specified time period. Ensure that the file `/etc/security/faillock.conf` contains the following entry: `fail_interval = <interval-in-seconds>` where `interval-in-seconds` is `900` or greater. In order to avoid errors when manually editing these files, it is recommended to use the appropriate tools, such as `authselect` or `authconfig`, depending on the OS version.
Warning: If the system relies on `authselect` tool to manage PAM settings, the remediation will also use `authselect` tool. However, if any manual modification was made in PAM files, the `authselect` integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the `/etc/security/faillock.conf` file, the pam_faillock parameters should be defined in `faillock.conf` file.

Rationale: | By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_interval

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, DSS05.04, DSS05.10, DSS06.10, CCI-000044, CCI-002236, CCI-002237, CCI-002238, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(a), PR.AC-7, FIA_AFL.1, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, SV-230334r627750_rule, SV-230335r743969_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_accounts_passwords_pam_faillock_fail_interval='900'

    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

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then
            sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth        required      pam_faillock.so preauth silent' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth        required      pam_faillock.so authfail' "$pam_file"
            sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account     required      pam_faillock.so' "$pam_file"
        fi
        sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required     \3/g' "$pam_file"
    done

    fi

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

    FAILLOCK_CONF="/etc/security/faillock.conf"
    if [ -f $FAILLOCK_CONF ]; then
        regex="^\s*fail_interval\s*="
        line="fail_interval = $var_accounts_passwords_pam_faillock_fail_interval"
        if ! grep -q $regex $FAILLOCK_CONF; then
            echo $line >> $FAILLOCK_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(fail_interval\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_fail_interval"'|g' $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.*\bfail_interval\b' "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks 's/(.*auth.*pam_faillock.so.*)\bfail_interval\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).*fail_interval' "$pam_file"; then
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ fail_interval='"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file"
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ fail_interval='"$var_accounts_passwords_pam_faillock_fail_interval"'/' "$pam_file"
            else
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*silent.*\)\('"fail_interval"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_fail_interval"'\3/' "$pam_file"
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"fail_interval"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_fail_interval"'\3/' "$pam_file"
            fi
        done
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Check if system relies
        on authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Remediation where authselect
        tool is present
      block:

      - name: Set Interval For Counting Failed Password Attempts - Check integrity of
          authselect current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

      - name: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Remediation where authselect
        tool is not present
      block:

      - name: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - not result_authselect_present.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_passwords_pam_faillock_fail_interval # promote to variable
      set_fact:
        var_accounts_passwords_pam_faillock_fail_interval: !!str 900
      tags:
        - always

    - name: Set Interval For Counting Failed Password Attempts - Check the presence of
        /etc/security/faillock.conf file
      ansible.builtin.stat:
        path: /etc/security/faillock.conf
      register: result_faillock_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so
        fail_interval parameter in /etc/security/faillock.conf
      ansible.builtin.lineinfile:
        path: /etc/security/faillock.conf
        regexp: ^\s*fail_interval\s*=
        line: fail_interval = {{ var_accounts_passwords_pam_faillock_fail_interval }}
        state: present
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so
        fail_interval parameter not in PAM files
      block:

      - name: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Set Interval For Counting Failed Password Attempts - Define the PAM file
            to be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/system-auth

        - name: Set Interval For Counting Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
            custom profile is used if authselect is present
          block:

          - name: Set Interval For Counting Failed Password Attempts - Check integrity
              of authselect current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

          - name: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - Ensure the "fail_interval"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bfail_interval\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Set Interval For Counting Failed Password Attempts - Define the PAM file
            to be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/password-auth

        - name: Set Interval For Counting Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Set Interval For Counting Failed Password Attempts - Ensure authselect
            custom profile is used if authselect is present
          block:

          - name: Set Interval For Counting Failed Password Attempts - Check integrity
              of authselect current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

          - name: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - 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: Set Interval For Counting Failed Password Attempts - Ensure the "fail_interval"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bfail_interval\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Set Interval For Counting Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Interval For Counting Failed Password Attempts - Ensure the pam_faillock.so
        fail_interval parameter in PAM files
      block:

      - name: Set Interval For Counting Failed Password Attempts - Check if pam_faillock.so
          fail_interval parameter is already enabled in pam files
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail).*fail_interval
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_fail_interval_parameter_is_present

      - name: Set Interval For Counting Failed Password Attempts - Ensure the inclusion
          of pam_faillock.so preauth fail_interval 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 fail_interval={{ var_accounts_passwords_pam_faillock_fail_interval
            }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_fail_interval_parameter_is_present.found == 0

      - name: Set Interval For Counting Failed Password Attempts - Ensure the inclusion
          of pam_faillock.so authfail fail_interval parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
          line: \1required\3 fail_interval={{ var_accounts_passwords_pam_faillock_fail_interval
            }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_fail_interval_parameter_is_present.found == 0

      - name: Set Interval For Counting Failed Password Attempts - Ensure the desired
          value for pam_faillock.so preauth fail_interval parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(fail_interval)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_fail_interval }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_fail_interval_parameter_is_present.found > 0

      - name: Set Interval For Counting Failed Password Attempts - Ensure the desired
          value for pam_faillock.so authfail fail_interval parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(fail_interval)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_fail_interval }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_fail_interval_parameter_is_present.found > 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_faillock_conf_check.stat.exists
      tags:
      - DISA-STIG-RHEL-08-020012
      - DISA-STIG-RHEL-08-020013
      - NIST-800-53-AC-7(a)
      - NIST-800-53-CM-6(a)
      - accounts_passwords_pam_faillock_interval
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Set Lockout Time for Failed Password Attempts [ref]

This rule configures the system to lock out accounts during a specified time period after a number of incorrect login attempts using `pam_faillock.so`. Ensure that the file `/etc/security/faillock.conf` contains the following entry: `unlock_time=<interval-in-seconds>` where `interval-in-seconds` is `900` or greater. pam_faillock.so module requires multiple entries in pam files. These entries must be carefully defined to work as expected. In order to avoid any errors when manually editing these files, it is recommended to use the appropriate tools, such as `authselect` or `authconfig`, depending on the OS version. If `unlock_time` is set to `0`, manual intervention by an administrator is required to unlock a user. This should be done using the `faillock` tool.
Warning: If the system supports the new `/etc/security/faillock.conf` file but the pam_faillock.so parameters are defined directly in `/etc/pam.d/system-auth` and `/etc/pam.d/password-auth`, the remediation will migrate the `unlock_time` parameter to `/etc/security/faillock.conf` to ensure compatibility with `authselect` tool. The parameters `deny` and `fail_interval`, if used, also have to be migrated by their respective remediation.
Warning: If the system relies on `authselect` tool to manage PAM settings, the remediation will also use `authselect` tool. However, if any manual modification was made in PAM files, the `authselect` integrity check will fail and the remediation will be aborted in order to preserve intentional changes. In this case, an informative message will be shown in the remediation report. If the system supports the `/etc/security/faillock.conf` file, the pam_faillock parameters should be defined in `faillock.conf` file.

Rationale: | By limiting the number of failed logon attempts the risk of unauthorized system access via user password guessing, otherwise known as brute-forcing, is reduced. Limits are imposed by locking the account.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_passwords_pam_faillock_unlock_time

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5.5.3, DSS05.04, DSS05.10, DSS06.10, 3.1.8, CCI-000044, CCI-002236, CCI-002237, CCI-002238, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, SR 1.1, SR 1.10, SR 1.2, SR 1.5, SR 1.7, SR 1.8, SR 1.9, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.9.2.1, A.9.2.4, A.9.3.1, A.9.4.2, A.9.4.3, CM-6(a), AC-7(b), PR.AC-7, FIA_AFL.1, Req-8.1.7, 8.3.4, SRG-OS-000329-GPOS-00128, SRG-OS-000021-GPOS-00005, 5.5.2, SV-230336r627750_rule, SV-230337r743972_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_accounts_passwords_pam_faillock_unlock_time='900'

    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

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")
    for pam_file in "${AUTH_FILES[@]}"
    do
        if ! grep -qE '^\s*auth\s+required\s+pam_faillock\.so\s+(preauth silent|authfail).*$' "$pam_file" ; then
            sed -i --follow-symlinks '/^auth.*sufficient.*pam_unix\.so.*/i auth        required      pam_faillock.so preauth silent' "$pam_file"
            sed -i --follow-symlinks '/^auth.*required.*pam_deny\.so.*/i auth        required      pam_faillock.so authfail' "$pam_file"
            sed -i --follow-symlinks '/^account.*required.*pam_unix\.so.*/i account     required      pam_faillock.so' "$pam_file"
        fi
        sed -Ei 's/(auth.*)(\[default=die\])(.*pam_faillock\.so)/\1required     \3/g' "$pam_file"
    done

    fi

    AUTH_FILES=("/etc/pam.d/system-auth" "/etc/pam.d/password-auth")

    FAILLOCK_CONF="/etc/security/faillock.conf"
    if [ -f $FAILLOCK_CONF ]; then
        regex="^\s*unlock_time\s*="
        line="unlock_time = $var_accounts_passwords_pam_faillock_unlock_time"
        if ! grep -q $regex $FAILLOCK_CONF; then
            echo $line >> $FAILLOCK_CONF
        else
            sed -i --follow-symlinks 's|^\s*\(unlock_time\s*=\s*\)\(\S\+\)|\1'"$var_accounts_passwords_pam_faillock_unlock_time"'|g' $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.*\bunlock_time\b' "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks 's/(.*auth.*pam_faillock.so.*)\bunlock_time\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).*unlock_time' "$pam_file"; then
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*preauth.*silent.*/ s/$/ unlock_time='"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file"
                sed -i --follow-symlinks '/^auth.*required.*pam_faillock\.so.*authfail.*/ s/$/ unlock_time='"$var_accounts_passwords_pam_faillock_unlock_time"'/' "$pam_file"
            else
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*preauth.*silent.*\)\('"unlock_time"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_unlock_time"'\3/' "$pam_file"
                sed -i --follow-symlinks 's/\(^auth.*required.*pam_faillock\.so.*authfail.*\)\('"unlock_time"'=\)[0-9]\+\(.*\)/\1\2'"$var_accounts_passwords_pam_faillock_unlock_time"'\3/' "$pam_file"
            fi
        done
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Check if system relies on
        authselect tool
      ansible.builtin.stat:
        path: /usr/bin/authselect
      register: result_authselect_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Remediation where authselect
        tool is present
      block:

      - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect
          current profile
        ansible.builtin.command:
          cmd: authselect check
        register: result_authselect_check_cmd
        changed_when: false
        failed_when: false

      - name: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Remediation where authselect
        tool is not present
      block:

      - name: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - not result_authselect_present.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_passwords_pam_faillock_unlock_time # promote to variable
      set_fact:
        var_accounts_passwords_pam_faillock_unlock_time: !!str 900
      tags:
        - always

    - name: Set Lockout Time for Failed Password Attempts - Check the presence of /etc/security/faillock.conf
        file
      ansible.builtin.stat:
        path: /etc/security/faillock.conf
      register: result_faillock_conf_check
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so
        unlock_time parameter in /etc/security/faillock.conf
      ansible.builtin.lineinfile:
        path: /etc/security/faillock.conf
        regexp: ^\s*unlock_time\s*=
        line: unlock_time = {{ var_accounts_passwords_pam_faillock_unlock_time }}
        state: present
      when:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so
        unlock_time parameter not in PAM files
      block:

      - name: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Set Lockout Time for Failed Password Attempts - Define the PAM file to
            be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/system-auth

        - name: Set Lockout Time for Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Set Lockout Time for Failed Password Attempts - Ensure authselect custom
            profile is used if authselect is present
          block:

          - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect
              current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

          - name: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - Ensure the "unlock_time"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bunlock_time\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - Check the proper remediation
          for the system
        block:

        - name: Set Lockout Time for Failed Password Attempts - Define the PAM file to
            be edited as a local fact
          ansible.builtin.set_fact:
            pam_file_path: /etc/pam.d/password-auth

        - name: Set Lockout Time for Failed Password Attempts - Check if system relies
            on authselect tool
          ansible.builtin.stat:
            path: /usr/bin/authselect
          register: result_authselect_present

        - name: Set Lockout Time for Failed Password Attempts - Ensure authselect custom
            profile is used if authselect is present
          block:

          - name: Set Lockout Time for Failed Password Attempts - Check integrity of authselect
              current profile
            ansible.builtin.command:
              cmd: authselect check
            register: result_authselect_check_cmd
            changed_when: false
            failed_when: false

          - name: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - 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: Set Lockout Time for Failed Password Attempts - Ensure the "unlock_time"
            option from "pam_faillock.so" is not present in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: (.*auth.*pam_faillock.so.*)\bunlock_time\b=?[0-9a-zA-Z]*(.*)
            replace: \1\2
          register: result_pam_option_removal

        - name: Set Lockout Time for Failed Password Attempts - 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:
      - '"pam" in ansible_facts.packages'
      - result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Set Lockout Time for Failed Password Attempts - Ensure the pam_faillock.so
        unlock_time parameter in PAM files
      block:

      - name: Set Lockout Time for Failed Password Attempts - Check if pam_faillock.so
          unlock_time parameter is already enabled in pam files
        ansible.builtin.lineinfile:
          path: /etc/pam.d/system-auth
          regexp: .*auth.*pam_faillock\.so (preauth|authfail).*unlock_time
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_faillock_unlock_time_parameter_is_present

      - name: Set Lockout Time for Failed Password Attempts - Ensure the inclusion of
          pam_faillock.so preauth unlock_time 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 unlock_time={{ var_accounts_passwords_pam_faillock_unlock_time
            }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_unlock_time_parameter_is_present.found == 0

      - name: Set Lockout Time for Failed Password Attempts - Ensure the inclusion of
          pam_faillock.so authfail unlock_time parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)
          line: \1required\3 unlock_time={{ var_accounts_passwords_pam_faillock_unlock_time
            }}
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_unlock_time_parameter_is_present.found == 0

      - name: Set Lockout Time for Failed Password Attempts - Ensure the desired value
          for pam_faillock.so preauth unlock_time parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so preauth.*)(unlock_time)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_unlock_time }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_unlock_time_parameter_is_present.found > 0

      - name: Set Lockout Time for Failed Password Attempts - Ensure the desired value
          for pam_faillock.so authfail unlock_time parameter in auth section
        ansible.builtin.lineinfile:
          path: '{{ item }}'
          backrefs: true
          regexp: (^\s*auth\s+)([\w\[].*\b)(\s+pam_faillock.so authfail.*)(unlock_time)=[0-9]+(.*)
          line: \1required\3\4={{ var_accounts_passwords_pam_faillock_unlock_time }}\5
          state: present
        loop:
        - /etc/pam.d/system-auth
        - /etc/pam.d/password-auth
        when:
        - result_pam_faillock_unlock_time_parameter_is_present.found > 0
      when:
      - '"pam" in ansible_facts.packages'
      - not result_faillock_conf_check.stat.exists
      tags:
      - CJIS-5.5.3
      - DISA-STIG-RHEL-08-020014
      - DISA-STIG-RHEL-08-020015
      - NIST-800-171-3.1.8
      - NIST-800-53-AC-7(b)
      - NIST-800-53-CM-6(a)
      - PCI-DSS-Req-8.1.7
      - PCI-DSSv4-8.3.4
      - accounts_passwords_pam_faillock_unlock_time
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Group Set Password Quality Requirements Group contains 1 group and 5 rules

[ref] The default `pam_pwquality` PAM module provides strength checking for passwords. It performs a number of checks, such as making sure passwords are not similar to dictionary words, are of at least a certain length, are not the previous password reversed, and are not simply a change of case from the previous password. It can also require passwords to be in certain character classes. The `pam_pwquality` module is the preferred way of configuring password requirements.
The man pages `pam_pwquality(8)` provide information on the capabilities and configuration of each.

Group Set Password Quality Requirements with pam_pwquality Group contains 5 rules

[ref] The `pam_pwquality` PAM module can be configured to meet requirements for a variety of policies.
For example, to configure `pam_pwquality` to require at least one uppercase character, lowercase character, digit, and other (special) character, make sure that `pam_pwquality` exists in `/etc/pam.d/system-auth`:

    password    requisite     pam_pwquality.so try_first_pass local_users_only retry=3 authtok_type=

If no such line exists, add one as the first line of the password section in `/etc/pam.d/system-auth`. Next, modify the settings in `/etc/security/pwquality.conf` to match the following:

    difok = 4
    minlen = 14
    dcredit = -1
    ucredit = -1
    lcredit = -1
    ocredit = -1
    maxrepeat = 3

The arguments can be modified to ensure compliance with your organization's security policy. Discussion of each parameter follows.

#### Rule Ensure PAM Enforces Password Requirements - Minimum Digit Characters [ref]

The pam_pwquality module's `dcredit` parameter controls requirements for usage of digits in a password. When set to a negative number, any password will be required to contain that many digits. When set to a positive number, pam_pwquality will grant +1 additional length credit for each digit. Modify the `dcredit` setting in `/etc/security/pwquality.conf` to require the use of a digit in passwords.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.
Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring digits makes password guessing attacks more difficult by ensuring a larger search space.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_dcredit

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000194, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, 8.3.6, SRG-OS-000071-GPOS-00039, SV-230359r858775_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_dcredit='-1'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^dcredit")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_dcredit"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^dcredit\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^dcredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-020130
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_dcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_dcredit # promote to variable
      set_fact:
        var_password_pam_dcredit: !!str -1
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Digit Characters - Ensure
        PAM variable dcredit is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*dcredit
        line: dcredit = {{ var_password_pam_dcredit }}
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020130
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_dcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters [ref]

The pam_pwquality module's `lcredit` parameter controls requirements for usage of lowercase letters in a password. When set to a negative number, any password will be required to contain that many lowercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each lowercase character. Modify the `lcredit` setting in `/etc/security/pwquality.conf` to require the use of a lowercase character in passwords.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.
Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possble combinations that need to be tested before the password is compromised. Requiring a minimum number of lowercase characters makes password guessing attacks more difficult by ensuring a larger search space.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_lcredit

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000193, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, 8.3.6, SRG-OS-000070-GPOS-00038, SV-230358r858773_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_lcredit='-1'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^lcredit")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_lcredit"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^lcredit\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^lcredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-020120
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_lcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_lcredit # promote to variable
      set_fact:
        var_password_pam_lcredit: !!str -1
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Lowercase Characters -
        Ensure PAM variable lcredit is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*lcredit
        line: lcredit = {{ var_password_pam_lcredit }}
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020120
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_lcredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure PAM Enforces Password Requirements - Minimum Length [ref]

The pam_pwquality module's `minlen` parameter controls requirements for minimum characters required in a password. Add `minlen=18` after pam_pwquality to set minimum password length requirements.

Rationale:

The shorter the password, the lower the number of possible combinations that need to be tested before the password is compromised.
Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks. Password length is one factor of several that helps to determine strength and how long it takes to crack a password. Use of more characters in a password helps to exponentially increase the time and/or resources required to compromise the password.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_minlen

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, 5.6.2.1.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000205, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, 8.3.6, SRG-OS-000078-GPOS-00046, 5.5.1, SV-230369r858785_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_minlen='18'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^minlen")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_minlen"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^minlen\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^minlen\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-08-020230
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_minlen
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_minlen # promote to variable
      set_fact:
        var_password_pam_minlen: !!str 18
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Length - Ensure PAM variable
        minlen is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*minlen
        line: minlen = {{ var_password_pam_minlen }}
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1.1
      - DISA-STIG-RHEL-08-020230
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - PCI-DSSv4-8.3.6
      - accounts_password_pam_minlen
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure PAM Enforces Password Requirements - Minimum Special Characters [ref]

The pam_pwquality module's `ocredit=` parameter controls requirements for usage of special (or "other") characters in a password. When set to a negative number, any password will be required to contain that many special characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each special character. Modify the `ocredit` setting in `/etc/security/pwquality.conf` to equal -1 to require use of a special character in passwords.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.
Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised. Requiring a minimum number of special characters makes password guessing attacks more difficult by ensuring a larger search space.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_ocredit

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-001619, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, SRG-OS-000266-GPOS-00101, SV-230375r858787_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_ocredit='-1'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^ocredit")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_ocredit"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^ocredit\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^ocredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-020280
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - accounts_password_pam_ocredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_ocredit # promote to variable
      set_fact:
        var_password_pam_ocredit: !!str -1
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Special Characters - Ensure
        PAM variable ocredit is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*ocredit
        line: ocredit = {{ var_password_pam_ocredit }}
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020280
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - accounts_password_pam_ocredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters [ref]

The pam_pwquality module's `ucredit=` parameter controls requirements for usage of uppercase letters in a password. When set to a negative number, any password will be required to contain that many uppercase characters. When set to a positive number, pam_pwquality will grant +1 additional length credit for each uppercase character. Modify the `ucredit` setting in `/etc/security/pwquality.conf` to require the use of an uppercase character in passwords.

Rationale:

Use of a complex password helps to increase the time and resources required to compromise the password. Password complexity, or strength, is a measure of the effectiveness of a password in resisting attempts at guessing and brute-force attacks.
Password complexity is one factor of several that determines how long it takes to crack a password. The more complex the password, the greater the number of possible combinations that need to be tested before the password is compromised.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_ucredit

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, CCI-000192, CCI-000193, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(a), CM-6(a), IA-5(4), PR.AC-1, PR.AC-6, PR.AC-7, FMT_SMF_EXT.1, Req-8.2.3, SRG-OS-000069-GPOS-00037, SRG-OS-000070-GPOS-00038, SV-230357r858771_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_ucredit='-1'

    # Strip any search characters in the key arg so that the key can be replaced without
    # adding any search characters to the config file.
    stripped_key=$(sed 's/[\^=\$,;+]*//g' <<< "^ucredit")

    # shellcheck disable=SC2059
    printf -v formatted_output "%s = %s" "$stripped_key" "$var_password_pam_ucredit"

    # If the key exists, change it. Otherwise, add it to the config_file.
    # We search for the key string followed by a word boundary (matched by \>),
    # so if we search for 'setting', 'setting2' won't match.
    if LC_ALL=C grep -q -m 1 -i -e "^ucredit\\>" "/etc/security/pwquality.conf"; then
        escaped_formatted_output=$(sed -e 's|/|\\/|g' <<< "$formatted_output")
        LC_ALL=C sed -i --follow-symlinks "s/^ucredit\\>.*/$escaped_formatted_output/gi" "/etc/security/pwquality.conf"
    else
        if [[ -s "/etc/security/pwquality.conf" ]] && [[ -n "$(tail -c 1 -- "/etc/security/pwquality.conf" || true)" ]]; then
            LC_ALL=C sed -i --follow-symlinks '$a'\\ "/etc/security/pwquality.conf"
        fi
        cce=""
        printf '# Per %s: Set %s in %s\n' "${cce}" "${formatted_output}" "/etc/security/pwquality.conf" >> "/etc/security/pwquality.conf"
        printf '%s\n' "$formatted_output" >> "/etc/security/pwquality.conf"
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - DISA-STIG-RHEL-08-020110
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - accounts_password_pam_ucredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_password_pam_ucredit # promote to variable
      set_fact:
        var_password_pam_ucredit: !!str -1
      tags:
        - always

    - name: Ensure PAM Enforces Password Requirements - Minimum Uppercase Characters -
        Ensure PAM variable ucredit is set accordingly
      ansible.builtin.lineinfile:
        create: true
        dest: /etc/security/pwquality.conf
        regexp: ^#?\s*ucredit
        line: ucredit = {{ var_password_pam_ucredit }}
      when: '"pam" in ansible_facts.packages'
      tags:
      - DISA-STIG-RHEL-08-020110
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(4)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.3
      - accounts_password_pam_ucredit
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Group Set Password Hashing Algorithm Group contains 1 rule
[ref] The system's default algorithm for storing password hashes in `/etc/shadow` is SHA-512. This can be configured in several locations.

#### Rule Set PAM''s Password Hashing Algorithm [ref]

The PAM system service can be configured to only store encrypted representations of passwords. In "/etc/pam.d/system-auth", the `password` section of the file controls which PAM modules execute during a password change. Set the `pam_unix.so` module in the `password` section to include the argument `sha512`, as shown below:

    password    sufficient    pam_unix.so sha512 other arguments...

This will help ensure when local users change their passwords, hashes for the new passwords will be generated using the SHA-512 algorithm. This is the default.

Rationale:

Passwords need to be protected at all times, and encryption is the standard method for protecting passwords. If passwords are not encrypted, they can be plainly read (i.e., clear text) and easily compromised. Passwords that are encrypted with a weak algorithm are no more protected than if they are kepy in plain text.
This setting ensures user and group account administration utilities are configured to store only encrypted representations of passwords. Additionally, the `crypt_style` configuration option ensures the use of a strong hashing algorithm that makes password cracking attacks more difficult.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_set_password_hashing_algorithm_systemauth

Identifiers and References

References: BP28(R32), 1, 12, 15, 16, 5, 5.6.2.2, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.13.11, CCI-000196, CCI-000803, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0418, 1055, 1402, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(c), IA-5(1)(c), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.1, 8.3.2, SRG-OS-000073-GPOS-00041, SRG-OS-000120-GPOS-00061, 5.5.4, SV-244524r809331_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    if [ -e "/etc/pam.d/system-auth" ] ; then
        PAM_FILE_PATH="/etc/pam.d/system-auth"
        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 "/etc/pam.d/system-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi
        if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*' "$PAM_FILE_PATH"; then
                # Line matching group + control + module was not found. Check group + module.
                if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                    # The control is updated only if one single line matches.
                    sed -i -E --follow-symlinks 's/^(\s*password\s+).*(\bpam_unix.so.*)/\1'"sufficient"' \2/' "$PAM_FILE_PATH"
                else
                    echo 'password    '"sufficient"'    pam_unix.so' >> "$PAM_FILE_PATH"
                fi
            fi
            # Check the option
            if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*\ssha512\b' "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks '/\s*password\s+'"sufficient"'\s+pam_unix.so.*/ s/$/ sha512/' "$PAM_FILE_PATH"
            fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "/etc/pam.d/system-auth was not found" >&2
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-08-010159
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_systemauth

    - name: Set PAM's Password Hashing Algorithm - Check if /etc/pam.d/system-auth file
        is present
      ansible.builtin.stat:
        path: /etc/pam.d/system-auth
      register: result_pam_file_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-08-010159
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_systemauth

    - name: Set PAM's Password Hashing Algorithm - Check the proper remediation for the
        system
      block:

      - name: Set PAM's Password Hashing Algorithm - Define the PAM file to be edited
          as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: Set PAM's Password Hashing Algorithm - Check if system relies on authselect
          tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Set PAM's Password Hashing Algorithm - Ensure authselect custom profile
          is used if authselect is present
        block:

        - name: Set PAM's Password Hashing Algorithm - Check integrity of authselect current
            profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          failed_when: false

        - name: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - 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: Set PAM's Password Hashing Algorithm - Check if expected PAM module line
          is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: Set PAM's Password Hashing Algorithm - Include or update the PAM module
          line in {{ pam_file_path }}
        block:

        - name: Set PAM's Password Hashing Algorithm - Check if required PAM module line
            is present in {{ pam_file_path }} with different control
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_unix.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: Set PAM's Password Hashing Algorithm - Ensure the correct control for
            the required PAM module line in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_unix.so.*)
            replace: \1sufficient \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: Set PAM's Password Hashing Algorithm - Ensure the required PAM module
            line is included in {{ pam_file_path }}
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            line: password    sufficient    pam_unix.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 PAM's Password Hashing Algorithm - 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:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: Set PAM's Password Hashing Algorithm - Check if the required PAM module
          option is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s*.*\ssha512\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_sha512_option_present

      - name: Set PAM's Password Hashing Algorithm - Ensure the "sha512" PAM option for
          "pam_unix.so" is included in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so.*)
          line: \1 sha512
          state: present
        register: result_pam_sha512_add
        when:
        - result_pam_module_sha512_option_present.found == 0

      - name: Set PAM's Password Hashing Algorithm - Ensure authselect changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - |-
          (result_pam_sha512_add is defined and result_pam_sha512_add.changed)
           or (result_pam_sha512_edit is defined and result_pam_sha512_edit.changed)
      when:
      - '"pam" in ansible_facts.packages'
      - result_pam_file_present.stat.exists
      tags:
      - CJIS-5.6.2.2
      - DISA-STIG-RHEL-08-010159
      - NIST-800-171-3.13.11
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(c)
      - NIST-800-53-IA-5(c)
      - PCI-DSS-Req-8.2.1
      - PCI-DSSv4-8.3.2
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - set_password_hashing_algorithm_systemauth

Group Protect Accounts by Restricting Password-Based Login Group contains 2 groups and 4 rules

[ref] Conventionally, Unix shell accounts are accessed by providing a username and password to a login program, which tests these values for correctness using the `/etc/passwd` and `/etc/shadow` files. Password-based login is vulnerable to guessing of weak passwords, and to sniffing and man-in-the-middle attacks against passwords entered over a network or at an insecure console. Therefore, mechanisms for accessing accounts by entering usernames and passwords should be restricted to those which are operationally necessary.

Group Set Password Expiration Parameters Group contains 2 rules

[ref] The file `/etc/login.defs` controls several password-related settings. Programs such as `passwd`, `su`, and `login` consult `/etc/login.defs` to determine behavior with regard to password aging, expiration warnings, and length. See the man page `login.defs(5)` for more information.
Users should be forced to change their passwords, in order to decrease the utility of compromised passwords. However, the need to change passwords often should be balanced against the risk that users will reuse or write down passwords if forced to change them too often. Forcing password changes every 90-360 days, depending on the environment, is recommended. Set the appropriate value as `PASS_MAX_DAYS` and apply it to existing accounts with the `-M` flag.
The `PASS_MIN_DAYS` (`-m`) setting prevents password changes for 7 days after the first change, to discourage password cycling. If you use this setting, train users to contact an administrator for an emergency password change in case a new password becomes compromised. The `PASS_WARN_AGE` (`-W`) setting gives users 7 days of warnings at login time that their passwords are about to expire.
For example, for each existing human user USER, expiration parameters could be adjusted to a 180 day maximum password age, 7 day minimum password age, and 7 day warning period with the following command:

    $ sudo chage -M 180 -m 7 -W 7 USER

#### Rule Set Password Maximum Age [ref]

To specify password maximum age for new accounts, edit the file `/etc/login.defs` and add or correct the following line:

    PASS_MAX_DAYS 90

A value of 180 days is sufficient for many environments. The DoD requirement is 60. The profile requirement is `90`.

Rationale:

Any password, no matter how complex, can eventually be cracked. Therefore, passwords need to be changed periodically. If the operating system does not limit the lifetime of passwords and force users to change their passwords, there is the risk that the operating system passwords could be compromised.
Setting the password maximum age ensures users are required to periodically change their passwords. Requiring shorter password lifetimes increases the risk of users writing down the password in a convenient location subject to physical compromise.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_maximum_age_login_defs

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, 5.6.2.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.6, CCI-000199, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0418, 1055, 1402, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(f), IA-5(1)(d), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, Req-8.2.4, 8.3.9, SRG-OS-000076-GPOS-00044, 5.6.1.1, SV-230366r646878_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q shadow-utils; then

    var_accounts_maximum_age_login_defs='90'

    grep -q ^PASS_MAX_DAYS /etc/login.defs && \
      sed -i "s/PASS_MAX_DAYS.*/PASS_MAX_DAYS     $var_accounts_maximum_age_login_defs/g" /etc/login.defs
    if ! [ $? -eq 0 ]; then
        echo "PASS_MAX_DAYS      $var_accounts_maximum_age_login_defs" >> /etc/login.defs
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1
      - DISA-STIG-RHEL-08-020200
      - NIST-800-171-3.5.6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3.9
      - accounts_maximum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_maximum_age_login_defs # promote to variable
      set_fact:
        var_accounts_maximum_age_login_defs: !!str 90
      tags:
        - always

    - name: Set Password Maximum Age
      lineinfile:
        create: true
        dest: /etc/login.defs
        regexp: ^#?PASS_MAX_DAYS
        line: PASS_MAX_DAYS {{ var_accounts_maximum_age_login_defs }}
      when: '"shadow-utils" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1
      - DISA-STIG-RHEL-08-020200
      - NIST-800-171-3.5.6
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(d)
      - NIST-800-53-IA-5(f)
      - PCI-DSS-Req-8.2.4
      - PCI-DSSv4-8.3.9
      - accounts_maximum_age_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Set Password Minimum Length in login.defs [ref]

To specify password length requirements for new accounts, edit the file `/etc/login.defs` and add or correct the following line:

    PASS_MIN_LEN 18

The DoD requirement is `15`. The FISMA requirement is `12`. The profile requirement is `18`. If a program consults `/etc/login.defs` and also another PAM module (such as `pam_pwquality`) during a password change operation, then the most restrictive must be satisfied. See PAM section for more information about enforcing password quality requirements.

Rationale: | Requiring a minimum password length makes password cracking attacks more difficult by ensuring a larger search space. However, any security benefit from an onerous requirement must be carefully weighed against usability problems, support costs, or counterproductive behavior that may result.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_minlen_login_defs

Identifiers and References

References: BP28(R18), 1, 12, 15, 16, 5, 5.6.2.1, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.03, DSS06.10, 3.5.7, CCI-000205, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.2, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.7, SR 1.8, SR 1.9, SR 2.1, 0421, 0422, 0431, 0974, 1173, 1401, 1504, 1505, 1546, 1557, 1558, 1559, 1560, 1561, A.18.1.4, A.7.1.1, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.2, A.9.4.3, IA-5(f), IA-5(1)(a), CM-6(a), PR.AC-1, PR.AC-6, PR.AC-7, SRG-OS-000078-GPOS-00046, SV-230370r627750_rule

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q shadow-utils; then

    var_accounts_password_minlen_login_defs='18'

    grep -q ^PASS_MIN_LEN /etc/login.defs && \
    sed -i "s/PASS_MIN_LEN.*/PASS_MIN_LEN\t$var_accounts_password_minlen_login_defs/g" /etc/login.defs
    if ! [ $? -eq 0 ]
    then
      echo -e "PASS_MIN_LEN\t$var_accounts_password_minlen_login_defs" >> /etc/login.defs
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - CJIS-5.6.2.1
      - DISA-STIG-RHEL-08-020231
      - NIST-800-171-3.5.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(f)
      - accounts_password_minlen_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy
    - name: XCCDF Value var_accounts_password_minlen_login_defs # promote to variable
      set_fact:
        var_accounts_password_minlen_login_defs: !!str 18
      tags:
        - always

    - name: Set Password Minimum Length in login.defs
      lineinfile:
        dest: /etc/login.defs
        regexp: ^PASS_MIN_LEN *[0-9]*
        state: present
        line: PASS_MIN_LEN        {{ var_accounts_password_minlen_login_defs }}
        create: true
      when: '"shadow-utils" in ansible_facts.packages'
      tags:
      - CJIS-5.6.2.1
      - DISA-STIG-RHEL-08-020231
      - NIST-800-171-3.5.7
      - NIST-800-53-CM-6(a)
      - NIST-800-53-IA-5(1)(a)
      - NIST-800-53-IA-5(f)
      - accounts_password_minlen_login_defs
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

Group Verify Proper Storage and Existence of Password Hashes Group contains 2 rules

[ref] By default, password hashes for local accounts are stored in the second field (colon-separated) in `/etc/shadow`. This file should be readable only by processes running with root credentials, preventing users from casually accessing others' password hashes and attempting to crack them. However, it remains possible to misconfigure the system and store password hashes in world-readable files such as `/etc/passwd`, or to even store passwords themselves in plaintext on the system. Using system-provided tools for password change/creation should allow administrators to avoid such misconfiguration.

#### Rule Set number of Password Hashing Rounds - password-auth [ref]

Configure the number or rounds for the password hashing algorithm. This can be accomplished by using the `rounds` option for the `pam_unix` PAM module.
In file `/etc/pam.d/password-auth` append `rounds=65536` to the `pam_unix.so` entry, as shown below:

    password sufficient pam_unix.so ...existing_options... rounds=65536

The system's default number of rounds is 5000.
Warning: Setting a high number of hashing rounds makes it more difficult to brute force the password, but requires more CPU resources to authenticate users.

Rationale: | Using a higher number of rounds makes password cracking attacks more difficult.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_rounds_password_auth
Identifiers and References | References: BP28(R68), CCI-000196, SRG-OS-000073-GPOS-00041

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_unix_rounds='65536'

    if [ -e "/etc/pam.d/password-auth" ] ; then
        PAM_FILE_PATH="/etc/pam.d/password-auth"
        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 "/etc/pam.d/password-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi
        if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*' "$PAM_FILE_PATH"; then
                # Line matching group + control + module was not found. Check group + module.
                if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                    # The control is updated only if one single line matches.
                    sed -i -E --follow-symlinks 's/^(\s*password\s+).*(\bpam_unix.so.*)/\1'"sufficient"' \2/' "$PAM_FILE_PATH"
                else
                    echo 'password    '"sufficient"'    pam_unix.so' >> "$PAM_FILE_PATH"
                fi
            fi
            # Check the option
            if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*\srounds\b' "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks '/\s*password\s+'"sufficient"'\s+pam_unix.so.*/ s/$/ rounds='"$var_password_pam_unix_rounds"'/' "$PAM_FILE_PATH"
            else
                sed -i -E --follow-symlinks 's/(\s*password\s+'"sufficient"'\s+pam_unix.so\s+.*)('"rounds"'=)[[:alnum:]]+\s*(.*)/\1\2'"$var_password_pam_unix_rounds"' \3/' "$PAM_FILE_PATH"
            fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "/etc/pam.d/password-auth was not found" >&2
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - accounts_password_pam_unix_rounds_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_password_pam_unix_rounds # promote to variable
      set_fact:
        var_password_pam_unix_rounds: !!str 65536
      tags:
        - always

    - name: Set number of Password Hashing Rounds - password-auth - Check if /etc/pam.d/password-auth
        file is present
      ansible.builtin.stat:
        path: /etc/pam.d/password-auth
      register: result_pam_file_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - accounts_password_pam_unix_rounds_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set number of Password Hashing Rounds - password-auth - Check the proper remediation
        for the system
      block:

      - name: Set number of Password Hashing Rounds - password-auth - Define the PAM file
          to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/password-auth

      - name: Set number of Password Hashing Rounds - password-auth - Check if system
          relies on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect
          custom profile is used if authselect is present
        block:

        - name: Set number of Password Hashing Rounds - password-auth - Check integrity
            of authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          failed_when: false

        - name: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - 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: Set number of Password Hashing Rounds - password-auth - Check if expected
          PAM module line is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: Set number of Password Hashing Rounds - password-auth - Include or update
          the PAM module line in {{ pam_file_path }}
        block:

        - name: Set number of Password Hashing Rounds - password-auth - Check if required
            PAM module line is present in {{ pam_file_path }} with different control
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_unix.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: Set number of Password Hashing Rounds - password-auth - Ensure the correct
            control for the required PAM module line in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_unix.so.*)
            replace: \1sufficient \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: Set number of Password Hashing Rounds - password-auth - Ensure the required
            PAM module line is included in {{ pam_file_path }}
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            line: password    sufficient    pam_unix.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 number of Password Hashing Rounds - password-auth - 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:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: Set number of Password Hashing Rounds - password-auth - Check if the required
          PAM module option is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s*.*\srounds\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_rounds_option_present

      - name: Set number of Password Hashing Rounds - password-auth - Ensure the "rounds"
          PAM option for "pam_unix.so" is included in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so.*)
          line: \1 rounds={{ var_password_pam_unix_rounds }}
          state: present
        register: result_pam_rounds_add
        when:
        - result_pam_module_rounds_option_present.found == 0

      - name: Set number of Password Hashing Rounds - password-auth - Ensure the required
          value for "rounds" PAM option from "pam_unix.so" in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s+.*)(rounds)=[0-9a-zA-Z]+\s*(.*)
          line: \1\2={{ var_password_pam_unix_rounds }} \3
        register: result_pam_rounds_edit
        when:
        - result_pam_module_rounds_option_present.found > 0

      - name: Set number of Password Hashing Rounds - password-auth - Ensure authselect
          changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - |-
          (result_pam_rounds_add is defined and result_pam_rounds_add.changed)
           or (result_pam_rounds_edit is defined and result_pam_rounds_edit.changed)
      when:
      - '"pam" in ansible_facts.packages'
      - result_pam_file_present.stat.exists
      tags:
      - accounts_password_pam_unix_rounds_password_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

#### Rule Set number of Password Hashing Rounds - system-auth [ref]

Configure the number or rounds for the password hashing algorithm. This can be accomplished by using the `rounds` option for the `pam_unix` PAM module.
In file `/etc/pam.d/system-auth` append `rounds=65536` to the `pam_unix.so` entry, as shown below:

    password sufficient pam_unix.so ...existing_options... rounds=65536

The system's default number of rounds is 5000.
Warning: Setting a high number of hashing rounds makes it more difficult to brute force the password, but requires more CPU resources to authenticate users.

Rationale: | Using a higher number of rounds makes password cracking attacks more difficult.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_accounts_password_pam_unix_rounds_system_auth
Identifiers and References | References: BP28(R68), CCI-000196, SRG-OS-000073-GPOS-00041

Remediation Shell script ⇲

    # Remediation is applicable only in certain platforms
    if rpm --quiet -q pam; then

    var_password_pam_unix_rounds='65536'

    if [ -e "/etc/pam.d/system-auth" ] ; then
        PAM_FILE_PATH="/etc/pam.d/system-auth"
        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 "/etc/pam.d/system-auth")
            PAM_FILE_PATH="/etc/authselect/$CURRENT_PROFILE/$PAM_FILE_NAME"

            authselect apply-changes -b
        fi
        if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*' "$PAM_FILE_PATH"; then
                # Line matching group + control + module was not found. Check group + module.
                if [ "$(grep -cP '^\s*password\s+.*\s+pam_unix.so\s*' "$PAM_FILE_PATH")" -eq 1 ]; then
                    # The control is updated only if one single line matches.
                    sed -i -E --follow-symlinks 's/^(\s*password\s+).*(\bpam_unix.so.*)/\1'"sufficient"' \2/' "$PAM_FILE_PATH"
                else
                    echo 'password    '"sufficient"'    pam_unix.so' >> "$PAM_FILE_PATH"
                fi
            fi
            # Check the option
            if ! grep -qP '^\s*password\s+'"sufficient"'\s+pam_unix.so\s*.*\srounds\b' "$PAM_FILE_PATH"; then
                sed -i -E --follow-symlinks '/\s*password\s+'"sufficient"'\s+pam_unix.so.*/ s/$/ rounds='"$var_password_pam_unix_rounds"'/' "$PAM_FILE_PATH"
            else
                sed -i -E --follow-symlinks 's/(\s*password\s+'"sufficient"'\s+pam_unix.so\s+.*)('"rounds"'=)[[:alnum:]]+\s*(.*)/\1\2'"$var_password_pam_unix_rounds"' \3/' "$PAM_FILE_PATH"
            fi
        if [ -f /usr/bin/authselect ]; then

            authselect apply-changes -b
        fi
    else
        echo "/etc/pam.d/system-auth was not found" >&2
    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: Gather the package facts
      package_facts:
        manager: auto
      tags:
      - accounts_password_pam_unix_rounds_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
    - name: XCCDF Value var_password_pam_unix_rounds # promote to variable
      set_fact:
        var_password_pam_unix_rounds: !!str 65536
      tags:
        - always

    - name: Set number of Password Hashing Rounds - system-auth - Check if /etc/pam.d/system-auth
        file is present
      ansible.builtin.stat:
        path: /etc/pam.d/system-auth
      register: result_pam_file_present
      when: '"pam" in ansible_facts.packages'
      tags:
      - accounts_password_pam_unix_rounds_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Set number of Password Hashing Rounds - system-auth - Check the proper remediation
        for the system
      block:

      - name: Set number of Password Hashing Rounds - system-auth - Define the PAM file
          to be edited as a local fact
        ansible.builtin.set_fact:
          pam_file_path: /etc/pam.d/system-auth

      - name: Set number of Password Hashing Rounds - system-auth - Check if system relies
          on authselect tool
        ansible.builtin.stat:
          path: /usr/bin/authselect
        register: result_authselect_present

      - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect
          custom profile is used if authselect is present
        block:

        - name: Set number of Password Hashing Rounds - system-auth - Check integrity
            of authselect current profile
          ansible.builtin.command:
            cmd: authselect check
          register: result_authselect_check_cmd
          changed_when: false
          failed_when: false

        - name: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - 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: Set number of Password Hashing Rounds - system-auth - Check if expected
          PAM module line is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s*.*
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_line_present

      - name: Set number of Password Hashing Rounds - system-auth - Include or update
          the PAM module line in {{ pam_file_path }}
        block:

        - name: Set number of Password Hashing Rounds - system-auth - Check if required
            PAM module line is present in {{ pam_file_path }} with different control
          ansible.builtin.lineinfile:
            path: '{{ pam_file_path }}'
            regexp: ^\s*password\s+.*\s+pam_unix.so\s*
            state: absent
          check_mode: true
          changed_when: false
          register: result_pam_line_other_control_present

        - name: Set number of Password Hashing Rounds - system-auth - Ensure the correct
            control for the required PAM module line in {{ pam_file_path }}
          ansible.builtin.replace:
            dest: '{{ pam_file_path }}'
            regexp: ^(\s*password\s+).*(\bpam_unix.so.*)
            replace: \1sufficient \2
          register: result_pam_module_edit
          when:
          - result_pam_line_other_control_present.found == 1

        - name: Set number of Password Hashing Rounds - system-auth - Ensure the required
            PAM module line is included in {{ pam_file_path }}
          ansible.builtin.lineinfile:
            dest: '{{ pam_file_path }}'
            line: password    sufficient    pam_unix.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 number of Password Hashing Rounds - system-auth - 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:
        - result_pam_line_present.found is defined
        - result_pam_line_present.found == 0

      - name: Set number of Password Hashing Rounds - system-auth - Check if the required
          PAM module option is present in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          regexp: ^\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s*.*\srounds\b
          state: absent
        check_mode: true
        changed_when: false
        register: result_pam_module_rounds_option_present

      - name: Set number of Password Hashing Rounds - system-auth - Ensure the "rounds"
          PAM option for "pam_unix.so" is included in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so.*)
          line: \1 rounds={{ var_password_pam_unix_rounds }}
          state: present
        register: result_pam_rounds_add
        when:
        - result_pam_module_rounds_option_present.found == 0

      - name: Set number of Password Hashing Rounds - system-auth - Ensure the required
          value for "rounds" PAM option from "pam_unix.so" in {{ pam_file_path }}
        ansible.builtin.lineinfile:
          path: '{{ pam_file_path }}'
          backrefs: true
          regexp: ^(\s*password\s+{{ 'sufficient' | regex_escape() }}\s+pam_unix.so\s+.*)(rounds)=[0-9a-zA-Z]+\s*(.*)
          line: \1\2={{ var_password_pam_unix_rounds }} \3
        register: result_pam_rounds_edit
        when:
        - result_pam_module_rounds_option_present.found > 0

      - name: Set number of Password Hashing Rounds - system-auth - Ensure authselect
          changes are applied
        ansible.builtin.command:
          cmd: authselect apply-changes -b
        when:
        - result_authselect_present.stat.exists
        - |-
          (result_pam_rounds_add is defined and result_pam_rounds_add.changed)
           or (result_pam_rounds_edit is defined and result_pam_rounds_edit.changed)
      when:
      - '"pam" in ansible_facts.packages'
      - result_pam_file_present.stat.exists
      tags:
      - accounts_password_pam_unix_rounds_system_auth
      - configure_strategy
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

#### Rule Enable authselect [ref]

Configure user authentication setup to use the `authselect` tool. If authselect profile is selected, the rule will enable the minimal profile.
Warning: If the `sudo authselect select` command returns an error informing that the chosen profile cannot be selected, it is probably because PAM files have already been modified by the administrator. If this is the case, in order to not overwrite the desired changes made by the administrator, the current PAM settings should be investigated before forcing the selection of the chosen authselect profile.

Rationale: | Authselect is a successor to authconfig. It is a tool to select system authentication and identity sources from a list of supported profiles instead of letting the administrator manually build the PAM stack. That way, it avoids potential breakage of configuration, as it ships several tested profiles that are well tested and supported to solve different use-cases.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_enable_authselect
Identifiers and References | References: BP28(R31), CCI-000213, 164.308(a)(1)(ii)(B), 164.308(a)(7)(i), 164.308(a)(7)(ii)(A), 164.310(a)(1), 164.310(a)(2)(i), 164.310(a)(2)(ii), 164.310(a)(2)(iii), 164.310(b), 164.310(c), 164.310(d)(1), 164.310(d)(2)(iii), AC-3, FIA_UAU.1, FIA_AFL.1, SRG-OS-000480-GPOS-00227, 1.2.3

Remediation Shell script ⇲

    var_authselect_profile='minimal'

    authselect select "$var_authselect_profile"

    if test "$?" -ne 0; then
        if rpm --quiet --verify pam; then
            authselect select --force "$var_authselect_profile"
        else
    	echo "Files in the 'pam' package have been altered, so the authselect configuration won't be forced" >&2
        fi
    fi

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | configure

    - name: XCCDF Value var_authselect_profile # promote to variable
      set_fact:
        var_authselect_profile: !!str minimal
      tags:
        - always

    - name: Enable authselect - Select authselect profile
      ansible.builtin.command:
        cmd: authselect select "{{ var_authselect_profile }}"
      register: result_authselect_select
      failed_when: false
      tags:
      - NIST-800-53-AC-3
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Verify if PAM has been altered
      ansible.builtin.command:
        cmd: rpm -qV pam
      register: result_altered_authselect
      failed_when: false
      when: result_authselect_select.rc != 0
      tags:
      - NIST-800-53-AC-3
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Informative message based on the authselect integrity
        check
      ansible.builtin.assert:
        that:
        - result_altered_authselect is skipped or result_altered_authselect.rc == 0
        fail_msg:
        - Files in the 'pam' package have been altered, so the authselect configuration
          won't be forced.
      tags:
      - NIST-800-53-AC-3
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

    - name: Enable authselect - Force authselect profile select
      ansible.builtin.command:
        cmd: authselect select --force "{{ var_authselect_profile }}"
      when:
      - result_authselect_select.rc != 0
      - result_altered_authselect is skipped or result_altered_authselect.rc == 0
      tags:
      - NIST-800-53-AC-3
      - configure_strategy
      - enable_authselect
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed

Group File Permissions and Masks Group contains 1 group and 7 rules

[ref] Traditional Unix security relies heavily on file and directory permissions to prevent unauthorized users from reading or modifying files to which they should not have access.
Several of the commands in this section search filesystems for files or directories with certain characteristics, and are intended to be run on every local partition on a given system. When the variable PART appears in one of the commands below, it means that the command is intended to be run repeatedly, with the name of each local partition substituted for PART in turn.
The following command prints a list of all xfs partitions on the local system, which is the default filesystem for Red Hat Enterprise Linux 8 installations:

    $ mount -t xfs | awk '{print $3}'

For any systems that use a different local filesystem type, modify this command as appropriate.

Group Verify Permissions on Important Files and Directories Group contains 7 rules
[ref] Permissions for many files on a system must be set restrictively to ensure sensitive information is properly protected. This section discusses important permission restrictions which can be verified to ensure that no harmful discrepancies have arisen.

#### Rule Ensure All World-Writable Directories Are Owned by root User [ref]

All directories in local partitions which are world-writable should be owned by root. If any world-writable directories are not owned by root, this should be investigated. Following this, the files should be deleted or assigned to root user.
Rationale: | Allowing a user account to own a world-writable directory is undesirable because it allows the owner of that directory to remove or replace any files that may be placed in the directory by other users.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_dir_perms_world_writable_root_owned
Identifiers and References | References: BP28(R40), CCI-000366, SRG-OS-000480-GPOS-00227, SRG-OS-000138-GPOS-00069, SV-230318r743960_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | restrict

    # At least under containerized env /proc can have files w/o possilibity to
    # modify even as root. And touching /proc is not good idea anyways.
    find / -path /proc -prune -o \
        -not -fstype afs -not -fstype ceph -not -fstype cifs -not -fstype smb3 -not -fstype smbfs \
        -not -fstype sshfs -not -fstype ncpfs -not -fstype ncp -not -fstype nfs -not -fstype nfs4 \
        -not -fstype gfs -not -fstype gfs2 -not -fstype glusterfs -not -fstype gpfs \
        -not -fstype pvfs2 -not -fstype ocfs2 -not -fstype lustre -not -fstype davfs \
        -not -fstype fuse.sshfs -type d -perm -0002 -uid +0 -exec chown root {} \;

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | medium
Reboot: | false
Strategy: | restrict

    - name: Ensure All World-Writable Directories Are Owned by root User - Define Excluded
        (Non-Local) File Systems and Paths
      ansible.builtin.set_fact:
        excluded_fstypes:
        - afs
        - ceph
        - cifs
        - smb3
        - smbfs
        - sshfs
        - ncpfs
        - ncp
        - nfs
        - nfs4
        - gfs
        - gfs2
        - glusterfs
        - gpfs
        - pvfs2
        - ocfs2
        - lustre
        - davfs
        - fuse.sshfs
        excluded_paths:
        - dev
        - proc
        - run
        - sys
        search_paths: []
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure All World-Writable Directories Are Owned by root User - Find Relevant
        Root Directories Ignoring Pre-Defined Excluded Paths
      ansible.builtin.find:
        paths: /
        file_type: directory
        excludes: '{{ excluded_paths }}'
        hidden: true
        recurse: false
      register: result_relevant_root_dirs
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure All World-Writable Directories Are Owned by root User - Include Relevant
        Root Directories in a List of Paths to be Searched
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.path]) }}'
      loop: '{{ result_relevant_root_dirs.files }}'
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure All World-Writable Directories Are Owned by root User - Increment Search
        Paths List with Local Partitions Mount Points
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.mount]) }}'
      loop: '{{ ansible_mounts }}'
      when:
      - item.fstype not in excluded_fstypes
      - item.mount != '/'
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure All World-Writable Directories Are Owned by root User - Increment Search
        Paths List with Local NFS File System Targets
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.device.split('':'')[1]]) }}'
      loop: '{{ ansible_mounts }}'
      when: item.device is search("localhost:")
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure All World-Writable Directories Are Owned by root User - Define Rule
        Specific Facts
      ansible.builtin.set_fact:
        world_writable_dirs: []
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure All World-Writable Directories Are Owned by root User - Find All Uncompliant
        Directories in Local File Systems
      ansible.builtin.command:
        cmd: find {{ item }} -xdev -type d -perm -0002 -uid +0
      loop: '{{ search_paths }}'
      changed_when: false
      register: result_found_dirs
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure All World-Writable Directories Are Owned by root User - Create List
        of World Writable Directories Not Owned by root
      ansible.builtin.set_fact:
        world_writable_dirs: '{{ world_writable_dirs | union(item.stdout_lines) | list
          }}'
      loop: '{{ result_found_dirs.results }}'
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Ensure All World-Writable Directories Are Owned by root User - Ensure root
        Ownership on Local World Writable Directories
      ansible.builtin.file:
        path: '{{ item }}'
        owner: root
      loop: '{{ world_writable_dirs }}'
      tags:
      - DISA-STIG-RHEL-08-010700
      - dir_perms_world_writable_root_owned
      - low_complexity
      - medium_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Verify that All World-Writable Directories Have Sticky Bits Set [ref]

When the so-called 'sticky bit' is set on a directory, only the owner of a given file may remove that file from the directory. Without the sticky bit, any user with write access to a directory may remove any file in the directory. Setting the sticky bit prevents users from removing each other's files. In cases where there is no reason for a directory to be world-writable, a better solution is to remove that permission rather than to set the sticky bit. However, if a directory is used by a particular application, consult that application's documentation instead of blindly changing modes.
To set the sticky bit on a world-writable directory DIR, run the following command:

    $ sudo chmod +t DIR

Rationale:

Failing to set the sticky bit on public directories allows unauthorized users to delete files in the directory structure.
The only authorized public directories are those temporary directories supplied with the system, or those designed to be temporary file repositories. The setting is normally reserved for directories used by the system, by users for temporary file storage (such as `/tmp`), and for directories requiring global read/write access.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_dir_perms_world_writable_sticky_bits

Identifiers and References

References: BP28(R40), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, CCI-001090, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, 2.2.6, SRG-OS-000138-GPOS-00069, 6.1.2, SV-230243r792857_rule

Remediation Shell script ⇲

    df --local -P | awk '{if (NR!=1) print $6}' \
    | xargs -I '$6' find '$6' -xdev -type d \
    \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null \
    -exec chmod a+t {} +

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | restrict

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Define Excluded
        (Non-Local) File Systems and Paths
      ansible.builtin.set_fact:
        excluded_fstypes:
        - afs
        - ceph
        - cifs
        - smb3
        - smbfs
        - sshfs
        - ncpfs
        - ncp
        - nfs
        - nfs4
        - gfs
        - gfs2
        - glusterfs
        - gpfs
        - pvfs2
        - ocfs2
        - lustre
        - davfs
        - fuse.sshfs
        excluded_paths:
        - dev
        - proc
        - run
        - sys
        search_paths: []
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Find Relevant
        Root Directories Ignoring Pre-Defined Excluded Paths
      ansible.builtin.find:
        paths: /
        file_type: directory
        excludes: '{{ excluded_paths }}'
        hidden: true
        recurse: false
      register: result_relevant_root_dirs
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Include
        Relevant Root Directories in a List of Paths to be Searched
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.path]) }}'
      loop: '{{ result_relevant_root_dirs.files }}'
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Increment
        Search Paths List with Local Partitions Mount Points
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.mount]) }}'
      loop: '{{ ansible_mounts }}'
      when:
      - item.fstype not in excluded_fstypes
      - item.mount != '/'
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Increment
        Search Paths List with Local NFS File System Targets
      ansible.builtin.set_fact:
        search_paths: '{{ search_paths | union([item.device.split('':'')[1]]) }}'
      loop: '{{ ansible_mounts }}'
      when: item.device is search("localhost:")
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Define Rule
        Specific Facts
      ansible.builtin.set_fact:
        world_writable_dirs: []
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Find All
        Uncompliant Directories in Local File Systems
      ansible.builtin.command:
        cmd: find {{ item }} -xdev -type d ( -perm -0002 -a ! -perm -1000 )
      loop: '{{ search_paths }}'
      changed_when: false
      register: result_found_dirs
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Create List
        of World Writable Directories Without Sticky Bit
      ansible.builtin.set_fact:
        world_writable_dirs: '{{ world_writable_dirs | union(item.stdout_lines) | list
          }}'
      loop: '{{ result_found_dirs.results }}'
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

    - name: Verify that All World-Writable Directories Have Sticky Bits Set - Ensure Sticky
        Bit is Set on Local World Writable Directories
      ansible.builtin.file:
        path: '{{ item }}'
        mode: a+t
      loop: '{{ world_writable_dirs }}'
      tags:
      - DISA-STIG-RHEL-08-010190
      - NIST-800-53-AC-6(1)
      - NIST-800-53-CM-6(a)
      - PCI-DSSv4-2.2.6
      - dir_perms_world_writable_sticky_bits
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - restrict_strategy

#### Rule Ensure All SGID Executables Are Authorized [ref]

The SGID (set group id) bit should be set only on files that were installed via authorized means. A straightforward means of identifying unauthorized SGID files is determine if any were not installed as part of an RPM package, which is cryptographically verified. Investigate the origin of any unpackaged SGID files. This configuration check considers authorized SGID files which were installed via RPM. It is assumed that when an individual has sudo access to install an RPM and all packages are signed with an organizationally-recognized GPG key, the software should be considered an approved package on the system. Any SGID file not deployed through an RPM will be flagged for further review.

Rationale: | Executable files with the SGID permission run with the privileges of the owner of the file. SGID files of uncertain provenance could allow for unprivileged users to elevate privileges. The presence of these files should be strictly controlled on the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_unauthorized_sgid

Identifiers and References

References: BP28(R37), BP28(R38), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, 6.1.15

#### Rule Ensure All SUID Executables Are Authorized [ref]

The SUID (set user id) bit should be set only on files that were installed via authorized means. A straightforward means of identifying unauthorized SUID files is determine if any were not installed as part of an RPM package, which is cryptographically verified. Investigate the origin of any unpackaged SUID files. This configuration check considers authorized SUID files which were installed via RPM. It is assumed that when an individual has sudo access to install an RPM and all packages are signed with an organizationally-recognized GPG key, the software should be considered an approved package on the system. Any SUID file not deployed through an RPM will be flagged for further review.

Rationale: | Executable files with the SUID permission run with the privileges of the owner of the file. SUID files of uncertain provenance could allow for unprivileged users to elevate privileges. The presence of these files should be strictly controlled on the system.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_unauthorized_suid

Identifiers and References

References: BP28(R37), BP28(R38), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, 6.1.14

#### Rule Ensure No World-Writable Files Exist [ref]

It is generally a good idea to remove global (other) write access to a file when it is discovered. However, check with documentation for specific applications before making changes. Also, monitor for recurring world-writable files, as these may be symptoms of a misconfigured application or user account. Finally, this applies to real files and not virtual files that are a part of pseudo file systems such as `sysfs` or `procfs`.

Rationale: | Data in world-writable files can be modified by any user on the system. In almost all circumstances, files can be configured using a combination of user and group permissions to support whatever legitimate access is needed without the risk caused by world-writable files.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_unauthorized_world_writable

Identifiers and References

References: BP28(R40), 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.04, DSS05.07, DSS06.02, 4.3.3.7.3, SR 2.1, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CIP-003-8 R5.1.1, CIP-003-8 R5.3, CIP-004-6 R2.3, CIP-007-3 R2.1, CIP-007-3 R2.2, CIP-007-3 R2.3, CIP-007-3 R5.1, CIP-007-3 R5.1.1, CIP-007-3 R5.1.2, CM-6(a), AC-6(1), PR.AC-4, PR.DS-5, 2.2.6, 6.1.11

Remediation Shell script ⇲

    find / -xdev -type f -perm -002 -exec chmod o-w {} \;

#### Rule Ensure All Files Are Owned by a Group [ref]

If any files are not owned by a group, then the cause of their lack of group-ownership should be investigated. Following this, the files should be deleted or assigned to an appropriate group. The following command will discover and print any files on local partitions which do not belong to a valid group:

    $ df --local -P | awk '{if (NR!=1) print $6}' | sudo xargs -I '{}' find '{}' -xdev -nogroup

To search all filesystems on a system including network mounted filesystems the following command can be run manually for each partition:

    $ sudo find PARTITION -xdev -nogroup

Warning: This rule only considers local groups. If you have your groups defined outside `/etc/group`, the rule won't consider those.

Rationale:

Unowned files do not directly imply a security problem, but they are generally a sign that something is amiss. They may be caused by an intruder, by incorrect software installation or draft software removal, or by failure to remove all files belonging to a deleted account. The files should be repaired so they will not cause problems when accounts are created in the future, and the cause should be discovered and addressed.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_file_permissions_ungroupowned

Identifiers and References

References: BP28(R55), 1, 11, 12, 13, 14, 15, 16, 18, 3, 5, APO01.06, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS05.10, DSS06.02, DSS06.03, DSS06.06, DSS06.10, CCI-000366, CCI-002165, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 5.2, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.18.1.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.2, A.9.2.3, A.9.2.4, A.9.2.6, A.9.3.1, A.9.4.1, A.9.4.2, A.9.4.3, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), PR.AC-1, PR.AC-4, PR.AC-6, PR.AC-7, PR.DS-5, PR.PT-3, 2.2.6, SRG-OS-000480-GPOS-00227, 6.1.13, SV-230327r627750_rule

#### Rule Ensure All Files Are Owned by a User [ref]

If any files are not owned by a user, then the cause of their lack of ownership should be investigated. Following this, the files should be deleted or assigned to an appropriate user. The following command will discover and print any files on local partitions which do not belong to a valid user:

    $ df --local -P | awk {'if (NR!=1) print $6'} | sudo xargs -I '{}' find '{}' -xdev -nouser

To search all filesystems on a system including network mounted filesystems the following command can be run manually for each partition:

    $ sudo find PARTITION -xdev -nouser

Warning: For this rule to evaluate centralized user accounts, `getent` must be working properly so that running the command

    getent passwd

returns a list of all users in your organization. If using the System Security Services Daemon (SSSD),

    enumerate = true

must be configured in your organization's domain to return a complete list of users
Warning: Enabling this rule will result in slower scan times depending on the size of your organization and number of centralized users.

Rationale:

Unowned files do not directly imply a security problem, but they are generally a sign that something is amiss. They may be caused by an intruder, by incorrect software installation or draft software removal, or by failure to remove all files belonging to a deleted account. The files should be repaired so they will not cause problems when accounts are created in the future, and the cause should be discovered and addressed.

Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_no_files_unowned_by_user

Identifiers and References

References: BP28(R55), 11, 12, 13, 14, 15, 16, 18, 3, 5, 9, APO01.06, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.04, DSS05.05, DSS05.07, DSS06.02, DSS06.03, DSS06.06, CCI-000366, CCI-002165, 4.3.3.2.2, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 5.2, SR 7.6, A.10.1.1, A.11.1.4, A.11.1.5, A.11.2.1, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.1.3, A.13.2.1, A.13.2.3, A.13.2.4, A.14.1.2, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.1.2, A.7.1.1, A.7.1.2, A.7.3.1, A.8.2.2, A.8.2.3, A.9.1.1, A.9.1.2, A.9.2.1, A.9.2.3, A.9.4.1, A.9.4.4, A.9.4.5, CM-6(a), AC-6(1), PR.AC-4, PR.AC-6, PR.DS-5, PR.IP-1, PR.PT-3, 2.2.6, SRG-OS-000480-GPOS-00227, 6.1.12, SV-230326r627750_rule

Group Services Group contains 10 groups and 13 rules

[ref] The best protection against vulnerable software is running less software. This section describes how to review the software which Red Hat Enterprise Linux 8 installs on a system and disable software which is not needed. It then enumerates the software packages installed on a default Red Hat Enterprise Linux 8 system and provides guidance about which ones can be safely disabled.
Red Hat Enterprise Linux 8 provides a convenient minimal install option that essentially installs the bare necessities for a functional system. When building Red Hat Enterprise Linux 8 systems, it is highly recommended to select the minimal packages and then build up the system from there.

Group DHCP Group contains 1 group and 1 rule

[ref] The Dynamic Host Configuration Protocol (DHCP) allows systems to request and obtain an IP address and other configuration parameters from a server.
This guide recommends configuring networking on clients by manually editing the appropriate files under `/etc/sysconfig`. Use of DHCP can make client systems vulnerable to compromise by rogue DHCP servers, and should be avoided unless necessary. If using DHCP is necessary, however, there are best practices that should be followed to minimize security risk.

Group Disable DHCP Server Group contains 1 rule
[ref] The DHCP server `dhcpd` is not installed or activated by default. If the software was installed and activated, but the system does not need to act as a DHCP server, it should be disabled and removed.

#### Rule Uninstall DHCP Server Package [ref]

If the system does not need to act as a DHCP server, the dhcp package can be uninstalled. The `dhcp-server` package can be removed with the following command:

    $ sudo yum erase dhcp-server

Rationale: | Removing the DHCP server ensures that it cannot be easily or accidentally reactivated and disrupt network operation.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_dhcp_removed

Identifiers and References

References: BP28(R1), 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000366, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, 2.2.4, 2.2.5

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove dhcp-server
    #	   from the system, and may remove any packages
    #	   that depend on dhcp-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "dhcp-server" ; then

        yum remove -y "dhcp-server"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=dhcp-server

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure dhcp-server is removed
      package:
        name: dhcp-server
        state: absent
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_dhcp_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_dhcp-server

    class remove_dhcp-server {
      package { 'dhcp-server':
        ensure => 'purged',
      }
    }

Group Mail Server Software Group contains 1 rule

[ref] Mail servers are used to send and receive email over the network. Mail is a very common service, and Mail Transfer Agents (MTAs) are obvious targets of network attack. Ensure that systems are not running MTAs unnecessarily, and configure needed MTAs as defensively as possible.
Very few systems at any site should be configured to directly receive email over the network. Users should instead use mail client programs to retrieve email from a central server that supports protocols such as IMAP or POP3. However, it is normal for most systems to be independently capable of sending email, for instance so that cron jobs can report output to an administrator. Most MTAs, including Postfix, support a submission-only mode in which mail can be sent from the local system to a central site MTA (or directly delivered to a local account), but the system still cannot receive mail directly over a network.
The `alternatives` program in Red Hat Enterprise Linux 8 permits selection of other mail server software (such as Sendmail), but Postfix is the default and is preferred. Postfix was coded with security in mind and can also be more effectively contained by SELinux as its modular design has resulted in separate processes performing specific actions. More information is available on its website, http://www.postfix.org.

#### Rule Uninstall Sendmail Package [ref]

Sendmail is not the default mail transfer agent and is not installed by default. The `sendmail` package can be removed with the following command:

    $ sudo yum erase sendmail

Rationale: | The sendmail software was not developed with security in mind and its design prevents it from being effectively contained by SELinux. Postfix should be used instead.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_sendmail_removed

Identifiers and References

References: BP28(R1), 11, 14, 3, 9, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS05.02, DSS05.05, DSS06.06, CCI-000381, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 7.6, A.12.1.2, A.12.5.1, A.12.6.2, A.14.2.2, A.14.2.3, A.14.2.4, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.IP-1, PR.PT-3, SRG-OS-000480-GPOS-00227, SRG-OS-000095-GPOS-00049, SV-230489r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # CAUTION: This remediation script will remove sendmail
    #	   from the system, and may remove any packages
    #	   that depend on sendmail. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "sendmail" ; then

        yum remove -y "sendmail"

    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=sendmail

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure sendmail is removed
      package:
        name: sendmail
        state: absent
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - DISA-STIG-RHEL-08-040002
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_sendmail_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_sendmail

    class remove_sendmail {
      package { 'sendmail':
        ensure => 'purged',
      }
    }

Group Obsolete Services Group contains 6 groups and 11 rules

[ref] This section discusses a number of network-visible services which have historically caused problems for system security, and for which disabling or severely limiting the service has been the best available guidance for some time. As a result of this, many of these services are not installed as part of Red Hat Enterprise Linux 8 by default.
Organizations which are running these services should switch to more secure equivalents as soon as possible. If it remains absolutely necessary to run one of these services for legacy reasons, care should be taken to restrict the service as much as possible, for instance by configuring host firewall software such as `iptables` to restrict access to the vulnerable service to only those remote hosts which have a known need to use it.

Group Xinetd Group contains 1 rule
[ref] The `xinetd` service acts as a dedicated listener for some network services (mostly, obsolete ones) and can be used to provide access controls and perform some logging. It has been largely obsoleted by other features, and it is not installed by default. The older Inetd service is not even available as part of Red Hat Enterprise Linux 8.

#### Rule Uninstall xinetd Package [ref]

The `xinetd` package can be removed with the following command:

    $ sudo yum erase xinetd

Rationale: | Removing the `xinetd` package decreases the risk of the xinetd service's accidental (or intentional) activation.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_package_xinetd_removed

Identifiers and References

References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000305, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, 2.2.4, 2.2.1

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # Remediation is applicable only in certain platforms
    if [ ! -f /.dockerenv ] && [ ! -f /run/.containerenv ]; then

    # CAUTION: This remediation script will remove xinetd
    #	   from the system, and may remove any packages
    #	   that depend on xinetd. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "xinetd" ; then

        yum remove -y "xinetd"

    fi

    else
        >&2 echo 'Remediation is not applicable, nothing was done'
    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=xinetd

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure xinetd is removed
      package:
        name: xinetd
        state: absent
      when: ansible_virtualization_type not in ["docker", "lxc", "openvz", "podman", "container"]
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - package_xinetd_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_xinetd

    class remove_xinetd {
      package { 'xinetd':
        ensure => 'purged',
      }
    }

Group NIS Group contains 2 rules
[ref] The Network Information Service (NIS), also known as 'Yellow Pages' (YP), and its successor NIS+ have been made obsolete by Kerberos, LDAP, and other modern centralized authentication services. NIS should not be used because it suffers from security problems inherent in its design, such as inadequate protection of important authentication information.

#### Rule Remove NIS Client [ref]

The Network Information Service (NIS), formerly known as Yellow Pages, is a client-server directory service protocol used to distribute system configuration files. The NIS client (`ypbind`) was used to bind a system to an NIS server and receive the distributed configuration files.
Rationale: | The NIS service is inherently an insecure system that has been vulnerable to DOS attacks, buffer overflows and has poor authentication for querying NIS maps. NIS generally has been replaced by such protocols as Lightweight Directory Access Protocol (LDAP). It is recommended that the service be removed.
Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_package_ypbind_removed
Identifiers and References | References: BP28(R1), 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 2.2.4, 2.3.1

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove ypbind
    #	   from the system, and may remove any packages
    #	   that depend on ypbind. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "ypbind" ; then

        yum remove -y "ypbind"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=ypbind

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure ypbind is removed
      package:
        name: ypbind
        state: absent
      tags:
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_ypbind_removed
      - unknown_severity

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_ypbind

    class remove_ypbind {
      package { 'ypbind':
        ensure => 'purged',
      }
    }

#### Rule Uninstall ypserv Package [ref]

The `ypserv` package can be removed with the following command:

    $ sudo yum erase ypserv

Rationale: | The NIS service provides an unencrypted authentication service which does not provide for the confidentiality and integrity of user passwords or the remote session. Removing the `ypserv` package decreases the risk of the accidental (or intentional) activation of NIS or NIS+ services.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_package_ypserv_removed

Identifiers and References

References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, Req-2.2.2, 2.2.4, SRG-OS-000095-GPOS-00049, 2.2.15

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove ypserv
    #	   from the system, and may remove any packages
    #	   that depend on ypserv. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "ypserv" ; then

        yum remove -y "ypserv"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=ypserv

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure ypserv is removed
      package:
        name: ypserv
        state: absent
      tags:
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-IA-5(1)(c)
      - PCI-DSS-Req-2.2.2
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_ypserv_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_ypserv

    class remove_ypserv {
      package { 'ypserv':
        ensure => 'purged',
      }
    }

Group Rlogin, Rsh, and Rexec Group contains 2 rules
[ref] The Berkeley r-commands are legacy services which allow cleartext remote access and have an insecure trust model.

#### Rule Uninstall rsh-server Package [ref]

The `rsh-server` package can be removed with the following command:

    $ sudo yum erase rsh-server

Warning: The package is not available in Red Hat Enterprise Linux 8.

Rationale:

The `rsh-server` service provides unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session and has very weak authentication. If a privileged user were to login using this service, the privileged user password could be compromised. The `rsh-server` package provides several obsolete and insecure network services. Removing it decreases the risk of those services' accidental (or intentional) activation.

Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_package_rsh-server_removed

Identifiers and References

References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), IA-5(1)(c), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, 2.2.4, SRG-OS-000095-GPOS-00049, SV-230492r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove rsh-server
    #	   from the system, and may remove any packages
    #	   that depend on rsh-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "rsh-server" ; then

        yum remove -y "rsh-server"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=rsh-server

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure rsh-server is removed
      package:
        name: rsh-server
        state: absent
      tags:
      - DISA-STIG-RHEL-08-040010
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - NIST-800-53-IA-5(1)(c)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_rsh-server_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_rsh-server

    class remove_rsh-server {
      package { 'rsh-server':
        ensure => 'purged',
      }
    }

#### Rule Uninstall rsh Package [ref]

The `rsh` package contains the client commands for the rsh services Warning: The package is not available in Red Hat Enterprise Linux 8.

Rationale:

These legacy clients contain numerous security exposures and have been replaced with the more secure SSH package. Even if the server is removed, it is best to ensure the clients are also removed to prevent users from inadvertently attempting to use these commands and therefore exposing their credentials. Note that removing the `rsh` package removes the clients for `rsh`,`rcp`, and `rlogin`.

Severity: | unknown
Rule ID: | xccdf_org.ssgproject.content_rule_package_rsh_removed
Identifiers and References | References: BP28(R1), 3.1.13, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), A.8.2.3, A.13.1.1, A.13.2.1, A.13.2.3, A.14.1.2, A.14.1.3, 2.2.4, 2.3.2

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove rsh
    #	   from the system, and may remove any packages
    #	   that depend on rsh. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "rsh" ; then

        yum remove -y "rsh"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=rsh

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure rsh is removed
      package:
        name: rsh
        state: absent
      tags:
      - NIST-800-171-3.1.13
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_rsh_removed
      - unknown_severity

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_rsh

    class remove_rsh {
      package { 'rsh':
        ensure => 'purged',
      }
    }

Group Chat/Messaging Services Group contains 2 rules
[ref] The talk software makes it possible for users to send and receive messages across systems through a terminal session.

#### Rule Uninstall talk-server Package [ref]

The `talk-server` package can be removed with the following command:

     $ sudo yum erase talk-server

Warning: The package is not available in Red Hat Enterprise Linux 8.

Rationale: | The talk software presents a security risk as it uses unencrypted protocols for communications. Removing the `talk-server` package decreases the risk of the accidental (or intentional) activation of talk services.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_talk-server_removed
Identifiers and References | References: BP28(R1), 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 2.2.4

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove talk-server
    #	   from the system, and may remove any packages
    #	   that depend on talk-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "talk-server" ; then

        yum remove -y "talk-server"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=talk-server

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure talk-server is removed
      package:
        name: talk-server
        state: absent
      tags:
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_talk-server_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_talk-server

    class remove_talk-server {
      package { 'talk-server':
        ensure => 'purged',
      }
    }

#### Rule Uninstall talk Package [ref]

The `talk` package contains the client program for the Internet talk protocol, which allows the user to chat with other users on different systems. Talk is a communication program which copies lines from one terminal to the terminal of another user. The `talk` package can be removed with the following command:

    $ sudo yum erase talk

Warning: The package is not available in Red Hat Enterprise Linux 8.

Rationale: | The talk software presents a security risk as it uses unencrypted protocols for communications. Removing the `talk` package decreases the risk of the accidental (or intentional) activation of talk client program.
Severity: | medium
Rule ID: | xccdf_org.ssgproject.content_rule_package_talk_removed
Identifiers and References | References: BP28(R1), 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 2.2.4, 2.3.3

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove talk
    #	   from the system, and may remove any packages
    #	   that depend on talk. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "talk" ; then

        yum remove -y "talk"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=talk

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure talk is removed
      package:
        name: talk
        state: absent
      tags:
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - medium_severity
      - no_reboot_needed
      - package_talk_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_talk

    class remove_talk {
      package { 'talk':
        ensure => 'purged',
      }
    }

Group Telnet Group contains 2 rules
[ref] The telnet protocol does not provide confidentiality or integrity for information transmitted on the network. This includes authentication information such as passwords. Organizations which use telnet should be actively working to migrate to a more secure protocol.

#### Rule Uninstall telnet-server Package [ref]

The `telnet-server` package can be removed with the following command:

    $ sudo yum erase telnet-server

Rationale:

It is detrimental for operating systems to provide, or install by default, functionality exceeding requirements or mission objectives. These unnecessary capabilities are often overlooked and therefore may remain unsecure. They increase the risk to the platform by providing additional attack vectors.
The telnet service provides an unencrypted remote access service which does not provide for the confidentiality and integrity of user passwords or the remote session. If a privileged user were to login using this service, the privileged user password could be compromised.
Removing the `telnet-server` package decreases the risk of the telnet service's accidental (or intentional) activation.

Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_package_telnet-server_removed

Identifiers and References

References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000381, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, Req-2.2.2, 2.2.4, SRG-OS-000095-GPOS-00049, 2.2.16, SV-230487r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove telnet-server
    #	   from the system, and may remove any packages
    #	   that depend on telnet-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "telnet-server" ; then

        yum remove -y "telnet-server"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=telnet-server

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure telnet-server is removed
      package:
        name: telnet-server
        state: absent
      tags:
      - DISA-STIG-RHEL-08-040000
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSS-Req-2.2.2
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_telnet-server_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_telnet-server

    class remove_telnet-server {
      package { 'telnet-server':
        ensure => 'purged',
      }
    }

#### Rule Remove telnet Clients [ref]

The telnet client allows users to start connections to other systems via the telnet protocol.
Rationale: | The `telnet` protocol is insecure and unencrypted. The use of an unencrypted transmission medium could allow an unauthorized user to steal credentials. The `ssh` package provides an encrypted session and stronger security and is included in Red Hat Enterprise Linux 8.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_package_telnet_removed
Identifiers and References | References: BP28(R1), 3.1.13, 164.308(a)(4)(i), 164.308(b)(1), 164.308(b)(3), 164.310(b), 164.312(e)(1), 164.312(e)(2)(ii), A.8.2.3, A.13.1.1, A.13.2.1, A.13.2.3, A.14.1.2, A.14.1.3, 2.2.4, 2.3.4

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove telnet
    #	   from the system, and may remove any packages
    #	   that depend on telnet. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "telnet" ; then

        yum remove -y "telnet"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=telnet

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure telnet is removed
      package:
        name: telnet
        state: absent
      tags:
      - NIST-800-171-3.1.13
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - package_telnet_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_telnet

    class remove_telnet {
      package { 'telnet':
        ensure => 'purged',
      }
    }

Group TFTP Server Group contains 2 rules
[ref] TFTP is a lightweight version of the FTP protocol which has traditionally been used to configure networking equipment. However, TFTP provides little security, and modern versions of networking operating systems frequently support configuration via SSH or other more secure protocols. A TFTP server should be run only if no more secure method of supporting existing equipment can be found.

#### Rule Uninstall tftp-server Package [ref]

The `tftp-server` package can be removed with the following command:

     $ sudo yum erase tftp-server

Rationale: | Removing the `tftp-server` package decreases the risk of the accidental (or intentional) activation of tftp services. If TFTP is required for operational support (such as transmission of router configurations), its use must be documented with the Information Systems Securty Manager (ISSM), restricted to only authorized personnel, and have access control rules established.
Severity: | high
Rule ID: | xccdf_org.ssgproject.content_rule_package_tftp-server_removed

Identifiers and References

References: BP28(R1), 11, 12, 14, 15, 3, 8, 9, APO13.01, BAI10.01, BAI10.02, BAI10.03, BAI10.05, DSS01.04, DSS05.02, DSS05.03, DSS05.05, DSS06.06, CCI-000318, CCI-000366, CCI-000368, CCI-001812, CCI-001813, CCI-001814, 4.3.3.5.1, 4.3.3.5.2, 4.3.3.5.3, 4.3.3.5.4, 4.3.3.5.5, 4.3.3.5.6, 4.3.3.5.7, 4.3.3.5.8, 4.3.3.6.1, 4.3.3.6.2, 4.3.3.6.3, 4.3.3.6.4, 4.3.3.6.5, 4.3.3.6.6, 4.3.3.6.7, 4.3.3.6.8, 4.3.3.6.9, 4.3.3.7.1, 4.3.3.7.2, 4.3.3.7.3, 4.3.3.7.4, 4.3.4.3.2, 4.3.4.3.3, SR 1.1, SR 1.10, SR 1.11, SR 1.12, SR 1.13, SR 1.2, SR 1.3, SR 1.4, SR 1.5, SR 1.6, SR 1.7, SR 1.8, SR 1.9, SR 2.1, SR 2.2, SR 2.3, SR 2.4, SR 2.5, SR 2.6, SR 2.7, SR 3.1, SR 3.5, SR 3.8, SR 4.1, SR 4.3, SR 5.1, SR 5.2, SR 5.3, SR 7.1, SR 7.6, A.11.2.6, A.12.1.2, A.12.5.1, A.12.6.2, A.13.1.1, A.13.2.1, A.14.1.3, A.14.2.2, A.14.2.3, A.14.2.4, A.6.2.1, A.6.2.2, A.9.1.2, CM-7(a), CM-7(b), CM-6(a), PR.AC-3, PR.IP-1, PR.PT-3, PR.PT-4, 2.2.4, SRG-OS-000480-GPOS-00227, 2.2.9, SV-230533r627750_rule

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove tftp-server
    #	   from the system, and may remove any packages
    #	   that depend on tftp-server. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "tftp-server" ; then

        yum remove -y "tftp-server"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=tftp-server

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure tftp-server is removed
      package:
        name: tftp-server
        state: absent
      tags:
      - DISA-STIG-RHEL-08-040190
      - NIST-800-53-CM-6(a)
      - NIST-800-53-CM-7(a)
      - NIST-800-53-CM-7(b)
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - high_severity
      - low_complexity
      - low_disruption
      - no_reboot_needed
      - package_tftp-server_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_tftp-server

    class remove_tftp-server {
      package { 'tftp-server':
        ensure => 'purged',
      }
    }

#### Rule Remove tftp Daemon [ref]

Trivial File Transfer Protocol (TFTP) is a simple file transfer protocol, typically used to automatically transfer configuration or boot files between systems. TFTP does not support authentication and can be easily hacked. The package `tftp` is a client program that allows for connections to a `tftp` server.
Rationale: | It is recommended that TFTP be removed, unless there is a specific need for TFTP (such as a boot server). In that case, use extreme caution when configuring the services.
Severity: | low
Rule ID: | xccdf_org.ssgproject.content_rule_package_tftp_removed
Identifiers and References | References: BP28(R1), 2.2.4, 2.3.6

Remediation Shell script ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    # CAUTION: This remediation script will remove tftp
    #	   from the system, and may remove any packages
    #	   that depend on tftp. Execute this
    #	   remediation AFTER testing on a non-production
    #	   system!

    if rpm -q --quiet "tftp" ; then

        yum remove -y "tftp"

    fi

Remediation Anaconda snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    package --remove=tftp

Remediation Ansible snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    - name: Ensure tftp is removed
      package:
        name: tftp
        state: absent
      tags:
      - PCI-DSSv4-2.2.4
      - disable_strategy
      - low_complexity
      - low_disruption
      - low_severity
      - no_reboot_needed
      - package_tftp_removed

Remediation Puppet snippet ⇲

Complexity: | low
Disruption: | low
Reboot: | false
Strategy: | disable

    include remove_tftp

    class remove_tftp {
      package { 'tftp':
        ensure => 'purged',
      }
    }

Red Hat and Red Hat Enterprise Linux are either registered trademarks or trademarks of Red Hat, Inc. in the United States and other countries. All other names are registered trademarks or trademarks of their respective companies.