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

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

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

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

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

Default ServiceAccount

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

Проверим default namespace:

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

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

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

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

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

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

default token

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

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

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

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

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

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

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

JWT token

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

token="ZXl...TWc="

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

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

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

  • заголовок (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.

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

minikube start

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ServiceAccounts и RBAC

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

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

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:/ ]$

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

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

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

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

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

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

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

[ 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
}

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

[ 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)

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

[ 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
}

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

RoleBindig для ServiceAccount

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

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

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

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

[ 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"
}
}
]

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

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

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

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

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