K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

本轮操作记录:身份、认证、授权、准入实验

前几轮我们已经把: 讲清了一部分。 这轮我切到另外一条必须掌握的主线: 本轮目标分成四层: 1. 先确认这套真实集群的认证、授权、准入基础设施是什么 2. 再创建最小可复现实验环境 3. 用真实返回码区分: 4. 最后把每一步背后的原理写成第六课 --- 因为仓库已经有: 我这轮不是重写旧内容,而是要把它升级成: 仓库原材料够做入门,但还不够达到你要的“能独

Markdown06-操作记录-身份认证授权准入实验.md2026年4月9日 18:26

本轮操作记录:身份、认证、授权、准入实验

本轮目标

前几轮我们已经把:

  • kubectl apply 主链路
  • 调度器选节点
  • 集群网络
  • NetworkPolicy

讲清了一部分。

这轮我切到另外一条必须掌握的主线:

谁在调用 Kubernetes API,为什么它能做某些事,又为什么它会被拒绝。

本轮目标分成四层:

  1. 先确认这套真实集群的认证、授权、准入基础设施是什么
  2. 再创建最小可复现实验环境
  3. 用真实返回码区分:
    • 无 token
    • 有 token 但无权
    • 有 token 且有最小授权
  4. 最后把每一步背后的原理写成第六课

Step 1:先看仓库里已有资料,避免脱离主线

实际命令

sed -n '1,260p' phase-1/02-configmap-secret.md
sed -n '1,260p' phase-1/04-rbac.md

为什么先看已有文档

因为仓库已经有:

  • ConfigMap / Secret 基础材料
  • RBAC 基础材料

我这轮不是重写旧内容,而是要把它升级成:

  • 更接近真实集群
  • 更能解释底层机制
  • 更适合排障和架构设计

我得到的结论

仓库原材料够做入门,但还不够达到你要的“能独立搭平台和排障”的层次。

缺的主要是:

  • 真实集群证据
  • API 请求全链路视角
  • token 投影与现代行为
  • Admission 与 RBAC 的衔接

所以我决定补一节完整的“身份与权限课”。


Step 2:确认集群里真的有这些 API 资源

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl api-resources | \
  rg 'serviceaccounts|roles|rolebindings|clusterroles|clusterrolebindings|secrets'

为什么要做这一步

虽然这些是 Kubernetes 标准对象,但我还是先确认:

  • API 资源确实可用
  • 没有因为版本、安装方式、权限问题出现异常

我看到了什么

集群里存在:

  • ServiceAccount
  • Role
  • RoleBinding
  • ClusterRole
  • ClusterRoleBinding
  • Secret

原理解释

这一步本质是在确认:

我们后面的实验,是真正操作 API 资源,而不是建立在假设上。


Step 3:先看现有 ServiceAccount 现状

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl get sa -A

为什么要先看全局现状

因为初学者很容易以为:

  • ServiceAccount 很少
  • 或者只是教学用

实际上,真实集群里几乎到处都是 ServiceAccount。

系统组件、业务组件、Operator、监控栈、GitOps 控制器,都会用它。

我看到的关键现象

大多数 ServiceAccount 的 SECRETS 列都是:

  • 0

这说明什么

这说明你的集群使用的是现代行为:

  • 不再默认给 ServiceAccount 自动创建长期 token Secret
  • 更依赖投影卷中的短期 token

这一步非常关键,因为它直接决定了后面文档里该怎么讲 token 模型。


Step 4:先看默认 ServiceAccount 能做什么

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl auth can-i --list \
  --as system:serviceaccount:default:default \
  -n default

为什么这一步很重要

很多人以为默认 default ServiceAccount 是“完全没权限”。

这是错误的。

它通常至少能做一些:

  • 自我权限探测
  • API 发现类访问
  • 非资源 URL 访问

我看到的关键信息

默认 ServiceAccount 可以访问一些非资源路径,例如:

  • /api
  • /apis
  • /openapi
  • /version
  • /healthz

但这不代表它有业务资源权限。

原理解释

这一步是在建立一个很重要的认知:

“能访问 API 的某些端点” 和 “能读写资源” 是两回事。


Step 5:直接去控制平面看真实配置

实际命令

ssh root@107.148.176.193 \
  'hostname && sudo sed -n "1,260p" /etc/kubernetes/manifests/kube-apiserver.yaml'

为什么不只靠 kubectl

因为很多系统行为,单靠 kubectl 是看不全的。

尤其你以后做平台、做安全、做集群治理时,经常要知道:

  • API Server 真正启用了什么模式
  • 用什么 key 给 ServiceAccount 签 token
  • Admission 插件怎么配的

这些信息最可信的来源之一就是控制平面的静态 Pod 清单。

我先踩到的一个小坑

我先尝试:

ssh us480851516617a ...
ssh 10.10.0.1 ...

连接都被远端关闭。

后来切换成文档里明确写的公网入口:

ssh root@107.148.176.193 ...

成功。

这个坑为什么值得记录

因为以后你做集群排障时,经常会遇到:

  • 文档里有多个地址
  • 内网地址和公网地址都存在
  • SSH 只监听其中一个

这不是“无关细节”,而是实战里非常常见的接入问题。

我确认到的关键配置

真实结果里最关键的是:

  • --authorization-mode=Node,RBAC
  • --enable-admission-plugins=NodeRestriction
  • --service-account-issuer=https://kubernetes.default.svc.cluster.local
  • --service-account-signing-key-file=/etc/kubernetes/pki/sa.key

原理解释

这说明:

  1. 授权主要走 Node + RBAC
  2. 节点侧有 NodeRestriction 保护
  3. ServiceAccount Token 有明确发行者
  4. Token 是有签名体系的,不是随便拼出来的字符串

Step 6:确认集群里存在真实 Admission 资源

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get validatingadmissionpolicies,validatingadmissionpolicybindings

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get validatingwebhookconfiguration monitoring-kube-prometheus-admission -o yaml | \
  sed -n '1,240p'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get mutatingwebhookconfiguration monitoring-kube-prometheus-admission -o yaml | \
  sed -n '1,220p'

为什么要看这一步

因为你如果只学 RBAC,会以为:

  • 权限通过就万事大吉

但真实集群里,请求还可能继续被 webhook 修改或拒绝。

我看到的结果

集群里存在:

  • ValidatingWebhookConfiguration/monitoring-kube-prometheus-admission
  • MutatingWebhookConfiguration/monitoring-kube-prometheus-admission

没有发现:

  • ValidatingAdmissionPolicy
  • ValidatingAdmissionPolicyBinding

原理解释

这说明你的集群目前确实在使用:

  • webhook 型准入链

而不是只停留在“Admission 这个概念在书上存在”。

以后你在监控栈 apply PrometheusRuleAlertmanagerConfig 时,如果被拒绝,就应该联想到这层。


Step 7:设计实验对象

我创建了哪些文件

路径:

对象包括:

  • Namespace/auth-lab
  • Pod/default-api-client
  • Pod/no-token-api-client
  • ServiceAccount/pod-reader
  • Secret/demo-secret
  • Role/pod-reader
  • RoleBinding/pod-reader
  • Pod/reader-api-client

为什么这样设计

这套设计不是随便拼的,而是为了同时拿到三种典型状态:

  1. no-token-api-client
    • 没有 token 文件
    • 观察匿名访问效果
  2. default-api-client
    • 有默认身份
    • 观察认证成功但无授权
  3. reader-api-client
    • 有最小授权
    • 观察允许读 Pod、拒绝读 Secret

为什么还专门放一个 demo-secret

因为如果没有 Secret 这个对照对象,你很难把“普通只读”和“高敏感读取”真正区分开。


Step 8:apply 实验清单

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/06-auth

为什么用目录 apply

因为这一轮实验对象彼此有关联:

  • namespace
  • sa
  • role
  • binding
  • pod

目录级 apply 可以让整个实验清单具备更好的:

  • 可复现性
  • 可审阅性
  • 可回放性

实际结果

成功创建:

  • namespace
  • 两个普通 Pod
  • 一个绑定最小权限的 Pod
  • 一个 ServiceAccount
  • 一个 Role
  • 一个 RoleBinding
  • 一个演示 Secret

Step 9:等待 Pod Ready,先确认实验环境没变量污染

实际命令

kubectl -n auth-lab wait --for=condition=Ready pod/default-api-client --timeout=180s
kubectl -n auth-lab wait --for=condition=Ready pod/no-token-api-client --timeout=180s
kubectl -n auth-lab wait --for=condition=Ready pod/reader-api-client --timeout=180s
kubectl -n auth-lab get pod -o wide

为什么这一步不能省

如果 Pod 还没起来,你后面测试 API 调用失败,就会出现变量污染:

  • 到底是权限问题
  • 还是 Pod 本身没 Ready
  • 还是镜像命令有问题

