SSO в Kubernetes: гайд на Dex + Headlamp + Google OAuth
Дисклеймер: Йтиметься про SSO в Kubernetes. Для поціновувачів класики «Це ж було вже!», поясню: я читав багато різних статей про SSO в Kubernetes, але всі вони або уривчасті, або неповні, або не для webUI. Повної статті простою, а тим більше українською мовою — ще не бачив. Тому постараюсь в цій статті викласти те, як зробити «добре», щоб це було зрозуміло. Робитиму це на прикладі свого робочого стеку (оскільки ця задача вирішувалась у робочих цілях), в якому: Google OAuth (оскільки юзери у нас в Google Workspace), AWS EKS в якості Kubernetes (хоча зроблю доповнення для інших дистрибутивів), а також Dex та Headlamp.
Також додам, що це моя перша публікація взагалі хоч чого-небудь де-небудь, і до цього статей не писав і не публікував ніколи, тому конструктивна критика в коментарях вітається 🤗
Вступ
Багато хто з тих, хто працює з Kubernetes (далі для спрощення — k8s), використовує, або хоча б раз користувався Kuberentes Dashboard. Якщо хтось все-таки пропустив — це такий WebUI для k8s. Він один з перших WebUI (та і UI вцілому), розробляється під егідою CNCF, є проєктом kubernetes-sigs, і для більшості є стандартом в наборі тулів для k8s.
І наче все з ним непогано, якби не кілька АЛЕ (якби не ці але, цієї статті і не було б):
- у ньому вже не вистачає функціоналу (як мінімум, просто перше, що можу привести як приклад — мультиселект);
- досить повільно розвивається: нові версії виходять не так часто, як хотілося б;
- він дуже сильно відстав від UI-конкурентів: Lens (який вже теж перетворився на бозна що), FreeLens, Headlamp (про який сьогодні і поговоримо), Rancher (трошки про інше, але все ж), k9s (консольний UI), і ще багато різних, менш відомих (як kubewise, kubernetic, kubevious, etc);
- не можна адекватно прикрутити SSO, щоб це працювало як нормальний сайт, а не якісь костилі.
Для початку розберімось, що таке і для чого Dex і Headlamp, і чому саме він.
Dex — це OpenID Connect (OIDC) провайдер, який може бути як посередником (брати перелік користувачів та групи з іншого OIDC-провайдера, як-от Google у нашому випадку), так і самостійним (користувачі і групи, до яких вони належать описуються в самому Dex’і).
— Але ж якщо Dex — OIDC-провайдер, хіба не можна використовувати Google OAuth (який теж є OIDC-провайдером) напряму?
В цілому можна, але є одне «але»: Google ID Token (який видається після аутентифікації), містить лише базову інформацію про користувача, таку як sub (унікальний ідентифікатор користувача), email (електронна адреса), name та picture. Для повноцінної роботи, нам не вистачає ще поля groups — переліку груп, до яких належить користувач в Google Workspace. Перелік груп можна дістати лише через API, використовуючи Service Account, що виходить за рамки OIDC. І от саме цю задачу вирішуватиме Dex: на «вході» (коли Dex є клієнтом для Google OIDC) братиме користувачів по Google OIDC, перевірятиме перелік груп, до яких належить той чи інший користувач по Google API за допомогою Service Account’у, і віддаватиме на «виході» (коли Dex є провайдером OIDC) все разом вже тільки по OIDC. Плюс до того, на «вхід» йому можна додати не тільки Google, але й ще декілька інших провайдерів (Microsoft, GitLab, GitHub — повний список у них на сайті, в документації), а на «виході» це все матиме вигляд єдиного OIDC-провайдера.
Headlamp — сучасний, функціональний, швидкий UI (у них є як веб, так і десктопна версії), має систему плагінів, підтримує SSO (що для нас ключове), активно розвивається, нещодавно переїхав до Kubernets SIG (що є добрим знаком: визнання спільнотою і надія на довготривалий розвиток), є проєктом CNCF Sandbox, основні мейнтейнри — люди з Microsoft.
Що ж, зі вступною частиною наче закінчили, тепер нарешті до справи.
Google OAuth + Service Account
Для початку необхідно створити проєкт в Google Cloud Platform.

Далі слід пройти цілий ряд кроків, щоб зареєструвати застосунок, через який користувачі будуть аутентифікуватись на нашому сайті.
Реєстрація застосунку: вирушаємо на сторінку APIs & Services. OAuth consent screen та заповнюємо форму, як на скрінах:


Обираємо один з режимів роботи екрану:
- Internal: аутентифікація буде можлива тільки для користувачів вашої організації. Тобто це якийсь корпоративний варіант, без верифікації.
- External: аутентифікація буде доступна для всіх користувачів, включаючи тестових. Для викочування на продакшн потрібно пройти верифікацію.
У нашому випадку вибираємо пункт External.
В Contant information вкажіть довільний email (можна свій).
Після натискання на кнопку «Create» переходимо до налаштування прав (scopes), щоб ми могли запитувати відповідні дані. Переходимо в розділ Data Access, натискаємо кнопку «Add or remove scopes» додаємо .../auth/userinfo.email, .../auth/userinfo.profile та profile. Натискаємо «Save».

Нарешті можемо знегерувати ключі доступу.
Тут заходимо в APIs & Services. Credentials і натискаємо на кнопку «Create credentials» і вибираємо пункт «OAuth client ID», щоб згенерувати новий ключ доступу. Заповнюємо форму: Тип програми: Web application і Name (k8s-sso, або власну за бажанням). В полі Authorized JavaScript origins пишемо dex.example.com, а в полі Authorized redirect URIs — dex.example.com/callback

Далі тиснемо на кнопку «Create» і отримуємо ClientID та ClientSecret. Зберігаємо інформацію, адже вона буде необхідна в наших наступних кроках.
Як я писав вище, цього нам недостатньо, тому потрібно створити ще Service account. На тій самій сторінці, знову тиснемо на кнопку «Create credentials» і вибираємо пункт Service account. Тут заповнюємо поле Service account name, решта полів заповнювати не потрібно, натискаємо «Done».
Примітка: більш детальні налаштування Service account і прав доступу виходять за рамки цієї статті. З ними можна ознайомитись на сторінках документації або просто пошуком в гуглі. Нас же цікавить тільки SSO.
Далі натискаємо «Keys», «Add key», «Create new key», «JSON», «Create». Зберігаємо файл. Він нам буде потрібен разом з ClientID та ClientSecret.

Повертаємось на вкладку «Details», тицяємо «Advanced settings» => копіюємо «Client ID» => «View Google Admin Console»

Тут переходимо в «Security» => «Access and data control» > «API controls» => «Manage domain wide delegations» => «Add new»
Client ID — ID, який щойно скопіювали.
В OAuth scopes додаємо:

