Neuigkeiten von trion.
Immer gut informiert.

Kubernetes Read-Only API Zugriff

Kubernetes

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:

Ausgabe der Umgebungsvariablen in einem Pod im 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.

Anonymer Zugriff auf die Kubernetes API mit curl
$ 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.

Authentifizierter Zugriff auf die Kubernetes API als 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.

Authentifizierter Zugriff auf die Kubernetes API als 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.

Erzeugung Kubernetes Serviceaccount
$ 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.

Verwendung des erzeugten Serviceaccounts in Pod-Deklaration
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.

Verwendung des Serviceaccount mit 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.

Erzeugung der globalen Rolle 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.

Berechtigung des Serviceaccounts
$ 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.

Erneuter Zugriff auf Kubernets mit Readonly Account
/ # 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.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.