Когда приложение внутри Pod хочет обратиться к Kubernetes API — прочитать ConfigMap, создать Job, посмотреть другие Pod — ему нужна идентичность. Эту идентичность предоставляет ServiceAccount (SA).
SA отличается от пользовательских аккаунтов принципиально: он является нативным объектом Kubernetes API, существует в конкретном namespace, и предназначен для процессов, а не людей. Kubernetes не имеет встроенных объектов для управления пользователями — аутентификация людей делегируется внешним системам (OIDC-провайдер, X.509-сертификаты). ServiceAccount — это слой идентичности полностью внутри кластера.
[source: kubernetes.io/docs/concepts/security/service-accounts/]
User Account vs ServiceAccount
Когда разработчик аутентифицируется через kubeconfig с OIDC-токеном — он user account. Когда приложение внутри Pod обращается к k8s API с токеном, смонтированным в файловую систему — это ServiceAccount.
Default ServiceAccount
Каждый namespace автоматически получает ServiceAccount с именем default. Если Pod не указывает spec.serviceAccountName — он автоматически использует default SA своего namespace.
kubectl get serviceaccounts -n default
# NAME SECRETS AGE
# default 0 30d
kubectl get serviceaccounts -n kube-system | head -10
# Много SA для системных компонентов
Default SA существует в каждом namespace, но по умолчанию не имеет никаких RBAC-прав. При этом токен всё равно монтируется в Pod автоматически — это расширяет attack surface без реальной пользы для большинства приложений. Скомпрометированный контейнер получает токен, с которым может обращаться к API (хотя и без прав).
Создание ServiceAccount
Через манифест:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-sa
namespace: app
labels:
app: my-app
annotations:
# Например, для IRSA в EKS
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/my-app-role
Через kubectl:
kubectl create serviceaccount my-app-sa -n app
# Посмотреть созданный SA
kubectl describe serviceaccount my-app-sa -n app
Назначение ServiceAccount для Pod
apiVersion: v1
kind: Pod
metadata:
name: my-app
namespace: app
spec:
serviceAccountName: my-app-sa
containers:
- name: app
image: my-app:1.0
Для Deployment — поле находится в spec.template.spec:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: app
spec:
replicas: 2
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
serviceAccountName: my-app-sa # здесь
containers:
- name: app
image: my-app:1.0
Поле spec.serviceAccountName задаётся при создании Pod и не может быть изменено после — это immutable поле. Если нужно изменить SA для Pod — пересоздай Pod (или сделай rolling update Deployment).
Как токен попадает в Pod
Начиная с Kubernetes 1.22, токен SA монтируется через projected volume с использованием TokenRequest API. Это принципиально лучше старого подхода на основе Secret:
- Токен bound к конкретному Pod и имени SA — после удаления Pod токен автоматически инвалидируется
- Имеет ограниченное время жизни (по умолчанию 1 час)
- Автоматически обновляется kubelet до истечения срока — приложению не нужно перезапускаться
- Содержит
audience(для какого API предназначен, по умолчанию kube-apiserver)
Токен монтируется по пути /var/run/secrets/kubernetes.io/serviceaccount/:
/var/run/secrets/kubernetes.io/serviceaccount/
├── token # JWT-токен (обновляется автоматически, не хардкодь в env)
├── ca.crt # CA-сертификат кластера для проверки TLS apiserver
└── namespace # имя текущего namespace (удобно для in-cluster клиентов)
Посмотреть содержимое токена можно так (он является стандартным JWT):
# Внутри Pod
cat /var/run/secrets/kubernetes.io/serviceaccount/token | \
cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
В payload будет sub (идентификатор SA), exp (время истечения), aud (audience), kubernetes.io/pod/name и другие claims.
Стандартные клиентские библиотеки (client-go, fabric8, kubernetes-client для Python) автоматически читают токен из этого пути при работе внутри кластера (режим in-cluster config). Явно указывать путь не нужно.
[source: kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/]
Что было до Kubernetes 1.22 и почему это плохо
До Kubernetes 1.22 токены создавались как отдельные объекты Secret типа kubernetes.io/service-account-token и не имели срока жизни. Они оставались действительными даже после удаления Pod, даже после удаления самого SA — до явного удаления Secret. Такие токены называют legacy service account tokens или non-expiring tokens.
В Kubernetes 1.24 автоматическое создание таких Secret было отключено. Начиная с 1.29, автоматически созданные legacy-токены, которые долго не использовались, помечаются как invalid и позже очищаются control plane.
Если видишь в кластере SA с полем secrets: и ссылкой на Secret типа kubernetes.io/service-account-token — это legacy. Мигрируй на projected volumes (они работают автоматически) или TokenRequest API.
# Найти legacy SA-токены в кластере
kubectl get secrets -A -o json | \
jq '.items[] | select(.type == "kubernetes.io/service-account-token") |
{ns: .metadata.namespace, name: .metadata.name}'
Отключение автомонтирования токена
Если Pod не обращается к Kubernetes API — монтировать токен не нужно. Это уменьшает attack surface: скомпрометированный контейнер без токена не сможет взаимодействовать с API.
На уровне ServiceAccount (действует для всех Pod с этим SA):
apiVersion: v1
kind: ServiceAccount
metadata:
name: no-api-access-sa
namespace: app
automountServiceAccountToken: false
На уровне Pod (переопределяет настройку SA):
apiVersion: v1
kind: Pod
metadata:
name: my-app
namespace: app
spec:
serviceAccountName: no-api-access-sa
automountServiceAccountToken: false
containers:
- name: app
image: my-app:1.0
Настройка на Pod имеет приоритет над настройкой SA. Это позволяет реализовать паттерн: automountServiceAccountToken: false на SA по умолчанию, и явно true только для тех Pod, которым нужен доступ.
Хорошая практика — всегда явно указывать automountServiceAccountToken: false для default SA в каждом namespace:
kubectl patch serviceaccount default -n app \
-p '{"automountServiceAccountToken": false}'
RBAC для ServiceAccount
ServiceAccount сам по себе не имеет никаких прав. Права добавляются через RoleBinding или ClusterRoleBinding — точно так же, как для пользователей. ServiceAccount является субъектом в RBAC.
При указании SA в subjects обязательно указывай namespace:
subjects:
- kind: ServiceAccount
name: my-app-sa
namespace: app # обязательно!
Полный пример: SA с правами на чтение ConfigMap и обращение к Kubernetes API для получения информации о Pod своего namespace:
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: config-reader-sa
namespace: app
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app-reader
namespace: app
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: config-reader-binding
namespace: app
subjects:
- kind: ServiceAccount
name: config-reader-sa
namespace: app
roleRef:
kind: Role
name: app-reader
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: config-app
namespace: app
spec:
replicas: 1
selector:
matchLabels:
app: config-app
template:
metadata:
labels:
app: config-app
spec:
serviceAccountName: config-reader-sa
containers:
- name: app
image: my-app:1.0
Проверка прав SA:
# Формат: system:serviceaccount:<namespace>:<name>
kubectl auth can-i get configmaps -n app \
--as system:serviceaccount:app:config-reader-sa
# yes
kubectl auth can-i get secrets -n app \
--as system:serviceaccount:app:config-reader-sa
# no
kubectl auth can-i delete pods -n app \
--as system:serviceaccount:app:config-reader-sa
# no
Ручное создание bound-токена
Иногда нужно временно получить токен SA вне Pod — для отладки, для CI/CD:
# Создать токен с истечением через 1 час
kubectl create token my-app-sa -n app --duration=1h
# Создать токен с кастомным audience (например, для Vault)
kubectl create token my-app-sa -n app \
--audience=https://vault.example.com \
--duration=30m
# Использовать токен для обращения к API
TOKEN=$(kubectl create token my-app-sa -n app)
kubectl get pods -n app --token="$TOKEN"
Это bound token — он не сохраняется как Secret и истекает автоматически. Никакого cleanup не нужно.
[source: kubernetes.io/docs/reference/access-authn-authz/service-accounts-admin/#bound-service-account-tokens]
Когда использовать
Отдельный SA на каждое приложение — основная практика. Даёт возможность точечно управлять правами и аудитировать, кто что делает в кластере. Если в namespace 5 сервисов — 5 разных SA.
SA для Kubernetes-оператора или контроллера — оператор всегда работает с собственным SA. Обычно нужны права на CRD уровня кластера: ClusterRole + ClusterRoleBinding. Права должны быть минимальными: только те CRD и ресурсы, которыми управляет оператор.
SA для CI/CD-компонентов — Argo CD, Flux, Tekton, каждый получает свой SA с правами только на namespace или ресурсы, которыми управляет. Не давай CI/CD cluster-admin.
automountServiceAccountToken: false по умолчанию — включай явно только там, где нужен доступ к API. Большинство приложений его не используют.
Не используй default SA для приложений — создавай отдельный SA с явными правами.
Типичные ошибки
Использование default SA для приложений с RBAC-правами. Default SA есть в каждом namespace. Если привязать к нему ClusterRole — все Pod без явного serviceAccountName внезапно получат эти права. Это тихая дыра в безопасности. Всегда создавай отдельный SA.
Забыть namespace в subjects. При создании RoleBinding для SA без namespace: в subjects Kubernetes примет манифест без ошибки, но привязка работать не будет — SA не будет найден.
# Неправильно — namespace не указан
subjects:
- kind: ServiceAccount
name: my-app-sa
# Правильно
subjects:
- kind: ServiceAccount
name: my-app-sa
namespace: app
Создавать legacy Secret-токены вручную без необходимости. Kubernetes всё ещё позволяет вручную создать Secret типа kubernetes.io/service-account-token, но это long-lived credential. Для временных токенов используй kubectl create token, для in-Pod — projected volume монтируется автоматически.
Один SA для нескольких несвязанных приложений. Если скомпрометировано одно приложение — злоумышленник получает доступ ко всему, что разрешено этому SA. Разделяй SA по приложениям.
Не отключать automount там, где API не нужен. Большинство приложений не обращаются к k8s API — статические сайты, базы данных, большинство микросервисов. Для них automountServiceAccountToken: false — правильная настройка, не паранойя.
Альтернативы
IRSA (IAM Roles for Service Accounts) в EKS — позволяет Pod получать временные AWS IAM credentials через SA без хранения AWS-ключей в кластере. Работает через OIDC: EKS выступает OIDC-провайдером, AWS STS верифицирует токен SA и выдаёт временные credentials. Настраивается через аннотацию на SA:
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/my-role
GKE Workload Identity — аналог IRSA для Google Cloud. SA в Kubernetes привязывается к Google Service Account. Pod получает временные Google credentials без ключей.
Azure Workload Identity — аналог для AKS. Использует federated credentials и OIDC для получения Azure Managed Identity.
Vault Agent Injector — общий паттерн для Vault: sidecar-контейнер использует SA-токен для аутентификации в Vault и получает оттуда секреты (пароли БД, TLS-сертификаты), которые монтирует в общую файловую систему с основным контейнером.
[source: kubernetes.io/docs/concepts/security/service-accounts/#service-account-token-volume-projection]
← 15: RBAC | 17: NetworkPolicy →