Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Введение
Зачем?
Представим ситуацию, что мы деплоим по 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
Три. Два. Один. Запуск
Как и ожидалось: неймспейсы получены, к сервисам доступа нет, а значит всё сработало точно так, как нам нужно
Теперь можно продумывать ролевую модель, нарезать доступы по неймспейсам, ограничивать деплои в прод только для ответственных инженеров и прочее, но это уже другая история...