Controlling kernel modules
Autoloading, blacklists and locking loading.
A kernel module is a piece of kernel code, usually a driver, filesystem or network protocol, that is loaded into the running kernel when something needs it. Once loaded it runs with the kernel's full privilege, so every module that can be loaded is attack surface, and some can be loaded by an ordinary user without any privilege at all. In this lesson you watch an unprivileged program pull a protocol module into the kernel, measure what Ubuntu and RHEL already block, and learn the difference between the two modprobe.d rules people use: blacklist, which closes one path, and install ... /bin/false, which closes them all. You also see what a rule does not reach (the initramfs, insmod, modules already loaded) and the stronger controls behind it.
How a module gets loaded without anyone asking
The kernel loads most modules on demand. When a program opens a socket for a protocol family the kernel has no code for, mounts a filesystem type it does not know, or a device appears, the kernel builds a name for what it needs (an alias such as net-pf-29, "network protocol family 29") and asks modprobe to provide it. modprobe reads its configuration in /etc/modprobe.d/ and /usr/lib/modprobe.d/, matches the alias against the aliases each module declares, and inserts the module with any others it depends on. The request runs as root, whoever triggered it.
That is how a bug in an obscure protocol becomes a local privilege escalation: an unprivileged user opens one socket, the kernel loads the buggy code, and the user attacks it. The textbook case, DCCP (CVE-2017-6074), is history now: DCCP was removed from the kernel in Linux 6.16, and neither lab kernel has it. The mechanism is unchanged, though. modinfo shows the aliases a module answers to; SCTP answers to "family 2 or 10 (IPv4, IPv6), protocol 132", and the CAN bus modules to family 29:
CAN (Controller Area Network) is a vehicle and industrial bus, and a server has no use for it, which makes it a clean example. As the unprivileged deploy user, create one CAN socket and compare lsmod (the list of loaded modules) before and after:
One socket loaded two modules into the kernel. modprobe -r removed them again because nothing was using them. That is not always possible: in the same experiment with SCTP, the module stayed "in use" after the socket closed and could not be removed, so on that VM only a reboot takes it out. A rule you write later affects the next load, not what is already in memory.
What Ubuntu and RHEL already block
Measure before you write rules. Ubuntu ships a set of files in /etc/modprobe.d/ and /usr/lib/modprobe.d/; most blacklist drivers that conflict with better ones. The security-relevant one is blacklist-rare-network.conf:
alias net-pf-21 off points the alias for RDS sockets at a module named off, which does not exist, so an unprivileged RDS socket cannot load anything. The list covers old protocol families; SCTP, TIPC and CAN are not on it. RHEL goes further: many rarely used modules are not in the base kernel packages at all but in kernel-modules-extra, and that package ships blacklist files for the protocols in it that a user could autoload.
The CIS RHEL 10 Level 1 profile in scap-security-guide 0.1.82 lists modules to disable: old filesystems (cramfs, freevxfs, hfs, hfsplus, jffs2), usb-storage, firewire-core, and the dccp, rds, sctp, tipc, can and atm protocols; many hardening lists add udf and squashfs. Check which of them exist on each kernel before writing anything. modinfo -F filename prints the module file, (builtin) for code compiled into the kernel, or an error when there is nothing to block:
On Ubuntu most of the list exists as loadable modules, and squashfs is built in: modprobe.d cannot stop built-in code, and snap packages need it anyway. On the Rocky lab only udf and usb-storage exist for the running kernel, so the unprivileged SCTP socket fails with "Protocol not supported" because there is nothing to load. Write rules for what exists and what the host does not need; a rule for a module that is not there is harmless but proves nothing.
blacklist against install /bin/false
The modprobe.d(5) man page describes blacklist narrowly: it makes modprobe ignore a module's own internal aliases. Test exactly that. With blacklist can in place, the unprivileged socket no longer loads anything, and Python reports "Address family not supported by protocol":
The alias path is closed, but nothing else is. Loading the module by name still works, and so does loading a module that depends on it: can_raw needs can, and modprobe inserts can without looking at the blacklist. Another module, or anyone with sudo, can still bring it in.
install MODULE COMMAND tells modprobe to run COMMAND instead of inserting the module, on every path that goes through modprobe: by alias, by name, and as a dependency. /bin/false does nothing and fails, so every attempt fails visibly. (/bin/true also stops the load, but reports success, which hides the attempt from scripts.) Keep the blacklist line too; it is what RHEL ships and what compliance scanners look for.
# SecOpsLog: this host has no CAN bus. "install" makes every modprobe of# can run /bin/false instead: by name, by alias, or as a dependency.install can /bin/false# "blacklist" also ignores the module's own aliases (the form RHEL ships).blacklist can
modprobe -n -v is a dry run: it prints what modprobe would do without doing it, and needs no root. For can_raw it shows the install command for the dependency first. Then every path fails, and lsmod confirms nothing was loaded:
The same gap explains why RHEL ships a separate blacklist file for sctp_diag: it has aliases of its own and depends on sctp, so blacklisting sctp alone would leave a way to load it. An install rule for the base module closes that gap. The CIS RHEL 10 profile checks for install MODULE /bin/false (it accepts /bin/true too). These rules are CIS and SecOpsLog recommendations, not distribution defaults, apart from Ubuntu's alias ... off list and RHEL's blacklist files.
Boot, impact and rollback
Early boot runs from the initramfs, a small filesystem image the boot loader loads with the kernel, and it carries its own copy of /etc/modprobe.d from the day it was built. Both Ubuntu 26.04 and RHEL 10 build it with dracut; on Ubuntu, update-initramfs is dracut's compatibility wrapper. The lab's initramfs has Ubuntu's seven files and not the new one:
A rule for a network protocol only matters once the system is up, so the running system's copy is enough. A rule for a storage or filesystem driver that the initramfs may load needs a rebuild to apply from the first second: sudo update-initramfs -u on Ubuntu, sudo dracut -f --regenerate-all on RHEL. The lab does not rebuild it, because a broken initramfs is a host that does not boot; do it where you have a console, and keep the previous kernel's entry in the boot menu as the fallback.
Operational impact comes from blocking something the host turns out to need. udf reads DVD images and the provisioning ISO that some cloud platforms (Azure among them) attach at first boot. usb-storage is how people plug in a disk at a physical console. squashfs is built into Ubuntu's kernel and cannot be blocked there. Before you roll a rule out, check lsmod on a sample of hosts for anything on your list. Rollback is to delete the file (and rebuild the initramfs if you rebuilt it for the rule); the next load works at once:
Stronger controls: the latch, signatures and audit
modprobe.d binds modprobe and nothing else. Root can insert a module file directly with insmod, which reads no configuration. Three controls reach further, and you can read their state without changing anything:
kernel.modules_disabled is 0. Writing 1 stops all module loading and unloading, by any path including insmod, until reboot; it is a one-way latch and cannot be set back to 0. That is its value (root cannot undo it either) and its danger: everything the host will ever need must be loaded before the latch is set. Put in a sysctl.d file, it is applied early in boot by systemd-sysctl, and every module needed after that point fails to load for the rest of the boot: nf_tables (a module on Ubuntu) when the firewall loads its ruleset, a driver for hardware plugged in later, a filesystem mounted later. It suits single-purpose appliances where the full module set is known and tested; the lab only reads it.
/proc/sys/kernel/tainted is 0, meaning no unsigned, out-of-tree or forced module has been loaded this boot; a non-zero value on a host that should run only distribution modules is worth investigating. The shipped can module is signed with the kernel build key, but with lockdown at [none] an unsigned module would still load; the disk encryption and boot integrity lesson explains signature enforcement through Secure Boot and lockdown. For a record of who loaded what, the auditd lesson's modules rule already logs every init_module, finit_module and delete_module call, and auditd ships the same rules as 43-module-load.rules.
Try this
Repeat the experiment with another CAN protocol on a lab Ubuntu machine. As an ordinary user, run python3 -c 'import socket; socket.socket(socket.AF_CAN, socket.SOCK_DGRAM, socket.CAN_BCM)' and check lsmod | grep '^can': can_bcm and can are loaded. Remove them with sudo modprobe -r can_bcm. Now write blacklist can_bcm into /etc/modprobe.d/can-bcm.conf and predict, before running each, whether the socket, sudo modprobe can_bcm and modprobe -n -v can_bcm will succeed; then run them and compare. Unload can_bcm again, replace the file's contents with install can_bcm /bin/false and blacklist can_bcm, predict again, and run the dry run and sudo modprobe can_bcm. Delete the file when you are done. What the lab saw: with the blacklist, the socket failed with "Protocol not supported" (the CAN address family was there, the BCM protocol module was not loaded), while the load by name worked; with the install line, the dry run printed install /bin/false and sudo modprobe can_bcm failed.
Takeaway
Check which listed modules exist on each kernel, then block the ones the host never needs with install MODULE /bin/false plus blacklist, because blacklist alone leaves loading by name and by dependency open. Remember what the file does not reach: modules already loaded, the initramfs until you rebuild it, and insmod, which only kernel.modules_disabled or enforced signatures stop.