首页 / Kubernetes (k8s) 入门教程 / ServiceAccount 服务账号

Kubernetes (k8s) 入门教程

ServiceAccount 服务账号

本教程共 65 篇 · 第 45 篇 · 更新于 2026-08-14 · 约 19 分钟阅读

KubernetesServiceAccount服务账号令牌投射RBAC安全

本节目标:搞懂 Pod 是靠什么身份访问 API 服务器的,学会给工作负载配一个权限刚好够用的 ServiceAccount。

第 43 章提过一句:Kubernetes 里没有 User 对象,真正被它当对象管理的身份只有 ServiceAccount。这一章就来看这个东西。

ServiceAccount(服务账号,常缩写 SA)是给非人类用户准备的身份。应用 Pod、系统组件、集群内外的自动化工具,都可以用某个 SA 的凭据把自己标识成那个 SA。

45-1 和用户账号有什么不一样

一张表说清:

ServiceAccount用户 / 组
存在于哪Kubernetes API(ServiceAccount 对象)外部系统
访问控制RBAC 或其他鉴权机制RBAC 或外部 IAM
给谁用工作负载、自动化工具

关键差别是位置。用户身份对 API 服务器来说是不透明数据——它只认证书或令牌里的那个用户名字符串,不知道这个人从哪来、还存不存在。而 ServiceAccount 是集群里实打实的一个对象,你能 kubectl get sa 看到它。

SA 有三个特点:

  • 命名空间限定——每个 SA 属于一个命名空间。
  • 轻量——就是个 API 对象,几行 YAML 建一个,用完就删。
  • 可移植——一套复杂应用的配置包可以把自己的 SA 定义一起带上,换集群不用改。

45-2 default 服务账号:那个你一直在用却没注意的东西

创建集群时,Kubernetes 会给每个命名空间自动建一个名叫 defaultServiceAccount。新建命名空间也一样。

kubectl get serviceaccounts -n default
# NAME      SECRETS   AGE
# default   0         12d

你前面 44 章建的所有 Pod,只要没指定 SA,用的都是它。

这个 default 账号的权限有多大?除了所有已认证主体都有的 API 发现权限,什么都没有。

也就是说,它能访问 /api/version/openapi 这类发现端点,但读不了 Pod、列不出 Service。默认是安全的。

Note

删不掉。你 kubectl delete sa default,控制平面会立刻建一个新的补上。别费劲了。

Warning

千万别给 default 服务账号加权限。它是所有未指定 SA 的 Pod 的默认身份,给它绑一个 edit,等于命名空间里每个”忘了配身份”的 Pod 都拿到了读写权限。这是我见过最常见的权限失控方式——加的时候图省事,出问题时根本查不到源头。正确做法永远是建一个专用 SA。

45-3 建一个专用 SA 并绑权限

三步:建 SA、建角色、绑起来。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: log-reader
  namespace: monitoring

或者一行命令:

kubectl create serviceaccount log-reader -n monitoring

然后用第 44 章学的 RBAC 授权。注意 subjects 里 SA 的写法——要 namespace,不要 apiGroup

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-log-reader
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: log-reader-binding
  namespace: default
subjects:
  - kind: ServiceAccount
    name: log-reader
    namespace: monitoring      # SA 在 monitoring
roleRef:
  kind: Role
  name: pod-log-reader
  apiGroup: rbac.authorization.k8s.io

留意这个例子:SA 在 monitoring 命名空间,RoleBindingdefault 命名空间。这就是跨命名空间授权的写法。

规则很好记:绑定放在被访问资源所在的命名空间,主体里写 SA 自己的命名空间。 上面的效果是——monitoring 里的 log-reader 能读 default 里的 Pod 日志。

监控组件、日志采集器基本都是这个模式:自己住在一个专门的命名空间,去读别人家的东西。

45-4 指派给 Pod

改一个字段就行:

apiVersion: v1
kind: Pod
metadata:
  name: log-collector
  namespace: monitoring
spec:
  serviceAccountName: log-reader     # 就这一行
  containers:
    - name: agent
      image: busybox:1.36
      command: ["sleep", "3600"]

