Введение

Зачем?

Представим ситуацию, что мы деплоим по 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

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

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

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

Комментарии (9)


  1. Inlore Автор
    27.12.2023 15:06

    Кстати, в 1.28 через KEP-3331 завезли возможность доверять нескольким провайдерам аутентификации - об этом летом писал Флант


  1. AlexGluck
    27.12.2023 15:06
    -1

    Кстати JWT токены депрекейтнуты, потратьте время сейчас, чтобы через год после обновления гитлаба получить кактус, вместо работающего сервиса.


    1. Inlore Автор
      27.12.2023 15:06
      +1

      Если внимательно читать указанный deprecation, то можно узнать, что:

      1. Депрекейтятся старые версии JWT, вместо которых в 15.7 появились другие JWT, которые назвали ID Tokens, о которых и говорится в статье

      2. Будет удалён путь /-/jwks, который был алиасом к пути /oauth/discovery/keys, и который был частью старых JWT

      kube-api-server не использует путь /-/jwks. Для OIDC discovery будет по-стандарту опрашиваться путь /.well-known/openid-configuration, откуда будет получен jwks_uri с публичными ключами для проверки подписи токенов

      Не вводите людей в заблуждение. Поставил минус вашему коменту за невнимательность


      1. AlexGluck
        27.12.2023 15:06
        +1

        Это уже третья версия токенов (не факт что последняя), не проще внутри куба поставить раннер с ролью доступа к Кубу? Не будет зависимости от авторизации на внешнем сервисе, что поднимет стабильность.


        1. mayorovp
          27.12.2023 15:06
          +1

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

          Возможность достать информацию из токена и проверить независимым от пайплайна способом выглядит куда надёжнее.


          1. AlexGluck
            27.12.2023 15:06

            Для деплоя же протектед параметр на ранере надо использовать и уже протектед теги\ветки управляются правами в гитлабе.


            1. mayorovp
              27.12.2023 15:06

              Они там слишком бинарно управляются.

              Пользователь либо имеет право пушить в защищённую ветку - тогда у него потенциальный доступ ко всем секретам. Либо не имеет таких прав - тогда доступа нет.


        1. Inlore Автор
          27.12.2023 15:06
          +1

          Это уже третья версия токенов (не факт что последняя)

          И... какой вывод вы хотите сделать из данного утверждения?

          Не будет зависимости от авторизации на внешнем сервисе, что поднимет стабильность

          Не согласен. Получается, вы под одну гребёнку сгребаете всякие dex, keycloak, vault и прочие внешние сервисы, "понижающие стабильность"

          На тему раннера в кубе вам уже сказали, что права будут общие для запускающих джобу, а я добавлю, что protected не запрещает запускать джобу конкретным юзерам. Да - этот вариант рабочий, да - он проще, но это не является альтернативой авторизации в кластере каждого юзера под своей УЗ


          1. AlexGluck
            27.12.2023 15:06
            -2

            Я вот не могу в менеджед кубере яндекс, селектела, вк, сберклауд сделать такую авторизацию как вы предлагаете, зато могу ранер поставить с правами. Выходит что решение частное и подходит под ваши случаи. Альтернатива же моя общая. Ну и я про деплой, а не персонифицированную авторизацию. Для неё действительно можно подбирать инструмент, но вряд ли в той конфигурации что вы предложили.