CIS Hardening of a Debian Linux Server Part 5: Level 1 Access Control

This is the sixth entry in the series documenting the Debian 13 server CIS hardening project.

This entry covers sections 5.1 through 5.4 of the benchmark. This is a Level 1 pass. Level 2 recommendations are deferred.

5.1: Configure SSH Server #

This section covers 5.1.1 through 5.1.23 of the benchmark. The introductory guidance for this section contains several references that appear to originate from the Red Hat Enterprise Linux benchmark rather than Debian. Examples include references to /etc/ssh/sshd_config.d/50-redhat.conf and the system-wide crypto policy framework under /etc/crypto-policies/. Neither is present on a standard Debian 13 installation. The OpenSSH configuration directives discussed throughout the remainder of the section remain applicable. No Match statements are used in the current configuration.

5.1.1: Ensure Access to /etc/ssh/sshd_config Is Configured #

# stat -Lc 'File: "%n" Mode: "%#a" Owner: "%U" Group: "%G"' /etc/ssh/sshd_config
File: "/etc/ssh/sshd_config" Mode: "0644" Owner: "root" Group: "root"

Mode 0644 does not meet the benchmark’s requirement of 0600.

/etc/ssh/sshd_config.d is empty, but the audit script covering that directory is run regardless:

#!/usr/bin/env bash
{
  while IFS= read -r -d $'\0' l_file; do
    stat -Lc 'File: "%n" Mode: "%#a" Owner: "%U" Group: "%G"' "$l_file"
  done < <(find /etc/ssh/sshd_config.d/ -xdev -mount -type f -name '*.conf' \
  \( -perm /077 -o ! -user root -o ! -group root \) -print0 2>/dev/null)
}

No output is returned, consistent with the directory being empty.

The benchmark notes that if other locations are listed in an Include statement, *.conf files in those locations should also be checked. The only Include line present in /etc/ssh/sshd_config is:

Include /etc/ssh/sshd_config.d/*.conf

This is already covered by the check above.

The remediation script is run:

#!/usr/bin/env bash
{
  chmod u-x,og-rwx /etc/ssh/sshd_config
  chown root:root /etc/ssh/sshd_config
  while IFS= read -r -d $'\0' l_file; do
    if [ -e "$l_file" ]; then
      chmod u-x,og-rwx "$l_file"
      chown root:root "$l_file"
    fi
  done < <(find /etc/ssh/sshd_config.d -type f -print0 2>/dev/null)
}

The recommendation is verified again following remediation:

# stat -Lc 'File: "%n" Mode: "%#a" Owner: "%U" Group: "%G"' /etc/ssh/sshd_config
File: "/etc/ssh/sshd_config" Mode: "0600" Owner: "root" Group: "root"

The recommendation now passes. The audit script for /etc/ssh/sshd_config.d still returns no output, as expected.

5.1.2: Ensure Access to SSH Private Host Key Files Is Configured #

#!/usr/bin/env bash
{
  l_sshd_cmd="$(readlink -e /usr/sbin/sshd || readlink -e /sbin/sshd)"
  l_keygen="$(readlink -e /usr/bin/ssh-keygen || readlink -e /bin/ssh- keygen)"
  while IFS= read -r l_file; do
    "$l_keygen" -lf &>/dev/null "$l_file" && \
    stat -Lc 'File: "%n" Mode: "%#a" Owner: "%U" Group: "%G"' "$l_file"
  done < <("$l_sshd_cmd" -T | awk '$1=="hostkey" {print $2}')
}
File: "/etc/ssh/ssh_host_rsa_key" Mode: "0600" Owner: "root" Group: "root"
File: "/etc/ssh/ssh_host_ecdsa_key" Mode: "0600" Owner: "root" Group: "root"
File: "/etc/ssh/ssh_host_ed25519_key" Mode: "0600" Owner: "root" Group: "root"

The recommendation passes without remediation.

5.1.3: Ensure Access to SSH Public Host Key Files Is Configured #

#!/usr/bin/env bash
{
  l_sshd_cmd="$(readlink -e /usr/sbin/sshd || readlink -e /sbin/sshd)"
  l_keygen="$(readlink -e /usr/bin/ssh-keygen || readlink -e /bin/ssh- keygen)"
  while IFS= read -r l_file; do
    "$l_keygen" -lf &>/dev/null "$l_file" && \
    stat -Lc 'File: "%n" Mode: "%#a" Owner: "%U" Group: "%G"' "$l_file"
  done < <("$l_sshd_cmd" -T | awk '$1=="hostkey" {print $2".pub"}' 2>/dev/null)
}
File: "/etc/ssh/ssh_host_rsa_key.pub" Mode: "0644" Owner: "root" Group: "root"
File: "/etc/ssh/ssh_host_ecdsa_key.pub" Mode: "0644" Owner: "root" Group: "root"
File: "/etc/ssh/ssh_host_ed25519_key.pub" Mode: "0644" Owner: "root" Group: "root"

The recommendation passes without remediation.

5.1.4: Ensure sshd Access Is Configured #

# sshd -T | grep -Pi -- '^\h*(allow|deny)(users|groups)\h+\H+'

No output is returned.

The following directive is added to /etc/ssh/sshd_config, before the Include line:

AllowUsers sharaf

The recommendation is verified again following remediation:

# sshd -T | grep -Pi -- '^\h*(allow|deny)(users|groups)\h+\H+'
allowusers sharaf

The recommendation now passes. The service is then reloaded:

# systemctl reload-or-restart sshd.service

The command produces no output, and the current session remains active, confirming the reload succeeded.

5.1.5: Ensure sshd Banner Is Configured #

# sshd -T | grep -Pi -- '^banner\h+\/\H+'

No output is returned.

The following block is built up in /etc/ssh/sshd_config, before the Include line:

# CIS hardening modifications
AllowUsers sharaf
Banner /etc/issue.net
# sshd -T | grep -Pi -- '^banner\h+\/\H+'
banner /etc/issue.net

The recommendation now passes. The configuration is reloaded as before, and the banner takes effect on subsequent connections.

5.1.6: Ensure sshd Ciphers Are Configured #

# sshd -T | grep -Pi -- '^ciphers\h+\"?([^#\n\r]+,)?((3des|blowfish|cast128|aes(128|192|256))-cbc|arcfour(128|256)?|rijndael-cbc@lysator\.liu\.se)\b'

No output is returned.

# sshd -T 2>&1 | awk '($1=="ciphers" && $2~/chacha20-poly1305@openssh\.com/){print}'
ciphers chacha20-poly1305@openssh.com,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr

The benchmark’s remediation guidance for this recommendation references CVE-2023-48795, affecting OpenSSH versions before 9.6:

# ssh -V
OpenSSH_10.0p2 Debian-7+deb13u4, OpenSSL 3.5.6 7 Apr 2026

The installed version is 10.0, which is unaffected. The recommendation passes on both counts.

The remediation section of the benchmark for this recommendation also references /etc/crypto-policies/, which, consistent with the note in the section introduction, is not present on this system and does not apply.

5.1.7: Ensure sshd ClientAliveInterval and ClientAliveCountMax Are Configured #

# sshd -T | grep -Pi -- '(clientaliveinterval|clientalivecountmax)'
clientaliveinterval 0
clientalivecountmax 3

A value of 0 for ClientAliveInterval disables the client-alive mechanism entirely, meaning dead sessions are not cleaned up. This does not meet the benchmark’s requirement.

The directive is updated in the same configuration block:

# CIS hardening modifications
AllowUsers sharaf
Banner /etc/issue.net
ClientAliveInterval 15
ClientAliveCountMax 3
# sshd -T | grep -Pi -- '(clientaliveinterval|clientalivecountmax)'
clientaliveinterval 15
clientalivecountmax 3

The recommendation now passes.

5.1.8: Ensure sshd DisableForwarding Is Enabled #

This is a Level 2 recommendation, deferred to a future Level 2 pass. The recommendation does not currently pass.

5.1.9: Ensure sshd GSSAPIAuthentication Is Disabled #

This is a Level 2 recommendation, deferred to a future Level 2 pass. The recommendation appears to pass the audit regardless.

5.1.10: Ensure sshd HostbasedAuthentication Is Disabled #

# sshd -T | grep hostbasedauthentication
hostbasedauthentication no

The recommendation passes without remediation.

5.1.11: Ensure sshd IgnoreRhosts Is Enabled #

# sshd -T | grep ignorerhosts
ignorerhosts yes

The recommendation passes without remediation.

5.1.12: Ensure sshd KexAlgorithms Is Configured #

# sshd -T | grep -Pi -- 'kexalgorithms\h+([^#\n\r]+,)?(diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group-exchange-sha1)\b'

No output is returned. The recommendation passes without remediation.

5.1.13: Ensure sshd Post-Quantum Cryptography Key Exchange Algorithms Are Configured #

# "$(readlink -e /usr/sbin/sshd || readlink -e /sbin/sshd)" -V 2>&1 | grep -Psio 'openssh_\d+\.\d+' | awk -F'_' '{print $2}'
10.0

The installed OpenSSH server version is 10.0.

# "$(readlink -e /usr/sbin/sshd || readlink -e /sbin/sshd)" -T | awk '($1=="kexalgorithms" && $2~/sntrup761x25519-sha512/) {print $2}'
mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp52

sntrup761x25519-sha512 is present in the output.

Since the installed version is greater than 9.9, the following additional check applies:

# "$(readlink -e /usr/sbin/sshd || readlink -e /sbin/sshd)" -T | awk '($1=="kexalgorithms" && $2~/mlkem768x25519-sha256/) {print $2}'
mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521

mlkem768x25519-sha256 is present in the output. Both checks pass without remediation.

5.1.14: Ensure sshd LoginGraceTime Is Configured #

# sshd -T | grep logingracetime
logingracetime 30

The recommendation passes without remediation. This setting was configured prior to the start of the project (see the introduction).

5.1.15: Ensure sshd LogLevel Is Configured #

# sshd -T | grep loglevel
loglevel INFO

The recommendation passes without remediation.

5.1.16: Ensure sshd MACs Are Configured #

# sshd -T | grep -Pi -- 'macs\h+([^#\n\r]+,)?(hmac-md5|hmac-md5-96|hmac-ripemd160|hmac-sha1-96|umac-64@openssh\.com|hmac-md5-etm@openssh\.com|hmac-md5-96-etm@openssh\.com|hmac-ripemd160-etm@openssh\.com|hmac-sha1-96-etm@openssh\.com|umac-64-etm@openssh\.com|umac-128-etm@openssh\.com)\b'
macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1

This does not meet the benchmark’s requirement, which expects no output. CVE-2023-48795 was already addressed in 5.1.6, where the installed OpenSSH version was confirmed to be unaffected.

A deny list covering all eleven weak MACs listed by the benchmark is added to the same configuration block:

# CIS hardening modifications
AllowUsers sharaf
Banner /etc/issue.net
ClientAliveInterval 15
ClientAliveCountMax 3
MACs -hmac-md5,hmac-md5-96,hmac-ripemd160,hmac-sha1-96,umac-64@openssh.com,hmac-md5-etm@openssh.com,hmac-md5-96-etm@openssh.com,hmac-ripemd160-etm@openssh.com,hmac-sha1-96-etm@openssh.com,umac-64-etm@openssh.com,umac-128-etm@openssh.com

This follows the sshd_config man page’s syntax for negated lists: a single leading - applies to the entire comma-separated list.

After reloading, the audit command returns no output, and the recommendation now passes.

5.1.17: Ensure sshd MaxAuthTries Is Configured #

# sshd -T | grep maxauthtries
maxauthtries 4

The recommendation passes without remediation. This setting was configured prior to the start of the project (see the introduction).

5.1.18: Ensure sshd MaxSessions Is Configured #

# sshd -T | grep maxsessions
maxsessions 10
# grep -Psi -- '^\h*MaxSessions\h+\"?(1[1-9]|[2-9][0-9]|[1-9][0-9][0-9]+)\b' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf

No output is returned. The recommendation passes.

5.1.19: Ensure sshd MaxStartups Is Configured #

# sshd -T | awk '$1 ~ /^\s*maxstartups/{split($2, a, ":");{if(a[1] > 10 || a[2] > 30 || a[3] > 60) print $0}}'
maxstartups 10:30:100

The “full” value of MaxStartups (see man sshd_config) should not exceed 60 per the benchmark. The current value of 100 does not meet this requirement.

The directive is added to the same configuration block:

MaxStartups 10:30:60

After reloading:

# sshd -T | awk '$1 ~ /^\s*maxstartups/{split($2, a, ":");{if(a[1] > 10 || a[2] > 30 || a[3] > 60) print $0}}'

No output is returned. The recommendation now passes.

5.1.20: Ensure sshd PermitEmptyPasswords Is Disabled #

# sshd -T | grep permitemptypasswords
permitemptypasswords no

The recommendation passes without remediation.

5.1.21: Ensure sshd PermitRootLogin Is Disabled #

# sshd -T | grep permitrootlogin
permitrootlogin no

The recommendation passes without remediation. This setting was configured prior to the start of the project (see the introduction).

5.1.22: Ensure sshd PermitUserEnvironment Is Disabled #

# sshd -T | grep permituserenvironment
permituserenvironment no

The recommendation passes without remediation.

5.1.23: Ensure sshd UsePAM Is Enabled #

# sshd -T | grep usepam
usepam yes

The recommendation passes without remediation.

5.2: Configure Privilege Escalation #

This section covers 5.2.1 through 5.2.7 of the benchmark. These recommendations concern sudo configuration and privilege escalation.

5.2.1: Ensure sudo Is Installed #

# dpkg-query -l | awk '{print $2}' | grep -Pio -- '^sudo|^sudo-ldap' &>/dev/null && echo "sudo is installed"
sudo is installed

The recommendation passes without remediation.

5.2.2: Ensure sudo Commands Use pty #

The benchmark cautions that incorrect edits to the sudo configuration can render sudo inoperable, and recommends using visudo exclusively when making changes.

# grep -rPi -- '^\h*Defaults\h+([^#\n\r]+,\h*)?use_pty\b' /etc/sudoers*
/etc/sudoers:Defaults   use_pty
# grep -rPi -- '^\h*Defaults\h+([^#\n\r]+,\h*)?!use_pty\b' /etc/sudoers*

No output is returned from the second command. The recommendation passes without remediation.

5.2.3: Ensure sudo Log File Exists #

# grep -rPsi "^\h*Defaults\h+([^#]+,\h*)?logfile\h*=\h*(\"|\')?\H+(\"|\')?(,\h*\H+\h*)*\h*(#.*)?$" /etc/sudoers*

No output is returned. No sudo log file is currently configured, so one is created manually:

# touch /var/log/sudo.log

The modification is placed in a dedicated file under /etc/sudoers.d rather than directly in /etc/sudoers. The file is named 99-cis-hardening, without a .conf extension, so that it is not overwritten by the existing 90-cloud-init-users file and is applied after it.

Since visudo invokes nano by default, the SUDO_EDITOR environment variable is set to vim before editing:

# export SUDO_EDITOR=vim
# touch /etc/sudoers.d/99-cis-hardening
# visudo -f /etc/sudoers.d/99-cis-hardening

The following line is added to the file:

Defaults logfile="/var/log/sudo.log"

The audit is run again:

# grep -rPsi "^\h*Defaults\h+([^#]+,\h*)?logfile\h*=\h*(\"|\')?\H+(\"|\')?(,\h*\H+\h*)*\h*(#.*)?$" /etc/sudoers*
/etc/sudoers.d/99-cis-hardening:Defaults logfile="/var/log/sudo.log"

The recommendation now passes.

5.2.4: Ensure Users Must Provide Password for Escalation #

This is a Level 2 recommendation, deferred to a future Level 2 pass. The recommendation does not currently pass.

5.2.5: Ensure Re-Authentication for Privilege Escalation Is Not Disabled Globally #

# grep -r '^[^#].*\!authenticate' /etc/sudoers*

No output is returned. The recommendation passes without remediation.

5.2.6: Ensure sudo timestamp_timeout Is Configured #

The benchmark’s audit description states that the caching timeout should not be disabled and should be greater than 15 minutes. This appears to be an inversion, since the audit itself checks for a timeout no greater than 15 minutes.

# grep -roP "timestamp_timeout=\K[0-9]*" /etc/sudoers*

No output is returned. The default value is in effect.

# sudo -V | grep -Psi -- 'timestamp\h+timeout\b'
Authentication timestamp timeout: 15.0 minutes

The recommendation passes without remediation.

5.2.7: Ensure Access to the su Command Is Restricted #

# grep -Pi '^\h*auth\h+(?:required|requisite)\h+pam_wheel\.so\h+(?:[^#\n\r]+\h+)?((?!\2)(use_uid\b|group=\H+\b))\h+(?:[^#\n\r]+\h+)?((?!\1)(use_uid\b|group=\H+\b))(\h+.*)?$' /etc/pam.d/su

No output is returned.

An empty group is created for this purpose:

# groupadd sugroup

The following line is appended to /etc/pam.d/su:

# CIS hardening additions
auth required pam_wheel.so use_uid group=sugroup

The audit is run again:

auth required pam_wheel.so use_uid group=sugroup

The recommendation now passes. A follow-up check confirms the group currently has no members:

# grep sugroup /etc/group
sugroup:x:1001:

5.3: Pluggable Authentication Modules #

This section covers 5.3.1.1 through 5.3.3.4.4 of the benchmark. These recommendations concern PAM package installation, pam-auth-update profile configuration, and PAM module argument tuning.

5.3.1: Configure PAM Software Packages #

This subsection covers 5.3.1.1 through 5.3.1.3. These recommendations confirm that the core PAM packages are installed and up to date.

5.3.1.1: Ensure Latest Version of pam Is Installed #

# dpkg-query -s libpam-runtime &>/dev/null && echo "libpam-runtime is installed"
libpam-runtime is installed
# apt list --upgradable 2>&1 | grep -P '^libpam-runtime\b'

No output is returned from the second command. The recommendation passes without remediation.

5.3.1.2: Ensure Latest Version of libpam-modules Is Installed #

# dpkg-query -s libpam-modules &>/dev/null && echo "libpam-modules is installed"
libpam-modules is installed
# apt list --upgradable 2>&1 | grep -P '^libpam-modules\b'

No output is returned from the second command. The recommendation passes without remediation.

5.3.1.3: Ensure Latest Version of libpam-pwquality Is Installed #

# dpkg-query -s libpam-pwquality &>/dev/null && echo "libpam-pwquality is installed"

No output is returned. The package is not installed and is added:

# apt install libpam-pwquality
Installing:                     
  libpam-pwquality

Installing dependencies:
  libcrack2  libpwquality-common  libpwquality1

Recommended packages:
  cracklib-runtime

Summary:
  Upgrading: 0, Installing: 4, Removing: 0, Not Upgrading: 0
  Download size: 123 kB
  Space needed: 803 kB / 36.7 GB available

Continue? [Y/n] y

(...)

The audit is run again:

# dpkg-query -s libpam-pwquality &>/dev/null && echo "libpam-pwquality is installed"
libpam-pwquality is installed
# apt list --upgradable 2>&1 | grep -P '^libpam-pwquality\b'

No output is returned from the second command. The recommendation now passes.

5.3.2: Configure pam-auth-update Profiles #

This subsection covers 5.3.2.1 through 5.3.2.4. These recommendations concern which PAM profiles are enabled via pam-auth-update.

5.3.2.1: Ensure pam_unix Module Is Enabled #

# grep -PH -- '\bpam_unix\.so\b' /etc/pam.d/common-{account,auth,password,session,session-noninteractive}
/etc/pam.d/common-account:account       [success=1 new_authtok_reqd=done default=ignore]        pam_unix.so 
/etc/pam.d/common-auth:auth     [success=1 default=ignore]      pam_unix.so nullok
/etc/pam.d/common-password:password     [success=1 default=ignore]      pam_unix.so obscure use_authtok try_first_pass yescrypt
/etc/pam.d/common-session:session       required        pam_unix.so 
/etc/pam.d/common-session-noninteractive:session        required        pam_unix.so

The recommendation passes without remediation.

5.3.2.2: Ensure pam_faillock Module Is Enabled #

# grep -P -- '\bpam_faillock\.so\b' /etc/pam.d/common-{auth,account}

No output is returned. Two profile files are created under /usr/share/pam-configs, faillock and faillock_notify, using the following scripts. Some tab characters from the benchmark’s PDF collapse to single spaces once copied; only the field separation itself is significant, not the specific whitespace character used.

#!/usr/bin/env bash
{
  arr=('Name: Enable pam_faillock to deny access' 'Default: yes' 'Priority: 0' 'Auth-Type: Primary' 'Auth:' ' [default=die] pam_faillock.so authfail')
  printf '%s\n' "${arr[@]}" > /usr/share/pam-configs/faillock
}
#!/usr/bin/env bash
{
  arr=('Name: Notify of failed login attempts and reset count upon success' 'Default: yes' 'Priority: 1024' 'Auth-Type: Primary' 'Auth:' ' requisite pam_faillock.so preauth' 'Account-Type: Primary' 'Account:' ' required pam_faillock.so')
  printf '%s\n' "${arr[@]}" > /usr/share/pam-configs/faillock_notify
}

Resulting file contents:

# cat /usr/share/pam-configs/faillock
Name: Enable pam_faillock to deny access
Default: yes
Priority: 0
Auth-Type: Primary
Auth:
 [default=die] pam_faillock.so authfail
# cat /usr/share/pam-configs/faillock_notify 
Name: Notify of failed login attempts and reset count upon success
Default: yes
Priority: 1024
Auth-Type: Primary
Auth:
 requisite pam_faillock.so preauth
Account-Type: Primary
Account:
 required pam_faillock.so

The profiles are then enabled:

# pam-auth-update --enable faillock
# pam-auth-update --enable faillock_notify

The audit is run again:

# grep -P -- '\bpam_faillock\.so\b' /etc/pam.d/common-{auth,account}
/etc/pam.d/common-auth:auth     requisite pam_faillock.so preauth
/etc/pam.d/common-auth:auth     [default=die] pam_faillock.so authfail
/etc/pam.d/common-account:account       required pam_faillock.so

The recommendation now passes.

5.3.2.3: Ensure pam_pwquality Module Is Enabled #

# grep -P -- '\bpam_pwquality\.so\b' /etc/pam.d/common-password
password        requisite                       pam_pwquality.so retry=3

The recommendation passes without remediation.

5.3.2.4: Ensure pam_pwhistory Module Is Enabled #

# grep -P -- '\bpam_pwhistory\.so\b' /etc/pam.d/common-password

No output is returned.

# grep -P -- '\bpam_pwhistory\.so\b' /usr/share/pam-configs/*

No output is returned from either check. A profile file named pwhistory is created under /usr/share/pam-configs with fitting contents, using the same approach as the pam_faillock profiles in 5.3.2.2:

#!/usr/bin/env bash
{
  arr=('Name: pwhistory password history checking' 'Default: yes' 'Priority: 1024' 'Password-Type: Primary' 'Password:' ' requisite pam_pwhistory.so remember=24 enforce_for_root use_authtok')
  printf '%s\n' "${arr[@]}" > /usr/share/pam-configs/pwhistory
}

The profile is then enabled:

# pam-auth-update --enable pwhistory

The audit is run again:

# grep -P -- '\bpam_pwhistory\.so\b' /etc/pam.d/common-password
password        requisite pam_pwhistory.so remember=24 enforce_for_root use_authtok

The recommendation now passes.

5.3.3: Configure PAM Arguments #

This subsection covers 5.3.3.1.1 through 5.3.3.4.4. These recommendations concern the specific arguments configured for the pam_faillock, pam_pwquality, pam_pwhistory, and pam_unix modules.

5.3.3.1: Configure pam_faillock Module #

5.3.3.1.1: Ensure Password Failed Attempts Lockout Is Configured #
# grep -Pi -- '^\h*deny\h*=\h*[1-5]\b' /etc/security/faillock.conf

No output is returned.

# grep -Pi -- '^\h*auth\h+(requisite|required|sufficient)\h+pam_faillock\.so\h+([^#\n\r]+\h+)?deny\h*=\h*(0|[6-9]|[1-9][0-9]+)\b' /etc/pam.d/common-auth

No output is returned from either check. The deny line in /etc/security/faillock.conf is uncommented and set to 5. Before applying the change, the pam-configs files are checked to confirm none of them override the deny argument directly:

# grep -Pl -- '\bpam_faillock\.so\h+([^#\n\r]+\h+)?deny\b' /usr/share/pam-configs/*

No output is returned. The audit is run again:

# grep -Pi -- '^\h*deny\h*=\h*[1-5]\b' /etc/security/faillock.conf
deny = 5
# grep -Pi -- '^\h*auth\h+(requisite|required|sufficient)\h+pam_faillock\.so\h+([^#\n\r]+\h+)?deny\h*=\h*(0|[6-9]|[1-9][0-9]+)\b' /etc/pam.d/common-auth

No output is returned from the second check. The recommendation now passes.

5.3.3.1.2: Ensure Password Unlock Time Is Configured #
# grep -Pi -- '^\h*unlock_time\h*=\h*(0|9[0-9][0-9]|[1-9][0-9]{3,})\b' /etc/security/faillock.conf

No output is returned.

# grep -Pi -- '^\h*auth\h+(requisite|required|sufficient)\h+pam_faillock\.so\h+([^#\n\r]+\h+)?unlock_time\h*=\h*([1-9]|[1-9][0-9]|[1-8][0-9][0-9])\b' /etc/pam.d/common-auth

No output is returned from either check. The unlock_time line in /etc/security/faillock.conf is uncommented and set to 900. As before, the pam-configs files are checked first:

# grep -Pl -- '\bpam_faillock\.so\h+([^#\n\r]+\h+)?unlock_time\b' /usr/share/pam-configs/*

No output is returned. The audit is run again:

# grep -Pi -- '^\h*unlock_time\h*=\h*(0|9[0-9][0-9]|[1-9][0-9]{3,})\b' /etc/security/faillock.conf
unlock_time = 900
# grep -Pi -- '^\h*auth\h+(requisite|required|sufficient)\h+pam_faillock\.so\h+([^#\n\r]+\h+)?unlock_time\h*=\h*([1-9]|[1-9][0-9]|[1-8][0-9][0-9])\b' /etc/pam.d/common-auth

No output is returned from the second check. The recommendation now passes.

5.3.3.1.3: Ensure Password Failed Attempts Lockout Includes Root Account #

This is a Level 2 recommendation, deferred to a future Level 2 pass. The recommendation does not currently pass.

5.3.3.2: Configure pam_pwquality Module #

5.3.3.2.1: Ensure Password Number of Changed Characters Is Configured #
# grep -Psi -- '^\h*difok\h*=\h*([2-9]|[1-9][0-9]+)\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf

No output is returned.

# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?difok\h*=\h*([0-1])\b' /etc/pam.d/common-password

No output is returned from either check.

Unlike pam_faillock, the benchmark recommends configuring pam_pwquality settings in a file under /etc/security/pwquality.conf.d/ rather than editing /etc/security/pwquality.conf directly, for the sake of clarity and durability. pam_faillock has no equivalent .d directory, which explains the difference in approach between the two modules. A single file, 60-cis-hardening.conf, is used to hold all pam_pwquality settings from this point forward, rather than one file per parameter.

#!/usr/bin/env bash
{
  sed -ri 's/^\s*difok\s*=/# &/' /etc/security/pwquality.conf
  [ ! -d /etc/security/pwquality.conf.d/ ] && mkdir /etc/security/pwquality.conf.d/
  printf '\n%s' "difok = 2" > /etc/security/pwquality.conf.d/60-cis-hardening.conf
}

As before, the pam-configs files are checked first:

# grep -Pl -- '\bpam_pwquality\.so\h+([^#\n\r]+\h+)?difok\b' /usr/share/pam-configs/*

No output is returned. The audit is run again:

# grep -Psi -- '^\h*difok\h*=\h*([2-9]|[1-9][0-9]+)\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf
/etc/security/pwquality.conf.d/60-cis-hardening.conf:difok = 2
# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?difok\h*=\h*([0-1])\b' /etc/pam.d/common-password

No output is returned from the second check. The recommendation now passes.

5.3.3.2.2: Ensure Password Length Is Configured #
# grep -Psi -- '^\h*minlen\h*=\h*(1[4-9]|[2-9][0-9]|[1-9][0-9]{2,})\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf

No output is returned.

# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?minlen\h*=\h*([0-9]|1[0-3])\b' /etc/pam.d/system-auth /etc/pam.d/common-password

No output is returned from either check. minlen = 14 is added to /etc/security/pwquality.conf.d/60-cis-hardening.conf. Every default in pwquality.conf is already known to be commented out.

As before, the pam-configs files are checked first:

# grep -Pl -- '\bpam_pwquality\.so\h+([^#\n\r]+\h+)?minlen\b' /usr/share/pam-configs/*

No output is returned. The audit is run again:

# grep -Psi -- '^\h*minlen\h*=\h*(1[4-9]|[2-9][0-9]|[1-9][0-9]{2,})\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf
/etc/security/pwquality.conf.d/60-cis-hardening.conf:minlen = 14
# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?minlen\h*=\h*([0-9]|1[0-3])\b' /etc/pam.d/system-auth /etc/pam.d/common-password

No output is returned from the second check. The recommendation now passes.

5.3.3.2.3: Ensure Password Complexity Is Configured #
# grep -Psi -- '^\h*(minclass|[dulo]credit)\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf

No output is returned.

# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?(minclass=\d*|[dulo]credit=-?\d*)\b' /etc/pam.d/common-password

No output is returned from either check.

As before, the pam-configs files are checked first:

# grep -Pl -- '\bpam_pwquality\.so\h+([^#\n\r]+\h+)?(minclass|[dulo]credit)\b' /usr/share/pam-configs/*

No output is returned. The benchmark does not specify particular values for this recommendation. Newer password guidance generally favors length over character composition, but the minlen = 14 value the benchmark specifies for 5.3.3.2.2 does not push length far enough for that shift to substitute for complexity here, so an all-four-character-classes policy is adopted instead.

The following is added to /etc/security/pwquality.conf.d/60-cis-hardening.conf:

minclass = 4
dcredit = -1
ucredit = -1
ocredit = -1
lcredit = -1

The audit is run again:

# grep -Psi -- '^\h*(minclass|[dulo]credit)\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf
/etc/security/pwquality.conf.d/60-cis-hardening.conf:minclass = 4
/etc/security/pwquality.conf.d/60-cis-hardening.conf:dcredit = -1
/etc/security/pwquality.conf.d/60-cis-hardening.conf:ucredit = -1
/etc/security/pwquality.conf.d/60-cis-hardening.conf:ocredit = -1
/etc/security/pwquality.conf.d/60-cis-hardening.conf:lcredit = -1
# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?(minclass=\d*|[dulo]credit=-?\d*)\b' /etc/pam.d/common-password

No output is returned from the second check. The recommendation now passes.

5.3.3.2.4: Ensure Password Same Consecutive Characters Is Configured #
# grep -Psi -- '^\h*maxrepeat\h*=\h*[1-3]\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf

No output is returned.

# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?maxrepeat\h*=\h*(0|[4-9]|[1-9][0-9]+)\b' /etc/pam.d/common-password

No output is returned from either check. maxrepeat = 3 is added to /etc/security/pwquality.conf.d/60-cis-hardening.conf. As before, the pam-configs files are checked first:

# grep -Pl -- '\bpam_pwquality\.so\h+([^#\n\r]+\h+)?maxrepeat\b' /usr/share/pam-configs/*

No output is returned. The audit is run again:

# grep -Psi -- '^\h*maxrepeat\h*=\h*[1-3]\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf
/etc/security/pwquality.conf.d/60-cis-hardening.conf:maxrepeat = 3
# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?maxrepeat\h*=\h*(0|[4-9]|[1-9][0-9]+)\b' /etc/pam.d/common-password

No output is returned from the second check. The recommendation now passes.

5.3.3.2.5: Ensure Password Maximum Sequential Characters Is Configured #
# grep -Psi -- '^\h*maxsequence\h*=\h*[1-3]\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf

No output is returned.

# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?maxsequence\h*=\h*(0|[4-9]|[1-9][0-9]+)\b' /etc/pam.d/common-password

No output is returned from either check. maxsequence = 3 is added to /etc/security/pwquality.conf.d/60-cis-hardening.conf. As before, the pam-configs files are checked first:

# grep -Pl -- '\bpam_pwquality\.so\h+([^#\n\r]+\h+)?maxsequence\b' /usr/share/pam-configs/*

No output is returned. The audit is run again:

# grep -Psi -- '^\h*maxsequence\h*=\h*[1-3]\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf
/etc/security/pwquality.conf.d/60-cis-hardening.conf:maxsequence = 3
# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?maxsequence\h*=\h*(0|[4-9]|[1-9][0-9]+)\b' /etc/pam.d/common-password

No output is returned from the second check. The recommendation now passes.

5.3.3.2.6: Ensure Password Dictionary Check Is Enabled #
# grep -Psi -- '^\h*dictcheck\h*=\h*0\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf

No output is returned. Dictionary checking is not explicitly disabled here, and according to pwquality.conf(5), it is enabled by default in the absence of this setting.

# grep -Psi -- '^\h*password\h+(requisite|required|sufficient)\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?dictcheck\h*=\h*0\b' /etc/pam.d/common-password

No output is returned. The recommendation passes without remediation.

5.3.3.2.7: Ensure Password Quality Checking Is Enforced #
# grep -PHsi -- '^\h*enforcing\h*=\h*0\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf
# grep -PHsi -- '^\h*password\h+[^#\n\r]+\h+pam_pwquality\.so\h+([^#\n\r]+\h+)?enforcing=0\b' /etc/pam.d/common-password

No output is returned from either check. The recommendation passes without remediation.

5.3.3.2.8: Ensure Password Quality Is Enforced for the Root User #
# grep -Psi -- '^\h*enforce_for_root\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf

No output is returned. enforce_for_root is added to /etc/security/pwquality.conf.d/60-cis-hardening.conf. The audit is run again:

# grep -Psi -- '^\h*enforce_for_root\b' /etc/security/pwquality.conf /etc/security/pwquality.conf.d/*.conf
/etc/security/pwquality.conf.d/60-cis-hardening.conf:enforce_for_root

The recommendation now passes.

5.3.3.3: Configure pam_pwhistory Module #

5.3.3.3.1: Ensure Password History Remember Is Configured #
# grep -Pi -- '^\h*remember\h*=\h*(2[4-9]|[3-9][0-9]|[1-9][0-9]{2,})\b' /etc/security/pwhistory.conf

No output is returned.

# grep -P -- '\bpam_pwhistory\.so\b' /etc/pam.d/common-password
password        requisite pam_pwhistory.so remember=24 enforce_for_root use_authtok

The recommendation passes through the module argument route. The benchmark notes that arguments specified directly on the pam_pwhistory module line take precedence over pwhistory.conf, and that pwhistory.conf is the preferred location, but also that only one of the two methods should be used. Since output from both is itself considered a failure and the module arguments already satisfy the recommendation, no change is made here.

5.3.3.3.2: Ensure Password History Is Enforced for the Root User #
# grep -Pi -- '^\h*enforce_for_root\b' /etc/security/pwhistory.conf

No output is returned.

# grep -Psi -- '^\h*password\h+[^#\n\r]+\h+pam_pwhistory\.so\h+([^#\n\r]+\h+)?enforce_for_root\b' /etc/pam.d/common-password
password        requisite pam_pwhistory.so remember=24 enforce_for_root use_authtok

enforce_for_root is present in the module arguments, so the recommendation passes through the same route as 5.3.3.3.1.

5.3.3.3.3: Ensure pam_pwhistory Includes use_authtok #
# grep -Pi -- '^\h*use_authtok\b' /etc/security/pwhistory.conf

No output is returned.

# grep -Psi -- '^\h*password\h+[^#\n\r]+\h+pam_pwhistory\.so\h+([^#\n\r]+\h+)?use_authtok\b' /etc/pam.d/common-password
password        requisite pam_pwhistory.so remember=24 enforce_for_root use_authtok

use_authtok is present in the module arguments, so the recommendation again passes through the same route.

5.3.3.4: Configure pam_unix Module #

5.3.3.4.1: Ensure pam_unix Does Not Include nullok #
# grep -PHs -- '^\h*[^#\n\r]+\h+pam_unix\.so\h+([^#\n\r]+\h+)?nullok\b' /etc/pam.d/common-{password,auth,account,session,session-noninteractive}
/etc/pam.d/common-auth:auth     [success=2 default=ignore]      pam_unix.so nullok try_first_pass

The recommendation does not pass.

# grep -PH -- '^\h*([^#\n\r]+\h+)?pam_unix\.so\h+([^#\n\r]+\h+)?nullok\b' /usr/share/pam-configs/*
/usr/share/pam-configs/unix:    [success=end default=ignore]    pam_unix.so nullok try_first_pass
/usr/share/pam-configs/unix:    [success=end default=ignore]    pam_unix.so nullok

nullok is removed from both lines in /usr/share/pam-configs/unix, and the corresponding file under /etc/pam.d is updated:

# pam-auth-update --enable unix

The audit is run again:

# grep -PHs -- '^\h*[^#\n\r]+\h+pam_unix\.so\h+([^#\n\r]+\h+)?nullok\b' /etc/pam.d/common-{password,auth,account,session,session-noninteractive}

No output is returned. The recommendation now passes.

5.3.3.4.2: Ensure pam_unix Does Not Include remember #
# grep -PHs -- '^\h*^\h*[^#\n\r]+\h+pam_unix\.so\h+([^#\n\r]+\h+)?remember=\d+\b' /etc/pam.d/common-{password,auth,account,session,session-noninteractive}

No output is returned. The recommendation passes without remediation.

5.3.3.4.3: Ensure pam_unix Includes a Strong Password Hashing Algorithm #
# grep -PH -- '^\h*password\h+([^#\n\r]+)\h+pam_unix\.so\h+([^#\n\r]+\h+)?(sha512|yescrypt)\b' /etc/pam.d/common-password
/etc/pam.d/common-password:password     [success=1 default=ignore]      pam_unix.so obscure use_authtok try_first_pass yescrypt

The recommendation passes without remediation.

5.3.3.4.4: Ensure pam_unix Includes use_authtok #
# grep -PH -- '^\h*password\h+([^#\n\r]+)\h+pam_unix\.so\h+([^#\n\r]+\h+)?use_authtok\b' /etc/pam.d/common-password
/etc/pam.d/common-password:password     [success=1 default=ignore]      pam_unix.so obscure use_authtok try_first_pass yescrypt

The recommendation passes without remediation.

5.4: User Accounts and Environment #

This section covers 5.4.1 through 5.4.3 of the benchmark. These recommendations concern shadow password suite parameters, root and system account configuration, and the default user environment.

5.4.1: Configure Shadow Password Suite Parameters #

This subsection covers 5.4.1.1 through 5.4.1.6. These recommendations concern the shadow password suite parameters in /etc/login.defs and /etc/shadow.

5.4.1.1: Ensure Password Expiration Is Configured #

# grep -Pi -- '^\h*PASS_MAX_DAYS\h+\d+\b' /etc/login.defs
PASS_MAX_DAYS   99999
# awk -F: '($2~/^\$.+\$/) {if($5 > 365 || $5 < 1)print "User: " $1 "PASS_MAX_DAYS: " $5}' /etc/shadow
User: sharafPASS_MAX_DAYS: 99999

Both checks fail.

Current standards advise against mandatory periodic password expiration, recommending instead that passwords be changed only when there is evidence of compromise rather than on a fixed schedule. The benchmark’s requirement is followed here regardless, since this shift in guidance is not reflected in the CIS recommendation as written.

Only the sharaf account is affected. root has no password set, indicated by a ! in the second field of /etc/shadow. PASS_MAX_DAYS in /etc/login.defs is changed from 99999 to 365, which sets the default for any account created going forward.

The benchmark also warns that if a password was set at system install without populating the last password change date, applying PASS_MAX_DAYS retroactively can cause the password to expire immediately, and suggests setting that date explicitly as a precaution. The existing account is checked first:

# chage -l sharaf
Last password change                                    : Jun 27, 2026
Password expires                                        : never
Password inactive                                       : never
Account expires                                         : never
Minimum number of days between password change          : 0
Maximum number of days between password change          : 99999
Number of days of warning before password expires       : 7

The last password change date is already populated, so this warning does not apply. The existing account is then updated directly, since the /etc/login.defs default only applies to accounts created afterward:

# chage --maxdays 365 sharaf

The audit is run again:

# grep -Pi -- '^\h*PASS_MAX_DAYS\h+\d+\b' /etc/login.defs
PASS_MAX_DAYS   365
# awk -F: '($2~/^\$.+\$/) {if($5 > 365 || $5 < 1)print "User: " $1 "PASS_MAX_DAYS: " $5}' /etc/shadow

No output is returned from the second check. The recommendation now passes.

5.4.1.2: Ensure Minimum Password Days Is Configured #

# grep -Pi -- '^\h*PASS_MIN_DAYS\h+\d+\b' /etc/login.defs
PASS_MIN_DAYS   0
# awk -F: '($2~/^\$.+\$/) {if($4 < 1)print "User: " $1 " PASS_MIN_DAYS: "$4}' /etc/shadow
User: sharaf PASS_MIN_DAYS: 0

Both checks fail.

PASS_MIN_DAYS in /etc/login.defs is changed from 0 to 1, and the existing account is updated directly:

# chage --mindays 1 sharaf

The audit is run again:

# grep -Pi -- '^\h*PASS_MIN_DAYS\h+\d+\b' /etc/login.defs
PASS_MIN_DAYS   1
# awk -F: '($2~/^\$.+\$/) {if($4 < 1)print "User: " $1 " PASS_MIN_DAYS: "$4}' /etc/shadow

No output is returned from the second check. The recommendation now passes.

5.4.1.3: Ensure Password Expiration Warning Days Is Configured #

# grep -Pi -- '^\h*PASS_WARN_AGE\h+\d+\b' /etc/login.defs
PASS_WARN_AGE   7
# awk -F: '($2~/^\$.+\$/) {if($6 < 7)print "User: " $1 " PASS_WARN_AGE: "$6}' /etc/shadow

No output is returned from the second check. The recommendation passes without remediation.

5.4.1.4: Ensure Strong Password Hashing Algorithm Is Configured #

# grep -Pi -- '^\h*ENCRYPT_METHOD\h+(SHA512|YESCRYPT)\b' /etc/login.defs
ENCRYPT_METHOD YESCRYPT

The recommendation passes without remediation.

5.4.1.5: Ensure Inactive Password Lock Is Configured #

# useradd -D | grep INACTIVE
INACTIVE=-1

This does not meet the benchmark’s requirement.

# awk -F: '($2~/^\$.+\$/) {if($7 > 45 || $7 < 0)print "User: " $1 " INACTIVE: " $7 " Days"}' /etc/shadow
User: sharaf INACTIVE:  Days

The value printed between INACTIVE: and Days is blank rather than a number, since the corresponding field in /etc/shadow is itself empty:

# grep sharaf /etc/shadow
sharaf:<REDACTED>:20631:1:365:7:::

The default is set for future accounts, and the existing account is updated directly:

# useradd -D -f 45
# chage --inactive 45 sharaf

The audit is run again:

# useradd -D | grep INACTIVE
INACTIVE=45
# awk -F: '($2~/^\$.+\$/) {if($7 > 45 || $7 < 0)print "User: " $1 " INACTIVE: " $7 " Days"}' /etc/shadow

No output is returned from the second check. The recommendation now passes.

5.4.1.6: Ensure All Users Last Password Change Date Is in the Past #

#!/usr/bin/env bash
{
  while IFS= read -r l_user; do
    l_change=$(date -d "$(chage --list $l_user | grep '^Last password change' | cut -d: -f2 | grep -v 'never$')" +%s)
    if [[ "$l_change" -gt "$(date +%s)" ]]; then
      echo "User: \"$l_user\" last password change was \"$(chage --list $l_user | grep '^Last password change' | cut -d: -f2)\""
    fi
  done < <(awk -F: '$2~/^\$.+\$/{print $1}' /etc/shadow)
}

The script returns no output. The recommendation passes without remediation.

5.4.2: Configure Root and System Accounts and Environment #

This subsection covers 5.4.2.1 through 5.4.2.8. These recommendations concern the root account, system accounts, and their associated environment settings.

5.4.2.1: Ensure root Is the Only UID 0 Account #

# awk -F: '($3 == 0) { print $1 }' /etc/passwd
root

The recommendation passes without remediation.

5.4.2.2: Ensure root Is the Only GID 0 Account #

# awk -F: '($1 !~ /^(sync|shutdown|halt|operator)/ && $4=="0") {print $1":"$4}' /etc/passwd
root:0

The recommendation passes without remediation.

5.4.2.3: Ensure Group root Is the Only GID 0 Group #

# awk -F: '$3=="0"{print $1":"$3}' /etc/group
root:0

The recommendation passes without remediation.

5.4.2.4: Ensure root Account Access Is Controlled #

# passwd -S root | awk '$2 ~ /^(P|L)/ {print "User: \"" $1 "\" Password is status: " $2}'
User: "root" Password is status: L

The recommendation passes without remediation.

5.4.2.5: Ensure root Path Integrity #

#!/usr/bin/env bash
{
  a_output2=() l_pmask="0022"
  l_maxperm="$( printf '%o' $(( 0777 & ~$l_pmask )) )"
  l_root_path="$(sudo su - root -c env | awk -F= '$1=="PATH"{print $2}')"
  IFS=":" read -ra a_path_loc <<< "$l_root_path"
  grep -q -- "::" <<< "$l_root_path" && \
  a_output2+=(" - root's path contains a empty directory (::)")
  grep -Pq -- ":\h*$" <<< "$l_root_path" && \
  a_output2+=(" - root's path contains a trailing (:)")
  grep -Pq -- '(^\h*|:)\.(:|\h*$)' <<< "$l_root_path" && \
  a_output2+=(" - root's path contains current working directory (.)")
  for l_path in "${a_path_loc[@]}"; do
    if [ -d "$l_path" ]; then
      while IFS=: read -r l_fmode l_fown; do
        [ "$l_fown" != "root" ] && \
        a_output2+=(" - Directory: \"$l_path\" is owned by: \"$l_fown\"" \
        " should be owned by \"root\"")
        [ $(( $l_fmode & $l_pmask )) -gt 0 ] && \
        a_output2+=(" - Directory: \"$l_path\" is mode: \"$l_fmode\"" \
        " and should be mode: \"$l_maxperm\" or more restrictive")
      done <<< "$(stat -Lc '%#a:%U' "$l_path")"
    else
      a_output2+=(" - \"$l_path\" is not a directory")
    fi
  done
  if [ "${#a_output2[@]}" -le 0 ]; then
    printf '%s\n' "" "- Audit Result:" " ** PASS **" \
    " - Root's path is correctly configured"
  else
    printf '%s\n' "" "- Audit Result:" " ** FAIL **" \
    "- * Reasons for audit failure * :" "${a_output2[@]}"
  fi
}
- Audit Result:
 ** PASS **
 - Root's path is correctly configured

The recommendation passes without remediation.

5.4.2.6: Ensure root User umask Is Configured #

# grep -Psi -- '^\h*umask\h+((\d{1,2}(\d[^7]|[^2-7]\d)\b)|(u=[rwx]{1,3},)?(((g=[rx]?[rx]?w[rx]?[rx]?\b)(,o=[rwx]{1,3})?)|((g=[wrx]{1,3},)?o=[wrx]{1,3}\b)))' /root/.profile /root/.bashrc

No output is returned. The recommendation passes without remediation.

5.4.2.7: Ensure System Accounts Do Not Have a Valid Login Shell #

#!/usr/bin/env bash
{
  l_valid_shells="^($(awk -F\/ '$NF != "nologin" {print}' /etc/shells | sed -rn '/^\//{s,/,\\\\/,g;p}' | paste -s -d '|' - ))$"
  awk -v pat="$l_valid_shells" -F: '($1!~/^(root|halt|sync|shutdown|nfsnobody)$/ && ($3<'"$(awk '/^\s*UID_MIN/{print $2}' /etc/login.defs)"' || $3 == 65534) && $(NF) ~ pat) {print "Service account: \"" $1 "\" has a valid shell: " $7}' /etc/passwd
}

The script returns no output. The recommendation passes without remediation. This can also be confirmed directly by inspecting /etc/passwd.

5.4.2.8: Ensure Accounts Without a Valid Login Shell Are Locked #

#!/usr/bin/env bash
{
  l_valid_shells="^($(awk -F\/ '$NF != "nologin" {print}' /etc/shells | sed -rn '/^\//{s,/,\\\\/,g;p}' | paste -s -d '|' - ))$"
  while IFS= read -r l_user; do
    passwd -S "$l_user" | awk '$2 !~ /^L/ {print "Account: \"" $1 "\" does not have a valid login shell and is not locked"}'
  done < <(awk -v pat="$l_valid_shells" -F: '($1 != "root" && $(NF) !~ pat) {print $1}' /etc/passwd)
}

The script returns no output. The recommendation passes without remediation.

5.4.3: Configure User Default Environment #

This subsection covers 5.4.3.1 through 5.4.3.3. These recommendations concern shell configuration and default settings applied to interactive user sessions.

5.4.3.1: Ensure nologin Is Not Listed in /etc/shells #

This is a Level 2 recommendation, deferred to a future Level 2 pass. The recommendation passes regardless.

5.4.3.2: Ensure Default User Shell Timeout Is Configured #

#!/usr/bin/env bash
{
  a_output=(); a_output2=(); l_tmout_set="900"
  f_tmout_read_chk()
  {
    a_out=(); a_out2=()
    l_tmout_readonly="$(grep -P -- '^\h*(typeset\h\-xr\hTMOUT=\d+|([^#\n\r]+)?\breadonly\h+TMOUT\b)' "$l_file")"
    l_tmout_export="$(grep -P -- '^\h*(typeset\h\-xr\hTMOUT=\d+|([^#\n\r]+)?\bexport\b([^#\n\r]+\b)?TMOUT\b)' "$l_file")"
    if [ -n "$l_tmout_readonly" ]; then
      a_out+=(" - Readonly is set as: \"$l_tmout_readonly\" in: \"$l_file\"")
    else
      a_out2+=(" - Readonly is not set in: \"$l_file\"")
    fi
    if [ -n "$l_tmout_export" ]; then
      a_out+=(" - Export is set as: \"$l_tmout_export\" in: \"$l_file\"")
    else
      a_out2+=(" - Export is not set in: \"$l_file\"")
    fi
  }
  while IFS= read -r l_file; do
    l_tmout_value="$(grep -Po -- '^([^#\n\r]+)?\bTMOUT=\d+\b' "$l_file" | awk -F= '{print $2}')"
    f_tmout_read_chk
    if [ -n "$l_tmout_value" ]; then
      if [[ "$l_tmout_value" -le "$l_tmout_set" && "$l_tmout_value" -gt "0" ]];
      then
        a_output+=(" - TMOUT is set to: \"$l_tmout_value\" in: \"$l_file\"")
        [ "${#a_out[@]}" -gt 0 ] && a_output+=("${a_out[@]}")
        [ "${#a_out2[@]}" -gt 0 ] && a_output2+=("${a_out[@]}")
      fi
      if [[ "$l_tmout_value" -gt "$l_tmout_set" || "$l_tmout_value" -le "0" ]];
      then
        a_output2+=(" - TMOUT is incorrectly set to: \"$l_tmout_value\" in: \"$l_file\"")
        [ "${#a_out[@]}" -gt 0 ] && a_output2+=(" ** Incorrect TMOUT value **" "${a_out[@]}")
        [ "${#a_out2[@]}" -gt 0 ] && a_output2+=("${a_out2[@]}")
      fi
    else
      [ "${#a_out[@]}" -gt 0 ] && a_output2+=(" - TMOUT is not set" "${a_out[@]}")
      [ "${#a_out2[@]}" -gt 0 ] && a_output2+=(" - TMOUT is not set" "${a_out2[@]}")
    fi
  done < <(grep -Pls -- '^([^#\n\r]+)?\bTMOUT\b' /etc/*bashrc /etc/profile /etc/profile.d/*.sh)
  [[ "${#a_output[@]}" -le 0 && "${#a_output2[@]}" -le 0 ]] && a_output2+=(" - TMOUT is not configured")
  if [ "${#a_output2[@]}" -le 0 ]; then
    printf '%s\n' "" "- Audit Result:" " ** PASS **" "${a_output[@]}"
  else
    printf '%s\n' "" "- Audit Result:" " ** FAIL **" " * Reasons for audit failure *" "${a_output2[@]}" ""
    [ "${#a_output[@]}" -gt 0 ] && printf '%s\n' "- Correctly set:" "${a_output[@]}"
  fi
}
- Audit Result:
 ** FAIL **
 * Reasons for audit failure *
 - TMOUT is not configured

TMOUT is not set in any of the shell configuration files the benchmark checks. An additional check, not part of the benchmark’s own audit script, confirms it is absent elsewhere as well:

# grep -rn "TMOUT" /etc/profile /etc/profile.d/

No output is returned. The following is added to a new file, following the 60-cis-hardening naming convention already used elsewhere in this pass:

# printf '%s\n' "# Set TMOUT to 900 seconds" "typeset -xr TMOUT=900" > /etc/profile.d/60-cis-hardening.sh

The audit is run again:

- Audit Result:
 ** PASS **
 - TMOUT is set to: "900" in: "/etc/profile.d/60-cis-hardening.sh"
 - Readonly is set as: "typeset -xr TMOUT=900" in: "/etc/profile.d/60-cis-hardening.sh"
 - Export is set as: "typeset -xr TMOUT=900" in: "/etc/profile.d/60-cis-hardening.sh"

The recommendation now passes.

5.4.3.3: Ensure Default User umask Is Configured #

# grep -Psi -- '^\h*umask\b' /etc/profile.d/*.sh
# grep -Psi -- '^\h*UMASK\b' /etc/login.defs

No output is returned from either check.

Even though umask is already confirmed absent from /etc/profile.d, the following script is run as a precaution before adding the setting:

#!/usr/bin/env bash
{
  while IFS= read -r -d $'\0' l_file; do
    sed -ri '/^\s*umask\s+0?(0[01][0-7]|0[0-7][^7]|[^0][0-7][0-7])(\s*|\s+.*)$/s/^/# /' "$l_file"
  done < <(find /etc/profile.d/ -type f -name '*.sh' -print0)
}

umask 0027 is then added to /etc/profile.d/60-cis-hardening.sh, and UMASK 027 is added to the end of /etc/login.defs.

The audit is run again:

# grep -Psi -- '^\h*umask\b' /etc/profile.d/*.sh
/etc/profile.d/60-cis-hardening.sh:umask 0027
# grep -Psi -- '^\h*UMASK\b' /etc/login.defs
UMASK 027

The recommendation now passes.

Running Lynis again after these changes shows the hardening index increasing from 69 to 72:

================================================================================

  Lynis security scan details:

  Scan mode:
  Normal [▆]  Forensics [ ]  Integration [ ]  Pentest [ ]

  Lynis modules:
  - Compliance status      [?]
  - Security audit         [V]
  - Vulnerability scan     [V]

  Details:
  Hardening index : 72 [##############      ]
  Tests performed : 275
  Plugins enabled : 2

  Software components:
  - Firewall               [V]
  - Intrusion software     [X]
  - Malware scanner        [X]

  Files:
  - Test and debug information      : /var/log/lynis.log
  - Report data                     : /var/log/lynis-report.dat

================================================================================