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-MAC-001Secure ConfigurationServer

No Mandatory Access Control Enforced

Checks that AppArmor or SELinux is enabled and enforcing.

Why It Matters
Mandatory access control confines services even when they run as root, so a compromised web server or database cannot freely read or change the rest of the system.
How It Detects
Reads /sys/fs/selinux/enforce (1 means enforcing) and /sys/module/apparmor/parameters/enabled (Y means enabled).
How to Fix
Ubuntu/Debian: keep AppArmor enabled (systemctl enable --now apparmor) and its profiles in enforce mode. RHEL family: set SELINUX=enforcing in /etc/selinux/config and relabel. Test applications in permissive or complain mode first.
How to Verify
getenforce or aa-status
CVSS v3.1 Vector
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N
Control Category
Secure Configuration: CIS Controls v8 4; NIST CSF 2.0 PR.PS
Fix Effort
High
CIS Controls v8 Safeguard
4.1
NIST CSF 2.0 Subcategory
PR.PS-01
CWE
CWE-250
NIST SP 800-53 Rev. 5
AC-3(3), AC-6
Benchmark Topic
Mandatory access control (topic only, no benchmark text)
References
Linux kernel documentation, Linux Security Module development and LSMs
Learn the theory behind this check: P1 Linux Server Hardening.

Source Code

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

internal/checks/host/hardening.go · Lines 1-476View on GitHub at This Commit
1package host
2
3import (
4 "context"
5 "fmt"
6 "io/fs"
7 "strconv"
8 "strings"
9
10 "github.com/openvapt/openvapt/internal/check"
11 "github.com/openvapt/openvapt/internal/checks/collect"
12 "github.com/openvapt/openvapt/internal/sys"
13)
14
15// fileModeRule checks that existing files matching patterns are not readable
16// or writable beyond what is safe. One table entry becomes one check.
17type fileModeRule struct {
18 id, title, summary, rationale, fix, verify string
19 sev check.Severity
20 cat check.Category
21 cvss string
22 patterns []string
23 bad fs.FileMode // permission bits that must not be set
24 why string
25 cwe string
26 refs []check.Ref
27}
28
29var fileModeRules = []fileModeRule{
30 {
31 id: "HOST-SSH-009", title: "SSH Host Private Keys Readable by Others", sev: check.High, cat: check.CatCrypto,
32 cvss: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
33 summary: "Checks that SSH host private keys are readable only by root.",
34 rationale: "Anyone who can read a host private key can impersonate the server and intercept SSH sessions from users who trust it.",
35 fix: "chown root:root /etc/ssh/ssh_host_*_key && chmod 600 /etc/ssh/ssh_host_*_key (0640 with group ssh_keys is acceptable on RHEL family).",
36 verify: "stat -c '%a %U:%G %n' /etc/ssh/ssh_host_*_key",
37 patterns: []string{"/etc/ssh/ssh_host_*_key"}, bad: 0o004 | 0o002, why: "readable or writable by others", cwe: "CWE-732",
38 refs: []check.Ref{refSSHDManual},
39 },
40 {
41 id: "HOST-SSH-010", title: "SSH Server Configuration Writable by Non-Root Users", sev: check.High, cat: check.CatAccess,
42 cvss: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
43 summary: "Checks that the SSH daemon configuration cannot be modified by group or other users.",
44 rationale: "Whoever can edit sshd_config can enable root login, add their own keys file location or weaken authentication.",
45 fix: "chown root:root /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf && chmod 600 /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf",
46 verify: "stat -c '%a %U %n' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*",
47 patterns: []string{"/etc/ssh/sshd_config", "/etc/ssh/sshd_config.d/*.conf"}, bad: 0o022, why: "writable by group or others", cwe: "CWE-732",
48 refs: []check.Ref{refSSHDManual},
49 },
50 {
51 id: "HOST-FS-006", title: "Boot Loader Configuration Exposed", sev: check.Medium, cat: check.CatAccess,
52 cvss: "CVSS:3.1/AV:P/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
53 summary: "Checks that the GRUB configuration is not readable or writable by normal users.",
54 rationale: "The boot loader configuration controls kernel parameters at boot. If writable, it allows boot-time privilege escalation; if readable, it may expose a boot password hash.",
55 fix: "chown root:root <grub.cfg> && chmod 600 <grub.cfg> (for example /boot/grub/grub.cfg or /boot/grub2/grub.cfg).",
56 verify: "stat -c '%a %U %n' /boot/grub/grub.cfg /boot/grub2/grub.cfg",
57 patterns: []string{"/boot/grub/grub.cfg", "/boot/grub2/grub.cfg", "/boot/grub2/user.cfg"}, bad: 0o077, why: "accessible by group or others", cwe: "CWE-732",
58 refs: []check.Ref{{Title: "GNU GRUB Manual, Security", URL: "https://www.gnu.org/software/grub/manual/grub/html_node/Security.html"}},
59 },
60 {
61 id: "HOST-FS-007", title: "Private TLS Keys Readable by Others", sev: check.High, cat: check.CatCrypto,
62 cvss: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
63 summary: "Checks that TLS private keys under the standard key directories are not readable by other users.",
64 rationale: "A stolen private key lets an attacker impersonate your websites and, with older key exchange, decrypt recorded traffic.",
65 fix: "chown root:root <key> && chmod 600 <key> (or 640 with a dedicated group the web server runs in). Rotate any key that was exposed.",
66 verify: "stat -c '%a %U:%G %n' /etc/ssl/private/* /etc/pki/tls/private/* /etc/letsencrypt/archive/*/privkey*.pem",
67 patterns: []string{"/etc/ssl/private/*", "/etc/pki/tls/private/*", "/etc/letsencrypt/archive/*/privkey*.pem"}, bad: 0o004 | 0o002, why: "readable or writable by others", cwe: "CWE-732",
68 refs: []check.Ref{{Title: "OWASP Key Management Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Key_Management_Cheat_Sheet.html"}},
69 },
70 {
71 id: "HOST-LOG-003", title: "Security Logs Readable by All Users", sev: check.Low, cat: check.CatLogging,
72 cvss: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
73 summary: "Checks that authentication and system logs are not world-readable.",
74 rationale: "Authentication logs contain user names, source addresses and sometimes passwords typed into the user name field. Only administrators should read them.",
75 fix: "chmod o-rwx /var/log/auth.log /var/log/secure /var/log/syslog /var/log/messages and set the matching create mode in /etc/logrotate.d.",
76 verify: "stat -c '%a %n' /var/log/auth.log /var/log/secure",
77 patterns: []string{"/var/log/auth.log", "/var/log/secure", "/var/log/syslog", "/var/log/messages"}, bad: 0o004 | 0o002, why: "readable or writable by others", cwe: "CWE-532",
78 refs: []check.Ref{{Title: "NIST SP 800-92, Guide to Computer Security Log Management", URL: "https://csrc.nist.gov/pubs/sp/800/92/final"}},
79 },
80}
81
82func init() {
83 for _, r := range fileModeRules {
84 registerFileMode(r)
85 }
86 registerHardening()
87}
88
89// registerFileMode turns one file permission rule into a registered check.
90func registerFileMode(r fileModeRule) {
91 {
92 check.Register(&check.Check{
93 ID: r.id, Title: r.title, Domain: check.DomainHost, Severity: r.sev, Category: r.cat, Effort: check.EffortLow, CVSS: r.cvss,
94 Summary: r.summary, Rationale: r.rationale,
95 Detection: "Stats every existing file matching " + strings.Join(r.patterns, ", ") + " (metadata only, contents are never read) and fails if it is " + r.why + ".",
96 Fix: r.fix, Verify: r.verify, Refs: r.refs,
97 Standards: check.Standards{CWE: []string{r.cwe}, NIST: []string{"AC-3", "AC-6"}},
98 Run: func(ctx context.Context, s sys.System) []check.Result {
99 var bad []string
100 seen := 0
101 for _, pat := range r.patterns {
102 paths := []string{pat}
103 if strings.ContainsAny(pat, "*?[") {
104 paths, _ = s.Glob(pat)
105 }
106 for _, p := range paths {
107 st, err := s.Stat(p)
108 if err != nil || st.IsDir {
109 continue
110 }
111 seen++
112 if st.Mode.Perm()&r.bad != 0 {
113 bad = append(bad, perm(st.Mode)+" "+p)
114 }
115 }
116 }
117 if seen == 0 {
118 return []check.Result{check.SkipR(r.patterns[0], "No matching files found")}
119 }
120 if len(bad) > 0 {
121 return []check.Result{check.FailR(r.patterns[0], fmt.Sprintf("%d file(s) are %s", len(bad), r.why), bad...)}
122 }
123 return []check.Result{check.PassR(r.patterns[0], fmt.Sprintf("%d file(s) have safe permissions", seen))}
124 },
125 })
126 }
127}
128
129func registerHardening() {
130 check.Register(&check.Check{
131 ID: "HOST-SSH-011", Title: "SSH Legacy Trust Options Enabled", Domain: check.DomainHost,
132 Severity: check.Medium, Category: check.CatIdentity, Effort: check.EffortLow,
133 CVSS: "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L",
134 Summary: "Checks for host-based trust and user environment options that weaken SSH authentication.",
135 Rationale: "HostbasedAuthentication and ignoring .rhosts trust other machines instead of users. PermitUserEnvironment lets users set variables such as LD_PRELOAD that can bypass restrictions.",
136 Detection: "Reads hostbasedauthentication, ignorerhosts and permituserenvironment from the effective sshd configuration (all evaluated Match contexts).",
137 Fix: "Set HostbasedAuthentication no , IgnoreRhosts yes and PermitUserEnvironment no in /etc/ssh/sshd_config, then reload sshd.",
138 Verify: "sshd -T | grep -Ei 'hostbasedauthentication|ignorerhosts|permituserenvironment'",
139 Refs: []check.Ref{refSSHDManual},
140 Standards: check.Standards{CWE: []string{"CWE-290", "CWE-15"}, NIST: []string{"IA-2", "CM-6"}, CISTopic: "SSH host-based authentication and user environment"},
141 Run: func(ctx context.Context, s sys.System) []check.Result {
142 cfg, ok := collect.SSHD(ctx, s)
143 if !ok {
144 return []check.Result{check.SkipR("sshd", "SSH daemon configuration not found")}
145 }
146 var bad []string
147 if cfg["hostbasedauthentication"] == "yes" {
148 bad = append(bad, "hostbasedauthentication yes")
149 }
150 if cfg["ignorerhosts"] == "no" {
151 bad = append(bad, "ignorerhosts no")
152 }
153 if v := cfg["permituserenvironment"]; v != "" && v != "no" {
154 bad = append(bad, "permituserenvironment "+v)
155 }
156 if len(bad) > 0 {
157 return []check.Result{check.FailR("sshd", "Legacy trust options are enabled", bad...)}
158 }
159 return []check.Result{check.PassR("sshd", "No legacy trust options enabled")}
160 },
161 })
162
163 check.Register(&check.Check{
164 ID: "HOST-SSH-012", Title: "SSH Logging Level Too Low", Domain: check.DomainHost,
165 Severity: check.Low, Category: check.CatLogging, Effort: check.EffortLow,
166 CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N",
167 Summary: "Checks that sshd logs at INFO or VERBOSE level.",
168 Rationale: "QUIET, FATAL or ERROR levels drop login records, so failed and successful logins cannot be investigated. VERBOSE also records the key fingerprint used.",
169 Detection: "Reads loglevel from the effective sshd configuration; INFO and VERBOSE pass.",
170 Fix: "Set LogLevel VERBOSE (or INFO) in /etc/ssh/sshd_config and reload sshd.",
171 Verify: "sshd -T | grep -i loglevel",
172 Refs: []check.Ref{refSSHDManual},
173 Standards: check.Standards{CWE: []string{"CWE-778"}, OWASP: []string{"A09:2021"}, NIST: []string{"AU-2", "AU-3"}},
174 Run: func(ctx context.Context, s sys.System) []check.Result {
175 cfg, ok := collect.SSHD(ctx, s)
176 if !ok {
177 return []check.Result{check.SkipR("sshd", "SSH daemon configuration not found")}
178 }
179 v := cfg["loglevel"]
180 if v == "" || v == "info" || v == "verbose" {
181 return []check.Result{check.PassR("sshd", "LogLevel "+orDefault(v))}
182 }
183 return []check.Result{check.FailR("sshd", "LogLevel "+v+" drops login records")}
184 },
185 })
186
187 check.Register(&check.Check{
188 ID: "HOST-FS-008", Title: "Temporary Filesystems Allow Executables or Devices", Domain: check.DomainHost,
189 Severity: check.Low, Category: check.CatConfig, Effort: check.EffortMedium,
190 CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
191 Summary: "Checks that /tmp, /var/tmp and /dev/shm are mounted with nodev and nosuid.",
192 Rationale: "World-writable temporary areas are where attackers drop tools. nosuid stops planted setuid programs working there, and nodev stops device files being created.",
193 Detection: "Reads /proc/mounts. For each of /tmp, /var/tmp and /dev/shm that is a separate mount, nodev and nosuid must be present. Paths that are not separate mounts are reported as skipped.",
194 Fix: "Mount the path as tmpfs or a separate filesystem with options nodev,nosuid (and noexec where software allows), for example via systemd tmp.mount or /etc/fstab, then remount.",
195 Verify: "findmnt -no OPTIONS /tmp /var/tmp /dev/shm",
196 Refs: []check.Ref{{Title: "mount(8), Linux man-pages (nodev, nosuid, noexec)", URL: "https://man7.org/linux/man-pages/man8/mount.8.html"}},
197 Standards: check.Standards{CWE: []string{"CWE-276"}, NIST: []string{"CM-6", "CM-7"}, CISTopic: "Separate partitions and mount options for temporary directories"},
198 Run: func(ctx context.Context, s sys.System) []check.Result {
199 text, _, ok := readText(ctx, s, "/proc/mounts")
200 if !ok {
201 return []check.Result{check.SkipR("/proc/mounts", "Not readable")}
202 }
203 opts := map[string]string{}
204 for _, line := range strings.Split(text, "\n") {
205 f := strings.Fields(line)
206 if len(f) >= 4 {
207 opts[f[1]] = f[3]
208 }
209 }
210 var out []check.Result
211 for _, mp := range []string{"/tmp", "/var/tmp", "/dev/shm"} {
212 o, ok := opts[mp]
213 if !ok {
214 continue
215 }
216 var miss []string
217 for _, need := range []string{"nodev", "nosuid"} {
218 if !strings.Contains(","+o+",", ","+need+",") {
219 miss = append(miss, need)
220 }
221 }
222 if len(miss) > 0 {
223 out = append(out, check.FailR(mp, mp+" is mounted without "+strings.Join(miss, ", "), "options "+o))
224 } else {
225 out = append(out, check.PassR(mp, mp+" has nodev and nosuid", "options "+o))
226 }
227 }
228 if len(out) == 0 {
229 return []check.Result{check.SkipR("/proc/mounts", "Temporary directories are not separate mounts")}
230 }
231 return out
232 },
233 })
234
235 check.Register(&check.Check{
236 ID: "HOST-FS-009", Title: "Default File Creation Mask Too Permissive", Domain: check.DomainHost,
237 Severity: check.Low, Category: check.CatAccess, Effort: check.EffortLow,
238 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
239 Summary: "Checks that the default umask makes new files unreadable to other users (027 or stricter).",
240 Rationale: "With umask 022 every new file is readable by every local user, including configuration exports, reports and backups created by administrators.",
241 Detection: "Reads UMASK from /etc/login.defs. Values that leave the 'other' read bit open (for example 022 or 002) fail.",
242 Fix: "Set UMASK 027 in /etc/login.defs (and, if used, in /etc/profile or /etc/bash.bashrc).",
243 Verify: "grep '^UMASK' /etc/login.defs",
244 Refs: []check.Ref{refLoginDefs, {Title: "umask(2), Linux man-pages", URL: "https://man7.org/linux/man-pages/man2/umask.2.html"}},
245 Standards: check.Standards{CWE: []string{"CWE-276"}, NIST: []string{"AC-6", "CM-6"}, CISTopic: "Default user umask"},
246 Run: func(ctx context.Context, s sys.System) []check.Result {
247 cfg := collect.ReadConfigs(ctx, s, "/etc/login.defs")
248 if len(cfg) == 0 {
249 return []check.Result{check.SkipR("/etc/login.defs", "File not found")}
250 }
251 v, _, ok := collect.First(cfg, "umask")
252 n, err := strconv.ParseUint(strings.TrimSpace(v), 8, 32)
253 if !ok || err != nil {
254 return []check.Result{check.FailR("/etc/login.defs", "UMASK is not set")}
255 }
256 if n&0o004 == 0 {
257 return []check.Result{check.FailR("/etc/login.defs", "UMASK "+v+" lets other users read new files (recommended 027)")}
258 }
259 return []check.Result{check.PassR("/etc/login.defs", "UMASK "+v)}
260 },
261 })
262
263 check.Register(&check.Check{
264 ID: "HOST-MAC-001", Title: "No Mandatory Access Control Enforced", Domain: check.DomainHost,
265 Severity: check.Low, Category: check.CatConfig, Effort: check.EffortHigh,
266 CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N",
267 Summary: "Checks that AppArmor or SELinux is enabled and enforcing.",
268 Rationale: "Mandatory access control confines services even when they run as root, so a compromised web server or database cannot freely read or change the rest of the system.",
269 Detection: "Reads /sys/fs/selinux/enforce (1 means enforcing) and /sys/module/apparmor/parameters/enabled (Y means enabled).",
270 Fix: "Ubuntu/Debian: keep AppArmor enabled (systemctl enable --now apparmor) and its profiles in enforce mode. RHEL family: set SELINUX=enforcing in /etc/selinux/config and relabel. Test applications in permissive or complain mode first.",
271 Verify: "getenforce or aa-status",
272 Refs: []check.Ref{{Title: "Linux kernel documentation, Linux Security Module development and LSMs", URL: "https://docs.kernel.org/admin-guide/LSM/index.html"}},
273 Standards: check.Standards{CWE: []string{"CWE-250"}, NIST: []string{"AC-3(3)", "AC-6"}, CISTopic: "Mandatory access control"},
274 Run: func(ctx context.Context, s sys.System) []check.Result {
275 if v, _, ok := readText(ctx, s, "/sys/fs/selinux/enforce"); ok && strings.TrimSpace(v) == "1" {
276 return []check.Result{check.PassR("selinux", "SELinux is enforcing")}
277 }
278 if v, _, ok := readText(ctx, s, "/sys/module/apparmor/parameters/enabled"); ok && strings.TrimSpace(v) == "Y" {
279 return []check.Result{check.PassR("apparmor", "AppArmor is enabled")}
280 }
281 return []check.Result{check.FailR("mac", "Neither SELinux (enforcing) nor AppArmor is active")}
282 },
283 })
284
285 check.Register(&check.Check{
286 ID: "HOST-UPD-001", Title: "Automatic Security Updates Not Configured", Domain: check.DomainHost,
287 Severity: check.Low, Category: check.CatPatch, Effort: check.EffortLow,
288 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
289 Summary: "Checks that security updates are installed automatically.",
290 Rationale: "Many compromises use vulnerabilities fixed days or weeks earlier. Automatic security updates close that window without relying on someone remembering.",
291 Detection: "Debian/Ubuntu: reads /etc/apt/apt.conf.d/20auto-upgrades (and 50unattended-upgrades) for APT::Periodic::Unattended-Upgrade \"1\". RHEL family: asks systemd whether dnf-automatic.timer or dnf-automatic-install.timer is active.",
292 Fix: "Debian/Ubuntu: apt install unattended-upgrades && dpkg-reconfigure -plow unattended-upgrades . RHEL family: dnf install dnf-automatic, set apply_updates = yes and upgrade_type = security, then systemctl enable --now dnf-automatic.timer .",
293 Verify: "Re-run this check.",
294 Refs: []check.Ref{{Title: "Debian, UnattendedUpgrades", URL: "https://wiki.debian.org/UnattendedUpgrades"}, {Title: "NIST SP 800-40 Rev. 4", URL: "https://csrc.nist.gov/pubs/sp/800/40/r4/final"}},
295 Standards: check.Standards{CWE: []string{"CWE-1104"}, OWASP: []string{"A06:2021"}, NIST: []string{"SI-2(2)"}},
296 Run: func(ctx context.Context, s sys.System) []check.Result {
297 for _, p := range []string{"/etc/apt/apt.conf.d/20auto-upgrades", "/etc/apt/apt.conf.d/50unattended-upgrades"} {
298 if t, _, ok := readText(ctx, s, p); ok && strings.Contains(t, `Unattended-Upgrade "1"`) {
299 return []check.Result{check.PassR(p, "Unattended upgrades are enabled")}
300 }
301 }
302 if u, ok := collect.ServiceActive(ctx, s, "dnf-automatic.timer", "dnf-automatic-install.timer"); ok {
303 return []check.Result{check.PassR(u, u+" is active")}
304 }
305 return []check.Result{check.FailR("updates", "No automatic security update mechanism is enabled")}
306 },
307 })
308
309 check.Register(&check.Check{
310 ID: "HOST-UPD-002", Title: "Reboot Required to Apply Updates", Domain: check.DomainHost,
311 Severity: check.Medium, Category: check.CatPatch, Effort: check.EffortMedium,
312 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L",
313 Summary: "Checks whether installed updates (often kernel or core library fixes) are waiting for a reboot.",
314 Rationale: "Updated packages do not protect anything until the running kernel and libraries are replaced. A pending reboot means known-fixed vulnerabilities are still live.",
315 Detection: "Checks for /var/run/reboot-required (Debian/Ubuntu) and reads the package list in /var/run/reboot-required.pkgs when present.",
316 Fix: "Schedule a maintenance window and reboot (or use live patching where supported).",
317 Verify: "ls /var/run/reboot-required should not exist after the reboot.",
318 Refs: []check.Ref{{Title: "NIST SP 800-40 Rev. 4", URL: "https://csrc.nist.gov/pubs/sp/800/40/r4/final"}},
319 Standards: check.Standards{CWE: []string{"CWE-1104"}, NIST: []string{"SI-2"}},
320 Run: func(ctx context.Context, s sys.System) []check.Result {
321 if _, err := s.Stat("/var/run/reboot-required"); err == nil {
322 ev := []string{"/var/run/reboot-required exists"}
323 if t, _, ok := readText(ctx, s, "/var/run/reboot-required.pkgs"); ok {
324 ev = append(ev, firstN(strings.Fields(t), 20)...)
325 }
326 return []check.Result{check.FailR("reboot", "A reboot is required to apply installed updates", ev...)}
327 }
328 if _, err := s.Stat("/etc/debian_version"); err != nil {
329 return []check.Result{check.SkipR("reboot", "Reboot-required marker is a Debian/Ubuntu feature")}
330 }
331 return []check.Result{check.PassR("reboot", "No reboot is pending")}
332 },
333 })
334
335 check.Register(&check.Check{
336 ID: "HOST-SVC-001", Title: "Unnecessary Legacy Services Running", Domain: check.DomainHost,
337 Severity: check.Low, Category: check.CatConfig, Effort: check.EffortLow,
338 CVSS: "CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
339 Summary: "Checks for desktop and legacy network services that servers rarely need.",
340 Rationale: "Each running service adds attack surface. Zeroconf discovery, printing, RPC portmapping and inetd-style super-servers are common, long-forgotten exposures.",
341 Detection: "Asks systemd whether avahi-daemon, cups, rpcbind, xinetd, telnet.socket, rsh.socket, rlogin.socket or vsftpd is active.",
342 Fix: "systemctl disable --now <service> for each listed service you do not need, or remove its package. Caution: check first that nothing depends on it (systemctl list-dependencies --reverse <service>) and keep console access when disabling network or remote-access services.",
343 Verify: "systemctl is-active avahi-daemon cups rpcbind xinetd",
344 Refs: []check.Ref{{Title: "NIST SP 800-123, Guide to General Server Security", URL: "https://csrc.nist.gov/pubs/sp/800/123/final"}},
345 Standards: check.Standards{CWE: []string{"CWE-1188"}, NIST: []string{"CM-7"}, CISTopic: "Remove or disable unnecessary services"},
346 Run: func(ctx context.Context, s sys.System) []check.Result {
347 var on []string
348 for _, u := range []string{"avahi-daemon", "cups", "rpcbind", "xinetd", "telnet.socket", "rsh.socket", "rlogin.socket", "vsftpd"} {
349 if _, ok := collect.ServiceActive(ctx, s, u); ok {
350 on = append(on, u)
351 }
352 }
353 if len(on) > 0 {
354 return []check.Result{check.FailR("services", "Unnecessary services are active", on...)}
355 }
356 return []check.Result{check.PassR("services", "No unnecessary legacy services are active")}
357 },
358 })
359
360 check.Register(&check.Check{
361 ID: "HOST-DKR-001", Title: "Users With Root-Equivalent Docker Access", Domain: check.DomainHost,
362 Severity: check.High, Category: check.CatIdentity, Effort: check.EffortMedium,
363 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
364 Summary: "Checks whether non-root users can control Docker, which is equivalent to root access.",
365 Rationale: "Members of the docker group, or anyone who can write to the Docker socket, can start a container that mounts the host filesystem and become root.",
366 Detection: "Reads members of the docker group from /etc/group and the permissions of /var/run/docker.sock.",
367 Fix: "Remove users who do not need it from the docker group (gpasswd -d <user> docker); consider rootless Docker or Podman; keep docker.sock at 660 root:docker.",
368 Verify: "getent group docker ; stat -c '%a %U:%G' /var/run/docker.sock",
369 Refs: []check.Ref{{Title: "Docker documentation, Docker daemon attack surface", URL: "https://docs.docker.com/engine/security/#docker-daemon-attack-surface"}},
370 Standards: check.Standards{CWE: []string{"CWE-250", "CWE-266"}, NIST: []string{"AC-6"}},
371 Run: func(ctx context.Context, s sys.System) []check.Result {
372 var ev []string
373 if t, _, ok := readText(ctx, s, "/etc/group"); ok {
374 for _, line := range strings.Split(t, "\n") {
375 f := strings.Split(line, ":")
376 if len(f) >= 4 && f[0] == "docker" && strings.TrimSpace(f[3]) != "" {
377 ev = append(ev, "docker group members: "+f[3])
378 }
379 }
380 }
381 st, err := s.Stat("/var/run/docker.sock")
382 if err == nil && st.Mode.Perm()&0o002 != 0 {
383 ev = append(ev, "/var/run/docker.sock is world-writable ("+perm(st.Mode)+")")
384 }
385 if err != nil && len(ev) == 0 {
386 return []check.Result{check.SkipR("docker", "Docker is not installed")}
387 }
388 if len(ev) > 0 {
389 return []check.Result{check.FailR("docker", "Non-root users can control Docker (root equivalent)", ev...)}
390 }
391 return []check.Result{check.PassR("docker", "Docker access is restricted to root")}
392 },
393 })
394
395 check.Register(&check.Check{
396 ID: "HOST-SUDO-001", Title: "Sudo Commands Not Logged to a Dedicated File", Domain: check.DomainHost,
397 Severity: check.Low, Category: check.CatLogging, Effort: check.EffortLow,
398 CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N",
399 Summary: "Checks that sudo uses a pseudo-terminal and keeps its own log file.",
400 Rationale: "Defaults use_pty stops background processes from a sudo session lingering after it ends. A dedicated logfile keeps an easily reviewed record of privileged commands.",
401 Detection: "Reads /etc/sudoers and /etc/sudoers.d/* for 'Defaults use_pty' and 'Defaults logfile='.",
402 Fix: "With visudo add: Defaults use_pty and Defaults logfile=\"/var/log/sudo.log\"",
403 Verify: "grep -rE 'use_pty|logfile' /etc/sudoers /etc/sudoers.d",
404 Refs: []check.Ref{{Title: "sudoers(5) manual", URL: "https://www.sudo.ws/docs/man/sudoers.man/"}},
405 Standards: check.Standards{CWE: []string{"CWE-778"}, OWASP: []string{"A09:2021"}, NIST: []string{"AU-2", "AU-12"}, CISTopic: "Sudo uses pty and log file"},
406 Run: func(ctx context.Context, s sys.System) []check.Result {
407 files := []string{"/etc/sudoers"}
408 more, _ := s.Glob("/etc/sudoers.d/*")
409 files = append(files, more...)
410 var all strings.Builder
411 read := false
412 for _, f := range files {
413 if t, _, ok := readText(ctx, s, f); ok {
414 read = true
415 all.WriteString(t + "\n")
416 }
417 }
418 if !read {
419 return []check.Result{check.SkipR("/etc/sudoers", "Not readable (run as root for this check)")}
420 }
421 var miss []string
422 if !strings.Contains(all.String(), "use_pty") {
423 miss = append(miss, "Defaults use_pty")
424 }
425 if !strings.Contains(all.String(), "logfile") {
426 miss = append(miss, "Defaults logfile=")
427 }
428 if len(miss) > 0 {
429 return []check.Result{check.FailR("/etc/sudoers", "Missing: "+strings.Join(miss, ", "))}
430 }
431 return []check.Result{check.PassR("/etc/sudoers", "sudo uses a pty and a dedicated log file")}
432 },
433 })
434
435 check.Register(&check.Check{
436 ID: "HOST-IDS-001", Title: "No Brute-Force Protection Service", Domain: check.DomainHost,
437 Severity: check.Low, Category: check.CatLogging, Effort: check.EffortLow,
438 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N",
439 Summary: "Checks that a log-based intrusion prevention service (fail2ban, sshguard or CrowdSec) is running.",
440 Rationale: "Internet-facing services receive constant guessing. Automatically blocking sources that fail repeatedly reduces noise and risk, and turns logs into active defence.",
441 Detection: "Asks systemd whether fail2ban, sshguard or crowdsec is active.",
442 Fix: "Install and enable one, for example: apt install fail2ban && systemctl enable --now fail2ban , with an sshd jail enabled.",
443 Verify: "systemctl is-active fail2ban",
444 Refs: []check.Ref{{Title: "fail2ban project", URL: "https://github.com/fail2ban/fail2ban"}, refNIST80053},
445 Standards: check.Standards{CWE: []string{"CWE-307"}, OWASP: []string{"A07:2021"}, NIST: []string{"SI-4", "AC-7"}},
446 Run: func(ctx context.Context, s sys.System) []check.Result {
447 if u, ok := collect.ServiceActive(ctx, s, "fail2ban", "sshguard", "crowdsec"); ok {
448 return []check.Result{check.PassR(u, u+" is active")}
449 }
450 return []check.Result{check.FailR("ids", "No brute-force protection service is active")}
451 },
452 })
453
454 check.Register(&check.Check{
455 ID: "HOST-LOG-004", Title: "Log Rotation Not Configured", Domain: check.DomainHost,
456 Severity: check.Low, Category: check.CatLogging, Effort: check.EffortLow,
457 CVSS: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L",
458 Summary: "Checks that logrotate (or journald size limits) keeps logs from filling the disk.",
459 Rationale: "Unrotated logs eventually fill the disk and stop services, and an attacker can deliberately flood logs to cause that or to hide activity.",
460 Detection: "Checks for /etc/logrotate.conf, or a SystemMaxUse setting in /etc/systemd/journald.conf.",
461 Fix: "Install logrotate (apt install logrotate / dnf install logrotate) and keep its daily timer enabled, or set SystemMaxUse= in journald.conf.",
462 Verify: "ls /etc/logrotate.conf ; systemctl is-active logrotate.timer",
463 Refs: []check.Ref{{Title: "logrotate(8), Linux man-pages", URL: "https://man7.org/linux/man-pages/man8/logrotate.8.html"}},
464 Standards: check.Standards{CWE: []string{"CWE-770"}, NIST: []string{"AU-4", "AU-11"}},
465 Run: func(ctx context.Context, s sys.System) []check.Result {
466 if _, err := s.Stat("/etc/logrotate.conf"); err == nil {
467 return []check.Result{check.PassR("/etc/logrotate.conf", "logrotate is configured")}
468 }
469 cfg := collect.ReadConfigs(ctx, s, "/etc/systemd/journald.conf")
470 if v, _, ok := collect.First(cfg, "systemmaxuse"); ok && v != "" {
471 return []check.Result{check.PassR("journald.conf", "SystemMaxUse="+v)}
472 }
473 return []check.Result{check.FailR("logs", "No log rotation or journal size limit configured")}
474 },
475 })
476}

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-MAC-001 No Mandatory Access Control Enforced (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-MAC-001.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_mac_001,
  author       = {{OpenVAPT}},
  title        = {{HOST-MAC-001 No Mandatory Access Control Enforced}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/HOST-MAC-001.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.