CIS Hardening of a Debian Linux Server Part 8: Level 2 Initial Setup

This is the ninth 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.

1.1.1 Configure Filesystem Kernel Modules #

This part covers 1.1.1.6 through 1.1.1.8 of the benchmark. The remainder of the section was handled in the Level 1 pass.

For each of these modules, if a dependent functionality is required later on, such as containers, the module must be re-enabled.

1.1.1.6 Ensure Overlay Kernel Module Is Not Available #

Disabling this module can severely disrupt containerized workloads, since applications such as Docker and Kubernetes rely on it.

#!/usr/bin/env bash
{
  l_mod_name="overlayfs" 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)
}
overlayfs exists in /usr/lib/modules/6.12.94+deb13-cloud-amd64/kernel/fs
overlayfs exists in /usr/lib/modules/6.12.95+deb13-cloud-amd64/kernel/fs

The module is present for both installed kernels, so remediation is required.

# lsmod | grep 'overlay'
# modprobe --showconfig | grep -P -- '\b(install|blacklist)\h+overlay\b'

Neither command returns output. The module is not currently loaded, but it is not yet disabled through configuration.

The unload commands are run regardless, as a precaution (both commands do approximately the same thing):

# modprobe -r overlay 2>/dev/null
# rmmod overlay 2>/dev/null

The module is then blacklisted and its install path pointed to /bin/false:

# printf '%s\n' "" "install overlay /bin/false" >> /etc/modprobe.d/60-overlay.conf
# printf '%s\n' "" "blacklist overlay" >> /etc/modprobe.d/60-overlay.conf

Rechecking confirms the change:

# modprobe --showconfig | grep -P -- '\b(install|blacklist)\h+overlay\b'
blacklist overlay
install overlay /bin/false

1.1.1.7 Ensure Squashfs Kernel Module Is Not Available #

Disabling this module causes Snap packages to fail, since Snap relies on SquashFS as its compressed filesystem format.

The audit script is identical to the one used for overlayfs, substituting squashfs for the module name:

squashfs exists in /usr/lib/modules/6.12.94+deb13-cloud-amd64/kernel/fs
squashfs exists in /usr/lib/modules/6.12.95+deb13-cloud-amd64/kernel/fs

Remediation is required.

# lsmod | grep 'squashfs'

No output. The module is not loaded.

# modprobe --showconfig | grep -P -- '\b(install|blacklist)\h+squashfs\b'

No output. The module is not yet disabled through configuration.

Since Snap depends on this module, its presence on the system is checked before proceeding:

# dpkg -l snapd
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name           Version      Architecture Description
+++-==============-============-============-=================================
un  snapd          <none>       <none>       (no description available)

The un status indicates snapd is not installed.

# which command
# command -v snap

Neither command returns a result, confirming Snap is absent. Remediation can proceed.

# modprobe -r squashfs 2>/dev/null
# rmmod squashfs 2>/dev/null
# printf '%s\n' "" "install squashfs /bin/false" >> /etc/modprobe.d/60-squashfs.conf
# printf '%s\n' "" "blacklist squashfs" >> /etc/modprobe.d/60-squashfs.conf
# modprobe --showconfig | grep -P -- '\b(install|blacklist)\h+squashfs\b'
blacklist squashfs
install squashfs /bin/false

1.1.1.8 Ensure Udf Kernel Module Is Not Available #

Microsoft Azure requires this module, and it should not be disabled on systems running there. That does not apply here, since this system runs on Hetzner Cloud rather than Azure.

The same audit script applies, substituting udf for the module name:

udf exists in /usr/lib/modules/6.12.94+deb13-cloud-amd64/kernel/fs
udf exists in /usr/lib/modules/6.12.95+deb13-cloud-amd64/kernel/fs

Remediation is required.

# lsmod | grep 'udf'

No output. The module is not loaded.

# modprobe --showconfig | grep -P -- '\b(install|blacklist)\h+udf\b'

No output. The module is not yet disabled through configuration. The unload commands are run regardless:

# modprobe -r udf 2>/dev/null
# rmmod udf 2>/dev/null
# printf '%s\n' "" "install udf /bin/false" >> /etc/modprobe.d/60-udf.conf
# printf '%s\n' "" "blacklist udf" >> /etc/modprobe.d/60-udf.conf
# modprobe --showconfig | grep -P -- '\b(install|blacklist)\h+udf\b'
blacklist udf
install udf /bin/false

1.1.2 Configure Filesystem Partitions #

This section covers 1.1.2.3 through 1.1.2.7 of the benchmark. Recommendations 1.1.2.1 and 1.1.2.2 were handled in the Level 1 pass.

An initial audit of /home shows the gap this section addresses:

#!/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 $1 " - " $6}' /etc/passwd
}
sharaf - /home/sharaf
# findmnt -kn /home

No output. /home is not on a separate partition, which is expected, as the system was originally installed with a single root partition.

