Five Linux kernel privilege escalation bugs in five weeks. Copy Fail, Dirty Frag, Fragnesia, ssh-keysign-pwn, CIFSwitch — each targeting a kernel subsystem that most production servers load by default but never actually use. Belgian sysadmin Jasper Nuyens saw the pattern and built a direct response. ModuleJail Linux kernel hardening is a GPLv3 shell script that scans your running system, identifies every kernel module not currently in use, and blacklists them all in a single modprobe.d file — no reboot required, no daemon, no complex configuration. This post gives you the full picture: how it works internally, how to install and run it safely, how to deploy it across a fleet, and what its limitations are so you don’t break things.

modprobe.d blacklist file. No reboot. No daemon. One execution.Why Kernel Module Reduction Matters in 2026
The Linux kernel ships with thousands of modules. A typical RHEL 9 installation has over 4,000 kernel modules available on disk. A production web server probably needs fewer than 200 of them during normal operation. The rest sit loadable on disk, available to any process that calls modprobe — or that triggers an automatic load via udev or request_key.
That gap between “available” and “needed” is the attack surface ModuleJail targets. Copy Fail exploited algif_aead in the AF_ALG crypto subsystem. CIFSwitch exploited the cifs.spnego upcall path inside the CIFS client. Dirty Frag exploited esp4, esp6, and rxrpc. In every case, the vulnerable subsystem was loaded — or loadable — on systems that had no business using it.
The traditional advice is to manually blacklist dangerous modules. Every sysadmin knows it. Our Linux server hardening checklist covers it. The problem is doing it consistently at scale, and keeping up with which modules become dangerous as new CVEs drop. Manually maintaining a blacklist across 50 servers is tedious. Across 500 it’s unmanageable. ModuleJail automates the entire process.
How ModuleJail Works Internally
Understanding the mechanics matters before you trust a tool with production hardening. ModuleJail is a single POSIX shell script — no compiled binary, no hidden logic, fully auditable. Here’s what it does step by step:
Step 1 — Build the keep-set. It runs lsmod to capture every module currently loaded on the running system. This becomes the baseline of modules that will never be blacklisted.
Step 2 — Add the built-in baseline. ModuleJail maintains an internal list of modules that are almost universally needed even if they’re not loaded at scan time — things like core filesystem drivers, common network interfaces, and modules required for standard hardware. These merge into the keep-set unconditionally.
Step 3 — Merge the sysadmin whitelist. If you provide a whitelist file, those module names join the keep-set. This protects modules that legitimately aren’t loaded at scan time but will be needed later — like wireguard, nfs, or a specific GPU driver.
Step 4 — Build the universe. It walks /lib/modules/$(uname -r)/ and enumerates every module available on disk. This is the full set of potentially loadable modules.
Step 5 — Set subtraction. Universe minus keep-set. Every module in that remainder gets a blacklist entry.
Step 6 — Write the blacklist file. Each blacklisted module gets an install <module> /bin/true directive in /etc/modprobe.d/modulejail-blacklist.conf. When something calls modprobe <module> for a blacklisted module, the kernel runs /bin/true instead — the call succeeds silently without actually loading anything. Applications that check for modprobe success won’t know the module was blocked.

