Эта статья на Хабре https://habr.com/ru/articles/783586/
Введение
Зачем?
Представим ситуацию, что мы деплоим по push-модели. В качестве платформы для запуска деплоя у нас используется Gitlab: в нём настроен пайплайн и джобы, разворачивающие приложения в разные окружения в Kubernetes
Какой бы инструмент мы не использовали (kubectl, helm), для манипуляций с ресурсами API нам в любом случае будет необходимо аутентифицироваться при выполнении запросов к Kubernetes. Для этого в запросе надо передать данные для аутентификации, будь то токен или сертификат. И тут возникает несколько вопросов:
-
Где хранить эти креды?
Хранить креды от кластера можно в Gitlab CI/CD Variables и подставлять в джобу деплоя, но тогда потенциально все пользователи будут деплоить с одними и теми же доступами
-
Как сделать так, чтобы у каждого пользователя были свои данные для доступа в кластер?
Можно было бы вручную запускать джобы деплоя и в параметры каждый раз подставлять свои аутентификационные данные, но, очевидно, такой подход неудобен и подходит далеко не всем
А что если сделать так, чтобы в качестве провайдера аутентификационных данных для Kubernetes выступал сам Gitlab? Тогда не надо было бы нигде хранить креды, и каждый пользователь мог бы аутентифицироваться в кубере под своей учёткой при запуске деплоя
Знакомьтесь. Gitlab ID Tokens
В версии Gitlab 15.7 появилась возможность прямо в джобе динамически создавать короткоживущие JWT токены, выпускаемые на имя того, кто запустил джобу
Всё, что нужно сделать - это дать название переменной с токеном и описать в поле aud
(audience) имя потенциального получателя токена (сервиса, которому токен будет отправляться)
job_with_id_tokens-job:
id_tokens:
MY_JWT_TOKEN:
aud: https://vault.example.com
script:
- echo $($MY_JWT_TOKEN | base64 -w0)
Пример полученного токена
{
"namespace_id": "72",
"namespace_path": "my-group",
"project_id": "20",
"project_path": "my-group/my-project",
"user_id": "1",
"user_login": "sample-user",
"user_email": "sample-user@example.com",
"user_identities": [
{"provider": "github", "extern_uid": "2435223452345"},
{"provider": "bitbucket", "extern_uid": "john.smith"},
],
"pipeline_id": "574",
"pipeline_source": "push",
"job_id": "302",
"ref": "feature-branch-1",
"ref_type": "branch",
"ref_path": "refs/heads/feature-branch-1",
"ref_protected": "false",
"environment": "test-environment2",
"environment_protected": "false",
"deployment_tier": "testing",
"environment_action": "start",
"runner_id": 1,
"runner_environment": "self-hosted",
"sha": "714a629c0b401fdce83e847fc9589983fc6f46bc",
"project_visibility": "public",
"ci_config_ref_uri": "gitlab.example.com/my-group/my-project//.gitlab-ci.yml@refs/heads/main",
"ci_config_sha": "714a629c0b401fdce83e847fc9589983fc6f46bc",
"jti": "235b3a54-b797-45c7-ae9a-f72d7bc6ef5b",
"iss": "https://gitlab.example.com",
"iat": 1681395193,
"nbf": 1681395188,
"exp": 1681398793,
"sub": "project_path:my-group/my-project:ref_type:branch:ref:feature-branch-1",
"aud": "https://vault.example.com"
}
Сам токен содержит множество полей, но нас в первую очередь интересует некий уникальный идентификатор, по которому можно однозначно определить пользователя. Для этого отлично подходит поле user_email
, которое содержит, как это не удивительно, пользовательский email
Отлично. Теперь что с этим можно сделать на стороне Kubernetes?
Доступ в Kubernetes через OIDC токены
Официальная документация k8s говорит нам о том, что кубовый API Server умеет работать со сторонними JWT токенами, а именно: проверять их подпись и вытаскивать из определённых полей имя пользователя и названия групп для того, чтобы матчить их с subjects, заданными в RBAC, для дальнейшей авторизации
Всё, что для этого нужно, это передать бинарнику kube-api-server дополнительные параметры при запуске, в которых мы укажем
- URL того, кто будет выпускать токены (и откуда забирать публичные ключи для проверки подписи)
- ID того, кто является получателем токена (тот самый
aud
) - Из какого поля (claim) вытаскивать имя пользователя и названия групп
- Дополнительные параметры, типа уникального префикса для имени пользователя (чтобы не перемешивать с пользователями, аутентифицирующимися другими способами) и CA файла self-hosted инстанса Gitlab
Что же, пришло время проверить, как это всё будет работать на практике
Аутентифицируемся в Kubernetes через Gitlab’овские JWT токены
Чисто для теста в рамках этой статьи представим, что у нас используется:
- Gitlab.com и его бесплатные шаренные раннеры
- Свой Kubernetes кластер с публичным белым IP и с открытым всему миру 6443 портом kube-api-server
Нам необходимо сделать так, чтобы:
- Можно было аутентифицироваться в кластере по пользовательскому JWT токену, полученному от Gitlab
- Доступы к ресурсам были те, которые прописаны в RBAC для пользователя
Настройка OIDC на kube-api-server
Тут всё просто. Передаём kube-api-server следующие параметры запуска
--oidc-issuer-url="https://gitlab.com"
--oidc-client-id="a-k8s-01"
--oidc-username-claim="user_email"
--oidc-username-prefix="oidc:"
- Токены выпускает https://gitlab.com (поле
iss
в токене) - Токены предназначены для a-k8s-01 (поле
aud
) - Имя пользователя берём из поля
user_email
- В RBAC мы будем использовать префикс
oidc:
перед именем пользователя
Создание RBAC
Проверять будем на кластерной роли с какими-нибудь простыми разрешениями (получение списка неймспейсов)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: maintainer
rules:
- apiGroups:
- ''
resources:
- 'namespaces'
verbs:
- 'get'
- 'list'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: maintainer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: maintainer
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: oidc:user@example.com
Создание тестовой джобы в Gitlab
Тут тоже всё просто:
- Зададим ID Token GITLAB_K8S_JWT_TOKEN с получателем (
aud
) a-k8s-01 - Для kubectl создадим конфиг, в который, в том числе, будет добавлен наш динамически создаваемый GITLAB_K8S_JWT_TOKEN
- Попробуем произвести какие-нибудь манипуляции с ресурсами кластера
stages:
- deploy
kubectl_deploy:
stage: deploy
id_tokens:
GITLAB_K8S_JWT_TOKEN:
aud: a-k8s-01
image:
name: bitnami/kubectl:1.28
entrypoint:
- ""
script:
- |
cat << EOF > /tmp/ca.pem
<CA_CERTIFICATE_CONTENT>
EOF
- API_SERVER="<api_server_url>"
- |
kubectl config set-cluster cluster --server="${API_SERVER}" --certificate-authority=/tmp/ca.pem
kubectl config set-credentials user --token="${GITLAB_K8S_JWT_TOKEN}"
kubectl config set-context context --cluster=cluster --user=user
kubectl config use-context context
- kubectl get ns
- kubectl get svc
Три. Два. Один. Запуск
Как и ожидалось: неймспейсы получены, к сервисам доступа нет, а значит всё сработало точно так, как нам нужно
Теперь можно продумывать ролевую модель, нарезать доступы по неймспейсам, ограничивать деплои в прод только для ответственных инженеров и прочее, но это уже другая история…