我确认到的结果

三个 Pod 都 Running / Ready,并且分布在真实工作节点上。

这说明:

  • 镜像可拉取
  • 命令可执行
  • 可以进入下一步权限验证

Step 10:确认默认 Pod 挂载了哪些 ServiceAccount 文件

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n auth-lab exec default-api-client -- \
  sh -c 'ls -l /var/run/secrets/kubernetes.io/serviceaccount && echo && cat /var/run/secrets/kubernetes.io/serviceaccount/namespace'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n auth-lab get pod default-api-client -o yaml | \
  sed -n '1,220p'

为什么先看文件和 Pod YAML

因为我要把“ServiceAccount Token”从抽象概念变成真实物件。

我看到的关键内容

目录中真实存在:

  • token
  • ca.crt
  • namespace

Pod YAML 中还看到卷来源是:

  • serviceAccountToken
  • configMap: kube-root-ca.crt
  • downwardAPI

原理解释

这说明:

  • token 是投影进来的
  • CA 证书来自 namespace 中的 kube-root-ca.crt
  • namespace 信息来自 downward API

这就是容器内访问 Kubernetes API 的最小信任包。


Step 11:确认 automountServiceAccountToken: false 的效果

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n auth-lab exec no-token-api-client -- \
  sh -c 'ls -l /var/run/secrets/kubernetes.io/serviceaccount'

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n auth-lab get pod no-token-api-client -o yaml | \
  sed -n '1,180p'

我看到的结果

  • 容器内目录不存在
  • Pod YAML 中明确有:
    • automountServiceAccountToken: false
  • 同时 Pod 仍然显示:
    • serviceAccountName: default

原理解释

这一步证明了:

“Pod 绑定哪个 ServiceAccount” 和 “是否把 token 自动挂进去” 是两件可以分开的事。

这在安全上很重要,因为很多业务 Pod 根本不需要访问 API。


Step 12:先用 kubectl auth can-i 看权限面

实际命令

printf 'default list pods: '; \
  KUBECONFIG=~/.kube/config-k8s-lab kubectl auth can-i list pods \
  --as system:serviceaccount:auth-lab:default -n auth-lab

printf 'default get secrets: '; \
  KUBECONFIG=~/.kube/config-k8s-lab kubectl auth can-i get secrets \
  --as system:serviceaccount:auth-lab:default -n auth-lab

printf 'reader list pods in auth-lab: '; \
  KUBECONFIG=~/.kube/config-k8s-lab kubectl auth can-i list pods \
  --as system:serviceaccount:auth-lab:pod-reader -n auth-lab

printf 'reader get secrets in auth-lab: '; \
  KUBECONFIG=~/.kube/config-k8s-lab kubectl auth can-i get secrets \
  --as system:serviceaccount:auth-lab:pod-reader -n auth-lab

printf 'reader list pods in learn-k8s: '; \
  KUBECONFIG=~/.kube/config-k8s-lab kubectl auth can-i list pods \
  --as system:serviceaccount:auth-lab:pod-reader -n learn-k8s

为什么先用 can-i

因为这是最直接的授权排障入口。

它不会替代真实 API 请求,但能快速回答:

  • 这个身份理论上能不能做这个动作

我得到的结果

  • default list pods: no
  • default get secrets: no
  • reader list pods in auth-lab: yes
  • reader get secrets in auth-lab: no
  • reader list pods in learn-k8s: no

原理解释

这一步把 RBAC 的四个维度都展示出来了:

  • 身份是谁
  • 动作是什么
  • 资源是什么
  • 作用域在哪个 namespace

Step 13:列出完整权限面,而不是只查单条

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl auth can-i --list \
  --as system:serviceaccount:auth-lab:default -n auth-lab | sed -n '1,80p'

KUBECONFIG=~/.kube/config-k8s-lab kubectl auth can-i --list \
  --as system:serviceaccount:auth-lab:pod-reader -n auth-lab | sed -n '1,80p'

为什么这一步很有价值

单条 can-i 只能回答一个问题。

--list 更适合排障,因为你能直接看出:

  • 某个身份完整具备哪些 verbs
  • 哪些是非资源 URL 访问
  • 哪些是资源访问

我看到的关键信息

默认身份主要只有:

  • 自我审查类资源
  • /api/apis/version 等非资源 URL

pod-reader 多了一条真实业务权限:

  • podsget/list/watch

