Fransys

Tech blog — Architecture, Cloud & DevOps

BlogServicesContactAbout

Follow me

githubGitHublinkedinLinkedinmailMail

© 2026 Fransys • Fransys

Fransys

Categories

  • All posts
  • Tags
  • productivity10
  • nas10
  • ai8
  • security7
  • self-hosting7
  • linux6
  • claude-code6
  • neovim5
  • docker5
  • editor4
  • networking4
  • mcp3
  • vpn3
  • lua2
  • terminal2
naslinuxsecurityhardening

Hardening your NAS Linux kernel to CIS Level 2 standards

Published on
January 27, 2026·7 min read
Avatar François GUERLEZFrançois GUERLEZ

Beyond the firewall game

A firewall helps. Fail2ban helps too. But let's be honest - that's just the obvious layer. The Linux kernel itself? Default settings way too permissive, unnecessary modules sitting in memory, mount points with no restrictions. It's a mess if you don't touch it. And the attack vectors? All documented. All known.

I run a TerraMaster F4-424 on Debian 13 (Trixie). After a while, I got tired of just basic protection. I wanted real hardening. Enter the CIS benchmarks (Center for Internet Security). Not a random forum post. Not an AI article. The actual industry standard. They have two levels: Level 1 for standard production, Level 2 for paranoid mode (or datacenters). A NAS storing family photos? That's Level 2. No debate.

The devsec.hardening collection

I could've spent an evening manually applying 200+ CIS recommendations. Sounds fun. Instead, I use the devsec.hardening Ansible collection. It's already done. It's maintained. The community has blessed it.

What I did: organized everything into specialized CIS roles - cis_permissions, cis_accounts, cis_cron, cis_sudo, cis_tcpwrappers, cis_logging, cis_services, cis_ntp. Each tagged. Each independent. Need just logging? One Ansible command, boom.

Forty-plus sysctl parameters

The file /etc/sysctl.d/99-hardening.conf holds most of it. Not two or three things. Everything that matters:

Memory and kernel protections

# Disable io_uring (frequent attack vector)
kernel.io_uring_disabled = 2

# Prevent dynamic kernel loading (kexec)
kernel.kexec_load_disabled = 1

# Full ASLR (Address Space Layout Randomization)
kernel.randomize_va_space = 2

# Restrict kernel pointer exposure in logs
kernel.kptr_restrict = 2

# Restrict dmesg to root users
kernel.dmesg_restrict = 1

# Prevent core dumps for SUID programs
fs.suid_dumpable = 0

# Restrict access to perf events
kernel.perf_event_paranoid = 3

# Disable vsyscalls (ROP gadget 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 ICMP broadcast requests
net.ipv4.icmp_echo_ignore_broadcasts = 1

# SYN flood protection
net.ipv4.tcp_syncookies = 1

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

Forty-something parameters total. Each one closes a real hole. Each one is in the CIS benchmark. No "just in case". No reading tea leaves from some blog.

GRUB: boot time parameters

Some hardening needs to happen at boot, when the kernel loads. The GRUB line:

GRUB_CMDLINE_LINUX="slab_nomerge pti=on randomize_kstack_offset=on vsyscall=none debugfs=off"
  • slab_nomerge: prevents slab cache merging (stops type confusion attacks)
  • pti=on: Page Table Isolation, the Spectre/Meltdown mitigation
  • randomize_kstack_offset: kernel stack randomized on every syscall
  • vsyscall=none: kills vsyscalls, eliminates a known ROP gadget
  • debugfs=off: shuts down the kernel debug filesystem

The slab_nomerge thing surprised me first time through. Modern attack specific, but once you get it (different memory structures can't merge together), it clicks.

Blacklisting unnecessary modules

Kernel modules you'll never use but could be exploited? We blacklist them. File /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? Cleaner than plain blacklist. Why? A blacklist can get bypassed by dependencies. /bin/true means "install nothing, silently, done." Spent 45 minutes debugging squashfs loading anyway. Once I switched to /bin/true, problem gone.

Hardening mount points

Sensitive mount points get restrictive options 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 execution (attacker can't drop payload and run it)
  • nosuid: ignore SUID/SGID bits
  • nodev: prevent device file creation

For /proc, add hidepid=2 which stops regular users from seeing other users' processes:

proc  /proc  proc  defaults,hidepid=2  0 0

I tested removing hidepid=2 for a week. Surprising what you learn about the system just watching /proc. Anyway, turned it back on.

AppArmor in enforce mode

AppArmor gives you MAC (Mandatory Access Control). Even if a process gets compromised, it can only touch what its profile allows. On Debian 13, AppArmor defaults to enabled but in "complain" mode (logging only). Hardening switches it to enforce:

aa-enforce /etc/apparmor.d/*

One command. Small change. Difference between "we found out later" and "it never happened in the first place."

Password policies (NIST SP 800-63B)

Following actual NIST recommendations, not gut feeling:

# /etc/security/pwquality.conf
minlen = 15          # 15 character minimum
minclass = 3         # At least 3 character classes
maxrepeat = 3        # Max 3 consecutive identical characters
# /etc/login.defs
PASS_MAX_DAYS  365   # Expires after 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 15-minute delay via PAM (pam_faillock). Core dumps globally disabled in /etc/security/limits.conf.

The 60-second timeout feels long at first. After two days you forget it exists.

Automatic updates without drama

unattended-upgrades applies security patches daily, auto-reboot at 4 AM if needed:

# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

An unpatched NAS is a compromised NAS eventually. I chose 4 AM. Nobody's streaming at 4 AM (at least I hope). Everyone hates it for the first two weeks, then stops noticing.

AIDE: file integrity monitoring

AIDE (Advanced Intrusion Detection Environment): you give it a baseline, it watches for changes. Actionable logs. After three months, it catches tampering before the attacker even knows they left fingerprints.

# Initialize 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 reason - and there's almost never a good reason - AIDE finds it.

Audit logging with auditd

auditd tracks security events. 40+ rules:

# /etc/audit/rules.d/hardening.rules

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

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

# Package management
-w /usr/bin/dpkg -p x -k packages
-w /usr/bin/apt -p x -k packages

# Systemd monitoring
-w /etc/systemd/ -p wa -k systemd
-w /usr/lib/systemd/ -p wa -k systemd

These logs matter after the fact. Who did what, when, with what. Not for passing an exam. For saying "OK, at 2:27 PM someone modified sudoers and changed the hostname." From there you investigate.

The takeaway

CIS Level 2 on a home NAS? Sounds paranoid. Here's the thing though: every parameter closes something real and documented. Not folklore. Not some random person's theory. Thanks to devsec.hardening and Ansible roles, it applies in one command and maintains itself. The actual work is understanding what each rule does. That's not "blindly applying a benchmark" - that's actually knowing your system. That's what makes the difference.


Debian NAS from scratch series — This article is part of a complete series on building a Debian NAS.

Previous: Firewall and Fail2ban: locking down NAS network access | Next: Securing SSH with post-quantum algorithms

Previous post

← AI completion in Neovim: Codeium, Gemini and nvim-cmp

Next post

Migrating Synology data to a Debian NAS without losing anything→
← Back to blog

Table of Contents

  • Beyond the firewall game
  • The devsec.hardening collection
  • Forty-plus sysctl parameters
  • Memory and kernel protections
  • Network protections
  • GRUB: boot time parameters
  • Blacklisting unnecessary modules
  • Hardening mount points
  • AppArmor in enforce mode
  • Password policies (NIST SP 800-63B)
  • Automatic updates without drama
  • AIDE: file integrity monitoring
  • Audit logging with auditd
  • The takeaway