← Back to notes

ServiceAccount: идентичность для
Pod в Kubernetes


Когда приложение внутри 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

ServiceAccount: идентичность для Pod в Kubernetes | Aleksandr Suprun