CIS Hardening of a Debian Linux Server Part 1: Level 1 Initial Setup
This is the second entry in the series documenting the Debian 13 server CIS hardening project.
This entry covers the benchmark’s Initial Setup section (1.1 through 1.7). Only Level 1 recommendations are addressed in this pass. Level 2 recommendations encountered along the way are noted and deferred to a later pass.
1.1: Filesystem #
1.1.1: Filesystem Kernel Modules #
This section covers 1.1.1.1 through 1.1.1.11 of the benchmark. These recommendations disable filesystem kernel modules that serve no purpose on a modern server: either legacy and niche filesystem formats, or removable storage buses that expand attack surface without operational benefit.
1.1.1.1 to 1.1.1.5: cramfs, freevxfs, hfs, hfsplus, jffs2 #
These five recommendations share an identical check script. Only the module name changes between runs.
#!/usr/bin/env bash
{
l_mod_name="cramfs" l_mod_type="fs"
while IFS= read -r l_mod_path; do
if [ -d "$l_mod_path/${l_mod_name//-/\/}" ] && \
[ -n "$(ls -A "$l_mod_path/${l_mod_name//-/\/}")" ]; then
printf '%s\n' "$l_mod_name exists in $l_mod_path"
fi
done < <(readlink -e /usr/lib/modules/**/kernel/$l_mod_type \
|| readlink -e /lib/modules/**/kernel/$l_mod_type)
}
Substituting l_mod_name for freevxfs, hfs, hfsplus, and jffs2 in turn,
every run returned nothing. None of these five filesystems ship as available
modules on this Debian 13 kernel. No remediation was required for any of them.
1.1.1.6 to 1.1.1.8: overlay, squashfs, udf (Level 2, deferred) #
These three recommendations are Level 2 and are deferred. overlay is required
by container runtimes, squashfs by snap, and udf is required by Microsoft
Azure. All three will be revisited during the Level 2 pass.
1.1.1.9 to 1.1.1.10: firewire-core, usb-storage #
Same check pattern as above, pointed at drivers instead of fs as the module
type. Neither firewire-core nor usb-storage returned as present on this
system. No remediation required.
1.1.1.11: Unused Filesystems #
Baseline of what is currently mounted:
# findmnt -Dkerno fstype | sort -u
devtmpfs
ext4
tmpfs
vfat
The benchmark’s audit script enumerates every filesystem module physically present under the kernel module tree:
#!/usr/bin/env bash
{
l_search="$(readlink -e /usr/lib/modules/ || readlink -e /lib/modules/)"
a_module=()
while IFS= read -r -d $'\0' l_module_dir; do
if [ ! "$(basename "$l_module_dir")" = "nls" ]; then
while IFS= read -r -d $'\0' l_module_file; do
l_mname="$(basename "$l_module_file" | cut -d'.' -f1)"
if [ -f "$l_module_file" ] && ! grep -Psiq -- '\b'"$l_mname"'\b' \
<<< "${a_module[*]}"; then
a_module+=("$l_mname")
fi
done < <(find -L "$l_module_dir" -mindepth 1 -maxdepth 1 -type f -print0)
fi
done < <(find "$l_search"/**/kernel/fs/ -mindepth 1 -maxdepth 1 -type d ! -empty -print0)
printf '%s\n' "" "${a_module[@]}"
}
Cross-referencing that list against lsmod:
# ./generate-fs-km-list > ~/outputs/fs-KMs/generated-fs-km-list.txt
# lsmod | awk 'NR>1 {print $1}' > ~/outputs/fs-KMs/lsmod-list-1st-column.txt
# comm -12 <(sort lsmod-list-1st-column.txt) <(sort generated-fs-km-list.txt)
autofs4
btrfs
configfs
efivarfs
vfat
None of these five are self-evidently necessary merely by virtue of being loaded. Each is examined individually below.
Default blacklist entries already present on this image:
# modprobe --showconfig | grep -Pi -- '^\h*(blacklist|install)\h+'
blacklist arkfb
blacklist aty128fb
blacklist atyfb
blacklist radeonfb
blacklist cirrusfb
blacklist cyber2000fb
blacklist kyrofb
blacklist matroxfb_base
blacklist mb862xxfb
blacklist neofb
blacklist pm2fb
blacklist pm3fb
blacklist s3fb
blacklist savagefb
blacklist sisfb
blacklist tdfxfb
blacklist tridentfb
blacklist vt8623fb
All framebuffer drivers. Not relevant to this recommendation.
The full audit script sorts every discovered filesystem module into three categories: currently mounted, currently loaded, and merely loadable.
#!/usr/bin/env bash
{
a_check=() a_output=() a_module=() a_output2=() a_output3=()
l_search="$(readlink -e /usr/lib/modules/ || readlink -e /lib/modules/)"
IFS=$'\n' read -r -d '' -a a_mounted < <(findmnt -Dkerno fstype \
| sort -u && printf '\0' )
IFS=$'\n' read -r -d '' -a a_lsmod < <(lsmod | awk '{print $1}' && printf '\0' )
IFS=$'\n' read -r -d '' -a a_showconfig < <(modprobe --showconfig | \
grep -Pi -- '^\h*(blacklist|install)\h+' && printf '\0')
while IFS= read -r -d $'\0' l_module_dir; do
if [ ! "$(basename "$l_module_dir")" = "nls" ]; then
while IFS= read -r -d $'\0' l_module_file; do
l_mname="$(basename "$l_module_file" | cut -d'.' -f1)"
if [ -f "$l_module_file" ] && ! grep -Psiq -- '\b'"$l_mname"'\b' <<< "${a_module[*]}"; then
a_module+=("$l_mname")
fi
done < <(find -L "$l_module_dir" -mindepth 1 -maxdepth 1 -type f -print0)
fi
done < <(find "$l_search"/**/kernel/fs/ -mindepth 1 -maxdepth 1 -type d ! -empty -print0)
for l_module in "${a_module[@]}"; do
if grep -Psoiq -- '\b'"$l_module"'\b' <<< "${a_mounted[*]}"; then
a_output+=(" - \"$l_module\"")
elif grep -Psoiq -- '\b'"$l_module"'\b' <<< "${a_lsmod[*]}"; then
a_output2+=(" - \"$l_module\"")
elif ! grep -Psioq -- '\binstall\h+'"${l_module//-/_}"'\h+\H+\b' <<< "${a_showconfig[*]}" || \
! grep -Psioq -- '\bblacklist\h+'"${l_module//-/_}"'\b' <<< "${a_showconfig[*]}"; then
a_output3+=(" - \"$l_module\"")
fi
done
[ "${#a_output[@]}" -gt 0 ] && printf '%s\n' "" \
"- There are \"${#a_output[@]}\" kernel modules currently mounted:" "${a_output[@]}"
[ "${#a_output2[@]}" -gt 0 ] && printf '%s\n' "" \
"- There are \"${#a_output2[@]}\" kernel modules currently loaded:" "${a_output2[@]}"
[ "${#a_output3[@]}" -gt 0 ] && printf '%s\n' "" \
"- There are \"${#a_output3[@]}\" Kernel modules currently loadable:" "${a_output3[@]}"
}
Result:
- There are "1" kernel modules currently mounted:
- "vfat"
- There are "5" kernel modules currently loaded:
- "efivarfs"
- "autofs4"
- "fat"
- "configfs"
- "btrfs"
- There are "32" Kernel modules currently loadable:
- "isofs"
- "cuse"
- "pstore_blk"
- "ramoops"
- "pstore_zone"
- "squashfs"
- "udf"
- "msdos"
- "bcachefs"
- "erofs"
- "quota_tree"
- "quota_v2"
- "quota_v1"
- "zonefs"
- "grace"
- "nfs_acl"
- "netfs"
- "xfs"
- "cachefiles"
- "nfsd"
- "nfsv3"
- "nfsv4"
- "nfs"
- "nfsv2"
- "romfs"
- "ceph"
- "orangefs"
- "lockd"
- "sysv"
- "exfat"
- "overlay"
- "ntfs3"
This defines the scope of work. The loadable bucket is addressed first, since it covers what is not currently in use, before moving to what is already loaded or mounted.
lsblk reports an EFI partition at sda15, while /sys/firmware/efi does not
exist on this system. This is the first indication that the host is BIOS-booted
with a residual EFI partition rather than EFI-booted.
The loadable bucket #
isofs: mounts.isofiles, not needed here.cuse: extends the FUSE infrastructure and provides userspace character-device support, no operational use on this host.pstore_blk,ramoops,pstore_zone: part of the kernel’s persistent-storage panic/diagnostic logging stack, currently not necessary.msdos: legacy filesystem driver.bcachefs: modern filesystem with no use case here.erofs: Huawei’s lightweight read-only filesystem, not needed here.quota_tree,quota_v1,quota_v2:/etc/fstabshows no existing quota configuration, and no active quota usage was identified on this host.xfs: unused on this host.netfs: kernel infrastructure support for other network filesystem drivers that are themselves unused.nfs,nfsd,nfsv2,nfsv3,nfsv4,nfs_acl,lockd,grace: the NFS client/server stack, unused in its entirety.cachefiles: on-disk backend fornetfs, equally irrelevant.exfat: No exFAT filesystem or required removable-storage workload is present on this host.ntfs3: NTFS filesystem support, no dependent workload on this host.zonefs,romfs,ceph,orangefs,sysv: niche, legacy, or distributed filesystems, none applicable here.
Remediation was applied using a modified version of the benchmark’s script, adjusted to accept the module name as an argument rather than a hardcoded value:
#!/usr/bin/env bash
{
[[ -z "$1" ]] && printf "Please provide the module name\n"
module_name=$1
a_output2=(); a_output3=(); l_dl="" # Initialize arrays and clear variables
l_mod_name="$module_name" # set module name
l_mod_type="fs" # set module type
l_mod_path="$(readlink -f /usr/lib/modules/**/kernel/$l_mod_type || readlink -f /lib/modules/**/kernel/$l_mod_type)"
f_module_fix()
{
l_dl="y" # Set to ignore duplicate checks
a_showconfig=() # Create array with modprobe output
while IFS= read -r l_showconfig; do
a_showconfig+=("$l_showconfig")
done < <(modprobe --showconfig | grep -P -- '\b(install|blacklist)\h+'"${l_mod_name//-/_}"'\b')
if lsmod | grep "$l_mod_name" &> /dev/null; then # Check if the module is currently loaded
a_output2+=(" - unloading kernel module: \"$l_mod_name\"")
modprobe -r "$l_mod_name" 2>/dev/null; rmmod "$l_mod_name" 2>/dev/null
fi
if ! grep -Pq -- '\binstall\h+'"${l_mod_name//-/_}"'\h+(\/usr)?\/bin\/(true|false)\b' <<< "${a_showconfig[*]}"; then
a_output2+=(" - setting kernel module: \"$l_mod_name\" to \"$(readlink -f /bin/false)\"")
printf '%s\n' "install $l_mod_name $(readlink -f /bin/false)" >> /etc/modprobe.d/"$l_mod_name".conf
fi
if ! grep -Pq -- '\bblacklist\h+'"${l_mod_name//-/_}"'\b' <<< "${a_showconfig[*]}"; then
a_output2+=(" - denylisting kernel module: \"$l_mod_name\"")
printf '%s\n' "blacklist $l_mod_name" >> /etc/modprobe.d/"$l_mod_name".conf
fi
}
for l_mod_base_directory in $l_mod_path; do # Check if the module exists on the system
if [ -d "$l_mod_base_directory/${l_mod_name/-/\/}" ] && [ -n "$(ls -A "$l_mod_base_directory/${l_mod_name/-/\/}")" ]; then
a_output3+=(" - \"$l_mod_base_directory\"")
[[ "$l_mod_name" =~ overlay ]] && l_mod_name="${l_mod_name::-2}"
[ "$l_dl" != "y" ] && f_module_fix
else
echo -e " - kernel module: \"$l_mod_name\" doesn't exist in \"$l_mod_base_directory\""
fi
done
[ "${#a_output3[@]}" -gt 0 ] && printf '%s\n' "" " -- INFO --" " - module: \"$l_mod_name\" exists in:" "${a_output3[@]}"
[ "${#a_output2[@]}" -gt 0 ] && printf '%s\n' "" "${a_output2[@]}" || printf '%s\n' "" " - No changes needed"
printf '%s\n' "" " - remediation of kernel module: \"$l_mod_name\" complete" ""
}
Applied against isofs, producing the expected configuration:
# cat /etc/modprobe.d/isofs.conf
install isofs /usr/bin/false
blacklist isofs
cuse does not fit the script’s path assumptions. Its .ko file resides under
/lib/modules/6.12.86+deb13-cloud-amd64/kernel/fs/fuse/, one directory level
deeper than .../kernel/fs/, which the script does not account for:
- kernel module: "cuse" doesn't exist in "/usr/lib/modules/6.12.86+deb13-cloud-amd64/kernel/fs"
- kernel module: "cuse" doesn't exist in "/usr/lib/modules/6.12.94+deb13-cloud-amd64/kernel/fs"
- No changes needed
- remediation of kernel module: "cuse" complete
modinfo cuse confirms the module exists regardless. The same path mismatch
affects pstore_blk, ramoops, pstore_zone, msdos, the quota_* family,
nfs_acl, nfsv2, nfsv3, nfsv4, and grace, all of which live one
directory deeper than the script searches. Remediation for these was applied
manually, for example:
echo "install cuse /bin/false" | sudo tee /etc/modprobe.d/cuse.conf
echo "blacklist cuse" | sudo tee -a /etc/modprobe.d/cuse.conf
Existence should be confirmed with modinfo directly rather than relying on the
script’s silence.
A minimal generic script was used to process the remainder:
#!/usr/bin/env bash
l_mod_name="$1"
echo "install $l_mod_name /bin/false" | sudo tee /etc/modprobe.d/"$l_mod_name".conf
echo "blacklist $l_mod_name" | sudo tee -a /etc/modprobe.d/"$l_mod_name".conf
All modules in the loadable bucket were disabled by the end of this pass,
with the exception of overlay, udf, and squashfs, which remain untouched
pending the Level 2 pass.
The loaded bucket #
Five modules remained: efivarfs, autofs4, fat, configfs, btrfs.
Given the BIOS/EFI discrepancy noted above, and confirming the system boots
via BIOS rather than EFI, efivarfs serves no function here and was therefore
disabled. Similarly, lsblk -f confirms btrfs backs no mounted filesystem
on this host, prompting its disablement. vfat (and by extension fat) backs
/boot/efi and is left untouched pending further investigation into its role
given the BIOS/EFI discrepancy.
autofs4 and configfs presented as straightforward removals and were not.
systemctl status autofs reports the unit does not exist, and
/etc/auto.master is empty, both signs pointing to autofs4 being unused.
Attempting removal fails:
# modprobe -r autofs4
modprobe: FATAL: Module autofs4 is in use.
The module is held open by systemd itself, via an automount unit unrelated to
the classic autofs service:
# systemctl list-units --type=automount
UNIT LOAD ACTIVE SUB DESCRIPTION
proc-sys-fs-binfmt_misc.automount loaded active running Arbitrary Executable File Formats File System Automount Point
autofs4 is left in place.
configfs follows the same pattern. ls /sys/kernel/config returns empty,
suggesting safe removal. Running the benchmark’s remediation script against
it reports an unload with no error. The audit script, however, still lists
configfs as loaded, and a manual unload attempt reproduces the identical
FATAL error seen with autofs4. configfs is in fact mounted, with systemd
mounting it by default as part of system initialization, and the audit script
under-reports it due to a quirk in the used findmnt command, which is why it
doesn’t appear in the “mounted” bucket. mount | grep configfs confirms this.
Since /sys/kernel/config is confirmed empty and unused, configfs was
unmounted directly, and the corresponding unit masked to prevent systemd from
remounting it on subsequent boots:
# umount /sys/kernel/config
# systemctl mask sys-kernel-config.mount
Created symlink '/etc/systemd/system/sys-kernel-config.mount' → '/dev/null'.
With the mount cleared, the module was unloaded and disabled using the same procedure applied elsewhere. Persistence across a reboot was confirmed.
Final audit state following all remediation:
# ./deny-and-not-loadable-audit
- There are "1" kernel modules currently mounted:
- "vfat"
- There are "2" kernel modules currently loaded:
- "autofs4"
- "fat"
- There are "3" Kernel modules currently loadable:
- "squashfs"
- "udf"
- "overlay"
Every module remaining is either operationally required (vfat/fat for
/boot/efi and autofs4 for systemd’s binfmt_misc automount) or deliberately
deferred to the Level 2 pass (squashfs, udf, overlay).
1.1.2: Configure Filesystem Partitions #
This section covers 1.1.2.1 (/tmp) and 1.1.2.2 (/dev/shm) of the benchmark.
Placing directories used for system-wide functions on their own partitions
guards against resource exhaustion and allows mount options tailored to each
directory’s intended use. The remaining recommendations, 1.1.2.3 through 1.1.2.7
(/home, /var, /var/tmp, /var/log, /var/log/audit), are Level 2 and
will be addressed in that pass.
1.1.2.1: Configure /tmp #
1.1.2.1.1: Ensure /tmp Is tmpfs or a Separate Partition #
# findmnt -kn /tmp
/tmp tmpfs tmpfs rw,nosuid,nodev,size=1959128k,nr_inodes=1048576,inode64
/tmp is tmpfs, which satisfies this recommendation.
# systemctl is-enabled tmp.mount
static
The unit is neither masked nor disabled, so systemd mounts it at boot.
1.1.2.1.2: Ensure nodev Option Set on /tmp Partition #
The benchmark’s wording here is ambiguous, phrased as applying “if a separate partition exists for /tmp,” which is read as covering the tmpfs case as well.
# findmnt -kn /tmp | grep -v nodev
No output. nodev is already set.
1.1.2.1.3: Ensure nosuid Option Set on /tmp Partition #
# findmnt -kn /tmp | grep -v nosuid
No output. nosuid is already set.
1.1.2.1.4: Ensure noexec Option Set on /tmp Partition #
# findmnt -kn /tmp | grep -v noexec
/tmp tmpfs tmpfs rw,nosuid,nodev,size=1959128k,nr_inodes=1048576,inode64
noexec is not set. The fix is applied directly in /etc/fstab:
tmpfs /tmp tmpfs rw,nosuid,nodev,noexec,size=1959128k,nr_inodes=1048576,inode64 0 0
# sudo mount -o remount /tmp
mount: (hint) your fstab has been modified, but systemd still uses
the old version; use 'systemctl daemon-reload' to reload.
Following the hint:
# systemctl daemon-reload
# sudo mount -o remount /tmp
# findmnt -kn /tmp | grep -v noexec
No output. noexec is now set.
1.1.2.2: Configure /dev/shm #
1.1.2.2.1: Ensure /dev/shm Is tmpfs or a Separate Partition #
# findmnt -kn /dev/shm
/dev/shm tmpfs tmpfs rw,nosuid,nodev,inode64
/dev/shm is tmpfs, which satisfies this recommendation.
1.1.2.2.2: Ensure nodev Option Set on /dev/shm Partition #
# findmnt -kn /dev/shm | grep -v nodev
No output. nodev is already set.
1.1.2.2.3: Ensure nosuid Option Set on /dev/shm Partition #
# findmnt -kn /dev/shm | grep -v nosuid
No output. nosuid is already set.
1.1.2.2.4: Ensure noexec Option Set on /dev/shm Partition #
# findmnt -kn /dev/shm | grep -v 'noexec'
/dev/shm tmpfs tmpfs rw,nosuid,nodev,inode64
noexec is not set. Added to /etc/fstab:
tmpfs /dev/shm tmpfs rw,nosuid,nodev,noexec,inode64 0 0
# systemctl daemon-reload
# sudo mount -o remount /dev/shm
# findmnt -kn /dev/shm | grep -v 'noexec'
No output. noexec is now set.
1.2: Package Management #
This section covers 1.2.1 and 1.2.2 of the benchmark. The policy adopted here is manual updates during active hardening, avoiding unattended changes mid-investigation, with a transition to unattended-upgrades on a security-only scope once the hardening is complete.
1.2.1: Configure Package Repositories #
1.2.1.1: Ensure the source.list and .source Files Use the Signed-By Option #
# grep -PRLs -- '^([^#\n\r]+)?\bSigned-By\b' /etc/apt/sources.list /etc/apt/sources.list.d/*.{list,sources}
/etc/apt/sources.list
/etc/apt/sources.list is flagged, but only because the file is empty. It isn’t a compliance issue.
# ls /etc/apt/sources.list.d
debian.sources hetzner-mirror.sources hetzner-security-mirror.sources
All three files are compliant.
A decision was made to prefer the Debian repositories over the Hetzner mirrors,
to minimize attack surface. Both Hetzner .sources files were set to Enabled: no.
1.2.1.2: Ensure Weak Dependencies Are Configured #
Level 2, deferred. The recommendation appears to pass the audit regardless.
1.2.1.3: Ensure Access to GPG Key Files Are Configured #
Audit script verifying that all .gpg key files under /usr/share/keyrings/
and /etc/apt/trusted.gpg.d/ are mode 0644 or more restrictive, owned by
root, and group-owned by root:
#! /usr/bin/env bash
{
while IFS= read -r -d $'\0' l_file; do
stat -Lc 'File: %n Mode: (%#a) User: (%U) Group: (%G)' "$l_file"
done < <(find -L /usr/share/keyrings/ /etc/apt/trusted.gpg.d/ \
-mount -xdev -type f \( ! -user root -o ! -group root -o -perm /133 \) \
-name '*gpg' -print0)
}
Nothing returned.
A second script verifies the same for .list or .sources files that include
the Signed-By option:
#!/usr/bin/env bash
{
while IFS= read -r -d $'\0' l_file; do
grep -Psq -- '^([^#\n\r]+)?\bSigned-By\b' "$l_file" && \
stat -Lc 'File: %n Mode: (%#a) User: (%U) Group: (%G)' "$l_file"
done < <(find -L /etc/apt/sources.list.d/ -mount -xdev -type f \
\( ! -user root -o ! -group root -o -perm /133 \) \
\( -name '*.list' -o -name '*.sources' \) -print0)
}
Nothing returned.
The recommendation passes.
1.2.1.4: Ensure Access to /etc/apt/trusted.gpg.d Directory Is Configured #
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/apt/trusted.gpg.d
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Compliant.
1.2.1.5: Ensure Access to /etc/apt/auth.conf.d Directory Is Configured #
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/apt/auth.conf.d
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Compliant.
1.2.1.6: Ensure Access to Files in the /etc/apt/auth.conf.d/ Directory Is Configured #
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/apt/auth.conf.d/* 2>/dev/null
No output, since no files exist under /etc/apt/auth.conf.d, consistent with
there being no repositories that require authentication.
1.2.1.7: Ensure Access to /usr/share/keyrings Directory Is Configured #
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /usr/share/keyrings
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Compliant.
1.2.1.8: Ensure Access to /etc/apt/sources.list.d Directory Is Configured #
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/apt/sources.list.d
Access: (755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Compliant.
1.2.1.9: Ensure Access to Files in /etc/apt/sources.list.d Are Configured #
# stat -Lc 'Access: (%a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /etc/apt/sources.list.d/*
Access: (644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: (644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: (644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Compliant.
1.2.2: Configure Package Updates #
1.2.2.1: Ensure Updates, Patches, and Additional Security Software Are Installed #
# apt update
Hit:1 http://deb.debian.org/debian trixie InRelease
Hit:2 http://deb.debian.org/debian trixie-updates InRelease
Hit:3 http://deb.debian.org/debian trixie-backports InRelease
Get:4 http://deb.debian.org/debian-security trixie-security InRelease [43.4 kB]
Fetched 43.4 kB in 0s (272 kB/s)
All packages are up to date.
# apt -s upgrade
Summary:
Upgrading: 0, Installing: 0, Removing: 0, Not Upgrading: 0
# if [ -f /var/run/reboot-required ]; then
cat /var/run/reboot-required
fi
No output from the last command. /var/run/reboot-required does not exist,
confirming no reboot is required.
Manual updates continue for now. Automatic updates will be enabled once the rest of the hardening is complete.
1.3: Mandatory Access Control #
1.3.1: Configure AppArmor #
1.3.1.1: Ensure apparmor packages are installed #
# dpkg-query -s apparmor &>/dev/null && echo "apparmor is installed"
# dpkg-query -s apparmor-utils &>/dev/null && echo "apparmor-utils is installed"
Neither apparmor nor apparmor-utils is installed. Installed with apt install apparmor apparmor-utils.
1.3.1.2: Ensure AppArmor is enabled #
# grep "^\s*linux" /boot/grub/grub.cfg | grep "apparmor=0"
No output. Compliant.
1.3.1.3: Ensure all AppArmor Profiles are enforcing #
Level 2, deferred. For reference, the current state:
# apparmor_status | grep profiles
105 profiles are loaded.
6 profiles are in enforce mode.
23 profiles are in complain mode.
0 profiles are in prompt mode.
0 profiles are in kill mode.
76 profiles are in unconfined mode.
0 processes have profiles defined.
# apparmor_status | grep processes
0 processes have profiles defined.
0 processes are in enforce mode.
0 processes are in complain mode.
0 processes are in prompt mode.
0 processes are in kill mode.
0 processes are unconfined but have a profile defined.
0 processes are in mixed mode.
Not all profiles are enforcing, and no process currently has a profile defined.
1.3.1.4: Ensure apparmor_restrict_unprivileged_unconfined is enabled #
# sysctl kernel.apparmor_restrict_unprivileged_unconfined
kernel.apparmor_restrict_unprivileged_unconfined = 0
Not compliant.
Script checking for an existing configuration file that sets this value:
#!/usr/bin/env bash
{
l_parameter_name="kernel.apparmor_restrict_unprivileged_unconfined"
l_grep="${l_parameter_name//./\\.}" a_output=()
l_systemdsysctl="$(readlink -e /lib/systemd/systemd-sysctl || readlink -e /usr/lib/systemd/systemd-sysctl)"
l_ufwscf="$([ -f /etc/default/ufw ] && awk -F= '/^\s*IPT_SYSCTL=/ {print $2}' /etc/default/ufw)"
l_opt="$(grep -Psoi '^\h*'"$l_grep"'\h*=\h*\H+\b' "$l_ufwscf" | tail -n 1)"
l_option_value="$(cut -d= -f2 <<< "$l_opt" | xargs)"
[ -n "$l_option_value" ] && a_output+=(" - UFW set: \"$l_parameter_name\" to: \"$l_option_value\" in: \"$l_file\"")
while IFS= read -r l_file; do
l_file="${l_file//# /}"
l_opt="$(grep -Psoi '^\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\" is set to: \"$l_option_value\" in: \"$l_file\"")
done < <("$l_systemdsysctl" --cat-config | tac | grep -Psoi '^\h*#\h*\/[^#\n\r\h]+\.conf\b')
[ "${#a_output[@]}" -gt "0" ] && printf '%s\n' "" "${a_output[@]}" ""
}
No output. Not compliant.
Remediation:
# printf "\n%s\n" "kernel.apparmor_restrict_unprivileged_unconfined = 1" >> /etc/sysctl.d/60-kernel_sysctl.conf
# sysctl -w kernel.apparmor_restrict_unprivileged_unconfined=1
kernel.apparmor_restrict_unprivileged_unconfined = 1
# 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 ...
kernel.core_pattern = core
kernel.sysrq = 0x01b6
kernel.core_uses_pid = 1
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.eth0.rp_filter = 2
net.ipv4.conf.lo.rp_filter = 2
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.eth0.accept_source_route = 0
net.ipv4.conf.lo.accept_source_route = 0
net.ipv4.conf.default.promote_secondaries = 1
net.ipv4.conf.eth0.promote_secondaries = 1
net.ipv4.conf.lo.promote_secondaries = 1
net.ipv4.ping_group_range = 0 2147483647
net.core.default_qdisc = fq_codel
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
fs.protected_regular = 2
fs.protected_fifos = 1
vm.max_map_count = 1048576
kernel.pid_max = 4194304
kernel.apparmor_restrict_unprivileged_unconfined = 1
# sysctl kernel.apparmor_restrict_unprivileged_unconfined
kernel.apparmor_restrict_unprivileged_unconfined = 1
Compliant.
1.4: Configure Bootloader #
1.4.1: Ensure bootloader password is set #
# grep "^set superusers" /boot/grub/grub.cfg
# awk -F. '/^\s*password/ {print $1"."$2"."$3}' /boot/grub/grub.cfg
No output. Not compliant.
A password hash is generated:
# grub-mkpasswd-pbkdf2 --iteration-count=600000 --salt=64
Enter password:
Reenter password:
PBKDF2 hash of your password is grub.pbkdf2.sha512.600000.<REDACTED>
This is appended to /etc/grub.d/40_custom (or any custom file in that directory):
set superusers="root"
password_pbkdf2 root grub.pbkdf2.sha512.600000.<REDACTED>
root here is just the chosen username for the GRUB superuser entry. It
does not need to correspond to an actual system user, though root is the
conventional choice.
--unrestricted is added to /etc/grub.d/10_linux, on the CLASS= line,
so that the password isn’t required for a plain boot or reboot, to avoid
availability problems.
# update-grub
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.12.94+deb13-cloud-amd64
Found initrd image: /boot/initrd.img-6.12.94+deb13-cloud-amd64
Found linux image: /boot/vmlinuz-6.12.86+deb13-cloud-amd64
Found initrd image: /boot/initrd.img-6.12.86+deb13-cloud-amd64
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
Adding boot menu entry for UEFI Firmware Settings ...
done
1.4.2: Ensure access to bootloader config is configured #
Even read access should be restricted to root, as broader read access could help an attacker locate weaknesses.
# stat -Lc 'Access: (%#a/%A) Uid: ( %u/ %U) Gid: ( %g/ %G)' /boot/grub/grub.cfg
Access: (0600/-rw-------) Uid: ( 0/ root) Gid: ( 0/ root)
Compliant. Nothing was changed here. Per the benchmark, the default should have been 0644. This may be a Hetzner-specific addition to the image.
1.5: Configure Additional Process Hardening #
1.5.1: Ensure fs.protected_hardlinks is configured #
# sysctl fs.protected_hardlinks
fs.protected_hardlinks = 1
Compliant.
#!/usr/bin/env bash
{
l_parameter_name="fs.protected_hardlinks"
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 -Psoi '^\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[@]}" ""
}
Output:
- "fs.protected_hardlinks = 1" is set in:
"/usr/lib/sysctl.d/50-default.conf
Compliant.
1.5.2: Ensure fs.protected_symlinks is configured #
Level 2, deferred. Already set to 1 regardless.
1.5.3: Ensure kernel.yama.ptrace_scope is configured #
# sysctl kernel.yama.ptrace_scope
kernel.yama.ptrace_scope = 0
Not compliant. The value should be 1, 2, or 3.
#!/usr/bin/env bash
{
l_parameter_name="kernel.yama.ptrace_scope"
l_grep="${l_parameter_name//./\\.}" a_output=()
l_systemdsysctl="$(readlink -e /lib/systemd/systemd-sysctl || readlink -e /usr/lib/systemd/systemd-sysctl)"
l_ufwscf="$([ -f /etc/default/ufw ] && awk -F= '/^\s*IPT_SYSCTL=/ {print $2}' /etc/default/ufw)"
l_opt="$(grep -Psoi '^\h*'"$l_grep"'\h*=\h*\H+\b' "$l_ufwscf" | tail -n 1)"
l_option_value="$(cut -d= -f2 <<< "$l_opt" | xargs)"
[ -n "$l_option_value" ] && a_output+=(" - UFW set: \"$l_parameter_name\" to: \"$l_option_value\" in: \"$l_file\"")
while IFS= read -r l_file; do
l_file="${l_file//# /}"
l_opt="$(grep -Pois '^\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\" is set to: \"$l_option_value\" in: \"$l_file\"")
done < <("$l_systemdsysctl" --cat-config | tac | grep -Pios '^\h*#\h*\/[^#\n\r\h]+\.conf\b')
[ "${#a_output[@]}" -gt "0" ] && printf '%s\n' "" "${a_output[@]}" ""
}
No output. Not compliant.
Remediation script, commenting out any existing non-compliant setting:
#!/usr/bin/env bash
{
l_option="kernel.yama.ptrace_scope" l_grep="${l_option//./\\.}"
l_value="(1|2|3)"
while IFS= read -r -d $'\0' l_file; do
grep -Pois '\h*'"$l_option"'\h*=\h*\H+\b' "$l_file" \
| grep -Pivq '^\h*'"$l_grep"'\h*=\h*'"$l_value"'\b' && \
sed -ri '/^\s*kernel.yama.ptrace_scope\s*=/s/^/# /' "$l_file"
done < <(find /etc/sysctl.d/ -type f -name '*.conf' -print0)
}
No output. A .conf file still needs to be added or edited in /etc/sysctl.d/.
The custom file created earlier, /etc/sysctl.d/60-kernel_sysctl.conf, is
edited to add kernel.yama.ptrace_scope = 1.
# sysctl --system
# sysctl kernel.yama.ptrace_scope
kernel.yama.ptrace_scope = 1
Compliant.
1.5.4: Ensure fs.suid_dumpable is configured #
# sysctl fs.suid_dumpable
fs.suid_dumpable = 0
Compliant.
The corresponding audit script returns nothing. The value isn’t set in any
specific configuration file, only the kernel default applies. It is still added
explicitly to the custom file /etc/sysctl.d/60-kernel_sysctl.conf, followed by
sysctl --system. The script then reports the value as set in that file.
1.5.5: Ensure kernel.dmesg_restrict is configured #
# sysctl kernel.dmesg_restrict
kernel.dmesg_restrict = 1
Compliant. The audit script again returns nothing. The value is written
explicitly to the custom file regardless, followed by sysctl --system.
1.5.6: Ensure prelink is not installed #
# dpkg-query -s prelink &>/dev/null && echo "prelink is installed"
No output. Compliant.
1.5.7: Ensure Automatic Error Reporting is configured #
This recommendation concerns Canonical’s Apport error-reporting service.
# dpkg-query -s apport &> /dev/null && grep -Psi -- '^\h*enabled\h*=\h*[^0]\b' /etc/default/apport
# systemctl is-active apport.service | grep '^active'
Both return nothing. Compliant.
1.5.8: Ensure kernel.kptr_restrict is configured #
# sysctl kernel.kptr_restrict
kernel.kptr_restrict = 0
Not compliant. Kernel pointers are not being replaced with zeroes for users
lacking CAP_SYSLOG. The audit script returns nothing. Remediation adds
kernel.kptr_restrict = 1 to the custom file, followed by sysctl --system.
The benchmark states the default value for this parameter is 1. The value found on this host was 0.
1.5.9: Ensure kernel.randomize_va_space is configured #
# sysctl kernel.randomize_va_space
kernel.randomize_va_space = 2
Compliant. The audit script returns nothing. The value is still added explicitly
to the custom file, kernel.randomize_va_space = 2.
1.5.10: Ensure kernel.yama.ptrace_scope is configured #
This recommendation duplicates 1.5.3 and was already addressed there.
1.5.11: Ensure core file size is configured #
# grep -Psi -- '^\h*\*\h+hard\h+core\b' /etc/security/limits.conf /etc/security/limits.d/*
/etc/security/limits.d/10-coredump-debian.conf:* hard core infinity
Not compliant. The value should be 0, not infinity.
Remediation attempted using the benchmark’s example command, which comments out entries with a hard core value above 0:
# sed -ri '/^\s*[^#\n\r]+\s+hard\s+core\s+([1-9][0-9]*)/s/^/# /' /etc/security/limits.conf /etc/security/limits.d/*
No output, but the change did not take effect. The file in question:
# cat /etc/security/limits.d/10-coredump-debian.conf
* soft core 0
root soft core 0
* hard core infinity
root hard core infinity
This is the only file in limits.d, and /etc/security/limits.conf has nothing
uncommented. Rather than edit this file directly, a new file is created in
limits.d that sorts lexicographically after it, 60-cis-hardening.conf,
matching the naming convention already in use.
The benchmark’s example remediation regex does not account for the value
infinity, which is why it failed to match. Both hard-limit lines in the
original file are commented out by hand. Only the * entry is actually in scope
for this recommendation, the root line isn’t.
# grep -Psi -- '^\h*\*\h+hard\h+core\b' /etc/security/limits.conf /etc/security/limits.d/*
/etc/security/limits.d/60-cis-hardening.conf:* hard core 0
Compliant.
1.5.12: Ensure systemd-coredump ProcessSizeMax is configured #
#!/usr/bin/env bash
{
l_analyze_cmd="$(readlink -e /bin/systemd-analyze || \
readlink -e /usr/bin/systemd-analyze)"
l_conf_file="systemd/coredump.conf" l_block="Coredump"
l_option="ProcessSizeMax" l_option_value="" a_output=()
while IFS= read -r l_file; do
l_file="${l_file//# /}"
l_opt="$(awk '/\['"$l_block"'\]/{a=1;next}/\[/{a=0}a' "$l_file" \
2>/dev/null | grep -Poi '^\h*'"$l_option"'\h*=\h*\H+\b' | tail -n 1)"
l_option_value="$(cut -d= -f2 <<< "$l_opt" | xargs)"
[ -n "$l_option_value" ] && \
a_output+=(" - \"$l_option\" is set to: \"$l_option_value\"" \
" in: \"$l_file\"")
done < <("$l_analyze_cmd" cat-config "$l_conf_file" | tac | \
grep -Pio '^\h*#\h*\/[^#\n\r\h]+\.conf\b')
if [ "${#a_output[@]}" -le "0" ]; then
l_file="$(readlink -e /etc/"$l_conf_file" || \
readlink -e /usr/lib/"$l_conf_file")"
l_opt="$(awk '/\['"$l_block"'\]/{a=1;next}/\[/{a=0}a' "$l_file" \
2>/dev/null | grep -Poim 1 '^(\h*#)?\h*'"$l_option"'\h*=\h*\H+\b')"
l_option_value="$(cut -d= -f2 <<< "${l_opt//# /}" | xargs)"
[ -n "$l_option_value" ] && \
a_output+=(" - The default value: \"${l_opt//#/}\"" \
" is being used in the configuration")
fi
[ "${#a_output[@]}" -gt "0" ] && printf '%s\n' "" "${a_output[@]}" ""
}
This hangs. One theory: awk hangs because neither /etc/systemd/coredump.conf
nor /usr/lib/systemd/coredump.conf exists. Creating the file resolves this:
# touch /etc/systemd/coredump.conf
The script then returns no output. A file 60-cis-hardening.conf is created in
/etc/systemd/coredump.conf.d/:
[Coredump]
ProcessSizeMax=0
Attempting to reload:
# systemctl reload-or-restart systemd-coredump.socket
Failed to reload-or-restart systemd-coredump.socket: Unit systemd-coredump.socket not found.
The benchmark notes that if the systemd-coredump package is not installed,
that counts as a passing state. Confirming:
# dpkg -s systemd-coredump 2>/dev/null | grep -i status
No output.
# dpkg -s systemd-coredump
dpkg-query: package `systemd-coredump` is not installed and no information is available
Use dpkg --info (= dpkg-deb --info) to examine archive files.
systemd-coredump is not installed, so the recommendation passes. The
configuration files created while troubleshooting are removed.
1.5.13: Ensure systemd-coredump Storage is configured #
systemd-coredump is not installed, so this recommendation passes as well.
1.6: Configure Command Line Warning Banners #
1.6.1: Ensure /etc/motd is configured #
#!/usr/bin/env bash
{
l_output="" l_output2=""
a_files=()
for l_file in /etc/motd{,.d/*}; do
if grep -Psqi -- "(\\\v|\\\r|\\\m|\\\s|\b$(grep ^ID= /etc/os-release |
cut -d= -f2 | sed -e 's/"//g')\b)" "$l_file"; then
l_output2="$l_output2\n - File: \"$l_file\" includes system information"
else
a_files+=("$l_file")
fi
done
if [ "${#a_files[@]}" -gt 0 ]; then
echo -e "\n- ** Please review the following files and verify their contents follow local site policy **\n"
printf '%s\n' "${a_files[@]}"
elif [ -z "$l_output2" ]; then
echo -e "- ** No MOTD files with any size were found. Please verify this conforms to local site policy ** -"
fi
if [ -z "$l_output2" ]; then
l_output=" - No MOTD files include system information"
echo -e "\n- Audit Result:\n ** PASS **\n$l_output\n"
else
echo -e "\n- Audit Result:\n ** FAIL **\n - Reason(s) for audit failure:\n$l_output2\n"
fi
}
Output of the audit script:
- ** Please review the following files and verify their contents follow local site policy **
/etc/motd.d/*
- Audit Result:
** FAIL **
- Reason(s) for audit failure:
- File: "/etc/motd" includes system information
Contents of the file:
# cat /etc/motd
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
The directory /etc/motd.d does not exist. The contents of /etc/motd are replaced with:
Authorized access only.
New audit result:
- ** Please review the following files and verify their contents follow local site policy **
/etc/motd
/etc/motd.d/*
- Audit Result:
** PASS **
- No MOTD files include system information
1.6.2: Ensure /etc/issue is configured #
# cat /etc/issue
Debian GNU/Linux 13 \n \l
Not compliant. Contents replaced with:
Authorized users only. All activity may be monitored and reported.
1.6.3: Ensure /etc/issue.net is configured #
/etc/issue.net serves the same purpose as /etc/issue, but for remote connections.
# cat /etc/issue.net
Debian GNU/Linux 13
Not compliant. Fixed the same way:
# cat /etc/issue.net
Authorized users only. All activity may be monitored and reported.
1.6.4: Ensure access to /etc/motd is configured #
# [ -e /etc/motd ] && stat -Lc 'Access: (%#a/%A) Uid: ( %u/ %U) Gid: { %g/ %G)' /etc/motd
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: { 0/ root)
Compliant.
1.6.5: Ensure access to /etc/issue is configured #
# stat -Lc 'Access: (%#a/%A) Uid: ( %u/ %U) Gid: { %g/ %G)' /etc/issue
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: { 0/ root)
Compliant.
1.6.6: Ensure access to /etc/issue.net is configured #
# stat -Lc 'Access: (%#a/%A) Uid: ( %u/ %U) Gid: { %g/ %G)' /etc/issue.net
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: { 0/ root)
Compliant.
1.7: Configure GNOME Display Manager #
# dpkg-query -s gdm3 &>/dev/null && echo "gdm3 is installed"
No output. GDM is not installed on this system.
1.7.1: Ensure GDM is removed #
GDM’s absence directly satisfies this recommendation.
The remaining recommendations in this section configure GDM-specific behavior and don’t apply where GDM isn’t installed. They are skipped in their entirety.
With this, the Initial Setup section (1.1 through 1.7) is complete. Running Lynis again gives a score of 68, three points above the starting baseline.
================================================================================
Lynis security scan details:
Scan mode:
Normal [▆] Forensics [ ] Integration [ ] Pentest [ ]
Lynis modules:
- Compliance status [?]
- Security audit [V]
- Vulnerability scan [V]
Details:
Hardening index : 68 [############# ]
Tests performed : 274
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
================================================================================