Debian GNU/Linux · Logging & auditing

Audit kernel module loading and unloading

When it fails: Kernel modules can be loaded or removed without a record. A malicious kernel module (rootkit) can be loaded without any trace in the audit log.

CB-DEB-AUDIT-025mediumactiveautomatedautomated fixprofile server-hardenedprofile desktop-hardened

As released in v0.1.0 beta · pre-release. Current state.

Description

auditd rules in /etc/audit/rules.d/60-cirsec-modules.rules, key kernel_modules: init_module, finit_module and delete_module called by users who logged in, 64-bit and 32-bit programs, and every run of kmod (behind modprobe, insmod, rmmod, lsmod).

Modules loaded automatically by udev at boot have no login user and are not recorded; this matches the intent (actions by people).

Rationale

Code loaded into the kernel runs with full privileges and can hide processes, files and connections. Recording module loads is one of the few ways to spot a kernel rootkit after the fact.

Impact

Running lsmod as a logged-in administrator also produces a record, since it is the same kmod program.

Setting

Location/etc/audit/rules.d/60-cirsec-modules.rules
DefaultNo audit rules
RecommendedRules loaded (auditctl -l) and present in /etc/audit/rules.d/60-cirsec-modules.rules

Audit

  1. Run auditctl -l as root. It must list the rules shown below (keys kernel_modules).
  2. Run grep -h . /etc/audit/rules.d/*.rules. The same rules must be present, so they load at boot.

Expected rules, as auditctl -l prints them:

-a always,exit -F arch=b64 -S init_module,delete_module,finit_module -F auid>=1000 -F auid!=-1 -F key=kernel_modules
-a always,exit -F arch=b32 -S init_module,delete_module,finit_module -F auid>=1000 -F auid!=-1 -F key=kernel_modules
-a always,exit -S all -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=-1 -F key=kernel_modules

Automated: every check must pass. Scanners report each check by its id.

rules

Rules loaded in the kernel and present in /etc/audit/rules.d.

script: |
  command -v auditctl >/dev/null 2>&1 || { echo "auditctl not found: auditd is not installed"; exit 1; }
  want() {
  cat <<'RULES'
  -a always,exit -F arch=b64 -S init_module,delete_module,finit_module -F auid>=1000 -F auid!=-1 -F key=kernel_modules
  -a always,exit -F arch=b32 -S init_module,delete_module,finit_module -F auid>=1000 -F auid!=-1 -F key=kernel_modules
  -a always,exit -S all -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=-1 -F key=kernel_modules
  RULES
  }
  loaded=$(auditctl -l 2>&1) || { echo "$loaded"; exit 3; }
  disk=$(cat /etc/audit/rules.d/*.rules 2>/dev/null)
  n=0 rc=0
  while IFS= read -r r; do
    [ -n "$r" ] || continue
    n=$((n + 1))
    printf '%s\n' "$loaded" | grep -Fxq -- "$r" || { echo "not loaded: $r"; rc=1; }
    printf '%s\n' "$disk" | grep -Fxq -- "$r" || { echo "not in /etc/audit/rules.d: $r"; rc=1; }
  done <<EOF
  $(want)
  EOF
  [ "$rc" = 1 ] || echo "$n rules loaded and present in /etc/audit/rules.d"
  exit "$rc"

Remediation

  1. Write the expected rules to /etc/audit/rules.d/60-cirsec-modules.rules (mode 0640).
  2. Run augenrules --load. If auditctl -s shows enabled 2, the configuration is locked: reboot to load the new rules.
Restartnone
Disruptionnone

Rollback

Delete /etc/audit/rules.d/60-cirsec-modules.rules and run augenrules --load (or reboot if the configuration is locked).

rules-file

command -v augenrules >/dev/null 2>&1 || { echo "auditd is not installed" >&2; exit 1; }
want() {
cat <<'RULES'
-a always,exit -F arch=b64 -S init_module,delete_module,finit_module -F auid>=1000 -F auid!=-1 -F key=kernel_modules
-a always,exit -F arch=b32 -S init_module,delete_module,finit_module -F auid>=1000 -F auid!=-1 -F key=kernel_modules
-a always,exit -S all -F path=/usr/bin/kmod -F perm=x -F auid>=1000 -F auid!=-1 -F key=kernel_modules
RULES
}
f=/etc/audit/rules.d/60-cirsec-modules.rules
t=$(mktemp)
{ echo '# Written by CIRSEC Baselines CB-DEB-AUDIT-025'; want; } >"$t"
if ! cmp -s "$t" "$f"; then
  cb_backup "$f"
  cat "$t" >"$f"
fi
rm -f "$t"
chmod 640 "$f"
if auditctl -s | grep -q '^enabled 2'; then
  echo "audit rules are locked (-e 2): reboot to load $f"
else
  augenrules --load >/dev/null
fi

Handwritten code reads parameters from CB_<NAME> environment variables. Each release turns these fixes into remediate.sh and an Ansible role per profile: dry run by default, a fix runs only when its check fails.

References

Mappings

How this control relates to the other framework's: equivalent, stricter, looser, partial or related.

Threats mitigated

People and lifecycle

Authors
Verified2026-10-18 by Sn0wAlice on 12, Proxmox VM, Debian 12 genericcloud image, kernel 6.1.0-54-cloud-amd64, auditd 1:3.0.9-1, BIOS boot. Rules fail on fresh install; fix writes /etc/audit/rules.d/60-cirsec-modules.rules and loads it; rules match auditctl -l, kept after reboot; second run idempotent.
Verified2026-10-18 by Sn0wAlice on 13, Proxmox VM, Debian 13 genericcloud image, kernel 6.12.113+deb13-cloud-amd64, auditd 1:4.0.2-2+deb13u1, BIOS boot. Rules fail on fresh install; fix writes /etc/audit/rules.d/60-cirsec-modules.rules and loads it; rules match auditctl -l, kept after reboot; second run idempotent.
Introduced inv0.1.0
Created, updated2026-10-18, 2026-10-18
Translationsfr