Для аутентификации CI/CD-пайплайнов в API Kubernetes доступны следующие методы:
- ServiceAccount — токен Kubernetes ServiceAccount.
- Basic Auth — логин и пароль через IdP.
- Token Exchange — обмен токена IdP на токен Dex.
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 должны быть соблюдены следующие требования:
- publishAPI включён.
- DexProvider настроен для IdP.
Включение
В 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 должны быть соблюдены следующие требования:
- publishAPI включён.
- DexProvider настроен как тип OIDC.
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-контроллер объект без неё. Добавить аннотацию может только субъект, которому разрешено обновлять ModuleConfiguser-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 .