lsmod output, walks /lib/modules/, and computes the set difference. The result is a clean, auditable blacklist file — review it before applying to production.Install ModuleJail
ModuleJail ships as a single shell script on GitHub, plus optional .deb and .rpm packages for distro-native installation. Choose the method that fits your environment.
Option A: Direct from GitHub (all distros)
# Download the script
curl -fsSL https://raw.githubusercontent.com/jnuyens/modulejail/master/modulejail \
-o /usr/local/sbin/modulejail
chmod 700 /usr/local/sbin/modulejail
# Verify before running as root
head -10 /usr/local/sbin/modulejail
modulejail --version
Option B: RPM package (RHEL / AlmaLinux / Rocky / Fedora)
LATEST=$(curl -s https://api.github.com/repos/jnuyens/modulejail/releases/latest \
| grep browser_download_url | grep rpm | cut -d'"' -f4)
curl -fsSL "$LATEST" -o /tmp/modulejail.rpm
rpm -ivh /tmp/modulejail.rpm
modulejail --version
Option C: DEB package (Debian / Ubuntu)
LATEST=$(curl -s https://api.github.com/repos/jnuyens/modulejail/releases/latest \
| grep browser_download_url | grep deb | cut -d'"' -f4)
curl -fsSL "$LATEST" -o /tmp/modulejail.deb
dpkg -i /tmp/modulejail.deb
modulejail --version
Package installs put the binary at /usr/bin/modulejail, a man page at /usr/share/man/man8/modulejail.8, and docs under /usr/share/doc/modulejail/. A Debian package is queued in the official Debian archive (ITP #1138266 filed May 30, 2026) and will land in Debian unstable and Ubuntu universe once it clears the NEW queue.
Run It Safely: The Right Sequence
Timing is everything with ModuleJail. Run it before all services are up and you’ll blacklist modules those services need. Run it at the wrong moment and you’ll lock out a storage driver, a VPN module, or a filesystem you use occasionally. The correct sequence for a production server:
# Confirm full steady state before scanning
# All services running, all storage mounted, all network interfaces up
systemctl list-units --state=failed
df -h
ip link show
# Dry run first — see exactly what would be blacklisted without writing anything
modulejail --dry-run 2>&1 | tee /tmp/modulejail-dryrun.txt
wc -l /tmp/modulejail-dryrun.txt
# Scan for modules you actually use but might not be loaded right now
grep -E "(cifs|nfs|wireguard|bluetooth|sound|usb)" /tmp/modulejail-dryrun.txt
If the dry-run shows modules you need, add them to a whitelist. Permissions must be exactly 600 — ModuleJail refuses to read a group- or world-writable whitelist file (injection prevention):
cat > /etc/modulejail-whitelist.txt << 'WLEOF'
wireguard
nfs
nfsv4
WLEOF
chmod 600 /etc/modulejail-whitelist.txt
# Now apply for real
modulejail --whitelist /etc/modulejail-whitelist.txt
# Confirm the blacklist file exists and covers known-bad modules
ls -lh /etc/modprobe.d/modulejail-blacklist.conf
grep -c "algif_aead\|rxrpc\|cifs\|esp4\|esp6" \
/etc/modprobe.d/modulejail-blacklist.conf

What ModuleJail Protects Against — and What It Doesn't
Be precise about what this tool buys you. It's a prevention layer, not a patch. Here's the full breakdown:
| Threat | Protected? | Why |
|---|---|---|
| LPE via unused module (Copy Fail, CIFSwitch) | Yes | Module can't load if blacklisted |
| LPE via currently loaded module (Dirty Frag on active ESP) | No | Blacklist only prevents future loads |
| Zero-day in a module not yet loaded | Yes | Attack surface doesn't exist if module is blocked |
| Exploit in a whitelisted or needed module | No | Patch normally — ModuleJail can't help here |
| Container escape via kernel module | Partial | Only for modules not needed by container runtime |
| SSH brute force, web exploits, credential attacks | No | Outside ModuleJail's scope entirely |
The real value is in the third row. Every module ModuleJail blacklists is one less potential CVE that can reach your system before you have a patch ready. A server that had blacklisted esp4 and rxrpc — because it doesn't run IPsec or RxRPC workloads — would have been immune to the Dirty Frag exploit before a kernel patch existed. That's the gap ModuleJail fills: between disclosure and patch availability.

install <module> /bin/true trick makes modprobe silently succeed without loading anything. Services that check modprobe exit codes won't notice the block.Fleet Deployment with Ansible
Running ModuleJail on one server takes two minutes. Running it consistently across a fleet requires a repeatable, auditable approach. Here's a production-ready Ansible playbook:
---
- name: Deploy ModuleJail kernel hardening
hosts: linux_servers
become: true
vars:
whitelist_modules:
- wireguard
- nfs
- nfsv4
tasks:
- name: Download ModuleJail script
get_url:
url: https://raw.githubusercontent.com/jnuyens/modulejail/master/modulejail
dest: /usr/local/sbin/modulejail
mode: "0700"
owner: root
group: root
- name: Create whitelist file
copy:
dest: /etc/modulejail-whitelist.txt
mode: "0600"
owner: root
group: root
content: |
{% for mod in whitelist_modules %}
{{ mod }}
{% endfor %}
- name: Run ModuleJail dry run and save log
shell: modulejail --dry-run 2>&1
register: dryrun
changed_when: false
- name: Save dry-run log for review
copy:
dest: /var/log/modulejail-dryrun.log
content: "{{ dryrun.stdout }}"
mode: "0640"
- name: Apply ModuleJail blacklist (runs once)
shell: modulejail --whitelist /etc/modulejail-whitelist.txt
args:
creates: /etc/modprobe.d/modulejail-blacklist.conf
- name: Verify high-risk modules are blocked
shell: >
grep -c "algif_aead\|rxrpc\|cifs\|esp4\|esp6"
/etc/modprobe.d/modulejail-blacklist.conf
register: blocked
changed_when: false
- name: Report
debug:
msg: "High-risk modules confirmed blocked: {{ blocked.stdout }}"
The creates guard makes the apply task idempotent — Ansible won't re-run ModuleJail on subsequent plays once the blacklist file exists. Add a separate task to remove and regenerate the blacklist after kernel updates. For large fleets, store the dry-run logs centrally and review them before the apply task runs.
The cve-watch Companion Script
ModuleJail ships with a companion tool: cve-watch.sh. It monitors the linux-cve-announce mailing list and the NVD REST API for new vulnerabilities affecting modules not yet in your blacklist. CVE identifiers typically go public before distro patches arrive. This gives you a window to block a vulnerable module within hours of disclosure — before a patch exists.
# Run cve-watch against your current blacklist
cve-watch.sh --blacklist /etc/modprobe.d/modulejail-blacklist.conf
# Run it daily via cron
cat > /etc/cron.d/cve-watch << 'CRONEOF'
0 6 * * * root /usr/local/sbin/cve-watch.sh \
--blacklist /etc/modprobe.d/modulejail-blacklist.conf \
>> /var/log/cve-watch.log 2>&1
CRONEOF
The output lists newly disclosed CVEs affecting modules that are still loadable on your system. It doesn't test for exploitation — it just tells you which modules to add to your next ModuleJail run or manually blacklist immediately.

creates guard keeps runs idempotent — the blacklist generates once and isn't overwritten on subsequent Ansible plays.Rollback if Something Breaks
Always have a rollback plan before running hardening tools on production. ModuleJail's rollback is clean because it only affects future module loads — it never touches the running kernel state:
# Option 1: Remove the blacklist and reboot (cleanest, full reset)
rm /etc/modprobe.d/modulejail-blacklist.conf
reboot
# Option 2: Rename aside without deleting (keeps a record)
mv /etc/modprobe.d/modulejail-blacklist.conf \
/etc/modprobe.d/modulejail-blacklist.conf.disabled
depmod -a
# Option 3: Unblock one specific module surgically
sed -i '/install cifs /d' /etc/modprobe.d/modulejail-blacklist.conf
modprobe cifs
# After rollback — verify the module loads cleanly
lsmod | grep cifs
Because ModuleJail doesn't touch initramfs or boot parameters, recovery from a bad blacklist is straightforward. Modules already running stay running throughout. After a rollback, update your whitelist and regenerate the blacklist with a new ModuleJail run.
Known Limitations
Hardware added after the scan. Plug in a USB NIC, webcam, or any device whose driver was blacklisted and that driver won't load. Fix: add the module to your whitelist and regenerate. On servers with stable, fixed hardware this rarely matters.
Kernel updates. A new kernel brings a new module tree. After every kernel update, boot into the new kernel, reach steady state, then re-run ModuleJail to regenerate the blacklist against the updated module set.
Modules loaded before ModuleJail runs. If an attacker already has code execution and loads a vulnerable module before ModuleJail has been applied, the blacklist doesn't help. ModuleJail is prevention, not detection.
Container runtimes. Disabling unprivileged user namespaces — which some ModuleJail baselines include — can break rootless Docker and Podman. Test in a staging environment before fleet-wide deployment if your servers run containers.
ModuleJail is not a replacement for patching. For the full picture on the 2026 kernel LPE wave, the write-up on Copy Fail CVE-2026-31431 covers the crypto subsystem angle, and the Linux server hardening checklist puts module reduction in the context of a complete hardening program. Full technical documentation is at github.com/jnuyens/modulejail and the Linuxiac deep-dive is at linuxiac.com.
Conclusion
ModuleJail Linux kernel hardening is one of the most practical defensive responses to the 2026 kernel LPE wave. A single script, no daemon, no reboot, works across every major Linux distro. Run it after full steady state, always do the dry run first, build a whitelist for modules you actually need, and deploy fleet-wide with Ansible. Set cve-watch.sh as a daily cron job. Regenerate the blacklist after every kernel update. It doesn't replace your patch cycle — it closes attack surface that patching can't touch until a CVE number exists. In 2026, with AI-assisted vulnerability discovery accelerating kernel LPE disclosures, that pre-patch gap matters more than it ever has.