З налаштуванням Google покінчили.
Інсталювання Dex
Для початку, створимо Namespace та необхідні сікрети в ньому:
kubectl create ns dex kubectl create secret generic google-service-account \ --from-file=service_account.json=**path_to_your_local_file.json** \ --namespace=dex kubectl create secret generic google-oauth \ --from-literal=client-id='**ВАШ_CLIENT_ID_РЯДОК**' \ --from-literal=client-secret='**ВАШ_CLIENT_SECRET_РЯДОК**' \ --namespace=dex
Підготовку виконали, тепер можна встановлювати.
Версія helm-чарту на момент написання статті — 0.24.0
Створимо dex-values.yaml файл:
config: issuer: https://dex.example.com connectors: - type: google id: google name: Google config: clientID: $GOOGLE_CLIENT_ID clientSecret: $GOOGLE_CLIENT_SECRET redirectURI: https://dex.example.com/callback hostedDomains: - example.com serviceAccountFilePath: /etc/dex/google-sa/service_account.json adminEmail: [email protected] staticClients: - name: "headlamp" id: headlamp redirectURIs: - https://headlamp.example.com/oidc-callback secret: 1234567890qwerty volumes: - name: google-sa secret: secretName: google-service-account volumeMounts: - name: google-sa mountPath: /etc/dex/google-sa readOnly: true envVars: - name: GOOGLE_CLIENT_ID valueFrom: secretKeyRef: name: google-oauth key: client-id - name: GOOGLE_CLIENT_SECRET valueFrom: secretKeyRef: name: google-oauth key: client-secret ingress: enabled: true className: "nginx-ingress" hosts: - host: dex.example.com paths: - path: / pathType: ImplementationSpecific tls: - secretName: example-com-ssl hosts: - dex.example.com
Пояснення:
Блок connectors — тут задаються OIDC-провайдери, до яких підключається сам Dex. Як вже казав, можна одночасно підключати декілька різних.
- clientID та clientSecret — беруться зі змінних оточення, які беруться з сікрету, який ми створили раніше.
- hostedDomains — дозволені домени пошт користувачів, які можна авторизовувати через цей провайдер.
- serviceAccountFilePath — шлях до json-файлу від service account. Він у нас підключається також з сікрету, створеного раніше.
Блок staticClients — тут задаються OIDC-клієнти, в нашому випадку це Headlamp.
- name — назва додатку (клієнта). Може бути довільною.
- id — ідентифікатор клієнта. Аналогічно до ClientID з провайдера.
- redirectURIs — куди перенаправляти після авторизації в Dex.
- secret — «пароль» від застосунку. Краще згенерувати щось більш сек’юрне, ніж в прикладі. Аналогічний до ClientSecret з провайдера. Важлива ремарка: якщо ви захочете його задати зі змінної оточення (яка буде братись з сікрету k8s), то аналогічно до clientID та clientSecret не вийде. Потрібно замість параметру secret використовувати secretEnv: ENV_NAME (саме так, без знаку $). Чому так — а от просто тому що так захотілось мейнтейнерам helm-чарту Dex. Можливо були причини, але зроблено неочевидно 🤷♂️
Блоки volumes та volumeMounts — стандартні блоки монтування сікретів в контейнер в k8s. Тут ми монтуємо наш service_account.json в под Dex. Важливо, щоб ви змонтували сікрет за тим же шляхом, що вказали в serviceAccountFilePath.
Блок envVars — також стандартні налаштування для k8s. Тут ми задаємо змінні оточення GOOGLE_CLIENT_ID та GOOGLE_CLIENT_SECRET із сікрету.
Блок ingress — стандартні налаштування ingress для застосунку. Єдине, на скільки я знаю, Google вимагає, щоб OIDC-клієнти (Dex у нашому випадку) не на localhost — використовували https, тому доведеться потурбуватись про актуальний SSL-сертифікат (з цим добре допоможе cert-manager).
Встановлюємо dex:
helm repo add dex https://charts.dexidp.io helm repo update helm install dex —namespace dex -f dex-values.yaml --wait dex/dex
Перевіряємо, що Dex запустився:
% kubectl get po --namespace dex NAME READY STATUS RESTARTS AGE dex-d9g7gkiu5-nprtq 1/1 Running 0 23s
Інсталювання Headlamp
Важливо: Headlamp встановлюється в namespace kube-system. Це обумовлено доступами RBAC, які необхідні для авторизації інших користувачів. Заглиблюватись в це не буду, при бажанні, запитайте у «всезнайок»-GPT.
Версія helm-чарту на момент написання статті — 0.38.0.
Створимо headlamp-values.yaml
config: oidc: clientID: "headlamp" clientSecret: "1234567890qwerty" issuerURL: "https://dex.example.com" scopes: "openid email profile groups" callbackURL: "https://headlamp.example.com/oidc-callback" validatorClientID: "headlamp" validatorIssuerURL: "https://dex.example.com" ingress: enabled: true ingressClassName: "nginx-ingress" hosts: - host: headlamp.example.com paths: - path: / type: ImplementationSpecific tls: - secretName: example-com-ssl hosts: - headlamp.example.com
Пояснення:
Блок config.oidc — тут задаються OIDC-провайдери, до яких підключається вже Headlamp, у нашому випадку, це наш Dex.
- clientID — id в Dex;
- clientSecret — secret (або secretEnv) в Dex;
- issuerURL — адреса Dex, бо він для Headlamp виступає правйдером (issuer’ом);
- scopes — тут ми перелічуємо, які поля OIDC нам потрібні від провайдера (Dex) в автентифікаційному токені;
- callbackURL — куди перенаправити після автентифікації в Dex.
validatorClientID та validatorIssuerURL — зазвичай дорівнюють clientID та issuerURL. Необхідні для валідації виданих токенів, що отриманий токен дійсний, і відповідає зстосунку (клієнту), для якого він призначений (а не для якогось іншого).
Блок ingress — стандартні налаштування ingress для додатку.
Встановлюємо Headlamp:
helm repo add headlamp [https://kubernetes-sigs.github.io/headlamp/](<https://kubernetes-sigs.github.io/headlamp/>) helm repo update helm install headlamp headlamp/headlamp --namespace kube-system -f headlamp-values.yaml
Перевіряємо:
% kubectl get po --namespace kube-system \| grep headlamp headlamp-595b67jkh6a-pgvnr 1/1 Running 0 28s
Налаштування k8s
Перш за все, створимо тестовий namespace та необхідні для роботи RBAC
kubectl create ns test
Файл rbac.yaml
# Створює Кластерну Роль, яка надає мінімальні, лише для читання, дозволи на рівні кластера, необхідні для роботи інтерфейсу (наприклад, для перегляду Node'ів, Health'у кластера та отримання ClusterRole'ів для RBAC-перевірок). apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: sso-users rules: - apiGroups: [""] resources: ["nodes", "namespaces", "persistentvolumes"] verbs: ["get", "list", "watch"] - apiGroups: ["rbac.authorization.k8s.io"] resources: ["clusterroles", "clusterrolebindings"] verbs: ["get", "list", "watch"] - apiGroups: ["apiextensions.k8s.io"] resources: ["customresourcedefinitions"] verbs: ["get", "list", "watch"] - apiGroups: ["storage.k8s.io"] resources: ["storageclasses"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: sso-users roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: sso-users subjects: - kind: Group name: [email protected] # <--- Ключовий момент - група в Google Workspace, до якої повинен належати користувач. apiGroup: rbac.authorization.k8s.io --- # Створює Роль із повним доступом (*) до всіх ресурсів у конкретному просторі імен test apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: sso-users-test-ns namespace: test rules: - apiGroups: ["*"] resources: ["*"] verbs: ["*"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: sso-users-test-ns namespace: test roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: sso-users-test-ns # Роль з повним доступом у 'test' subjects: - kind: Group name: [email protected] # Ваша OIDC-група в Google Workspace apiGroup: rbac.authorization.k8s.io ---
Якщо потрібно надати повні права на кластер, просто додаємо ClusterRoleBinding до вже наявної ClusterRole — cluster-admin
kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: full-access roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-admin subjects: - kind: Group apiGroup: rbac.authorization.k8s.io name: [email protected]
Застосовуємо файл:
kubectl apply -f rbac.yaml
Тепер налаштовуємо OIDC провайдера в k8s.
Я працюю з EKS, то ж дам налаштування для нього. Переходимо в консоль => EKS => ваш кластер => Access => гортаємо вниз до OIDC identity providers => Associate. Заповнюємо, як на скріні, натискаємо «Associate». Процес асоціації займе трошки часу, бо під капотом перезапускається kube-api-server з новими параметрами.

Нижче додам варіанти для kubeadm, microk8s та minikube дистрибуцій кубера:
Kubeadm:
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
У розділі spec.containers[].command (або spec.containers[].args) знайдіть список аргументів і додайте необхідні прапори, що стосуються OIDC.
… --oidc-issuer-url=https://dex.example.com --oidc-client-id=headlamp --oidc-username-claim=email --oidc-groups-claim=groups # Додатковий префікс для імені користувача (опціонально) --oidc-username-prefix=oidc: …
microk8s
sudo vi /var/snap/microk8s/current/args/kube-apiserver
Просто додайте необхідні OIDC прапори в окремих рядках до кінця файлу:
--oidc-issuer-url=https://dex.example.com --oidc-client-id=headlamp --oidc-username-claim=email --oidc-groups-claim=groups
Після чого перезапустіть microk8s:
sudo snap restart microk8s
minikube
minikube start \ --extra-config=apiserver.authorization-mode=RBAC \ --extra-config=apiserver.oidc-issuer-url=https://dex.example.com \ --extra-config=apiserver.oidc-client-id=headlamp \ --extra-config=apiserver.oidc-username-claim=email \ --extra-config=apiserver.oidc-groups-claim=groups
Нарешті, перевіряємо🎉
Переходимо в headlamp.example.com, логінимось під користувачем в групі [email protected], і якщо все добре, отримуємо доступ тільки до неймспейсу, який ми дозволили. Якщо залогінитись під користувачем в групі [email protected] — отримаємо доступ до всього кластеру.



Висновок
Дуже сподіваюсь, цей лонгрід стане комусь в нагоді 🤗
Чекаю вас усіх в коментарях.
Дякую за увагу 👋
4 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів