跳转至

证书管理与 cert-manager

Kubernetes 里几乎每条连接都要过 TLS:apiserver 对外提供服务端证书,kubelet 与用户用客户端证书认证,ServiceAccount 用 token 换身份。手工签发和续期迟早会出错。本章先梳理这套证书体系,再用 cert-manager 把签发、续期、Ingress TLS 自动化,最后走一遍不依赖 cert-manager 的 CSR API 手动签发。

难度 · 进阶 预计 75 分钟 前置:认证授权与 Secret

你将学到

  • 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 信任,适合给运维用户或内部组件签发客户端证书。

kube-root-clusterissuer.yaml
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

private-ca.yaml
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
kubectl apply -f private-ca.yaml

步骤 2 · 用根 CA 签发服务端证书

nginx-server-cert.yaml
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
kubectl apply -f nginx-server-cert.yaml
kubectl get certificate

两张 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 组绑上去

进阶挑战

  1. 用 kube-root-ca 作为 ClusterIssuer,给一个运维用户签发 system:masters 组的客户端证书,并说明这种做法的风险。
  2. 配置 DNS-01 挑战,为 *.magedu.com 申请通配符证书,并比较它与 HTTP-01 的适用边界。

小结

  • 控制面 PKI 由 kubeadm 管理,业务证书由 cert-manager 管理,两者共用同一套「CA 签 CSR」的模型。
  • SA token 已改为 TokenRequest 投射卷,短期、可轮换;不要再依赖自动创建的长效 Secret。
  • 客户端证书的 CN 是用户名、O 是组;私有 CA 的标准路径是 selfSigned 签根、ca Issuer 签叶子。

延伸阅读