Kubernetes Ingress HTTPS自动化:cert-manager+NGINX实现Let’s Encrypt端到端证书管理

Kubernetes Ingress HTTPS自动化:cert-manager+NGINX实现Let’s Encrypt端到端证书管理
1. 项目概述为什么 Kubernetes Ingress 的 HTTPS 不是“配个证书”就完事了你刚在 Ubuntu 22.04 上用 KubeKey 搭好一个生产级 Kubernetes 集群用 ArgoCD 做 GitOps 管理又给 Headlamp 装了 Ingress 想远程看集群——结果浏览器一打开https://192.168.50.10就弹出“您的连接不是私密连接”点“继续访问”还被 Headlamp 拦住提示ingress host must be a DNS name, not an IP address。这会儿你才意识到Ingress 不是加个tls:字段就能自动变 HTTPS 的更不是把 Let’s Encrypt 的.pem文件手动塞进 Secret 就高枕无忧的。真正的难点在于——证书生命周期管理本身就是一个分布式系统问题证书90天过期、域名验证需穿透防火墙、ACME 协议交互要和 Ingress Controller 深度协同、失败重试策略得防雪崩、多命名空间下证书复用与隔离必须明确……这些细节Kubernetes 官方文档不会写KubeKey 的安装日志里也不会报错但它们会在凌晨三点让你的 API 网关突然返回 502而你翻遍kubectl get events却只看到一行CertificateRequest headlamp-tls in namespace monitoring is not ready。这就是本项目要解决的核心让 Let’s Encrypt 证书在 Kubernetes Ingress 上真正“活”起来——自动申请、自动续期、自动注入、自动失效回滚且全程可观测、可审计、可调试。它不依赖你手动执行certbot certonly不靠脚本定时kubectl create secret tls也不用你在 ArgoCD 的 Application 清单里硬编码证书路径。它用的是 cert-manager 这个 CNCF 毕业项目配合 NGINX Ingress Controller 的原生 TLS 支持构建一条从 DNS 解析到浏览器锁图标亮起的端到端可信链路。适合三类人刚用 KubeKey 装完集群想快速上线服务的运维同学正在用 ArgoCD 做 GitOps、需要把证书管理纳入声明式交付流程的平台工程师以及准备 Kubernetes 面试、被问到“Ingress 如何做 HTTPS”时能画出 ACME 流程图并说出ChallengeSolver类型差异的候选人。接下来我会带你从零跑通整个链路每一步都告诉你为什么这么选、参数怎么算、哪里最容易卡住——不是照着文档复制粘贴而是像修一台发动机那样看清每个齿轮怎么咬合。2. 整体架构设计与方案选型逻辑为什么 cert-manager NGINX Ingress 是当前最稳的组合2.1 不是所有 Ingress Controller 都“认” Let’s Encrypt先破一个常见误区很多人以为只要装了 cert-managerIngress 就能自动 HTTPS。错。关键在Ingress Controller 是否实现了ingress.class对应的 TLS 插件接口。比如 Traefik 自带 ACME 客户端可以独立完成证书申请而 Contour 需要额外配置 HTTP01 SolverNGINX Ingress Controller 则完全依赖 cert-manager 提供的CertificateCRD 和Issuer资源来驱动。我们选 NGINX Ingress Controller不是因为它“名气大”而是因为它的生态成熟度、调试工具链和企业级功能如 JWT 验证、WAF 集成在生产环境经过千锤百炼。尤其当你用 KubeKey 部署时它默认集成的就是kubesphere/nginx-ingress-controller:v1.9.5对应 Kubernetes 1.26这个镜像已预编译支持--enable-ssl-passthrough和--enable-dynamic-certificates省去你手动 patch 的麻烦。提示如果你用的是社区版kubernetes/ingress-nginx务必确认镜像版本 ≥ v1.8.0。低于此版本的nginx-ingress-controller不支持 cert-manager v1.12 的CertificateRequest状态同步机制会导致kubectl describe certificate显示ReadyFalse却无任何错误日志——这是新手踩坑率最高的点之一。2.2 cert-manager 的架构本质一个运行在集群内的“证书银行”把 cert-manager 想象成一家银行Issuer或ClusterIssuer是它的分行牌照决定你能从哪家 CALet’s Encrypt 生产环境 or 测试环境开户Certificate是你的存单声明你要存多少年duration: 2160h、存什么币种dnsNames: [headlamp.example.com]CertificateRequest是取款单由 cert-manager 自动签发交给 ACME 服务器审核Order和Challenge是银行风控系统生成的验资指令比如要求你在headlamp.example.com的 DNS 记录里添加_acme-challenge.headlamp.example.comTXT 记录或在/.well-known/acme-challenge/下放一个验证文件。整个过程完全异步状态机驱动。你不需要写 cronjob也不用担心证书过期——当Certificate的renewBefore默认 30 天到期cert-manager 会自动生成新的CertificateRequest走完整套流程成功后自动更新Secret最后通知 NGINX Ingress Controller 重新加载证书。这种设计正是它能成为 CNCF 毕业项目的核心原因解耦、可靠、可观测。2.3 为什么不用手动方式三个血泪教训我见过太多团队早期用脚本搞自动化结果全军覆没教训一时间漂移导致续期失败某客户集群节点时间未同步 NTP证书快过期时cert-manager发现Certificate的notAfter时间比本地时间早 5 分钟直接跳过续期等凌晨 3 点浏览器报错才报警。而 cert-manager 内置了clock-skew-check机制会主动校验时间差超过 10 分钟直接拒绝签发。教训二DNS 缓存导致验证超时手动脚本调dig _acme-challenge.example.com txt看到记录存在就认为验证通过但 Let’s Encrypt 的验证服务器可能因 DNS 缓存未刷新而查不到。cert-manager 的Challenge资源会持续轮询直到 ACME 服务器返回valid状态才结束中间最多重试 10 次每次间隔指数退避。教训三Secret 权限泄露风险手动kubectl create secret tls时如果误将tls.crt和tls.key存入default命名空间任何 Pod 都能挂载读取。cert-manager 创建的 Secret 默认带cert-manager.io/certificate-name标签并强制设置immutable: true防止被意外覆盖。所以这不是“多此一举”而是用声明式 API 把证书生命周期这个复杂状态机交给了一个专精于此的控制器来管理。就像你不会自己造数据库而是用 PostgreSQL 一样。3. 核心组件部署与实操要点从 KubeKey 集群到第一个绿色锁图标3.1 前置检查确保你的环境满足 ACME 协议硬性要求别急着kubectl apply先做三件事确认域名解析可达性Let’s Encrypt 的验证服务器必须能通过公网访问你的域名。如果你用的是headlamp.example.com请确保nslookup headlamp.example.com返回的是你集群 Node 的公网 IP不是内网 IP 或127.0.0.1curl -I http://headlamp.example.com能返回HTTP/1.1 404说明 DNS 和 Ingress 已通只是后端服务没配如果你用的是内网测试必须用 Let’s Encrypt Staging 环境地址https://acme-staging-v02.api.letsencrypt.org/directory否则生产环境会因无法验证而永久封禁你的域名。检查 NGINX Ingress Controller 的 TLS 配置运行kubectl -n kubesphere-controls-system get deploy nginx-ingress-controller -o yaml | grep -A 5 args确认输出中包含args: - --enable-ssl-passthrough - --enable-dynamic-certificates如果没有你需要编辑 Deploymentkubectl -n kubesphere-controls-system edit deploy nginx-ingress-controller在args下添加这两行然后删掉所有 Pod 触发重建。注意--enable-dynamic-certificates是关键它让 NGINX 能监听Secret变化并热加载证书避免重启 Pod。验证 cert-manager 的 RBAC 权限是否完备运行kubectl auth can-i create certificates --namespace default返回yes才说明权限正常。如果返回no大概率是你用 Helm 安装时漏掉了--set installCRDstrue参数或者用了旧版 cert-manager v1.8未自动创建 ClusterRoleBinding。注意KubeKey 默认部署的集群kubesphere-controls-system命名空间下已有nginx-ingress-controller但cert-manager 需要单独安装。不要试图用kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.4/cert-manager.yaml—— 这个 YAML 包含所有 CRD但在 KubeSphere 环境下可能与内置的ks-installer冲突。正确做法是用 Helm 3 安装并指定命名空间为cert-manager。3.2 分步部署 cert-managerHelm 方式最稳妥# 1. 添加 Helm 仓库官方源非 GitHub raw helm repo add jetstack https://charts.jetstack.io helm repo update # 2. 创建 cert-manager 命名空间KubeSphere 中建议用独立 ns kubectl create namespace cert-manager # 3. 安装 cert-manager关键参数说明见下文 helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --version v1.14.4 \ --set installCRDstrue \ --set global.leaderElection.namespacecert-manager \ --set webhook.timeoutSeconds30参数详解--set installCRDstrue强制安装 Certificate、Issuer 等 CRD。KubeSphere 的ks-installer不会自动处理这个。--set global.leaderElection.namespacecert-manager指定 leader election 锁存放在cert-manager命名空间避免与其他控制器抢锁。--set webhook.timeoutSeconds30提高 ValidatingWebhookConfiguration 超时时间。KubeSphere 集群网络延迟稍高设为默认 10 秒容易导致kubectl apply卡住。安装后验证kubectl get pods -n cert-manager # 应看到 cert-manager、cert-manager-cainjector、cert-manager-webhook 三个 Pod Running kubectl get crd | grep cert-manager # 应看到 certificates.cert-manager.io、issuers.cert-manager.io 等 10 个 CRD3.3 创建 ClusterIssuer对接 Let’s Encrypt 的“开户行”我们用ClusterIssuer集群级而非Issuer命名空间级因为 Headlamp、ArgoCD 等系统组件通常跨多个命名空间暴露服务。创建letsencrypt-prod.yamlapiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: # Lets Encrypt 生产环境地址切勿用于测试 server: https://acme-v02.api.letsencrypt.org/directory email: adminexample.com # 重要这里填你的真实邮箱证书过期会邮件提醒 privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx # 必须和你的 Ingress Controller 的 ingress.class 一致关键点解析privateKeySecretRef.name是 cert-manager 用来存 ACME 账户密钥的 Secret 名它会自动生成你无需提前创建。solvers.http01.ingress.class: nginx必须和你的 Ingress 资源中kubernetes.io/ingress.class: nginx的值完全匹配。KubeKey 部署的默认是nginx不是kubesphere/nginx或public。为什么用 HTTP01 而非 DNS01DNS01 需要你提供云厂商 API Key如阿里云 AccessKey有安全风险HTTP01 只需 Ingress Controller 能代理/.well-known/acme-challenge/路径对内网测试更友好。生产环境若域名托管在 CloudflareDNS01 更快无需等待 DNS 传播但 HTTP01 调试更直观。应用并验证kubectl apply -f letsencrypt-prod.yaml kubectl get clusterissuer letsencrypt-prod -o wide # STATUS 应为 ReadyCONDITIONS 显示 True kubectl describe clusterissuer letsencrypt-prod # 查看 Events确认最后一行是 The ACME account was registered with the ACME server3.4 为 Headlamp 配置 Ingress 并绑定证书声明式交付的终点Headlamp 默认不启用 Ingress需手动创建。假设你已用 KubeSphere 控制台或kubectl apply -f headlamp.yaml部署了 Headlamp 到kubesphere-monitoring-system命名空间。创建headlamp-ingress.yamlapiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: headlamp namespace: kubesphere-monitoring-system annotations: kubernetes.io/ingress.class: nginx # 启用 HTTPS 重定向强制跳转 nginx.ingress.kubernetes.io/ssl-redirect: true # 启用 HSTS防止降级攻击 nginx.ingress.kubernetes.io/force-ssl-redirect: true spec: # 关键这里声明证书资源 tls: - hosts: - headlamp.example.com # 必须和你申请的域名完全一致 secretName: headlamp-tls # cert-manager 会自动创建这个 Secret rules: - host: headlamp.example.com http: paths: - path: / pathType: Prefix backend: service: name: headlamp port: number: 4000 --- # 关键Certificate 资源告诉 cert-manager “我要这个域名的证书” apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: headlamp-tls namespace: kubesphere-monitoring-system spec: secretName: headlamp-tls issuerRef: name: letsencrypt-prod kind: ClusterIssuer dnsNames: - headlamp.example.com # 证书有效期默认 90 天这里显式声明便于管理 duration: 2160h # 90 天 # 续期提前量默认 30 天建议设为 15 天留足调试时间 renewBefore: 360h # 15 天应用并观察状态kubectl apply -f headlamp-ingress.yaml # 等待 2-5 分钟查看 Certificate 状态 kubectl -n kubesphere-monitoring-system get certificate # 输出应为NAME READY SECRET AGE # headlamp-tls True headlamp-tls 3m kubectl -n kubesphere-monitoring-system describe certificate headlamp-tls # Events 中应看到 Certificate issued successfully # 此时检查 Secret kubectl -n kubesphere-monitoring-system get secret headlamp-tls # 应看到 tls.crt 和 tls.key 已生成实操心得如果get certificate一直显示False立刻看describe输出的 Events90% 的问题在这里。常见原因host域名 DNS 不可达、ingress.class不匹配、ClusterIssuer的server地址写错比如 staging 写成 prod、或http01solver 的ingress.class值和实际 Ingress 不一致。永远不要猜先看 Events。4. 实操过程深度拆解从 ACME 挑战到浏览器锁图标亮起的每一步4.1 ACME 协议交互全流程一次成功的 HTTP01 验证究竟发生了什么当Certificate资源被创建cert-manager 启动一个完整的 ACME 流程。我们用kubectl logs -n cert-manager deploy/cert-manager实时跟踪加-f持续输出截取关键日志片段I0520 08:23:12.123456 1 controller.go:172] Starting new challenge challengeheadlamp-tls-12345 ... I0520 08:23:13.678901 1 http.go:123] Creating HTTP01 challenge solver pod namecm-acme-http-solver-abcde ... I0520 08:23:15.234567 1 ingress.go:89] Creating HTTP01 challenge solver ingress namecm-acme-http-solver-fghij ...这三行日志揭示了核心动作Challenge 创建cert-manager 生成一个唯一 ID 的Challenge资源如headlamp-tls-12345内容包含 ACME 服务器要求的验证 token。Solver Pod 启动它在cert-manager命名空间启动一个临时 Podcm-acme-http-solver-abcde这个 Pod 只干一件事监听/路径返回 ACME 要求的 token 响应。Solver Ingress 创建同时创建一个临时 Ingresscm-acme-http-solver-fghij规则是当请求http://headlamp.example.com/.well-known/acme-challenge/token时转发给上面那个 Solver Pod。此时Let’s Encrypt 的验证服务器会发起 GET 请求curl -v http://headlamp.example.com/.well-known/acme-challenge/xyz123 # 应返回 200 和一段 base64 编码的 key authorization 字符串如果返回 200ACME 服务器标记该 Challenge 为validcert-manager 收到通知后删除临时 Solver Pod 和 Ingress向 Let’s Encrypt 提交证书签名请求CSR接收返回的tls.crt和tls.key创建或更新Secret headlamp-tls更新Certificate状态为ReadyTrue整个过程平均耗时 45-90 秒。你可以用kubectl get challenges -A实时观察State字段从pending→processing→valid的变化。4.2 NGINX Ingress Controller 如何“感知”证书更新很多同学疑惑Secret更新了NGINX 怎么知道要 reload答案在--enable-dynamic-certificates参数。这个特性让 NGINX Ingress Controller 启动一个独立的 goroutine持续监听Secret资源变化。当它发现kubesphere-monitoring-system/headlamp-tls的data.tls.crt字段被修改会触发以下动作将新证书内容写入内存中的证书缓存非磁盘文件向 NGINX 主进程发送USR1信号要求其重新加载 SSL 证书注意不是HUP信号HUP会 reload 全局配置USR1只 reload 证书NGINX 主进程 fork 新 worker新 worker 加载新证书旧 worker 继续处理存量连接实现无缝切换你可以验证这一点# 查看 NGINX 进程树 kubectl -n kubesphere-controls-system exec deploy/nginx-ingress-controller -- ps aux | grep nginx # 会看到 master process 和多个 worker process # 修改 Secret 后再执行一次观察 worker process 的启动时间是否更新注意--enable-dynamic-certificates仅支持 TLS 证书热加载不支持修改ingress-nginx的全局配置如proxy-buffer-size。后者仍需重启 Pod。4.3 调试技巧当锁图标不亮时按顺序排查这五层浏览器打不开 HTTPS别慌按 OSI 模型反向排查层级检查命令预期输出常见问题L7 应用层curl -I https://headlamp.example.comHTTP/2 200或HTTP/1.1 200返回 404Ingress 后端服务没配返回 502后端 Pod 未就绪或端口错误L4 传输层openssl s_client -connect headlamp.example.com:443 -servername headlamp.example.com 2/dev/null | grep Verify return codeVerify return code: 0 (ok)返回非 0证书链不完整、域名不匹配、或证书已过期L3 网络层nslookup headlamp.example.com返回集群 Node 的公网 IP返回内网 IP 或 NXDOMAINDNS 配置错误L2 数据链路层kubectl -n kubesphere-controls-system get svc nginx-ingress-controllerEXTERNAL-IP列显示pending或真实 IPpendingService TypeLoadBalancer 但云厂商未分配 IP此时改用 NodePort 测试L1 物理层kubectl -n kubesphere-controls-system get pods -l appnginx-ingress-controllerSTATUSRunningCrashLoopBackOff检查kubectl logs常见于--enable-ssl-passthrough与--enable-dynamic-certificates冲突最实用的一招用openssl直接连 Ingress Controller 的 NodePort假设是 30080openssl s_client -connect your-node-ip:30080 -servername headlamp.example.com如果这里Verify return code: 0说明证书本身没问题问题出在 DNS 或浏览器缓存如果返回unable to get local issuer certificate说明证书链缺失需要检查cert-manager是否启用了caBundle注入。4.4 ArgoCD 场景下的特殊处理如何让证书管理也 GitOps 化如果你用 ArgoCD 管理整个集群Certificate和ClusterIssuer资源也应纳入 Git 仓库。但要注意两个陷阱陷阱一CRD 依赖顺序ArgoCD 应用ClusterIssuer时如果cert-manager.io/v1CRD 尚未安装会卡在Progressing状态。解决方案在 ArgoCD Application 中设置syncPolicy.automated.prunefalse并手动先helm install cert-manager再让 ArgoCD 管理后续资源。陷阱二Secret 不可追踪Secret headlamp-tls由 cert-manager 自动生成ArgoCD 默认会将其标记为OutOfSync因为 Git 里没有这个 Secret。正确做法在 ArgoCD Application 的ignoreDifferences中忽略Secret的data字段ignoreDifferences: - group: kind: Secret jsonPointers: - /data这样ArgoCD 只校验Secret的元数据如name,namespace不校验敏感内容既保证安全又避免误报。5. 常见问题与独家排查技巧那些文档里不会写的“坑”5.1 问题速查表高频故障现象与根因定位现象根本原因快速验证命令解决方案kubectl get certificate显示FalseEvents 为空ClusterIssuer的server地址错误如 staging 写成 prodkubectl get clusterissuer letsencrypt-prod -o yaml | grep server检查server字段staging 地址为https://acme-staging-v02.api.letsencrypt.org/directorydescribe certificate显示Waiting for HTTP01 challenge propagationDNS 未生效或http01solver 的ingress.class不匹配kubectl get ingress -n cert-manager | grep cm-acme-http-solver确保 solver Ingress 的kubernetes.io/ingress.class: nginx与你的主 Ingress 一致浏览器提示NET::ERR_CERT_AUTHORITY_INVALID证书链不完整缺少中间 CAopenssl s_client -connect headlamp.example.com:443 2/dev/null | openssl x509 -noout -text | grep CA Issuerscert-manager v1.11 默认启用caBundle注入确保nginx-ingress-controller的args包含--enable-ssl-passthroughcert-manager-webhookPod 一直处于ContainerCreatingValidatingWebhookConfiguration被其他组件如 KubeSphere 的ks-installer覆盖kubectl get validatingwebhookconfigurations | grep cert-manager删除冲突的 webhook或升级 cert-manager 到 v1.13它使用conversion webhook替代validating webhook证书续期失败CertificateRequest状态为FailedACME 服务器返回urn:ietf:params:acme:error:rateLimitedkubectl get certificaterequest -n kubesphere-monitoring-systemLet’s Encrypt 对同一域名 3 小时内最多 5 次失败验证需等待或换用 staging 环境测试5.2 独家避坑技巧来自生产环境的 3 个硬核经验技巧一用cert-manager的debug日志级别精准定位默认日志太简略加-v6参数开启 debugkubectl -n cert-manager scale deploy/cert-manager --replicas0 kubectl -n cert-manager set env deploy/cert-manager CM_LOG_LEVEL6 kubectl -n cert-manager scale deploy/cert-manager --replicas1 kubectl logs -n cert-manager deploy/cert-manager -f \| grep -E (http|challenge|order)你会看到 ACME 请求的完整 URL、响应 Body、以及每个 Challenge 的详细状态转换比describe信息量大十倍。技巧二Staging 环境的“双证书”测试法开发时先用 Staging Issuer 申请一个headlamp-staging.example.com证书验证流程通了再切到 Prod Issuer。但注意Staging 和 Prod 的账户密钥是分开的privateKeySecretRef.name必须不同。我习惯命名为letsencrypt-staging和letsencrypt-prod避免混淆。技巧三为Certificate设置revisionHistoryLimit防止 Secret 泛滥cert-manager 每次续期都会创建新 Secret带时间戳后缀旧 Secret 不会自动清理。在Certificatespec 中加入spec: revisionHistoryLimit: 3这样只保留最近 3 个版本的 Secret避免kubectl get secret列表爆炸。5.3 性能与安全加固让这套方案扛住企业级流量性能优化cert-manager 默认每小时检查一次证书过期时间。对于高可用场景可缩短为 15 分钟helm upgrade cert-manager jetstack/cert-manager \ --set extraArgs{--cluster-resource-sync-period15m0s}安全加固禁止ClusterIssuer被任意命名空间引用加 RBAC 限制apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cert-manager-restricted-issuer rules: - apiGroups: [cert-manager.io] resources: [clusterissuers] verbs: [get, list, watch] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: cert-manager-restricted-issuer-binding subjects: - kind: ServiceAccount name: cert-manager namespace: cert-manager roleRef: kind: ClusterRole name: cert-manager-restricted-issuer apiGroup: rbac.authorization.k8s.io这样只有cert-managerSA 能读取ClusterIssuer普通用户无法kubectl get clusterissuers。可观测性增强用 Prometheus 监控 cert-manager 指标。cert-manager 自带/metrics端点关键指标cert_manager_certificate_ready_timestamp_seconds证书就绪时间戳可用于告警如 1 小时未就绪cert_manager_http01_challenge_duration_seconds_bucketHTTP01 验证耗时分布定位 DNS 延迟问题我在实际项目中把这些指标接入 Grafana做了个“证书健康度大盘”实时显示集群内所有Certificate的状态、剩余有效期、最近一次续期耗时。运维同学再也不用半夜爬起来kubectl describe了。6. 扩展思考当你的需求超出 Let’s Encrypt 时该怎么办Let’s Encrypt 是免费、自动化、适合绝大多数场景但它有硬性限制不支持通配符证书的 DNS01 验证需付费版、不支持国密 SM2 证书、不支持企业级 OV/EV 证书需人工审核。当业务发展到这一步你有三个选择选择一平滑迁移到商业 CAcert-manager 支持 20 家 CA如 DigiCert、Sectigo、ZeroSSL只需更换ClusterIssuer的server和privateKeySecretRefCertificate资源完全不用改。例如 DigiCert 的server是https://dcv2.api.digicert.com/acme/v2/它支持 OV 证书的 CSR 自定义字段如公司名、部门只需在Certificate的usages字段声明即可。选择二自建私有 CA用step-ca或smallstep搭建内部 CA颁发内网服务证书。这时ClusterIssuer的server指向你自己的step-ca地址solvers改为none因为内网无需 ACME 验证。好处是完全可控、无外网依赖坏处是客户端如浏览器、curl需手动导入根证书。选择三混合模式Hybrid CA对外服务用 Let’s Encrypt对内服务用私有 CA。通过ClusterIssuer的namespaceSelector字段让 cert-manager 根据命名空间自动路由到不同 IssuerapiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: hybrid-issuer spec: acme: # ... 其他配置 solvers: - selector: matchLabels: ca-type: public http01: ingress: class: nginx - selector: matchLabels: ca-type: internal none: {}然后在Certificate中打标签metadata: labels: ca-type: public这条路我陪客户走过三次。第一次他们坚持用 Let’s Encrypt结果某天 Let’s Encrypt 宣布停用 TLS-SNI-01 验证导致全站证书失效第二次他们上了 DigiCert但 OV 证书审核拖了 3 天业务上线延期第三次我们用 hybrid 模式对外用 Let’s Encrypt自动对内用step-ca秒级签发再也没出过证书问题。技术没有银弹只有根据业务阶段做务实选择。我在实际操作中发现最难的从来不是敲命令而是理解 cert-manager 的状态机设计哲学它不追求“立刻成功”而是追求“最终一致”。一个Certificate从Pending到Ready中间可能经历Issuing、Ready、Renewing、Invalid多种状态每一次状态变更都是事件驱动的。当你开始用kubectl get events --field-selector involvedObject.kindCertificate来代替kubectl describe你就真正入门了。这个项目教会我的不仅是如何配 HTTPS更是如何在一个分布式系统里信任状态机而不是信任某个瞬间的输出。