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
================================================================================