Kubernetes Read-Only API Zugriff
Kubernetes ist durch seine erweiterbare API etwas besonderes: Auf der einen Seite werden für die Clusterverwaltung benötigte Objekte durch die Kubernetes-API bereitgestellt und erlauben damit, beliebige Programme in die Clusterverwaltung einzubeziehen. Zum anderen können beliebige Anwendungen und Produkte die Kubernetes-API um eigene Funktionalität erweitern und damit auf einheitliche, herstellerunabhängige Weise bereitgestellt werden.
Auch - oder gerade - innerhalb eines Kubernetes Clusters kann ein API Zugriff sinnvoll sein.
Kubernetes stellt den API-Server daher standardmäßig als Service kubernetes
im default
Namespace zur Verfügung.
Im Folgenden wird gezeigt, wie die API verwendet werden kann, und wie ein spezieller Serviceaccount angelegt wird, der lediglich Leserechte für die Kubernetes-API besitzt.
Die Kubernetes-API wird innerhalb des Clusters als Kubernetes Service bereitgestellt, wie sich auch durch die entsprechenden Umgebungsvariablen zeigt:
default
Namespace$ kubectl run shell --image=busybox --restart=Never --rm --tty -i -- sh -c "export"
export HOME='/root'
export HOSTNAME='shell'
export KUBERNETES_PORT='tcp://10.96.0.1:443'
export KUBERNETES_PORT_443_TCP='tcp://10.96.0.1:443'
export KUBERNETES_PORT_443_TCP_ADDR='10.96.0.1'
export KUBERNETES_PORT_443_TCP_PORT='443'
export KUBERNETES_PORT_443_TCP_PROTO='tcp'
export KUBERNETES_SERVICE_HOST='10.96.0.1'
export KUBERNETES_SERVICE_PORT='443'
export KUBERNETES_SERVICE_PORT_HTTPS='443'
export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
export PWD='/'
export SHLVL='1'
export TERM='xterm'
pod "shell" deleted
Um auf die API zuzugreifen, kann beispielsweise curl
verwendet werden.
Ohne Credentials zur Authentifizierung (Token, Client-Zertifikat) ist lediglich ein anonymer Zugriff möglich.
Typischerweise ist dies bei Kubernetes mit RBAC (role based access control) nicht erlaubt, sodass eine entsprechende Fehlermeldung zurückgeliefert wird.
$ kubectl run shell --image=alpine --restart=Never --rm --tty -i
If you don't see a command prompt, try pressing enter.
/ # apk add curl
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/aarch64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/aarch64/APKINDEX.tar.gz
(1/5) Installing ca-certificates (20190108-r0)
(2/5) Installing nghttp2-libs (1.35.1-r0)
(3/5) Installing libssh2 (1.8.2-r0)
(4/5) Installing libcurl (7.64.0-r1)
(5/5) Installing curl (7.64.0-r1)
Executing busybox-1.29.3-r10.trigger
Executing ca-certificates-20190108-r0.trigger
OK: 7 MiB in 19 packages
/ # curl -sk https://kubernetes/
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {
},
"code": 403
}
Standardmäßig stellt Kubernetes jedem Pod einen Default-Serviceaccount zur Verfügung.
Serviceaccounts dienen in Kubernetes zur Authentifizierung von meist technischen Usern.
Der Default-Serviceaccount eines Pods wird innerhalb des Kubernetes Pods als Secret im Dateisystem unter /run/secrets/kubernetes.io/serviaccount
bereitgestellt und besteht aus drei Dateien:
- ca.crt
-
CA-Zertifikat zur Validierung der TLS Verbindung
- namespace
-
Namespace des aktuellen Pods
- token
-
Enthält das zur Authentifizierung zu verwendende Token
Mit diesem Serviceaccount kann nun ein authentifizierter Request gegen die Kubernetes API durchgeführt werden. Das CA-Zertifikat dient dazu, sicherzustellen, dass die Gegenstelle korrekt ist, und dass das zur Authentifizierung eingesetzte Token nicht an eine falsche Stelle übermittelt wird.
Das Token wird im Authorization-Header als Bearer
mitgegeben, das CA-Zertifikat als Trust Anchor für das Zertifikat der Gegenseite verwendet.
Im Gegensatz zu einem anonymen Request wird dieser nicht abgewiesen und der Server identifiziert sich mit der ihm zugewiesenen IP.
system:serviceaccount:default:default
/ # TOKEN=$(cat /run/secrets/kubernetes.io/serviceaccount/token)
/ # curl --cacert /run/secrets/kubernetes.io/serviceaccount/ca.crt \
--header "Authorization: Bearer $TOKEN" -X GET https://kubernetes/api
{
"kind": "APIVersions",
"versions": [
"v1"
],
"serverAddressByClientCIDRs": [
{
"clientCIDR": "0.0.0.0/0",
"serverAddress": "10.23.202.120:6443"
}
]
}~
Der verwendete Default-Account besitzt jedoch wenig Rechte. So ist bereits die Abfrage der Pods des default-Namespaces nicht erlaubt.
system:serviceaccount:default:default
/ # curl --cacert /run/secrets/kubernetes.io/serviceaccount/ca.crt \
--header "Authorization: Bearer $TOKEN" -X GET https://kubernetes/api/v1/namespaces/default/pods
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "pods is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"pods\" in API group \"\" in the namespace \"default\"",
"reason": "Forbidden",
"details": {
"kind": "pods"
},
"code": 403
}~
Abhilfe schafft ein neuer Serviceaccount, der mit entsprechenden Rechten ausgestattet wird.
Erzeugung eines Kubernetes Read-Only Serviceaccount
Ein Serviceaccount lässt sich über die Kubernetes API bzw. kubectl
erzugen.
$ kubectl create serviceaccount readonly
serviceaccount/readonly created
$ kubectl describe serviceaccount readonly
Name: readonly
Namespace: default
Labels: <none>
Annotations: <none>
Image pull secrets: <none>
Mountable secrets: readonly-token-fqrsd
Tokens: readonly-token-fqrsd
Events: <none>
Der so erzeugte Serviceaccount kann einem Pod zugewiesen werden.
apiVersion: v1
kind: Pod
metadata:
name: demo
spec:
serviceAccountName: readonly
containers:
- name: demo
image: alpine
args:
- sleep
- "3600"
Auch von der Kommandozeile wird durch kubectl
die Verwendung des Serviceaccounts unterstützt.
Dazu muss lediglich der Parameter --serviceaccount
spezifiziert werden.
Jedoch kann auch mit diesem Account die API noch nicht sinnvoll verwendet werden, denn dem Account sind keine Rechte zugeordnet.
kubectl
$ kubectl run shell --image=alpine --restart=Never --serviceaccount=readonly --rm --tty -i
If you don't see a command prompt, try pressing enter.
/ # apk add curl
...
/ # TOKEN=$(cat /run/secrets/kubernetes.io/serviceaccount/token)
/ # curl --cacert /run/secrets/kubernetes.io/serviceaccount/ca.crt \
--header "Authorization: Bearer $TOKEN" -X GET https://kubernetes/api/v1/namespaces/default/pods
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Failure",
"message": "pods is forbidden: User \"system:serviceaccount:default:readonly\" cannot list resource \"pods\" in API group \"\" in the namespace \"default\"",
"reason": "Forbidden",
"details": {
"kind": "pods"
},
"code": 403
}
Es werden also noch die Berechtigungen für den Serviceaccount auf der Kubernetes API benötigt.
Kubernets Cluster Read-Only Rechte
In Kubernetes werden Rechte an eine Rolle geknüpft. Die Rolle kann dann einem Serviceaccount zugeordnet werden. Sowohl die Rollen, als auch die Assoziation (Binding) von Nutzern (User, Group oder Serviceaccount) zu einer Rolle können entweder zu einem Namespace gehören, oder global für den Cluster gelten.
Zunächst wird eine ClusterRole
als globale Rolle angelegt.
Der Rolle werden dabei Leserechte auf allen API-Gruppen und darunter befindlichen Ressourcen eingeräumt.
Die Art des Zugriffs wird dabei über die Operationen (Verben) ausgedrückt:
get
, list
und watch
sind lediglich Leseoprationen.
Im Gegensatz zu einer Role
kann eine ClusterRole
auch Rechte auf globalen Objekten, die keinem Namespace zugeordnet sind, einräumen.
Beispiele dafür sind die Node
-Objekte des Clusters oder Objekte, die keine Kubernetes Ressourcen sind, wie die /healthz
Endpoints.
readonly
$ cat <<EOF | kubectl apply -f - apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: annotations: rbac.authorization.kubernetes.io/autoupdate: "true" labels: name: readonly rules: - apiGroups: - "" resources: ["*"] verbs: - get - list - watch - apiGroups: - extensions resources: ["*"] verbs: - get - list - watch - apiGroups: - apps resources: ["*"] verbs: - get - list - watch EOF clusterrole.rbac.authorization.k8s.io/readonly created
Diese Rolle wird nun, wieder global, an den Serviceaccount gebunden, wodurch die Rechte eingeräumt werden.
$ cat <<EOF | kubectl apply -f -
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: readonly-binding
subjects:
- kind: ServiceAccount
name: readonly
namespace: default
roleRef:
kind: ClusterRole
name: readonly
apiGroup: rbac.authorization.k8s.io
EOF
Wäre stattdessen ein RoleBinding
zum Einsatz gekommen, dann wären die Rechte nur auf dem im RoleBinding
deklarierten Namespace eingeräumt worden.
Nun kann mit dem Serviceaccount ein Zugriff auf die API erfolgen.
/ # curl --cacert /run/secrets/kubernetes.io/serviceaccount/ca.crt \
--header "Authorization: Bearer $TOKEN" -X GET https://kubernetes/api/v1/namespaces/default/pods
{
"kind": "PodList",
"apiVersion": "v1",
"metadata": {
"selfLink": "/api/v1/namespaces/default/pods",
"resourceVersion": "1126123"
},
"items": [
{
"metadata": {
"name": "shell",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/pods/shell",
"uid": "d9179dd4-7745-11e9-8eef-001e064203b9",
"resourceVersion": "1123326",
"creationTimestamp": "2019-05-15T20:15:49Z",
"labels": {
"run": "shell"
}
},
...
Dank der erweiterbaren API bietet Kubernetes umfangreiche Optionen zur Integration in eigene Programme und Systeme.
Mit Hilfe der flexiblen Konfiguration von Rollen und Berechtigungen ist es leicht möglich, den Zugriff auf die Kubernetes-API für den jeweiligen Anwendungsfall fein granular zu steuern.
Keinesfalls sollten zu viele Rechte gewährt werden, schnell ist eine Anwendung sonst nicht nur root
auf einer Maschine, sondern im ganzen Cluster.
Zu den Themen Kubernetes, Docker und Cloud Architektur bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.