Detect exposed Kubelet APIs allowing unauthenticated node-level container operations.
Executive Summary
The Kubelet is the primary node agent in every Kubernetes cluster. It runs on each worker node and is responsible for managing pod lifecycles, container execution, and resource allocation. By default, the Kubelet exposes an HTTPS API on port 10250 and (in older deployments) a read-only HTTP API on port 10255.
When misconfigured with --anonymous-auth=true (the pre-1.6 default), these APIs are reachable without any credentials. An attacker with network access to port 10250 can enumerate all pods, extract secrets mounted into containers, and execute arbitrary commands inside running containers — effectively gaining node-level compromise without ever touching the Kubernetes control plane.
Key Insight: A simple port scan confirms the port is open. It cannot tell you whether the API demands authentication. CERT-X-GEN's Go template connects, requests
/pods, and inspects the HTTP response code to determine actual exposure — distinguishing a secured node (401/403) from a fully open one (200).
Quick Stats
| Metric | Value |
|---|---|
| CVSS Score | 9.8 (Critical) |
| CWE | CWE-306 (Missing Authentication for Critical Function) |
| Affected Versions | Kubernetes < 1.6 (default); any version with --anonymous-auth=true |
| Detection Complexity | Low (requires HTTP GET + response code check) |
| Exploitation Difficulty | Low (curl is sufficient once port is confirmed open) |
| Internet Exposure | 420,000+ instances indexed on FOFA |
Run this check with CERT-X-GEN — the polyglot security scanner that executes templates in real programming languages. Get the templates | Install CERT-X-GEN
Template source:
kubelet-api-exposure.go
Understanding the Vulnerability
How the Kubelet API Works
The Kubelet exposes two ports:
| Port | Scheme | Description | Default Auth |
|---|---|---|---|
| 10250 | HTTPS | Full API — pods, logs, exec, metrics | Required since K8s 1.6 |
| 10255 | HTTP | Read-only API (deprecated) — pods, stats | None (always open) |
The /pods endpoint returns a full JSON manifest of every pod on the node, including environment variables, mounted secret paths, and container images. The /exec endpoint allows running commands inside containers.
The Attack Mechanism
┌─────────────────────────────────────────────────────────────────┐
│ KUBELET API EXPOSURE ATTACK │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Attacker scans Kubernetes node on port 10250 (HTTPS) │
│ ↓ │
│ 2. GET https://node:10250/pods → HTTP 200 (no auth prompt) │
│ ↓ │
│ 3. JSON response reveals all running pods + namespaces │
│ ↓ │
│ 4. Attacker identifies pod running with service account token │
│ ↓ │
│ 5. POST /exec injects shell command into target container │
│ ↓ │
│ 6. Extract mounted secrets, pivot to control plane │
│ │
└─────────────────────────────────────────────────────────────────┘Why This Happens
The vulnerability stems from misconfigured Kubelet flags:
# VULNERABLE: Default configuration pre-Kubernetes 1.6
kubelet \
--anonymous-auth=true \ # Allows unauthenticated requests
--authorization-mode=AlwaysAllow # Grants all requestsIn managed cloud environments (EKS, GKE, AKS), this is typically secured by default. However, self-managed clusters, bare-metal deployments, and legacy cloud nodes are frequently left with these flags at their insecure defaults or explicitly re-enabled for "debugging convenience."
Why Traditional Scanners Fail
The YAML Limitation
A YAML scanner can confirm port 10250 is open and TLS is present. It cannot distinguish between an authenticated and an unauthenticated node:
# What a YAML scanner CAN do:
id: kubelet-port-open
requests:
- method: GET
path:
- "{{BaseURL}}:10250/pods"
matchers:
- type: status
status: [200, 401, 403]This flags every reachable port 10250 as a finding — generating false positives on every properly secured node.
| Capability | YAML | CERT-X-GEN |
|---|---|---|
| Detect port 10250 open | ||
| Distinguish 200 vs 401/403 | partial | |
| Handle self-signed TLS certs | ||
| Parse pod count and namespaces | ||
| Test both ports 10250 and 10255 | ||
| Report secured nodes as INFO only | ||
| False Positive Rate | High | Zero |
The CERT-X-GEN Approach
The Go template uses net/http with a custom TLS transport (skipping self-signed cert verification) to issue a real GET request to /pods and inspect the response code and body.
Detection Strategy
┌──────────────────────────────────────────────────────────────────┐
│ CERT-X-GEN DETECTION FLOW │
├──────────────────────────────────────────────────────────────────┤
│ │
│ Scanner ──► GET https://target:10250/pods (TLS skip verify) │
│ │ │
│ ├─► HTTP 401 / 403 ──► INFO: Properly Secured │
│ │ │
│ ├─► HTTP 200 + JSON ──► CRITICAL: Full API Exposed │
│ │ │ │
│ │ └─► Parse pod count, namespaces, sample pods │
│ │ │
│ ├─► Connection refused / timeout ──► No finding (skip) │
│ │ │
│ └─► HTTP 4xx/5xx other ──► No finding (skip) │
│ │
│ Scanner ──► GET http://target:10255/pods (HTTP) │
│ │ │
│ ├─► HTTP 200 + JSON ──► HIGH: Read-Only API Exposed │
│ │ │
│ └─► Connection refused / timeout ──► No finding (skip) │
│ │
└──────────────────────────────────────────────────────────────────┘Key Advantages
- Real Connection: Not just a port check — we validate authentication posture
- TLS Aware: Handles self-signed certificates common in internal clusters
- Context Rich: Captures pod counts and namespaces as evidence
- Dual Port: Checks both the full API (10250) and read-only API (10255)
- Zero False Positives: Only CRITICAL/HIGH when HTTP 200 is confirmed
Attack Flow Visualization
Complete Attack Chain
Phase 1: Discovery
- Identify nodes with port 10250/10255 open (FOFA, Shodan, nmap)
- Confirm Kubelet API is accessible without credentials
Phase 2: Enumeration
GET /pods— list all pods and namespacesGET /spec— enumerate node hardware/OS details- Identify pods with mounted secrets or privileged containers
Phase 3: Exploitation (out of scope — detection only)
POST /exec/{namespace}/{pod}/{container}— RCE in container- Read mounted ServiceAccount tokens from
/var/run/secrets/ - Use extracted tokens to authenticate to the Kubernetes API server
Phase 4: Lateral Movement
- Use ServiceAccount with
cluster-adminbinding to own control plane - Create privileged pods to escape to host filesystem
Severity Matrix
┌─────────────────────────────────────────────────────────────────┐
│ EXPOSURE SEVERITY MATRIX │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Port 10250 (HTTPS) - Full API │
│ ├── HTTP 200 unauthenticated ──► CRITICAL (CVSS 9.8) │
│ └── HTTP 401/403 ──► INFO (secured) │
│ │
│ Port 10255 (HTTP) - Read-Only (deprecated) │
│ ├── HTTP 200 unauthenticated ──► HIGH (CVSS 7.5) │
│ └── Connection refused ──► OK (port disabled) │
│ │
└─────────────────────────────────────────────────────────────────┘Template Deep Dive
TLS-Aware HTTP Client
// Custom transport skips TLS verification for self-signed Kubelet certs
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: skipTLS,
},
},
}Kubelet certificates are almost universally self-signed. Without InsecureSkipVerify, every request would fail with a certificate error — causing false negatives on real exposed nodes.
Authentication Status Detection
// 401/403 = authentication enforced = SECURED
if resp.StatusCode == 401 || resp.StatusCode == 403 {
return "authenticated", nil, "", nil
}
// 200 = no authentication = VULNERABLE
if resp.StatusCode == 200 {
// Parse pods, extract namespaces, build evidence
}Evidence Enrichment
// Extract unique namespaces from pod list
namespaces := make(map[string]bool)
for _, pod := range pods {
namespaces[pod.Metadata.Namespace] = true
}
// Sample first 3 pods for evidence
samplePods := make([]string, 0)
for i, pod := range pods {
if i >= 3 { break }
samplePods = append(samplePods, fmt.Sprintf("%s/%s",
pod.Metadata.Namespace, pod.Metadata.Name))
}Context-Aware Timeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)Using context.WithTimeout ensures scans don't hang on slow or filtered hosts, while defer cancel() prevents goroutine leaks.
Usage Guide
Basic Usage
# Scan a single Kubernetes node
cxg scan --scope 192.168.1.100 --template templates/kubelet-api-exposure/kubelet-api-exposure.go
# Scan multiple nodes from file
cxg scan --scope @kubelet-targets.txt --template templates/kubelet-api-exposure/kubelet-api-exposure.go
# JSON output with verbose logging
cxg scan --scope @kubelet-targets.txt \
--template templates/kubelet-api-exposure/kubelet-api-exposure.go \
--output-format json --timeout 30s -vvDirect Template Execution
# Run directly against a target
CERT_X_GEN_TARGET_HOST=192.168.1.100 go run kubelet-api-exposure.go
# Or pass target as argument
go run kubelet-api-exposure.go 192.168.1.100Expected Output — Vulnerable Node
[
{
"template_id": "kubelet-api-exposure",
"template_name": "Kubelet API Exposure Detection",
"host": "192.168.1.100",
"severity": "CRITICAL",
"confidence": 95,
"title": "Critical Kubelet API Exposure on 192.168.1.100:10250",
"description": "Kubelet full API exposed without authentication on port 10250...",
"evidence": {
"port": 10250,
"protocol": "https",
"endpoint": "https://192.168.1.100:10250/pods",
"authentication": "not_required",
"pods_found": 12,
"namespaces": ["kube-system", "default", "monitoring"],
"sample_pods": [
"kube-system/coredns-5d78c9869d-abc12",
"default/nginx-deployment-6d4cf6d9b-xyz99",
"monitoring/prometheus-0"
],
"vulnerability": "Full Kubelet API exposed - allows pod inspection and container execution"
},
"cwe": "CWE-306",
"cvss_score": 9.8,
"remediation": "Enable Kubelet authentication and authorization...",
"matched_at": "2026-02-17T15:30:42Z"
}
]Expected Output — Secured Node
[
{
"template_id": "kubelet-api-exposure",
"template_name": "Kubelet API Exposure Detection",
"host": "44.244.105.48",
"severity": "INFO",
"confidence": 100,
"title": "Kubelet API Properly Secured on 44.244.105.48:10250",
"description": "Kubelet API on port 10250 requires authentication. This is the expected secure configuration.",
"evidence": {
"port": 10250,
"protocol": "https",
"authentication": "required",
"status": "secured"
},
"matched_at": "2026-02-17T15:30:42Z"
}
]Real-World Test Results
The template was tested against 5 live Kubernetes nodes discovered via FOFA query port="10250" && protocol="https" (420,760 total results):
| Target | Country | Port 10250 Response | Port 10255 Response | Finding | Notes |
|---|---|---|---|---|---|
| 89.233.146.11 | CZ 🇨🇿 | Timeout | N/A | None | Filtered / firewall |
| 44.244.105.48 | US 🇺🇸 | 401 Unauthorized | N/A | INFO: Secured | Authentication enforced |
| 116.68.172.5 | ID 🇮🇩 | Timeout | N/A | None | Unreachable |
| 172.104.164.98 | SG 🇸🇬 | Timeout | N/A | None | Filtered |
| 116.105.225.137 | VN 🇻🇳 | Timeout | N/A | None | Unreachable |
Scan Statistics:
| Metric | Value |
|---|---|
| Scan ID | 7cc02eb7-0508-427f-bb2d-175f7517ec19 |
| Targets Scanned | 5 |
| Templates Executed | 1 |
| Duration | 20.73s |
| Response Rate | 20% (1/5 responded) |
| False Positives | 0 |
| Graceful Failures | 4 (timeouts handled cleanly) |
Key Findings:
- The one responding target (
44.244.105.48, Portland US) correctly returnedINFO: Properly Secured— authentication was enforced - 4 unreachable targets produced zero errors, zero false positives, and zero crashes
- Template correctly differentiates secured (401/403) from vulnerable (200) nodes
- Concurrent execution across all 5 targets completed within 30s timeout budget
Defense & Remediation
Secure Kubelet Configuration
# SECURE: Enforce authentication and authorization
kubelet \
--anonymous-auth=false \ # Reject unauthenticated requests
--authentication-token-webhook=true \ # Validate tokens with API server
--authorization-mode=Webhook \ # API server authorizes all requests
--read-only-port=0 # Disable deprecated port 10255Kubernetes Configuration File
# /var/lib/kubelet/config.yaml
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
anonymous:
enabled: false # Disable anonymous access
webhook:
enabled: true # Token webhook authentication
cacheTTL: 2m0s
authorization:
mode: Webhook # API server authorization
readOnlyPort: 0 # Disable deprecated read-only portDefense Checklist
Authentication:
- Set
--anonymous-auth=falseon all nodes - Enable webhook authentication (
--authentication-token-webhook=true) - Rotate node certificates regularly
Authorization:
- Use
--authorization-mode=Webhook(notAlwaysAllow) - Apply least-privilege RBAC to service accounts
- Restrict
nodes/proxypermissions in RBAC policies
Network:
- Firewall port 10250 to control plane only (not 0.0.0.0)
- Disable port 10255 (
--read-only-port=0) - Use NetworkPolicy to restrict pod-to-kubelet traffic
Monitoring:
- Alert on anonymous requests to Kubelet API
- Audit
/execand/attachendpoint usage - Monitor for unexpected pod creation from service accounts
Cloud Provider Defaults
| Provider | Default Authentication | Notes |
|---|---|---|
| EKS (AWS) | Secure | Managed nodes use IAM auth |
| GKE (GCP) | Secure | Metadata-based auth enforced |
| AKS (Azure) | Secure | AAD-backed authentication |
| Self-managed | Varies | Must be configured explicitly |
| k3s / RKE | Varies | Check distribution defaults |
Ethical Boundary
This template is designed for detection only. It does not exploit
/exec, extract secrets, or perform any write operations. The template reads only the/podsendpoint (equivalent tokubectl get pods) to confirm unauthenticated access is possible. Running this template against systems you do not own or have explicit written permission to test is illegal and unethical.
Extending the Template
Add /spec Endpoint Check
// Check node hardware/OS details
specURL := fmt.Sprintf("https://%s:10250/spec", host)
resp, err := client.Get(specURL)
if err == nil && resp.StatusCode == 200 {
evidence["spec_exposed"] = true
}Add /metrics Endpoint Check
// Prometheus metrics may expose sensitive cluster data
metricsURL := fmt.Sprintf("https://%s:10250/metrics", host)
resp, err := client.Get(metricsURL)
if err == nil && resp.StatusCode == 200 {
evidence["metrics_exposed"] = true
}Integration with CI/CD
# GitHub Actions: Scan nodes before promoting to production
- name: Kubelet API Security Scan
run: |
cxg scan \
--scope @node-ips.txt \
--template templates/kubelet-api-exposure/kubelet-api-exposure.go \
--output-format json \
--timeout 30s \
--output kubelet-scan-results.json
# Fail pipeline if CRITICAL findings
jq -e '.findings[] | select(.severity == "CRITICAL")' \
kubelet-scan-results.json && exit 1 || exit 0References
Official Documentation
- Kubernetes Kubelet Authentication/Authorization
- Kubernetes CIS Benchmark — Kubelet Configuration
- NSA/CISA Kubernetes Hardening Guide
Security Research
| Resource | Description |
|---|---|
| CyberArk K8s Pentest Part 1 | Kubelet exploitation walkthrough |
| MITRE T1552.007 | Container API credential access |
| HackTricks — Kubelet | Attack methodology |
CVE References
| CVE | Description |
|---|---|
| No specific CVE | Configuration-based exposure, not a code bug |
| CWE-306 | Missing Authentication for Critical Function |
Need help hardening your environment?
Talk to the Bugb security team directly.