Для аутентификации CI/CD-пайплайнов в API Kubernetes доступны следующие методы:


ServiceAccount

Токен ServiceAccount используется напрямую для аутентификации в API. Внешний IdP не требуется. Для ServiceAccount можно создавать долгоживущие токены (без TTL, с возможностью отзыва или ротации) и короткоживущие токены.

При совместном использовании одного ServiceAccount несколькими пайплайнами в audit-логах не будет информации о конкретном пайплайне.

Предварительные требования

Для настройки аутентификации через токен для ServiceAccount должны быть соблюдены следующие требования:

  • Доступ к кластеру с правами на создание ServiceAccount и секретов.
  • Для внешнего доступа: опубликованный через Ingress API-сервер Kubernetes (для публикации используется параметр publishAPI) или прямой доступ к API через VPN.

Создание ServiceAccount и долгоживущего токена

Для создания ServiceAccount и токена используйте команду:

d8 k create ns ci-deploy || true

cat <<EOF | d8 k apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gitlab-runner-deploy
  namespace: ci-deploy
---
apiVersion: v1
kind: Secret
metadata:
  name: gitlab-runner-deploy-token
  namespace: ci-deploy
  annotations:
    kubernetes.io/service-account.name: gitlab-runner-deploy
type: kubernetes.io/service-account-token
EOF

Секрет типа kubernetes.io/service-account-token — устаревший подход. Рекомендуется использовать TokenRequest API (d8 k create token ...).

Выдача прав

Подробнее о выдаче прав — в разделе «Выдача прав пользователям и сервисным аккаунтам».

Назначьте необходимые для ServiceAccount права.

Для текущей ролевой модели используйте ClusterAuthorizationRule:

cat <<EOF | d8 k apply -f -
apiVersion: deckhouse.io/v1
kind: ClusterAuthorizationRule
metadata:
  name: gitlab-runner-access
spec:
  subjects:
  - kind: ServiceAccount
    name: gitlab-runner-deploy
    namespace: ci-deploy
  accessLevel: Admin
  portForwarding: true
EOF

Доступные уровни: User, PrivilegedUser, Editor, Admin, ClusterEditor, ClusterAdmin, SuperAdmin.

Для экспериментальной ролевой модели используйте ClusterRoleBinding:

cat <<EOF | d8 k apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: gitlab-runner-access
subjects:
- kind: ServiceAccount
  name: gitlab-runner-deploy
  namespace: ci-deploy
roleRef:
  kind: ClusterRole
  name: d8:manage:all:manager
  apiGroup: rbac.authorization.k8s.io
EOF

Получение URL API и сертификата CA

При использовании publishAPI:

API_HOST=$(d8 k -n d8-user-authn get ingress kubernetes-api -o jsonpath='{.spec.rules[0].host}')
echo "API endpoint: https://${API_HOST}"

Если сертификат API подписан публичным CA (Let’s Encrypt), параметр --certificate-authority не требуется.

Для приватного CA:

d8 k -n d8-user-authn get secret kubernetes-api-ca-key-pair -o jsonpath='{.data.ca\.crt}' | base64 -d > /tmp/ca.crt

Создание kubeconfig

Пример генерации kubeconfig для созданного ранее ServiceAccount:

export CLUSTER_NAME=my-cluster
export USER_NAME=gitlab-runner-deploy
export CONTEXT_NAME=${CLUSTER_NAME}-${USER_NAME}
export FILE_NAME=kube.config

# Для публичного CA уберите --certificate-authority и --embed-certs.
d8 k config set-cluster $CLUSTER_NAME \
  --server=https://${API_HOST} \
  --certificate-authority=/tmp/ca.crt \
  --embed-certs=true \
  --kubeconfig=$FILE_NAME

d8 k config set-credentials $USER_NAME \
  --token=$(d8 k -n ci-deploy get secret gitlab-runner-deploy-token -o jsonpath='{.data.token}' | base64 -d) \
  --kubeconfig=$FILE_NAME

d8 k config set-context $CONTEXT_NAME \
  --cluster=$CLUSTER_NAME --user=$USER_NAME \
  --kubeconfig=$FILE_NAME

d8 k config use-context $CONTEXT_NAME --kubeconfig=$FILE_NAME

Короткоживущие токены (TokenRequest)

TokenRequest API позволяет создавать токены с ограниченным сроком действия. При создании такого токена укажите в команде имя ServiceAccount, которому нужно предоставить доступ (в примере — gitlab-runner-deploy):

d8 k create token gitlab-runner-deploy -n ci-deploy --duration=1h

Команда d8 k create token сама требует доступа к Kubernetes API. Если CI-задача (job) не имеет начального доступа к кластеру, выполнить TokenRequest внутри задачи невозможно.

Типовые сценарии к использованию в CI

Bootstrap-токен с минимальными правами. CI хранит долгоживущий kubeconfig/токен, который имеет право только на создание TokenRequest для конкретного ServiceAccount. В начале задачи bootstrap-токен выпускает короткоживущий токен, который используется для деплоя.

Внешний token broker. Оператор, секрет-менеджер (Vault, External Secrets) или внешний сервис периодически выпускает короткоживущие токены и помещает их в CI-переменные или секреты. CI-задача только использует готовый токен.

Bootstrap-токен должен иметь минимальные права: только create на ресурс serviceaccounts/token для конкретного ServiceAccount/namespace. Не используйте cluster-admin. Ограничивайте TTL выпускаемых токенов и оперативно отзывайте bootstrap-доступ при его компрометации.

Пример использования без файла kubeconfig:

export KUBE_SERVER="https://${API_HOST}"
export KUBE_TOKEN=$(d8 k create token gitlab-runner-deploy -n ci-deploy --duration=1h)
d8 k --server=$KUBE_SERVER --token=$KUBE_TOKEN get ns

Basic Auth

Аутентификация по логину и паролю через IdP (LDAP, OIDC).

Пароль передаётся в DP и проверяется через basic-auth-proxy/Dex.

Только один DexProvider в кластере может иметь enableBasicAuth: true.

Предварительные требования

Для настройки Basic Auth должны быть соблюдены следующие требования:

Включение

В DexProvider добавьте enableBasicAuth: true. Пример настройки DexProvider приведён в документации модуля user-authn.

Получение эндпоинта API

Для получения эндпоинта API используйте команду:

API_HOST=$(d8 k -n d8-user-authn get ingress kubernetes-api -o jsonpath='{.spec.rules[0].host}')
echo "https://${API_HOST}"

Проверка

Для проверки доступа используйте команду:

curl -q -u "$K8S_USER:$K8S_PASSWORD" "https://${API_HOST}/version"

Возможные ошибки:

  • 401 — неверные учётные данные или Basic Auth не включён.
  • 403 — аутентификация прошла, но RBAC запрещает доступ.

Настройка kubeconfig

Пример настройки kubeconfig для Basic Auth:

apiVersion: v1
kind: Config
clusters:
- name: my-cluster
  cluster:
    server: https://<API_HOST>
users:
- name: basic-auth-user
  user:
    username: "<USERNAME>"
    password: "<PASSWORD>"
contexts:
- name: default
  context:
    cluster: my-cluster
    user: basic-auth-user
current-context: default

Использование в GitLab CI

Пример использования Basic Auth в GitLab CI:

deploy:
  script:
    - d8 k --server="$K8S_SERVER" --username="$K8S_USER" --password="$K8S_PASSWORD" get ns

Переменные K8S_SERVER, K8S_USER, K8S_PASSWORD задаются в настройках CI/CD проекта.


Обмен токена (token exchange)

Пайплайн получает токен от IdP, обменивает его в Dex на токен с aud=kubernetes и обращается с ним к API.

Рекомендуется для GitLab CI и GitHub Actions.

DP/Dex не получает пароль пользователя. Способ получения IDP_TOKEN зависит от IdP: OIDC job token (GitLab/GitHub) или эндпоинт токена IdP (client_credentials).

Предварительные требования

Для настройки Token Exchange должны быть соблюдены следующие требования:

Token exchange не работает с коннекторами type: GitLab или type: GitHub. Настройте GitLab/GitHub как OIDC-провайдеры.

Создание DexClient

Для создания DexClient используйте команду:

cat <<EOF | d8 k apply -f -
apiVersion: deckhouse.io/v1
kind: DexClient
metadata:
  name: ci-token-exchange
  namespace: d8-user-authn
  annotations:
    dexclient.deckhouse.io/allow-access-to-kubernetes: "true"
spec: {}
EOF

Аннотация dexclient.deckhouse.io/allow-access-to-kubernetes позволяет клиенту запрашивать токены с aud=kubernetes.

Предоставляемый таким образом доступ к Kubernetes API действует на уровне всего кластера. По этой причине для добавления аннотации или изменения её значения на "true" необходимы права на изменение конфигурации модуля user-authn — например, роль d8:manage:permission:module:user-authn:edit. Прав на создание DexClient в отдельном неймспейсе недостаточно: admission-контроллер отклонит запрос с сообщением, содержащим имя аннотации.

Добавление аннотации ограничено независимо от указанного значения, включая "false". Это необходимо для совместимости с предыдущими версиями DP, в которых доступ предоставляется при наличии аннотации независимо от её значения.

Ограничение не распространяется на объект, у которого аннотация уже установлена. Удаление и повторное создание DexClient считается повторным добавлением аннотации. Поэтому GitOps-контроллеру, который пересоздаёт объект вместо его обновления, требуется право update на ресурс moduleconfigs с именем user-authn.

Получение client secret:

d8 k -n d8-user-authn get secret dex-client-ci-token-exchange -o jsonpath='{.data.clientSecret}' | base64 -d

Формат client_id: dex-client-ci-token-exchange@d8-user-authn.

Получение URL Dex и API

Для получения URL Dex и API используйте команды:

DEX_HOST=$(d8 k -n d8-user-authn get ingress dex -o jsonpath='{.spec.rules[0].host}')
API_HOST=$(d8 k -n d8-user-authn get ingress kubernetes-api -o jsonpath='{.spec.rules[0].host}')

Выдача RBAC

DP настраивает kube-apiserver на проверку токенов Dex. Claims email и groups из токена используются для RBAC.

Набор claims, которые требуются kube-apiserver для аутентификации, зависит от конфигурации. Если kube-apiserver требует name, добавьте scope profile.

Подробнее о выдаче прав — в разделе «Выдача прав пользователям и сервисным аккаунтам».

Для текущей ролевой модели используйте ClusterAuthorizationRule:

cat <<EOF | d8 k apply -f -
apiVersion: deckhouse.io/v1
kind: ClusterAuthorizationRule
metadata:
  name: ci-deployer
spec:
  subjects:
  - kind: User
    name: deployer@example.com
  accessLevel: Admin
EOF

Для экспериментальной ролевой модели используйте ClusterRoleBinding:

cat <<EOF | d8 k apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: ci-deployer
subjects:
- kind: User
  name: deployer@example.com
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: d8:manage:all:manager
  apiGroup: rbac.authorization.k8s.io
EOF

Запрос обмена токена

Пример запроса на обмен токена:

