Kubernetes Credential Files Readable by Others
Checks that kubeconfig files and Kubernetes PKI private keys are not readable by other users.
- Why It Matters
- admin.conf and the cluster CA key grant full control of the cluster; any local user who can read them becomes a cluster administrator.
- How It Detects
- Stats every existing file matching /etc/kubernetes/admin.conf, /etc/kubernetes/super-admin.conf, /etc/kubernetes/kubelet.conf, /etc/kubernetes/controller-manager.conf, /etc/kubernetes/scheduler.conf, /etc/kubernetes/pki/*.key, /etc/kubernetes/pki/etcd/*.key (metadata only, contents are never read) and fails if it is readable or writable by group or others.
- How to Fix
- chmod 600 /etc/kubernetes/*.conf /etc/kubernetes/pki/*.key /etc/kubernetes/pki/etcd/*.key && chown root:root on each.
- How to Verify
stat -c '%a %n' /etc/kubernetes/*.conf /etc/kubernetes/pki/*.key- CVSS v3.1 Vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H- Control Category
- Identity and Authentication: CIS Controls v8 5, 6; NIST CSF 2.0 PR.AA
- Fix Effort
- Low
- CIS Controls v8 Safeguard
- 5.2
- NIST CSF 2.0 Subcategory
- PR.AA-01
- CWE
- CWE-732
- NIST SP 800-53 Rev. 5
- AC-3, AC-6
- References
- Kubernetes documentation, Securing a cluster
Source Code
The complete file that defines this check. Run it with openvapt scan --only HOST-K8S-003, or read it offline with openvapt explain HOST-K8S-003.
internal/checks/host/hardening.go · Lines 1-476View on GitHub at This Commit
| 1 | package host |
| 2 | |
| 3 | import ( |
| 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. |
| 17 | type 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 | |
| 29 | var 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 | |
| 82 | func 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. |
| 90 | func 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 | |
| 129 | func 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.
OpenVAPT. (2026). HOST-K8S-003 Kubernetes Credential Files Readable by Others (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-K8S-003.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.
@misc{openvapt_host_k8s_003,
author = {{OpenVAPT}},
title = {{HOST-K8S-003 Kubernetes Credential Files Readable by Others}},
year = {2026},
howpublished = {\url{https://openvapt.com/checks/HOST-K8S-003.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.