bigywater.tech
Kubernetes · 7

API Server 请求链:认证、鉴权、准入、持久化各在哪一步

一次 kubectl create 到底在 API Server 里经过了哪几道关?这篇不画图纸,而是用 curl 直接打 API,让同一个请求在不同关卡被拒绝,从状态码和报错文字反推顺序;再用服务端干跑、/metrics 和 etcd 里的 key 看准入改了什么、什么时候才真正落盘。

401 → 403 → 422 → 403
依次在认证、鉴权、对象校验、校验型准入被拒绝时的 HTTP 状态码
13 → 49
--dry-run=server 回来的 Pod 比提交的多出 36 个字段(不含 status)
15 + 11
创建一个 Pod 时被调用的变更型 + 校验型准入控制器个数

四道关卡各自的拒绝

实测 · 本机执行输出
  [1 令牌是假的] HTTP 401 reason=Unauthorized message=Unauthorized
  [2 不带令牌(匿名)] HTTP 403 reason=Forbidden message=pods is forbidden: User "system:anonymous" cannot create resource "pods" in API group "" in the namespace "k1"
  [3 合法身份但没有 RBAC 权限,请求体还是错的] HTTP 403 reason=Forbidden message=pods is forbidden: User "system:serviceaccount:k1:default" cannot create resource "pods" in API group "" in the namespace "k1"
  [4 有权限,请求体不合法又违反 restricted] HTTP 422 reason=Invalid message=Pod "t422" is invalid: spec.containers[0].ports[0].containerPort: Invalid value: 99999: must be between 1 and 65535, inclusive
  [5 有权限,请求体合法,违反 restricted] HTTP 403 reason=Forbidden message=pods "t403c" is forbidden: violates PodSecurity "restricted:latest": privileged (container "c" must not set securityContext.privileged=true), allowPrivilegeEscalation != false (container "c" must set securityContext.allowPrivilege
  [6 有权限,合法,满足 restricted] HTTP 201 reason=None message=

假令牌是 401:连“你是谁”都没认出来。不带令牌却是 403:kubeadm 集群默认允许匿名访问,匿名请求被认成 system:anonymous,一直走到鉴权才因为没有 RBAC 权限被拒。

第 3 行的请求体本来就有错,服务端回的却是 403 而不是 422——鉴权排在对象校验前面,没有权限的人拿不到“你的对象哪里写错了”这类信息。第 4 行有权限,对象既不合法(端口 99999)又不满足 restricted,返回 422 而不是 403:对象校验排在 PodSecurity 这类校验型准入前面。只有对象合法,PodSecurity 才有机会说 403(第 5 行)。

服务端干跑:准入改了什么

实测 · 本机执行输出
  提交 13 个字段,服务端回来 49 个(不含 status);多出来的 36 个里:
    + metadata.creationTimestamp = "2026-10-10T08:07:36Z"
    + metadata.uid = "359bc628-e0d5-4291-8c02-7693251b9b21"
    + spec.containers[0].imagePullPolicy = "IfNotPresent"
    + spec.containers[0].terminationMessagePath = "/dev/termination-log"
    + spec.containers[0].terminationMessagePolicy = "File"
    + spec.containers[0].volumeMounts[0].mountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
    + spec.containers[0].volumeMounts[0].name = "kube-api-access-4jd29"
    + spec.containers[0].volumeMounts[0].readOnly = true
    + spec.dnsPolicy = "ClusterFirst"
    + spec.enableServiceLinks = true
    + spec.preemptionPolicy = "PreemptLowerPriority"
    + spec.priority = 0
    + spec.restartPolicy = "Always"
    + spec.schedulerName = "default-scheduler"
    + spec.serviceAccount = "default"
    + spec.serviceAccountName = "default"
    + spec.terminationGracePeriodSeconds = 30
    + spec.tolerations[0].effect = "NoExecute"
    + spec.tolerations[0].key = "node.kubernetes.io/not-ready"
    + spec.tolerations[0].operator = "Exists"
    + spec.tolerations[0].tolerationSeconds = 300
    + spec.tolerations[1].effect = "NoExecute"
    + spec.tolerations[1].key = "node.kubernetes.io/unreachable"
    + spec.tolerations[1].operator = "Exists"
    + spec.tolerations[1].tolerationSeconds = 300
    + spec.volumes[0].name = "kube-api-access-4jd29"
    + spec.volumes[0].projected.defaultMode = 420
    + spec.volumes[0].projected.sources[0].serviceAccountToken.expirationSeconds = 3607
    + spec.volumes[0].projected.sources[0].serviceAccountToken.path = "token"
    + spec.volumes[0].projected.sources[1].configMap.items[0].key = "ca.crt"
    + spec.volumes[0].projected.sources[1].configMap.items[0].path = "ca.crt"
    + spec.volumes[0].projected.sources[1].configMap.name = "kube-root-ca.crt"
    + spec.volumes[0].projected.sources[2].downwardAPI.items[0].fieldRef.apiVersion = "v1"
    + spec.volumes[0].projected.sources[2].downwardAPI.items[0].fieldRef.fieldPath = "metadata.namespace"
    + spec.volumes[0].projected.sources[2].downwardAPI.items[0].path = "namespace"
  干跑之后 kubectl get pod k1-dry:Error from server (NotFound): pods "k1-dry" not found

--dry-run=server 会把请求完整走一遍认证、鉴权、准入和校验,只是不写 etcd。提交的 13 个字段回来变成 49 个:imagePullPolicy、restartPolicy、dnsPolicy、terminationGracePeriodSeconds、schedulerName 是 API 的默认值;serviceAccountName 和 kube-api-access-xxxxx 这个 projected 卷、两条 tolerationSeconds: 300 的容忍、priority: 0 与 preemptionPolicy 是准入控制器按各自职责补上的。

回来的对象带 uid 和 creationTimestamp,但紧接着 kubectl get 得到 NotFound——干跑没有留下任何东西。

哪些准入控制器被调用

实测 · 本机执行输出
  type=admit(变更阶段)被调用的准入控制器(15 个):DefaultIngressClass, DefaultStorageClass, DefaultTolerationSeconds, LimitRanger, MutatingAdmissionPolicy, MutatingAdmissionWebhook, NamespaceLifecycle, NodeRestriction, PodGroupProtection, PodTopologyLabels, Priority, RuntimeClass, ServiceAccount, StorageObjectInUseProtection, TaintNodesByCondition
  type=validate(校验阶段)被调用的准入控制器(11 个):CertificateSubjectRestriction, ClusterTrustBundleAttest, LimitRanger, NodeDeclaredFeatureValidator, PodSecurity, Priority, ResourceQuota, RuntimeClass, ServiceAccount, ValidatingAdmissionPolicy, ValidatingAdmissionWebhook

变更阶段(type=admit)有 15 个,校验阶段(type=validate)有 11 个。ServiceAccount、Priority、LimitRanger、RuntimeClass 在两个阶段都出现;PodSecurity、ResourceQuota 只出现在校验阶段,它们不能改对象,只能放行或拒绝。集群里一个 webhook 都没配,MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook 也照样被调用——它们遍历到的 webhook 列表是空的而已。

这份名单来自两次 /metrics 之间计数增加的控制器,窗口内集群里还有别的写入,所以它是“被调用过”的集合,不是只属于这一个请求的。

落盘才有 resourceVersion

实测 · 本机执行输出
  etcd 里 /registry/pods/k1/ 下的 key:['/registry/pods/k1/k1-real', '/registry/pods/k1/t201']
  k1-real:uid=8b55afd1… resourceVersion=1078 creationTimestamp=2026-10-10T08:07:37Z
  干跑返回的对象:uid=a6e75dee resourceVersion=(无) creationTimestamp=2026-10-10T08:07:37Z

真正创建之后,etcd 的 /registry/pods/k1/ 下有 k1-real 和 t201 两个 key,干跑的 k1-dry 没有。resourceVersion 是写进 etcd 时才产生的(这次是 1078),干跑返回的对象没有这个字段;uid 和创建时间则在干跑里就已经生成。

顺序因此是:认证 → 鉴权 → 变更准入 → 对象校验 → 校验型准入 → 写 etcd,每一道关卡拒绝时返回的状态码和报错措辞都不同,排查时可以反过来用它判断请求死在了哪一步。

分步演示

请求链
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / kind(Docker 里的单节点或多节点真实集群,节点是 Docker Desktop 虚拟机里的容器)/ kubectl;用空的 kubeconfig 之外的独立 kubeconfig,不碰用户自己的集群;计时用 date +%s%N / kubectl 事件时间戳,数字每次运行会有小幅抖动。三节点 kind 集群 kx-main(1 个控制面 + 2 个工作节点,Kubernetes v1.37.0,默认的准入插件和 kubeadm 默认的匿名访问设置);用 curl 直接请求 apiserver 的回环端口,管理员身份用绑定了 cluster-admin 的临时 ServiceAccount 令牌,无权限身份用 k1 命名空间的 default ServiceAccount 令牌;k1 命名空间带 pod-security.kubernetes.io/enforce=restricted;etcd 里的 key 用 etcd 静态 Pod 里的 etcdctl 读出;每种情形各一次
    • 没有验证:变更准入先于对象校验这个顺序(本文只直接验证了鉴权先于校验、校验先于校验型准入);每个新增字段具体是哪个准入控制器补的(按各控制器的职责对应,没有逐个关闭插件);配置了 webhook、ValidatingAdmissionPolicy 之后的顺序(后面有专门的文章);审计日志里这些阶段的记录;聚合 API 与 CRD 请求的差别
    京ICP备2026067502号-1京公网安备11010502063487号