Перейти к содержимому
HoginHogin
Назад

Keycloak в Kubernetes: один OIDC для kubectl, Argo CD и Grafana — SSO на своём железе

9 мин чтения

Посчитайте формы логина в своём внутреннем хостинге: 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, потому что «так быстрее».

Каждая точка — отдельная поверхность аутентификации со своими правилами:

Пять админок с отдельными паролями против одного Keycloak как единственной точки входа

Проблема не в том, что «слабые пароли», а в том, что поверхностей аутентификации пять — и каждая требует своей эксплуатации.

Что меняет один OIDC-провайдер

Keycloak — open source IAM-сервер: realm (изолированный домен пользователей), клиенты (приложения), группы и роли, встроенные TOTP/WebAuthn, аудит-события. Всё, что умеет OIDC, подключается к нему по стандартному flows — authorization code + PKCE.

Что это даёт конкретно:

Поток OIDC: приложение редиректит на Keycloak, токен с groups возвращается, приложение маппит роль

Важно: OIDC не заменяет RBAC. Приложение (или apiserver) по-прежнему решает, что можно роли admins — Keycloak отвечает только на «кто это».

Зоопарк против Keycloak

Basic Auth + локальные учёткиDex + внешний IdPKeycloak
Хранение пользователейнигде / в ConfigMapнет своих, нужен upstreamrealm: пользователи, группы, политики
Пароль-политика и ротациянетзависит от upstreamвстроенная
MFA (TOTP/WebAuthn)нетнет (или внешний)встроенная
kubectl без client-сертификатовнетдада
Отключить одного человекаN операций1 (в upstream)1
Аудит входовнетчастичноevents в realm
Компоненты в кластере01 маленький + LDAP/IdP1 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" }
  ]
}

Realm в git: PR → CI применяет через Admin API, секреты приходят из External Secrets

Цена вопроса: 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

Итог

Один Keycloak на всю внутреннюю панель — меньше поверхностей аутентификации, чем у любой связки из Basic Auth и dex-прокладок: один провайдер отвечает «кто это», kubectl/Argo CD/Grafana маппят одну и ту же группу admins на свои роли, MFA и аудит включаются в одном месте, а realm живёт в git. Цена — два пода JVM и честный Postgres под ними, ~2–3 ГБ RAM: это дешёвая страховка от «пароль от Grafana лежал в ConfigMap». Если Postgres под Keycloak вы ещё не поднимали — стартовая схема на CloudNativePG разобрана здесь.


Поделиться:

Следующая статья
VictoriaMetrics вместо Prometheus+Thanos: один бинарь вместо трёх — когда это честная экономия