Deployment 里要写在 spec.template.spec 下面,别写错层级:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: log-collector
spec:
  replicas: 1
  selector:
    matchLabels:
      app: log-collector
  template:
    metadata:
      labels:
        app: log-collector
    spec:
      serviceAccountName: log-reader   # 在 template.spec 下
      containers:
        - name: agent
          image: busybox:1.36
Tip

还有个 serviceAccount 字段(不带 Name),那是老写法,已废弃。统一用 serviceAccountName

45-5 令牌是怎么进到容器里的

这是本章最值得搞明白的部分。

指定了 SA 之后,Kubernetes 自动把凭据送进 Pod。v1.22 及以后的做法是:通过 TokenRequest API 获取一个短期有效、自动轮换的令牌,以**投射卷(Projected Volume)**的形式挂进容器。

默认挂载路径固定:

/var/run/secrets/kubernetes.io/serviceaccount/
├── token       # JWT 令牌
├── ca.crt      # 集群 CA 证书
└── namespace   # 当前命名空间名

进容器里能直接看到:

kubectl exec -it log-collector -- ls /var/run/secrets/kubernetes.io/serviceaccount/
kubectl exec -it log-collector -- cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

应用要访问 API,就读这个 token 文件,放到请求头里:

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
curl --cacert $CACERT \
  -H "Authorization: Bearer $TOKEN" \
  https://kubernetes.default.svc/api/v1/namespaces/default/pods

kubernetes.default.svc 是集群内访问 API 服务器的固定 DNS 名。官方客户端库(client-go、Python 的 kubernetes 库等)会自动读这几个文件,你一般不用手写。

为什么”自动轮换”很重要

v1.24 之前,Kubernetes 给每个 SA 自动创建一个 Secret,里面是永不过期的静态令牌。这东西一旦泄露,攻击者可以一直用,除非你手动删掉重建。

现在的投射令牌有到期时间,kubelet 会在过期前主动换新。泄露一份也只有有限的窗口期。

Note

应用代码要重新读取令牌文件,别在启动时读一次就缓存到进程内存里用一整天。令牌轮换后旧的会失效,缓存的那份用着就 401 了。这个坑相当隐蔽——服务跑几个小时才突然开始报错,重启又好了。主流客户端库都已经处理了这件事,自己手撸 HTTP 请求就要注意。

自定义投射:改有效期和受众

想更精细地控制,可以自己写投射卷:

apiVersion: v1
kind: Pod
metadata:
  name: custom-token-pod
spec:
  serviceAccountName: log-reader
  containers:
    - name: app
      image: nginx:1.27
      volumeMounts:
        - name: sa-token
          mountPath: /var/run/secrets/tokens
          readOnly: true
  volumes:
    - name: sa-token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              expirationSeconds: 3600      # 有效期 1 小时
              audience: vault              # 限定受众

两个字段的含义:

  • expirationSeconds——令牌有效期。kubelet 会在过期前轮换。
  • audience——受众。默认是 API 服务器;填别的值,这个令牌就只能拿去向那个受众认证,拿到 API 服务器这里是无效的。

audience 在对接外部系统时很有用。比如让 Pod 用 SA 令牌去 Vault 换取数据库密码——把 audience 设为 vault,即使令牌泄露,也不能用来操作 Kubernetes API。

45-6 手动拿令牌:给集群外的东西用

CI/CD 流水线要访问集群,也可以用 SA 身份。推荐用 kubectl create token

# 签发一个短期令牌,默认 1 小时
kubectl create token ci-bot -n ci

# 指定有效期
kubectl create token ci-bot -n ci --duration=24h

这个命令背后就是 TokenRequest API。

老办法是手动创建一个 kubernetes.io/service-account-token 类型的 Secret:

apiVersion: v1
kind: Secret
metadata:
  name: ci-bot-token
  namespace: ci
  annotations:
    kubernetes.io/service-account.name: ci-bot
type: kubernetes.io/service-account-token

能用,但不推荐。这种令牌不过期、不轮换,属于长期有效的持有者令牌(Bearer Token),泄露风险大。

Warning

Kubernetes 官方明确建议:给集群外应用做认证,不要用长期 SA 令牌。更好的选择是保护良好的私钥 + 证书,或者自己实现认证 Webhook 对接企业身份系统。要是必须用令牌,就用 TokenRequest 签短期的,让流水线每次跑的时候现签。

45-7 imagePullSecrets:拉私有镜像

SA 还有个常被忽略的用途——挂私有仓库凭据。

不用给每个 Pod 都写一遍 imagePullSecrets,绑到 SA 上,用这个 SA 的所有 Pod 自动生效:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-sa
  namespace: production
imagePullSecrets:
  - name: my-registry-secret

或者打补丁:

kubectl patch serviceaccount app-sa -n production \
  -p '{"imagePullSecrets": [{"name": "my-registry-secret"}]}'

如果整个命名空间都从同一个私有仓库拉镜像,把这个配到 default SA 上是合理的——注意这里配的是镜像凭据,不是 API 权限,不违反前面”别给 default 加权限”的原则。

45-8 不需要 API 权限的 Pod:把令牌关掉

绝大多数应用根本不访问 Kubernetes API。一个 Nginx、一个 Java 后端,要 SA 令牌干什么?

但默认它们都挂载着。攻击者一旦拿下容器,第一件事就是去 /var/run/secrets/kubernetes.io/serviceaccount/ 摸令牌。

关掉:

apiVersion: v1
kind: Pod
metadata:
  name: plain-web
spec:
  automountServiceAccountToken: false      # Pod 级别
  containers:
    - name: web
      image: nginx:1.27

也可以在 SA 上一次性关掉,用这个 SA 的 Pod 全部不挂载:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: no-api-sa
  namespace: production
automountServiceAccountToken: false

Pod 级别的设置优先级更高,可以覆盖 SA 级别的。

这是性价比最高的一条加固措施:改一行,零成本,直接掐掉一整类攻击路径。

45-9 常用排查命令

# 看某命名空间的所有 SA
kubectl get sa -n production

# 看 SA 详情(含 imagePullSecrets、挂载的 Secret)
kubectl describe sa app-sa -n production

# 这个 SA 能干什么?
kubectl auth can-i --list \
  --as=system:serviceaccount:production:app-sa -n production

# 具体某个动作行不行
kubectl auth can-i get secrets \
  --as=system:serviceaccount:production:app-sa -n production

# 找出所有绑定到这个 SA 的角色
kubectl get rolebindings,clusterrolebindings -A -o json \
  | grep -B5 -A5 "app-sa"

--as=system:serviceaccount:<命名空间>:<名称> 这个格式建议背下来,排查 Pod 403 全靠它。

45-10 几个容易搞错的点

SA 名字写错了,Pod 起不来。 spec.serviceAccountName 指向一个不存在的 SA,Pod 会卡在创建阶段。kubectl describe pod 能看到类似 error looking up service account 的事件。

以为改了 SA 权限就立刻生效。 RBAC 规则改动是立即生效的,但已经签发出去的令牌里的身份不会变。权限是每次请求时现查的,所以加权限立即生效;不过如果你换了 Pod 的 serviceAccountName,必须重建 Pod——令牌是在 Pod 创建时投射进去的。

enforce-mountable-secrets 注解别再用了。 老文章里会提到 kubernetes.io/enforce-mountable-secrets: "true",用来限制 SA 的 Secret 只能挂到特定资源上。这个注解从 v1.32 起已弃用。官方建议改用独立命名空间来做隔离。

跨命名空间绑定放错位置。 再强调一次:RoleBinding 要建在被访问资源所在的命名空间,不是 SA 所在的命名空间。放错了不报错,就是不生效——最难查的那种问题。

小结

  • ServiceAccount 是工作负载的身份,是 Kubernetes 唯一原生管理的身份对象。
  • 每个命名空间自带 default SA,删了会自动重建,永远不要给它加权限
  • spec.serviceAccountName 指派;Deployment 里写在 template.spec 下。
  • v1.22 起令牌通过投射卷挂载,短期有效且自动轮换,路径固定在 /var/run/secrets/kubernetes.io/serviceaccount/。应用要重读文件,别缓存。
  • 集群外用 kubectl create token 签短期令牌,别造永久 Secret 令牌。
  • 不访问 API 的 Pod 设 automountServiceAccountToken: false
  • 跨命名空间授权:绑定放资源侧,主体写 SA 侧。

下一章讲 Pod 安全标准和 Pod 安全准入——控制 Pod 自己能有多大权力。