Skip to content

Instantly share code, notes, and snippets.

@vorlon001
Created September 23, 2026 18:43
Show Gist options
  • Select an option

  • Save vorlon001/0863adfae1bdc7be53c13d6650197295 to your computer and use it in GitHub Desktop.

Select an option

Save vorlon001/0863adfae1bdc7be53c13d6650197295 to your computer and use it in GitHub Desktop.
Kubernetes анонимус или почему все не работает с токеном

Для того чтобы детально разобраться в проблеме, рассмотрим архитектуру процесса kubeadm join (фаза bootstrap-discovery), проанализируем текущую логику кода Kubernetes v1.37.0 и определим, почему возникает ошибка 401 Unauthorized.


Архитектурная суть процесса Discovery

Когда новый узел (воркер) пытается присоединиться к кластеру, он находится в состоянии «нулевого доверия»:

  1. У него нет клиентских сертификатов для авторизации в API-сервере.
  2. У него нет корневого сертификата CA кластера, чтобы проверить, является ли мастер-узел подлинным, а не поддельным (защита от атаки Man-in-the-Middle).

Поэтому процедура kubeadm join по умолчанию разделена на два изолированных этапа:

  • Этап 1 (Анонимный): Узел запрашивает публично доступный файл конфигурации cluster-info из пространства имен kube-public. Запрос идет без авторизации (анонимно). Получив этот ConfigMap, узел сверяет содержащуюся в нем подпись JWS с хэшем, переданным в параметре --discovery-token-ca-cert-hash. Если хэши совпадают — мастер признается доверенным.
  • Этап 2 (Авторизованный): Убедившись в подлинности мастера, узел использует секретный Bootstrap-токен для полноценной регистрации.

Почему не работает «как есть» (Анализ текущей архитектуры и кода)

Ошибка 401 Unauthorized на Этапе 1 означает, что kube-apiserver отклонил анонимный запрос. Это происходит в одной из двух ситуаций:

  1. Администратор кластера принудительно отключил анонимный доступ на самом API-сервере с помощью флага --anonymous-auth=false.
  2. В кластере нарушены или удалены стандартные правила RBAC, которые разрешают группе system:unauthenticated (анонимам) читать данный ConfigMap.

Как это реализовано в коде v1.37.0 сейчас:

  1. Создание пустого (анонимного) конфигурационного файла: В файле cmd/kubeadm/app/discovery/token/token.go функция RetrieveValidatedClusterInfo намеренно инициализирует объект конфигурации без указания параметров AuthInfo (токенов или ключей) [1.32].
  • Файл: cmd/kubeadm/app/discovery/token/token.go
  • Строки кода: ~125–130
config := clientcmdapi.NewConfig()
config.Clusters[""] = &clientcmdapi.Cluster{
	Server:                endpoint,
	InsecureSkipTLSVerify: true,
}// Секция config.AuthInfos отсутствует. Данные пользователя не заполняются.
  1. Превращение в транспортный клиент client-go: Затем этот пустой конфиг передается в утилиту преобразования.
  • Файл: cmd/kubeadm/app/util/kubeconfig/kubeconfig.go
  • Строки кода: ~70–82 (Функция ToClientSet)
func ToClientSet(config *clientcmdapi.Config) (clientset.Interface, error) {
	clientConfig, err := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}).ClientConfig()
	if err != nil {
		return nil, err
	}
	return clientset.NewForConfig(clientConfig)
}

Метод .ClientConfig() возвращает структуру *rest.Config, в которой поля BearerToken, CertData и KeyData остаются пустыми.

  1. Отправка запроса в сеть:

Функция getClusterInfo осуществляет чтение ресурса [1.32].

  • Файл: cmd/kubeadm/app/discovery/token/token.go
  • Строки кода: ~239–241
func getClusterInfo(ctx context.Context, client kitclient.Interface) (*corev1.ConfigMap, error) {
	return client.CoreV1().ConfigMaps(metav1.NamespacePublic).Get(ctx, bootstrapapi.ConfigMapClusterInfo, metav1.GetOptions{})
}

Поскольку rest.Config пуст, сетевой транспорт client-go (staging/src/k8s.io/client-go/transport/transport.go) собирает стандартный HTTP-пакет без заголовка Authorization.

Если на стороне kube-apiserver анонимный доступ запрещен, сервер моментально возвращает 401 Unauthorized, прерывая выполнение kubeadm join.


Как должно быть в работающем кластере (Как исправить без изменения кода)

Чтобы текущая архитектура Kubernetes работала корректно, kube-apiserver должен быть настроен в соответствии со стандартными манифестами инсталляции.

  1. Флаг анонимного доступа в манифесте API-сервера (/etc/kubernetes/manifests/kube-apiserver.yaml) должен находиться в состоянии --anonymous-auth=true (это значение по умолчанию).
  2. Должна существовать стандартная системная роль system:bootstrap-signer-clusterinfo, разрешающая доступ к ConfigMap. Если она повреждена, доступ восстанавливается стандартной командой RBAC на управляющем узле (Control Plane):
kubectl create clusterrolebinding kubeadm:bootstrap-signer-clusterinfo \
  --clusterrole=system:bootstrap-signer-clusterinfo \
  --group=system:unauthenticated

Это правило разрешает группе неаутентифицированных пользователей (system:unauthenticated) выполнять операцию GET для cluster-info, обеспечивая безопасное прохождение Этапа 1.


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

Если бы разработчики Kubernetes отказались от концепции анонимного Discovery (например, заложив условие, что администратор обязан заранее вручную скопировать CA-сертификат на воркер-ноду через защищенный канал, тем самым решив проблему доверия к мастеру), то код kubeadm использовал бы токен авторизации с самого первого запроса.

Модифицированный вариант архитектуры в коде:

  1. Изменение инициализации конфига в token.go: В функцию RetrieveValidatedClusterInfo (строки ~125) потребовалось бы передавать строку токена discovery и явно внедрять её в структуру AuthInfo:
// Гипотетический измененный код kubeadm
config := clientcmdapi.NewConfig()
config.Clusters[""] = &clientcmdapi.Cluster{
	Server:                endpoint,
	InsecureSkipTLSVerify: false, // Изменяем на false, так как теперь мы обязаны передать CA локально
}
// Прямое добавление токена в конфигурацию аутентификации:
config.AuthInfos[""] = &clientcmdapi.AuthInfo{
	Token: discoveryToken, // Например: "bu5tiw.yourtokensecretstring"
}
  1. Поведение на транспортном уровне (client-go): При такой конфигурации внутренний метод createRoundTripper в файле staging/src/k8s.io/client-go/transport/transport.go (строки ~100–120) отработал бы по альтернативной ветке:
// Этот блок сработал бы автоматически, обнаружив токен в rest.Configif len(config.BearerToken) > 0 {
	rt = NewBearerAuthRoundTripper(config.BearerToken, rt)
}
  1. Результат в сети: При вызове метода Get(...) в функции getClusterInfo транспортный уровень добавил бы к HTTP-пакету заголовок авторизации. Запрос перестал бы быть анонимным:
GET /api/v1/namespaces/kube-public/configmaps/cluster-info HTTP/1.1
Host: 192.168.10.188:6443
User-Agent: kubeadm/v1.37.0 (linux/amd64) kubernetes/f54c212
Accept: application/vnd.kubernetes.protobuf,application/json
Authorization: Bearer bu5tiw.yourtokensecretstring

В этом гипотетическом случае API-сервер сразу определил бы пользователя по токену (system:bootstrap-token:...) и отдал бы ConfigMap даже при полностью выключенном анонимном доступе (--anonymous-auth=false). Однако это потребовало бы наличия преднастроенного локального CA-файла на стороне воркера. Если вы хотите исправить текущую ошибку в вашей среде, сообщить ли вам, как проверить конфигурацию флага --anonymous-auth в манифесте вашего kube-apiserver или как проверить текущий статус роли ClusterRoleBinding для анонимных пользователей?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment