
Hardening your NAS Linux kernel to CIS Level 2 standards
Beyond the firewall
A firewall and fail2ban only cover the surface. The Linux kernel itself — permissive defaults, unused modules loaded in memory, mount points without restrictions — remains wide open, and those attack vectors are all documented and exploitable.
The CIS benchmarks (Center for Internet Security) offer two levels: Level 1 for standard servers, Level 2 for sensitive environments. For a NAS holding family photos, backups and home automation data, it's Level 2. Reference setup here: a TerraMaster F4-424 running Debian 13 (Trixie).
The devsec.hardening collection
Applying 200+ CIS recommendations by hand makes no sense when the devsec.hardening Ansible collection already does it, maintained and community-validated.
The setup splits it into specialised CIS roles — cis_permissions, cis_accounts, cis_cron, cis_sudo, cis_tcpwrappers, cis_logging, cis_services, cis_ntp — each tagged and independent. Running just the logging role is one Ansible command.
Over 40 sysctl parameters
/etc/sysctl.d/99-hardening.conf holds the bulk of it.
Memory and kernel protections
# Disable io_uring (frequent attack vector)
kernel.io_uring_disabled = 2
# Forbid dynamic kernel loading (kexec)
kernel.kexec_load_disabled = 1
# Full ASLR (address space randomisation)
kernel.randomize_va_space = 2
# Restrict kernel pointer exposure in logs
kernel.kptr_restrict = 2
# Restrict dmesg to root
kernel.dmesg_restrict = 1
# Forbid core dumps for SUID programs
fs.suid_dumpable = 0
# Restrict access to perf events
kernel.perf_event_paranoid = 3
# Disable vsyscalls (ROP vector)
# (configured via GRUB: vsyscall=none)
Network protections
# Reverse path filtering (anti-spoofing)
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
# Ignore broadcast ICMP requests
net.ipv4.icmp_echo_ignore_broadcasts = 1
# SYN flood protection
net.ipv4.tcp_syncookies = 1
# Forbid source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Disable IPv6 forwarding (the NAS is not a router)
net.ipv6.conf.all.forwarding = 0
# Log martian packets
net.ipv4.conf.all.log_martians = 1
Around forty parameters in total, each documented in the CIS benchmark and closing a specific vector.
GRUB: boot parameters
Some hardening has to be applied as the kernel loads:
GRUB_CMDLINE_LINUX="slab_nomerge pti=on randomize_kstack_offset=on vsyscall=none debugfs=off"
- slab_nomerge: prevents slab cache merging, limiting type-confusion attacks
- pti=on: Page Table Isolation, the Spectre and Meltdown mitigation
- randomize_kstack_offset: the kernel stack is randomised on every syscall
- vsyscall=none: eliminates a known ROP gadget
- debugfs=off: closes the kernel debug filesystem
Blacklisting unused modules
In /etc/modprobe.d/hardening.conf:
# Exotic filesystems
install cramfs /bin/true
install freevxfs /bin/true
install jffs2 /bin/true
install hfs /bin/true
install hfsplus /bin/true
install squashfs /bin/true
install udf /bin/true
# Rarely used network protocols
install dccp /bin/true
install sctp /bin/true
install rds /bin/true
install tipc /bin/true
# USB storage (disabled on a headless NAS)
install usb-storage /bin/true
The install <module> /bin/true trick is sturdier than a plain blacklist: blacklists can be bypassed through dependencies, whereas "install /bin/true" silently does nothing. A lone blacklist squashfs still lets the module load.
Mount point hardening
In /etc/fstab:
tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0
tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0
- noexec: no binary can execute, so no dropped-then-run payload
- nosuid: SUID/SGID bits ignored
- nodev: device file creation forbidden
For /proc, hidepid=2 stops regular users from seeing other users' processes:
proc /proc proc defaults,hidepid=2 0 0
AppArmor in enforce mode
AppArmor provides MAC (Mandatory Access Control): even when compromised, a process can only touch what its profile allows. On Debian 13, AppArmor is enabled by default but often in "complain" mode (logging only). Hardening switches it to enforce:
aa-enforce /etc/apparmor.d/*
One command, but it's the difference between "we know what happened" and "we prevented it from happening".
Password policies (NIST SP 800-63B)
# /etc/security/pwquality.conf
minlen = 15 # 15 characters minimum
minclass = 3 # At least 3 character classes
maxrepeat = 3 # Max 3 identical consecutive characters
# /etc/login.defs
PASS_MAX_DAYS 365 # Expiry at 365 days
PASS_MIN_DAYS 1 # 1 day minimum between changes
LOGIN_RETRIES 3 # 3 login attempts
LOGIN_TIMEOUT 60 # 60-second timeout
UMASK 077 # Restrictive default permissions
Plus account lockout after 3 failures with a 15-minute delay via PAM (pam_faillock), and core dumps globally disabled in /etc/security/limits.conf.
Automatic updates
unattended-upgrades applies security patches daily, with an automatic reboot at 4am if needed:
# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
An unpatched NAS eventually gets compromised. The 4am window avoids interrupting real usage.
AIDE: file integrity monitoring
AIDE (Advanced Intrusion Detection Environment) compares system state against a baseline and flags everything that changed:
# Initialise the database
aide --init
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# Daily check (via cron)
aide --check
If a system binary changes without a legitimate reason — and there are few — AIDE sees it.
Audit logging with auditd
auditd traces security events, here with over 40 rules:
# /etc/audit/rules.d/hardening.rules
# Watch authentication changes
-w /etc/passwd -p wa -k auth_changes
-w /etc/shadow -p wa -k auth_changes
-w /etc/group -p wa -k auth_changes
# Watch sudo
-w /etc/sudoers -p wa -k sudo_changes
-w /etc/sudoers.d/ -p wa -k sudo_changes
# Kernel module loading
-a always,exit -F arch=b64 -S init_module -S delete_module -k modules
# Network changes
-a always,exit -F arch=b64 -S sethostname -S setdomainname -k network
# Watch packages
-w /usr/bin/dpkg -p x -k packages
-w /usr/bin/apt -p x -k packages
# Watch systemd
-w /etc/systemd/ -p wa -k systemd
-w /usr/lib/systemd/ -p wa -k systemd
These logs are what let you establish, after the fact, who did what, when and with which tool — the starting point of any investigation.
What to take away
CIS Level 2 on a home NAS can look excessive, but each parameter closes a real, documented vector. Thanks to devsec.hardening and Ansible roles, applying it is one command and maintaining it takes no effort. The real work is understanding what each rule does: applying a benchmark without reading it protects very little.
Related articles