CIS Hardening of a Debian Linux Server Part 2: Level 1 Services

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

This section covers 2.1 through 2.4 of the benchmark. This is a Level 1 pass. Level 2 recommendations are deferred.

2.1: Configure Server Services #

This section covers 2.1.1 through 2.1.23. These recommendations check that services with no clear operational need on this system are neither installed nor listening, on the reasoning that any running service expands the attack surface.

2.1.1: Ensure Autofs Services Are Not in Use #

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

No output is returned. autofs is not installed.

2.1.2: Ensure Avahi Daemon Services Are Not in Use #

# dpkg-query -s avahi-daemon &>/dev/null && echo "avahi-daemon is installed"

No output. avahi-daemon is not installed.

2.1.3: Ensure DHCP Server Services Are Not in Use #

Kea is the ISC’s (Internet Systems Consortium) current DHCP server, intended as a replacement for the older isc-dhcp-server.

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

No output. Kea is not installed.

2.1.4: Ensure DNS Server Services Are Not in Use #

Bind9, also from ISC, is the DNS server checked here. It has had recent CVEs.

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

No output. Not installed.

2.1.5: Ensure Dnsmasq Services Are Not in Use #

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

No output. Not installed.

2.1.6: Ensure FTP Server Services Are Not in Use #

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

No output. Not installed.

The benchmark notes that other FTP server packages may exist and should be audited if not required and authorized by local site policy:

# dpkg -l | grep -Ei 'ftp'
ii  openssh-sftp-server                   1:10.0p1-7+deb13u4                   amd64        secure shell (SSH) sftp server module, for SFTP access from remote machines

The only match is an SSH module, which is an authorized access path rather than an FTP server. This is a clean result.

2.1.7: Ensure LDAP Server Services Are Not in Use #

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

No output. Not installed.

2.1.8: Ensure Message Access Server Services Are Not in Use #

# dpkg-query -s dovecot-imapd &>/dev/null && echo "dovecot-imapd is installed"
# dpkg-query -s dovecot-pop3d &>/dev/null && echo "dovecot-pop3d is installed"

No output from either. dovecot-imapd and dovecot-pop3d are not installed.

As with the FTP recommendation, the benchmark notes that other message access server packages may exist:

# dpkg -l | grep -Ei 'dovecot|courier-(imap|pop)|cyrus-imapd|uw-imap|qpopper|dbmail'

No output. Nothing further to remediate.

2.1.9: Ensure Network File System Services Are Not in Use #

# dpkg-query -s nfs-kernel-server &>/dev/null && echo "nfs-kernel-server is installed"

No output. Not installed.

2.1.10: Ensure NIS Server Services Are Not in Use #

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

No output. Not installed.

2.1.11: Ensure Print Server Services Are Not in Use #

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

No output. Not installed.

2.1.12: Ensure Rpcbind Services Are Not in Use #

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

No output. Not installed.

2.1.13: Ensure Rsync Services Are Not in Use #

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

Unlike the preceding recommendations, rsync is present and requires remediation.

# systemctl is-enabled rsync.service 2>/dev/null | grep 'enabled'
# systemctl is-active rsync.service 2>/dev/null | grep '^active'

Neither command returns output. The service is not enabled and not active.

Before removal, a check is made for what pulled the package in and what, if anything, depends on it:

# apt-mark showmanual | grep -i rsync
rsync
# apt-mark showauto | grep -i rsync
# aptitude why rsync
i   git              Suggests   git-svn
p   git-svn          Suggests   subversion
p   subversion       Suggests   subversion-tools
p   subversion-tools Recommends rsync

rsync shows up under apt-mark showmanual, meaning it was installed directly rather than pulled in as a dependency, and apt-mark showauto returns nothing, confirming this. The aptitude why output shows only Suggests and Recommends relationships against packages that are themselves not installed (p prefix), with no hard Depends in the chain. Nothing on the system requires rsync to remain, so it is safe to remove.

# systemctl stop rsync.service
# apt purge rsync
REMOVING:
  rsync*

Summary:
  Upgrading: 0, Installing: 0, Removing: 1, Not Upgrading: 0
  Freed space: 850 kB

Continue? [Y/n] y
(Reading database ... 39255 files and directories currently installed.)
Removing rsync (3.4.1+ds1-5+deb13u3) ...
Processing triggers for man-db (2.13.1-1) ...
(Reading database ... 39223 files and directories currently installed.)
Purging configuration files for rsync (3.4.1+ds1-5+deb13u3) ...

The systemctl stop command is run as a precaution even though the service was already confirmed inactive.

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

No output. rsync is no longer installed.

2.1.14: Ensure Samba File Server Services Are Not in Use #

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

Not installed.

2.1.15: Ensure SNMP Services Are Not in Use #

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

Not installed.

2.1.16: Ensure Telnet-Server Services Are Not in Use #

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

Not installed.

2.1.17: Ensure TFTP Server Services Are Not in Use #

# dpkg-query -s tftpd-hpa &>/dev/null && echo "tftpd-hpa is installed"

Not installed.

2.1.18: Ensure Web Proxy Server Services Are Not in Use #

A decades-old vulnerability in Squid was disclosed recently, which makes this check more relevant than usual.

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

Not installed.

2.1.19: Ensure Web Server Services Are Not in Use #

# dpkg-query -s apache2 &>/dev/null && echo "apache2 is installed"
# dpkg-query -s nginx &>/dev/null && echo "nginx is installed"

Neither apache2 nor nginx is installed.

A check for less common web servers is also run, per the benchmark’s note about auditing alternative packages:

# dpkg -l | grep -Ei 'lighttpd|caddy|hiawatha|webfs|darkhttpd|thttpd|monkey|boa\b'

No output.

2.1.20: Ensure Xinetd Services Are Not in Use #

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

Not installed.

2.1.21: Ensure X Window Server Services Are Not in Use #

Level 2, deferred. The recommendation appears to pass the audit regardless:

# dpkg-query -s xserver-common &>/dev/null && echo "xserver-common is installed"

No output.

2.1.22: Ensure Mail Transfer Agents Are Configured for Local-Only Mode #

#!/usr/bin/env bash
{
  a_output=(); a_output2=(); a_port_list=("25" "465" "587")
  for l_port_number in "${a_port_list[@]}"; do
    if ss -plntu | grep -P -- ':'"$l_port_number"'\b' | grep -Pvq -- '\h+(127\.0\.0\.1|\[?::1\]?):'"$l_port_number"'\b'; then
      a_output2+=(" - Port \"$l_port_number\" is listening on a non- loopback network interface")
    else
      a_output+=(" - Port \"$l_port_number\" is not listening on a non- loopback network interface")
      fi
  done
  if command -v postconf &> /dev/null; then
    l_interfaces="$(postconf -n inet_interfaces)"
  elif command -v exim &> /dev/null; then
    l_interfaces="$(exim -bP local_interfaces)"
  elif command -v sendmail &> /dev/null; then
    l_interfaces="$(grep -i "O DaemonPortOptions=" /etc/mail/sendmail.cf |
    grep -oP '(?<=Addr=)[^,+]+' | grep -v '^127\.0\.0\.1$')"
  fi
  if [ -n "$l_interfaces" ]; then
    if grep -Pqi '\ball\b' <<< "$l_interfaces"; then
      a_output2+=(" - MTA is bound to all network interfaces")
    elif ! grep -Pqi '(inet_interfaces\h*=\h*)?(0\.0\.0\.0|::1|loopback- only)' <<< "$l_interfaces"; then
      a_output2+=(" - MTA is bound to a network interface" " \"$l_interfaces\"")
    else
      a_output+=(" - MTA is not bound to a non loopback network interface" " \"$l_interfaces\"")
    fi
  else
    a_output+=(" - MTA not detected or in use")
  fi
  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:
 ** PASS **
 - Port "25" is not listening on a non- loopback network interface
 - Port "465" is not listening on a non- loopback network interface
 - Port "587" is not listening on a non- loopback network interface
 - MTA not detected or in use

Pass. The benchmark notes that this recommendation is built around Postfix specifically, and that an alternative MTA such as Exim4 or Sendmail would need to be configured according to its own documentation. Since the audit script itself already accounts for the alternatives when checking, this only affects remediation guidance, not the result here.

2.1.23: Ensure Only Approved Services Are Listening on a Network Interface #

# ss -plntu
Netid    State     Recv-Q    Send-Q                          Local Address:Port        Peer Address:Port    Process
udp      UNCONN    0         0                                 <REDACTED>:68               0.0.0.0:*        users:(("dhcpcd",pid=665,fd=3))
udp      UNCONN    0         0                                 <REDACTED>:546                 [::]:*        users:(("dhcpcd",pid=770,fd=3))
udp      UNCONN    0         0                                 <REDACTED>:546                 [::]:*        users:(("dhcpcd",pid=640,fd=3))
tcp      LISTEN    0         128                                   0.0.0.0:22               0.0.0.0:*        users:(("sshd",pid=906,fd=6))
tcp      LISTEN    0         128                                      [::]:22                  [::]:*        users:(("sshd",pid=906,fd=7))

Two processes are listening: dhcpcd on the DHCP client ports, and sshd on port 22, which is the approved management access path.

2.2: Configure Client Services #

This section covers 2.2.1 through 2.2.6.

2.2.1: Ensure NIS Client Is Not Installed #

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

No output. Not installed.

2.2.2: Ensure RSH Client Is Not Installed #

# dpkg-query -s rsh-client &>/dev/null && echo "rsh-client is installed"

Not installed.

2.2.3: Ensure Talk Client Is Not Installed #

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

No output. Not installed.

2.2.4: Ensure Telnet Client Is Not Installed #

# dpkg-query -l | grep -E 'telnet|inetutils-telnet' &>/dev/null && echo "telnet is installed"
telnet is installed

inetutils-telnet is present and needs to be removed. As with rsync, the dependency chain is checked first:

# aptitude why inetutils-telnet
i   grub-common     Suggests desktop-base (>= 4.0.6)
p   desktop-base    Suggests gnome | kde-standard | xfce4 | wmaker
p   wmaker          Suggests x-terminal-emulator
p   deepin-terminal Provides x-terminal-emulator
p   deepin-terminal Depends  zssh
p   zssh            Depends  openssh-client | telnet | telnet-client
p   telnet          Depends  inetutils-telnet
# apt-mark showmanual | grep -i telnet
inetutils-telnet
# apt-mark showauto | grep -i telnet

Every package in the chain above inetutils-telnet other than grub-common itself is marked p (purged, i.e. not actually present), so the Depends relationship further down the chain does not apply to any package that is actually installed. apt-mark showmanual confirms inetutils-telnet was installed directly, and apt-mark showauto returns nothing, so nothing on the system depends on it. It is safe to remove.

# apt purge inetutils-telnet
REMOVING:
  inetutils-telnet*

Summary:
  Upgrading: 0, Installing: 0, Removing: 1, Not Upgrading: 0
  Freed space: 286 kB

Continue? [Y/n] y
(Reading database ... 39221 files and directories currently installed.)
Removing inetutils-telnet (2:2.6-3+deb13u3) ...
Processing triggers for man-db (2.13.1-1) ...
# dpkg-query -l | grep -E 'telnet|inetutils-telnet' &>/dev/null && echo "telnet is installed"

No output. inetutils-telnet is no longer installed.

2.2.5: Ensure LDAP Client Is Not Installed #

# dpkg-query -s ldap-utils &>/dev/null && echo "ldap-utils is installed"

No output. Not installed.

2.2.6: Ensure FTP Client Is Not Installed #

# dpkg-query -l | awk '{print $2}' 2>&1 | grep -Pqs -- '^(ftp|tnftp)' && echo "ftp is installed"

No output. Not installed.

2.3: Configure Time Synchronization #

This section covers 2.3.1 through 2.3.3.

2.3.1: Ensure Time Synchronization Is in Use #

The benchmark treats systemd-timesyncd and chrony as the two acceptable time synchronization daemons and requires that exactly one of them be in use.

2.3.1.1: Ensure a Single Time Synchronization Daemon Is in Use #

The benchmark notes that on virtualized systems with host-based time synchronization available, this section should be skipped in favor of the hypervisor’s own mechanism. A check is made first for that possibility:

# cat /sys/devices/system/clocksource/clocksource0/current_clocksource
kvm-clock
# ls /dev/ptp* 2>/dev/null
# grep -i ptp /etc/chrony/chrony.conf 2>/dev/null

No PTP device and no PTP-related chrony configuration are present, indicating host-based time synchronization is not in use here.

#!/usr/bin/env bash
{
  l_output="" l_output2=""
  service_not_enabled_chk()
  {
    l_out2=""
    if systemctl is-enabled "$l_service_name" 2>/dev/null | grep -q 'enabled'; then
      l_out2="$l_out2\n - Daemon: \"$l_service_name\" is enabled on the system"
    fi
    if systemctl is-active "$l_service_name" 2>/dev/null | grep -q '^active'; then
      l_out2="$l_out2\n - Daemon: \"$l_service_name\" is active on the system"
    fi
  }
  l_service_name="systemd-timesyncd.service" # Check systemd-timesyncd daemon
  service_not_enabled_chk
  if [ -n "$l_out2" ]; then
    l_timesyncd="y"
    l_out_tsd="$l_out2"
  else
    l_timesyncd="n"
    l_out_tsd="\n - Daemon: \"$l_service_name\" is not enabled and not active on the system"
  fi
  l_service_name="chrony.service" # Check chrony
  service_not_enabled_chk
  if [ -n "$l_out2" ]; then
    l_chrony="y"
    l_out_chrony="$l_out2"
  else
    l_chrony="n"
    l_out_chrony="\n - Daemon: \"$l_service_name\" is not enabled and not active on the system"
  fi
  l_status="$l_timesyncd$l_chrony"
  case "$l_status" in
    yy)
      l_output2=" - More than one time sync daemon is in use on the system$l_out_tsd$l_out_chrony"
      ;;
    nn)
      l_output2=" - No time sync daemon is in use on the system$l_out_tsd$l_out_chrony"
      ;;
    yn|ny)
      l_output=" - Only one time sync daemon is in use on the system$l_out_tsd$l_out_chrony"
      ;;
    *)
      l_output2=" - Unable to determine time sync daemon(s) status"
      ;;
  esac
  if [ -z "$l_output2" ]; then
    echo -e "\n- Audit Result:\n ** PASS **\n$l_output\n"
  else
    echo -e "\n- Audit Result:\n ** FAIL **\n - * Reasons for audit failure * :\n$l_output2\n"
  fi
}
- Audit Result:
 ** PASS **
 - Only one time sync daemon is in use on the system
 - Daemon: "systemd-timesyncd.service" is enabled on the system
 - Daemon: "systemd-timesyncd.service" is active on the system
 - Daemon: "chrony.service" is not enabled and not active on the system

The benchmark presents systemd-timesyncd and chrony as equally valid choices for this recommendation, and the check above already confirms no host-based synchronization is in play. Chrony is a full implementation of NTP and offers some precision advantages, but with no concrete requirement pushing toward those extra features, the already-enabled, already-compliant default is left in place. The benchmark itself notes that only the subsection matching the daemon actually in use should be followed, with the other skipped.

# dpkg-query -s chrony
dpkg-query: package 'chrony' is not installed and no information is available
Use dpkg --info (= dpkg-deb --info) to examine archive files.

chrony is not installed, which is consistent with systemd-timesyncd being the sole daemon in use.

2.3.2: Configure systemd-timesyncd #

2.3.2.1: Ensure systemd-timesyncd Configured With Authorized Timeserver #

#!/usr/bin/env bash
{
  a_output=() a_output2=() a_output3=() a_out=() a_out2=() a_parlist=("NTP=[^#\n\r]+" "FallbackNTP=[^#\n\r]+")
  l_analyze_cmd="$(readlink -f /bin/systemd-analyze)"
  l_systemd_config_file="/etc/systemd/timesyncd.conf"
  f_config_file_parameter_chk()
  {
    l_used_parameter_setting=""
    while IFS= read -r l_file; do
      l_file="$(tr -d '# ' <<< "$l_file")"
      l_used_parameter_setting="$(grep -PHs -- '^\h*'"$l_parameter_name"'\b' "$l_file" | tail -n 1)"
      [ -n "$l_used_parameter_setting" ] && break
    done < <($l_analyze_cmd cat-config "$l_systemd_config_file" | tac | grep -Pio '^\h*#\h*\/[^#\n\r\h]+\.conf\b')
    if [ -n "$l_used_parameter_setting" ]; then
      while IFS=: read -r l_file_name l_file_parameter; do
        while IFS="=" read -r l_file_parameter_name l_file_parameter_value; do
          if grep -Pq -- "$l_parameter_value" <<< "$l_file_parameter_value"; then
            a_out+=(" - Parameter: \"${l_file_parameter_name// /}\"" \
            " correctly set to: \"${l_file_parameter_value// /}\"" \
            " in the file: \"$l_file_name\"")
          else
            a_out2+=(" - Parameter: \"${l_file_parameter_name// /}\"" \
            " incorrectly set to: \"${l_file_parameter_value// /}\"" \
            " in the file: \"$l_file_name\"" \
            " Should be set to: \"$l_value_out\"")
          fi
        done <<< "$l_file_parameter"
      done <<< "$l_used_parameter_setting"
    else
      a_out2+=(" - Parameter: \"$l_parameter_name\" is not set in an included file" \
      " *** Note: \"$l_parameter_name\" May be set in a file that's ignored by load procedure ***")
    fi
  }
  while IFS="=" read -r l_parameter_name l_parameter_value; do # Assess and check parameters
    l_parameter_name="${l_parameter_name// /}"; l_parameter_value="${l_parameter_value// /}"
    l_value_out="${l_parameter_value//-/ through }"; l_value_out="${l_value_out//|/ or }"
    l_value_out="$(tr -d '(){}' <<< "$l_value_out")"
    f_config_file_parameter_chk
  done < <(printf '%s\n' "${a_parlist[@]}")
  if [ "${#a_out[@]}" -gt 0 ]; then
    a_output+=("${a_out[@]}"); [ "${#a_out2[@]}" -gt 0 ] && a_output3+=(" ** INFO: **"
    "${a_out2[@]}")
  else
    a_output2+=("${a_out2[@]}")
  fi
  if [ "${#a_output2[@]}" -le 0 ]; then
    printf '%s\n' "" "- Audit Result:" " ** PASS **" "${a_output[@]}" ""
    [ "${#a_output3[@]}" -gt 0 ] && printf '%s\n' "${a_output3[@]}"
  else
    printf '%s\n' "" "- Audit Result:" " ** FAIL **" " - Reason(s) for audit failure:"
    "${a_output2[@]}"
    [ "${#a_output[@]}" -gt 0 ] && printf '%s\n' "" "- Correctly set:" "${a_output[@]}" ""
  fi
}
- Audit Result:
 ** PASS **
 - Parameter: "NTP"
 correctly set to: "ntp.hetzner.com"
 in the file: "/etc/systemd/timesyncd.conf.d/hetzner.conf"

 ** INFO: **
 - Parameter: "FallbackNTP" is not set in an included file
 *** Note: "FallbackNTP" May be set in a file that's ignored by load procedure ***

Pass, with FallbackNTP unset. The NTP parameter is set by Hetzner’s own configuration:

# cat /etc/systemd/timesyncd.conf.d/hetzner.conf
[Time]
NTP=ntp.hetzner.com

Since FallbackNTP is flagged as unset in any loaded file, it is addressed as well:

# grep -r FallbackNTP /etc/systemd
/etc/systemd/timesyncd.conf:#FallbackNTP=0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org

Rather than uncommenting the shipped default, a dedicated drop-in is created, following the convention used throughout this hardening pass, combining Hetzner’s NTP servers with the Debian defaults as fallbacks:

# cat /etc/systemd/timesyncd.conf.d/60-cis-hardening.conf
[Time]
FallbackNTP=ntp1.hetzner.de ntp2.hetzner.com ntp3.hetzner.net 0.debian.pool.ntp.org 1.debian.pool.ntp.org 2.debian.pool.ntp.org 3.debian.pool.ntp.org

Re-running the audit script confirms both parameters are now set:

- Audit Result:
 ** PASS **
 - Parameter: "NTP"
 correctly set to: "ntp.hetzner.com"
 in the file: "/etc/systemd/timesyncd.conf.d/hetzner.conf"
 - Parameter: "FallbackNTP"
 correctly set to: "ntp1.hetzner.dentp2.hetzner.comntp3.hetzner.net0.debian.pool.ntp.org1.debian.pool.ntp.org2.debian.pool.ntp.org3.debian.pool.ntp.org"
 in the file: "/etc/systemd/timesyncd.conf.d/60-cis-hardening.conf"

The configuration is loaded with:

# systemctl reload-or-restart systemd-timesyncd

2.3.2.2: Ensure systemd-timesyncd Is Enabled and Running #

# systemctl is-enabled systemd-timesyncd.service
enabled
# systemctl is-active systemd-timesyncd.service
active

Pass.

2.3.3: Configure Chrony #

Since systemd-timesyncd was confirmed as the sole time synchronization daemon in 2.3.1.1, and the benchmark instructs following only the subsection matching the daemon in use, the chrony subsection (2.3.3.1 to 2.3.3.3) does not apply here and is skipped.

2.4: Job Schedulers #

This section covers 2.4.1 and 2.4.2. Both cron and at are installed on this system, so both are configured rather than removed. The recommendations here focus on restricting who can use them and locking down permissions on their configuration paths.

2.4.1: Configure Cron #

# dpkg-query -s cron
Package: cron
Status: install ok installed
Priority: important
Section: admin
Installed-Size: 249
...

cron is installed.

2.4.1.1: Ensure Cron Daemon Is Enabled and Active #

# systemctl list-unit-files | awk '$1~/^crond?\.service/{print $2}'
enabled
# systemctl list-units | awk '$1~/^crond?\.service/{print $3}'
active

Pass.

2.4.1.2: Ensure Access to /etc/crontab Is Configured #

# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/crontab
Access: (644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)

Fails. Remediated, directly from the benchmark’s remediation command, even though only the read bits for group and other strictly needed removing:

# chmod og-rwx /etc/crontab
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/crontab
Access: (600/-rw-------) Uid: ( 0/ root) Gid: ( 0/ root)

Pass.

2.4.1.3: Ensure Access to /etc/cron.hourly Is Configured #

# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.hourly/
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)

Fails.

# chmod og-rwx /etc/cron.hourly/
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.hourly/
Access: (700/drwx------) Uid: ( 0/ root) Gid: ( 0/ root)

Pass. The execute bit is retained for the owner since this is a directory.

2.4.1.4: Ensure Access to /etc/cron.daily Is Configured #

# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.daily/
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)

Fails.

# chmod og-rwx /etc/cron.daily/
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.daily/
Access: (700/drwx------) Uid: ( 0/ root) Gid: ( 0/ root)

Pass.

2.4.1.5: Ensure Access to /etc/cron.weekly Is Configured #

Same process as 2.4.1.3 and 2.4.1.4.

# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.weekly/
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
# chmod og-rwx /etc/cron.weekly/
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.weekly/
Access: (700/drwx------) Uid: ( 0/ root) Gid: ( 0/ root)

Pass.

2.4.1.6: Ensure Access to /etc/cron.monthly Is Configured #

Same process.

# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.monthly/
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
# chmod og-rwx /etc/cron.monthly/
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.monthly/
Access: (700/drwx------) Uid: ( 0/ root) Gid: ( 0/ root)

Pass.

2.4.1.7: Ensure Access to /etc/cron.yearly Is Configured #

Same process.

# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.yearly/
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
# chmod og-rwx /etc/cron.yearly/
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.yearly/
Access: (700/drwx------) Uid: ( 0/ root) Gid: ( 0/ root)

Pass.

2.4.1.8: Ensure Access to /etc/cron.d Is Configured #

Same process.

# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.d/
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
# chmod og-rwx /etc/cron.d/
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/cron.d/
Access: (700/drwx------) Uid: ( 0/ root) Gid: ( 0/ root)

Pass.

2.4.1.9: Ensure Access to Crontab Is Configured #

# cat /etc/cron.allow
cat: /etc/cron.allow: No such file or directory
# cat /etc/cron.deny
cat: /etc/cron.deny: No such file or directory

Neither file exists. The remediation script creates /etc/cron.allow, sets ownership to root and either the root or crontab group depending on which exists, and locks down the mode to 640 or more restrictive:

#!/usr/bin/env bash
{
  [ ! -e "/etc/cron.allow" ] && touch /etc/cron.allow
    chmod u-x,g-wx,o-rwx /etc/cron.allow
  if grep -Pq -- '^\h*crontab\:' /etc/group; then
    chown root:crontab /etc/cron.allow
  else
    chown root:root /etc/cron.allow
  fi
}
# stat -Lc 'Access: (%a/%A) Owner: (%U) Group: (%G)' /etc/cron.allow
Access: (640/-rw-r-----) Owner: (root) Group: (crontab)

Pass. Going forward, a user must be listed in /etc/cron.allow to be permitted to use crontab.

2.4.2: Configure At #

# dpkg-query -s at
Package: at
Status: install ok installed
Priority: standard
Section: admin
Installed-Size: 158
...

at is installed.

2.4.2.1: Ensure Access to At Is Configured #

# stat -Lc 'Access: (%a/%A) Owner: (%U) Group: (%G)' /etc/at.allow
stat: cannot statx '/etc/at.allow': No such file or directory
# stat -Lc 'Access: (%a/%A) Owner: (%U) Group: (%G)' /etc/at.deny
Access: (640/-rw-r-----) Owner: (root) Group: (daemon)

/etc/at.allow does not exist, and /etc/at.deny exists with correct ownership and permissions already. The benchmark’s own reasoning favors an allow list over a deny list here, since a deny list depends on remembering to add every new user, whereas an allow list defaults to excluding anyone not explicitly listed. /etc/at.allow is created accordingly, using the same remediation script that also normalizes /etc/at.deny:

#!/usr/bin/env bash
{
  grep -Pq -- '^daemon\b' /etc/group && l_group="daemon" || l_group="root"
  [ ! -e "/etc/at.allow" ] && touch /etc/at.allow
  chown root:"$l_group" /etc/at.allow
  chmod u-x,g-wx,o-rwx /etc/at.allow
  [ -e "/etc/at.deny" ] && chown root:"$l_group" /etc/at.deny
  [ -e "/etc/at.deny" ] && chmod u-x,g-wx,o-rwx /etc/at.deny
}
# stat -Lc 'Access: (%a/%A) Owner: (%U) Group: (%G)' /etc/at.allow
Access: (640/-rw-r-----) Owner: (root) Group: (daemon)
# stat -Lc 'Access: (%a/%A) Owner: (%U) Group: (%G)' /etc/at.deny
Access: (640/-rw-r-----) Owner: (root) Group: (daemon)

Pass. With /etc/at.allow now present, /etc/at.deny is effectively ignored, which is itself a passing state.

With Section 2 complete, a Lynis rerun shows no change from the previous section: the hardening index remains at 68.