I’ve been meaning to write a post about Kubelet authorization for a while now, and as there have been some posts this week where it got a mention, now seems like a good time!
The Kubelet is the Kubernetes component which runs on each worker (and possible control plane) node and is responsible for managing the container runtime on the host. It communicates with the Kubernetes API server to get information about workloads that should be running on the node and then instantiates them using a container runtime like Containerd. To do this, it obviously needs credentials to access the API server, and needs rights to things like pods and also associated objects like secrets.
From a security perspective these Kubelet credentials are important as, if an attacker breaks out from a container to the underlying node, there will generally be a set of kubelet credentials available to them, so they could be used to escalate rights to the cluster, as a result it’s been necessary for the Kubernetes project to take steps to restrict what the Kubelet can do, to reduce the risk of privilege escalation.
A Brief aside - Kubernetes authorization modes
An important aspect of Kubernetes to discuss before talking about exactly how Kubelet authorization works, is how Kubernetes generally handles authorization. Whilst most clusters will use RBAC, it’s possible to have multiple authorization modes in any cluster. Rights provided by each authorization mode are cumulative, so it’s important to be careful about inadvertently granting rights to users. Also, if you’re using Kubernetes in-built tooling for listing all of a users permissions (via the kubectl auth can-i --list
command showne below), it’s worth noting that this only works with RBAC, rights granted via other authorization modes will not be analyzed, although you can check for individual rights granted via any authorization mode using the kubectl auth can-i
command.
Kubelet authorization
Having talked about the fundamentals, let’s look at how Kubelet authorization works. We’ll start with a KinD cluster and look at what’s visible there.
For this we’ll use a cluster with two worker nodes, using a simple kind config
# three node (two workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
Then we can start the cluster up with kind create cluster --config kind-config.yaml --name kubeletauthz
. Once the cluster is up and running we can shell into one of the worker nodes to look at the Kubelet credentials. You can find out what Kubeconfig file the Kubelet is using by looking at the parameters passed to the Kubelet on the command line. In the case of Kubeadm the default will be --kubeconfig=/etc/kubernetes/kubelet.conf
. Once we know the location we can use that with Kubectl, for example to see a listing of pods in the cluster
kubectl --kubeconfig=/etc/kubernetes/kubelet.conf get po -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-787d4945fb-4rspx 1/1 Running 0 4m43s
kube-system coredns-787d4945fb-p5g6f 1/1 Running 0 4m43s
kube-system etcd-kubeletauthz-control-plane 1/1 Running 0 4m56s
kube-system kindnet-clmjl 1/1 Running 0 4m43s
kube-system kindnet-jhngj 1/1 Running 0 4m26s
kube-system kindnet-kjsvt 1/1 Running 0 4m27s
kube-system kube-apiserver-kubeletauthz-control-plane 1/1 Running 0 4m55s
kube-system kube-controller-manager-kubeletauthz-control-plane 1/1 Running 0 4m55s
kube-system kube-proxy-9v62x 1/1 Running 0 4m43s
kube-system kube-proxy-q8t5c 1/1 Running 0 4m26s
kube-system kube-proxy-vnrt6 1/1 Running 0 4m27s
kube-system kube-scheduler-kubeletauthz-control-plane 1/1 Running 0 4m56s
local-path-storage local-path-provisioner-75f5b54ffd-52xm9 1/1 Running 0 4m43s
Usually to check the rights of a principal in Kubernetes we’d use the command kubectl auth can-i --list
and if we try that with the Kubelet credentials we get back something like this.
kubectl --kubeconfig=/etc/kubernetes/kubelet.conf auth can-i --list
Warning: the list may be incomplete: node authorizer does not support user rule resolution
Resources Non-Resource URLs Resource Names Verbs
selfsubjectaccessreviews.authorization.k8s.io [] [] [create]
selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
certificatesigningrequests.certificates.k8s.io/selfnodeclient [] [] [create]
[/api/*] [] [get]
[/api] [] [get]
[/apis/*] [] [get]
[/apis] [] [get]
[/healthz] [] [get]
[/healthz] [] [get]
[/livez] [] [get]
[/livez] [] [get]
[/openapi/*] [] [get]
[/openapi] [] [get]
[/readyz] [] [get]
[/readyz] [] [get]
[/version/] [] [get]
[/version/] [] [get]
[/version] [] [get]
[/version] [] [get]
Notably there are no rights to the pod
objects that we just looked at! The clue to what’s going on here is the Warning
line at the top which notes that the node authorizor
doesn’t support user rule resolution.
One quick aside is that you may be confused by the Kubelet not using RBAC as there is a clusterrole
called system:node
which looks like it would provide rights to nodes, however the corresponding clusterrolebinding
doesn’t actually have any subjects (which is weird), so it has no effect!
Node authorizer
The key to what’s going on here can be see in the configuration of the Kubernetes API server. If you look at the parameters passed to the kube-apiserver
component you’ll see this stanza --authorization-mode=Node,RBAC
indicating that there are two modes of authorization configured and, as we said earlier, the rights from these are cumulative.
The Node authorization mode is an authorization mode with one purpose which is to provide rights to Kubelets. From the documentation page we can see that this authorization mode allows access to the kind of resources that the Kubelet needs to use like pods
, nodes
, configmaps
and secrets
. Within that group it also needs to restrict which secrets etc are actually accessible as you don’t want a Kubelet on one node to be able to access secrets intended for pods running on another node.
The exact logic of what is allowed can be seen in the code
// NodeAuthorizer authorizes requests from kubelets, with the following logic:
// 1. If a request is not from a node (NodeIdentity() returns isNode=false), reject
// 2. If a specific node cannot be identified (NodeIdentity() returns nodeName=""), reject
// 3. If a request is for a secret, configmap, persistent volume or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node:
// node <- configmap
// node <- pod
// node <- pod <- secret
// node <- pod <- configmap
// node <- pod <- pvc
// node <- pod <- pvc <- pv
// node <- pod <- pvc <- pv <- secret
// 4. For other resources, authorize all nodes uniformly using statically defined rules
You can see this in effect if you try to get secrets
from a cluster with Kubelet credentials
kubectl --kubeconfig=/etc/kubernetes/kubelet.conf get secrets -A
Error from server (Forbidden): secrets is forbidden: User "system:node:kubeletauthz-worker" cannot list resource "secrets" in API group "" at the cluster scope: can only read namespaced object of this type
One important point to note on this is where the logic is “reject”, this just passes the request to other configured authorization modes, so if cluster RBAC has been modified to allow the system:nodes
group to do something in excess of what the Node authorization mode allows then that will still be allowed.
You can see this by, for example, editing the system:node
clusterrolebinding
to add the system:nodes
group as a subject, by adding these lines to it.
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: system:nodes
Once you’ve done that, if you use Kubelet credentials to try and get secrets at a cluster level, it works fine :)
kubectl --kubeconfig=/etc/kubernetes/kubelet.conf get secrets -A
NAMESPACE NAME TYPE DATA AGE
kube-system bootstrap-token-abcdef bootstrap.kubernetes.io/token 6 34m
There are a couple of places where this mode can’t effectively restrict permissions, which are in node and pod properties, for that we need another component.
NodeRestriction Admission Controller
This is where a specialized admission controller comes in. The NodeRestriction admission controller looks at requests from Kubelets and, where they relate to pods
and nodes
it limits the rights to only those that are appropriate for the Kubelet. For example it restricts what properties of node
objects can be modified, to stop it changing it’s own security classification, for example.
Variations in Kubernetes distributions
It’s important to note that, as with most Kubernetes configuration topics, what we’ve discussed here relates to vanilla Kubernetes, in this case Kubeadm. Distribution providers are free to change this configuration and indeed some do so. For example Azure AKS currently defaults to allowing Kubelets GET
access to secrets
at the cluster level!
Conclusion
When looking at Kubernetes authorization it can be tempting to focus purely on RBAC as it’s the most common option deployed in Kubernetes clusters today. However as we’ve seen there are times when RBAC alone won’t provide an adequate level of security and supplemental authorization modes and admission controllers are required. In this post we’ve looked at the Node authorization mode and NodeRestriction admission controller which are used to provide rights to Kubelets to access the resources they need to function.