MODULEJAIL(8)
πΈ modulejail πΈ
okay bestie, gather round, because modulejail β that's section 8 of the manual, the system-administration girlies β is about to become your new favorite little gremlin π
β¨ NAME β¨
modulejail β shrinks down a Linux host's kernel-module attack surface π¦
β¨ SYNOPSIS β¨
modulejail [-p profile] [-o output] [--whitelist-file path]
[--no-syslog-logging] [--verbose-logging] [--dry-run]
[--quiet|--verbose] [-f] [--output-format format]
modulejail {-h|--help|-V|--version}
π DESCRIPTION π
so here's the tea π: modulejail shrinks the kernel-module attack surface of a Linux host by writing a modprobe.d(5) blacklist file that turns OFF every kernel module that isn't currently in use. literally closing the door on all the modules that are just standing there menacingly. periodt.
when you invoke it, the script snapshots the set of currently loaded modules from /proc/modules, then computes the complement against the full module tree under /lib/modules/$(uname -r). every module living in that complement β minus a built-in baseline of essential modules and minus an optional whitelist you supply as the sysadmin β gets emitted as an install mod <body> directive into /etc/modprobe.d/modulejail-blacklist.conf (or wherever you pointed -o). π«
and since v1.2: when /usr/bin/logger is executable on the host AND --no-syslog-logging is not set, that <body> is a lil shell invocation that calls logger(1) with tag modulejail, so blocked module load attempts actually produce a syslog entry. otherwise <body> is just /bin/true, matching the silent v1.1.4 behaviour. go peek at VIEWING BLOCKED ATTEMPTS below for more π
the script itself is a POSIX-friendly /bin/sh program with no runtime dependencies beyond awk, comm, find, sha256sum, and standard coreutils. no cap. it's built for Linux fleet operators who need to harden a whole bunch of servers against the wave of AI-assisted kernel privilege-escalation discoveries β because every additional loaded module is additional latent attack surface for the next disclosed CVE ππ₯
π OPTIONS π
-p,--profile {minimal|conservative|desktop|none}β picks the built-in baseline profile of always-preserved modules (default:conservative). go read PROFILES below bestie.
the special value none zeroes out the built-in baseline entirely; the keep-set then becomes the union of currently loaded modules, the in-script WHITELIST, and any --whitelist-file additions. the >99% blacklist sanity guard is disabled under -p none (the loaded-keep invariant remains the boot-safety contract tho, that one never leaves). this is only recommended when --whitelist-file is supplied, so that essential modules that aren't currently loaded still get preserved. available since v1.3 β¨
-o,--output pathβ writes the generated blacklist to path (default:/etc/modprobe.d/modulejail-blacklist.conf). the target directory has to be writable; non-root invocations are supported when the chosen path lives outside/etc/modprobe.d/π«Ά--whitelist-file pathβ reads site-local module names from path and appends them to the in-scriptWHITELIST(additive, bestie, it adds). the file format is described down in SITE-LOCAL WHITELIST FILE below: one module name per line, with#introducing a comment and blank lines ignored. every non-comment line must match[a-zA-Z0-9_-]+exactly; malformed lines exit65(EX_DATAERR) with a line-numbered stderr diagnostic π€. the file mode must not be group- or world-writable; if it is,modulejailexits77(EX_NOPERM) and prints achmod go-whint. when this flag isn't given,modulejailauto-detects/etc/modulejail/whitelist.confand uses it if present (and prints aninfo:line on stderr noting the choice). pass--no-whitelist-fileto skip that default for one run. available since v1.2.--no-whitelist-fileβ skips the default whitelist file even if/etc/modulejail/whitelist.confis right there. mutually exclusive with--whitelist-file path; combining them exits64(EX_USAGE) π. use it during recovery, or when you want a guaranteed-empty external whitelist contribution for a single run. available since v1.2.--no-syslog-loggingβ forces the v1.1.4-styleinstall <mod> /bin/trueinstall-line body, suppressing the default-on syslog emission that got added in v1.2. use this on hosts without/usr/bin/logger, in minimal or initramfs builds, or whenever a byte-identical regression contract across versions is required π. available since v1.2.--dry-runβ simulates the whole pipeline without writing any file at all. prints a one-lineDRY-RUN: would blacklistsummary to stdout (or a JSON/logfmt object when--output-formatis set). prints the would-be 8-line header block plus the full set of would-beinstalldirectives to stderr (unless--quietis also set). writes nothing under/etc/modprobe.d/or the path passed to-o. exits0on simulated success. chef's kiss for previewing what would get blacklisted before you commit a profile change or a whitelist edit β just redirect stderr to a file to capture the preview π. available since v1.3.--quietβ suppresses all non-error stderr output: the default-whitelistinfo:line, the-p noneinfo:line, the dry-run would-be header block, and the update-checknotice:line. also suppresses the stdout summary line. severity-prefixederror:messages on stderr are never suppressed, soEX_USAGE,EX_NOPERM, andEX_SOFTWAREfailures stay visible no matter what π¨. mutually exclusive with--verbose; combining them exits64(EX_USAGE). available since v1.3.--verboseβ emits one decision line per module to stderr, shaped likekeep: NAME (loaded|whitelist|baseline)orblacklist: NAME. reasons show up only on keep lines. precedence among keep-sources goes: loaded > whitelist > baseline. these lines carry no severity prefix (they're operator-requested telemetry, not script-status diagnostics, and they're living their truth π«). mutually exclusive with--quiet; combining them exits64(EX_USAGE). available since v1.3.--output-format {json|logfmt}β emits a single-line machine-readable run summary on stdout instead of the human-readable summary line. valid formats arejsonandlogfmt. see OUTPUT FORMATS below for the full 11-field schema v1 β¨. the machine-readable output is not silenced by--quietβ it is the parser surface, bestie. combinable with--dry-run(the JSON/logfmtdry_runfield becomestrueandoutput_pathbecomes the would-be path; no file is written). available since v1.3.-f,--fail-on-module-loadβ makes blocked module loads return a non-zero exit code so that modprobe fails loudly for blacklisted modules. by default, blocked loads silently succeed (modprobe returns0even though the module was never loaded, which is very sneaky of it π). with this flag, the install-line body uses/bin/falseinstead ofexit 0or/bin/true, somodprobe <module>returns non-zero for any blacklisted module. use this when blocked loads should produce a visible error rather than silently succeeding.--verbose-loggingβ enriches the per-blocked-load logger call with caller context, read straight from/proc/$PPID/: the parent PID (ppid), the audit loginuid (persists across su/sudo), the parent's short command name (pcomm), and argv[0] (pexe, truncated at the first NUL by command substitution; full cmdline is oneps -fp <ppid>away π). the default logger output is the bareblocked: <module>form. requires/usr/bin/loggerto be executable (modulejail exits66(EX_NOINPUT) otherwise); mutually exclusive with--no-syslog-logging. available since v1.3.4.--warn-onlyβ permissive audit mode π¦. instead of blocking, each install line logs awould-block: <module>event to syslog (tagmodulejail) and then loads the module anyway viamodprobe --ignore-install <module>. the--ignore-installflag does double duty here: it performs the real insmod AND stops modprobe from re-entering the same install directive (which would otherwise recurse, and we do not do infinite loops in this house π). nothing is actually blocked, so the host keeps working while you observe, in the log, exactly what an enforcing run would block. this is the safe way to trial a profile on an unfamiliar system before committing, fr fr. requires/usr/bin/loggerand an executable modprobe (searched at/usr/sbin/modprobethen/sbin/modprobe; modulejail exits66(EX_NOINPUT) if either is missing). mutually exclusive with-f/--fail-on-module-loadand--no-syslog-logging(both exit64(EX_USAGE)); composes with--verbose-logging. available since v1.6 π₯-V,--versionβ prints version, repository URL, and license, then exits0.-h,--helpβ prints a usage summary including exit codes and environment, then exits0.
π PROFILES π
minimalβ core filesystems plus essential kernel modules only. the bare minimum, the skinny legend πconservativeβminimalplus common server and VM drivers. this is the default; appropriate for virtualised or bare-metal Linux servers across the mainstream hardware matrix π₯οΈβ¨desktopβconservativeplus WiFi, Bluetooth, audio, and video drivers. for laptops and workstations, iykyk π§πΆ
π§ SYSADMIN WHITELIST π§
a site-local WHITELIST variable near the top of the script holds space-separated module names that are always preserved, beyond whichever baseline you selected. it ships empty. edit the script in place and go look for the === SYSADMIN WHITELIST === banner π. those === anchors are designed for Ansible template insertion (lineinfile or blockinfile), which is honestly so iconic of them.
since v1.2, an external file is also supported via --whitelist-file; see SITE-LOCAL WHITELIST FILE below. the two mechanisms are additive: the in-script value remains the sysadmin's editable default, and the file appends to it π«Ά
SITE-LOCAL WHITELIST FILE π
Okay bestie, so the --whitelist-file flag lets you read the modules-to-keep list from a file you choose. This is the move when modulejail got installed via .deb/.rpm/curl | sh, because if you go editing the script in place, all your cute site-local additions get yeeted on the next reinstall. We don't want that. π
If you don't pass the flag, modulejail auto-detects /etc/modulejail/whitelist.conf and uses it when it's there, with the same strict mode and content gates as an explicitly-passed path. It prints an info: line on stderr so the choice is never silent β no sneaky behaviour here, bestie. And if you want out for one run, pass --no-whitelist-file.
File format:
# Comments start with # and run to end of line.
# Blank lines are ignored.
# Each non-comment line is one module name, matching [a-zA-Z0-9_-]+.
# Trailing whitespace and CR (CRLF endings) are stripped silently.
# Module names are accepted in both dash and underscore form;
# the pipeline normalises - to _ internally.
nft_compat
xt_owner
zfs
The file mode must NOT be group-writable or world-writable β it's the exact same hardening that sshd(8) applies to authorized_keys and that sudo(8) applies to sudoers. modulejail calls stat(1) with the GNU -c '%a' form, falling back to the BSD -f '%Lp' form for cross-distro portability. If either the group- or world-write bits are set, it refuses to run with exit code 77 (EX_NOPERM) and prints chmod go-w PATH as the remediation hint.
OUTPUT FORMATS β¨
When you pass --output-format json or --output-format logfmt, modulejail emits a single-line machine-readable run summary to stdout instead of the human-readable line. The schema is small, versioned, and additive-only across future schema versions. So it'll keep working, fr fr. π«Ά
Schema version 1 fields
schema_versionβ integer (always1)toolβ object{name, version}kernel_versionβ string (hostuname -routput)profileβ string (minimal|conservative|desktop|none)dry_runβ boolean (truewhen--dry-runwas passed)whitelist_fileβ string or null (nullwhen no--whitelist-file)modules_availableβ integer (total.ko*files under/lib/modules/KVER)modules_loadedβ integer (lines in/proc/modulesat snapshot time)modules_blacklistedβ integer (entries written to the output file)fingerprintβ string (64-char lowercase hex, no"sha256:"prefix)output_pathβ string (target path; under--dry-run, the would-be path)
JSON example
{"schema_version":1,"tool":{"name":"modulejail","version":"1.3.0"},
"kernel_version":"6.8.0-31-generic","profile":"conservative",
"dry_run":false,"whitelist_file":"/etc/modulejail/whitelist.conf",
"modules_available":4912,"modules_loaded":47,
"modules_blacklisted":4810,"fingerprint":"a3f5b9c2e1d4...",
"output_path":"/etc/modprobe.d/modulejail-blacklist.conf"}
logfmt example
schema_version=1 tool_name=modulejail tool_version=1.3.0 kernel_version=6.8.0-31-generic profile=conservative dry_run=false whitelist_file=/etc/modulejail/whitelist.conf modules_available=4912 modules_loaded=47 modules_blacklisted=4810 fingerprint=a3f5b9c2e1d4... output_path=/etc/modprobe.d/modulejail-blacklist.conf
Fleet automation idiom
Without --quiet, modulejail may emit human-readable info: lines to stderr (like the default-whitelist notice, or the -p none sanity-guard notice) right alongside the machine-readable stdout record. Fleet automation that captures stderr into a unified log (2>&1) will receive both the structured output and these human-readable lines. To suppress all non-error stderr output while keeping the structured stdout record intact, combine --output-format with --quiet:
modulejail --output-format json --quiet | jq .
modulejail --output-format logfmt --quiet
--quiet never suppresses the machine-readable stdout record β only the human-targeted stderr diagnostics. π
Fingerprint format
The fingerprint field in JSON and logfmt output is raw 64-character lowercase hex, without the sha256: prefix that appears in the generated file header (# fingerprint: sha256:HEX). So machine consumers parsing via jq(1) or awk don't need to do string slicing to extract the hash β and the file header keeps the algorithm tag for human auditability. Everybody wins. π
Schema stability contract
schema_version=1 is stable. Future schema versions are strictly additive: new fields may be added; renames and removals require schema_version=2. This mirrors the reproducible-builds.org stability convention: field semantics locked once published. Periodt.
Known limitation π
Operator-supplied paths containing a literal double-quote character ("), a backslash (\), or a newline are not JSON-escaped in the v1 schema. Real-world paths essentially never contain these characters, but operators using exotic paths may receive malformed JSON and a jq(1) parse error. A future schema_version may add full path escaping.
VIEWING BLOCKED ATTEMPTS π΅οΈββοΈ
Since v1.2, when /usr/bin/logger is executable on the host running modulejail and --no-syslog-logging is not set, the generated install lines emit a syslog entry tagged modulejail every single time a blocked module is targeted by modprobe(8). View the entries via:
journalctl -t modulejail --since '1 hour ago' # systemd / journald
grep modulejail /var/log/syslog # classic syslog
The generated blacklist file's header annotation records which install-line form is in use:
# install-line: /bin/sh + logger (syslog tag: modulejail)
# install-line: /bin/true (silent, --no-syslog-logging or logger absent)
Heads up bestie: if/usr/bin/loggeris absent and--no-syslog-loggingwas NOT set,modulejailsilently falls back to the/bin/trueform. No stderr warning is emitted; the header annotation is the only visible cue. Use--no-syslog-loggingexplicitly when the v1.1.4 install-line body is a hard requirement.
INVOCATION π
A header line starting with # invocation: is added to the blacklist, which registers the invocation that was used to produce the blacklist β and it can be copied and pasted straight into the terminal for reproducible results. Iconic.
HEADER FIELDS</header>
The generated blacklist file carries an 8-line #-prefixed header documenting the tool version, repository, generation profile, host kernel, fingerprint, install-line form, invocation, and a do-not-edit-by-hand disclaimer. So much lore in eight little lines. πΈ
# modulejail VERSION
Tool name and version that produced the file.
# https://modulejail.com
Project home page (source repository is at https://github.com/jnuyens/modulejail).
# profile: PROFILE
The profile name passed to -p (or conservative when the default was used).
# kernel: KVER
The host kernel version string from uname -r at the time modulejail ran. This is the same value as the kernel_version field in the JSON and logfmt output (see OUTPUT FORMATS above).
# fingerprint: sha256:HEX
A sha256 hash over canonical inputs: sorted loaded-module set, sorted baseline, sorted whitelist, profile name, and kernel version. The sha256: prefix identifies the algorithm for human readers; the JSON and logfmt fingerprint field strips this prefix and carries raw hex only.
# install-line: FORM
Records which install-line body is in use: /bin/sh + logger (syslog tag: modulejail) or /bin/true (silent, --no-syslog-logging or logger absent).
# invocation: ARGV-STRING
The full command line that produced the file, in POSIX-canonical single-quote encoding. See INVOCATION above.
# Do not edit by hand -- regenerate with modulejail(8).
Disclaimer. Edits are overwritten on the next run. π¬
The # kernel: line is regression-locked: test suite fixture header-kernel-line-locked.sh asserts that this line exists and carries a non-empty kernel-version token in every generated file. See OUTPUT FORMATS for the machine-readable schema that parallels these header fields.
SCOPE OF THE BLACKLIST π¦
A modprobe.d(5) blacklist intercepts modprobe(8)-mediated module loads: udev hotplug events, dependency resolution, autoloaded modules via the alias system. It does not intercept, by design:
insmod(8)βinsmodbypassesmodprobe(8)entirely; it never reads/etc/modprobe.d/. A root user can always insert a.kodirectly.modprobe --ignore-installβmodprobe(8)'s explicit escape hatch from install-line indirection. Treating it as suspicious is a higher-layer concern (auditd, eBPF) outside themodprobe.dcontract.
modulejail is a default-safe policy on modprobe(8). It removes the auto-loading attack surface that unprivileged code or remote attackers can reach, and it does not pretend to block a root user with intent. No cap. π
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.
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 loaded, all filesystems mounted. Running it on a partial or in-flux system risks blacklisting a module that is occasionally needed. π
The built-in baseline ensures that core filesystems, storage controllers, and essential networking modules are never blacklisted regardless of the running profile.
IDEMPOTENCY π
Two consecutive runs on an unchanged host produce byte-identical output files. The generated blacklist header carries a sha256 fingerprint over canonical inputs (sorted loaded-module set, sorted baseline, sorted whitelist, profile name, and kernel version) rather than a wall-clock timestamp. Identical inputs therefore produce an identical fingerprint, and an identical output file. So satisfying. β¨
Fleet operators can use the fingerprint to correlate hardening state across machines: two hosts with the same fingerprint had identical loaded sets, baseline, whitelist, profile, and kernel version when modulejail ran. Slay.
π₯ EXIT CODES π₯
okay so bestie, exit codes here follow the sysexits(3) conventions β that's the grown-up Unix way of saying "the number we quit with tells you exactly what went wrong." no guessing, no vibes-based debugging. β¨
0β Success. we ate. π64β Command-line argument error: bad flag, missing value, or unknown profile. basically you typed something we don't recognise. π«65β Invalid data in--whitelist-file(EX_DATAERR): malformed module name on at least one non-comment, non-blank line. the whitelist file is giving typos. π66β Required kernel input missing (/proc/modulesor/lib/modules/<kernel>). can't find the stuff we need, periodt.70β Sanity guard tripped: the computed blacklist is empty, or it covers more than 99% of the module universe. Aborting to avoid an unbootable host. π¨ this one is the big one β it's the seatbelt. respect the seatbelt.71β OS-level error: can't create the temporary work directory, or we find reported errors while walking/lib/modules.73β Output path cannot be created: symlinked target, existing directory, trailing slash, ormktempfailure in the target directory. π77β Target directory not writable. Trysudo, or use-o other-path.
πΏ ENVIRONMENT πΏ
the env vars, bestie. most of these are test-only plumbing β real operators leave them unset, iykyk.
MODULEJAIL_NO_UPDATE_CHECKβ Set to any non-empty value to skip the post-run check for a newer release on GitHub. The check has a 10-second hard timeout and is silent on every failure mode; this variable disables it entirely for offline fleets, restricted networks, or pipeline-style automation where any unexpected output is noise. β¨MODULEJAIL_LOGGER_PATHβ Path to thelogger(1)binary used for the v1.2 syslog install-line detection (default:/usr/bin/logger). Test-only plumbing that lets acceptance tests exercise the absent-fallback path without a chroot or namespace; end-user operators leave this unset.MODULEJAIL_MODULES_ROOTβ Root of the kernel modules tree to walk (default:/lib/modules). Test-only plumbing parallel toMODULEJAIL_PROC_MODULESandMODULEJAIL_KVER; lets host-local cases on non-Linux dev boxes point modulejail at a synthetic tree under$TMPDIR. End-user operators leave this unset.MODULEJAIL_PROC_MODULESβ Path to read the loaded-module list from (default:/proc/modules). Test-only plumbing.MODULEJAIL_KVERβ Kernel version string used to locate the module tree (default:uname -r). Test-only plumbing.
ποΈ FILES ποΈ
the receipts of where everything lives π
/etc/modprobe.d/modulejail-blacklist.confβ Default output path. Overridable with-o./proc/modulesβ Read at run time as the keep-list. π/lib/modules/$(uname -r)β Walked to enumerate the full module universe (including DKMS and weak-updates modules).
π EXAMPLES π
okay this is the fun part, let's actually do stuff π
Hardening a server with the default profile and default output path:
sudo modulejail
Workstation hardening, preserving WiFi, Bluetooth, audio, and video drivers:
sudo modulejail -p desktop
Writing to a custom path (does not require root if the directory is writable):
modulejail -o /tmp/site-blacklist.conf
Fleet-automation context: disable the update check to keep output deterministic.
MODULEJAIL_NO_UPDATE_CHECK=1 sudo modulejail
Site-local whitelist via an operator-owned file (preferred when modulejail is installed via package manager):
# /etc/modulejail/whitelist.conf: chmod 0644 (or stricter)
#
# nft_compat
# xt_owner
# zfs
sudo modulejail --whitelist-file /etc/modulejail/whitelist.conf
View syslog entries for blocked module load attempts (since v1.2):
journalctl -t modulejail --since '1 hour ago'
grep modulejail /var/log/syslog
Suppress syslog emission to restore the v1.1.4 install-line body byte-for-byte (useful on hosts without logger, or for byte-identical regression contracts):
sudo modulejail --no-syslog-logging
π¦ REVERTING π¦
changed your mind? totally fine, no judgement π
Remove the generated file and reboot:
sudo rm /etc/modprobe.d/modulejail-blacklist.conf
sudo reboot
To bring back a specific module without rebooting (the restriction re-applies after the next reboot unless the file has been removed):
sudo modprobe <module_name>
π SEE ALSO π
modprobe(8), modprobe.d(5), sysexits(3), lsmod(8)
β¨ AUTHOR β¨
Jasper Nuyens <jnuyens@linuxbe.com>
π COPYRIGHT π
Copyright Β© 2026 Jasper Nuyens. License GPL-3.0-only.
This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
πΈ HOMEPAGE πΈ
Source repository, issue tracker, signed release tarballs and binary artifacts: https://github.com/jnuyens/modulejail
annnnd that's a wrap bestie π«Ά you just read the whole MODULEJAIL(8) page β this version slaps as modulejail 1.6.0, dated 2026-08-19. go forth, harden that kernel, and stay iconic π
π
π¦ where this page comes from
- ubuntu stonking: modulejail 1.6.0-1 Β· GPL-3.0-only
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms