Внутри любого кластера Kubernetes живет опасная иллюзия безопасности: раз сервисы запущены в единой виртуальной сети за общим внешним шлюзом (Ingress), значит, вокруг свои. Однако сетевая модель Kubernetes по умолчанию устроена по принципу открытого офиса — любой под может отправить TCP-пакет на IP-адрес соседа. Если вспомогательный сервис скомпрометирован через уязвимость в зависимостях, злоумышленник получает доступ ко всем внутренним портам. Базовые правила NetworkPolicy изолируют сетевые маршруты на уровне L3/L4, но ничего не знают о контексте приложения: внутреннему сервису базы данных или хранилищу data-store необходимо железобетонно знать, кто именно к нему обращается.
Обычно команды выбирают между двумя крайностями. Первая — сложить статические токены в Kubernetes Secret и передать сервисам через переменные окружения. Это быстро превращается в проблему: секреты не ротируются годами, а утечка одного ключа требует ручной замены настроек по всему кластеру. Вторая крайность — развернуть выделенный Identity Provider вроде Keycloak или внедрить Service Mesh (Istio или Linkerd) с взаимным mTLS. Но поднимать тяжелый сервер авторизации с отдельной СУБД ради закрытого контура избыточно, а подсаживать sidecar-прокси к каждому микросервису означает отдавать ощутимую долю памяти и CPU на инфраструктурный оверхед.
В самом ядре Kubernetes уже заложен нативный и легковесный механизм взаимного удостоверения сервисов: связка сервисных аккаунтов (ServiceAccount), проецируемых токенов ограниченной аудитории и системного интерфейса TokenReview API.
Как Kubernetes превращается в доверенный удостоверяющий центр
Каждый под в кластере выполняется от имени учетной записи ServiceAccount. Это цифровая машинная идентичность приложения. В ранних версиях платформы к аккаунту генерировался бессрочный статический токен в формате секрета: если такой токен утекал, злоумышленник мог бесконечно обращаться от имени пода к любым API.
Ситуация изменилась с появлением проекции токенов — Service Account Token Volume Projection (TokenRequest API). Kubelet на рабочей ноде самостоятельно запрашивает у API криптографически подписанный JWT и монтирует его прямо в файловую систему пода. Этот токен обладает тремя ключевыми свойствами:
- Ограниченный срок жизни (
expirationSeconds): токен регулярно обновляется на диске kubelet'ом без перезапуска контейнера. - Привязка к экземпляру пода: при удалении пода выданный ему токен аннулируется.
- Ограничение целевой аудитории (
audience-bound): токен содержит полеaud, определяющее, для кого именно он выпущен.
Представьте служебный пропуск. Обычный статический токен похож на универсальный мастер-ключ от всех дверей здания без срока давности. Спроецированный токен с привязкой к аудитории — это временный талон, на котором напечатано: «Действителен 60 минут, разрешает вход исключительно в хранилище data-store». Если злоумышленник перехватит такой талон и попытается предъявить его на центральном сервере Kubernetes API или в сервисе биллинга, проверка провалится: аудитория не совпадет.
Проекция целевого токена в файловую систему вызывающего пода
Чтобы микросервис api-service мог обращаться к хранилищу data-store, ему не нужны сторонние генераторы JWT. Достаточно описать в манифесте Deployment монтирование проецируемого тома с целевым сервисом в поле audience.
Манифест вызывающего пода настраивает автоматическую проекцию токена с часовым временем жизни:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
namespace: frontend
spec:
replicas: 2
selector:
matchLabels:
app: api-service
template:
metadata:
labels:
app: api-service
spec:
serviceAccountName: api-sa
containers:
- name: app
image: internal/api:v1.0
volumeMounts:
- name: bound-token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: bound-token
projected:
sources:
- serviceAccountToken:
path: data-store-token
expirationSeconds: 3600
audience: data-store
Внутри контейнера по пути /var/run/secrets/tokens/data-store-token появляется текстовый файл с актуальным JWT. Вызывающий сервис читает его при формировании исходящего HTTP-запроса и добавляет в заголовок Authorization: Bearer <token>.
Выдача мандата на проверку: роль system:auth-delegator
Запрос с токеном прибывает в принимающий сервис data-store. Сервису необходимо проверить подлинность подписи, срок действия и принадлежность токена аккаунту api-service.
Сервис обращается к системному валидатору — TokenReview API на сервере Kubernetes (/apis/authentication.k8s.io/v1/tokenreviews). В модели безопасности Kubernetes поды не имеют права проверять чужие токены без явного разрешения. Чтобы сервис получил право делегированной проверки, его сервисному аккаунту назначается встроенная роль system:auth-delegator.
Для этого объявляется манифест ClusterRoleBinding:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: datastore-tokenreview-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: datastore-sa
namespace: backend
Сервис data-store под аккаунтом datastore-sa получает право отправлять запросы в TokenReview API.
Проверка токена на стороне сервиса: TokenReview на Go
Принимающий сервис перехватывает заголовок Authorization, формирует структуру TokenReview и отправляет ее в kube-apiserver.

Сервер API проверяет криптографическую подпись своим открытым ключом, проверяет аудиторию (data-store) и возвращает регистрационные данные: имя аккаунта, пространство имен (namespace) и UID.
Практическая реализация валидатора на языке Go с использованием client-go:
package main
import (
"context"
"fmt"
"net/http"
"strings"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
type TokenValidator struct {
clientset *kubernetes.Clientset
audience string
}
func NewTokenValidator(audience string) (*TokenValidator, error) {
config, err := rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("ошибка получения внутрикластерного конфига: %w", err)
}
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("ошибка инициализации clientset: %w", err)
}
return &TokenValidator{clientset: clientset, audience: audience}, nil
}
func (tv *TokenValidator) ValidateToken(ctx context.Context, token string) (*authenticationv1.UserInfo, error) {
review := &authenticationv1.TokenReview{
Spec: authenticationv1.TokenReviewSpec{
Token: token,
Audiences: []string{tv.audience},
},
}
result, err := tv.clientset.AuthenticationV1().TokenReviews().Create(ctx, review, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("сбой обращения к TokenReview API: %w", err)
}
if !result.Status.Authenticated {
return nil, fmt.Errorf("токен отклонен кластером: %s", result.Status.Error)
}
return &result.Status.User, nil
}
func authMiddleware(tv *TokenValidator, allowedIdentity string, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" || token == authHeader {
http.Error(w, "отсутствует bearer-токен", http.StatusUnauthorized)
return
}
user, err := tv.ValidateToken(r.Context(), token)
if err != nil {
http.Error(w, "ошибка авторизации: "+err.Error(), http.StatusForbidden)
return
}
// Проверяем машинную идентичность формата system:serviceaccount:<ns>:<name>
if user.Username != allowedIdentity {
http.Error(w, "доступ для данного аккаунта запрещен", http.StatusForbidden)
return
}
next(w, r)
}
}
Middleware проверяет точное совпадение user.Username с доверенной строкой вида system:serviceaccount:frontend:api-sa. Запрос от любого другого пода отсекается с кодом 403 Forbidden.
Накладные расходы, локальное кэширование и транспортная безопасность
Синхронный вызов к kube-apiserver на каждый входящий запрос добавляет сетевую задержку от 2 до 10 мс. При потоке в тысячи RPS это перегрузит мастер-ноды кластера.
Решение задачи — локальное кэширование проверенных токенов в памяти (in-memory LRU cache). Поскольку токены действительны от 10 до 60 минут, сервис может кэшировать результат проверки на 1–5 минут. Повторные запросы валидируются мгновенно, а пятиминутное окно инвалидации безопасно для большинства внутренних сервисов.
Второй фактор — защита сетевого канала. TokenReview API обеспечивает строгую аутентификацию на прикладном уровне L7, но не шифрует сетевой трафик. Чтобы предотвратить перехват пакетов в оверлейной сети, взаимодействие должно идти поверх внутрикластерного HTTPS или шифроваться средствами CNI (WireGuard в Cilium).
Итоговый вердикт: когда использовать нативную аутентификацию
Паттерн на базе Service Accounts и TokenReview API закрывает потребности микросервисов в пределах одного кластера без развертывания сторонних платформ.
Вам не нужен Keycloak, если задача сводится к разграничению прав внутренних сервисов. Вам не нужен тяжелый Service Mesh, если от него требуется только проверка идентичности вызывающей стороны. Ядро Kubernetes предоставляет готовый, проверенный временем механизм аутентификации из коробки.
