Для аутентицикации и авторизации в 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