Аутентификация сервисов через Kubernetes SA

Kubernetes как источник идентификации: аутентификация между микросервисами

Вкратце: в этой статье мы используем Kubernetes Service Accounts и Token Review API для аутентификации запросов между двумя сервисами внутри кластера, а затем улучшим схему с помощью привязанных к аудитории (audience-bound) проецируемых токенов Service Account.

Это третья статья из четырёхчастной серии о том, как запрос проходит через API-сервер Kubernetes.

Когда инфраструктура состоит из нескольких взаимодействующих приложений, рано или поздно встаёт вопрос о защите коммуникаций между сервисами — чтобы исключить приём неаутентифицированных запросов.

Представьте два приложения:

  • API-сервис

  • Хранилище данных (Data store)

Нам может понадобиться, чтобы хранилище отвечало только на запросы от API и отклоняло все остальные.

Как хранилище данных будет решать, пропустить запрос или отклонить?

Распространённый подход — запрашивать и передавать токены идентификации при каждом межсервисном вызове.

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

Токен несёт в себе контекст, который позволяет хранилищу принять запрос от API-сервиса и отклонить его от любого другого источника.

Именно этот контекст используется для разрешения или запрета запроса.

  • 1/4 Поступает запрос к API-компоненту.

  • 2/4 Единственный способ для API аутентифицироваться перед хранилищем данных — иметь действительный токен. API запрашивает токен у сервера авторизации, используя свои учётные данные.

  • 3/4 API обращается к хранилищу данных и прикладывает токен как подтверждение своей идентичности.

  • 4/4 Перед ответом на запрос хранилище данных проверяет токен через сервер авторизации.

При реализации этого механизма аутентификации есть несколько вариантов:

Можно использовать статические токены без срока действия. В этом случае отдельный сервер аутентификации не нужен. Можно развернуть OAuth-сервер. Можно реализовать собственный механизм аутентификации и авторизации — например, на основе взаимного TLS (mutual TLS).

Задача любого сервера аутентификации и авторизации сводится к следующему:

Аутентифицировать вызывающую сторону — у неё должна быть действительная и проверяемая идентичность. Сгенерировать токен с ограниченной областью действия, сроком жизни и нужной аудиторией. Проверить токен — взаимодействие между сервисами допустимо только при наличии легитимного токена и при условии, что вызывающая сторона принята целевым сервисом.

Среди готовых инструментов, реализующих инфраструктуру аутентификации и авторизации, выделяются Keycloak и Dex.

При работе с Keycloak процесс выглядит так:

  • Пользователь входит по email и паролю — его личность проверяется.

  • Для него создаётся сессия, в которой может быть указано членство в группах.

  • Каждый последующий запрос проходит проверку; при недействительной сессии система потребует повторного входа.

Аналогичная схема работает для двух приложений в инфраструктуре.

  • Бэкенд-компонент обращается к Keycloak с API-ключом и секретом, чтобы получить токен сессии.

  • Бэкенд обращается ко второму приложению, передавая токен сессии.

  • Второе приложение извлекает токен из запроса и проверяет его через Keycloak.

  • Если токен действителен — отвечает на запрос.

Что важно не упустить: Kubernetes предоставляет готовые примитивы для реализации аутентификации и авторизации к Kubernetes API — Service Accounts, Roles и RoleBindings.

Kubernetes как центр выдачи и проверки токенов

В Kubernetes идентичности назначаются через Service Accounts.

Поды могут использовать эти идентичности для аутентификации в API и отправки запросов.

Service Accounts связываются с ролями (Roles), которые определяют доступ к ресурсам.

Если роль разрешает создавать и удалять поды, она не позволит изменять Secrets или создавать ConfigMaps.

Можно ли использовать Service Accounts для аутентификации запросов между приложениями в кластере?

Что если Kubernetes API использовать как сервис выдачи и проверки токенов?

Давайте проверим.

Создание кластера

Нам нужен кластер Kubernetes, в котором поды могут использовать проецирование тома токена Service Account (Service Account Token Volume projection).

Если проецируемые тома токенов Service Account — незнакомая концепция, не беспокойтесь: статья объясняет её позже.

Kubernetes монтирует токены Service Account пода через проецируемые тома. В демонстрации аудитория API-сервера настроена как api, чтобы примеры TokenReview давали предсказуемый вывод.

Поддержка изменения аудитории API-сервера варьируется у разных управляемых провайдеров Kubernetes.

Локальный кластер в minikube с нужной для этой статьи аудиторией запускается так:

minikube start \
--extra-config=apiserver.service-account-signing-key-file=/var/lib/minikube/certs/sa.key \
--extra-config=apiserver.service-account-key-file=/var/lib/minikube/certs/sa.pub \
--extra-config=apiserver.service-account-issuer=kubernetes/serviceaccount \
--extra-config=apiserver.api-audiences=api

Исходный код демонстрации доступен в репозитории kubernetes-service-account-auth-demo.

Манифесты в этой статье используют публичные образы api и data-store, поэтому собирать образы локально не нужно.

Мы развернём два сервиса:

  • Мы будем называть их API-сервисом и хранилищем данных.

  • Они написаны на языке Go и взаимодействуют по HTTP.

  • Каждый сервис работает в отдельном пространстве имён (namespace) и использует собственный Service Account.

  • Хранилище данных успешно отвечает на запросы только при наличии у вызывающей стороны действительной и разрешённой идентичности; в противном случае оно отклоняет запрос с ошибкой.

Пример сосредоточен на аутентификации идентичности вызывающей стороны и не заменяет TLS для шифрования трафика между сервисами.

Развёртывание API-компонента

API-сервис — это веб-приложение, слушающее на порту 8080.

При поступлении запроса API-компонент:

  • Отправляет HTTP GET запрос к хранилищу данных, прикладывая свою идентичность Service Account.

  • Передаёт ответ обратно вызывающей стороне.

Развернуть приложение, создать для него сервис в кластере и дождаться готовности можно так:

BASE=https://raw.githubusercontent.com/learnk8s/kubernetes-service-account-auth-demo/master
kubectl apply -f "${BASE}/service_accounts/api/deployment.yaml"
namespace/api created
serviceaccount/api created
deployment.apps/app created
service/app created
kubectl --namespace api rollout status deployment/app
deployment "app" successfully rolled out

URL API-сервиса можно получить командой:

API_URL=$(minikube --namespace api service app --url)
printf '%s\n' "${API_URL}"
http://192.168.49.2:31796

Если отправить запрос к этому приложению, получим ли мы успешный ответ?

Проверим:

curl -sS "${API_URL}"
Get "http://app.data-store.svc.cluster.local": dial tcp: \
lookup app.data-store.svc.cluster.local on 10.96.0.10:53: no such host

Ошибка ожидаема: мы ещё не развернули хранилище данных.

Оставим этот терминал открытым.

В новом терминале выполним следующие шаги.

Развёртывание хранилища данных

Хранилище данных — ещё одно веб-приложение, слушающее на порту 8081.

При получении любого запроса хранилище данных:

  • Ищет токен в заголовке запроса. Если его нет — отвечает с HTTP 401.

  • Проверяет токен через Kubernetes API на предмет действительности. Если токен недействителен — отвечает с HTTP 403.

  • Если токен действителен и принадлежит разрешённой идентичности — обрабатывает исходный запрос.

Создать хранилище данных и дождаться его готовности:

BASE=https://raw.githubusercontent.com/learnk8s/kubernetes-service-account-auth-demo/master
kubectl apply -f "${BASE}/service_accounts/data-store/deployment.yaml"
namespace/data-store created
serviceaccount/data-store created
clusterrolebinding.rbac.authorization.k8s.io/role-tokenreview-binding created
deployment.apps/app created
service/app created
kubectl --namespace data-store rollout status deployment/app
deployment "app" successfully rolled out

Теперь снова отправим запрос к API-сервису через curl:

curl -sS "${API_URL}"
Hello from data store. You have been authenticated

Хранилище данных успешно проверило токен и ответило на запрос.

API передаёт ответ обратно нам.

А что если обратиться к хранилищу данных напрямую?

Сервис хранилища данных имеет тип ClusterIP, поэтому откроем его локально через port-forward:

kubectl port-forward --namespace data-store service/app 8081:80
Forwarding from 127.0.0.1:8081 -> 8081
Forwarding from [::1]:8081 -> 8081

Отправим запрос через curl:

curl -sS http://127.0.0.1:8081
X-Client-Id not supplied

Не работает.

Попробуем передать недействительный заголовок X-Client-Id:

curl -sS -H 'X-Client-Id: invalid-token' http://127.0.0.1:8081
Invalid token

Отлично!

Тоже не работает!

Мы защитили хранилище данных от неаутентифицированного доступа с помощью Kubernetes и Service Accounts.

Запросы к нему принимаются только при наличии действительного и принятого токена.

Но как всё это работает? Разберёмся подробнее.

Под капотом

Service Accounts — это способ связать рабочие нагрузки Kubernetes с конкретной идентичностью.

Объединив Service Account с Role и RoleBinding, мы определяем, кто и к каким ресурсам кластера имеет доступ.

Например, чтобы ограничить чтение Secrets только для администраторов кластера, можно воспользоваться RBAC.

  • 1/4 Service Accounts — это идентичности рабочих нагрузок. Обычно они назначаются подам.

  • 2/4 Roles — это список разрешений, привязанных к пространству имён. ClusterRoles — список разрешений в масштабе всего кластера.

  • 3/4 Идентичности не имеют никаких разрешений без связи с Role. Для привязки идентичностей к ClusterRole используются ClusterRoleBindings.

  • 4/4 Для привязки идентичностей к Role используются RoleBindings.

Service Accounts не предназначены для людей.

Людей аутентифицируют по учётным данным, а приложения — через Service Accounts.

Если нашему приложению нужно получить список всех подов в кластере, мы создаём Service Account с правами на чтение Pod API.

При развёртывании двух приложений ранее мы также создали два Service Account:

kubectl get serviceaccount --namespace api
NAME SECRETS AGE
api 0 62s
default 0 62s
kubectl get serviceaccount --namespace data-store
NAME SECRETS AGE
data-store 0 47s
default 0 47s

Эти Service Accounts являются идентичностями приложений, но сами по себе не определяют никаких разрешений.

Для этого можно изучить ClusterRoleBinding демонстрации:

kubectl get clusterrolebinding role-tokenreview-binding \
-o custom-columns=\
'KIND:kind,BINDING:metadata.name,ROLE:roleRef.name,SERVICE_ACCOUNTS:subjects[?(@.kind=="ServiceAccount")].name'
KIND BINDING ROLE SERVICE_ACCOUNTS
ClusterRoleBinding role-tokenreview-binding system:auth-delegator data-store

Команда выше использует пользовательские столбцы kubectl для фильтрации вывода kubectl get.

Из таблицы видно, что ClusterRoleBinding связан с ClusterRole, а указанный субъект — это Service Account.

Привязка RBAC есть только у хранилища данных.

У API нет ни RoleBinding, ни ClusterRoleBinding.

Как может существовать Service Account без Role и RoleBinding?

У API-приложения есть Service Account без каких-либо прикладных разрешений.

Однако этот Service Account можно использовать для аутентификации запросов к Kubernetes API (при этом создавать, обновлять или удалять ресурсы всё равно нельзя).

А что насчёт хранилища данных?

Какой у него доступ?

Изучим ClusterRoleBinding:

kubectl describe clusterrolebinding role-tokenreview-binding
Name: role-tokenreview-binding
Labels: <none>
Annotations: <none>
Role:
Kind: ClusterRole
Name: system:auth-delegator
Subjects:
Kind Name Namespace
---- ---- ---------
ServiceAccount data-store data-store

Из вывода видно, что ClusterRoleBinding связывает Service Account data-store с ClusterRole system:auth-delegator.

Какие разрешения предоставляет эта ClusterRole?

Узнаем:

kubectl describe clusterrole system:auth-delegator
Name: system:auth-delegator
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
tokenreviews.authentication.k8s.io [] [] [create]
subjectaccessreviews.authorization.k8s.io [] [] [create]

ClusterRole system:auth-delegator предоставляет права на вызов Token Review API и SubjectAccessReview API.

Что это за разрешения?

Проверить разрешения можно через kubectl с подкомандой can-i и флагом имперсонации --as:

kubectl auth can-i create deployments --as=system:serviceaccount:data-store:data-store --namespace data-store
no
kubectl auth can-i list pods --as=system:serviceaccount:data-store:data-store --namespace data-store
no
kubectl auth can-i delete services --as=system:serviceaccount:data-store:data-store --namespace data-store
no

Можно перебрать все ресурсы Kubernetes, но у этого Service Account есть только делегированные разрешения на проверку аутентификации и авторизации.

kubectl auth can-i create tokenreviews --as=system:serviceaccount:data-store:data-store --all-namespaces
yes

Что такое TokenReview?

Запросы к Kubernetes API

Kubernetes API проверяет идентичности Service Account.

За их валидацию и отклонение отвечает специальный компонент — Token Review API.

Token Review API принимает токены и возвращает результат их проверки — да, всё именно так просто.

Давайте вручную проверим идентичность API-компонента через Token Review API.

Поскольку это Token Review API, нам нужен токен.

Какой именно?

Когда под использует Service Account, Kubernetes монтирует в под токен этого Service Account.

Именно этот токен нужно проверять через Token Review API.

Демонстрационный образ не содержит утилит вроде cat, поэтому вместо чтения смонтированного файла через kubectl exec мы генерируем эквивалентный токен для того же Service Account и сохраняем его в переменную:

TOKEN=$(kubectl create token api --namespace api --audience api)
test -n "${TOKEN}"

Токен — это подписанный JSON Web Token (JWT).

Настало время проверить токен.

Для проверки действительности токена создадим ресурс TokenReview и выведем нужные поля:

TOKEN=$(kubectl create token api --namespace api --audience api)
kubectl create -f - \
-o jsonpath='{.status.authenticated}{"\n"}{.status.user.username}{"\n"}{.status.audiences[0]}{"\n"}' \
<<EOF
apiVersion: authentication.k8s.io/v1
kind: TokenReview
spec:
token: ${TOKEN}
EOF
true
system:serviceaccount:api:api
api

Обратите внимание на флаг -o jsonpath, который выводит только поля статуса, возвращаемые командой kubectl create.

Ключевая информация содержится в объекте status со следующими полями:

  • Authenticated: значение true означает, что токен успешно прошёл проверку.

  • В объекте user находятся следующие свойства:

    • username соответствует Service Account пода — system:serviceaccount:api:api.

    • uid аутентифицированного Service Account.

    • Groups — группы, к которым относится пользователь.

  • Audiences содержит аудитории, принятые для этого TokenReview. Поскольку в запросе не задан spec.audiences, Kubernetes проверяет токен для аудитории API-сервера, которая в этом кластере равна api.

Отлично, мы только что проверили токен Service Account!

Нам теперь известно:

  • Токен действителен.

  • Идентичность вызывающей стороны (Service Account API-сервиса).

  • Группы, к которым принадлежит вызывающая сторона.

Раз мы можем проверять токены, этот механизм можно использовать в компоненте хранилища данных для аутентификации запросов!

При этом хранилище данных должно само решать, какие идентичности оно принимает.

Посмотрим, как реализовать описанную логику в наших приложениях с помощью Go-клиента Kubernetes.

Реализация сервисов

Вот как два сервиса взаимодействуют друг с другом и с Kubernetes API:

  • Перед каждым исходящим запросом API-компонент читает актуальный токен Service Account с диска.

  • API-компонент обращается к хранилищу данных, передавая токен в HTTP-заголовке X-Client-Id.

  • Получив запрос, хранилище данных читает токен из заголовка X-Client-Id и отправляет запрос к Token Review API для проверки его действительности.

  • Если ответ подтверждает аутентификацию и идентичность разрешена, хранилище данных отвечает успехом; в противном случае — ошибкой.

Следующая схема описывает этот поток вызовов:

  • 1/4 API-компонент получает назначенный ему токен Service Account.

  • 2/4 При поступлении запроса к API токен передаётся во всех последующих запросах.

  • 3/4 Хранилище данных извлекает токен из запроса.

  • 4/4 Хранилище данных проверяет идентичность через Token Review API.

Начнём с реализации API-сервиса.

Код приложения находится в файле service_accounts/api/main.go.

Токен Service Account автоматически монтируется по пути /var/run/secrets/kubernetes.io/serviceaccount/token, и его значение можно прочитать так:

func readToken() (string, error) {
b, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
if err != nil {
return "", err
}
return string(b), nil
}

При стандартном поведении привязанного тома токена Service Account Kubernetes монтирует краткосрочные токены, поэтому пример читает токен с диска перед каждым запросом, а не кеширует его навсегда.

Затем токен передаётся в запрос к хранилищу данных через HTTP-заголовок X-Client-Id:

func handleIndex(w http.ResponseWriter, r *http.Request) {
serviceConnstring := os.Getenv("DATA_STORE_CONNSTRING")
if len(serviceConnstring) == 0 {
panic("DATA_STORE_CONNSTRING expected")
}
serviceToken, err := readToken()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, serviceConnstring, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
req.Header.Add("X-Client-Id", serviceToken)
resp, err := client.Do(req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
}

После получения ответа от хранилища данных он сразу же отправляется обратно:

w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)

Для развёртывания API-сервиса используется следующий YAML-манифест:

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: api
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: api
namespace: api
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: api
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
serviceAccountName: api
containers:
- name: app
image: ghcr.io/learnk8s/api:sa-1
env:
- name: LISTEN_ADDR
value: ":8080"
- name: DATA_STORE_CONNSTRING
value: "http://app.data-store.svc.cluster.local"
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: app
namespace: api
spec:
type: NodePort
selector:
app: api
ports:
- port: 8080
targetPort: 8080
EOF
namespace/api created
serviceaccount/api created
deployment.apps/app created
service/app created

Обратите внимание: в манифесте Deployment нет ничего особенного, кроме привязки Service Account.

Перейдём к сервису хранилища данных.

Полный код приложения находится в service_accounts/data-store/main.go.

Хранилище данных выполняет два ключевых действия:

  • Читает значение заголовка X-Client-Id из входящего запроса.

  • Вызывает Token Review API Kubernetes для проверки действительности токена.

Шаг (1) выполняется следующим кодом:

clientId := r.Header.Get("X-Client-Id")
if len(clientId) == 0 {
http.Error(w, "X-Client-Id not supplied", http.StatusUnauthorized)
return
}

Шаг (2) выполняется через Go-клиент Kubernetes.

Сначала создаём объект ClientSet:

config, err := rest.InClusterConfig()
clientset, err := kubernetes.NewForConfig(config)

Функция InClusterConfig() автоматически читает токен Service Account для пода, поэтому указывать путь вручную не нужно.

Затем строим объект TokenReview, указывая токен для проверки в поле Token:

tr := authv1.TokenReview{
Spec: authv1.TokenReviewSpec{
Token: clientId,
},
}

Поскольку в этом TokenReview поле Audiences не задано, токен проверяется для аудитории API-сервера Kubernetes, а не для специфичной для хранилища данных аудитории.

Наконец, выполняем запрос TokenReview:

`result, err := clientset.AuthenticationV1().TokenReviews().Create(ctx, &tr, metav1.CreateOptions{})`

Хранилище данных должно проверить как результат аутентификации, так и идентичность:

if !result.Status.Authenticated || result.Status.User.Username != allowedUsername {
http.Error(w, "Invalid token", http.StatusForbidden)
return
}

В демонстрации allowedUsername равен system:serviceaccount:api:api, поэтому действительный токен другого Service Account всё равно будет отклонён.

Следующий YAML-манифест создаёт все необходимые ресурсы для сервиса хранилища данных:

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: data-store
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: data-store
namespace: data-store
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: role-tokenreview-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: data-store
namespace: data-store
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: data-store
spec:
replicas: 1
selector:
matchLabels:
app: data-store
template:
metadata:
labels:
app: data-store
spec:
serviceAccountName: data-store
containers:
- name: app
image: ghcr.io/learnk8s/data-store:sa-1
env:
- name: LISTEN_ADDR
value: ":8081"
ports:
- containerPort: 8081
---
apiVersion: v1
kind: Service
metadata:
name: app
namespace: data-store
spec:
selector:
app: data-store
ports:
- protocol: TCP
port: 80
targetPort: 8081
EOF
namespace/data-store created
serviceaccount/data-store created
clusterrolebinding.rbac.authorization.k8s.io/role-tokenreview-binding created
deployment.apps/app created
service/app created

В отличие от API-сервиса, для хранилища данных требуется создать ClusterRoleBinding, который связывает Service Account data-store с ClusterRole system:auth-delegator.

Вернёмся к терминалу, где разворачивали хранилище данных, и посмотрим на логи:

kubectl --namespace data-store logs deploy/app
2026/06/01 11:28:01 {
"authenticated": true,
"user": {
"username": "system:serviceaccount:api:api",
"uid": "8897c869-71f4-4025-a55c-3842ae78c96a",
"groups": [
"system:serviceaccounts",
"system:serviceaccounts:api",
"system:authenticated"
],
"extra": {
"authentication.kubernetes.io/credential-id": [
"JTI=9cf5ca37-c300-440f-b228-9db979441bca"
],
"authentication.kubernetes.io/node-name": [
"minikube"
],
"authentication.kubernetes.io/node-uid": [
"225825b2-6831-4a69-a1a3-8b80e93ca843"
],
"authentication.kubernetes.io/pod-name": [
"app-854bbcc84c-xf6vk"
],
"authentication.kubernetes.io/pod-uid": [
"81f57562-568e-45a8-8c98-de3f74cea3e5"
]
}
},
"audiences": [
"api"
]
}
2026/06/01 11:28:24 {
"user": {},
"error": "invalid bearer token"
}

Вывод — это отформатированный JSON-объект статуса TokenReview. Если мы выполняли тест с недействительным токеном, в логах также появится соответствующий отклонённый ответ TokenReview.

Мы видим, как API-компонент читает токен Service Account и передаёт его хранилищу данных для подтверждения своей идентичности.

Хранилище данных извлекает токен и проверяет его через Kubernetes API.

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

Реализация работает, но у неё есть два ограничения.

Стандартный автоматически монтируемый токен предназначен для Kubernetes API

При стандартном поведении привязанного тома токена Service Account смонтированный в поде токен уже является краткосрочным проецируемым токеном, а не долгосрочным токеном на основе Secret.

Однако этот токен в первую очередь предназначен для обращений к Kubernetes API.

Мы можем передать его другому сервису, и тот проверит его через Token Review API.

Но токен не специфичен для хранилища данных.

Иначе говоря, хранилище может проверить, кто предъявил токен, но не может удостовериться, что токен был выдан именно для него.

Кроме того, любой, кто может прочитать токен из пода API, может использовать его повторно, пока он действителен.

Если у Service Account есть права на вызов Kubernetes API, тот же токен может быть использован и для этих целей.

Поэтому разумно сохранять минимальный набор разрешений у Service Account и не использовать один Service Account для несвязанных рабочих нагрузок.

Отсутствие привязки к конкретной аудитории приложения

Целевой сервис должен проверять, что токен был предназначен именно для него.

Представьте, что вы купили билет на рейс из Лондона в Нью-Йорк.

Если билет куплен у British Airways, им нельзя воспользоваться для посадки на рейс Virgin Atlantic.

Наш билет привязан к конкретной аудитории (British Airways).

Но если хранилище данных принимает любой действительный токен Service Account без проверки аудитории, тот же «билет» сгодится для любой «авиакомпании».

Обе проблемы можно решить с помощью взаимного TLS или JWT-решений с центральным сервером авторизации.

Однако в Kubernetes можно явно создать дополнительный токен Service Account с нужным путём, сроком жизни и специфичной для приложения аудиторией.

API-сервер Kubernetes выступает в роли центрального сервера авторизации, а kubelet берёт на себя обновление токена на диске.

В следующем разделе мы переработаем код аутентификации приложений с использованием проецирования тома токена Service Account (Service Account Token Volume Projection).

Сначала удалим два пространства имён:

kubectl delete namespace api
namespace "api" deleted
kubectl delete namespace data-store
namespace "data-store" deleted
kubectl delete clusterrolebinding role-tokenreview-binding
clusterrolebinding.rbac.authorization.k8s.io "role-tokenreview-binding" deleted

Аутентификация между сервисами с помощью проецирования тома токена Service Account

Явные токены Service Account, предоставляемые рабочим нагрузкам через проецируемый источник тома serviceAccountToken, имеют ограниченный срок действия, привязаны к аудитории и не связаны с объектами Secret.

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

Проецирование тома serviceAccountToken — один из типов проецируемых (projected) томов.

Проецируемый том — это том, который монтирует несколько существующих томов в один каталог.

Когда этот тип тома добавляется в под, токен Service Account монтируется в файловую систему.

Но есть одно отличие.

Kubelet автоматически обновляет токен по мере приближения срока его истечения.

Кроме того, можно настроить путь, по которому токен будет доступен.

Посмотрим, как изменить API-компонент для использования проецирования тома токена Service Account.

API-компонент

Токен Service Account, смонтированный через проецирование тома, читается так:

func readToken() (string, error) {
b, err := os.ReadFile("/var/run/secrets/tokens/api-token")
if err != nil {
return "", err
}
log.Print("Refreshing service account token")
return string(b), nil
}

Обратите внимание: путь к токену Service Account отличается от предыдущего варианта (ранее использовался /var/run/secrets/kubernetes.io/serviceaccount/token).

Поскольку kubelet обновляет токен на диске, приложению не следует кешировать его на весь срок работы процесса.

В демонстрации токен перечитывается перед каждым исходящим запросом:

serviceToken, err := readToken()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
req.Header.Add("X-Client-Id", serviceToken)

Функция readToken() читает /var/run/secrets/tokens/api-token и возвращает актуальное значение токена.

Полный код приложения находится в service_accounts_volume_projection/api/main.go.

Теперь развернём этот сервис.

Используем образ в манифесте развёртывания (service_accounts_volume_projection/api/deployment.yaml):

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: api
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: api
namespace: api
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: api
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
serviceAccountName: api
volumes:
- name: api-token
projected:
sources:
- serviceAccountToken:
path: api-token
expirationSeconds: 600
audience: data-store
containers:
- name: app
image: ghcr.io/learnk8s/api:sa-2
env:
- name: LISTEN_ADDR
value: ":8080"
- name: DATA_STORE_CONNSTRING
value: "http://app.data-store.svc.cluster.local"
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: api-token
---
apiVersion: v1
kind: Service
metadata:
name: app
namespace: api
spec:
type: NodePort
selector:
app: api
ports:
- port: 8080
targetPort: 8080
EOF
namespace/api created
serviceaccount/api created
deployment.apps/app created
service/app created

Создаётся том api-token типа projected с источником serviceAccountToken.

Том определяет три дополнительных свойства:

  • Поле path — путь, по которому токен будет доступен внутри тома.

  • Поле audience — предназначенная аудитория для токена.

  • Поле expirationSeconds — срок действия токена; минимальное значение — 600 секунд (10 минут).

Обратите внимание: поле audience указывает, что этот токен Service Account предназначен для сервиса, принимающего аудиторию data-store.

Если хранилище данных запросит у Token Review API проверку аудитории data-store, а токен её не содержит — токен не будет принят, ведь это не его аудитория!

Учтите: если в кластере применяются стандарты безопасности подов (Pod Security Standards), убедитесь, что политика рабочей нагрузки разрешает использование данного проецируемого тома.

Создадим новое развёртывание API:

BASE=https://raw.githubusercontent.com/learnk8s/kubernetes-service-account-auth-demo/master
kubectl apply -f "${BASE}/service_accounts_volume_projection/api/deployment.yaml"
namespace/api created
serviceaccount/api created
deployment.apps/app created
service/app created
kubectl --namespace api rollout status deployment/app
deployment "app" successfully rolled out

Получим URL API-сервиса:

API_URL=$(minikube --namespace api service app --url)
printf '%s\n' "${API_URL}"
http://192.168.49.2:32687

Отправим запрос:

curl -sS "${API_URL}"
Get "http://app.data-store.svc.cluster.local": dial tcp: \
lookup app.data-store.svc.cluster.local on 10.96.0.10:53: no such host

Это ожидаемо: хранилище данных ещё не развёрнуто.

Оставим терминал открытым.

Перейдём к изменению и развёртыванию хранилища данных.

Хранилище данных

Теперь полезная нагрузка TokenReview для хранилища данных выглядит так:

tr := authv1.TokenReview{
Spec: authv1.TokenReviewSpec{
Token: clientId,
Audiences: []string{"data-store"},
},
}

Теперь в объекте TokenReview хранилище данных явно передаёт data-store в качестве аудитории.

Если токен не содержит data-store в качестве аудитории, Token Review API не аутентифицирует его для этой аудитории.

Хранилище данных также должно проверять, что в ответе TokenReview поле status.audiences содержит data-store.

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

Проверка идентичности из предыдущего раздела сохраняется: хранилище данных должно принимать только те идентичности Service Account, которым разрешено к нему обращаться.

Полный код приложения находится в service_accounts_volume_projection/data-store/main.go.

Развернём этот сервис.

Создадим развёртывание и дождёмся его готовности:

BASE=https://raw.githubusercontent.com/learnk8s/kubernetes-service-account-auth-demo/master
kubectl apply -f "${BASE}/service_accounts_volume_projection/data-store/deployment.yaml"
namespace/data-store created
serviceaccount/data-store created
clusterrolebinding.rbac.authorization.k8s.io/role-tokenreview-binding created
deployment.apps/app created
service/app created
kubectl --namespace data-store rollout status deployment/app
deployment "app" successfully rolled out

Проверим состояние сервиса:

kubectl --namespace data-store describe service app
Name: app
Namespace: data-store
Labels: <none>
Annotations: <none>
Selector: app=data-store
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.108.238.76
IPs: 10.108.238.76
Port: <unset> 80/TCP
TargetPort: 8081/TCP
Endpoints: 10.244.0.29:8081
Session Affinity: None
Internal Traffic Policy: Cluster
Events: <none>

Значение поля Endpoints в выводе показывает, что приложение запущено и работает.

Отправим запрос к API-сервису через curl:

curl -sS "${API_URL}"
Hello from data store. You have been authenticated

Посмотрим на логи хранилища данных:

kubectl --namespace data-store logs deploy/app
2026/06/01 14:46:55 {
"authenticated": true,
"user": {
"username": "system:serviceaccount:api:api",
"uid": "9e92ce88-8695-4f60-bc03-fbbc5fac1bc7",
"groups": [
"system:serviceaccounts",
"system:serviceaccounts:api",
"system:authenticated"
],
"extra": {
"authentication.kubernetes.io/credential-id": [
"JTI=f343c40a-bf08-4304-ae76-37f85d420723"
],
"authentication.kubernetes.io/node-name": [
"minikube"
],
"authentication.kubernetes.io/node-uid": [
"225825b2-6831-4a69-a1a3-8b80e93ca843"
],
"authentication.kubernetes.io/pod-name": [
"app-7c9b54c8db-lwrfr"
],
"authentication.kubernetes.io/pod-uid": [
"4d882fae-13d2-44aa-b441-6c5b08d2d4fe"
]
}
},
"audiences": [
"data-store"
]
}

Переключившись на логи API-сервиса, можно увидеть строки, демонстрирующие моменты повторного чтения токена Service Account из файловой системы:

kubectl --namespace api logs deploy/app
2026/06/01 14:46:37 Refreshing service account token
2026/06/01 14:46:55 Refreshing service account token

Итоги

Проецирование тома токена Service Account позволяет связывать с рабочими нагрузками Kubernetes неглобальные токены с ограниченным сроком действия и привязкой к аудитории.

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

Kubernetes-нативные инструменты, такие как Linkerd и Istio, используют примитивы идентификации рабочих нагрузок Kubernetes для внутренних коммуникаций. Управляемые провайдеры Kubernetes, такие как GKE и AWS EKS, применяют проецируемые токены Service Account для построения более надёжных систем идентификации подов.

В заключительной статье этой серии мы перейдём к политикам и управлению для рабочих нагрузок Kubernetes.

© 2026 meganuke