Skip to content

jnuyens/modulejail

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ModuleJail: lock down unused kernel modules. Reduce risk. Stay secure.

A single POSIX shell script that shrinks a Linux host's kernel-module attack surface by writing a modprobe.d blacklist for every kernel module not currently in use, minus a built-in baseline and an optional sysadmin whitelist. No daemons, no initramfs changes, no AI inside the tool. One script, one run, one blacklist file.

Why?

AI-assisted security scanning is about to do to the Linux kernel what large-scale fuzzing did to userspace code a decade ago, only faster and at a much larger scale. Many years of latent privilege-escalation bugs in kernel modules are about to surface in quick succession over the coming weeks and months. Long term, this is a major win for kernel security: every disclosure closes a door that an attacker could otherwise have walked through unseen. Short term, it is a nightmare for sysadmins. Every public release brings another race against patch cycles, vendor backports, and reboots across thousands of hosts.

ModuleJail does not try to fix kernel bugs, and it cannot. It does the one thing a sysadmin can do today, on any host, in seconds: shrink the attack surface so that the next disclosed bug is more likely to land on a module the host is not even loading. A typical Linux host ships with several thousand kernel modules and uses a few hundred. ModuleJail blacklists the rest. The next CVE in the unused 90% becomes a non-event on that host, and the fleet operator buys time to schedule the patch on their own terms instead of emergency-paging at 03:00.

This is intentionally a boring tool. No AI inside it, no daemon, no continuous monitoring, no risk scoring, no CVE database lookups. Just one shell script, run once on a steady-state host, that writes /etc/modprobe.d/modulejail-blacklist.conf to blacklist the thousands of unused modules, specific to your system.

Quickstart

curl -fsSL https://raw.githubusercontent.com/jnuyens/modulejail/v1.1.4/modulejail | sudo sh

WARNING: convenient, not safe. This pipes unverified bytes from the network to a root shell. The safer alternative below is the recommended path.

The script writes its blacklist to /etc/modprobe.d/modulejail-blacklist.conf by default. To use a different path:

curl -fsSL https://raw.githubusercontent.com/jnuyens/modulejail/v1.1.4/modulejail | sudo sh -s -- -o /etc/modprobe.d/site-blacklist.conf

The safer alternative

Download, inspect, then run:

curl -fsSL https://raw.githubusercontent.com/jnuyens/modulejail/v1.1.4/modulejail -o /tmp/modulejail
less /tmp/modulejail
sudo sh /tmp/modulejail

This is the recommended path for any production deployment. The script is plain POSIX shell and inspection takes under ten minutes.

Native packages (.deb / .rpm)

For Debian/Ubuntu and RHEL/Fedora/Rocky hosts, prebuilt packages are attached to the GitHub release page:

# Debian / Ubuntu:
curl -fsSLO https://github.com/jnuyens/modulejail/releases/download/v1.1.4/modulejail_1.1.4_all.deb
sudo dpkg -i modulejail_1.1.4_all.deb

# RHEL / Fedora / Rocky:
curl -fsSLO https://github.com/jnuyens/modulejail/releases/download/v1.1.4/modulejail-1.1.4-1.noarch.rpm
sudo rpm -i modulejail-1.1.4-1.noarch.rpm

Both packages install /usr/bin/modulejail, the modulejail(8) manpage under /usr/share/man/man8/, and the README and LICENSE under /usr/share/doc/modulejail/. They depend on coreutils, findutils, and awk/gawk (all standard) and recommend curl or wget so the optional post-run update check can reach GitHub.

After install, man 8 modulejail shows the full reference: options, profiles, safety model, idempotency, exit codes, environment, and examples.

To rebuild the packages locally from a checkout:

./packaging/build.sh           # builds whatever this host's tooling supports
./packaging/build.sh --deb     # .deb only (requires dpkg-deb)
./packaging/build.sh --rpm     # .rpm only (requires rpmbuild)

Output goes to packaging/dist/. The script skips gracefully on hosts without the matching tooling.

What ModuleJail is

ModuleJail snapshots the set of currently loaded modules (/proc/modules) and computes the complement against the full module tree (/lib/modules/$(uname -r)). Every module in the complement, minus a built-in baseline of essential modules and an optional sysadmin-supplied whitelist, is emitted as an install <mod> /bin/true directive in a modprobe.d-compatible blacklist file.

