Посчитайте формы логина в своём внутреннем хостинге: kubectl — client-сертификаты, Argo CD — локальный admin, Grafana — собственная база, у каждого второго сервиса — Basic Auth из ConfigMap. Отдельные пароли, отдельные «забыл пароль», MFA нет нигде. А повод разобрать это сейчас — релиз Keycloak 26.8.1 от 13 октября 2026: он закрыл 23 CVE, включая утечку маскированных клиентских секретов через Admin REST API (CVE-2026-15943) и подделку logout-токенов (CVE-2026-18569). Свежая линейка 26.x активно развивается — в 26.7 появился новый REST API регистрации клиентов — так что это хороший момент подняться на актуальную версию и заодно свести весь зоопарк к одному OIDC-провайдеру.
Содержание
Открыть содержание
Пять админок — пять паролей
Зоопарк растёт незаметно. Сначала кластер — доступ через клиентский сертификат, выданный на год и непонятно кому скопированный. Потом Argo CD со встроенным admin и паролем в secret. Потом Grafana, где у каждого дашборд-зрителя своя учётка в SQLite. Промежуточные сервисы получают Basic Auth, потому что «так быстрее».
Каждая точка — отдельная поверхность аутентификации со своими правилами:
- пароли живут где попало — в secret, в ConfigMap, в чьей-то голове; политика сложности и ротации — нигде;
- отключить уволенного — N отдельных операций, одну забудешь и не заметишь;
- MFA нет — клиентский сертификат на ноутбуке и пароль
admin/adminне считаются; - аудита нет — кто вошёл в Grafana в субботу ночью, неизвестно;
- onboarding нового админа — час выдачи доступов в пяти местах.
Проблема не в том, что «слабые пароли», а в том, что поверхностей аутентификации пять — и каждая требует своей эксплуатации.
Что меняет один OIDC-провайдер
Keycloak — open source IAM-сервер: realm (изолированный домен пользователей), клиенты (приложения), группы и роли, встроенные TOTP/WebAuthn, аудит-события. Всё, что умеет OIDC, подключается к нему по стандартному flows — authorization code + PKCE.
Что это даёт конкретно:
- один логин — браузер, kubectl, Argo CD и Grafana аутентифицируют у одного провайдера: залогинился один раз — сессия общая;
- группы вместо раздачи доступов руками — realm-группа
adminsприезжает в токен claim-омgroups, а приложения маппят её на свои роли:cluster-adminу apiserver,role:adminу Argo CD,Adminу Grafana; - MFA в одном месте — включили TOTP/WebAuthn один раз для realm — он действует везде;
- аудит —
kc-eventsпишут, кто и когда вошёл; отключение пользователя — одна операция, все приложения сразу отвергают его токены; - kube-apiserver тоже — Kubernetes нативно умеет OIDC: флаги
--oidc-issuer-url/--oidc-client-id, иkubectlходит через тот же Keycloak.
Важно: OIDC не заменяет RBAC. Приложение (или apiserver) по-прежнему решает, что можно роли admins — Keycloak отвечает только на «кто это».
Зоопарк против Keycloak
| Basic Auth + локальные учётки | Dex + внешний IdP | Keycloak | |
|---|---|---|---|
| Хранение пользователей | нигде / в ConfigMap | нет своих, нужен upstream | realm: пользователи, группы, политики |
| Пароль-политика и ротация | нет | зависит от upstream | встроенная |
| MFA (TOTP/WebAuthn) | нет | нет (или внешний) | встроенная |
| kubectl без client-сертификатов | нет | да | да |
| Отключить одного человека | N операций | 1 (в upstream) | 1 |
| Аудит входов | нет | частично | events в realm |
| Компоненты в кластере | 0 | 1 маленький + LDAP/IdP | 1 JVM + Postgres |
| RAM на стек | ~0 | ~100 МБ + upstream | ~1–2 ГБ |
Dex честен как разъем «OIDC спереди — LDAP/Google/GitHub сзади», но своих пользователей не хранит: пароли и MFA всё равно живут где-то ещё. Keycloak закрывает всю цепочку сам.
Что нужно, чтобы поднять SSO на своём кластере
Исходные данные: свой кластер (kubeadm/Cluster-API/виртуалки — где можно править флаги kube-apiserver), ingress с TLS, домен sso.example.com. Работаем в namespace keycloak, образ — quay.io/keycloak/keycloak:26.8.1.
1. Postgres на CloudNativePG. Keycloak в prod-режиме требует внешнюю базу; dev-режим с H2 для продакшена не годится. Если CloudNativePG ещё не стоит — разбирали его отдельно; для SSO хватит двух инстансов:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: keycloak-db
namespace: keycloak
spec:
instances: 2
storage:
size: 10Gi
bootstrap:
initdb:
database: keycloak
owner: keycloak
2. Keycloak через helm-чарт. Основные values (секреты — из External Secrets, как здесь):
production: true # start --optimized, строгий hostname
replicaCount: 2
image:
tag: "26.8.1"
auth:
adminUser: admin
existingSecret: keycloak-admin
externalDatabase:
host: keycloak-db-rw.keycloak.svc.cluster.local
port: 5432
database: keycloak
user: keycloak
existingSecret: keycloak-db-app
existingSecretPasswordKey: password
postgresql:
enabled: false # база — CNPG, не вложенный чарт
proxy: edge # TLS терминируется на ingress
ingress:
enabled: true
hostname: sso.example.com
ingressClassName: nginx
tls: true
helm upgrade --install keycloak oci://registry-1.io/bitnami/keycloak \
--namespace keycloak --create-namespace -f values.yaml
После старта создайте realm homelab и включите у клиентов встроенный client scope groups — без него claim groups в токен не попадёт, и маппинг ролей молча не заработает. Это классическая ловушка.
3. kube-apiserver. Флаги на control plane (kubeadm — /etc/kubernetes/manifests/kube-apiserver.yaml):
- --oidc-issuer-url=https://sso.example.com/realms/homelab
- --oidc-client-id=kubectl
- --oidc-username-claim=preferred_username
- --oidc-username-prefix=oidc:
- --oidc-groups-claim=groups
- --oidc-groups-prefix=oidc:
Префиксы oidc: не дают подделать имя группы system:masters именем группы из realm. apiserver должен достучаться до issuer по TLS с доверенным сертификатом; для приватного CA добавьте --oidc-ca-file. На managed-кластерах (EKS/GKE) эти флаги напрямую недоступны — у EKS есть свой API associate-identity-provider-config, но статья про своё железо. Отметим: классические --oidc-* флаги в maintenance mode, новый путь — structured authentication (--authentication-config с JWT authenticator), поля там те же.
Клиент в Keycloak: kubectl, public, redirect URI http://localhost:8000/* — туда возвращается kubelogin.
4. kubeconfig с kubelogin. Плагин открывает браузер, вы логинитесь в Keycloak (с MFA), токен обновляется сам:
kubectl krew install oidc-login
# kubeconfig-фрагмент
users:
- name: oidc
user:
exec:
apiVersion: client.authentication.k8s.io/v1
command: kubectl
interactiveMode: IfAvailable
args:
- oidc-login
- gettoken
- --oidc-issuer-url=https://sso.example.com/realms/homelab
- --oidc-client-id=kubectl
- --oidc-extra-scope=email
- --oidc-extra-scope=groups
Роль — через привычный RBAC, субъект — группа с префиксом:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-admins
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: Group
name: oidc:admins
apiGroup: rbac.authorization.k8s.io
5. Argo CD. В values чарта argo/argo-cd:
configs:
cm:
oidc.config: |
name: Keycloak
issuer: https://sso.example.com/realms/homelab
clientID: argocd
clientSecret: $oidc.keycloak.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]
secret:
extra:
oidc.keycloak.clientSecret: "<из Keycloak>"
rbac:
policy.csv: |
g, admins, role:admin
g, viewers, role:readonly
Строка g, admins, role:admin берёт группу из claim groups — те же realm-группы, что у kubectl.
6. Grafana. Клиент grafana в Keycloak делаем confidential (client id + secret). Секция grafana.ini:
[auth.generic_oauth]
enabled = true
name = Keycloak
client_id = grafana
client_secret = $__env{GRAFANA_CLIENT_SECRET}
scopes = openid profile email groups
auth_url = https://sso.example.com/realms/homelab/protocol/openid-connect/auth
token_url = https://sso.example.com/realms/homelab/protocol/openid-connect/token
api_url = https://sso.example.com/realms/homelab/protocol/openid-connect/userinfo
role_attribute_path = contains(groups[*], 'admins') && 'Admin' || 'Viewer'
allow_sign_up = true
Одна и та же группа admins даёт cluster-admin в kubectl и Admin в Grafana — потому что источник один.
Realm как код
Настройки realm, созданные руками в админке, — такой же drift, как kubectl edit. Лечится экспортом:
kubectl -n keycloak exec keycloak-0 -- \
/opt/keycloak/bin/kc.sh export --realm homelab \
--dir /tmp/export --users realm_file
Файл кладётся в git рядом с helm values; применяем changes через PR: первичный bootstrap — монтирование ConfigMap в /opt/keycloak/data/import и старт с --import-realm, далее в CI — partial import через Admin REST API (policy SKIP/OVERWRITE), без рестарта подов. Секреты из экспорта вырезаем и держим в External Secrets — в git только их ссылки. Ядро файла выглядит так:
{
"realm": "homelab",
"enabled": true,
"clients": [
{
"clientId": "kubectl",
"publicClient": true,
"redirectUris": ["http://localhost:8000/*"],
"defaultClientScopes": ["profile", "email", "groups"]
},
{
"clientId": "argocd",
"secret": "${ARGOCD_CLIENT_SECRET}",
"redirectUris": ["https://argocd.example.com/auth/callback"],
"defaultClientScopes": ["profile", "email", "groups"]
},
{
"clientId": "grafana",
"secret": "${GRAFANA_CLIENT_SECRET}",
"redirectUris": ["https://grafana.example.com/login/generic_oauth"],
"defaultClientScopes": ["profile", "email", "groups"]
}
],
"groups": [
{ "name": "admins", "path": "/admins" },
{ "name": "viewers", "path": "/viewers" }
]
}
Цена вопроса: JVM и память
Честный счёт: Keycloak — это JVM. Реплика с heap ~512 МиБ на realm из десятков пользователей и сотни клиентов — норма; закладывайте запрос ~768 МиБ и лимит ~1.5 ГиБ на под, две реплики за availability. Плюс два пода Postgres от CNPG по ~256 МиБ. Итого весь SSO-стек — порядка 2–3 ГБ RAM.
Почему это всё равно дешевле зоопарка: Basic Auth стоит ноль RAM, но каждая учётка, ротация и «забыл пароль» — ручная операция, и у вас их пять систем подряд. Dex сам по себе ~100 МБ, но пользователей не хранит — нужен upstream: Google/GitHub (данные ушли наружу) или self-hosted LDAP, а это уже OpenLDAP, его эксплуатация и отдельный механизм MFA. Один Keycloak закрывает directory, полисики паролей, TOTP/WebAuthn, брокеринг к внешним IdP и аудит — одним бинарником с одной схемой обновления.
Как проверить, что всё работает
curl -s https://sso.example.com/realms/homelab/.well-known/openid-configuration | jq -r .issuer
kubectl auth whoami
- apiserver:
kubectl auth whoamiпосле логина через kubelogin покажетUsername: oidc:aliceиGroups: [oidc:admins …]— значит, claim-ы и префиксы на месте; - Argo CD: логин через «Login via Keycloak», у пользователя из
viewersне должно быть кнопки Sync; - Grafana: после входа в профиле пользователя виден
Admin/Viewerизrole_attribute_path; - токен:
kubectl oidc-login gettoken --oidc-issuer-url=… --oidc-client-id=kubectlи декодируйте payload на jwt.io — там должен бытьgroups: ["admins"]. Еслиgroupsнет — вернитесь к пункту про client scopegroups.
Итог
Один Keycloak на всю внутреннюю панель — меньше поверхностей аутентификации, чем у любой связки из Basic Auth и dex-прокладок: один провайдер отвечает «кто это», kubectl/Argo CD/Grafana маппят одну и ту же группу admins на свои роли, MFA и аудит включаются в одном месте, а realm живёт в git. Цена — два пода JVM и честный Postgres под ними, ~2–3 ГБ RAM: это дешёвая страховка от «пароль от Grafana лежал в ConfigMap». Если Postgres под Keycloak вы ещё не поднимали — стартовая схема на CloudNativePG разобрана здесь.