Kubernetes: ServiceAccounts, JWT-токены, аутентификация и RBAC-авторизация

Автор: | 17/11/2020

Для аутентицикации и авторизации в Kubernetes имеются такие понятия как User Accounts и Service Accounts.

User Accounts – профили обычных пользователей, используемые для доступа к клатеру снаружи кластера, тогда как Service Accounts используются для аутентификации сервисов внутри кластера.

ServiceAccounts предназначены для предоставления идентификатора, используя который Kubernetes Pod, а точнее контейнер(ы) в нём, могут быть аутенифицированы и авторизованы для выполнения API-запросов к API-серверу Kubernetes.

Default ServiceAccount

В каждом Kubernetes Namespace имеется свой дефолтный ServiceAccount (SA), который создаётся вместе с самими нейспейсом.

Проверим default namespace:

[simterm]

$ kubectl --namespace default get serviceaccount
NAME                   SECRETS   AGE
default                1         176d

[/simterm]

Для каждого создаваемого ServiceAccount генерируется токен, который хранится в Kubernetes Secret.

Проверяем default SA:

[simterm]

$ kubectl --namespace default get serviceaccount default -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2020-05-25T12:04:49Z"
  name: default
  namespace: default
  resourceVersion: "296"
  selfLink: /api/v1/namespaces/default/serviceaccounts/default
  uid: 19cc2b5f-fbc3-403e-a7c7-d62361a4038a
secrets:
- name: default-token-292g9

[/simterm]

Токен этого SA – в секрете default-token-292g9:

[simterm]

...
secrets: 
- name: default-token-292g9

[/simterm]

default token

Посмотрим содержимое секрета:

[simterm]

$ kubectl get secret default-token-292g9 -o yaml
apiVersion: v1
data:
  ca.crt: LS0...sdA==
  token: ZXl...TWc=
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: default
    kubernetes.io/service-account.uid: 19cc2b5f-fbc3-403e-a7c7-d62361a4038a
  creationTimestamp: "2020-05-25T12:04:49Z"
  name: default-token-292g9
  namespace: default
  resourceVersion: "294"
  selfLink: /api/v1/namespaces/default/secrets/default-token-292g9
  uid: 07a46645-0083-45a0-a640-6e6a78ebd9b1
type: kubernetes.io/service-account-token

[/simterm]

Во-первых, тип – kubernetes.io/service-account-token.

Второй интересный блок – собственно data, в которой хранятся две записи – ca.cert и token.

Если токен не default namespace – то будет ещё и третье поле с указанием неймспейса, которому токен принадлежит.

ca.cert подписывается приватным ключём кластера (который играет роль Certificate Authority) и позволяет поду или приложению валидировать запросы к API-серверу, что бы убедиться что это именно тот API-сервер, который нужен.

А вот на token остановимся подробнее.

JWT token

Для удобства сохраним строку из data.token в переменную:

[simterm]

$ token="ZXl...TWc="

[/simterm]

С помощью base64 получаем содержимое:

[simterm]

$ echo $token | base64 -d
eyJ[...]iJ9.eyJ[...]ifQ.g5I[...]3Mg

[/simterm]

Тут в […] вырезана часть, но видим, что токен точками разделён на три части:

  • заголовок (header) – описывает способ подписи токена
  • данные (payload) – собственно данные токена, такие как дата выдачи и срок действия, кем выдан, и другие, см. RFC-7519
  • подпись (signature) – используется для проверки того, что токен не был модифицирован и может использоваться для проверки отправителя

См. документацию>>>.

Прсомотреть содержимое токена можно с помощью консольной jwt, или на сайте jwt.io.

В нашем случае payload токена будет таким:

{
  "iss": "kubernetes/serviceaccount",
  "kubernetes.io/serviceaccount/namespace": "default",
  "kubernetes.io/serviceaccount/secret.name": "default-token-s8m4t",
  "kubernetes.io/serviceaccount/service-account.name": "default",
  "kubernetes.io/serviceaccount/service-account.uid": "b4514006-4c9a-4c30-92c8-1cc1c058b31c",
  "sub": "system:serviceaccount:default:default"
}

Тут в sub видим имя сервис аккаунта, т.е. когда предъявитель токена передаёт его API-серверу Kubernetes – кластер знает, от чьего имени этот токен пришёл.

Но что на счёт пароля? В sub есть “имя пользователя” – но где его “пароль”?

Тут работает третья часть токена – signature.

JWT токен и аутентификация

Почему-то нигде из нагугленных материалов этот момент не рассматривается (см. ссылки в Ссылки по теме), хотя он тут наверно самый интересный.

Вернёмся к первой части токена – header, который в нашем случае содержит тип алгоритма RS256, т.е. RSA (Rivest-Shamir-Adleman) – ассиметричный алгоритм с использованием приватного и публичного ключа, и SHA-256 подписи для проверки данных.

Проверим наш токен на jwt.io:

Invalid Signature – так как мы не предоставили приватный и публичный ключ для верификации.

В силу того, что в AWS Elastic Kubernetes Service доступ к приватному ключу кластера мы получить не можем, ибо он хранится на Мастер-нодах, то используем minikube.

Запускаем локальный кластер:

[simterm]

$ minikube start

[/simterm]

В его default нейспейсе уже есть дефолтный токен:

[simterm]

$ kubectl get secrets                                                                                                                                                                                
NAME                  TYPE                                  DATA   AGE                                                                                                                                                                       
default-token-s8m4t   kubernetes.io/service-account-token   3      2m44s

[/simterm]

Получаем из него поле token, декриптим base64 строку:

[simterm]

$ kubectl get secrets -o jsonpath='{.items[0].data.token}' | base64 -d
eyJhbGciO[...]61O_LxbM_-tiLjyjeCZw

[/simterm]

Снова идём на jwt.io, вставляем токен:

Пока что Invalid Signature – берём публичный сертификат minikube – файл ~/.minikube/ca.crt:

[simterm]

$ cat ~/.minikube/ca.crt 
-----BEGIN CERTIFICATE-----
MIIDBjCCAe6gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p
...
0g+FhVM92T+yV38vYLO/HaKeiOzIcgHHkAoLJZd/K/Mu7crwIuGlcCVhrjcHoa3p
Md34ZTeqxA4J3w==
-----END CERTIFICATE-----

[/simterm]

Вставляем в поле Public Key or Certificate.

Получаем приватный ключ кластера – собственно он и используется для подписи тех же ca.crt и токенов – файл ~/.minikube/ca.key:

[simterm]

$ cat ~/.minikube/ca.key 
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAtDRDag2D7UBaBmWQwTKVLjuKTuat4eD/oThRgfi5bcCnwooG
...
xnL96EHthflb3NaS4GKuJYzNAPhfOdMw96Ce8KtNYpMYjRhNF9TN
-----END RSA PRIVATE KEY-----

[/simterm]

Вставляем в поле Private Key:

Signature Verified – готово, подлинность предъявителя сертификата проверена.

Итак, возвращаясь к ServiceAccount:

  • для ServiceAccount создаётся токен, в котором указано имя ServiceAccount
  • токен подписывается приватным ключём Kubernetes-кластера
  • под используя этот токен выполняет запрос к API-серверу
  • API-сервер валидирует этот токен, используя публичную часть своего ключа, и проверяет, что токен не был изменён, и был выдан именно этим сервером

Теперь, посмотрим, как это выглядит на практике, плюс проверим, как тут работает Kubernetes RBAC.

ServiceAccounts и RBAC

Для каждого пода, которому явно не задан ServiceAccount, подключается ServiceAccount default, и монтируется дефолтный токен.

Возвращаемся к нашему EKS кластеру, запускаем под:

[simterm]

$ kubectl run -i --tty --rm ca-test-pod --image=radial/busyboxplus:curl
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
If you don't see a command prompt, try pressing enter.
[ root@ca-test-pod-5c96c78d7f-wqlsq:/ ]$

[/simterm]

Проверяем его volumeMounts, serviceAccount и volumes:

[simterm]

$ kubectl get pod ca-test-pod-5c96c78d7f-wqlsq -o yaml
apiVersion: v1
kind: Pod
...
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: default-token-292g9
      readOnly: true
...
  serviceAccount: default
  serviceAccountName: default
...
  volumes:
  - name: default-token-292g9
    secret:
      defaultMode: 420
      secretName: default-token-292g

[/simterm]

В поде проверяем содержимое /var/run/secrets/kubernetes.io/serviceaccount:

[simterm]

[ root@ca-test-pod-5c96c78d7f-wqlsq:/ ]$ ls -1 /var/run/secrets/kubernetes.io/serviceaccount
ca.crt
namespace
token

[/simterm]

И вспоминаем содержимое data секрета default-token-292g9:

[simterm]

$ kubectl get secret default-token-292g9 -o yaml
apiVersion: v1
data:
  ca.crt: LS0t[...]
  namespace: ZGVmYXVsdA==
  token: ZXlKaGJ
...

[/simterm]

Теперь пробуем обратиться к API-серверу без аутентификации – используем специальный Service kubernetes, добавляем -k или --insecure к curl, что бы не валидировать сертификат API-сервера:

[simterm]

[ root@ca-test-pod-5c96c78d7f-wqlsq:/ ]$ curl -k https://kubernetes
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
    
  },
  "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
  "reason": "Forbidden",
  "details": {
    
  },
  "code": 403
}

[/simterm]

Добавим две переменные – с сертификатом API-сервера и сам токен:

[simterm]

[ root@ca-test-pod-5c96c78d7f-wqlsq:/ ]$ CERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
[ root@ca-test-pod-5c96c78d7f-wqlsq:/ ]$ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

[/simterm]

И вызываем curl снова – пробуем получить список подов в нашем неймспейсе, на этот раз без --insecure и с авторизацией, передавая Authorization заголовок:

[simterm]

[ root@ca-test-pod-5c96c78d7f-wqlsq:/ ]$ curl --cacert $CERT -H "Authorization: Bearer $TOKEN" "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
}

[/simterm]

Тут уже в ошибке видим нашего пользователя – User "system:serviceaccount:default:default", но у него не хватает прав, так как по умолчанию пользователи и сервис-аккаунты не имеют никаких прав доступа.

RoleBindig для ServiceAccount

Что бы SericeAccount получил права доступа ему, как и обычному User Account, надо создать RoleBinding или ClusterRoleBinding.

Cоздаём RoleBinding на дефолтную ClusterRole view, см. User-facing roles:

[simterm]

$ kubectl create rolebinding ca-test-view --clusterrole=view --serviceaccount=default:default
rolebinding.rbac.authorization.k8s.io/ca-test-view created

[/simterm]

И пробуем curl снова:

[simterm]

[ root@ca-test-pod-5c96c78d7f-wqlsq:/ ]$ curl --cacert $CERT -H "Authorization: Bearer $TOKEN" "https://kubernetes/api/v1/namespaces/default/pods/"
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "selfLink": "/api/v1/namespaces/default/pods/",
    "resourceVersion": "66892356"
  },
  "items": [
    {
      "metadata": {
        "name": "ca-test-pod-5c96c78d7f-wqlsq",
        "generateName": "ca-test-pod-5c96c78d7f-",
        "namespace": "default",
        "selfLink": "/api/v1/namespaces/default/pods/ca-test-pod-5c96c78d7f-wqlsq",
        "uid": "f0d77cfe-38ab-48e9-aaf3-f344f1d343f3",
        "resourceVersion": "66888089",
        "creationTimestamp": "2020-11-17T16:08:09Z",
        "labels": {
          "pod-template-hash": "5c96c78d7f",
          "run": "ca-test-pod"
        },
        ...
        "qosClass": "BestEffort"
      }
    }
  ]

[/simterm]

ServiceAccounts и безопасность

Учитывайте, что имея доступ к Secrets и ServiceAccounts, к любому поду можно подключить любой токен, и используя его получить доступ к определённым ресурсам.

Например, используя ServiceAccount для ExternalDNS – можно получить доступ к API AWS Route53, и натворить дел в доменах.

Поэтому важно разделять доступ к ресурсам используя RBAC-правила и роли для пользователей, таких как разработчики, например – разрешать доступ только в рамках определённого нейспейса.

Ссылки по теме