The tool is aimed at Linux fleet operators who need to harden many servers against the wave of AI-assisted kernel privilege-escalation discoveries. Every additional loaded module is additional latent attack surface for the next disclosed CVE. ModuleJail's model is simple: if it is not loaded today on a steady-state host, blacklist it.

The script is portable across Debian/Ubuntu, RHEL/Rocky, Arch, Alpine, and SUSE families. It has no runtime dependencies beyond awk, comm, find, sha256sum, and standard coreutils, all present in every base Linux install including busybox.

The safety model

The invariant is: whatever is currently loaded is assumed necessary for the host to function, and is preserved. ModuleJail does not guess; it reads /proc/modules at run time and treats that exact set as the keep-list.

This means the operator's responsibility is to run ModuleJail when the host is in a known-good, steady-state configuration: after all services are started, all kernel drivers are loaded, all filesystems are mounted. Running it on a partial or in-flux system risks blacklisting a module that is occasionally needed.

The generated file is placed under /etc/modprobe.d/. To revert, remove the file and reboot (see the Reverting section). The built-in baseline ensures that core filesystems, storage controllers, and essential networking modules are never blacklisted regardless of the running profile.

Explicit limitations

  • No initramfs handling. Modules baked into initramfs are out of scope. The loaded-module surface is the target; baked-in modules are not the relevant attack vector.
  • No revert tooling. The revert path is "remove the generated file and reboot." Sysadmin discipline replaces tool guardrails.
  • No daemon or continuous monitoring. One-shot script by design.
  • No AI inside the tool. AI is the threat-model backdrop, not a feature.
  • No per-distro packaging in v1. The curl one-liner and a cloned repo are the distribution channels.
  • No module risk scoring. The model is "unused implies blacklist," not "vulnerable implies blacklist."
  • No kernel rebuild. Runtime blacklist only.

Profiles

ModuleJail ships three built-in baseline profiles. The selected profile determines which modules are always preserved regardless of loaded state.

# Profile selection via -p (default: conservative)
sudo sh modulejail -p conservative
sudo sh modulejail -p minimal
sudo sh modulejail -p desktop

Profile descriptions (from --help):

  minimal       Core filesystems + essential kernel modules only
  conservative  Minimal + common server/VM drivers (default)
  desktop       Conservative + WiFi, Bluetooth, audio, video drivers

conservative is the right choice for virtualised or bare-metal server Linux. desktop is for laptops and workstations where WiFi, Bluetooth, audio, and video drivers must be preserved. minimal is for environments where you have full control over which drivers are loaded and want the smallest possible baseline.

The sysadmin whitelist

A site-local WHITELIST variable near the top of the script holds space-separated module names that are always preserved, beyond the selected baseline. It ships empty.

To use it, open the script and find the === SYSADMIN WHITELIST === section:

# === SYSADMIN WHITELIST ===
# Site-local additions to the keep-set, in addition to the selected baseline
# profile. Modules listed here will never appear in the generated blacklist.
#
# Format: space-separated module names in canonical underscore form
#         (the pipeline normalizes - to _, so either form works).
# Default: empty.
#
# Example (uncomment and adapt):
# WHITELIST='nft_compat xt_owner'
WHITELIST=''
# === END SYSADMIN WHITELIST ===

Edit WHITELIST='' to add your site-specific modules. The === banner anchors are designed for Ansible template insertion (lineinfile or blockinfile).

Exit codes

Exit codes follow sysexits.h conventions (see man 3 sysexits). Fleet automation tools can case $? cleanly.

Code Meaning
0 success
64 command-line argument error (bad flag, missing value, unknown profile)
66 required kernel input missing (/proc/modules or /lib/modules/<kernel>)
70 sanity guard tripped (empty blacklist or >99% of modules blacklisted)
71 OS-level error (mktemp work dir, or find errors on /lib/modules)
73 output path cannot be created (symlink/directory/trailing-slash, or mktemp failure)
77 target directory not writable (try sudo, or use -o <other-path>)

Idempotency contract

Two consecutive runs on an unchanged host produce byte-identical output files. The generated blacklist header carries a sha256 run fingerprint, not a wall-clock timestamp, computed over the canonical inputs: sorted loaded-module set, sorted baseline set, sorted whitelist, profile name, and kernel version. Because the fingerprint is a deterministic function of inputs, identical inputs produce an identical fingerprint and thus an identical output file.

