Аутентификация в Kubernetes через Gitlab'овские JWT токены

Эта статья на Хабре https://habr.com/ru/articles/783586/

Введение

Зачем?

Представим ситуацию, что мы деплоим по push-модели. В качестве платформы для запуска деплоя у нас используется Gitlab: в нём настроен пайплайн и джобы, разворачивающие приложения в разные окружения в Kubernetes

Какой бы инструмент мы не использовали (kubectl, helm), для манипуляций с ресурсами API нам в любом случае будет необходимо аутентифицироваться при выполнении запросов к Kubernetes. Для этого в запросе надо передать данные для аутентификации, будь то токен или сертификат. И тут возникает несколько вопросов:

  1. Где хранить эти креды?

    Хранить креды от кластера можно в Gitlab CI/CD Variables и подставлять в джобу деплоя, но тогда потенциально все пользователи будут деплоить с одними и теми же доступами

  2. Как сделать так, чтобы у каждого пользователя были свои данные для доступа в кластер?

    Можно было бы вручную запускать джобы деплоя и в параметры каждый раз подставлять свои аутентификационные данные, но, очевидно, такой подход неудобен и подходит далеко не всем

А что если сделать так, чтобы в качестве провайдера аутентификационных данных для 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

Нам необходимо сделать так, чтобы:

  1. Можно было аутентифицироваться в кластере по пользовательскому JWT токену, полученному от Gitlab
  2. Доступы к ресурсам были те, которые прописаны в 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

Три. Два. Один. Запуск

Как и ожидалось: неймспейсы получены, к сервисам доступа нет, а значит всё сработало точно так, как нам нужно

Теперь можно продумывать ролевую модель, нарезать доступы по неймспейсам, ограничивать деплои в прод только для ответственных инженеров и прочее, но это уже другая история…