原理解释

这说明我们新增的 Role / RoleBinding 确实生效了,而且生效范围正好是:

  • auth-lab

没有越界。


Step 14:用容器内真实 API 请求验证“匿名访问”

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n auth-lab exec no-token-api-client -- \
  sh -c "echo 'GET /version without token'; \
  curl -sS -k -o /tmp/version.out -w 'HTTP %{http_code}\n' https://kubernetes.default.svc/version; \
  sed -n '1,40p' /tmp/version.out; \
  echo; \
  echo 'GET pods in auth-lab without token'; \
  curl -sS -k -o /tmp/pods.out -w 'HTTP %{http_code}\n' https://kubernetes.default.svc/api/v1/namespaces/auth-lab/pods; \
  sed -n '1,40p' /tmp/pods.out"

为什么这里用了 -k

因为这个 Pod 明确关闭了 token 自动挂载,所以:

  • 没有 ca.crt

因此我只能先用 -k 跳过证书校验,专门观察:

  • 没有身份材料时,API Server 会把请求当成谁

我看到的结果

  • GET /version 返回 HTTP 200
  • GET pods in auth-lab 返回 HTTP 403
  • 错误里明确写:
    • User "system:anonymous"

原理解释

这一步非常重要。

它证明:

  1. 没有 token 的请求并不是“什么都不是”,而是可能被识别成匿名身份
  2. 某些非资源 URL 可以匿名访问
  3. 真正的资源访问仍然会被权限系统拦住

Step 15:用默认 ServiceAccount 访问 API

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n auth-lab exec default-api-client -- \
  sh -c 'TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token); \
  CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt; \
  echo "GET /version"; \
  curl -sS --cacert $CACERT -H "Authorization: Bearer $TOKEN" \
    https://kubernetes.default.svc/version; \
  echo; echo; \
  echo "GET pods in auth-lab"; \
  curl -sS -o /tmp/pods.out -w "HTTP %{http_code}\n" \
    --cacert $CACERT -H "Authorization: Bearer $TOKEN" \
    https://kubernetes.default.svc/api/v1/namespaces/auth-lab/pods; \
  sed -n "1,40p" /tmp/pods.out'

这一步体现的三个原理

第一,真正的 API 调用是 HTTPS

不是 http://

API Server 默认就是:

  • https://kubernetes.default.svc

第二,Bearer Token 在 HTTP Header 里传

也就是:

Authorization: Bearer <token>

第三,客户端还需要信任 API Server 证书

所以这里用了:

  • --cacert $CACERT

而不是盲目 -k

我看到的结果

  • GET /version 成功
  • GET pods in auth-lab 返回 HTTP 403
  • 返回里明确写身份是:
    • system:serviceaccount:auth-lab:default

原理解释

这说明:

  • 认证成功
  • 但授权失败

这是整个实验里最关键的一条分界线。


Step 16:用最小权限 pod-reader 身份访问 API

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n auth-lab exec reader-api-client -- \
  sh -c 'TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token); \
  CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt; \
  echo "GET pods in auth-lab"; \
  curl -sS -o /tmp/pods.out -w "HTTP %{http_code}\n" \
    --cacert $CACERT -H "Authorization: Bearer $TOKEN" \
    https://kubernetes.default.svc/api/v1/namespaces/auth-lab/pods; \
  sed -n "1,40p" /tmp/pods.out; \
  echo; \
  echo "GET secret demo-secret"; \
  curl -sS -o /tmp/secret.out -w "HTTP %{http_code}\n" \
    --cacert $CACERT -H "Authorization: Bearer $TOKEN" \
    https://kubernetes.default.svc/api/v1/namespaces/auth-lab/secrets/demo-secret; \
  sed -n "1,40p" /tmp/secret.out'

我看到的结果

  • 读取 Pod 列表:HTTP 200
  • 读取 demo-secretHTTP 403

原理解释

这一步把“最小权限”解释得非常具体:

  • 不是说给了一个“读者角色”就什么都能读
  • 而是只给了 podsget/list/watch
  • 所以 Secret 仍然被拒绝

这正是你以后做生产 RBAC 设计时应该追求的粒度。


Step 17:解码临时 token,只看 payload,不暴露完整凭据

实际命令

TOKEN=$(KUBECONFIG=~/.kube/config-k8s-lab \
  kubectl -n auth-lab create token pod-reader --duration=1h)

python3 - "$TOKEN" <<'PY'
import sys, json, base64
parts = sys.argv[1].split('.')
payload = parts[1]
payload += '=' * (-len(payload) % 4)
obj = json.loads(base64.urlsafe_b64decode(payload))
keys = ['aud','iss','sub','iat','nbf','exp','jti','kubernetes.io']
print(json.dumps({k: obj.get(k) for k in keys}, ensure_ascii=False, indent=2))
PY

为什么只解码 payload,不打印完整 token

因为完整 token 就是凭据本身。

教学可以看结构,但不能养成把敏感 token 到处复制的坏习惯。

我看到的关键字段

  • aud = ["https://kubernetes.default.svc.cluster.local"]
  • iss = "https://kubernetes.default.svc.cluster.local"
  • sub = "system:serviceaccount:auth-lab:pod-reader"
  • iat = 2026-04-10 02:20:20 CST
  • exp = 2026-04-10 03:20:20 CST

原理解释

这一步把几条重要事实串起来了:

  1. token 是 JWT 风格的结构化令牌
  2. token 有发行者和受众
  3. token 有明确过期时间
  4. token 对应的身份字符串就是 ServiceAccount 身份

这也是为什么我在课程文档里明确强调:

  • 现代 ServiceAccount Token 不是“永久门禁卡”

Step 18:确认 kube-root-ca.crt 的来源

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n auth-lab get configmap kube-root-ca.crt -o yaml | sed -n '1,120p'

为什么这一步有必要

很多人会用:

curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt ...

但不知道这个 ca.crt 从哪里来的。

我看到的结果

auth-lab namespace 中真实存在:

  • ConfigMap/kube-root-ca.crt

原理解释

这说明 Pod 里的 ca.crt 并不是魔法文件,而是来自 namespace 中的根 CA ConfigMap,再由 projected volume 组合进容器目录。


Step 19:把实验对象清单再核对一遍

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n auth-lab get role,rolebinding,secret,serviceaccount,pod -o wide

为什么最后还要做一次总核对

因为我要确保教学文档写下来的内容,和真实集群当前状态完全一致。

我确认到的结果

auth-lab 中当前存在:

  • role/pod-reader
  • rolebinding/pod-reader
  • secret/demo-secret
  • serviceaccount/default
  • serviceaccount/pod-reader
  • pod/default-api-client
  • pod/no-token-api-client
  • pod/reader-api-client

原理解释

这一步相当于给整个实验收口,确保:

  • 文档中的对象
  • 集群中的对象
  • 实际观测到的行为

三者是一致的。


本轮最重要的结论

这轮实验最核心的,不是多创建了几个对象,而是把以下几件事变成了“你亲眼看到的事实”:

  1. Pod 能访问 API Server,不等于 Pod 有业务资源权限。
  2. 没有 token 时,系统可能把请求当成 system:anonymous
  3. 默认 ServiceAccount 往往有少量发现类权限,但默认没有业务资源权限。
  4. Role / RoleBinding 只会在绑定作用域内生效。
  5. Secret 权限应被视为高敏感权限,不应顺手就给。
  6. Admission 是真实存在的链路,不是理论补充项。
  7. 现代 Kubernetes 中,ServiceAccount 通常不再自动带长期 Secret,而是使用短期投影 token。

这轮命令里值得特别提醒的两个细节

细节 1:kubectl exec 套壳时要小心本地变量展开

我在拼 kubectl exec ... sh -c ... 这类命令时,刻意把这个坑也记录下来。

原因是:

  • 本地 shell
  • kubectl exec
  • 容器内 sh

是三层解释器。

如果引号处理不好,原本想让容器执行的:

TOKEN=$(cat /var/run/secrets/...)

就可能被本地 shell 先展开,得到完全错误的行为。

这类问题以后在自动化脚本和平台工具里非常常见。

细节 2:curl -k--cacert 不是一回事

-k 的含义是:

  • 不校验证书

--cacert 的含义是:

  • 明确指定信任的 CA 来校验证书

本轮实验里:

  • no-token Pod 没有 ca.crt,所以我用 -k 只为观察匿名身份
  • default / reader Pod 有 ca.crt,所以我用 --cacert 做正规 HTTPS 验证

这不是命令细枝末节,而是安全意识差异。


交付物

本轮新增内容包括:

下一轮可以继续接:

  • ConfigMap / Secret 深入
  • Secret 安全边界
  • 投影卷更新机制
  • env / volume / subPath 行为差异