Короткая история об отладке: файл учётных данных двухлетней давности, кластер EKS и цепочка учётных данных AWS, которая молча делала именно то, для чего была создана.
Ко мне обратились по поводу кластера EKS, к которому не удавалось подключиться с бастиона. Каждая команда kubectl возвращала одно и то же сообщение:
[E0511 11:19:38 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list:
the server has asked for the client to provide credentials"
error: You must be logged in to the server (the server has asked for the client to provide credentials)
Кластер был здоров. Конфиг kubeconfig выглядел правильным. Бастион работал «до прошлой недели, вроде бы». А единственное, что пользователь недавно менял, — это… ничего.
Ниже — короткий разбор того, как удалось распутать этот случай, и урок, который меня немного удивил, — хотя в ретроспективе документация AWS излагает всё совершенно чётко.
Ложный рефлекс
Первый ложный рефлекс — начать копаться в Kubernetes.
-
Неправильно настроен RBAC?
-
Отсутствует роль в
aws-authConfigMap? -
Кто-то создал новую запись доступа и забыл про бастион?
-
API-эндпоинт стал приватным?
Я сам попадал в эту ловушку. Сообщение об ошибке гласит: «Сервер запросил у клиента учётные данные». На первый взгляд — проблема аутентификации в Kubernetes. Но формулировка точная, и её стоит перечитать внимательно:
API-сервер Kubernetes получил запрос, посмотрел на bearer-токен и отклонил его.
Это не RBAC. RBAC возвращает Forbidden. Это сбой аутентификации — сам токен оказался недействительным. Значит, вопрос в другом: кто выписал токен?
На кластере EKS ответ почти всегда один — exec-плагин aws eks get-token в вашем kubeconfig.
Послойная проверка вместо угадывания
Это подтвердил kubeconfig:
users:
- name: arn:aws:eks:<region>:<account-id>:cluster/<cluster-name>
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: aws
args:
- --region
- <region>
- eks
- get-token
- --cluster-name
- <cluster-name>
Стандартный exec-плагин EKS. kubectl вызывает aws eks get-token, который подписывает presigned-URL к STS, используя те учётные данные AWS, которые CLI удаётся найти, и передаёт результат API-серверу в виде bearer-токена.
Это означает, что сбой мог произойти на любом из четырёх уровней:
-
Сам
kubectl -
Вызов exec-плагина
-
AWS CLI / цепочка учётных данных (credential chain)
-
Маппинг авторизации на стороне API-сервера EKS
Вместо того чтобы угадывать, я проверил самый дешёвый уровень первым:
$ aws sts get-caller-identity
An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity operation:
The security token included in the request is invalid.
Сам STS отвергал учётные данные. Одна эта команда закрывает всё расследование: если sts get-caller-identity не работает, ничто, что стоит ниже по цепочке, работать не может. Kubernetes был исправен. Сломана была аутентификация AWS.
Читайте сообщение об ошибке дословно
Коды ошибок AWS точны. Они не взаимозаменяемы, и умение различать их экономит много времени:
| Ошибка | Что это означает |
|---|---|
|
Идентификатор ключа доступа (access key ID) не существует в IAM. Удалён, отключён или изначально недействителен. |
|
Ключ доступа существует, но секретный ключ неверен. |
|
Временный токен сессии истёк. Нужно обновить. |
|
Учётные данные действительны, но политика IAM не разрешает это действие. |
У нас был InvalidClientTokenId. Это значит, что AWS прямо сейчас ничего не знает о таком ключе. Либо его никогда не существовало, либо он был ротирован или удалён где-то выше по цепочке, а бастион об этом так и не узнал.
Откуда на самом деле брались учётные данные
$ aws configure list
access_key *** shared-credentials-file
secret_key *** shared-credentials-file
region <region> imds
Итак, AWS CLI брал ключ доступа из shared-credentials-file, а регион — из сервиса метаданных EC2 (IMDS). Быстрый взгляд на файл:
$ ls -la ~/.aws/
-rw------- 1 ubuntu ubuntu 116 Sep 7 2023 credentials
Файл с учётными данными не трогали с сентября 2023 года. Два с половиной года. Статичный ключ IAM-пользователя, лежавший на общем бастионе с тех пор, когда половины нынешней команды ещё и в помине не было.
Скорее всего, произошло следующее: где-то по ходу дела ключ этого IAM-пользователя был ротирован или сам пользователь был удалён при очистке, но бастион никто не обновил, потому что ничего не сломалось немедленно. Сессии остаются закешированными. Долгоживущие поды продолжают работать. Проблема всплывает только тогда, когда что-то вынуждает сделать новый вызов STS — например, aws eks get-token.
Запасной вариант, которым никто не пользовался
На бастионе уже был прикреплён профиль экземпляра (instance profile):
$ curl -sH "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/info
{
"InstanceProfileArn": "arn:aws:iam::<account-id>:instance-profile/ec2-ssm-role",
...
}
Эта роль уже была добавлена в записи доступа (access entries) кластера EKS. Она была авторизована. Она ждала своего часа. Просто ею никто не пользовался, потому что цепочка учётных данных AWS читает ~/.aws/credentials раньше, чем обращается к IMDS. Мёртвый статичный ключ молча перекрывал прекрасно работающий instance profile.
Этот момент стоит запомнить. Согласно документации AWS, порядок разрешения учётных данных по умолчанию примерно такой:
-
Переменные окружения (
AWS_ACCESS_KEY_IDи др.) -
Файл shared credentials (
~/.aws/credentials) -
SSO / web identity / профили из файла конфигурации
-
Учётные данные контейнера ECS / EKS
-
Профиль экземпляра EC2 через IMDS
Если на хосте есть статичный файл учётных данных и одновременно прикреплён instance profile — выигрывает файл. Всегда. Даже если ключ в нём протух два года назад.
Обратимая проверка вместо гипотетических рассуждений
На этом этапе у меня была гипотеза: если убрать с дороги плохой файл учётных данных, CLI провалится до instance profile, и всё заработает.
Самый безопасный способ проверить это, не потеряв данные, — переименовать файл, а не удалять, и обернуть тест в shell-ловушку trap, которая восстановит файл при выходе:
mv ~/.aws/credentials ~/.aws/credentials.disabled.$$
trap "mv ~/.aws/credentials.disabled.$$ ~/.aws/credentials" EXIT
aws sts get-caller-identity
# {
# "UserId": "AROAxxxxxxxxxxxxxxxxx:i-0xxxxxxxxxxxxxxxx",
# "Account": "<account-id>",
# "Arn": "arn:aws:sts::<account-id>:assumed-role/ec2-ssm-role/i-0xxxxxxxxxxxxxxxx"
# }
kubectl get ns
# NAME STATUS AGE
# default Active ...
# kube-system Active ...
# ...
Сработало. Instance profile взял управление на себя, STS вернул действительную идентификацию, aws eks get-token выписал настоящий bearer-токен, и API-сервер EKS его принял.
Мне нравится такой стиль проверки. Одна команда. Обратимо по замыслу. Если бы гипотеза оказалась ошибочной, ловушка EXIT вернула бы файл точно в прежнее состояние, и бастион остался бы нетронутым. Когда не понимаешь, что происходит, предпочитай однострочные эксперименты, которые можно отменить.
Исправление
Постоянное исправление на бастионе — то же самое, что сделала ловушка, только без ловушки:
mv ~/.aws/credentials ~/.aws/credentials.dead-key-2026
Переименовать, не удалять. Сохранить след для аудита. Больше ничего на хосте трогать не потребовалось — ни правок в kubeconfig, ни изменений в IAM, ни работы с политиками. Кластер EKS, роль экземпляра и записи доступа уже были настроены правильно.
Первопричина находилась вовсе не на стороне кластера. Это был устаревший файл на одном-единственном EC2-хосте.
Уроки на будущее
-
aws sts get-caller-identity— самая дешёвая первая проверка. Если STS недоволен, ничто, построенное поверх него, работать не будет. Это должна быть первая команда при любой ошибке с учётными данными в AWS-инструментах — не десятая. -
Коды ошибок AWS точны. Читайте их медленно.
InvalidClientTokenId,SignatureDoesNotMatchиExpiredToken— три разные проблемы с тремя разными решениями. Сообщение об ошибке — это данные. -
Долгоживущие ключи IAM-пользователей на общих хостах устаревают незаметно. Ключи ротируются где-то выше по цепочке. Пользователи удаляются при плановой очистке. Хост об этом не узнаёт, пока что-то не вынудит сделать новый вызов STS. Краткосрочные STS-учётные данные, IAM-роли или сессии IAM Identity Center лишены этого изъяна.
-
Обратимые проверки лучше правдоподобных гипотез. Переименование файла — однострочный эксперимент, устанавливающий причинно-следственную связь без риска для системы. Если гипотеза ошибочна — ничего не потеряно. Если верна — исправление уже задокументировано.
-
Формулировка ошибки часто указывает, на каком уровне искать. «Сервер запросил у клиента учётные данные» означает конкретно, что API-сервер отверг bearer-токен. Это указывает на генерацию токена, а не на RBAC, сеть или сам кластер.
Карманный рецепт диагностики kubectl 401 на кластере с поддержкой AWS
# 1. AWS вообще доволен?
aws sts get-caller-identity
# 2. Откуда, по мнению CLI, берутся учётные данные?
aws configure list
# 3. Может ли exec-плагин из kubeconfig вернуть токен?
kubectl config view --minify --raw # найти exec-команду
aws --region <r> eks get-token --cluster-name <c>
# 4. Только если все три шага выше успешны — смотреть на авторизацию EKS:
aws eks describe-cluster --name <c> --query 'cluster.accessConfig'
aws eks list-access-entries --cluster-name <c>
Если шаг 1 провалился — остановитесь. Это проблема с учётными данными AWS, а не с Kubernetes. Всё остальное, что вы будете делать до исправления шага 1, — впустую.
Итог
kubectl 401 против кластера EKS чаще всего укажет вам совсем не на Kubernetes. API-сервер Kubernetes — последний уровень в длинной цепочке: kubeconfig → exec-плагин → AWS CLI → цепочка учётных данных → STS → presigned URL → bearer-токен — и большая часть этой цепочки живёт в мире AWS.
Идите по цепочке от самого дешёвого уровня к самому дорогому. Читайте сообщение об ошибке точно так, как оно написано. Предпочитайте проверки, которые можно отменить. И если вы когда-нибудь найдёте файл ~/.aws/credentials с датой двухлетней давности на хосте, к которому также прикреплён instance profile, — скорее всего, проблема именно в нём.
Теги: AWS, EKS, Kubernetes, kubectl, IAM, DevOps, Debugging, SRE