CoursesLinux hardeningKernel module control

Kernel module control

Blacklist what you never load.

Advanced10 min · lesson 10 of 16

Your kitchen has a drawer of attachments for the stand mixer. Dough hook, whisk, pasta roller, and a couple of odd tools you have never once used. The mixer will snap in whichever attachment a recipe asks for, right away, no questions asked. The Linux kernel (the core program that talks straight to your hardware) keeps a drawer like this too. Its attachments are called modules: drivers and features it can click into a running system the moment something needs one. Convenient. Also a problem, because a few attachments in that drawer are old, rusty, and sharp.

A kernel module runs with full kernel power. Once it loads, its code is part of the most privileged program on the machine. Most modules are fine and you want them there. But a handful cover strange filesystems and forgotten network protocols your server will never touch, and some of those have shipped with real security holes. Give an attacker a way to make the kernel load one of those buggy modules and you have handed them a bug to attack at the highest privilege level on the box. The hardening move is one plain idea: tape over the drawer slots you will never use, so the kernel refuses to fetch those attachments at all.

How a module loads when you never asked

Here is the part that surprises people. You do not type anything to load a module. The kernel autoloads on demand. Plug in a USB webcam and the kernel reads the device's ID, looks up which module claims that ID, and loads it for you. Ask the network stack for a socket of an unusual type and it does the same for the protocol code behind it. The lookup runs on aliases, small 'if you see X, load module Y' labels baked into each module. When the kernel decides it needs one, it hands the job to a tiny user-space helper called modprobe, which finds the file and inserts it.

That on-demand path is the attack surface. An ordinary user with no special powers cannot load a module by hand, but they can open a socket, and opening a socket for an odd protocol is enough to make the kernel go fetch the code behind it, running the loader with full root power on the user's behalf. DCCP (Datagram Congestion Control Protocol, a transport protocol almost nobody uses) is the textbook case. In 2017 a use-after-free bug in the kernel's DCCP code, tracked as CVE-2017-6074 (a CVE is a public ID for a known security flaw), let a local user climb all the way to root, and the trigger was creating one DCCP socket. On a server that never speaks DCCP, that code had no business being in memory in the first place.

~/secopslog — bash
$ # nothing DCCP is loaded on a fresh server lsmod | grep dccp # a normal, non-root user opens a single DCCP socket... python3 -c 'import socket; socket.socket(socket.AF_INET, socket.SOCK_DCCP, socket.IPPROTO_DCCP)' # ...and the kernel has now pulled the whole DCCP stack into itself lsmod | grep dccp
dccp_ipv4 24576 0 dccp 118784 1 dccp_ipv4
Where a modprobe.d rule stops an autoload
1Something needs a feature
socket(SOCK_DCCP), a plugged-in device, a first-time mount
2Kernel resolves an alias
'who handles this?' maps to a module name
3Kernel calls modprobe
request_module() shells out to user space
4modprobe reads /etc/modprobe.d
your install and blacklist rules
5Rule refuses, or no rule loads it
install /bin/false stops every path right here
6Module runs inside the kernel
full privilege, unless step 5 blocked it

Blacklist the modules you will never use

The fix lives in /etc/modprobe.d, a directory of rules modprobe reads every time it runs. Create one file for your hardening rules. The protocols below are DCCP, SCTP (Stream Control Transmission Protocol), RDS (Reliable Datagram Sockets), and TIPC (Transparent Inter-Process Communication), all niche transports with a CVE history, alongside a few old filesystem formats a normal server never mounts, like cramfs (a tiny compressed read-only filesystem) and udf (the format used on DVDs).

/etc/modprobe.d/hardening.conf
# --- Old, niche filesystems this host will never mount ---
install cramfs /bin/false
install freevxfs /bin/false
install jffs2 /bin/false
install udf /bin/false
# --- Obscure network protocols with a history of kernel bugs ---
install dccp /bin/false
install sctp /bin/false
install rds /bin/false
install tipc /bin/false
# Also strip each module's own autoload aliases (belt and suspenders)
blacklist dccp
blacklist sctp
blacklist rds
blacklist tipc

Read 'install cramfs /bin/false' as a swap. It tells modprobe that 'loading' cramfs now means running /bin/false, a tiny program that does nothing and exits with a failure code. Every attempt to load that module through modprobe, whether the kernel triggered it automatically or someone typed it by hand, runs /bin/false and gives up. The real module never enters the kernel. The older version of this recipe pointed at /bin/true (does nothing, exits success), and that blocks the load equally well. /bin/false is the better choice because anything that scripted the load now sees a clear non-zero failure instead of a quiet, misleading 'success'.

The 'blacklist' lines do something narrower, and the difference matters. 'blacklist dccp' tells modprobe to ignore that module's built-in autoload aliases. It shuts the automatic door, the one the DCCP socket trick walked through. But a direct 'modprobe dccp' by name still loads the module, because blacklist never touched that path. That gap is the whole reason the 'install' line is the real lock, and why the CIS benchmarks (CIS is the Center for Internet Security, which publishes widely used hardening checklists) pair the two directives together.

~/secopslog — bash
$ sudo modprobe dccp ; echo "exit=$?" modprobe -n -v dccp # dry run: what WOULD loading do? lsmod | grep -c dccp
exit=1 install /bin/false 0

New rules take effect for the next load, not retroactively. A module already sitting in memory stays until you remove it with 'sudo modprobe -r dccp' or reboot, so check 'lsmod' after you write the file. One more step people forget: some filesystem modules get baked into the initramfs (the small temporary filesystem the kernel unpacks early in boot), so run 'sudo update-initramfs -u' to carry your blacklist into that early copy too.

Turn module loading off entirely

Blacklisting handles modules you can name ahead of time. For a locked-down, single-purpose host you can go further and weld the drawer shut once everything you need is already snapped in. The switch is a kernel tunable named kernel.modules_disabled, set through sysctl (the interface for reading and writing kernel settings on a running system). Flip it to 1 and the running kernel refuses every new module load, by any path, until the next reboot.

~/secopslog — bash
$ sudo sysctl -w kernel.modules_disabled=1 sudo modprobe msr # try to load msr, an ordinary, unblocked module sudo sysctl -w kernel.modules_disabled=0
kernel.modules_disabled = 1 modprobe: ERROR: could not insert 'msr': Operation not permitted sysctl: setting key "kernel.modules_disabled": Invalid argument

Notice the last line. This knob is a one-way latch: it takes only the value 1, so you cannot set it back to 0, and the kernel rejects the attempt as an invalid argument. A reboot is the only reset, which is the whole point. An attacker who lands root on this host still cannot load a kernel module, so the usual route to planting a kernel rootkit is shut for the rest of this boot. It also covers a gap that blacklists leave open. A modprobe.d rule binds only modprobe, and a root user can walk around modprobe and shove a module file straight in with 'insmod /path/to/module.ko' (insmod is the bare-bones insert-a-module command: no alias lookup, no install rules, just this file into the kernel). Because modules_disabled blocks the underlying load syscall itself, it stops insmod too. The other control for that same gap is enforcing signed modules, so the kernel loads only modules signed by a trusted key (Secure Boot and kernel lockdown can require this).

modules_disabled is easy to fire into your own foot
Because it is a one-way latch, dropping kernel.modules_disabled=1 into /etc/sysctl.d applies it on every boot, and systemd reads those files early. Set it before all your real drivers and filesystems have loaded and the host can fail to mount a disk, refuse a hot-plugged device, or lose networking, with no way to undo it short of booting to recovery. Load everything you need first (pin required modules in /etc/modules-load.d), test on a machine whose console you can actually reach, then enable it. Good fit for appliances, handle general servers with care.

What a defender watches for

Controls are only half the job. You also want to see the day one of these lines gets crossed. Start with a baseline: 'lsmod' on a healthy host is your known-good list, and anything new showing up later is a question to answer. The kernel also keeps a running 'tainted' flag that records whether anything unusual has been loaded.

~/secopslog — bash
$ cat /proc/sys/kernel/tainted
0

Zero means a clean, in-tree, properly signed set of modules. The value is a bitmask, and loading an out-of-tree or unsigned module flips specific bits (an unsigned module, for instance, sets the 8192 bit). A box that should only ever run stock, signed modules suddenly reporting a non-zero taint is worth a hard look. For a real record of who loaded what and when, put the module syscalls under the audit daemon (auditd), the built-in Linux service that logs security-relevant kernel events.

/etc/audit/rules.d/50-modules.rules
## Log every kernel module load and unload, tagged "modules"
-a always,exit -F arch=b64 -S init_module,finit_module,delete_module -k modules
-a always,exit -F arch=b32 -S init_module,finit_module,delete_module -k modules

init_module and finit_module are the syscalls that insert a module; delete_module removes one. With those rules loaded ('sudo augenrules --load'), every load and unload lands in the audit log stamped with the user, the timestamp, and the command, and 'sudo ausearch -k modules' replays them during an investigation. An unexpected entry in 'lsmod', matched to a load event whose parent process has no business touching the kernel, is one of the cleaner signals that someone pushed a kernel rootkit onto the host.

One hard truth sits under all of this. Asking a compromised kernel what modules are loaded is like asking a burglar whether anyone broke in. Once a malicious module is inside the kernel, it owns the very code your tools lean on for answers. It can hide its own process, files, and network connections from ps, ls, ss, and even lsmod, because every one of those tools asks the kernel, and the kernel is now lying to them. That is why the strongest signals come from outside the running system: an audit log shipped off the box before anyone can edit it, a taint flag you noticed before the rootkit learned to fake it, a file baseline checked from a trusted boot. Treat a loaded module you cannot explain as an incident, not a curiosity.

Quick check
01You want to be certain an obscure protocol module can never be loaded through modprobe on a running server, short of disabling all module loading. Which line does that?
Incorrect — blacklist only strips the module's autoload aliases, so an explicit 'modprobe dccp' by name still loads it.
Correct — modprobe runs /bin/false instead of inserting the module, so both autoload and a hand-typed modprobe fail to load it.
Incorrect — 'options' passes parameters to a module when it loads; there is no generic 'disabled' flag, and it does nothing to stop the load.
Incorrect — that force-loads dccp at every boot, the exact opposite of what you want.
02An ordinary user with no special privileges can't run modprobe or insmod. How can that user still make the kernel load a vulnerable module like DCCP (Datagram Congestion Control Protocol)?
Correct — asking for an unusual socket type makes the kernel fetch the code behind it, and that on-demand autoload is the attack surface.
Incorrect — writing to /etc/modprobe.d needs root, so an unprivileged user can't change those rules.
Incorrect — the whole point is that autoload runs the loader as root, triggered by an unprivileged action like opening a socket.
Incorrect — reading kallsyms only prints symbol names and addresses; it never loads a module.
03You've written install and blacklist rules for every risky module, but you want to guarantee that even an attacker who gains root can't insert a kernel module for the rest of this boot. Why aren't the modprobe.d rules enough, and what closes the gap?
Incorrect — modprobe.d rules bind only modprobe, and root can bypass them entirely with insmod.
Incorrect — blacklist only strips autoload aliases and doesn't even stop a by-name modprobe, let alone insmod.
Incorrect — that only carries the same modprobe.d rules earlier into boot; insmod still bypasses modprobe.
Correct — modprobe.d rules bind only modprobe, whereas modules_disabled blocks loading at the syscall level as a one-way latch, closing the insmod path.

Try this

Work through “What a defender watches for” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: modules_disabled is easy to fire into your own foot. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related