Resolving this requires repartitioning a live, already-installed system, a procedure not covered in step-by-step detail by the benchmark itself. A companion guide to this series, published on Hetzner’s community platform: Post-Install Repartitioning for CIS Hardening Compliance, walks through it in full. What follows is that procedure executed against this server.

The procedure starts with disabling automatic root partition growth via touch /etc/growroot-disabled, and a snapshot of the server is taken as a safety net. The Hetzner rescue system is then enabled and the server rebooted into it, since the root partition cannot be resized while mounted and in active use.

The root filesystem is shrunk and checked from the rescue system:

root@rescue ~ # e2fsck -f -y /dev/sda1
e2fsck 1.47.0 (5-Feb-2023)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
/dev/sda1: 47085/2436864 files (0.2% non-contiguous), 602222/9937403 blocks
root@rescue ~ # resize2fs /dev/sda1 8G
resize2fs 1.47.0 (5-Feb-2023)
Resizing the filesystem on /dev/sda1 to 2097152 (4k) blocks.
The filesystem on /dev/sda1 is now 2097152 (4k) blocks long.

root@rescue ~ # parted /dev/sda
GNU Parted 3.5
Using /dev/sda
Welcome to GNU Parted! Type 'help' to view a list of commands.
(parted) print                                                            
Model: QEMU QEMU HARDDISK (scsi)
Disk /dev/sda: 41.0GB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags: 

Number  Start   End     Size    File system  Name  Flags
14      1049kB  2097kB  1049kB                     bios_grub
15      2097kB  258MB   256MB   fat32              boot, esp
 1      258MB   41.0GB  40.7GB  ext4

(parted) resizepart 1 10GiB                                               
Warning: Shrinking a partition can cause data loss, are you sure you want to continue?
Yes/No? yes                                                               
(parted) quit                                                             
Information: You may need to update /etc/fstab.

root@rescue ~ # resize2fs /dev/sda1                                       
resize2fs 1.47.0 (5-Feb-2023)
Resizing the filesystem on /dev/sda1 to 2558464 (4k) blocks.
The filesystem on /dev/sda1 is now 2558464 (4k) blocks long.

root@rescue ~ # e2fsck -f -y /dev/sda1
e2fsck 1.47.0 (5-Feb-2023)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
/dev/sda1: 47085/633264 files (0.3% non-contiguous), 487828/2558464 blocks

The remaining space is used to create one partition per target mount point:

root@rescue ~ # parted -a optimal /dev/sda mkpart home ext4 10GiB 20GiB
Information: You may need to update /etc/fstab.

root@rescue ~ # parted -a optimal /dev/sda mkpart var ext4 20GiB 24GiB    
Information: You may need to update /etc/fstab.

root@rescue ~ # parted -a optimal /dev/sda mkpart vartmp ext4 24GiB 26GiB 
Information: You may need to update /etc/fstab.

root@rescue ~ # parted -a optimal /dev/sda mkpart varlog ext4 26GiB 32GiB 
Information: You may need to update /etc/fstab.

root@rescue ~ # parted -a optimal /dev/sda mkpart varlogaudit ext4 32GiB 36GiB
Information: You may need to update /etc/fstab.

root@rescue ~ # lsblk                                                     
NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
loop0     7:0    0  3.8G  1 loop 
sda       8:0    0 38.1G  0 disk 
├─sda1    8:1    0  9.8G  0 part 
├─sda2    8:2    0   10G  0 part 
├─sda3    8:3    0    4G  0 part 
├─sda4    8:4    0    2G  0 part 
├─sda5    8:5    0    6G  0 part 
├─sda6    8:6    0    4G  0 part 
├─sda14   8:14   0    1M  0 part 
└─sda15   8:15   0  244M  0 part 
sr0      11:0    1 1024M  0 rom 
root@rescue ~ # mkfs.ext4 /dev/sda2
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done                            
Creating filesystem with 2621440 4k blocks and 655360 inodes
Filesystem UUID: ca933296-6391-4495-a400-b8ce9e4c79f3
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done 

root@rescue ~ # mkfs.ext4 /dev/sda3
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done                            
Creating filesystem with 1048576 4k blocks and 262144 inodes
Filesystem UUID: e4840e1e-c3d4-4b8c-8406-96cb8ad68ca0
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done 

root@rescue ~ # mkfs.ext4 /dev/sda4
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done                            
Creating filesystem with 524288 4k blocks and 131072 inodes
Filesystem UUID: deeeefa0-c179-4bff-85c6-15632670806a
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done 

root@rescue ~ # mkfs.ext4 /dev/sda5
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done                            
Creating filesystem with 1572864 4k blocks and 393216 inodes
Filesystem UUID: af407528-4cd2-44c9-9c3d-890aa626ef66
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done 

root@rescue ~ # mkfs.ext4 /dev/sda6
mke2fs 1.47.0 (5-Feb-2023)
Discarding device blocks: done                            
Creating filesystem with 1048576 4k blocks and 262144 inodes
Filesystem UUID: d1fc4ca8-9bf4-485f-8111-90ae55a31a39
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done 

Data is then migrated from the root filesystem to each new partition using rsync:

root@rescue ~ # mkdir -p /mnt/old_root /mnt/new_home /mnt/new_var /mnt/new_vartmp /mnt/new_varlog /mnt/new_varaudit
root@rescue ~ # mount /dev/sda1 /mnt/old_root
root@rescue ~ # mount /dev/sda2 /mnt/new_home
root@rescue ~ # mount /dev/sda3 /mnt/new_var
root@rescue ~ # mount /dev/sda4 /mnt/new_vartmp
root@rescue ~ # mount /dev/sda5 /mnt/new_varlog
root@rescue ~ # mount /dev/sda6 /mnt/new_varaudit
root@rescue ~ # rsync -aHAX /mnt/old_root/home/ /mnt/new_home/
root@rescue ~ # rsync -aHAX /mnt/old_root/var/tmp/ /mnt/new_vartmp/
root@rescue ~ # rsync -aHAX --exclude='tmp/' --exclude='log/' /mnt/old_root/var/ /mnt/new_var/
root@rescue ~ # rsync -aHAX /mnt/old_root/var/log/ /mnt/new_varlog/

The migrated data is verified on each new partition before continuing:

root@rescue ~ # ls -la /mnt/new_home/
total 24K
drwxr-xr-x 4 root root 4.0K Jun 27 15:49 .
drwxr-xr-x 1 root root  160 Jul 19 01:19 ..
drwx------ 2 root root  16K Jul 19 01:15 lost+found
drwx------ 3 1000 1000 4.0K Jul 18 05:06 sharaf
root@rescue ~ # ls -la /mnt/new_vartmp/
total 24K
drwxrwxrwt 4 root root 4.0K Jul 19 00:50 .
drwxr-xr-x 1 root root  160 Jul 19 01:19 ..
drwxrwxrwt 2 root root 4.0K Jun 30 00:18 cloud-init
drwx------ 2 root root  16K Jul 19 01:15 lost+found
root@rescue ~ # ls -la /mnt/new_var/
total 52K
drwxr-xr-x 10 root root 4.0K May 12 15:23 .
drwxr-xr-x  1 root root  160 Jul 19 01:19 ..
drwxr-xr-x  2 root root 4.0K Jul 15 02:00 backups
drwxr-xr-x  9 root root 4.0K Jun 30 23:01 cache
drwxr-xr-x 26 root root 4.0K Jul 14 11:17 lib
drwxr-xr-x  2 root root 4.0K Jan  2  2026 local
lrwxrwxrwx  1 root root    9 May 12 15:19 lock -> /run/lock
drwx------  2 root root  16K Jul 19 01:15 lost+found
drwxrwsr-x  2 root mail 4.0K May 12 15:19 mail
drwxr-xr-x  2 root root 4.0K May 12 15:19 opt
lrwxrwxrwx  1 root root    4 May 12 15:19 run -> /run
drwxr-xr-x  4 root root 4.0K Jul 12 00:34 spool
-rw-r--r--  1 root root  208 May 12 15:19 .updated
root@rescue ~ # ls -la /mnt/new_varlog/
total 14M
drwxr-xr-x  10 root root            4.0K Jul 19 00:50 .
drwxr-xr-x   1 root root             160 Jul 19 01:19 ..
drwxr-sr-x   2  988 adm             4.0K Jul 18 04:37 aide
-rw-r-----   1 root root            1.5K Jul 12 08:36 alternatives.log
-rw-r-----   1 root root             815 Jun 27 15:41 alternatives.log.1
drwxr-xr-x   2 root root            4.0K Apr 10  2025 apparmor
drwxr-xr-x   2 root root            4.0K Jul 14 11:17 apt
-rw-r-----   1 root adm             7.4M Jul 19 00:50 auth.log
-rw-rw----   1 root utmp               0 Jul  1 02:28 btmp
-rw-rw----   1 root utmp               0 May 12 15:19 btmp.1
-rw-r-----   1 root adm             196K Jun 30 00:18 cloud-init.log
-rw-r-----   1 root adm               7.3K Jun 30 00:18 cloud-init-output.log
-rw-r-----   1 root adm              21K Jul 19 00:17 cron.log
-rw-r-----   1 root root             33K Jul 14 11:17 dpkg.log
-rw-r-----   1 root root             59K Jun 30 23:01 dpkg.log.1
drwxr-sr-x+  4 root systemd-journal 4.0K Jun 27 13:38 journal
-rw-r-----   1 root adm             1.1M Jul 18 23:13 kern.log
-rw-rw-r--   1 root utmp            286K Jul 19 00:48 lastlog
drwx------   2 root root             16K Jul 19 01:15 lost+found
-rw-r-----   1 root root            446K Jul 16 04:00 lynis.log
-rw-r-----   1 root root             57K Jul 16 04:00 lynis-report.dat
drwx------   2 root root            4.0K Jun 27 13:38 private
lrwxrwxrwx   1 root root              39 May 12 15:19 README -> ../../usr/share/doc/systemd/README.logs
drwxr-xr-x   3 root root            4.0K Jun 27 15:40 runit
-rw-r-----   1 root root            5.8K Jul 19 00:49 sudo.log
-rw-r-----   1 root adm             1.8M Jul 19 00:50 syslog
-rw-r-----   1 root adm             890K Jul 18 23:13 ufw.log
-rw-r-----   1 root adm             156K Jul 12 01:00 ufw.log.1
drwxr-xr-x   2 root root            4.0K Jul  7 02:08 unattended-upgrades
-rw-r-----   1 root adm               80 Jul 13 03:42 user.log
-rw-r-----   1 root adm             1.2M Jul 19 00:50 warnings.log
-rw-rw-r--   1 root utmp             48K Jul 19 00:48 wtmp
-rw-r--r--   1 root root             20K Jul 19 00:50 wtmp.db

/var/log/audit did not yet exist on this system, so nothing was migrated to it. The original data is then removed from the root filesystem to reclaim space, and each new partition is unmounted, leaving /dev/sda1 mounted for the fstab update that follows:

root@rescue ~ # rm -Rf /mnt/old_root/home/
root@rescue ~ # rm -Rf /mnt/old_root/var/
root@rescue ~ # mkdir -p /mnt/old_root/home
root@rescue ~ # mkdir -p /mnt/old_root/var
root@rescue ~ # mkdir -p /mnt/new_var/tmp
root@rescue ~ # mkdir -p /mnt/new_var/log
root@rescue ~ # mkdir -p /mnt/new_varlog/audit
root@rescue ~ # umount /mnt/new_home
root@rescue ~ # umount /mnt/new_var
root@rescue ~ # umount /mnt/new_vartmp
root@rescue ~ # umount /mnt/new_varlog
root@rescue ~ # umount /mnt/new_varaudit

The UUIDs of the new partitions are collected and added to /etc/fstab on the mounted root filesystem, using 2 as the fsck pass value to enable checks:

root@rescue ~ # blkid | grep -E "sda[2-6]"
/dev/sda4: UUID="deeeefa0-c179-4bff-85c6-15632670806a" BLOCK_SIZE="4096" TYPE="ext4" PARTLABEL="vartmp" PARTUUID="c46818d6-7db7-4188-995e-b03e445cc9dc"
/dev/sda2: UUID="ca933296-6391-4495-a400-b8ce9e4c79f3" BLOCK_SIZE="4096" TYPE="ext4" PARTLABEL="home" PARTUUID="7943b6fe-0457-4610-b45d-e7dd95995b4e"
/dev/sda5: UUID="af407528-4cd2-44c9-9c3d-890aa626ef66" BLOCK_SIZE="4096" TYPE="ext4" PARTLABEL="varlog" PARTUUID="a820080f-5f39-486c-b85d-273a3dd4bf83"
/dev/sda3: UUID="e4840e1e-c3d4-4b8c-8406-96cb8ad68ca0" BLOCK_SIZE="4096" TYPE="ext4" PARTLABEL="var" PARTUUID="84915022-b77b-40cf-8bd3-5c7b0b819176"
/dev/sda6: UUID="d1fc4ca8-9bf4-485f-8111-90ae55a31a39" BLOCK_SIZE="4096" TYPE="ext4" PARTLABEL="varlogaudit" PARTUUID="bd705270-db64-4eff-a53e-d722e85e78b9"
root@rescue ~ # vim /mnt/old_root/etc/fstab
root@rescue ~ # cat /mnt/old_root/etc/fstab
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# systemd generates mount units based on this file, see systemd.mount(5).
# Please run 'systemctl daemon-reload' after making changes here.
#
# <file system> <mount point>   <type>  <options>       <dump>  <pass>
# / was on /dev/sda3 during installation
UUID=8ba91a8e-417d-4e0c-96a7-1f1468d56d79 /               ext4    errors=remount-ro 0       1
# /boot/efi was on /dev/sda2 during installation
UUID=8E15-D0E4  /boot/efi       vfat    umask=0077      0       1
/dev/sr0        /media/cdrom0   udf,iso9660 user,noauto     0       0
tmpfs   /tmp   tmpfs   rw,nosuid,nodev,noexec,size=1959128k,nr_inodes=1048576,inode64   0   0
tmpfs   /dev/shm   tmpfs   rw,nosuid,nodev,noexec,inode64   0   0
UUID=ca933296-6391-4495-a400-b8ce9e4c79f3 /home ext4 defaults,rw,nosuid,nodev,relatime 0 2
UUID=e4840e1e-c3d4-4b8c-8406-96cb8ad68ca0 /var ext4 defaults,rw,nosuid,nodev,relatime 0 2
UUID=deeeefa0-c179-4bff-85c6-15632670806a /var/tmp ext4 defaults,rw,nosuid,nodev,noexec,relatime 0 2
UUID=af407528-4cd2-44c9-9c3d-890aa626ef66 /var/log ext4 defaults,rw,nosuid,nodev,noexec,relatime 0 2
UUID=d1fc4ca8-9bf4-485f-8111-90ae55a31a39 /var/log/audit ext4 defaults,rw,nosuid,nodev,noexec,relatime 0 2

Running findmnt against the updated file confirms it is free of parse errors:

root@rescue ~ # findmnt --verify --fstab --tab-file /mnt/old_root/etc/fstab
/boot/efi
   [E] unreachable on boot required target: No such file or directory
/media/cdrom0
   [W] unreachable target: No such file or directory
   [W] cannot detect on-disk filesystem type (No medium found)
/var/log/audit
   [E] unreachable on boot required target: No such file or directory

0 parse errors, 2 errors, 2 warnings

The reported errors and warnings stem from findmnt checking targets against the rescue environment’s own filesystem, which does not have these directories, rather than against the target system. With 0 parse errors, the file itself is syntactically sound. The rescue root filesystem is then unmounted and the server rebooted into the newly repartitioned disk:

root@rescue ~ # umount /mnt/old_root
root@rescue ~ # reboot

Post-reboot verification confirms each partition mounted at the intended location with the expected options:

# mount | column -t
sysfs        on  /sys                                         type  sysfs        (rw,nosuid,nodev,noexec,relatime)
proc         on  /proc                                        type  proc         (rw,nosuid,nodev,noexec,relatime)
udev         on  /dev                                         type  devtmpfs     (rw,nosuid,relatime,size=1944220k,nr_inodes=486055,mode=755,inode64)
devpts       on  /dev/pts                                     type  devpts       (rw,nosuid,noexec,relatime,gid=5,mode=600,ptmxmode=000)
tmpfs        on  /run                                         type  tmpfs        (rw,nosuid,nodev,noexec,relatime,size=391828k,mode=755,inode64)
/dev/sda1    on  /                                            type  ext4         (rw,relatime,errors=remount-ro)
securityfs   on  /sys/kernel/security                         type  securityfs   (rw,nosuid,nodev,noexec,relatime)
tmpfs        on  /dev/shm                                     type  tmpfs        (rw,nosuid,nodev,noexec,inode64)
cgroup2      on  /sys/fs/cgroup                               type  cgroup2      (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)
pstore       on  /sys/fs/pstore                               type  pstore       (rw,nosuid,nodev,noexec,relatime)
bpf          on  /sys/fs/bpf                                  type  bpf          (rw,nosuid,nodev,noexec,relatime,mode=700)
systemd-1    on  /proc/sys/fs/binfmt_misc                     type  autofs       (rw,relatime,fd=41,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=390)
mqueue       on  /dev/mqueue                                  type  mqueue       (rw,nosuid,nodev,noexec,relatime)
hugetlbfs    on  /dev/hugepages                               type  hugetlbfs    (rw,nosuid,nodev,relatime,pagesize=2M)
tmpfs        on  /run/lock                                    type  tmpfs        (rw,nosuid,nodev,noexec,relatime,size=5120k,inode64)
debugfs      on  /sys/kernel/debug                            type  debugfs      (rw,nosuid,nodev,noexec,relatime)
tracefs      on  /sys/kernel/tracing                          type  tracefs      (rw,nosuid,nodev,noexec,relatime)
tmpfs        on  /run/credentials/systemd-journald.service    type  tmpfs        (ro,nosuid,nodev,noexec,relatime,nosymfollow,size=1024k,nr_inodes=1024,mode=700,inode64,noswap)
fusectl      on  /sys/fs/fuse/connections                     type  fusectl      (rw,nosuid,nodev,noexec,relatime)
tmpfs        on  /tmp                                         type  tmpfs        (rw,nosuid,nodev,noexec,relatime,size=1959128k,nr_inodes=1048576,inode64)
/dev/sda2    on  /home                                        type  ext4         (rw,nosuid,nodev,relatime)
/dev/sda3    on  /var                                         type  ext4         (rw,nosuid,nodev,relatime)
/dev/sda15   on  /boot/efi                                    type  vfat         (rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=ascii,shortname=mixed,utf8,errors=remount-ro)
/dev/sda5    on  /var/log                                     type  ext4         (rw,nosuid,nodev,noexec,relatime)
/dev/sda4    on  /var/tmp                                     type  ext4         (rw,nosuid,nodev,noexec,relatime)
/dev/sda6    on  /var/log/audit                               type  ext4         (rw,nosuid,nodev,noexec,relatime)
binfmt_misc  on  /proc/sys/fs/binfmt_misc                     type  binfmt_misc  (rw,nosuid,nodev,noexec,relatime)
tmpfs        on  /run/credentials/serial-getty@ttyS0.service  type  tmpfs        (ro,nosuid,nodev,noexec,relatime,nosymfollow,size=1024k,nr_inodes=1024,mode=700,inode64,noswap)
tmpfs        on  /run/credentials/getty@tty1.service          type  tmpfs        (ro,nosuid,nodev,noexec,relatime,nosymfollow,size=1024k,nr_inodes=1024,mode=700,inode64,noswap)
tmpfs        on  /run/user/1000                               type  tmpfs        (rw,nosuid,nodev,relatime,size=391824k,nr_inodes=97956,mode=700,uid=1000,gid=1000,inode64)
# df -h
Filesystem      Size  Used Avail Use% Mounted on
udev            1.9G     0  1.9G   0% /dev
tmpfs           383M  592K  383M   1% /run
/dev/sda1       9.6G  1.2G  8.0G  13% /
tmpfs           1.9G     0  1.9G   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           1.0M     0  1.0M   0% /run/credentials/systemd-journald.service
tmpfs           1.9G     0  1.9G   0% /tmp
/dev/sda2       9.8G  312K  9.3G   1% /home
/dev/sda3       3.9G  324M  3.4G   9% /var
/dev/sda15      241M  290K  240M   1% /boot/efi
/dev/sda5       5.9G  158M  5.4G   3% /var/log
/dev/sda4       2.0G   44K  1.8G   1% /var/tmp
/dev/sda6       3.9G   24K  3.7G   1% /var/log/audit
tmpfs           1.0M     0  1.0M   0% /run/credentials/serial-getty@ttyS0.service
tmpfs           1.0M     0  1.0M   0% /run/credentials/getty@tty1.service
tmpfs           383M  8.0K  383M   1% /run/user/1000

1.1.2.3 Configure /home #

# findmnt -kn /home
/home /dev/sda2 ext4 rw,nosuid,nodev,relatime

/home is now on its own partition.

# findmnt -kn /home | grep -v nodev
# findmnt -kn /home | grep -v nosuid

No output for either command. Both the nodev and nosuid options are set.

1.1.2.4 Configure /var #

# findmnt -kn /var
/var /dev/sda3 ext4 rw,nosuid,nodev,relatime
# findmnt -kn /var | grep -v nodev
# findmnt -kn /var | grep -v nosuid

No output for either command. Both options are set.

1.1.2.5 Configure /var/tmp #

# findmnt -kn /var/tmp
/var/tmp /dev/sda4 ext4 rw,nosuid,nodev,noexec,relatime
# findmnt -kn /var/tmp | grep -v nodev
# findmnt -kn /var/tmp | grep -v nosuid
# findmnt -kn /var/tmp | grep -v noexec

No output for any of the three commands. nodev, nosuid, and noexec are all set.

1.1.2.6 Configure /var/log #

# findmnt -kn /var/log
/var/log /dev/sda5 ext4 rw,nosuid,nodev,noexec,relatime
# findmnt -kn /var/log | grep -v nodev
# findmnt -kn /var/log | grep -v nosuid
# findmnt -kn /var/log | grep -v noexec

No output for any of the three commands. All three options are set.

1.1.2.7 Configure /var/log/audit #

# findmnt -kn /var/log/audit
/var/log/audit /dev/sda6 ext4 rw,nosuid,nodev,noexec,relatime
# findmnt -kn /var/log/audit | grep -v nodev
# findmnt -kn /var/log/audit | grep -v nosuid
# findmnt -kn /var/log/audit | grep -v noexec

No output for any of the three commands. All three options are set.

1.2.1.2 Ensure Weak Dependencies Are Configured #

# /usr/bin/apt-config dump | /usr/bin/grep "APT::Install-"
APT::Install-Recommends "false";
APT::Install-Suggests "0";

Install-Recommends is set to false rather than 0. Although the two are likely functionally equivalent, the value is changed directly rather than relying on that assumption.

# grep "APT::Install-Recommends" /etc/apt/apt.conf.d/*
/etc/apt/apt.conf.d/00InstallRecommends:APT::Install-Recommends "false";

false is changed to 0 in /etc/apt/apt.conf.d/00InstallRecommends, and the audit is run again:

# /usr/bin/apt-config dump | /usr/bin/grep "APT::Install-"
APT::Install-Recommends "0";
APT::Install-Suggests "0";

Both values now read 0.

1.3.1.3 Ensure All AppArmor Profiles Are Enforcing #

# 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.

The first check fails, since not every profile is in enforce mode, though all are loaded. The second passes, since no running process is unconfined.

Before enforcing, the loaded profiles are reviewed to determine whether doing so is safe:

# aa-status
apparmor module is loaded.
105 profiles are loaded.
6 profiles are in enforce mode.
   /usr/bin/man
   lsb_release
   man_filter
   man_groff
   nvidia_modprobe
   nvidia_modprobe//kmod
23 profiles are in complain mode.
   Xorg
   plasmashell
   plasmashell//QtWebEngineProcess
   sbuild
   sbuild-abort
   sbuild-adduser
   sbuild-apt
   sbuild-checkpackages
   sbuild-clean
   sbuild-createchroot
   sbuild-destroychroot
   sbuild-distupgrade
   sbuild-hold
   sbuild-shell
   sbuild-unhold
   sbuild-update
   sbuild-upgrade
   transmission-cli
   transmission-daemon
   transmission-gtk
   transmission-qt
   unix-chkpwd
   unprivileged_userns
0 profiles are in prompt mode.
0 profiles are in kill mode.
76 profiles are in unconfined mode.
   1password
   Discord
   MongoDB Compass
   QtWebEngineProcess
   balena-etcher
   brave
   buildah
   busybox
   cam
   ch-checkns
   ch-run
   chrome
   chromium
   crun
   devhelp
   element-desktop
   epiphany
   evolution
   firefox
   flatpak
   foliate
   geary
   github-desktop
   goldendict
   ipa_verify
   kchmviewer
   keybase
   lc-compliance
   libcamerify
   linux-sandbox
   loupe
   lxc-attach
   lxc-create
   lxc-destroy
   lxc-execute
   lxc-stop
   lxc-unshare
   lxc-usernsexec
   mmdebstrap
   msedge
   nautilus
   notepadqq
   obsidian
   opam
   opera
   pageedit
   polypane
   privacybrowser
   qcam
   qmapshack
   qutebrowser
   rootlesskit
   rpm
   rssguard
   runc
   scide
   signal-desktop
   slack
   slirp4netns
   steam
   stress-ng
   surfshark
   systemd-coredump
   toybox
   trinity
   tup
   tuxedo-control-center
   userbindmount
   uwsgi-core
   vdens
   virtiofsd
   vivaldi-bin
   vpnns
   vscode
   wike
   wpcom
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.

These profiles match the defaults shipped by AppArmor’s own packages rather than anything custom. The benchmark notes that switching a profile to enforce mode removes its unconfined status, which can interrupt an application’s normal operation, and that further tuning may be required for anything still in active use. Since none of the loaded profiles have been customized, all of them are set to enforce, with journalctl -k | grep -i apparmor used afterward to catch anything that breaks as a result:

# aa-enforce /etc/apparmor.d/*
Setting /etc/apparmor.d/1password to enforce mode.
Profile for /etc/apparmor.d/abi not found, skipping
Profile for /etc/apparmor.d/abstractions not found, skipping
Setting /etc/apparmor.d/balena-etcher to enforce mode.
Setting /etc/apparmor.d/brave to enforce mode.
Setting /etc/apparmor.d/buildah to enforce mode.
Setting /etc/apparmor.d/busybox to enforce mode.
Setting /etc/apparmor.d/cam to enforce mode.
Setting /etc/apparmor.d/ch-checkns to enforce mode.
Setting /etc/apparmor.d/chrome to enforce mode.
Setting /etc/apparmor.d/chromium to enforce mode.
Setting /etc/apparmor.d/ch-run to enforce mode.
Setting /etc/apparmor.d/code to enforce mode.
Setting /etc/apparmor.d/crun to enforce mode.
Setting /etc/apparmor.d/devhelp to enforce mode.
Profile for /etc/apparmor.d/disable not found, skipping
Setting /etc/apparmor.d/Discord to enforce mode.
Setting /etc/apparmor.d/element-desktop to enforce mode.
Setting /etc/apparmor.d/epiphany to enforce mode.
Setting /etc/apparmor.d/evolution to enforce mode.
Setting /etc/apparmor.d/firefox to enforce mode.
Setting /etc/apparmor.d/flatpak to enforce mode.
Setting /etc/apparmor.d/foliate to enforce mode.
Profile for /etc/apparmor.d/force-complain not found, skipping
Setting /etc/apparmor.d/geary to enforce mode.
Setting /etc/apparmor.d/github-desktop to enforce mode.
Setting /etc/apparmor.d/goldendict to enforce mode.
Setting /etc/apparmor.d/ipa_verify to enforce mode.
Setting /etc/apparmor.d/kchmviewer to enforce mode.
Setting /etc/apparmor.d/keybase to enforce mode.
Setting /etc/apparmor.d/lc-compliance to enforce mode.
Setting /etc/apparmor.d/libcamerify to enforce mode.
Setting /etc/apparmor.d/linux-sandbox to enforce mode.
Profile for /etc/apparmor.d/local not found, skipping
Setting /etc/apparmor.d/loupe to enforce mode.
Setting /etc/apparmor.d/lsb_release to enforce mode.
Setting /etc/apparmor.d/lxc-attach to enforce mode.
Setting /etc/apparmor.d/lxc-create to enforce mode.
Setting /etc/apparmor.d/lxc-destroy to enforce mode.
Setting /etc/apparmor.d/lxc-execute to enforce mode.
Setting /etc/apparmor.d/lxc-stop to enforce mode.
Setting /etc/apparmor.d/lxc-unshare to enforce mode.
Setting /etc/apparmor.d/lxc-usernsexec to enforce mode.
Setting /etc/apparmor.d/mmdebstrap to enforce mode.
Setting /etc/apparmor.d/MongoDB_Compass to enforce mode.
Setting /etc/apparmor.d/msedge to enforce mode.
Setting /etc/apparmor.d/nautilus to enforce mode.
Setting /etc/apparmor.d/notepadqq to enforce mode.
Setting /etc/apparmor.d/nvidia_modprobe to enforce mode.
Setting /etc/apparmor.d/obsidian to enforce mode.
Setting /etc/apparmor.d/opam to enforce mode.
Setting /etc/apparmor.d/opera to enforce mode.
Setting /etc/apparmor.d/pageedit to enforce mode.
Setting /etc/apparmor.d/plasmashell to enforce mode.
Setting /etc/apparmor.d/polypane to enforce mode.
Setting /etc/apparmor.d/privacybrowser to enforce mode.
Setting /etc/apparmor.d/qcam to enforce mode.
Setting /etc/apparmor.d/qmapshack to enforce mode.
Setting /etc/apparmor.d/QtWebEngineProcess to enforce mode.
Setting /etc/apparmor.d/qutebrowser to enforce mode.
Setting /etc/apparmor.d/rootlesskit to enforce mode.
Setting /etc/apparmor.d/rpm to enforce mode.
Setting /etc/apparmor.d/rssguard to enforce mode.
Setting /etc/apparmor.d/runc to enforce mode.
Setting /etc/apparmor.d/sbuild to enforce mode.
Setting /etc/apparmor.d/sbuild-abort to enforce mode.
Setting /etc/apparmor.d/sbuild-adduser to enforce mode.
Setting /etc/apparmor.d/sbuild-apt to enforce mode.
Setting /etc/apparmor.d/sbuild-checkpackages to enforce mode.
Setting /etc/apparmor.d/sbuild-clean to enforce mode.
Setting /etc/apparmor.d/sbuild-createchroot to enforce mode.
Setting /etc/apparmor.d/sbuild-destroychroot to enforce mode.
Setting /etc/apparmor.d/sbuild-distupgrade to enforce mode.
Setting /etc/apparmor.d/sbuild-hold to enforce mode.
Setting /etc/apparmor.d/sbuild-shell to enforce mode.
Setting /etc/apparmor.d/sbuild-unhold to enforce mode.
Setting /etc/apparmor.d/sbuild-update to enforce mode.
Setting /etc/apparmor.d/sbuild-upgrade to enforce mode.
Setting /etc/apparmor.d/scide to enforce mode.
Setting /etc/apparmor.d/signal-desktop to enforce mode.
Setting /etc/apparmor.d/slack to enforce mode.
Setting /etc/apparmor.d/slirp4netns to enforce mode.
Setting /etc/apparmor.d/steam to enforce mode.
Setting /etc/apparmor.d/stress-ng to enforce mode.
Setting /etc/apparmor.d/surfshark to enforce mode.
Setting /etc/apparmor.d/systemd-coredump to enforce mode.
Setting /etc/apparmor.d/toybox to enforce mode.
Setting /etc/apparmor.d/transmission to enforce mode.
Setting /etc/apparmor.d/trinity to enforce mode.
Profile for /etc/apparmor.d/tunables not found, skipping
Setting /etc/apparmor.d/tup to enforce mode.
Setting /etc/apparmor.d/tuxedo-control-center to enforce mode.
Setting /etc/apparmor.d/unix-chkpwd to enforce mode.
Setting /etc/apparmor.d/unprivileged_userns to enforce mode.
Setting /etc/apparmor.d/userbindmount to enforce mode.
Setting /etc/apparmor.d/usr.bin.man to enforce mode.
Setting /etc/apparmor.d/uwsgi-core to enforce mode.
Setting /etc/apparmor.d/vdens to enforce mode.
Setting /etc/apparmor.d/virtiofsd to enforce mode.
Setting /etc/apparmor.d/vivaldi-bin to enforce mode.
Setting /etc/apparmor.d/vpnns to enforce mode.
Setting /etc/apparmor.d/wike to enforce mode.
Setting /etc/apparmor.d/wpcom to enforce mode.
Setting /etc/apparmor.d/Xorg to enforce mode.

The audit is run again:

# apparmor_status | grep profiles
105 profiles are loaded.
105 profiles are in enforce mode.
0 profiles are in complain mode.
0 profiles are in prompt mode.
0 profiles are in kill mode.
0 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.

All 105 profiles are now in enforce mode.

# sysctl fs.protected_symlinks
fs.protected_symlinks = 1

The runtime value is already correct.

#!/usr/bin/env bash
{
  l_parameter_name="fs.protected_symlinks"
  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[@]}" ""
}
 - "fs.protected_symlinks = 1" is set in:
 "/usr/lib/sysctl.d/50-default.conf"

The parameter is set persistently at boot as well as at runtime.

A Lynis rescan is then performed:

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

  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 rose from 74 to 78 following this pass.