RESPONSE=$(curl -q -s -X POST "https://${DEX_HOST}/token" \
  -u "${DEX_CLIENT_ID}:${DEX_CLIENT_SECRET}" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "subject_token=${IDP_TOKEN}" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:id_token" \
  -d "connector_id=${CONNECTOR_ID}" \
  -d "scope=openid profile email groups audience:server:client_id:kubernetes" \
  -d "requested_token_type=urn:ietf:params:oauth:token-type:id_token")

DEX_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')

Параметры:

  • subject_token — токен от IdP.
  • subject_token_type — id_token (GitLab/GitHub) или access_token (Keycloak).
  • connector_id — metadata.name ресурса DexProvider.
  • scope — обязательно audience:server:client_id:kubernetes и profile.

Ожидаемая audience всегда kubernetes. Если API возвращает 401 с ошибкой несоответствия audience, убедитесь что scope содержит audience:server:client_id:kubernetes.

GitLab CI

Пример запроса токена для GitLab CI:

deploy:
  id_tokens:
    GITLAB_OIDC_TOKEN:
      aud: https://<DEX_HOST>/
  script:
    - |
      RESPONSE=$(curl -q -s -X POST "https://${DEX_HOST}/token" \
        -u "${DEX_CLIENT_ID}:${DEX_CLIENT_SECRET}" \
        -H "Content-Type: application/x-www-form-urlencoded" \
        -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
        -d "subject_token=${GITLAB_OIDC_TOKEN}" \
        -d "subject_token_type=urn:ietf:params:oauth:token-type:id_token" \
        -d "connector_id=gitlab" \
        -d "scope=openid profile email groups audience:server:client_id:kubernetes" \
        -d "requested_token_type=urn:ietf:params:oauth:token-type:id_token")
      DEX_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')
      d8 k --server="${K8S_SERVER}" --token="${DEX_TOKEN}" auth whoami

Keycloak

Получение токена из Keycloak:

KEYCLOAK_TOKEN=$(curl -q -s -X POST "https://<KEYCLOAK_HOST>/realms/<REALM>/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=<KEYCLOAK_CLIENT_ID>" \
  -d "client_secret=<KEYCLOAK_CLIENT_SECRET>" | jq -r '.access_token')

Обмен в Dex (для Keycloak используется subject_token_type=access_token):

Для обмена access_token в DexProvider требуется getUserInfo: true.

RESPONSE=$(curl -q -s -X POST "https://${DEX_HOST}/token" \
  -u "${DEX_CLIENT_ID}:${DEX_CLIENT_SECRET}" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "subject_token=${KEYCLOAK_TOKEN}" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "connector_id=keycloak" \
  -d "scope=openid profile email groups audience:server:client_id:kubernetes" \
  -d "requested_token_type=urn:ietf:params:oauth:token-type:id_token")

DEX_TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')
d8 k --server="https://${API_HOST}" --token="${DEX_TOKEN}" auth whoami

Устранение неполадок

Используйте d8 k auth whoami для проверки под какой идентичностью выполняется запрос.

Флаг --token может не работать — kubeconfig с auth-provider, exec или tokenFile переопределяет токен из флага. Для отладки используйте -v=8:

d8 k --server="https://${API_HOST}" --token="${DEX_TOKEN}" auth whoami -v=8

В выводе будет видно какой Authorization заголовок отправляется. Для надёжной работы с токеном используйте минимальный kubeconfig только с server и token, без auth-provider/exec.

Dex 400 — неверный subject_token, subject_token_type или connector_id.

Dex 401 — неверные учётные данные клиента или невалидный subject token.

API 401 — токен не прошёл проверку. Проверьте:

  • Аннотация dexclient.deckhouse.io/allow-access-to-kubernetes на DexClient. Если её нет, проверьте, не отклонил ли admission-контроллер запрос, который должен был её добавить, и не пересоздал ли GitOps-контроллер объект без неё. Добавить аннотацию может только субъект, которому разрешено обновлять ModuleConfig user-authn.
  • Scope содержит audience:server:client_id:kubernetes и profile.
  • Синхронизация времени между CI-раннером и кластером.

API 403 — аутентификация прошла, но RBAC не разрешает доступ для пользователя или группы из токена.

Декодирование токена для проверки claims:

echo "${DEX_TOKEN}" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

Дополнительные ресурсы