KCSA — Kubelet & Container Runtime Security
The Kubelet — Node Agent
The kubelet is the agent that runs on every worker node (and often on control plane nodes too). It’s responsible for:
- Receiving Pod specifications from the API server
- Managing the Pod lifecycle — starting, stopping, and monitoring containers
- Reporting node and Pod status back to the API server
- Mounting volumes, managing secrets, and handling health checks
The kubelet has direct access to the container runtime and the host operating system. A compromised kubelet means a compromised node — and potentially all the Pods running on it.
Kubelet Authentication
Like the API server, the kubelet exposes an HTTPS API (on port 10250) that accepts requests. By default, this API can be dangerously permissive if not configured correctly.
Authentication Modes
| Mode | Flag | Behavior | Security Level |
|---|---|---|---|
| Anonymous | --anonymous-auth=true |
Accepts requests without any credentials. Anyone who can reach port 10250 can query or command the kubelet | ❌ Dangerous — disable immediately |
| Webhook | --authentication-token-webhook=true |
Forwards bearer tokens to the API server for validation | ✅ Recommended — integrates with cluster identity |
| Client Certificate | --client-ca-file=/path/to/ca.crt |
Requires client certificate signed by the specified CA | ✅ Strong — used for API server → kubelet communication |
Always disable anonymous authentication on the kubelet:
--anonymous-auth=false
Anonymous kubelet access has been exploited in real-world attacks. An attacker on the pod network could run arbitrary commands inside containers via the kubelet’s /exec endpoint.
Kubelet Authorization
Even after authentication, the kubelet needs to decide whether the authenticated identity is allowed to perform the requested action.
Authorization Modes
| Mode | Flag | Behavior | Security Level |
|---|---|---|---|
| AlwaysAllow | --authorization-mode=AlwaysAllow |
Every authenticated request is allowed | ❌ Dangerous — no access control at all |
| Webhook | --authorization-mode=Webhook |
Delegates authorization decisions to the API server’s authorization layer (RBAC) | ✅ Recommended — enforces the same RBAC rules used for the API server |
The recommended configuration for production:
--authorization-mode=Webhook
--anonymous-auth=false
--authentication-token-webhook=true
With AlwaysAllow, anyone who can authenticate to the kubelet can execute commands in any container on that node. With Webhook, the API server’s RBAC rules decide what’s permitted.
Kubelet Ports
The kubelet listens on two ports. Understanding the difference is critical for the exam.
| Port | Purpose | Authentication | Risk |
|---|---|---|---|
| 10250 | Kubelet’s main HTTPS API — serves metrics, exec, logs, port-forward | Configurable (can require certs or tokens) | Secure when properly configured |
| 10255 | Read-only HTTP port — serves metrics and Pod information without any authentication | ❌ None — completely unauthenticated | High risk — exposes Pod specs, environment variables (which may contain secrets) |
Disable the Read-Only Port
--read-only-port=0
The read-only port (10255) was designed for monitoring tools but provides zero authentication. Anyone on the network can query it to enumerate Pods, read environment variables, and gather intelligence about running workloads.
Always disable it in production. Modern monitoring tools (Prometheus, metrics-server) should communicate via the authenticated port (10250).
Container Runtime Interface (CRI) Security
The kubelet doesn’t manage containers directly — it delegates to a container runtime through the Container Runtime Interface (CRI).
CRI Architecture
┌───────────┐ CRI gRPC ┌────────────────────┐ OCI Runtime
│ kubelet │ ──────────────▶ │ CRI Runtime │ ──────────────▶ Container
│ │ │ (containerd/CRI-O) │
└───────────┘ └────────────────────┘
The CRI runtime is responsible for:
- Pulling container images
- Creating and managing containers
- Managing container networking (via CNI plugins)
- Enforcing resource limits (cgroups)
CRI Socket Security
The CRI runtime listens on a Unix socket (e.g., /run/containerd/containerd.sock). Access to this socket grants the ability to create, modify, and delete containers on the node.
| Practice | Description |
|---|---|
| Restrict socket permissions | Only the kubelet user should have access to the CRI socket |
| No Pod access to socket | Never mount the CRI socket into a Pod — this grants the Pod full container management capability on the node |
| Monitor socket access | Audit access to the CRI socket for unauthorized use |
Mounting the container runtime socket into a Pod (e.g., for CI/CD build tools) is a critical security anti-pattern. It effectively gives the Pod root access to the node.
Container Runtime Comparison
Kubernetes supports multiple container runtimes. The KCSA exam expects you to know the differences, especially regarding security isolation.
| Runtime | Type | Isolation | Overhead | Use Case |
|---|---|---|---|---|
| containerd | Standard OCI runtime | Linux namespaces + cgroups (shared kernel) | Low | Default runtime for most Kubernetes distributions. Suitable for trusted workloads |
| CRI-O | Lightweight CRI-only runtime | Linux namespaces + cgroups (shared kernel) | Low | Optimized for Kubernetes. Used by OpenShift. Similar security model to containerd |
| gVisor (runsc) | Sandboxed runtime | Application-level kernel in userspace. Containers don’t share the host kernel directly | Medium | Multi-tenant environments where workloads are untrusted. Intercepts system calls via a user-space kernel |
| Kata Containers | Micro-VM based runtime | Each container runs in a lightweight virtual machine with its own kernel | Higher | Strongest isolation. Each Pod gets its own VM. Ideal for running untrusted or third-party code |
Standard vs. Sandboxed Runtimes
Standard Runtime (containerd/CRI-O):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Container │ │ Container │ │ Container │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
└──────────────┼──────────────┘
│
┌──────┴──────┐
│ Host Kernel │
└─────────────┘
Sandboxed Runtime (gVisor):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Container │ │ Container │ │ Container │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
┌─────┴────┐ ┌─────┴────┐ ┌─────┴────┐
│ Sentry │ │ Sentry │ │ Sentry │
│ (user- │ │ (user- │ │ (user- │
│ space │ │ space │ │ space │
│ kernel) │ │ kernel) │ │ kernel) │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
└──────────────┼──────────────┘
│
┌──────┴──────┐
│ Host Kernel │
└─────────────┘
Micro-VM Runtime (Kata Containers):
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Container │ │ Container │ │ Container │
│ + Guest │ │ + Guest │ │ + Guest │
│ Kernel │ │ Kernel │ │ Kernel │
│ + Mini VM │ │ + Mini VM │ │ + Mini VM │
└─────┬────┘ └─────┬────┘ └─────┬────┘
│ │ │
└──────────────┼──────────────┘
│
┌──────┴──────┐
│ Host Kernel │
│ + Hypervisor│
└─────────────┘
For the KCSA exam: gVisor provides a user-space kernel that intercepts syscalls. Kata Containers provides full VM-level isolation. Both are used when workloads cannot be trusted.
Using RuntimeClasses
Kubernetes lets you select the container runtime per Pod using RuntimeClass:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
apiVersion: v1
kind: Pod
metadata:
name: untrusted-workload
spec:
runtimeClassName: gvisor
containers:
- name: app
image: nginx
This allows you to run trusted workloads on the standard runtime and untrusted workloads in a sandbox — on the same cluster.
NodeRestriction Admission Controller
The NodeRestriction admission controller limits what kubelet can do via the API server. Without it, a compromised kubelet could modify resources that belong to other nodes.
What NodeRestriction Enforces
| Action | Allowed? |
|---|---|
| Kubelet modifying its own Node object’s labels (non-protected) | ✅ Yes |
| Kubelet modifying its own Node object’s status | ✅ Yes |
| Kubelet modifying Pod status for Pods scheduled on its node | ✅ Yes |
| Kubelet modifying another Node’s labels or status | ❌ No |
| Kubelet modifying Pods on another node | ❌ No |
Kubelet adding labels under node-restriction.kubernetes.io/ prefix |
❌ No |
Enable it by adding NodeRestriction to --enable-admission-plugins on the API server. It works in conjunction with the Node authorization mode.
Without NodeRestriction, a compromised node could manipulate scheduling, impersonate other nodes, or interfere with Pods across the cluster.
Key Takeaways for the Exam
| Concept | What to Remember |
|---|---|
| Kubelet authentication | Disable anonymous auth (--anonymous-auth=false). Use webhook or client cert authentication |
| Kubelet authorization | Use Webhook mode — never AlwaysAllow. Delegates to API server RBAC |
| Port 10250 vs 10255 | 10250 = authenticated API. 10255 = unauthenticated read-only. Disable 10255 (--read-only-port=0) |
| CRI socket | Never mount the container runtime socket into a Pod |
| containerd / CRI-O | Standard runtimes. Shared kernel. Low overhead. Good for trusted workloads |
| gVisor | User-space kernel. Intercepts syscalls. Medium overhead. For untrusted workloads |
| Kata Containers | Lightweight VM per Pod. Strongest isolation. Higher overhead. For highly untrusted workloads |
| RuntimeClass | Kubernetes resource that lets you select the runtime per Pod |
| NodeRestriction | Admission controller that prevents kubelet from modifying resources on other nodes |
Node-level security is often overlooked — but a compromised node means compromised workloads. Lock down the kubelet and choose the right runtime for your trust model.