
Для аутентицикации и авторизации в 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-правила и роли для пользователей, таких как разработчики, например — разрешать доступ только в рамках определённого нейспейса.
Ссылки по теме
- Using RBAC with Service Accounts in Kubernetes
- Kubernetes Access Control: Exploring Service Accounts
- Kubernetes Tips: Using a ServiceAccount
- What is JSON Web Token?
- kubernetes.io: Service Accounts
- kubernetes.io: Authenticating
- kubernetes.io: Using RBAC Authorization
- The Dark Arts of IAM & RBAC – Read-only Kubernetes Access
- Kubernetes Authentication
- Istio End-User Authentication for Kubernetes using JSON Web Tokens (JWT) and Auth0
- Kubernetes RBAC 101: Authentication
- How to Authorize Non-Kubernetes Clients With Istio on Your K8s Cluster
- Kubernetes Client Authentication on Amazon EKS
- Verifying EKS digital certificates
- 3 Realistic Approaches to Kubernetes RBAC







