Skip to Content
OpenVAPTAlpha
Pre-release. Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. Free for your own use, 100% at your own risk. This website collects no data. Read MoreRead Less
Pre-release. OpenVAPT has no Stable release yet. Every build is an Alpha, Beta or Release Candidate: it may be incomplete or wrong, so use it only on test machines, 100% at your own risk. See Release Channels
Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. These are AI systems, not people or registered engineers. Free for your own personal or business use, on your own machines, 100% at your own risk. Not for sale. Not for providing services. This website collects no data. Read the Acknowledgements
Pre-release. OpenVAPT has no Stable release yet. Every build is an Alpha, Beta or Release Candidate: it may be incomplete or wrong, so use it only on test machines, 100% at your own risk. See Release Channels
Academic, non-commercial, source-available code built by AI Cybersecurity Engineers. These are AI systems, not people or registered engineers. Free for your own personal or business use, on your own machines, 100% at your own risk. Not for sale. Not for providing services. This website collects no data. Read the Acknowledgements
LowHOST-BOOT-005Secure ConfigurationServer

Kernel Module Loading Unrestricted

Checks that the running kernel refuses unsigned modules or has module loading switched off entirely.

Why It Matters
A root-level attacker who can load any module owns the kernel: rootkits hide processes and files from every tool on the box. Signature enforcement or kernel.modules_disabled = 1 after boot makes that persistence step fail.
How It Detects
Reads /proc/sys/kernel/modules_disabled (1 passes), otherwise /sys/module/module/parameters/sig_enforce (Y passes) and /sys/kernel/security/lockdown ([integrity] or [confidentiality] passes). Kernels built without loadable module support are skipped; a value that exists but cannot be read is incomplete.
How to Fix
Boot with module.sig_enforce=1 (or lockdown=integrity) on the kernel command line and sign any out-of-tree modules, or set kernel.modules_disabled = 1 from a late boot service once every needed module is loaded (it cannot be turned back on without a reboot).
How to Verify
sysctl kernel.modules_disabled; cat /sys/module/module/parameters/sig_enforce /sys/kernel/security/lockdown
CVSS v3.1 Vector
CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:L
Control Category
Secure Configuration: CIS Controls v8 4; NIST CSF 2.0 PR.PS
Fix Effort
Medium
CIS Controls v8 Safeguard
4.8
NIST CSF 2.0 Subcategory
PR.PS-01, PR.IR-01
CWE
CWE-1188
NIST SP 800-53 Rev. 5
SI-7, CM-7
Benchmark Topic
Restrict kernel module loading (CIS Controls v8 4.8) (topic only, no benchmark text)
References
Linux kernel documentation, Kernel module signing facility
Kernel Self Protection Project, Recommended Settings
Learn the theory behind this check: P17 Kernel, Filesystem and Boot Hardening.

Source Code

The complete file that defines this check. Run it with openvapt scan --only HOST-BOOT-005, or read it offline with openvapt explain HOST-BOOT-005.

internal/checks/host/boot.go · Lines 1-450View on GitHub at This Commit
1package host
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "io/fs"
8 "regexp"
9 "sort"
10 "strings"
11
12 "github.com/openvapt/openvapt/internal/check"
13 "github.com/openvapt/openvapt/internal/checks/collect"
14 "github.com/openvapt/openvapt/internal/sys"
15)
16
17// Boot and integrity: boot loader password, Secure Boot state, kernel module
18// restrictions, file integrity monitoring, prelink and core dumps. Every
19// check reads files only; no command is run.
20//
21// Sources: GNU GRUB manual, UEFI Specification, Linux kernel documentation,
22// NIST SP 800-123, NIST SP 800-53 CM-6, CM-7, SI-7 and AC-6, CIS Controls
23// v8 safeguards 4.1 and 4.8 by id.
24
25var (
26 refGrubSec = check.Ref{Title: "GNU GRUB Manual, Security", URL: "https://www.gnu.org/software/grub/manual/grub/html_node/Security.html"}
27 refUEFI = check.Ref{Title: "UEFI Specification, Secure Boot and Driver Signing", URL: "https://uefi.org/specs/UEFI/2.10/32_Secure_Boot_and_Driver_Signing.html"}
28 refModprobeD = check.Ref{Title: "modprobe.d(5), Linux man-pages", URL: "https://man7.org/linux/man-pages/man5/modprobe.d.5.html"}
29 refModSign = check.Ref{Title: "Linux kernel documentation, Kernel module signing facility", URL: "https://docs.kernel.org/admin-guide/module-signing.html"}
30 refAIDE = check.Ref{Title: "AIDE, Advanced Intrusion Detection Environment", URL: "https://aide.github.io/"}
31 refLimits = check.Ref{Title: "limits.conf(5), Linux-PAM", URL: "https://man7.org/linux/man-pages/man5/limits.conf.5.html"}
32 refCoredump = check.Ref{Title: "coredump.conf(5), systemd", URL: "https://www.freedesktop.org/software/systemd/man/latest/coredump.conf.html"}
33)
34
35// readSome reads path and reports (text, exists, readable). A file that is
36// absent is a normal observation; one that exists but cannot be read fully
37// must make a check incomplete rather than pass.
38func readSome(ctx context.Context, s sys.System, path string) (string, bool, bool) {
39 r, err := s.ReadFile(ctx, path)
40 if err != nil {
41 if errors.Is(err, fs.ErrNotExist) {
42 return "", false, true
43 }
44 return "", true, false
45 }
46 return string(r.Data), true, r.Complete
47}
48
49// fsModules are filesystem drivers a server almost never needs. Each one
50// has carried kernel bugs reachable by mounting a crafted image.
51var fsModules = []string{"cramfs", "freevxfs", "jffs2", "hfs", "hfsplus", "udf", "squashfs"}
52
53// modprobeState reports, for each module, whether /etc/modprobe.d disables
54// it (install <mod> /bin/false or /bin/true, or blacklist <mod>). Both
55// forms count; the strictest hardening uses both. unreadable is true when
56// a modprobe.d file exists but could not be read.
57func modprobeState(ctx context.Context, s sys.System, mods []string) (map[string][]string, bool) {
58 state := map[string][]string{}
59 files, _ := s.Glob("/etc/modprobe.d/*.conf")
60 more, _ := s.Glob("/usr/lib/modprobe.d/*.conf")
61 files = append(files, more...)
62 sort.Strings(files)
63 unreadable := false
64 for _, f := range files {
65 text, _, ok := readSome(ctx, s, f)
66 if !ok {
67 unreadable = true
68 continue
69 }
70 for _, line := range strings.Split(text, "\n") {
71 fields := strings.Fields(strings.TrimSpace(line))
72 if len(fields) < 2 || strings.HasPrefix(fields[0], "#") {
73 continue
74 }
75 name := strings.ReplaceAll(fields[1], "-", "_")
76 for _, m := range mods {
77 if name != strings.ReplaceAll(m, "-", "_") {
78 continue
79 }
80 switch {
81 case fields[0] == "install" && len(fields) >= 3 && (fields[2] == "/bin/false" || fields[2] == "/bin/true" || fields[2] == "/usr/bin/false" || fields[2] == "/usr/bin/true"):
82 state[m] = append(state[m], "install "+fields[1]+" "+fields[2]+" ("+f+")")
83 case fields[0] == "blacklist":
84 state[m] = append(state[m], "blacklist "+fields[1]+" ("+f+")")
85 }
86 }
87 }
88 }
89 return state, unreadable
90}
91
92func init() {
93 check.Register(&check.Check{
94 ID: "HOST-BOOT-001", Title: "Boot Loader Has No Password", Domain: check.DomainHost,
95 Severity: check.Medium, Category: check.CatAccess, Effort: check.EffortMedium,
96 CVSS: "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
97 Summary: "Checks that GRUB is configured with a superuser and a hashed password so the boot entries cannot be edited at the console.",
98 Rationale: "Without a boot loader password anyone at the console or virtual console can edit the kernel command line (for example add init=/bin/bash) and get a root shell without any credentials, bypassing every control on the running system.",
99 Detection: "Reads /boot/grub/grub.cfg, /boot/grub2/grub.cfg, /boot/grub2/user.cfg, /boot/efi/EFI/*/grub.cfg, /boot/efi/EFI/*/user.cfg and /etc/grub.d/* and looks for set superusers= together with password_pbkdf2 (or password). Only the presence of these directives is reported; hashes are never shown. Skipped when no GRUB configuration exists; incomplete when a file exists but cannot be read.",
100 Fix: "Run grub-mkpasswd-pbkdf2 (grub2-setpassword on RHEL family), add set superusers=\"admin\" and password_pbkdf2 admin <hash> to /etc/grub.d/40_custom (RHEL family writes /boot/grub2/user.cfg), then regenerate with update-grub or grub2-mkconfig -o /boot/grub2/grub.cfg. Add --unrestricted to the normal menu entry so unattended reboots still work.",
101 Verify: "grep -E 'set superusers|password_pbkdf2' /boot/grub/grub.cfg /boot/grub2/grub.cfg /boot/grub2/user.cfg /etc/grub.d/* 2>/dev/null | sed 's/grub.pbkdf2.*/<hash>/'",
102 Refs: []check.Ref{refGrubSec, refNIST800123},
103 Standards: check.Standards{CWE: []string{"CWE-306"}, NIST: []string{"AC-3", "AC-6", "CM-6"}, CISTopic: "Boot loader password (CIS Controls v8 4.1)"},
104 Run: func(ctx context.Context, s sys.System) []check.Result {
105 paths := []string{"/boot/grub/grub.cfg", "/boot/grub2/grub.cfg", "/boot/grub2/user.cfg", "/boot/grub/user.cfg"}
106 for _, pat := range []string{"/boot/efi/EFI/*/grub.cfg", "/boot/efi/EFI/*/user.cfg", "/etc/grub.d/*"} {
107 more, _ := s.Glob(pat)
108 sort.Strings(more)
109 paths = append(paths, more...)
110 }
111 var super, pass []string
112 seen, unreadable := 0, 0
113 for _, p := range paths {
114 text, exists, ok := readSome(ctx, s, p)
115 if !exists {
116 continue
117 }
118 seen++
119 if !ok {
120 unreadable++
121 continue
122 }
123 if grubSuperusers.MatchString(text) {
124 super = append(super, p)
125 }
126 if grubPassword.MatchString(text) {
127 pass = append(pass, p)
128 }
129 }
130 switch {
131 case seen == 0:
132 return []check.Result{check.SkipR("/boot/grub", "No GRUB configuration found")}
133 case len(super) > 0 && len(pass) > 0:
134 return []check.Result{check.PassR("/boot/grub", "GRUB has a superuser and a hashed password", "superusers in "+strings.Join(super, ", "), "password directive in "+strings.Join(pass, ", "))}
135 case unreadable > 0:
136 return []check.Result{check.IncompleteR("/boot/grub", fmt.Sprintf("%d GRUB file(s) could not be read; no password directive found in the readable ones", unreadable))}
137 case len(super) > 0:
138 return []check.Result{check.FailR("/boot/grub", "GRUB names a superuser but sets no password", "superusers in "+strings.Join(super, ", "))}
139 default:
140 return []check.Result{check.FailR("/boot/grub", fmt.Sprintf("No superuser or password directive in %d GRUB file(s)", seen))}
141 }
142 },
143 })
144
145 check.Register(&check.Check{
146 ID: "HOST-BOOT-002", Title: "UEFI Secure Boot Disabled", Domain: check.DomainHost,
147 Severity: check.Medium, Category: check.CatConfig, Effort: check.EffortHigh,
148 CVSS: "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
149 Summary: "Checks the firmware's SecureBoot variable to see whether the boot chain is verified.",
150 Rationale: "Secure Boot makes the firmware refuse unsigned boot loaders and kernels, which blocks boot kits and unsigned kernels planted by an attacker with disk or console access. Without it, anything written to the boot partition runs at the next reboot.",
151 Detection: "Reads /sys/firmware/efi/efivars/SecureBoot-8be4df61-93ca-11d2-aa0d-00e098032b8c (the four-byte attribute header followed by one data byte; 1 means enabled). Machines without /sys/firmware/efi boot in legacy BIOS mode and are skipped, as are UEFI machines that expose no SecureBoot variable; an unreadable variable is incomplete. No command such as mokutil is run.",
152 Fix: "Enable Secure Boot in the firmware set-up and boot a signed shim and kernel (every mainstream distribution ships them). Custom kernels and out-of-tree modules then need signing with a Machine Owner Key. Where the platform cannot do Secure Boot, record the exception. Caution: unsigned kernels and modules (for example DKMS drivers such as NVIDIA or ZFS) stop loading once it is on, so sign or replace them first and keep console access for the first boot.",
153 Verify: "mokutil --sb-state or od -An -t u1 /sys/firmware/efi/efivars/SecureBoot-* (the fifth byte is 1 when enabled).",
154 Refs: []check.Ref{refUEFI, refNIST800123},
155 Standards: check.Standards{CWE: []string{"CWE-1326"}, NIST: []string{"SI-7", "CM-6"}, CISTopic: "Boot integrity (CIS Controls v8 4.1)"},
156 Run: func(ctx context.Context, s sys.System) []check.Result {
157 vars, _ := s.Glob("/sys/firmware/efi/efivars/SecureBoot-*")
158 if len(vars) == 0 {
159 if _, err := s.Stat("/sys/firmware/efi"); errors.Is(err, fs.ErrNotExist) {
160 return []check.Result{check.SkipR("/sys/firmware/efi", "Legacy BIOS boot; Secure Boot does not apply")}
161 }
162 return []check.Result{check.SkipR("/sys/firmware/efi/efivars", "UEFI firmware exposes no SecureBoot variable")}
163 }
164 sort.Strings(vars)
165 r, err := s.ReadFile(ctx, vars[0])
166 if err != nil || !r.Complete || len(r.Data) < 5 {
167 return []check.Result{check.IncompleteR(vars[0], "SecureBoot variable could not be read")}
168 }
169 if r.Data[4] == 1 {
170 return []check.Result{check.PassR(vars[0], "Secure Boot is enabled", "SecureBoot = 1")}
171 }
172 return []check.Result{check.FailR(vars[0], "Secure Boot is disabled", fmt.Sprintf("SecureBoot = %d", r.Data[4]))}
173 },
174 })
175
176 check.Register(&check.Check{
177 ID: "HOST-BOOT-003", Title: "Uncommon Filesystem Modules Not Disabled", Domain: check.DomainHost,
178 Severity: check.Medium, Category: check.CatConfig, Effort: check.EffortLow,
179 CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L",
180 Summary: "Checks that the cramfs, freevxfs, jffs2, hfs, hfsplus, udf and squashfs kernel modules are disabled in modprobe.d.",
181 Rationale: "Each filesystem driver is kernel code that parses untrusted data. A local user who can mount removable media or a loop device gets the kernel to parse a crafted image with one of these rarely maintained drivers, and several have had memory-corruption bugs. Removing drivers nobody uses removes that attack surface.",
182 Detection: "Reads /etc/modprobe.d/*.conf and /usr/lib/modprobe.d/*.conf and, for each module, accepts install <module> /bin/false (or /bin/true) or blacklist <module>. squashfs is left out when /snap exists because snaps need it. Modules with neither line fail; an unreadable modprobe.d file makes the check incomplete.",
183 Fix: "Create /etc/modprobe.d/openvapt-filesystems.conf with, for each module: install <module> /bin/false and blacklist <module> then unload any loaded one with modprobe -r <module>. Caution: first confirm that no listed module is in use (lsmod, findmnt); blocking a filesystem a mount or package format needs (for example squashfs for snap packages, or vfat for the EFI system partition) breaks those mounts at the next boot.",
184 Verify: "for m in cramfs freevxfs jffs2 hfs hfsplus udf squashfs; do modprobe -n -v $m; lsmod | grep -w $m; done",
185 Refs: []check.Ref{refModprobeD, refKSPP},
186 Standards: check.Standards{CWE: []string{"CWE-1188"}, NIST: []string{"CM-7", "CM-6"}, CISTopic: "Disable unused filesystem kernel modules (CIS Controls v8 4.8)"},
187 Run: func(ctx context.Context, s sys.System) []check.Result {
188 mods := fsModules
189 if _, err := s.Stat("/snap"); err == nil {
190 mods = mods[:len(mods)-1]
191 }
192 state, unreadable := modprobeState(ctx, s, mods)
193 var missing, ev []string
194 for _, m := range mods {
195 if len(state[m]) == 0 {
196 missing = append(missing, m)
197 } else {
198 ev = append(ev, state[m]...)
199 }
200 }
201 if len(missing) > 0 {
202 if unreadable {
203 return []check.Result{check.IncompleteR("/etc/modprobe.d", "Some modprobe.d files could not be read; not disabled in the readable ones: "+strings.Join(missing, ", "))}
204 }
205 return []check.Result{check.FailR("/etc/modprobe.d", "Filesystem modules not disabled: "+strings.Join(missing, ", "), ev...)}
206 }
207 return []check.Result{check.PassR("/etc/modprobe.d", fmt.Sprintf("All %d uncommon filesystem modules are disabled", len(mods)), ev...)}
208 },
209 })
210
211 check.Register(&check.Check{
212 ID: "HOST-BOOT-004", Title: "USB Storage Driver Not Disabled", Domain: check.DomainHost,
213 Severity: check.Medium, Category: check.CatConfig, Effort: check.EffortLow,
214 CVSS: "CVSS:3.1/AV:P/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
215 Summary: "Checks that the usb-storage kernel module is disabled in modprobe.d on servers that do not need removable media.",
216 Rationale: "A USB stick plugged into a server is the simplest way to copy data out or bring malware in without touching the network. Disabling the driver makes the device invisible to the kernel.",
217 Detection: "Reads /etc/modprobe.d/*.conf and /usr/lib/modprobe.d/*.conf for install usb-storage /bin/false (or /bin/true) or blacklist usb-storage. Neither present fails; an unreadable file makes the check incomplete.",
218 Fix: "Create /etc/modprobe.d/openvapt-usb.conf containing install usb-storage /bin/false and blacklist usb-storage then modprobe -r usb-storage. Servers that need USB backups should record the exception instead.",
219 Verify: "modprobe -n -v usb-storage should print install /bin/false; lsmod | grep usb_storage should print nothing.",
220 Refs: []check.Ref{refModprobeD, {Title: "MITRE ATT&CK T1091, Replication Through Removable Media", URL: "https://attack.mitre.org/techniques/T1091/"}},
221 Standards: check.Standards{CWE: []string{"CWE-1188"}, NIST: []string{"MP-7", "CM-7"}, CISTopic: "Disable USB storage (CIS Controls v8 4.8)"},
222 Run: func(ctx context.Context, s sys.System) []check.Result {
223 state, unreadable := modprobeState(ctx, s, []string{"usb-storage"})
224 if len(state["usb-storage"]) > 0 {
225 return []check.Result{check.PassR("/etc/modprobe.d", "usb-storage is disabled", state["usb-storage"]...)}
226 }
227 if unreadable {
228 return []check.Result{check.IncompleteR("/etc/modprobe.d", "Some modprobe.d files could not be read; usb-storage is not disabled in the readable ones")}
229 }
230 return []check.Result{check.FailR("/etc/modprobe.d", "usb-storage is not disabled in modprobe.d")}
231 },
232 })
233
234 check.Register(&check.Check{
235 ID: "HOST-BOOT-005", Title: "Kernel Module Loading Unrestricted", Domain: check.DomainHost,
236 Severity: check.Low, Category: check.CatConfig, Effort: check.EffortMedium,
237 CVSS: "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:L",
238 Summary: "Checks that the running kernel refuses unsigned modules or has module loading switched off entirely.",
239 Rationale: "A root-level attacker who can load any module owns the kernel: rootkits hide processes and files from every tool on the box. Signature enforcement or kernel.modules_disabled = 1 after boot makes that persistence step fail.",
240 Detection: "Reads /proc/sys/kernel/modules_disabled (1 passes), otherwise /sys/module/module/parameters/sig_enforce (Y passes) and /sys/kernel/security/lockdown ([integrity] or [confidentiality] passes). Kernels built without loadable module support are skipped; a value that exists but cannot be read is incomplete.",
241 Fix: "Boot with module.sig_enforce=1 (or lockdown=integrity) on the kernel command line and sign any out-of-tree modules, or set kernel.modules_disabled = 1 from a late boot service once every needed module is loaded (it cannot be turned back on without a reboot).",
242 Verify: "sysctl kernel.modules_disabled; cat /sys/module/module/parameters/sig_enforce /sys/kernel/security/lockdown",
243 Refs: []check.Ref{refModSign, refKSPP},
244 Standards: check.Standards{CWE: []string{"CWE-1188"}, NIST: []string{"SI-7", "CM-7"}, CISTopic: "Restrict kernel module loading (CIS Controls v8 4.8)"},
245 Run: func(ctx context.Context, s sys.System) []check.Result {
246 v, err := collect.Sysctl(s, "kernel.modules_disabled")
247 if err != nil {
248 if errors.Is(err, fs.ErrNotExist) {
249 return []check.Result{check.SkipR("kernel.modules_disabled", "Kernel has no loadable module support")}
250 }
251 return []check.Result{check.IncompleteR("kernel.modules_disabled", "Parameter could not be read")}
252 }
253 if v == "1" {
254 return []check.Result{check.PassR("kernel.modules_disabled", "Module loading is disabled for this boot", "kernel.modules_disabled = 1")}
255 }
256 var ev []string
257 for _, p := range []string{"/sys/module/module/parameters/sig_enforce", "/sys/kernel/security/lockdown"} {
258 text, exists, ok := readSome(ctx, s, p)
259 if !exists {
260 continue
261 }
262 if !ok {
263 return []check.Result{check.IncompleteR(p, "Value could not be read")}
264 }
265 text = strings.TrimSpace(text)
266 ev = append(ev, p+" = "+text)
267 if (strings.HasSuffix(p, "sig_enforce") && text == "Y") || strings.Contains(text, "[integrity]") || strings.Contains(text, "[confidentiality]") {
268 return []check.Result{check.PassR("kernel modules", "Unsigned modules are refused", ev...)}
269 }
270 }
271 return []check.Result{check.FailR("kernel.modules_disabled", "Any module can be loaded: modules_disabled = "+v+" and no signature enforcement or lockdown", ev...)}
272 },
273 })
274
275 check.Register(&check.Check{
276 ID: "HOST-BOOT-006", Title: "No File Integrity Monitoring", Domain: check.DomainHost,
277 Severity: check.Medium, Category: check.CatLogging, Effort: check.EffortMedium,
278 CVSS: "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:H/A:N",
279 Summary: "Checks that a file integrity tool (AIDE, Tripwire, Samhain or equivalent) is installed and scheduled to run.",
280 Rationale: "After a compromise the first question is which files changed. Without a baseline and a scheduled comparison, a replaced binary, an added SUID file or an edited PAM stack stays invisible for months.",
281 Detection: "Looks for the tools' programs and configuration (/usr/bin/aide, /usr/sbin/aide, /etc/aide/aide.conf, /etc/aide.conf, /usr/sbin/tripwire, /etc/tripwire, /usr/sbin/samhain, /etc/samhainrc, /usr/sbin/osqueryd and /opt/ossec/bin/ossec-syscheckd) and then for a schedule: a file in /etc/cron.d, /etc/cron.daily or /etc/cron.weekly, a systemd timer under /etc/systemd/system or /usr/lib/systemd/system, or a line in /etc/crontab that names the tool. Tool present with no schedule still fails. Stat and glob only; configuration contents are not read except /etc/crontab.",
282 Fix: "Install AIDE (apt install aide / dnf install aide), initialise the database with aideinit or aide --init, move the new database into place, and enable the daily check (dailyaidecheck.timer on Debian family, or a cron.daily entry) that mails or logs the differences.",
283 Verify: "aide --check (or your tool's check command) runs and reports; systemctl list-timers | grep -i aide or ls /etc/cron.daily | grep -i aide",
284 Refs: []check.Ref{refAIDE, refNIST80053, refNIST800123},
285 Standards: check.Standards{CWE: []string{"CWE-778"}, NIST: []string{"SI-7", "CM-3"}, CISTopic: "File integrity checking (CIS Controls v8 4.1)"},
286 Run: func(ctx context.Context, s sys.System) []check.Result {
287 tools := []struct {
288 name string
289 paths []string
290 }{
291 {"aide", []string{"/usr/bin/aide", "/usr/sbin/aide", "/etc/aide/aide.conf", "/etc/aide.conf"}},
292 {"tripwire", []string{"/usr/sbin/tripwire", "/etc/tripwire"}},
293 {"samhain", []string{"/usr/sbin/samhain", "/etc/samhainrc"}},
294 {"osquery", []string{"/usr/sbin/osqueryd", "/usr/bin/osqueryd"}},
295 {"ossec", []string{"/opt/ossec/bin/ossec-syscheckd", "/var/ossec/bin/ossec-syscheckd", "/var/ossec/bin/wazuh-syscheckd"}},
296 }
297 var installed []string
298 for _, t := range tools {
299 for _, p := range t.paths {
300 if _, err := s.Stat(p); err == nil {
301 installed = append(installed, t.name)
302 break
303 }
304 }
305 }
306 if len(installed) == 0 {
307 return []check.Result{check.FailR("integrity", "No file integrity tool found (AIDE, Tripwire, Samhain, osquery or OSSEC)")}
308 }
309 var sched []string
310 for _, name := range installed {
311 if name == "osquery" || name == "ossec" {
312 sched = append(sched, name+" runs as a daemon")
313 continue
314 }
315 for _, pat := range []string{"/etc/cron.d/*", "/etc/cron.daily/*", "/etc/cron.weekly/*", "/etc/systemd/system/*.timer", "/etc/systemd/system/timers.target.wants/*.timer", "/usr/lib/systemd/system/*.timer", "/lib/systemd/system/*.timer"} {
316 paths, _ := s.Glob(pat)
317 sort.Strings(paths)
318 for _, p := range paths {
319 if strings.Contains(strings.ToLower(p), name) {
320 sched = append(sched, p)
321 }
322 }
323 }
324 if text, _, ok := readSome(ctx, s, "/etc/crontab"); ok && strings.Contains(text, name) {
325 sched = append(sched, "/etc/crontab names "+name)
326 }
327 }
328 if len(sched) == 0 {
329 return []check.Result{check.FailR("integrity", strings.Join(installed, ", ")+" installed but no scheduled check was found")}
330 }
331 return []check.Result{check.PassR("integrity", strings.Join(installed, ", ")+" installed and scheduled", sched...)}
332 },
333 })
334
335 check.Register(&check.Check{
336 ID: "HOST-BOOT-007", Title: "Prelink Installed", Domain: check.DomainHost,
337 Severity: check.Low, Category: check.CatConfig, Effort: check.EffortLow,
338 CVSS: "CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:L/A:N",
339 Summary: "Checks that the prelink tool is absent, or disabled where the package is still installed.",
340 Rationale: "Prelink rewrites binaries and libraries in place, which changes their hashes and defeats integrity monitoring and package verification, and it weakens address randomisation by fixing load addresses. Modern systems gain nothing from it.",
341 Detection: "Stats /usr/sbin/prelink, /usr/bin/prelink and /etc/prelink.conf. If any exists, reads /etc/sysconfig/prelink and /etc/default/prelink for PRELINKING=no, which passes with a note. Absent files pass; a stat error other than not-found is incomplete.",
342 Fix: "Undo and remove it: prelink -ua && apt purge prelink (or dnf remove prelink). Then re-initialise the integrity database (aide --init).",
343 Verify: "command -v prelink should print nothing.",
344 Refs: []check.Ref{{Title: "prelink(8), Linux man-pages", URL: "https://man7.org/linux/man-pages/man8/prelink.8.html"}, refNIST80053},
345 Standards: check.Standards{CWE: []string{"CWE-344"}, NIST: []string{"SI-7", "CM-7"}, CISTopic: "Prelink not installed (CIS Controls v8 4.8)"},
346 Run: func(ctx context.Context, s sys.System) []check.Result {
347 var found []string
348 for _, p := range []string{"/usr/sbin/prelink", "/usr/bin/prelink", "/etc/prelink.conf"} {
349 _, err := s.Stat(p)
350 switch {
351 case err == nil:
352 found = append(found, p)
353 case !errors.Is(err, fs.ErrNotExist):
354 return []check.Result{check.IncompleteR(p, "Could not stat "+p)}
355 }
356 }
357 if len(found) == 0 {
358 return []check.Result{check.PassR("prelink", "Prelink is not installed")}
359 }
360 for _, cfg := range []string{"/etc/sysconfig/prelink", "/etc/default/prelink"} {
361 text, exists, ok := readSome(ctx, s, cfg)
362 if exists && !ok {
363 return []check.Result{check.IncompleteR(cfg, "Prelink configuration could not be read")}
364 }
365 if prelinkOff.MatchString(text) {
366 return []check.Result{check.PassR("prelink", "Prelink is installed but disabled (PRELINKING=no); removing the package is cleaner", found...)}
367 }
368 }
369 return []check.Result{check.FailR("prelink", "Prelink is installed", found...)}
370 },
371 })
372
373 check.Register(&check.Check{
374 ID: "HOST-BOOT-008", Title: "Core Dumps Not Restricted", Domain: check.DomainHost,
375 Severity: check.Low, Category: check.CatExposure, Effort: check.EffortLow,
376 CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
377 Summary: "Checks that process core dumps are turned off through PAM limits, and through systemd-coredump where the kernel hands dumps to it.",
378 Rationale: "A core dump is a copy of a program's memory, including passwords, keys and session tokens it held. On a server nobody debugs, dumps are only a leak waiting in /var/lib/systemd/coredump or the working directory. HOST-KRN-004 covers setuid programs; this check covers every process.",
379 Detection: "Reads /etc/security/limits.conf and /etc/security/limits.d/*.conf for a * hard core 0 line, and /proc/sys/kernel/core_pattern. When the pattern pipes dumps to systemd-coredump, /etc/systemd/coredump.conf and /etc/systemd/coredump.conf.d/*.conf must set Storage=none (ProcessSizeMax=0 also counts). Either the limits line or the systemd setting passes. Missing files are observed as missing; a file that exists but cannot be read makes the check incomplete.",
380 Fix: "Add * hard core 0 to /etc/security/limits.conf (or a file in limits.d), set Storage=none and ProcessSizeMax=0 in the [Coredump] section of /etc/systemd/coredump.conf, and keep fs.suid_dumpable = 0.",
381 Verify: "grep -E '^\\*\\s+hard\\s+core\\s+0' /etc/security/limits.conf /etc/security/limits.d/*; grep -E '^(Storage|ProcessSizeMax)' /etc/systemd/coredump.conf; cat /proc/sys/kernel/core_pattern",
382 Refs: []check.Ref{refLimits, refCoredump, refKernelDocs},
383 Standards: check.Standards{CWE: []string{"CWE-528"}, NIST: []string{"SI-11", "CM-6", "AC-6"}, CISTopic: "Restrict core dumps (CIS Controls v8 4.1)"},
384 Run: func(ctx context.Context, s sys.System) []check.Result {
385 var ev []string
386 partial := false
387 files := []string{"/etc/security/limits.conf"}
388 more, _ := s.Glob("/etc/security/limits.d/*.conf")
389 sort.Strings(more)
390 files = append(files, more...)
391 limited := false
392 for _, p := range files {
393 text, exists, ok := readSome(ctx, s, p)
394 if !exists {
395 continue
396 }
397 if !ok {
398 partial = true
399 continue
400 }
401 if hardCore0.MatchString(text) {
402 limited = true
403 ev = append(ev, p+": * hard core 0")
404 }
405 }
406 pattern, _, _ := readSome(ctx, s, "/proc/sys/kernel/core_pattern")
407 pattern = strings.TrimSpace(pattern)
408 if pattern != "" {
409 ev = append(ev, "kernel.core_pattern = "+pattern)
410 }
411 systemdOff := false
412 if strings.Contains(pattern, "systemd-coredump") {
413 cfgs := []string{"/etc/systemd/coredump.conf"}
414 more, _ := s.Glob("/etc/systemd/coredump.conf.d/*.conf")
415 sort.Strings(more)
416 cfgs = append(cfgs, more...)
417 for _, p := range cfgs {
418 text, exists, ok := readSome(ctx, s, p)
419 if !exists {
420 continue
421 }
422 if !ok {
423 partial = true
424 continue
425 }
426 if storageNone.MatchString(text) {
427 systemdOff = true
428 ev = append(ev, p+": Storage=none or ProcessSizeMax=0")
429 }
430 }
431 }
432 switch {
433 case limited || systemdOff:
434 return []check.Result{check.PassR("core dumps", "Core dumps are restricted", ev...)}
435 case partial:
436 return []check.Result{check.IncompleteR("core dumps", "A limits or coredump configuration file could not be read", ev...)}
437 default:
438 return []check.Result{check.FailR("core dumps", "No hard core 0 limit and no systemd-coredump Storage=none", ev...)}
439 }
440 },
441 })
442}
443
444var (
445 grubSuperusers = regexp.MustCompile(`(?m)^\s*set\s+superusers\s*=`)
446 grubPassword = regexp.MustCompile(`(?m)^\s*password(_pbkdf2)?\s+\S+\s+\S+`)
447 prelinkOff = regexp.MustCompile(`(?mi)^\s*PRELINKING\s*=\s*"?no"?\s*$`)
448 hardCore0 = regexp.MustCompile(`(?m)^\s*\*\s+(hard|-)\s+core\s+0\s*$`)
449 storageNone = regexp.MustCompile(`(?mi)^\s*(Storage\s*=\s*none|ProcessSizeMax\s*=\s*0)\s*$`)
450)

Also available as plain Markdown for notes, scripts and AI tools. Same facts, same source, no styling.

Cite This Page

If this page helped your work, please cite it. The version and web address are filled in; the access date is today's date in your browser. Every other style, and when credit is required, is on How to Cite OpenVAPT.

APA 7
OpenVAPT. (2026). HOST-BOOT-005 Kernel Module Loading Unrestricted (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-BOOT-005.html

In text: (OpenVAPT, 2026). Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.

BibTeX
@misc{openvapt_host_boot_005,
  author       = {{OpenVAPT}},
  title        = {{HOST-BOOT-005 Kernel Module Loading Unrestricted}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/HOST-BOOT-005.html}},
  note         = {OpenVAPT Check Catalogue, version 0.1.0-alpha.6. Accessed 16 October 2026}
}

Accessed: the date shown is today's date from your own browser (the site's build date without JavaScript); change it to the day you read the page.

Citing OpenVAPT does not mean OpenVAPT endorses, approves or has reviewed your work.