# fingerprint: sha256:e284ee9741eb544adf1af6c0fffc162dedd6029191673237a8155cd497908686

Fleet operators can use the fingerprint to correlate "what was on the host at hardening time" across machines: two hosts with the same fingerprint had identical loaded sets, baseline, whitelist, profile, and kernel version when ModuleJail ran. No wall-clock drift, no spurious diffs in configuration management systems.

Update check

After a successful run, ModuleJail performs a best-effort lookup against the GitHub tags API to see whether a newer release is available. The check has a 10-second hard timeout and is silent on every failure mode (no network, no curl or wget installed, parse failure, current version equal to or newer than the latest tag). It only prints a stderr notice when the upstream release is strictly newer than the running version.

To disable the check entirely (for offline fleets, restricted networks, or pipeline-style automation where any unexpected output is noise), set:

export MODULEJAIL_NO_UPDATE_CHECK=1

The check fires only on a successful run. Error paths (bad arguments, missing /proc/modules, sanity-guard trip, etc.) exit before reaching it.

Cross-distro support

ModuleJail has been verified across two confidence tiers.

Real-kernel tier (live SSH hosts)

Distro Kernel Result
Ubuntu 24.04.4 LTS (Noble Numbat) 6.8.0-110-generic PASS (6363 of 6474 modules blacklisted)
Debian GNU/Linux 13.4 (trixie) 6.12.74+deb13+1-amd64 PASS (4091 of 4227 modules blacklisted)
Rocky Linux 9.7 (Blue Onyx) 5.14.0-503.35.1.el9_5.x86_64 PASS (2253 of 2338 modules blacklisted)

Note for Rocky/RHEL hosts: on hosts with strict SELinux enforcement, non-root execution may encounter a find permission denial on /lib/modules/, causing exit code 71 (EX_OSERR). This is expected, documented behaviour. Use sudo, or relax the relevant SELinux policy, if this occurs.

Fixture-container tier (synthetic kernel module trees)

Distro Base image Shell Result
Arch Linux (latest) archlinux:latest /bin/sh (bash) PASS (10/10 assertions)
Alpine Linux (latest) alpine:latest busybox ash PASS (10/10 assertions)
openSUSE Tumbleweed opensuse/tumbleweed:latest /bin/sh PASS (10/10 assertions)

Fixture containers run against a synthetic /lib/modules/6.99.0-fixture/ tree with representative .ko, .ko.gz, .ko.xz, and .ko.zst files to exercise all four suffix variants.

The MODULEJAIL_PROC_MODULES and MODULEJAIL_KVER environment variables are test-only plumbing (analogous to TMPDIR or GIT_DIR) used by the fixture harness to point the script at synthetic /proc/modules and module tree paths. End-user operators leave these unset.

Reverting

Remove the generated file under /etc/modprobe.d/ and reboot, or modprobe the specific modules you need back without rebooting.

# Full revert (requires reboot):
sudo rm /etc/modprobe.d/modulejail-blacklist.conf
sudo reboot

# Selective reload without reboot (bring back a specific module):
sudo modprobe <module_name>

The generated file uses install <module> /bin/true directives, which block autoloading. Explicitly loading with modprobe overrides the blacklist for the current boot session only; the restriction re-applies after the next reboot unless the file has been removed.

Contributing

The test matrix lives in tests/. Both harnesses are POSIX shell scripts runnable by anyone with the prerequisites:

# Container fixture suite (Arch/Alpine/openSUSE):
# Requires: docker or podman; exits 77 if neither is found (graceful skip).
./tests/run-fixtures.sh

# Real-SSH-host acceptance suite:
# Requires: SSH key access to the hosts configured in the harness.
./tests/run-ssh-hosts.sh

./tests/run-fixtures.sh exits 77 on any host without a container runtime; that is the documented graceful degradation (autoconf/TAP skip convention). Run it on a Linux host with Docker or Podman.

Both harnesses are shellcheck-clean (shellcheck --shell=sh).

License

Copyright (C) 2026 Jasper Nuyens jnuyens@linuxbe.com

GPL-3.0-only. See the LICENSE file for the full text.

About

Proactively shrink a Linux host's kernel-module attack surface by blacklisting every module not currently in use.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages