CIS Hardening of a Debian Linux Server Part 9: Level 2 Services to Access Control

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

This pass covers Level 2 recommendations that were deferred in the Level 1 pass (Services, Network, Host Based Firewall, Access Control).

Services #

This section covers the single Level 2 recommendation deferred from the earlier services pass, 2.1.21.

2.1.21 Ensure X Window Server Services Are Not In Use #

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

No output is returned. xserver-common is not installed, and the recommendation passes.

Network #

This section covers 3.3.1.1, the single Level 2 network recommendation deferred from the earlier network pass.

3.3.1.1 Ensure net.ipv4.ip_forward Is Configured #

# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0
#!/usr/bin/env bash
{
  l_parameter_name="net.ipv4.ip_forward"
  l_grep="${l_parameter_name//./(\\.|\\/)}" a_output=() a_files=()
  l_systemdsysctl="$(readlink -e /lib/systemd/systemd-sysctl \
  || readlink -e /usr/lib/systemd/systemd-sysctl)"
  l_ufw_file="$([ -f /etc/default/ufw ] && \
  awk -F= '/^\s*IPT_SYSCTL=/ {print $2}' /etc/default/ufw)"
  [ -f "$(readlink -e "$l_ufw_file")" ] && \
  a_files+=("$l_ufw_file"); a_files+=("/etc/sysctl.conf")
  while IFS= read -r l_fname; do
    l_file="$(readlink -e "${l_fname//# /}")"
    [ -n "$l_file" ] && ! grep -Psiq -- '(^|\h+)'"$l_file"'\b' \
    <<< "${a_files[*]}" && a_files+=("$l_file")
  done < <("$l_systemdsysctl" --cat-config | tac | \
  grep -Psio -- '^\h*#\h*\/[^#\n\r\h]+\.conf\b')
  for l_file in "${a_files[@]}"; do
    l_opt="$(grep -Psio '^\h*'"$l_grep"'\h*=\h*\H+\b' "$l_file" | tail -n 1)"
    l_option_value="$(cut -d= -f2 <<< "$l_opt" | xargs)"
    [ -n "$l_option_value" ] && \
    a_output+=(" - \"$l_parameter_name = $l_option_value\" is set in: \"$l_file\"")
  done
  [ "${#a_output[@]}" -gt "0" ] && printf '%s\n' "" "${a_output[@]}" ""
}

This produces no output.

Remediation:

# printf '%s\n' "" "net.ipv4.ip_forward = 0" >> /etc/sysctl.d/61-ipv4_sysctl.conf
# sysctl --system
* Applying /usr/lib/sysctl.d/10-coredump-debian.conf ...
* Applying /usr/lib/sysctl.d/50-default.conf ...
* Applying /usr/lib/sysctl.d/50-pid-max.conf ...
* Applying /etc/sysctl.d/60-kernel_sysctl.conf ...
* Applying /etc/sysctl.d/61-ipv4_sysctl.conf ...
* Applying /etc/sysctl.d/61-ipv6_sysctl.conf ...
kernel.core_pattern = core
kernel.sysrq = 0x01b6
kernel.core_uses_pid = 1
net.ipv4.conf.eth0.rp_filter = 2
net.ipv4.conf.lo.rp_filter = 2
...

Running the audit script again confirms the fix:

 - "net.ipv4.ip_forward = 0" is set in: "/etc/sysctl.d/61-ipv4_sysctl.conf"

The recommendation passes.

Host Based Firewall #

This section covers 4.1.4, the single Level 2 firewall recommendation deferred from the earlier firewall pass.

4.1.4 Ensure ufw Outgoing Default Is Configured #

The output of ufw status verbose shows the outgoing default policy set to allow. Before switching the default to deny, explicit allow rules are added for the outbound traffic the server depends on:

# ufw allow out http
ufw allow out https
ufw allow out ntp
ufw allow out to any port 53    # DNS
ufw allow out to any port 853   # DNS over TLS
Rule added
Rule added (v6)
Rule added
Rule added (v6)
Rule added
Rule added (v6)
Rule added
Rule added (v6)
Rule added
Rule added (v6)

The outgoing default is then changed to deny:

# ufw default deny outgoing
Default outgoing policy changed to 'deny'
(be sure to update your rules accordingly)

Access Control #

This section covers 5.1.8, 5.1.9, 5.2.4, 5.3.3.1.3, and 5.4.3.1, the Level 2 access control recommendations deferred from the earlier access control pass.

5.1.8 Ensure sshd DisableForwarding Is Enabled #

# sshd -T | grep disableforwarding
disableforwarding no

The recommendation fails. The expected value is yes.

DisableForwarding yes is added to /etc/ssh/sshd_config, immediately before the Include directive. The full hardening block becomes:

# CIS hardening modifs
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
MaxStartups 10:30:60
DisableForwarding yes
# sshd -T | grep disableforwarding
disableforwarding yes

The recommendation passes.

5.1.9 Ensure sshd GSSAPIAuthentication Is Disabled #

# sshd -T | grep gssapiauthentication
gssapiauthentication no

The recommendation passes with no changes required.

5.2.4 Ensure Users Must Provide Password For Escalation #

# grep -r "^[^#].*NOPASSWD" /etc/sudoers*
/etc/sudoers.d/90-cloud-init-users:root ALL=(ALL) NOPASSWD:ALL

A matching line is found, so the recommendation fails.

# cat /etc/sudoers.d/90-cloud-init-users
# Created by cloud-init v. 25.1.4 on Sat, 27 Jun 2026 11:38:56 +0000

# User rules for root
root ALL=(ALL) NOPASSWD:ALL

This line allows root to use sudo without a password. Root, however, does not need sudo to act as root, so the rule grants no privilege that root does not already hold, and removing it carries no functional risk. The line is removed with visudo -f:

# visudo -f /etc/sudoers.d/90-cloud-init-users
# cat /etc/sudoers.d/90-cloud-init-users
# Created by cloud-init v. 25.1.4 on Sat, 27 Jun 2026 11:38:56 +0000

# User rules for root
# grep -r "^[^#].*NOPASSWD" /etc/sudoers*

No output is returned. The recommendation passes.

5.3.3.1.3 Ensure Password Failed Attempts Lockout Includes Root Account #

# grep -Pi -- '^\h*(even_deny_root|root_unlock_time\h*=\h*\d+)\b' /etc/security/faillock.conf

No output is returned, so the recommendation fails.

In /etc/security/faillock.conf, the even_deny_root line is uncommented, and the root_unlock_time line is uncommented and changed from 900 to 90.

Running the audit again:

# grep -Pi -- '^\h*(even_deny_root|root_unlock_time\h*=\h*\d+)\b' /etc/security/faillock.conf
even_deny_root
root_unlock_time = 90

A secondary check confirms the unlock time is not left below the minimum threshold:

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

Both return no output. The recommendation passes.

5.4.3.1 Ensure nologin Is Not Listed In /etc/shells #

# grep -Ps '^\h*([^#\n\r]+)?\/nologin\b' /etc/shells

No output is returned. The recommendation passes.

A Lynis run at the end of this pass produces the following:

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

  Lynis security scan details:

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

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

  Details:
  Hardening index : 78 [###############     ]
  Tests performed : 278
  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

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

The hardening index remains at 78, unchanged from the previous audit.