证书管理与 cert-manager¶
Kubernetes 里几乎每条连接都要过 TLS:apiserver 对外提供服务端证书,kubelet 与用户用客户端证书认证,ServiceAccount 用 token 换身份。手工签发和续期迟早会出错。本章先梳理这套证书体系,再用 cert-manager 把签发、续期、Ingress TLS 自动化,最后走一遍不依赖 cert-manager 的 CSR API 手动签发。
你将学到¶
- Kubernetes 控制面 PKI 里每个证书文件的用途,以及 SA token 在 v1.31 的签发方式。
- cert-manager 的 Issuer、ClusterIssuer、Certificate、CertificateRequest 关系,以及三条签发路径。
- 用
certificates.k8s.io/v1的 CSR API 给用户 mason 签发客户端证书并完成认证。
为什么需要它¶
证书的难点不在「怎么签」,而在「怎么管」。一个中等集群同时存在 apiserver 服务端证书、访问 kubelet 的客户端证书、etcd peer 证书、front-proxy 证书、用户 kubeconfig 与业务 HTTPS 证书。手工签发的后果是到期时间散落各处、私钥被复制多份、续期靠人记日历,最终以一次集群不可用收场。
Kubernetes 只解决控制面 PKI 的引导:kubeadm 在 /etc/kubernetes/pki 下生成 CA 与各类证书,kube-controller-manager 的 csrsigning 控制器负责批准后的签发。业务与 Ingress 证书不在此范围,cert-manager 补的正是这一段。
核心概念¶
Kubernetes 的证书体系¶
控制面是以 ca.crt 为根的 PKI:apiserver 用 apiserver.crt 证明自己,用 apiserver-kubelet-client.crt 访问 kubelet;etcd 与 front-proxy 各有独立 CA。这些证书由 kubeadm init 生成,默认有效期一年,kubeadm certs renew 负责续期。
用户认证也走客户端证书:CN 是用户名,O 是组。apiserver 用 --client-ca-file 校验签名后把 CN/O 交给 RBAC,所以签证书本质是用集群 CA 签一个 subject 正确的 CSR;证书不带权限,权限仍由 RoleBinding 决定。
ServiceAccount 走另一条路。v1.24 起不再自动创建长期 token Secret,kubelet 通过 TokenRequest API 挂投射卷,token 有 audience、绑定 Pod、默认一小时过期并由 kubelet 轮换。短期 token 用 kubectl create token <sa>。
cert-manager 的资源模型¶
Issuer 是命名空间级签发者,ClusterIssuer 是集群级;Certificate 描述 DNS 名、用途、私钥算法、有效期与 renewBefore;CertificateRequest 是控制器生成的 CSR 对象,签发结果写入 Secret。ACME 场景还会多出 Order 与 Challenge 跟踪域名验证。
flowchart LR
I["Ingress 注解或 Certificate"] --> CR["CertificateRequest"]
CR --> IS["Issuer / ClusterIssuer"]
IS --> CA["自签名、私有 CA 或 ACME"]
CA --> S["Secret 含 tls.crt tls.key ca.crt"]
S --> W["Ingress 引用或 Pod 挂载"]
图:cert-manager 从请求到 Secret 的链路,Issuer 决定用哪种方式签名。
自签名与私有 CA¶
selfSigned Issuer 不依赖任何 CA,直接用 Certificate 自己的私钥签自己。这是引导私有 CA 的标准做法:先用 selfSigned 签出 isCA: true 的根证书存入 Secret,再建 ca 类型 Issuer 引用它,后续叶子证书都由这张根 CA 签发。
这条链签出的证书默认不被系统信任,必须把 ca.crt 分发给客户端。好处是离线可控,适合内网;ca Issuer 只读取 Secret 中的 tls.crt/tls.key,不会自动续期根证书。
ACME 与 Let's Encrypt¶
ACME 是公开 CA 的自动化协议,Let's Encrypt 生产端点为 https://acme-v02.api.letsencrypt.org/directory;支持 HTTP-01 与 DNS-01,前者在 Ingress 上放临时路径由公网访问,后者写 TXT 记录。公网域名优先 HTTP-01,内网或通配符用 DNS-01,并把 solver 配成对应的 DNS provider。
复用集群 CA 作为 ClusterIssuer¶
仓库的 kube-root-ca/ 示例把集群 CA 变成 cert-manager 的签发者:先把 /etc/kubernetes/pki/ca.crt 与 ca.key 导入 cert-manager 命名空间的 TLS Secret(kubectl create secret tls kube-root-ca --cert=/etc/kubernetes/pki/ca.crt --key=/etc/kubernetes/pki/ca.key -n cert-manager),再建 ca 类型 ClusterIssuer。这样签出的证书天然被 apiserver 与 kubelet 信任,适合给运维用户或内部组件签发客户端证书。
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: kube-root-ca-issuer
spec:
ca:
secretName: kube-root-ca
Ingress 自动签发与轮换监控¶
在 Ingress 上加 cert-manager.io/cluster-issuer,并在 spec.tls 里给出 secretName,ingress-shim 会自动创建 Certificate 并填好 Secret。证书续期由 cert-manager 在后台完成,Secret 更新后 ingress-nginx 会热加载。
轮换策略看两个字段:duration 是证书有效期,renewBefore 是提前多久续期;privateKey.rotationPolicy: Always 表示每次续期都换私钥。监控至少要看 certmanager_certificate_expiration_timestamp_seconds 与 certmanager_certificate_ready_status,前者算剩余天数,后者发现签发失败。
CSR API 手动签发¶
不走 cert-manager 时,可以直接用 certificates.k8s.io/v1 的 CertificateSigningRequest。关键字段是 request(PEM 格式 CSR 的 base64)、signerName(用户客户端证书用 kubernetes.io/kube-apiserver-client)与 usages。创建后需要有人 kubectl certificate approve,kube-controller-manager 才会用集群 CA 签名,结果放在 status.certificate。
审批权限要收紧:能 approve 的人等于能让任意 subject 拿到集群 CA 签名,生产环境应用 RBAC 限制 certificatesigningrequests/approval 并校验 signerName。
动手实验¶
实验一:用 Helm 安装 cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--version v1.16.2 --set crds.enabled=true
kubectl -n cert-manager get pods
cmctl check api
三个 Pod 全部 Running,cmctl check api 输出 The cert-manager API is ready 即就绪。仓库用静态清单与 v1.13.3;v1.13 的 chart 用 installCRDs=true,v1.16 改为 crds.enabled=true。
实验二:创建自签名 Issuer、私有 CA 与服务端证书
步骤 1 · 用 selfSigned 签出根 CA
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: selfsigned-issuer
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: private-ca
spec:
isCA: true
commonName: private-ca
secretName: private-ca-secret
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-issuer
kind: Issuer
步骤 2 · 用根 CA 签发服务端证书
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: private-ca-issuer
spec:
ca:
secretName: private-ca-secret
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: nginx-server
spec:
secretName: nginx-server-tls
isCA: false
usages:
- server auth
- client auth
dnsNames:
- "nginx-server.default.svc.cluster.local"
- "nginx-server"
issuerRef:
name: private-ca-issuer
kind: Issuer
两张 Certificate 都应为 READY=True,SECRET 分别是 private-ca-secret 与 nginx-server-tls。
步骤 3 · 验证证书链
kubectl get secret nginx-server-tls -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt
kubectl get secret nginx-server-tls -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt
openssl verify -CAfile ca.crt tls.crt
openssl x509 -in tls.crt -noout -subject -issuer
预期 tls.crt: OK,且 Issuer 是 CN = private-ca。tls.crt 里是服务端证书,ca.crt 是根 CA,tls.key 是私钥,三个键缺一不可。
实验三:用 CSR API 给 mason 签发客户端证书
openssl genrsa -out mason.key 2048
openssl req -new -key mason.key -out mason.csr -subj "/CN=mason/O=developers"
CSR_B64=$(base64 < mason.csr | tr -d '\n')
cat > certificatesignrequest-mason.yaml <<EOF
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: mason
spec:
request: ${CSR_B64}
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 864000
usages:
- client auth
EOF
kubectl apply -f certificatesignrequest-mason.yaml
kubectl certificate approve mason && kubectl get csr mason
预期 CONDITION 为 Approved,Issued。仓库 README 的 openssl req 命令写的是 -key myuser.key,与实际生成的 mason.key 不一致,照抄会报找不到文件,这里已修正。
kubectl get csr mason -o jsonpath='{.status.certificate}' | base64 -d > mason.crt
openssl x509 -in mason.crt -noout -subject -issuer
kubectl --client-certificate=mason.crt --client-key=mason.key \
--certificate-authority=ca.crt --server=https://kubeapi.magedu.com:6443 auth whoami
预期输出:
subject=CN = mason, O = developers
issuer=CN = kubernetes
ATTRIBUTE VALUE
Username mason
Groups [developers system:authenticated]
此时 mason 只有身份,没有任何权限。执行 kubectl auth can-i get pods 会得到 no,需要另外创建 RoleBinding 把 developers 组绑到某个 Role 上。
验证清单¶
- cert-manager 三个 Pod 全部 Running,
cmctl check api返回 ready。 -
kubectl get certificate中私有 CA 与服务端证书都是READY=True。 - 用 mason 的证书执行
auth whoami,用户名与组显示正确。
常见问题与排错¶
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
Certificate 一直 READY=False |
Issuer 不存在、Secret 缺键、DNS 名不合法 | kubectl describe certificate 看事件;检查 issuerRef 的名字与 kind |
| ACME Challenge 一直 pending | HTTP-01 路径不可达、Ingress class 不匹配 | kubectl describe challenge;确认域名解析与 80 端口 |
| Ingress 没有自动创建 Certificate | 注解拼错或 tls.secretName 缺失 |
用 cert-manager.io/cluster-issuer,并补全 spec.tls |
| CSR 一直 Pending | 没人 approve,或 signerName 不被支持 | kubectl certificate approve;核对 signer 名称 |
| mason 认证成功但操作 403 | 只有身份没有授权 | 创建 Role/RoleBinding,把 developers 组绑上去 |
进阶挑战¶
- 用
kube-root-ca作为 ClusterIssuer,给一个运维用户签发system:masters组的客户端证书,并说明这种做法的风险。 - 配置 DNS-01 挑战,为
*.magedu.com申请通配符证书,并比较它与 HTTP-01 的适用边界。
小结¶
- 控制面 PKI 由 kubeadm 管理,业务证书由 cert-manager 管理,两者共用同一套「CA 签 CSR」的模型。
- SA token 已改为 TokenRequest 投射卷,短期、可轮换;不要再依赖自动创建的长效 Secret。
- 客户端证书的 CN 是用户名、O 是组;私有 CA 的标准路径是 selfSigned 签根、ca Issuer 签叶子。