NEWBravos is availableCreate your account
The Knowledge Base
11 min read

Kubelet API Exposure: Node-Level Container Access Detection

Infrastructure SecurityBy BugB Security Team

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

MetricValue
CVSS Score9.8 (Critical)
CWECWE-306 (Missing Authentication for Critical Function)
Affected VersionsKubernetes < 1.6 (default); any version with --anonymous-auth=true
Detection ComplexityLow (requires HTTP GET + response code check)
Exploitation DifficultyLow (curl is sufficient once port is confirmed open)
Internet Exposure420,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:

PortSchemeDescriptionDefault Auth
10250HTTPSFull API — pods, logs, exec, metricsRequired since K8s 1.6
10255HTTPRead-only API (deprecated) — pods, statsNone (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:

bash
# VULNERABLE: Default configuration pre-Kubernetes 1.6
kubelet \
 --anonymous-auth=true \ # Allows unauthenticated requests
 --authorization-mode=AlwaysAllow # Grants all requests

In 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:

yaml
# 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.

CapabilityYAMLCERT-X-GEN
Detect port 10250 open
Distinguish 200 vs 401/403partial
Handle self-signed TLS certs
Parse pod count and namespaces
Test both ports 10250 and 10255
Report secured nodes as INFO only
False Positive RateHighZero

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

  1. Real Connection: Not just a port check — we validate authentication posture
  2. TLS Aware: Handles self-signed certificates common in internal clusters
  3. Context Rich: Captures pod counts and namespaces as evidence
  4. Dual Port: Checks both the full API (10250) and read-only API (10255)
  5. 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 namespaces
  • GET /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-admin binding 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

go
// 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

go
// 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

go
// 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

go
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

bash
# 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 -vv

Direct Template Execution

bash
# 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.100

Expected Output — Vulnerable Node

json
[
 {
 "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

json
[
 {
 "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):

TargetCountryPort 10250 ResponsePort 10255 ResponseFindingNotes
89.233.146.11CZ 🇨🇿TimeoutN/ANoneFiltered / firewall
44.244.105.48US 🇺🇸401 UnauthorizedN/AINFO: SecuredAuthentication enforced
116.68.172.5ID 🇮🇩TimeoutN/ANoneUnreachable
172.104.164.98SG 🇸🇬TimeoutN/ANoneFiltered
116.105.225.137VN 🇻🇳TimeoutN/ANoneUnreachable

Scan Statistics:

MetricValue
Scan ID7cc02eb7-0508-427f-bb2d-175f7517ec19
Targets Scanned5
Templates Executed1
Duration20.73s
Response Rate20% (1/5 responded)
False Positives0
Graceful Failures4 (timeouts handled cleanly)

Key Findings:

  • The one responding target (44.244.105.48, Portland US) correctly returned INFO: 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

bash
# 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 10255

Kubernetes Configuration File

yaml
# /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 port

Defense Checklist

Authentication:

  • Set --anonymous-auth=false on all nodes
  • Enable webhook authentication (--authentication-token-webhook=true)
  • Rotate node certificates regularly

Authorization:

  • Use --authorization-mode=Webhook (not AlwaysAllow)
  • Apply least-privilege RBAC to service accounts
  • Restrict nodes/proxy permissions 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 /exec and /attach endpoint usage
  • Monitor for unexpected pod creation from service accounts

Cloud Provider Defaults

ProviderDefault AuthenticationNotes
EKS (AWS)SecureManaged nodes use IAM auth
GKE (GCP)SecureMetadata-based auth enforced
AKS (Azure)SecureAAD-backed authentication
Self-managedVariesMust be configured explicitly
k3s / RKEVariesCheck 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 /pods endpoint (equivalent to kubectl 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

go
// 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

go
// 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

yaml
# 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 0

References

Official Documentation

  1. Kubernetes Kubelet Authentication/Authorization
  2. Kubernetes CIS Benchmark — Kubelet Configuration
  3. NSA/CISA Kubernetes Hardening Guide

Security Research

ResourceDescription
CyberArk K8s Pentest Part 1Kubelet exploitation walkthrough
MITRE T1552.007Container API credential access
HackTricks — KubeletAttack methodology

CVE References

CVEDescription
No specific CVEConfiguration-based exposure, not a code bug
CWE-306Missing Authentication for Critical Function

Need help hardening your environment?

Talk to the Bugb security team directly.

Book a session