This is a building concierge who will carry your package anywhere if you already hold the right service badge
CVE-2021-25740 is an architectural flaw in Kubernetes Services routing: a user who can create or modify Endpoints or EndpointSlices can point a Service, shared Ingress, or LoadBalancer at backend IPs they should not normally reach. Kubernetes stated all versions are affected, and later clarified in May 2026 that this remains an unfixed design trade-off, not a bug with a normal code patch path. The practical blast radius is cross-namespace traffic forwarding and policy bypass, mainly where shared ingress/load-balancer components are trusted by NetworkPolicy or other allow rules.
The vendor's LOW 3.1 rating is basically right. In the real world this requires an attacker who is already authenticated to the Kubernetes API and already has unusually strong write permissions on endpoint objects; that means this is usually post-initial-access lateral movement, not a clean external compromise path. The 2026 clarification that the issue is unfixed matters for asset hygiene and scanner interpretation, but it does not turn this into a higher-priority emergency.
4 steps from start to impact.
Get namespace write capability
Endpoints or EndpointSlices in a namespace, typically via edit, admin, or a custom Role/ClusterRole. In practice this is usually exercised with kubectl or direct Kubernetes API calls, not a specialized exploit kit.- Authenticated access to the Kubernetes API
- RBAC allows
create/update/patchonendpointsorendpointslices
- Most enterprises do not hand endpoint-object write to random users
- Kubernetes v1.22+ new clusters removed default
Endpointswrite from built-inadmin/editroles - Attack dies immediately if RBAC was reconciled or endpoint writes are limited to controllers
kubectl auth can-i checks and RBAC audits are the right detection path.Repoint service backends
kubectl edit, kubectl patch, or raw API requests, the attacker modifies an Endpoints or EndpointSlice object so traffic for an allowed Service resolves to IPs of a different backend. Datadog published a working proof of concept showing this redirection model against shared ingress/load-balancer patterns.- A target Service path exists through a shared Ingress or LoadBalancer, or another trusted proxy path
- Attacker can identify victim backend IPs or another sensitive destination
- Backend IP discovery is not always trivial
- Some controllers overwrite manual changes quickly
- Many clusters are not meaningfully multi-tenant, so cross-namespace value may be limited
update/patch events on endpoints or endpointslices. Few commercial scanners model controller overwrite behavior.Abuse a trusted frontend hop
LoadBalancerSourceRanges when those controls trust the frontend component.- Frontend component is trusted by the victim path
- Routing layer actually consults the modified backend object
- If each tenant has a dedicated ingress/load balancer, the cross-tenant path largely disappears
- Well-segmented architectures avoid shared trust chokepoints
- Some ingress implementations do not support the risky forwarding patterns the advisory warns about
Reach otherwise blocked workload
- Victim application accepts traffic from the trusted frontend
- Sensitive data or actions are exposed at the application layer
- Application auth still may block the attacker
- Blast radius is bounded to what the redirected backend exposes
- This is traffic redirection, not arbitrary code execution
The supporting signals.
| In-the-wild status | No public evidence of active exploitation found in CISA KEV, and the Kubernetes advisory asks defenders to report exploitation if seen. This looks like a known architectural abuse path, not a campaign driver. |
|---|---|
| Proof-of-concept availability | Yes, public PoC exists. Datadog Security Labs states a full proof of concept is available and walks the exploit mechanics for shared ingress/load-balancer environments. |
| EPSS | 0.00519 (~0.52%), roughly 66.9th percentile based on current third-party EPSS displays. That is low absolute probability even if the percentile is middling. |
| KEV status | Not KEV-listed. No CISA KEV entry as of the catalog state consulted; no known due date because it is absent from KEV. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N is directionally fair. The important part is PR:L plus AC:H: the attack is network-reachable *only after* the attacker already has API access and the right RBAC grants. |
| Affected versions | All Kubernetes versions are affected. Kubernetes clarified on 2026-05-26 that this CVE remains an unfixed architectural issue across all versions, with record corrections scheduled for 2026-06-01. |
| Fixed versions / backports | No upstream patched version exists. New clusters created on Kubernetes v1.22+ do not include default Endpoints write in built-in admin/edit roles, but that is a configuration hardening change, not a full vulnerability fix; upgraded older clusters can retain the legacy grants until reconciled. |
| Exposure population | Not internet-scannable in the usual sense. Exposure is driven by RBAC and multi-tenant shared-routing design, not by an externally fingerprintable vulnerable listener. Shodan/Censys-style counts are therefore poor prioritization signals here; the real question is who can write endpoint objects in your clusters. |
| Disclosure / reporting | Disclosed 2021-09-20 in NVD/CVE publication flow; the public Kubernetes issue was opened 2021-07-14. The 2026 Kubernetes clarification credits QiQi Xu, Javier Provecho, and others for identifying these broader architectural risks. |
noisgate verdict.
The single decisive factor is that exploitation requires authenticated Kubernetes API access plus endpoint-object write permissions, which makes this a post-auth RBAC abuse case rather than an internet-reachable compromise path. Even where the routing trick works, the usual impact is limited to unauthorized network reachability through shared ingress/load-balancer trust, not cluster takeover.
Why this verdict
- Start from the vendor baseline: Kubernetes scored it LOW 3.1, and that is already close to the real-world risk because the attack is high-friction and confidentiality-only.
- Downward pressure from attacker position: this requires an attacker who is already authenticated to the Kubernetes API and already authorized to write
EndpointsorEndpointSlices; that implies prior compromise, delegated tenant access, or unusually broad developer RBAC. - Downward pressure from exposure population: new clusters created on v1.22+ no longer grant default
Endpointswrite through built-inadmin/editroles, so the reachable population is materially smaller unless you upgraded an older cluster or reintroduced the permission. - Downward pressure from blast radius: successful exploitation redirects traffic through a trusted frontend path and usually yields lateral application access or data exposure, not RCE, node compromise, or cluster-admin by itself.
- Upward pressure that keeps it from IGNORE: the issue is architectural and unfixed across all versions, and shared-ingress/multi-tenant clusters still exist; if your tenants can write endpoint objects, the control failure is real, not theoretical.
Why not higher?
This is not unauthenticated remote exploitation, and it is not a bug an internet scanner can mass-weaponize against arbitrary clusters. The chain depends on preexisting RBAC mistakes or permissive tenancy design, then on a shared frontend that is trusted to reach the victim backend. That is too much compounded friction for MEDIUM or HIGH.
Why not lower?
It is still a genuine policy-bypass primitive, not paperwork. In clusters where developers or tenants can write endpoint objects, a shared ingress or load balancer can be turned into a lateral-movement relay that defeats intended namespace isolation. Because Kubernetes has now clarified that the issue remains unfixed across all versions, defenders should keep it in RBAC hardening scope instead of dismissing it as obsolete.
What to do — in priority order.
- Audit endpoint-object writers — Enumerate every subject that can
create,update,patch, ordeleteendpointsandendpointslicesacross all namespaces, then remove those rights from broad user-facing roles. For a LOW verdict there is no SLA (treat as backlog hygiene), but because there is no patch path, this should still be handled in your next normal Kubernetes access-control review. - Reconcile legacy
system:aggregate-to-editgrants — If the cluster was created before v1.22 or upgraded from older releases, manually reconcile the built-in aggregate role soadmin/editno longer inheritEndpointswrite by default. There is no SLA (treat as backlog hygiene) for LOW, but upgraded clusters are where this issue most often survives unnoticed. - Create purpose-built roles for controllers only — If some automation genuinely needs endpoint-object writes, carve that into tightly scoped Roles or ClusterRoles bound only to the specific service accounts that require it. This prevents human users and general tenant roles from inheriting a lateral-routing primitive while preserving application behavior.
- Review shared ingress and ExternalName patterns — Validate whether your ingress/load-balancer design lets one tenant's path reach backends owned by another, and disable risky forwarding patterns such as unneeded
ExternalNamehandling where supported. In LOW/backlog terms this is architecture hygiene, but it meaningfully reduces the blast radius of any remaining RBAC slip. - Alert on endpoint mutations — Add Kubernetes audit detections for unexpected writes to
endpointsandendpointslices, especially by human users, CI identities, or tenant service accounts. There is no SLA (treat as backlog hygiene) for LOW, but this control is your best shot at catching exploitation because version scanners do not model live RBAC.
- NetworkPolicy alone doesn't solve this when the target already trusts the shared ingress/load-balancer path; the advisory explicitly calls out that the frontend can become the bypass channel.
- Container image scanning is irrelevant because this is not a vulnerable package version you replace in an image; it is a control-plane design and RBAC problem.
- Perimeter WAFs won't help much because the abuse usually rides legitimate frontend-to-backend application traffic inside the cluster after routing has already been manipulated.
Crowdsourced verification payload.
Run this from an auditor workstation or admin jump host that already has kubectl configured for the target cluster. Invoke it as python3 cve_2021_25740_check.py; it needs cluster-wide read access to RBAC objects (ClusterRole, Role, ClusterRoleBinding, RoleBinding) and should ideally run with cluster-admin or equivalent read permissions.
#!/usr/bin/env python3
# CVE-2021-25740 exposure check for Kubernetes
# Checks for non-system subjects bound to roles that can write Endpoints or EndpointSlices.
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 1 vulnerable, 0 patched, 2 unknown
import json
import subprocess
import sys
from typing import Dict, List, Tuple
WRITE_VERBS = {"create", "update", "patch", "delete", "deletecollection", "*"}
TARGETS = [
("", "endpoints"),
("discovery.k8s.io", "endpointslices"),
]
def run_kubectl(args: List[str]) -> dict:
cmd = ["kubectl"] + args + ["-o", "json"]
try:
p = subprocess.run(cmd, capture_output=True, text=True, check=False)
except FileNotFoundError:
print("UNKNOWN: kubectl not found")
sys.exit(2)
if p.returncode != 0:
print(f"UNKNOWN: kubectl failed: {' '.join(cmd)} :: {p.stderr.strip()}")
sys.exit(2)
try:
return json.loads(p.stdout)
except json.JSONDecodeError:
print(f"UNKNOWN: invalid JSON from kubectl: {' '.join(cmd)}")
sys.exit(2)
def rule_is_risky(rule: dict) -> bool:
api_groups = set(rule.get("apiGroups", []))
resources = set(rule.get("resources", []))
verbs = set(rule.get("verbs", []))
for api_group, resource in TARGETS:
if (api_group in api_groups or "*" in api_groups) and (resource in resources or "*" in resources):
if WRITE_VERBS & verbs:
return True
return False
def collect_risky_roles(clusterroles: dict, roles: dict) -> Tuple[Dict[str, dict], Dict[Tuple[str, str], dict]]:
risky_clusterroles = {}
risky_roles = {}
for item in clusterroles.get("items", []):
if any(rule_is_risky(r) for r in item.get("rules", [])):
risky_clusterroles[item["metadata"]["name"]] = item
for item in roles.get("items", []):
if any(rule_is_risky(r) for r in item.get("rules", [])):
ns = item["metadata"]["namespace"]
name = item["metadata"]["name"]
risky_roles[(ns, name)] = item
return risky_clusterroles, risky_roles
def subject_is_non_system(subject: dict) -> bool:
kind = subject.get("kind", "")
name = subject.get("name", "")
namespace = subject.get("namespace", "")
if kind == "Group" and name.startswith("system:"):
return False
if kind == "User" and name.startswith("system:"):
return False
if kind == "ServiceAccount" and namespace == "kube-system":
return False
return True
def format_subject(subject: dict) -> str:
kind = subject.get("kind", "?")
name = subject.get("name", "?")
ns = subject.get("namespace")
return f"{kind}:{ns + '/' if ns else ''}{name}"
def main() -> int:
# Basic connectivity test
version = subprocess.run(["kubectl", "version", "--request-timeout=10s"], capture_output=True, text=True)
if version.returncode != 0:
print(f"UNKNOWN: cannot talk to cluster: {version.stderr.strip()}")
return 2
clusterroles = run_kubectl(["get", "clusterroles"])
roles = run_kubectl(["get", "roles", "-A"])
crbs = run_kubectl(["get", "clusterrolebindings"])
rbs = run_kubectl(["get", "rolebindings", "-A"])
risky_clusterroles, risky_roles = collect_risky_roles(clusterroles, roles)
findings = []
for item in crbs.get("items", []):
ref = item.get("roleRef", {})
if ref.get("kind") == "ClusterRole" and ref.get("name") in risky_clusterroles:
for subj in item.get("subjects", []):
if subject_is_non_system(subj):
findings.append({
"binding": f"ClusterRoleBinding/{item['metadata']['name']}",
"role": f"ClusterRole/{ref.get('name')}",
"subject": format_subject(subj),
})
for item in rbs.get("items", []):
ns = item["metadata"].get("namespace", "")
ref = item.get("roleRef", {})
risky = False
role_label = ""
if ref.get("kind") == "ClusterRole" and ref.get("name") in risky_clusterroles:
risky = True
role_label = f"ClusterRole/{ref.get('name')}"
elif ref.get("kind") == "Role" and (ns, ref.get("name")) in risky_roles:
risky = True
role_label = f"Role/{ns}/{ref.get('name')}"
if risky:
for subj in item.get("subjects", []):
if subject_is_non_system(subj):
findings.append({
"binding": f"RoleBinding/{ns}/{item['metadata']['name']}",
"role": role_label,
"subject": format_subject(subj),
})
if findings:
print("VULNERABLE: non-system subjects can write Endpoints or EndpointSlices")
for f in findings[:25]:
print(f"- {f['subject']} via {f['binding']} -> {f['role']}")
if len(findings) > 25:
print(f"- ... {len(findings) - 25} additional risky bindings omitted")
return 1
print("PATCHED: no non-system subject bindings to Roles/ClusterRoles with endpoint-object write permissions were found")
return 0
if __name__ == "__main__":
sys.exit(main())
If you remember one thing.
Endpoints/EndpointSlices, reconcile legacy pre-1.22 role grants, and close the gap in the next normal maintenance cycle rather than burning an out-of-band patch window.Sources
- Kubernetes GitHub issue #103675
- Kubernetes security advisory discussion
- NVD CVE-2021-25740
- Kubernetes blog: Reconciling the Past: Correcting Records for Unfixed Kubernetes CVEs
- Kubernetes RBAC documentation
- CISA Known Exploited Vulnerabilities Catalog
- Datadog Security Labs: Unpatchable Vulnerabilities of Kubernetes: CVE-2021-25740
- Wiz vulnerability database entry for CVE-2021-25740
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.