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
CriticalHOST-K8S-001Identity and AuthenticationServer

Kubelet Allows Anonymous or Unauthorised Access

Checks the kubelet configuration for anonymous authentication, AlwaysAllow authorisation or the unauthenticated read-only port.

Why It Matters
The kubelet API can run commands in every pod on the node. With anonymous access or AlwaysAllow, anyone who can reach port 10250 controls the node's workloads; the read-only port 10255 leaks pod and environment details.
How It Detects
Reads /var/lib/kubelet/config.yaml and looks for authentication.anonymous.enabled: true, authorization.mode: AlwaysAllow, or a non-zero readOnlyPort.
How to Fix
Set authentication.anonymous.enabled: false, authorization.mode: Webhook and readOnlyPort: 0 in the kubelet configuration, then restart the kubelet.
How to Verify
grep -A2 -E 'anonymous|authorization|readOnlyPort' /var/lib/kubelet/config.yaml
CVSS v3.1 Vector
CVSS:3.1/AV:N/AC:L/PR:N/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
Medium
CIS Controls v8 Safeguard
5.2
NIST CSF 2.0 Subcategory
PR.AA-01
CWE
CWE-306, CWE-862
OWASP Top 10
A01:2021, A07:2021
NIST SP 800-53 Rev. 5
AC-3, IA-2
References
Kubernetes documentation, Kubelet authentication and authorization
Kubernetes documentation, Securing a cluster
Learn the theory behind this check: P9 Containers and Kubernetes.

Source Code

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

internal/checks/host/containers.go · Lines 1-322View on GitHub at This Commit
1package host
2
3import (
4 "context"
5 "encoding/json"
6 "path/filepath"
7 "regexp"
8 "strconv"
9 "strings"
10
11 "github.com/openvapt/openvapt/internal/check"
12 "github.com/openvapt/openvapt/internal/sys"
13)
14
15var (
16 refDockerSec = check.Ref{Title: "Docker documentation, Docker Engine security", URL: "https://docs.docker.com/engine/security/"}
17 refDockerSock = check.Ref{Title: "Docker documentation, Protect the Docker daemon socket", URL: "https://docs.docker.com/engine/security/protect-access/"}
18 refOWASPDock = check.Ref{Title: "OWASP Docker Security Cheat Sheet", URL: "https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html"}
19 refKubelet = check.Ref{Title: "Kubernetes documentation, Kubelet authentication and authorization", URL: "https://kubernetes.io/docs/reference/access-authn-authz/kubelet-authn-authz/"}
20 refK8sSec = check.Ref{Title: "Kubernetes documentation, Securing a cluster", URL: "https://kubernetes.io/docs/tasks/administer-cluster/securing-a-cluster/"}
21 refNIST190 = check.Ref{Title: "NIST SP 800-190, Application Container Security Guide", URL: "https://csrc.nist.gov/pubs/sp/800/190/final"}
22)
23
24// dockerDaemon is the subset of /etc/docker/daemon.json these checks read.
25type dockerDaemon struct {
26 Hosts []string `json:"hosts"`
27 TLSVerify bool `json:"tlsverify"`
28 NoNewPrivileges bool `json:"no-new-privileges"`
29 ICC *bool `json:"icc"`
30 UsernsRemap string `json:"userns-remap"`
31 LiveRestore bool `json:"live-restore"`
32}
33
34// hostConfig is the subset of a container's hostconfig.json these checks read.
35type hostConfig struct {
36 Privileged bool `json:"Privileged"`
37 PidMode string `json:"PidMode"`
38 NetworkMode string `json:"NetworkMode"`
39 IpcMode string `json:"IpcMode"`
40 CapAdd []string `json:"CapAdd"`
41 Binds []string `json:"Binds"`
42}
43
44// containerConfigs reads every container's hostconfig.json (needs root). The
45// container id is shortened to 12 characters in evidence, like docker ps.
46func containerConfigs(ctx context.Context, s sys.System) (map[string]hostConfig, bool) {
47 paths, _ := s.Glob("/var/lib/docker/containers/*/hostconfig.json")
48 out := map[string]hostConfig{}
49 complete := true
50 for _, p := range paths {
51 r, err := s.ReadFile(ctx, p)
52 if err != nil {
53 complete = false
54 continue
55 }
56 var hc hostConfig
57 if json.Unmarshal(r.Data, &hc) != nil || !r.Complete {
58 complete = false
59 continue
60 }
61 id := filepath.Base(filepath.Dir(p))
62 if len(id) > 12 {
63 id = id[:12]
64 }
65 out[id] = hc
66 }
67 return out, complete
68}
69
70func init() {
71 check.Register(&check.Check{
72 ID: "HOST-CTR-001", Title: "Docker API Exposed Over TCP Without TLS", Domain: check.DomainHost,
73 Severity: check.Critical, Category: check.CatNetwork, Effort: check.EffortLow,
74 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
75 Summary: "Checks whether the Docker daemon is configured to listen on a TCP socket without TLS client verification.",
76 Rationale: "The Docker API is equivalent to root on the host. Exposed over TCP without mutual TLS, anyone who can reach the port can start a privileged container and take over the machine; this is a common cause of cryptomining infections.",
77 Detection: "Reads /etc/docker/daemon.json (hosts and tlsverify) and Docker systemd unit files and drop-ins for -H tcp:// flags.",
78 Fix: "Remove tcp:// listeners and use the local Unix socket or SSH (docker context with ssh://). If remote access is unavoidable, enable tlsverify with client certificates and restrict the port in the firewall.",
79 Verify: "ss -tlnp | grep -E ':(2375|2376)' and grep -r 'tcp://' /etc/docker /etc/systemd/system/docker.service.d",
80 Refs: []check.Ref{refDockerSock, refOWASPDock},
81 Standards: check.Standards{CWE: []string{"CWE-306", "CWE-319"}, OWASP: []string{"A05:2021", "A07:2021"}, NIST: []string{"AC-3", "SC-8"}},
82 Run: func(ctx context.Context, s sys.System) []check.Result {
83 var tcp []string
84 tlsOn := false
85 found := false
86 if t, _, ok := readText(ctx, s, "/etc/docker/daemon.json"); ok {
87 found = true
88 var d dockerDaemon
89 if json.Unmarshal([]byte(t), &d) == nil {
90 tlsOn = d.TLSVerify
91 for _, h := range d.Hosts {
92 if strings.HasPrefix(h, "tcp://") {
93 tcp = append(tcp, "daemon.json hosts: "+h)
94 }
95 }
96 }
97 }
98 units := []string{"/lib/systemd/system/docker.service", "/usr/lib/systemd/system/docker.service"}
99 more, _ := s.Glob("/etc/systemd/system/docker.service.d/*.conf")
100 units = append(units, more...)
101 for _, u := range units {
102 t, _, ok := readText(ctx, s, u)
103 if !ok {
104 continue
105 }
106 found = true
107 for _, line := range strings.Split(t, "\n") {
108 if strings.Contains(line, "tcp://") && !strings.HasPrefix(strings.TrimSpace(line), "#") {
109 tcp = append(tcp, u+": "+strings.TrimSpace(line))
110 if strings.Contains(line, "--tlsverify") {
111 tlsOn = true
112 }
113 }
114 }
115 }
116 if !found {
117 return []check.Result{check.SkipR("docker", "Docker is not installed")}
118 }
119 if len(tcp) > 0 && !tlsOn {
120 return []check.Result{check.FailR("docker", "Docker API listens on TCP without TLS verification", tcp...)}
121 }
122 return []check.Result{check.PassR("docker", "Docker API is not exposed over unauthenticated TCP")}
123 },
124 })
125
126 check.Register(&check.Check{
127 ID: "HOST-CTR-002", Title: "Docker Daemon Not Hardened", Domain: check.DomainHost,
128 Severity: check.Medium, Category: check.CatConfig, Effort: check.EffortMedium,
129 CVSS: "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:L/I:L/A:L",
130 Summary: "Checks the Docker daemon defaults: no-new-privileges, inter-container communication, user namespace remapping and live restore.",
131 Rationale: "Hardened daemon defaults limit what a compromised container can do: no-new-privileges blocks setuid escalation, disabling icc isolates containers, and user namespaces map container root to an unprivileged host user.",
132 Detection: "Reads /etc/docker/daemon.json and lists each hardening setting that is missing: no-new-privileges true, icc false, userns-remap set, live-restore true.",
133 Fix: "Add to /etc/docker/daemon.json: \"no-new-privileges\": true, \"icc\": false, \"userns-remap\": \"default\", \"live-restore\": true then restart Docker after testing your containers.",
134 Verify: "cat /etc/docker/daemon.json ; docker info | grep -Ei 'security|userns'",
135 Refs: []check.Ref{refDockerSec, refNIST190},
136 Standards: check.Standards{CWE: []string{"CWE-250", "CWE-1188"}, NIST: []string{"CM-6", "AC-6"}, CISTopic: "Docker daemon configuration"},
137 Run: func(ctx context.Context, s sys.System) []check.Result {
138 t, complete, ok := readText(ctx, s, "/etc/docker/daemon.json")
139 if !ok {
140 if _, err := s.Stat("/var/lib/docker"); err != nil {
141 return []check.Result{check.SkipR("docker", "Docker is not installed")}
142 }
143 return []check.Result{check.FailR("/etc/docker/daemon.json", "No daemon.json: all hardening options are at their defaults")}
144 }
145 var d dockerDaemon
146 if json.Unmarshal([]byte(t), &d) != nil || !complete {
147 return []check.Result{check.IncompleteR("/etc/docker/daemon.json", "daemon.json could not be parsed")}
148 }
149 var miss []string
150 if !d.NoNewPrivileges {
151 miss = append(miss, "no-new-privileges is not true")
152 }
153 if d.ICC == nil || *d.ICC {
154 miss = append(miss, "icc is not false")
155 }
156 if d.UsernsRemap == "" {
157 miss = append(miss, "userns-remap is not set")
158 }
159 if !d.LiveRestore {
160 miss = append(miss, "live-restore is not true")
161 }
162 if len(miss) > 0 {
163 return []check.Result{check.FailR("/etc/docker/daemon.json", "Docker daemon hardening options are missing", miss...)}
164 }
165 return []check.Result{check.PassR("/etc/docker/daemon.json", "Docker daemon hardening options are set")}
166 },
167 })
168
169 check.Register(&check.Check{
170 ID: "HOST-CTR-003", Title: "Privileged or Host-Namespace Containers", Domain: check.DomainHost,
171 Severity: check.High, Category: check.CatConfig, Effort: check.EffortMedium,
172 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
173 Summary: "Looks for containers running privileged, sharing the host's PID, network or IPC namespace, or with dangerous added capabilities.",
174 Rationale: "A privileged container, or one with CAP_SYS_ADMIN or host namespaces, has almost no isolation: a compromise of the application inside is a compromise of the host.",
175 Detection: "Reads /var/lib/docker/containers/*/hostconfig.json (needs root) and reports Privileged true, PidMode/NetworkMode/IpcMode host, or CapAdd containing ALL, SYS_ADMIN, SYS_PTRACE, SYS_MODULE or NET_ADMIN.",
176 Fix: "Recreate each container without --privileged, --pid=host, --net=host and --ipc=host; drop all capabilities and add back only the specific ones needed (--cap-drop=ALL --cap-add=...).",
177 Verify: "docker inspect --format '{{.Name}} {{.HostConfig.Privileged}} {{.HostConfig.PidMode}} {{.HostConfig.NetworkMode}} {{.HostConfig.CapAdd}}' $(docker ps -aq)",
178 Refs: []check.Ref{refNIST190, refOWASPDock},
179 Standards: check.Standards{CWE: []string{"CWE-250", "CWE-653"}, NIST: []string{"AC-6", "SC-39"}},
180 Run: func(ctx context.Context, s sys.System) []check.Result {
181 cfgs, complete := containerConfigs(ctx, s)
182 if len(cfgs) == 0 {
183 if !complete {
184 return []check.Result{check.IncompleteR("containers", "Container configuration could not be read (run as root)")}
185 }
186 return []check.Result{check.SkipR("containers", "No Docker containers found")}
187 }
188 danger := map[string]bool{"ALL": true, "SYS_ADMIN": true, "SYS_PTRACE": true, "SYS_MODULE": true, "NET_ADMIN": true, "CAP_SYS_ADMIN": true}
189 var bad []string
190 for id, hc := range cfgs {
191 var why []string
192 if hc.Privileged {
193 why = append(why, "privileged")
194 }
195 for name, mode := range map[string]string{"pid": hc.PidMode, "network": hc.NetworkMode, "ipc": hc.IpcMode} {
196 if mode == "host" {
197 why = append(why, name+"=host")
198 }
199 }
200 for _, c := range hc.CapAdd {
201 if danger[strings.ToUpper(c)] {
202 why = append(why, "cap-add "+c)
203 }
204 }
205 if len(why) > 0 {
206 bad = append(bad, id+": "+strings.Join(why, ", "))
207 }
208 }
209 if len(bad) > 0 {
210 return []check.Result{check.FailR("containers", "Containers with weak isolation", bad...)}
211 }
212 if !complete {
213 return []check.Result{check.IncompleteR("containers", "Some container configurations could not be read")}
214 }
215 return []check.Result{check.PassR("containers", strconv.Itoa(len(cfgs))+" container(s) checked; none privileged or sharing host namespaces")}
216 },
217 })
218
219 check.Register(&check.Check{
220 ID: "HOST-CTR-004", Title: "Containers Mount the Docker Socket or Host Root", Domain: check.DomainHost,
221 Severity: check.High, Category: check.CatAccess, Effort: check.EffortMedium,
222 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
223 Summary: "Looks for containers that bind-mount the Docker socket, the host root filesystem or sensitive host paths.",
224 Rationale: "A container with /var/run/docker.sock can control Docker and therefore the host. Mounting / or /etc gives direct write access to host configuration.",
225 Detection: "Reads Binds from /var/lib/docker/containers/*/hostconfig.json (needs root) and reports mounts of /var/run/docker.sock, /run/docker.sock, /, /etc, /root, /proc or /sys from the host.",
226 Fix: "Remove these mounts. If a tool genuinely needs the Docker API, use a restricted socket proxy that allows only the required read-only calls.",
227 Verify: "docker inspect --format '{{.Name}} {{.HostConfig.Binds}}' $(docker ps -aq)",
228 Refs: []check.Ref{refDockerSock, refNIST190},
229 Standards: check.Standards{CWE: []string{"CWE-250", "CWE-732"}, NIST: []string{"AC-6", "SC-39"}},
230 Run: func(ctx context.Context, s sys.System) []check.Result {
231 cfgs, complete := containerConfigs(ctx, s)
232 if len(cfgs) == 0 {
233 if !complete {
234 return []check.Result{check.IncompleteR("containers", "Container configuration could not be read (run as root)")}
235 }
236 return []check.Result{check.SkipR("containers", "No Docker containers found")}
237 }
238 sensitive := map[string]bool{"/var/run/docker.sock": true, "/run/docker.sock": true, "/": true, "/etc": true, "/root": true, "/proc": true, "/sys": true}
239 var bad []string
240 for id, hc := range cfgs {
241 for _, b := range hc.Binds {
242 src := strings.SplitN(b, ":", 2)[0]
243 if sensitive[filepath.Clean(src)] {
244 bad = append(bad, id+": "+b)
245 }
246 }
247 }
248 if len(bad) > 0 {
249 return []check.Result{check.FailR("containers", "Containers mount the Docker socket or sensitive host paths", bad...)}
250 }
251 return []check.Result{check.PassR("containers", "No containers mount the Docker socket or host root")}
252 },
253 })
254
255 check.Register(&check.Check{
256 ID: "HOST-K8S-001", Title: "Kubelet Allows Anonymous or Unauthorised Access", Domain: check.DomainHost,
257 Severity: check.Critical, Category: check.CatIdentity, Effort: check.EffortMedium,
258 CVSS: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
259 Summary: "Checks the kubelet configuration for anonymous authentication, AlwaysAllow authorisation or the unauthenticated read-only port.",
260 Rationale: "The kubelet API can run commands in every pod on the node. With anonymous access or AlwaysAllow, anyone who can reach port 10250 controls the node's workloads; the read-only port 10255 leaks pod and environment details.",
261 Detection: "Reads /var/lib/kubelet/config.yaml and looks for authentication.anonymous.enabled: true, authorization.mode: AlwaysAllow, or a non-zero readOnlyPort.",
262 Fix: "Set authentication.anonymous.enabled: false, authorization.mode: Webhook and readOnlyPort: 0 in the kubelet configuration, then restart the kubelet.",
263 Verify: "grep -A2 -E 'anonymous|authorization|readOnlyPort' /var/lib/kubelet/config.yaml",
264 Refs: []check.Ref{refKubelet, refK8sSec},
265 Standards: check.Standards{CWE: []string{"CWE-306", "CWE-862"}, OWASP: []string{"A01:2021", "A07:2021"}, NIST: []string{"AC-3", "IA-2"}},
266 Run: func(ctx context.Context, s sys.System) []check.Result {
267 t, _, ok := readText(ctx, s, "/var/lib/kubelet/config.yaml")
268 if !ok {
269 return []check.Result{check.SkipR("kubelet", "No kubelet configuration found")}
270 }
271 var bad []string
272 if regexp.MustCompile(`(?m)anonymous:\s*\n\s+enabled:\s*true`).MatchString(t) {
273 bad = append(bad, "authentication.anonymous.enabled: true")
274 }
275 if regexp.MustCompile(`(?m)^\s*mode:\s*AlwaysAllow`).MatchString(t) {
276 bad = append(bad, "authorization.mode: AlwaysAllow")
277 }
278 if m := regexp.MustCompile(`(?m)^\s*readOnlyPort:\s*(\d+)`).FindStringSubmatch(t); m != nil && m[1] != "0" {
279 bad = append(bad, "readOnlyPort: "+m[1])
280 }
281 if len(bad) > 0 {
282 return []check.Result{check.FailR("/var/lib/kubelet/config.yaml", "Kubelet allows unauthenticated or unauthorised access", bad...)}
283 }
284 return []check.Result{check.PassR("/var/lib/kubelet/config.yaml", "Kubelet requires authentication and authorisation")}
285 },
286 })
287
288 check.Register(&check.Check{
289 ID: "HOST-K8S-002", Title: "Kubernetes Data Directory Accessible to Others", Domain: check.DomainHost,
290 Severity: check.High, Category: check.CatAccess, Effort: check.EffortLow,
291 CVSS: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N",
292 Summary: "Checks that the etcd data directory is accessible only by its owner.",
293 Rationale: "etcd stores every Kubernetes object, including Secrets, usually unencrypted at rest. Read access to its data directory exposes all cluster credentials.",
294 Detection: "Stats /var/lib/etcd and fails if group or others have any permission.",
295 Fix: "chmod 700 /var/lib/etcd && chown etcd:etcd /var/lib/etcd (or root:root for kubeadm static pods). Enable encryption at rest for Secrets.",
296 Verify: "stat -c '%a %U:%G %n' /var/lib/etcd",
297 Refs: []check.Ref{{Title: "Kubernetes documentation, Operating etcd clusters for Kubernetes", URL: "https://kubernetes.io/docs/tasks/administer-cluster/configure-upgrade-etcd/"}, refK8sSec},
298 Standards: check.Standards{CWE: []string{"CWE-732"}, NIST: []string{"AC-3", "SC-28"}},
299 Run: func(ctx context.Context, s sys.System) []check.Result {
300 st, err := s.Stat("/var/lib/etcd")
301 if err != nil {
302 return []check.Result{check.SkipR("/var/lib/etcd", "No etcd data directory found")}
303 }
304 if st.Mode.Perm()&0o077 != 0 {
305 return []check.Result{check.FailR("/var/lib/etcd", "etcd data directory is accessible by group or others", "mode "+perm(st.Mode))}
306 }
307 return []check.Result{check.PassR("/var/lib/etcd", "etcd data directory is owner-only", "mode "+perm(st.Mode))}
308 },
309 })
310
311 registerFileMode(fileModeRule{
312 id: "HOST-K8S-003", title: "Kubernetes Credential Files Readable by Others", sev: check.High, cat: check.CatIdentity,
313 cvss: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
314 summary: "Checks that kubeconfig files and Kubernetes PKI private keys are not readable by other users.",
315 rationale: "admin.conf and the cluster CA key grant full control of the cluster; any local user who can read them becomes a cluster administrator.",
316 fix: "chmod 600 /etc/kubernetes/*.conf /etc/kubernetes/pki/*.key /etc/kubernetes/pki/etcd/*.key && chown root:root on each.",
317 verify: "stat -c '%a %n' /etc/kubernetes/*.conf /etc/kubernetes/pki/*.key",
318 patterns: []string{"/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"},
319 bad: 0o044 | 0o022, why: "readable or writable by group or others", cwe: "CWE-732",
320 refs: []check.Ref{refK8sSec},
321 })
322}

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-K8S-001 Kubelet Allows Anonymous or Unauthorised Access (Version 0.1.0-alpha.6). OpenVAPT Check Catalogue. Retrieved October 16, 2026, from https://openvapt.com/checks/HOST-K8S-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_k8s_001,
  author       = {{OpenVAPT}},
  title        = {{HOST-K8S-001 Kubelet Allows Anonymous or Unauthorised Access}},
  year         = {2026},
  howpublished = {\url{https://openvapt.com/checks/HOST-K8S-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.