LDAP + Kubernetes через Dex: OIDC без облака

Связываем корпоративный LDAP и OIDC с помощью Dex

Стек: Kubernetes · Dex v2.44.0 · OpenLDAP · Helm | Руководство по настройке производственного уровня

Обзорная схема интеграции OpenLDAP, Dex и Kubernetes

Проверка реальностью

Вы только что запустили свой первый производственный кластер Kubernetes. На следующей неделе — проверка безопасности. И тут команда инфраструктуры пишет в Slack: «У нас уже 5 000 пользователей в LDAP — просто подключи Kubernetes к нему».

Просто, правда?

Только вот Kubernetes не умеет говорить на LDAP. Он говорит на OIDC. И теперь именно вам предстоит построить мост между ними.

Это происходит в каждой средней и крупной организации, которая переносит рабочие нагрузки на Kubernetes, сохраняя при этом существующий каталог удостоверений на собственных серверах. Управляемые облачные провайдеры идентификации (IdP), такие как Okta или Azure AD, решают задачу чисто — но только если вы имеете право их использовать. Изолированные среды (air-gapped), регулируемые отрасли и команды с ограниченным бюджетом нередко лишены этой возможности.

Поэтому приходится строить самим.

Мы свяжем воедино три компонента: OpenLDAP (каталог пользователей), Dex (транслятор аутентификации) и Kubernetes API Server (привередливый потребитель токенов удостоверений). Каждая ошибка, каждая неверная конфигурация и каждый момент «почему это вообще не работает» задокументированы здесь — потому что я лично натолкнулся на всё это.

Архитектурный замысел

Прежде чем касаться хоть одной команды Helm, нужно сформировать правильную мысленную модель. Она важнее YAML-файлов.

OpenLDAP — это ваша база данных пользователей. Он знает, кто такие ваши люди, в каких группах они состоят, и умеет проверять пароли через операцию bind. Но одного он не умеет категорически — выпускать криптографически подписанные токены, которым доверяет Kubernetes.

Dex — транслятор аутентификации. Он находится между LDAP и Kubernetes: принимает учётные данные пользователя, проверяет их в LDAP и выдаёт токен OIDC (JWT), которому Kubernetes настроен доверять. Думайте о Dex как о паспортном столе: LDAP проверяет ваши документы, Dex ставит штамп в паспорт.

Kubernetes API Server — это придирчивый пограничный контроль. Он принимает только паспорта от тех издателей, которым его явно велено доверять, и проверяет криптографическую подпись на каждом токене.

2a. Цепочка доверия — начинаем с сертификатов

Большинство людей небрежно пропускают этот шаг. Не делайте так. Ошибки здесь порождают ошибки TLS, которые выглядят как баги приложения — и их крайне сложно отлаживать, не понимая лежащей в основе цепочки.

API-сервер Kubernetes должен доверять TLS-сертификату Dex. Вы устанавливаете это доверие, подписывая сертификат Dex своим CA под вашим контролем, а затем указывая API-серверу на этот CA. Ниже — полная последовательность генерации сертификатов.

Шаг 1 — Генерация ключевой пары CA:

openssl genrsa -out ca.key 2048
openssl req -x509 -new -noenc -key ca.key -subj "/CN=dex-ca.crt" -out ca.crt

Шаг 2 — Генерация ключа сервера Dex и запроса на подпись сертификата (CSR):

openssl genrsa -out dex.key 2048
openssl req -new -key dex.key -out dex.csr -subj "/CN=dex.ldap.com"

Шаг 3 — Создание файла расширений SAN (dex.ext). Это обязательно для современного TLS: без полей Subject Alternative Names браузеры и TLS-библиотека Go отклонят сертификат:

subjectAltName = @alt_names

[ alt_names ]
DNS.1 = dex
DNS.2 = dex.ldap.com
DNS.3 = dex.dex.svc
DNS.4 = dex.dex.svc.cluster.local
IP.1 = 10.0.0.191

Шаг 4 — Подпись сертификата вашим CA:

openssl x509 -req -in dex.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out dex.crt -days 10000 \
  -extfile dex.ext -sha256

ВНИМАНИЕ: CN в вашем CSR (/CN=dex.ldap.com) и DNS-записи в файле SAN должны точно совпадать с URL издателя Dex. Только это несоответствие является причиной значительной доли ошибок TLS, с которыми вы столкнётесь впоследствии.

Шаг 5 — Копирование сертификатов в постоянное расположение на хосте:

sudo mkdir -p /etc/dex/tls
sudo cp ca.crt dex.crt

Шаг 6 — Добавление CA в системное хранилище доверия:

Этот шаг критичен и часто пропускается. Процессу kube-apiserver нужно доверять CA при получении JWKS-ключей от Dex по HTTPS. Добавление в системное хранилище закрывает эту потребность:

sudo cp /etc/dex/tls/ca.crt /usr/local/share/ca-certificates/dex-ca.crt
sudo update-ca-certificates

Вы должны увидеть вывод вида:

1 added, 0 removed; done.

Без этого шага kube-apiserver не пройдёт TLS-проверку при обращении к JWKS-эндпоинту Dex, и каждый OIDC-токен будет молча отклоняться с обобщённой ошибкой Unauthorized.

Ловушка с именами файлов в Kubernetes Secret

Получив сертификаты, вы должны передать их в pod Dex через Kubernetes Secret. Именно здесь подстерегает очень распространённая и неочевидная ошибка.

Инстинктивно рука тянется к типу секрета tls:

# НЕ ИСПОЛЬЗУЙТЕ ЭТО ДЛЯ DEX
kubectl create secret tls dex-tls-certs --cert=dex.crt --key=dex.key -n dex

Проблема: kubectl create secret tls хранит файлы внутри под именами tls.crt и tls.key. Helm-чарт Dex монтирует секрет и ожидает найти там dex.crt и dex.key — как прописано в values.yaml. Имена файлов не совпадают, pod не находит файлы сертификатов и падает с вводящей в заблуждение ошибкой «no such file or directory».

Вместо этого используйте тип generic с явным маппингом ключей в имена файлов:

kubectl create secret generic dex-tls-certs \
  --from-file=dex.crt=dex.crt \
  --from-file=dex.key=dex.key \
  -n dex

Сразу после создания проверьте:

kubectl describe secret dex-tls-certs -n dex

Вывод должен выглядеть именно так:

Data
====
dex.crt: xxxx bytes
dex.key: xxxx bytes

СОВЕТ: В Kubernetes имена ключей внутри секрета становятся именами файлов, которые появляются в pod по пути монтирования. Тип секрета и имена ключей должны точно соответствовать тому, что ожидает приложение. Одно это правило предотвратило бы три из пяти производственных ошибок, описанных в разделе 3.

Вывод терминала с командой describe, показывающей dex.crt и dex.key в секции Data

Вывод терминала команды describe — dex.crt и dex.key отображаются в разделе Data, подтверждая правильные имена ключей

2b. OpenLDAP на Kubernetes — основа каталога

Разворачиваем OpenLDAP с помощью Helm-чарта openldap-stack-ha, который поставляется вместе с phpLDAPadmin — веб-интерфейсом администратора:

ldap-values.yaml:

global:
  ldapDomain: ldap.com
  adminuser: admin
  adminPassword: adminpassword
  configUserEnabled: true

replicaCount: 1

persistence:
  enabled: false

env:
  LDAP_ORGANISATION: "LDAP CompanyX"
helm repo add helm-openldap https://jp-gouin.github.io/helm-openldap/

helm install openldap helm-openldap/openldap-stack-ha \
  -f ldap-values.yaml -n ldap --create-namespace
Терминал с выводом 'helm install openldap', STATUS: deployed и списком созданных pod-ов

Терминал с выводом helm install openldap, статус deployed и список созданных pod-ов

Проблема с TLS в phpLDAPadmin

После установки phpLDAPadmin может отказываться загружаться с ошибкой, связанной с TLS. В ConfigMap по умолчанию включён TLS для LDAP-клиента, однако в нашей схеме TLS терминируется на уровне Dex/сервиса, а не на phpLDAPadmin. Исправьте ConfigMap и перезапустите:

Сначала измените тип сервиса openldap-phpldapadmin на NodePort с nodePort: 32000:

apiVersion: v1
kind: Service
metadata:
  annotations:
    meta.helm.sh/release-name: openldap
    meta.helm.sh/release-namespace: ldap
  creationTimestamp: "2026-04-02T13:18:46Z"
  labels:
    app: phpldapadmin
    app.kubernetes.io/managed-by: Helm
    chart: phpldapadmin-0.1.2
    heritage: Helm
    release: openldap
  name: openldap-phpldapadmin
  namespace: ldap
  resourceVersion: "146101"
  uid: e4a9ecd7-e23e-4467-8c9c-db047442a590
spec:
  clusterIP: 10.103.42.114
  clusterIPs:
  - 10.103.42.114
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - name: http
    port: 80
    protocol: TCP
    nodePort: 32000
    targetPort: http
  selector:
    app: phpldapadmin
    release: openldap
  sessionAffinity: None
  type: NodePort
status:
  loadBalancer: {}

Ниже — эталонный файл phpldapadmin-cm.yaml:

apiVersion: v1
data:
  PHPLDAPADMIN_HTTPS: "false"
  PHPLDAPADMIN_LDAP_CLIENT_TLS_REQCERT: never
  PHPLDAPADMIN_LDAP_HOSTS: '#PYTHON2BASH:[{ ''openldap.ldap''  : [{''server'': [{''tls'':
    False},{''port'':389}]},{''login'': [{''bind_id'': ''cn=admin,dc=ldap,dc=com''  }]}]}]'
  PHPLDAPADMIN_TRUST_PROXY_SSL: "true"
kind: ConfigMap
metadata:
  annotations:
    meta.helm.sh/release-name: openldap
    meta.helm.sh/release-namespace: ldap
  creationTimestamp: "2026-04-02T13:18:46Z"
  labels:
    app: phpldapadmin
    app.kubernetes.io/managed-by: Helm
    chart: phpldapadmin-0.1.2
    heritage: Helm
    release: openldap
  name: openldap-phpldapadmin
  namespace: ldap
  resourceVersion: "146085"
  uid: 6b1e84ff-3b02-447d-967c-64ec9002446d
kubectl -n ldap get cm openldap-phpldapadmin -o yaml > openldap-phpldapadmin-cm.yaml

# Отредактируйте: установите PHPLDAPADMIN_LDAP_CLIENT_TLS в false

kubectl replace -f openldap-phpldapadmin-cm.yaml --force

kubectl rollout restart deployment -n ldap openldap-phpldapadmin
Браузер с формой входа phpLDAPadmin, доступной на порту 32000 после исправления TLS ConfigMap

Браузер показывает форму входа phpLDAPadmin, доступную на порту 32000 после исправления ConfigMap

Наполнение каталога через base.ldif

dn: dc=ldap,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o: LDAP CompanyX
dc: ldap

dn: ou=user,dc=ldap,dc=com
objectClass: organizationalUnit
ou: user

dn: ou=groups,dc=ldap,dc=com
objectClass: organizationalUnit
ou: groups

dn: cn=Belle Ruiz,ou=user,dc=ldap,dc=com
givenName: Belle
sn: Ruiz
cn: Belle Ruiz
uid: bruiz
mail: bell@gmail.com
userPassword: Password123
uidNumber: 1000
gidNumber: 502
homeDirectory: /home/users/bruiz
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: top

dn: cn=k8s-admins,ou=groups,dc=ldap,dc=com
cn: k8s-admins
gidNumber: 500
objectClass: posixGroup
objectClass: top

Создайте базовый LDIF-файл, описывающий организационную структуру, и загрузите его:

kubectl -n ldap cp base.ldif openldap-0:/tmp/base.ldif
kubectl -n ldap exec -it openldap-0 -- bash
ldapadd -x -H ldap://localhost:1389 \
  -D "cn=admin,dc=ldap,dc=com" \
  -w adminpassword \
  -f /tmp/base.ldif

Примечание: Флаг -w требует значения пароля сразу после себя — ldapadd …​ -w adminpassword. Если поставить -f раньше пароля, ldapadd тихо завершится с ошибкой и выведет справочный экран.

Добавление пользователей в группы LDAP

Этот шаг легко упустить, а потом часами гадать, в чём дело. Пользователь должен быть членом группы, чтобы атрибут groups появился в OIDC-токене. Без членства в группе политики RBAC, основанные на группах, будут тихо не срабатывать.

Создайте файл add-member.ldif:

dn: cn=k8s-admins,ou=groups,dc=ldap,dc=com
changetype: modify
add: memberUid
memberUid: bruiz

Примените его:

kubectl cp add-member.ldif ldap/openldap-0:/tmp/add-member.ldif
kubectl exec -it -n ldap openldap-0 -- \
  ldapmodify -x -H ldap://localhost:1389 \
  -D "cn=admin,dc=ldap,dc=com" \
  -w adminpassword \
  -f /tmp/add-member.ldif

Проверьте, что членство применено:

kubectl exec -it -n ldap openldap-0 -- \
  ldapsearch -x -H ldap://localhost:1389 \
  -D "cn=admin,dc=ldap,dc=com" \
  -w adminpassword \
  -b "cn=k8s-admins,ou=groups,dc=ldap,dc=com"

В выводе должна присутствовать строка memberUid: bruiz. Если пропустить этот шаг, JWT-токен будет иметь пустой атрибут groups, и ваши RBAC-привязки никогда не совпадут.

2c. Dex — транслятор аутентификации

Устанавливаем Dex через Helm:

helm repo add dex https://charts.dexidp.io
helm repo update
helm install dex dex/dex -f dex-values.yaml -n dex

dex-values.yaml:

https:
  enabled: true

config:
  issuer: https://dex.ldap.com:31000

  storage:
    type: kubernetes
    config:
      inCluster: true

  web:
    http: 0.0.0.0:5556
    https: 0.0.0.0:5554
    tlsCert: /etc/dex/tls/dex.crt
    tlsKey: /etc/dex/tls/dex.key

  connectors:
  - type: ldap
    id: ldap
    name: LDAP
    config:
      host: openldap.ldap.svc.cluster.local:389
      insecureNoSSL: true
      bindDN: cn=admin,dc=ldap,dc=com
      bindPW: adminpassword
      userSearch:
        baseDN: ou=user,dc=ldap,dc=com
        filter: "(objectClass=inetOrgPerson)"
        username: uid
        idAttr: uid
        emailAttr: mail
        nameAttr: cn
      groupSearch:
        baseDN: ou=groups,dc=ldap,dc=com
        filter: "(objectClass=posixGroup)"
        userMatchers:
        - userAttr: uid
          groupAttr: memberUid
        nameAttr: cn

  staticClients:
  - id: kubernetes
    redirectURIs:
    - 'http://localhost:8000/callback'
    name: 'Example App'
    secret: MySuperSecretPassword123

volumes:
- name: dex-tls
  secret:
    secretName: dex-tls-certs

volumeMounts:
- name: dex-tls
  mountPath: /etc/dex/tls
  readOnly: true

Файл dex-values.yaml содержит четыре раздела, которые нужно настроить правильно.

issuer: URL, который Dex проставляет в каждом выпускаемом JWT. Он должен быть доступен, совпадать с флагами API-сервера и соответствовать вашему TLS-сертификату. Его смена впоследствии ломает всё — относитесь к нему как к неизменяемому значению.

issuer: https://dex.ldap.com:31000

connectors: Сообщает Dex, как взаимодействовать с LDAP — хост, учётные данные для bind, базовый DN для поиска пользователей и маппинг LDAP-атрибутов на OIDC-клеймы (особенно атрибут groups).

staticClients: Определяет, какие приложения могут запрашивать токены от Dex. Kubernetes нужен клиент с id: kubernetes и корректным URL обратного вызова.

storage: В производственной среде используйте постоянный бэкенд (PostgreSQL или CockroachDB). Тип memory из примеров документации теряет всё состояние при перезапуске pod-а — в том числе активные сессии.

Открываем Dex через NodePort

Установка Helm по умолчанию создаёт сервис типа ClusterIP. Необходим NodePort, чтобы браузерный редирект мог достичь Dex извне кластера:

kubectl patch svc dex -n dex --type='json' -p='[
  {"op": "replace", "path": "/spec/type", "value": "NodePort"},
  {"op": "replace", "path": "/spec/ports/1/nodePort", "value": 31000}
]'

Проверьте сервис:

kubectl get svc -n dex

В выводе должна присутствовать строка 5554:31000/TCP.

Проверяем доступность Dex

Прежде чем двигаться дальше, убедитесь, что Dex отвечает корректно — как без проверки сертификата, так и с ней:

# Быстрая проверка (без верификации TLS)
curl -k https://dex.ldap.com:31000/.well-known/openid-configuration

# Полноценная проверка с вашим CA-сертификатом
curl --cacert /etc/dex/tls/ca.crt https://dex.ldap.com:31000/.well-known/openid-configuration

Обе команды должны вернуть JSON-документ с полем issuer, совпадающим с вашим URL. Если вторая команда завершается ошибкой — проблема с CA-сертификатом или разрешением имён. Исправьте это прежде чем продолжать: от этого зависит всё остальное.

Вывод 'kubectl get pods -n dex', показывающий pod dex в состоянии Running с READY 1/1

Вывод kubectl get pods -n dex — pod Dex в состоянии Running, READY 1/1

2d. Настройка DNS — CoreDNS и файл hosts

Именно здесь большинство руководств останавливается, не дойдя до конца. Нужно настроить два независимых DNS-пространства, и пропуск любого из них приводит к тихому сбою при проверке токенов.

Хост Linux — /etc/hosts

Машина Linux, на которой запускается kubectl, должна уметь разрешать dex.ldap.com:

echo "10.0.0.191 dex.ldap.com" | sudo tee -a /etc/hosts

Машина Windows — файл hosts

Если вы подключаетесь к кластеру с Windows, добавьте ту же запись. Запустите PowerShell от имени администратора:

Add-Content -Path "C:\Windows\System32\drivers\etc\hosts" -Value "10.0.0.191 dex.ldap.com"

CoreDNS — внутри кластера

Этот шаг почти всегда упускают. kube-apiserver получает публичные JWKS-ключи Dex с адреса https://dex.ldap.com:31000/keys для проверки подписей токенов. Даже если kube-apiserver работает с hostNetwork: true, за разрешение имён внутри сетевого стека кластера отвечает CoreDNS.

Если dex.ldap.com не прописан в CoreDNS, API-сервер тихо не сможет получить JWKS-ключи, и каждый OIDC-токен будет отклонён как invalid bearer token — без единого сообщения об ошибке, указывающего на DNS.

Отредактируйте ConfigMap CoreDNS:

kubectl edit configmap coredns -n kube-system

Добавьте блок hosts внутри секции .:53. Обратите внимание на пробел между hosts и { — он обязателен:

.:53 {
    errors
    health {
       lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
       ttl 30
    }
    hosts {
       10.0.0.191 dex.ldap.com
       fallthrough
    }
    prometheus :9153
    forward . /etc/resolv.conf {
       max_concurrent 1000
    }
    cache 30
    loop
    reload
    loadbalance
}

Перезапустите CoreDNS для применения изменений:

kubectl rollout restart deployment coredns -n kube-system
kubectl get pods -n kube-system | grep coredns

Проверьте разрешение имён изнутри кластера:

kubectl run dns-test --rm -it --image=busybox --restart=Never -- \
  nslookup dex.ldap.com

В выводе должна быть строка Address: 10.0.0.191. Если это не так — не продолжайте: API-сервер не сможет проверять токены.

2e. Настройка OIDC для Kubernetes API Server

Это то, что заставляет Kubernetes фактически доверять токенам, выданным Dex. В кластерах на базе kubeadm отредактируйте /etc/kubernetes/manifests/kube-apiserver.yaml:

# изменения в kube-apiserver.yaml

    - --oidc-issuer-url=https://dex.ldap.com:31000
    - --oidc-client-id=kubernetes
    - --oidc-ca-file=/etc/dex/tls/dex.crt
    - --oidc-username-claim=sub
    - --oidc-groups-claim=groups

ВАЖНО: Для --oidc-ca-file используйте ca.crt, а не dex.crt. API-серверу нужен CA-сертификат, которым подписан сертификат Dex, а не сам серверный сертификат. Указание dex.crt приведёт к ошибкам TLS-верификации при попытке API-сервера получить JWKS-ключи.

Что делает каждый флаг:

  • --oidc-issuer-url: Принимаются только токены, где клейм iss точно совпадает с этим URL. Должен совпадать с полем issuer в dex-values.yaml.

  • --oidc-client-id: Действительны только токены, выданные для этого client ID. Соответствует полю id в конфигурации staticClients.

  • --oidc-ca-file: CA-сертификат для проверки TLS-сертификата Dex. Именно для этого вы создавали свой CA — не для проверки пользователей, а для установления доверия к самому Dex.

  • --oidc-username-claim: Какой клейм JWT становится именем пользователя в Kubernetes. sub — надёжный и стабильный вариант по умолчанию.

  • --oidc-groups-claim: Какой клейм JWT содержит членства в группах. Именно это позволяет группам LDAP управлять RBAC Kubernetes.

ВНИМАНИЕ: API-сервер — это статический pod, управляемый kubelet. Редактирование манифеста запускает автоматический перезапуск. Следите за ним командой: kubectl get pods -n kube-system | grep apiserver

Монтирование директории с сертификатами в pod API-сервера

Этот шаг большинство руководств полностью упускает. kube-apiserver работает как статический pod внутри контейнера. Даже если файл /etc/dex/tls/ca.crt существует на хосте, контейнер не сможет его прочитать, пока он не смонтирован явно.

Добавьте следующее в kube-apiserver.yaml в разделы volumeMounts и volumes:

# В containers[0].volumeMounts:
- mountPath: /etc/dex/tls
  name: dex-tls
  readOnly: true
# В volumes:
- hostPath:
    path: /etc/dex/tls
    type: DirectoryOrCreate
  name: dex-tls

ВНИМАНИЕ: API-сервер — статический pod под управлением kubelet. Редактирование манифеста запускает автоматический перезапуск. Однако — и это важно — перезапуск не всегда происходит мгновенно, и работающий процесс может не подхватить новые флаги до принудительного завершения. Всегда проверяйте командой:

sudo crictl inspect $(sudo crictl ps | grep "kube-apiserver " | awk '{print $1}') | python3 -c "
import sys, json
data = json.load(sys.stdin)
args = data['info']['runtimeSpec']['process']['args']
oidc = [a for a in args if 'oidc' in a]
print('OIDC args:', oidc)
"

Если команда вернула пустой список — работающий процесс не подхватил новую конфигурацию. Принудительно перезапустите:

sudo crictl rm -f $(sudo crictl ps -a | grep "kube-apiserver " | awk '{print $1}')
sleep 15
sudo crictl ps | grep kube-apiserver

Повторно выполните команду inspect и убедитесь, что все OIDC-флаги присутствуют в выводе, прежде чем продолжать.

2f. Клиентская сторона — плагин oidc-login

На каждой машине разработчика установите плагин kubectl oidc-login через krew:

# Сначала установите krew, если он ещё не установлен
(
  set -x; cd "$(mktemp -d)" && OS="$(uname | tr '[:upper:]' '[:lower:]')" && \
  ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/arm64/arm64/')" && \
  KREW="krew-${OS}_${ARCH}" && \
  curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" && \
  tar zxvf "${KREW}.tar.gz" && ./"${KREW}" install krew
)
export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
# Установите oidc-login
kubectl krew install oidc-login

Настройте учётные данные в kubeconfig:

kubectl config set-credentials dex-user \
  --exec-api-version=client.authentication.k8s.io/v1beta1 \
  --exec-command=kubectl \
  --exec-arg=oidc-login \
  --exec-arg=get-token \
  --exec-arg=--oidc-issuer-url=https://dex.ldap.com:31000 \
  --exec-arg=--oidc-client-id=kubernetes \
  --exec-arg=--oidc-client-secret=MySuperSecretPassword123 \
  --exec-arg=--oidc-redirect-url=http://localhost:8000/callback \
  --exec-arg=--insecure-skip-tls-verify \
  --exec-arg=--grant-type=authcode-keyboard \
  --exec-arg=--oidc-extra-scope=email \
  --exec-arg=--oidc-extra-scope=groups

kubectl config set-context dex-context --cluster=kubernetes --user=dex-user
kubectl config use-context dex-context

SSH-сессии: Если вы запускаете kubectl по SSH на безголовой Linux-машине, всегда используйте --grant-type=authcode-keyboard. Стандартный поток с открытием браузера завершится ошибкой Missing X server or $DISPLAY. Клавиатурный режим выводит URL в терминал — скопируйте его, откройте в своём локальном браузере, выполните вход, а затем вставьте обратно в терминал только значение кода (часть после ?code= и до &state=).

2g. RBAC — замыкаем петлю управления доступом

Получить валидный OIDC-токен — это только полдела. Атрибут groups в JWT ничего не даёт, пока не созданы RBAC-привязки, сопоставляющие группы LDAP с ролями Kubernetes.

Правильный производственный подход — привязки на основе групп, а не на основе отдельных пользователей. В этом случае добавление или удаление пользователя из группы LDAP немедленно меняет его доступ к Kubernetes при следующем входе. Никакого ручного управления kubeconfig, никаких индивидуальных ClusterRoleBinding.

ldap-rbac.yaml:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ldap-k8s-admins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: k8s-admins
kubectl apply -f ldap-rbac.yaml

Теперь каждый пользователь, состоящий в группе LDAP k8s-admins, автоматически получает доступ cluster-admin. Хотите дать другой команде доступ только для чтения? Создайте ещё одну группу в LDAP, привяжите её к ClusterRole view — и готово. Управление доступом целиком живёт в вашем существующем каталоге LDAP.

2h. Декодируем и проверяем токен

Никогда не доверяйте токену, который вы не декодировали. Прежде чем запускать kubectl get pods, убедитесь, что клеймы в токене соответствуют ожиданиям.

Получите токен и декодируйте его одной командой:

kubectl oidc-login get-token \
  --oidc-issuer-url=https://dex.ldap.com:31000 \
  --oidc-client-id=kubernetes \
  --oidc-client-secret=MySuperSecretPassword123 \
  --oidc-redirect-url=http://localhost:8000/callback \
  --insecure-skip-tls-verify \
  --grant-type=authcode-keyboard \
  --oidc-extra-scope=email \
  --oidc-extra-scope=groups 2>/dev/null | python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
token = data['status']['token']
payload = token.split('.')[1]
payload += '=' * (4 - len(payload) % 4)
decoded = json.loads(base64.b64decode(payload))
print(json.dumps(decoded, indent=2))
print()
print('USERNAME:', decoded.get('email', 'MISSING'))
print('GROUPS:', decoded.get('groups', 'MISSING'))
"

Содержимое нормального токена выглядит так:

{
  "iss": "https://dex.ldap.com:31000",
  "sub": "CgVicnVpehIEbGRhcA",
  "aud": "kubernetes",
  "exp": 1775253989,
  "email": "bell@gmail.com",
  "email_verified": true,
  "groups": [
    "k8s-admins"
  ]
}
USERNAME: bell@gmail.com
GROUPS: ['k8s-admins']

Прежде чем что-то отлаживать, проверьте три вещи:

  • iss точно совпадает с вашим URL издателя

  • email содержит корректное значение (не отсутствует)

  • groups содержит ожидаемые членства в группах LDAP

Если groups отсутствует — пользователь не является членом ни одной группы LDAP, вернитесь и проверьте, что ldapmodify был применён корректно. Если отсутствует email — флаг --oidc-extra-scope=email не указан в учётных данных kubeconfig.

Можно также проверить токен напрямую против API-сервера без kubectl:

TOKEN="<вставьте-токен-сюда>"
curl -k -H "Authorization: Bearer $TOKEN" \
  https://10.0.0.191:6443/api/v1/namespaces/default/pods

Успешный ответ возвращает объект PodList. Ответ Unauthorized означает, что API-сервер отклоняет токен — вернитесь и проверьте OIDC-флаги и конфигурацию CA-сертификата.

3. Скрытые препятствия

Каждая из описанных ниже ошибок стоила реального времени на отладку. Ни одна из них не была очевидна из сообщений об ошибках.

Ошибка 1 — TLS-сертификат не найден в pod-е

invalid config: get HTTP TLS: load TLS config: loading TLS keypair:
open /etc/dex/tls/dex.crt: no such file or directory

Файлы сертификатов существовали на хосте Linux по пути /etc/dex/tls/ и были совершенно корректны. Но pod Dex читает не с файловой системы хоста, а из смонтированного Kubernetes Secret. Файлы на хосте были совершенно несущественны.

Решение: Создайте Kubernetes Secret правильно, используя тип generic с явным маппингом имён файлов (см. раздел 2a).

Ошибка 2 — Несоответствие типа секрета, неверные имена файлов

Использование kubectl create secret tls хранит файлы внутри как tls.crt и tls.key. Dex ожидает dex.crt и dex.key. Pod запускается, не находит ожидаемые имена файлов по пути монтирования и немедленно падает. Ошибка указывает на отсутствующий файл, а не на неверный тип секрета — именно поэтому при первом столкновении это действительно сложно отладить.

Решение: Всегда используйте generic-секреты с явным маппингом имён файлов.

Ошибка 3 — Недопустимое имя ресурса Kubernetes для кодов авторизации

invalid kubernetes resource name: must match the pattern ^[a-z0-9]

Dex генерирует коды авторизации, содержащие прописные буквы и специальные символы. При использовании Kubernetes CRD в качестве бэкенда хранилища эти коды становятся именами ресурсов, которые должны соответствовать соглашениям об именовании Kubernetes. В результате в браузере после входа отображается ошибка базы данных.

Решение: Добавьте oauth2.skipApprovalScreen: true в dex-values.yaml. Это убирает экран подтверждения и упрощает формат кода авторизации.

Ошибка 4 — Путаница с доменами в трёх конфигурациях

В ходе разных сессий редактирования в конфигурациях фигурировали три разных домена: dex.example.com в раннем черновике, dex.ldap.com в реальном файле values и файл hosts на Windows, указывавший на неверный домен.

Решение: Определитесь с доменом до того, как писать любую конфигурацию. Запишите его. И не меняйте. Перед применением любых изменений выполните grep по всем файлам:

grep -r "dex\." dex-values.yaml /etc/kubernetes/manifests/kube-apiserver.yaml ~/.kube/config

Ошибка 5 — ldapadd показывает справку вместо выполнения

Флаг -w требует значения пароля сразу после себя. Если поставить -f перед паролем, ldapadd воспринимает -f как значение пароля, тихо завершается с ошибкой и выводит справочный экран.

# Неправильно — -w без значения, -f поглощается как пароль
ldapadd -x -H ldap://localhost:1389 -D "cn=admin,dc=ldap,dc=com" -w -f /tmp/base.ldif
# Правильно
ldapadd -x -H ldap://localhost:1389 -D "cn=admin,dc=ldap,dc=com" -w adminpassword -f /tmp/base.ldif

Ошибка 6 — 127.0.1.1 в файле hosts на Windows

В файле hosts на Windows вместо реального маршрутизируемого IP машины Linux был указан адрес обратной петли 127.0.1.1. SSH-соединения зависали, браузер не мог достичь Dex.

Решение: Всегда выполняйте hostname -I на хосте Linux для получения реального IP, а затем используйте этот адрес везде одинаково.

Ошибка 7 — kube-apiserver не загружает конфигурацию OIDC

После редактирования kube-apiserver.yaml инспектирование работающего процесса показало:

OIDC args: []

Kubelet не перезапустил контейнер с новой конфигурацией, несмотря на то что манифест был обновлён. Это привело к нескольким часам замешательства, когда конфигурация OIDC выглядела корректно в YAML, но фактически никогда не активировалась.

Решение: Принудительно завершите контейнер и дайте kubelet воссоздать его:

sudo crictl rm -f $(sudo crictl ps -a | grep "kube-apiserver " | awk '{print $1}')

Всегда проверяйте через crictl inspect после любого изменения манифеста.

Ошибка 8 — CoreDNS не разрешает dex.ldap.com внутри кластера

Домен dex.ldap.com был прописан в /etc/hosts на машине Linux, но не в CoreDNS. kube-apiserver пытался получить JWKS-ключи с https://dex.ldap.com:31000/keys, CoreDNS возвращал NXDOMAIN, и каждый токен молча отклонялся как invalid bearer token. Никаких DNS-связанных ошибок ни в одном журнале не было — только обобщённый сбой аутентификации.

Решение: Добавьте блок hosts в ConfigMap CoreDNS (см. раздел 2d).

Ошибка 9 — CA-сертификат не доверен kube-apiserver

tls: failed to verify certificate: x509: certificate signed by unknown authority

kube-apiserver не мог проверить TLS-сертификат Dex при получении JWKS-ключей по двум причинам: (1) CA-сертификат не был добавлен в системное хранилище доверия, и (2) флаг --oidc-ca-file был временно убран в ходе отладки. Вместе это означало, что у API-сервера не было никакого способа доверять TLS-сертификату Dex.

Решение: Добавьте CA в системное хранилище доверия И восстановите --oidc-ca-file:

sudo cp /etc/dex/tls/ca.crt /usr/local/share/ca-certificates/dex-ca.crt
sudo update-ca-certificates

Ошибка 10 — Отсутствуют клеймы email и groups в токене

После успешного входа токены по-прежнему отклонялись. Декодирование JWT показало:

{
  "iss": "https://dex.ldap.com:31000",
  "sub": "CgVicnVpehIEbGRhcA",
  "aud": "kubernetes"
}

Ни email, ни groups. kube-apiserver был настроен использовать --oidc-username-claim=email, но токен не содержал клейма email. Флаги --oidc-extra-scope=email и --oidc-extra-scope=groups отсутствовали в exec-учётных данных kubeconfig.

Решение: Добавьте оба дополнительных scope в команду kubectl config set-credentials (см. раздел 2f).

Ошибка 11 — Пользователь LDAP не состоит в группе

Группа k8s-admins существовала в LDAP, но у пользователя bruiz не было записи memberUid. Токен генерировался успешно, но клейм groups был пустым. Групповой RBAC никогда не срабатывал, и результатом была постоянная ошибка Unauthorized без каких-либо информативных сообщений.

Решение: Используйте ldapmodify для добавления memberUid: bruiz в группу (см. раздел 2b). Всегда проверяйте членство в группе, декодируя JWT и проверяя клейм groups, прежде чем приступать к отладке RBAC.

Ошибка 12 — Браузер не открывается из SSH-сессии

Missing X server or $DISPLAY
The platform failed to initialize. Exiting.

Плагин oidc-login пытался открыть браузер на машине Linux, но через SSH графического дисплея не было.

Решение: Используйте --grant-type=authcode-keyboard в exec-учётных данных и командах get-token. Это выводит URL в терминал, который вы открываете вручную в своём локальном браузере.

4. Дисциплина важнее инструментов

Технология в этом руководстве не сложна. Генерация сертификатов — пять команд. Helm-установки — однострочники. Сложность здесь в другом — в дисциплине: воспринимать URL издателя как контракт, а конфигурацию — как систему, а не как набор независимых файлов.

URL издателя — это контракт

Каждый компонент в этом стеке хранит ссылку на URL вашего издателя Dex: конфиг Dex, флаги API-сервера, запись в kubeconfig и файлы hosts на каждой машине. Никакой инструмент не обеспечивает согласованность между ними. Это делаете вы.

Определите домен до того, как начнёте писать любую конфигурацию. Запишите его. И не меняйте. Смена URL издателя после развёртывания требует одновременного обновления каждого компонента — пропустите один, и вы проведёте час за отладкой сбоя проверки токена, который окажется несоответствием URL.

Проверьте TLS до того, как трогать Kubernetes

Выполните это до настройки единого флага API-сервера. Если это не работает — исправьте, прежде чем двигаться дальше:

curl -v --cacert ca.crt https://dex.ldap.com:31000/.well-known/openid-configuration

Успешный ответ возвращает JSON-документ с полем issuer, совпадающим с вашим URL. Если TLS падает здесь — он упадёт везде ниже по цепочке, а сообщения об ошибках там будут читать сложнее, чем это.

Всегда проверяйте, что реально запущено

Разрыв между тем, что написано в конфиг-файле, и тем, что реально работает в процессе, стоил на этом стеке больше часов отладки, чем любой настоящий баг. Прежде чем делать вывод о некорректной конфигурации, проверьте работающий процесс:

# Проверить, что OIDC-флаги kube-apiserver реально загружены
sudo crictl inspect $(sudo crictl ps | grep "kube-apiserver " | awk '{print $1}') | \
  python3 -c "
import sys, json
data = json.load(sys.stdin)
args = data['info']['runtimeSpec']['process']['args']
print([a for a in args if 'oidc' in a])
"
# Проверить, что CoreDNS разрешает dex.ldap.com
kubectl run dns-test --rm -it --image=busybox --restart=Never -- nslookup dex.ldap.com
# Проверить доступность Dex с доверием к CA
curl --cacert /etc/dex/tls/ca.crt https://dex.ldap.com:31000/.well-known/openid-configuration

Если все три проверки пройдены — инфраструктура настроена верно. Если токены после этого всё равно отклоняются — декодируйте JWT и проверяйте клеймы.

5. Эксплуатация после запуска (Day 2)

Запустить стек — это первый день. Второй день — там, где большинство команд попадают в беду.

Истечение срока сертификатов: Ваши сертификаты действительны 10 000 дней. Для лаборатории — нормально. В продакшене используйте cert-manager для автоматического обновления. Сертификат, обслуживаемый вручную и тихо истёкший, выводит аутентификацию всего кластера без единого предупреждения.

Ротация секретов: Секрет staticClients в конфигурации Dex — это долгоживущие учётные данные. Храните его в менеджере секретов, ротируйте по расписанию и никогда не фиксируйте в git-репозитории.

Доступность LDAP: Если OpenLDAP упадёт, никто не сможет аутентифицироваться в Kubernetes. Это скрытая единая точка отказа. Планируйте репликацию LDAP или держите «аварийный» сервисный аккаунт, не зависящий от OIDC.

Журналирование аудита: Включите журналирование аудита API-сервера Kubernetes. Каждая попытка OIDC-аутентификации видна там. Это бесценно, когда доступ ломается в 23:00 и вам нужно за пять минут найти причину.

Записи CoreDNS: Если IP-адрес хоста Dex когда-либо изменится, не забудьте обновить и /etc/hosts на всех машинах, и ConfigMap CoreDNS. Устаревший IP в CoreDNS приведёт к тому, что вся проверка OIDC-токенов тихо перестанет работать.

6. Заключительные мысли

Связка OpenLDAP + Dex + OIDC — это надёжный, проверенный в бою паттерн, хорошо подходящий для on-premises или изолированных Kubernetes-кластеров. Никакой магии и ничего особенно сложного, если вы понимаете ответственность каждого компонента.

Но это операционная нагрузка. Теперь вы управляете провайдером удостоверений. А значит, несёте ответственность за его доступность, жизненный цикл сертификатов, безопасность и путь обновления. Это реальные обязательства, которые должны влиять на ваше архитектурное решение.

Если ваша организация уже эксплуатирует Keycloak — рассмотрите его как альтернативу: он нативно говорит на LDAP и выпускает OIDC-токены, становясь универсальным решением ценой большего объёма развёртывания. Если на столе есть управляемые облачные IdP вроде Okta или Azure AD — управляемый вариант сэкономит вам значительные операционные усилия.

Паттерн из этого руководства оправдывает свою сложность именно тогда, когда использовать управляемый IdP действительно нельзя.

Но я хочу быть с вами честен: добраться до этой точки в реальной среде редко бывает так гладко. Сертификаты не совпадают. CoreDNS не разрешает то, что вы ожидаете. kube-apiserver тихо игнорирует изменения конфигурации, и вы тратите час в недоумении, почему токены по-прежнему отклоняются.

Я лично упёрся в каждую из этих стен. В Части 2 я описываю двенадцать ошибок, которые подбросила мне эта установка — каждую с точным сообщением об ошибке, реальной первопричиной и исправлением, которое действительно сработало. Если ваша установка ведёт себя не так, как описано в этом руководстве, — следующий шаг туда.

Финальный совет: Сначала правильно настройте цепочку сертификатов, и только потом пишите хоть строчку в Helm values. Провер

© 2026 meganuke