一次 kubectl create 到底在 API Server 里经过了哪几道关?这篇不画图纸,而是用 curl 直接打 API,让同一个请求在不同关卡被拒绝,从状态码和报错文字反推顺序;再用服务端干跑、/metrics 和 etcd 里的 key 看准入改了什么、什么时候才真正落盘。
[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 之间计数增加的控制器,窗口内集群里还有别的写入,所以它是“被调用过”的集合,不是只属于这一个请求的。
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,每一道关卡拒绝时返回的状态码和报错措辞都不同,排查时可以反过来用它判断请求死在了哪一步。