T
traeai
登录
返回首页
freeCodeCamp.org

如何使用cert-manager、Let's Encrypt和内部TLS加密Kubernetes流量

8.5Score

TL;DR · AI 摘要

本文详细指导如何通过cert-manager、Let's Encrypt和内部TLS加密Kubernetes流量,涵盖证书管理、Ingress TLS配置及服务间加密实践。

核心要点

  • cert-manager通过Issuer/ClusterIssuer与Certificate资源实现证书自动签发和轮换,避免因证书过期导致的停机
  • 使用ACME Issuer配置Let's Encrypt需完成DNS验证,可自动化Ingress TLS配置并支持HTTP/HTTPS双协议
  • 内部服务加密需自建私有CA,通过ClusterIssuer实现跨命名空间证书管理,Pod间通信需配合mTLS或服务网格

结构提纲

按章节快速跳转。

  1. 明确Kubernetes默认加密范围,指出Pod间通信和Ingress流量未加密的典型安全漏洞

  2. §cert-manager核心机制解析

    介绍cert-manager作为Kubernetes Operator的工作原理,通过自定义资源实现证书全生命周期管理

  3. 对比命名空间级与集群级证书签发器的适用场景,强调ACME与私有CA的配置差异

  4. 分步演示Let's Encrypt Ingress TLS配置和内部服务加密方案的具体实施方法

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Kubernetes流量加密方案
    • 证书管理
      • cert-manager
      • Issuer/ClusterIssuer
      • 自动轮换
    • 加密场景
      • Ingress TLS
      • 服务间通信
      • 私有CA
    • 配置组件
      • ACME协议
      • Kubernetes Secret
      • DNS验证

金句 / Highlights

值得收藏与分享的关键句。

#Kubernetes#cert-manager#TLS#Let's Encrypt#Ingress
打开原文

Most engineers assume their Kubernetes cluster encrypts all of its traffic. It doesn't. The commands you run with kubectl are encrypted — your client and the API server speak TLS. The API server talking to etcd is usually encrypted too, depending on how the cluster was provisioned.

But traffic between your pods? Plaintext by default. Ingress traffic from the internet to your services? Only encrypted if you explicitly configure TLS. And certificates for internal services? You have to provision those yourself.

This is not a Kubernetes oversight. It's a deliberate design choice — Kubernetes provides the primitives and leaves the implementation to you. The problem is that certificate management is notoriously painful. Certificates expire. Provisioning them manually doesn't scale. Forgetting to rotate them causes outages.

cert-manager solves this. It runs as a controller inside your cluster, watches for Certificate resources, requests certificates from configured issuers, stores them in Kubernetes Secrets, and rotates them automatically before they expire. You declare what you want, cert-manager makes it happen and keeps it that way.

In this article you'll work through how cert-manager's core model works, automate public Ingress TLS using Let's Encrypt, set up an internal Certificate Authority for service-to-service encryption, and understand how certificate rotation works so outages caused by expired certificates become a thing of the past.

Prerequisites

  • A kind cluster with the nginx Ingress controller installed
  • Helm 3 installed
  • A domain name with DNS you control — needed for the Let's Encrypt demo
  • Basic understanding of TLS: you know what a certificate, a private key, and a CA are

All demo files are in the DevOps-Cloud-Projects GitHub repository.

Table of Contents

Kubernetes 中哪些流量是加密的,哪些不是?

在安装任何工具之前,先明确集群已有的保护机制和存在的安全缺口:

| 流量路径 | 默认加密? | 说明 | | --- | --- | --- | | kubectl → API 服务器 | 是 | 通过集群 CA 进行 TLS 加密 | | API 服务器 → etcd | 通常加密 | 取决于集群创建方式 — 需验证具体配置 | | API 服务器 → kubelet | 是 | TLS 加密,但 kubelet 证书验证依赖配置 | | Pod → Pod(同集群) | | 默认明文传输,需添加服务网格或 mTLS | | 互联网 → Ingress | | 需主动配置 — 在 Ingress 资源中设置 TLS | | Pod → Kubernetes API | 是 | 通过服务账户令牌和集群 CA 验证 |

实践中最需要关注的两个缺口是 Pod 到 Pod 的流量和 Ingress TLS 加密。本文将覆盖通过 Let's Encrypt 实现 Ingress TLS 加密,以及使用私有 CA 实现服务间加密。

cert-manager 是一个 Kubernetes Operator。它通过自定义资源扩展 Kubernetes API,表示证书请求及其配置。当你创建 Certificate 资源时,cert-manager 控制器会自动处理:向配置的签发方请求证书,将证书和私钥存储到 Kubernetes Secret,并在到期前自动续期。应用程序只需读取 Secret,无需关心证书管理细节。

四大核心资源

cert-manager 引入了四个常用自定义资源:

| 资源 | 代表内容 | | --- | --- | | Issuer | 证书颁发机构或 ACME 账户 — 命名空间作用域 | | ClusterIssuer | 与 Issuer 相同,但集群范围内可用 | | Certificate | 证书请求声明 — 定义所需证书规格 | | CertificateRequest | 单个签名请求 — 由 cert-manager 自动生成,极少直接操作 |

实际使用中主要配置 ClusterIssuerCertificateClusterIssuer 定义证书来源,Certificate 定义所需证书规格及存储位置。

Issuer 和 ClusterIssuer

Issuer 仅能在所属命名空间内签发证书,而 ClusterIssuer 可跨命名空间使用。对于 Let's Encrypt 等共享基础设施,建议使用 ClusterIssuer;对于特定应用的内部 CA,则应使用限定在应用命名空间的 Issuer

cert-manager 支持多种签发方类型,最常见的三种是:

ACME — 用于从 Let's Encrypt 或兼容 ACME 协议的 CA 获取公共证书。通过 HTTP-01 或 DNS-01 挑战验证域名所有权。

CA — 使用存储在 Kubernetes Secret 中私钥的 CA 签发内部证书。用于集群内的服务间 TLS 加密。

Self-signed — 生成自签名证书。单独使用场景较少,但作为创建内部 CA 的初始步骤不可或缺。

证书生命周期

创建 Certificate 资源后,cert-manager 会按以下流程处理:

  1. 创建包含 CSR(证书签名请求)的 CertificateRequest
  1. 将 CSR 提交给配置的签发方
  1. 对于 ACME 签发方:创建 Challenge 资源并完成验证(后文详述)
  1. 从签发方获取已签名证书
  1. 将证书和私钥存储到 spec.secretName 指定的 Kubernetes Secret
  1. 监控证书有效期 — 默认在有效期限的 2/3 时触发续期

您的应用程序挂载 Secret。cert-manager 会静默更新它。大多数能够监听文件变化的应用无需重启即可获取新证书。

ACME 挑战:HTTP-01 vs DNS-01

Let's Encrypt 需要验证您对域名的控制权才能签发证书。ACME 协议为此定义了两种挑战类型。

HTTP-01 通过让 cert-manager 在 http://<your-domain>/.well-known/acme-challenge/<token> 创建临时 HTTP 端点实现。Let's Encrypt 会请求该 URL,若响应与预期 token 匹配则挑战通过。这需要集群在 80 端口可从互联网访问。

DNS-01 通过让 cert-manager 在 _acme-challenge.<your-domain> 创建临时 DNS TXT 记录实现。Let's Encrypt 会验证该记录是否存在。此方法无需入向 HTTP 访问,适合私有集群,并且是获取通配符证书(如 *.example.com)的唯一方式。

权衡:HTTP-01 配置简单但仅适用于单域名且需要可公开访问的基础设施;DNS-01 需要 DNS 提供商的 API 访问权限,但支持内部集群和通配符证书。

演示 1 — 安装 cert-manager 并使用 Pebble 和 Let's Encrypt 签发证书

Pebble 是 Let's Encrypt 的本地 ACME 测试服务器。它运行在集群内部,使用与 Let's Encrypt 完全相同的 ACME 协议签发证书,且无需公开域名或互联网访问。通过 Pebble 可在普通 kind 集群上完整测试 cert-manager 的流程(挑战、签发、续期)。

理解本地流程后,切换到真实 Let's Encrypt 仅需一行配置修改:替换 ClusterIssuer 的服务器 URL,并将 DNS 记录指向可公开访问的集群。其余配置保持一致。

您将安装 cert-manager、创建 Let's Encrypt 的 ClusterIssuer、部署带有 Ingress 的示例应用,并观察证书自动签发和存储过程。

步骤 1:安装 cert-manager

cert-manager 现通过 OCI Helm 图表从 quay.io/jetstack 发布。--set crds.enabled=true 标志会随图表自动安装 CRD:

bash
helm upgrade cert-manager oci://quay.io/jetstack/charts/cert-manager \
  --install \
  --create-namespace \
  --namespace cert-manager \
  --set crds.enabled=true \
  --version v1.17.0 \
  --wait

还需安装 nginx Ingress 控制器(cert-manager 通过它路由 HTTP-01 挑战)。controller.service.type=ClusterIP 是针对 kind 的特殊配置:默认的 LoadBalancer Service 在 kind 中无法获取 EXTERNAL-IP(因为没有云负载均衡器),会导致 --wait 挂起。真实集群中可移除此覆盖并保留 LoadBalancer:

bash
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.service.type=ClusterIP \
  --wait

确认所有组件运行正常:

bash
kubectl get pods -n cert-manager
kubectl get pods -n ingress-nginx
bash
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-76f84784c8-r4fx4              1/1     Running   0          6m45s
cert-manager-cainjector-66fbf49587-gv25n   1/1     Running   0          6m45s
cert-manager-webhook-577fddf86-l5wj4       1/1     Running   0          6m45s

NAME                                        READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-6c7cd85885-h7zgx   1/1     Running   0          3m34s

kind 特定注意事项 — 立即删除 nginx admission webhook。**在 kind 中,nginx admission webhook 使用自签名证书,而 Kubernetes API 服务器无法验证。首次创建 Ingress 资源时会报错 failed calling webhook "validate.nginx.ingress.kubernetes.io": ... x509: certificate signed by unknown authority。提前删除 webhook 可避免后续问题:

bash
kubectl delete validatingwebhookconfiguration ingress-nginx-admission

步骤 2:安装 Pebble

Pebble 是由 JupyterHub 维护的本地 ACME 测试服务器,配套部署的 pebble-coredns 用于 ACME 验证期间的域名解析:

bash
helm install pebble pebble \
  --repo https://jupyterhub.github.io/helm-chart/ \
  --namespace pebble \
  --create-namespace \
  --wait

确认两个 Pod 运行正常:

bash
kubectl get pods -n pebble
bash
NAME                              READY   STATUS    RESTARTS   AGE
pebble-8d8d49d64-lz8ck            1/1     Running   0          36s
pebble-coredns-7fb5c7cbf4-4jw9h   1/1     Running   0          36s

步骤 3:配置虚假域名的 DNS

我们将为 echo.pebble.local 签发证书。该域名不存在于真实 DNS 中,因此需提前配置两个独立的 DNS 解析器:

| 解析器 | 由...使用 | 需要实现的功能 | | --- | --- | --- | | pebble-coredns(pebble 命名空间) | Pebble 本身,在发起 HTTP-01 验证请求时 | 将 echo.pebble.local 解析到 ingress-nginx ClusterIP | | 集群 CoreDNS(kube-system) | cert-manager 的 HTTP-01 预检 | 将 pebble.local 查询转发到 pebble-coredns |

若遗漏任一配置,订单(Order)会因 DNS 解析失败进入 invalid 状态。

首先获取所需 IP 地址:

bash
NGINX_IP=$(kubectl get svc -n ingress-nginx ingress-nginx-controller \
  -o jsonpath='{.spec.clusterIP}')
PEBBLE_DNS_IP=$(kubectl get svc pebble-coredns -n pebble \
  -o jsonpath='{.spec.clusterIP}')
echo "NGINX_IP=$NGINX_IP PEBBLE_DNS_IP=$PEBBLE_DNS_IP"

修补pebble-coredns以使用入口控制器的IP响应*.pebble.local。CoreDNS的template插件在整块配置压缩为单行时解析不可靠,因此需要应用真正的多行ConfigMap:

code
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: pebble-coredns
  namespace: pebble
data:
  Corefile: |
    .:8053 {
      errors
      health
      ready
      template ANY ANY pebble.local {
        answer "{{ .Name }} 60 IN A ${NGINX_IP}"
      }
      forward . /etc/resolv.conf
      cache 2
      reload
    }
EOF

kubectl rollout restart deploy/pebble-coredns -n pebble
kubectl rollout status deploy/pebble-coredns -n pebble

验证响应是否正确:

code
kubectl run dnstest --rm -it --restart=Never --image=busybox -- \
  nslookup echo.pebble.local ${PEBBLE_DNS_IP}

响应中应显示Address: <NGINX_IP>。如果出现SERVFAIL,检查kubectl logs -n pebble deploy/pebble-coredns——类似not a TTL: "}"的解析错误表示模板块又被压缩为单行。

修补集群CoreDNS以便cert-manager的自检能解析相同名称。添加将pebble.local转发到pebble-coredns的存根区域:

code
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        errors
        health {
           lameduck 5s
        }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           fallthrough in-addr.arpa ip6.arpa
           ttl 30
        }
        forward . /etc/resolv.conf {
           max_concurrent 1000
        }
        cache 30
        loop
        reload
        loadbalance
    }
    pebble.local:53 {
        forward . ${PEBBLE_DNS_IP}
    }
EOF

kubectl rollout restart deploy/coredns -n kube-system
kubectl rollout status deploy/coredns -n kube-system

验证集群解析器现在能否响应echo.pebble.local(无需指定服务器——将使用默认的kube-dns):

code
kubectl run dnstest --rm -it --restart=Never --image=busybox -- \
  nslookup echo.pebble.local

响应中应同时出现Server: 10.96.0.10Address: <NGINX_IP>

第4步:获取Pebble CA并创建ClusterIssuer

Pebble使用存储在pebble ConfigMap的root-cert.pem中的自签名根证书签发证书。cert-manager需要信任该CA才能与Pebble的ACME目录通信,因此需将CA以base64编码的caBundle形式传递到ClusterIssuer:

code
kubectl get configmap pebble -n pebble \
  -o jsonpath='{.data.root-cert\.pem}' > pebble-ca.crt

head -1 pebble-ca.crt   # 应显示-----BEGIN CERTIFICATE-----

CA_BUNDLE=$(base64 -i pebble-ca.crt | tr -d '\n')
echo "CA_BUNDLE长度: ${#CA_BUNDLE}"   # 约1600字符,连续一行

通过heredoc创建ClusterIssuer——${CA_BUNDLE}环境变量会在kubectl读取前被替换到YAML中:

code
kubectl apply -f - <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: pebble
spec:
  acme:
    server: https://pebble.pebble.svc.cluster.local/dir
    email: test@example.com
    privateKeySecretRef:
      name: pebble-account-key
    caBundle: ${CA_BUNDLE}
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
EOF

检查签发器状态:

code
kubectl get clusterissuer pebble
code
NAME     READY   AGE
pebble   True    5s

如果READY保持False,最常见的两个原因是caBundle格式错误(确认是无换行的单行base64)或Pebble在cert-manager命名空间不可达。检查可达性:

code
kubectl run test-curl --rm -it --restart=Never \
  --image=curlimages/curl:latest \
  --namespace cert-manager -- \
  curl -k https://pebble.pebble.svc.cluster.local/dir

若返回JSON则说明Pebble可达。

第5步:部署示例应用

code
# echo-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
        - name: echo
          image: ealen/echo-server:latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: echo
  namespace: default
spec:
  selector:
    app: echo
  ports:
    - port: 80
      targetPort: 80
code
kubectl apply -f echo-app.yaml

验证资源是否正常启动:

code
kubectl get deploy,pod,svc -n default
code
NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/echo   1/1     1            1           32s

NAME                        READY   STATUS    RESTARTS   AGE
pod/echo-5665fbcfdd-mbgxj   1/1     Running   0          36s

NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/echo         ClusterIP   10.96.103.114   <none>        80/TCP    40s
service/kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   32m

第6步:创建带TLS的Ingress

cert-manager.io/cluster-issuer: pebble注解会指示cert-manager自动为该Ingress创建Certificate资源,使用我们刚创建的签发器。主机名echo.pebble.local无需外部解析——我们在第3步已配置两个DNS解析器。

echo-ingress.yaml

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: echo namespace: default annotations: cert-manager.io/cluster-issuer: pebble spec: ingressClassName: nginx tls:

  • hosts:
  • echo.pebble.local

secretName: echo-tls # cert-manager 将创建此 Secret rules:

  • host: echo.pebble.local

http: paths:

  • path: /

pathType: Prefix backend: service: name: echo port: number: 80

code
kubectl apply -f echo-ingress.yaml  

步骤7:监视证书的签发过程

code
# 监控证书资源(当 Ready=True 时按 Ctrl+C 退出)  
kubectl get certificate echo-tls -n default -w  
code
NAME       READY   SECRET     AGE  
echo-tls   False   echo-tls   5s  
echo-tls   True    echo-tls   28s  

READY 变为 True 时,证书已签发并存储在 echo-tls Secret 中。在健康的集群中,整个流程(CertificateRequest → Order → Challenge → solver pod → Secret)可在一分钟内完成:

code
kubectl get certificate,certificaterequest,order,challenge -n default  
code
NAME                                   READY   SECRET     AGE  
certificate.cert-manager.io/echo-tls   True    echo-tls   81s  

NAME                                            APPROVED   DENIED   READY   ISSUER   AGE  
certificaterequest.cert-manager.io/echo-tls-1   True                True    pebble   81s  

NAME                                               STATE   AGE  
order.acme.cert-manager.io/echo-tls-1-1824732543   valid   81s  

(Challenge 资源在 Order 完成后会被自动删除,因此此时 kubectl get challenge -n default 通常显示为空——这是成功而非失败的标志。)

如果 READY 超过一分钟仍为 False,请参考本节末尾的故障排查提示。

检查签发的证书以确认 Pebble 已签名:

code
kubectl get secret echo-tls -n default -o jsonpath='{.data.tls\.crt}' | \  
  base64 -d | openssl x509 -noout -issuer -subject -dates  
code
issuer=CN=Pebble Intermediate CA 05478c  
subject=  
notBefore=May 17 19:09:22 2026 GMT  
notAfter=Aug 15 19:09:21 2026 GMT  

颁发者是 Pebble 的中间 CA,证明 ACME 流程端到端正常工作。证书有效期为 90 天,cert-manager 会在第 60 天自动续期。

从集群内部通过 HTTPS 访问 Ingress 确认所有组件已正确连接:

code
kubectl run curltest --rm -it --restart=Never --image=curlimages/curl -- \  
  curl -sk https://echo.pebble.local/  

回显服务器应返回一个 JSON 数据块——注意其中的 "x-forwarded-proto":"https" 字段,这证明请求是通过 TLS 经过 nginx 转发的。

如果证书始终无法就绪的故障排查:

  • kubectl describe order -n default —— 检查事件中的 "DNS problem" 或 "Connection refused" 错误。
  • kubectl logs -n pebble deploy/pebble --tail=50 —— Pebble 日志会记录验证期间尝试访问的具体 URL 及错误信息。
  • 如果 Order 卡在 pending 状态且无事件:cert-manager 尚未完成协调。等待 30 秒。
  • 如果 Order 状态为 invalid:步骤3中的 DNS 层配置有误。重新运行两次 nslookup 检查。
  • 如果 Ingress 应用本身因 x509 webhook 错误失败:您跳过了步骤1中的 kubectl delete validatingwebhookconfiguration ingress-nginx-admission 步骤。

步骤8:切换到 Let's Encrypt 测试环境(真实公网域名)

Pebble 验证了本地流程的可行性。现在切换到指向公网集群的真实可访问域名。此时无需步骤3的 DNS 操作——真实域名会被所有 DNS 解析器正常识别。

首先使用 Let's Encrypt 测试环境。它与生产环境使用相同的 ACME 协议,但速率限制更宽松,避免测试失败导致账号被封:

code
# clusterissuer-staging.yaml  
apiVersion: cert-manager.io/v1  
kind: ClusterIssuer  
metadata:  
  name: letsencrypt-staging  
spec:  
  acme:  
    server: https://acme-staging-v02.api.letsencrypt.org/directory  
    email: your-email@example.com  
    privateKeySecretRef:  
      name: letsencrypt-staging-account-key  
    solvers:  
      - http01:  
          ingress:  
            ingressClassName: nginx  
code
kubectl apply -f clusterissuer-staging.yaml  

# 将 Ingress 指向测试环境和真实域名,强制重新签发证书  
kubectl annotate ingress echo \  
  cert-manager.io/cluster-issuer=letsencrypt-staging --overwrite -n default  
kubectl delete secret echo-tls -n default  

新证书的颁发者会显示类似 (STAGING) Let's Encrypt 的标识。

步骤9:切换到 Let's Encrypt 生产环境

测试环境验证成功后,重复上述流程使用生产环境。唯一区别是服务器地址:

code
# clusterissuer-prod.yaml  
apiVersion: cert-manager.io/v1  
kind: ClusterIssuer  
metadata:  
  name: letsencrypt-prod  
spec:  
  acme:  
    server: https://acme-v02.api.letsencrypt.org/directory  
    email: your-email@example.com  
    privateKeySecretRef:  
      name: letsencrypt-prod-account-key  
    solvers:  
      - http01:  
          ingress:  
            ingressClassName: nginx  
code
kubectl apply -f clusterissuer-prod.yaml  
kubectl annotate ingress echo \  
  cert-manager.io/cluster-issuer=letsencrypt-prod --overwrite -n default  
kubectl delete secret echo-tls -n default  

cert-manager 会检测到缺失的 Secret 并立即向生产环境的 Let's Encrypt 请求受浏览器信任的证书。

cert-manager 会检测到缺失的 Secret 并立即触发使用生产颁发者的新证书请求。

如何通过 DNS-01 挑战获取通配符证书

DNS-01 挑战适用于集群无法公开访问(如内部集群、离线环境、通过 VPN 访问的 staging 命名空间)或需要通配符证书(覆盖所有子域名)的场景。

DNS-01 要求 cert-manager 能够在 DNS 提供商处创建和删除 TXT 记录。cert-manager 内置支持 Route53、Cloud DNS、Cloudflare、Azure DNS 等服务商。

以下是使用 AWS Route53 的 DNS-01 ClusterIssuer 示例:

code
# clusterissuer-dns01.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-dns01
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your-email@example.com
    privateKeySecretRef:
      name: letsencrypt-dns01-account-key
    solvers:
      - dns01:
          route53:
            region: us-east-1
            # 生产环境建议使用 IRSA(IAM Roles for Service Accounts)
            # 而非静态凭证
            hostedZoneID: YOUR_HOSTED_ZONE_ID

使用该颁发机构的通配符 Certificate 配置:

code
# wildcard-cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example-com
  namespace: default
spec:
  secretName: wildcard-example-com-tls
  issuerRef:
    name: letsencrypt-dns01
    kind: ClusterIssuer
  commonName: "*.example.com"
  dnsNames:
    - "*.example.com"
    - "example.com"        # 同时覆盖根域名
  duration: 2160h           # 90天
  renewBefore: 720h         # 提前30天续签

生成的 Secret wildcard-example-com-tls 可被 default 命名空间中的任意 Ingress 引用。所有子域名(如 api.example.com、dashboard.example.com、staging.example.com)都将通过自动轮换的同一证书受保护。

若使用 Cloudflare 而非 Route53,solver 配置如下:

code
solvers:
      - dns01:
          cloudflare:
            email: your-email@example.com
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: api-token

示例 2 — 配置内部 CA 实现服务间 TLS

Let's Encrypt 证书适合公开服务,但内部服务(如 gRPC 微服务调用、Web 应用访问数据库)无需公共信任。我们需要集群信任的 CA,并为不存在公共 DNS 记录的服务名称签发证书。

cert-manager 的 CA 颁发机构功能可满足此需求。创建根 CA 后,通过该 CA 签发内部服务证书。所有信任根 CA 的服务都将信任其签发的所有证书。

步骤1:创建自签名 ClusterIssuer

自签名颁发机构生成的证书由证书本身签名,作为创建根 CA 的初始步骤:

code
# selfsigned-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: selfsigned
spec:
  selfSigned: {}
code
kubectl apply -f selfsigned-issuer.yaml

步骤2:创建根 CA 证书

使用自签名颁发机构生成 CA 证书,isCA: true 标识其可签发其他证书:

code
# internal-ca.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-ca
  namespace: cert-manager    # 存储在 cert-manager 命名空间
spec:
  isCA: true
  commonName: internal-ca
  secretName: internal-ca-secret
  duration: 87600h           # 根 CA 有效期10年
  renewBefore: 720h
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer
code
kubectl apply -f internal-ca.yaml
kubectl get certificate internal-ca -n cert-manager
code
NAME          READY   SECRET               AGE
internal-ca   True    internal-ca-secret   8s

步骤3:创建基于根 CA 的 ClusterIssuer

创建使用刚生成的根 CA Secret 的 ClusterIssuer,用于签发内部服务证书:

code
# internal-ca-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-ca-secret   # 引用 cert-manager 命名空间中的 Secret
code
kubectl apply -f internal-ca-issuer.yaml
kubectl get clusterissuer internal-ca
code
NAME          READY   AGE
internal-ca   True    5s

步骤4:为内部服务签发证书

为 gRPC 服务签发证书,使用 Kubernetes 内部 DNS 名称(如 <service>.<namespace>.svc.cluster.local):

code
# payments-cert.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: payments-tls
  namespace: production
spec:
  secretName: payments-tls-secret
  issuerRef:
    name: internal-ca
    kind: ClusterIssuer
  commonName: payments.production.svc.cluster.local
  dnsNames:
    - payments.production.svc.cluster.local
    - payments.production.svc
    - payments
  duration: 2160h     # 90天
  renewBefore: 360h   # 提前15天续签
code
kubectl create namespace production
kubectl apply -f payments-cert.yaml
kubectl get certificate payments-tls -n production
code
NAME           READY   SECRET                AGE
payments-tls   True    payments-tls-secret   6s

Secret payments-tls-secret 包含 tls.crttls.keyca.crt。将其挂载到应用 Pod:

code
# 在 Deployment 配置中
volumes:
  - name: tls
    secret:
      secretName: payments-tls-secret
containers:
  - name: payments
    volumeMounts:
      - name: tls
        mountPath: /etc/tls
        readOnly: true

您的应用程序通过读取 /etc/tls/tls.crt/etc/tls/tls.key 配置 TLS。需要信任该应用的其他服务则读取 /etc/tls/ca.crt

第 6 步:通过 trust-manager 分发 CA 证书包

使用自定义 CA 的问题是每个服务都需要知道它的存在。cert-manager 的配套工具 trust-manager 通过将 CA 证书包以 ConfigMap 形式分发到每个命名空间来解决这一问题:

bash
helm upgrade trust-manager oci://quay.io/jetstack/charts/trust-manager \
  --install \
  --namespace cert-manager \
  --wait

创建一个 Bundle 资源,从 internal-ca-secret 中获取 CA 证书并集群范围内分发:

yaml
# ca-bundle.yaml
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
  name: internal-ca-bundle
spec:
  sources:
    - secret:
        name: internal-ca-secret
        key: ca.crt
  target:
    configMap:
      key: ca-bundle.crt
    namespaceSelector:
      matchLabels:
        # 分发到带有此标签的所有命名空间
        kubernetes.io/metadata.name: production
bash
kubectl apply -f ca-bundle.yaml

几秒钟后,所有匹配的命名空间都会有一个名为 internal-ca-bundle 的 ConfigMap,其中包含 CA 证书。应用程序可通过挂载此 ConfigMap 自动信任内部签发的证书,无需逐个服务配置。

第 7 步:验证证书链

bash
# 提取 CA 证书和服务证书
kubectl get secret payments-tls-secret -n production \
  -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt

kubectl get secret payments-tls-secret -n production \
  -o jsonpath='{.data.tls\.crt}' | base64 -d > payments.crt

# 验证证书是否由 CA 签名
openssl verify -CAfile ca.crt payments.crt
bash
payments.crt: OK

证书轮换机制

证书轮换是证书管理中最容易导致生产集群故障的部分。cert-manager 虽然能自动处理,但理解其机制有助于调优和排查问题。

cert-manager 监控所有管理的 Certificate 资源,并检查 Secret 中证书的过期时间。当剩余有效期低于 renewBefore 阈值时,cert-manager 触发续签。默认 renewBefore 是证书总有效期的 1/3 —— 例如 90 天证书会在第 60 天开始续签。

续签会创建新的 CertificateRequest,完成完整的签发流程,并原地更新 Secret。新证书会原子性替换旧证书。使用文件挂载并监听变更的现代 Web 服务器和 gRPC 框架无需重启即可获取新证书。

bash
# 查看当前轮换状态
kubectl describe certificate echo-tls -n default

在输出中查找以下字段:

code
Status:
  Not After:   2024-06-18T10:00:00Z
  Not Before:  2024-03-20T10:00:00Z
  Renewal Time: 2024-05-18T10:00:00Z   # cert-manager 开始续签的时间
  Conditions:
    Type:    Ready
    Status:  True
    Message: Certificate is up to date and has not expired

如果续签失败(例如 HTTP-01 挑战无法完成),cert-manager 会以指数退避策略重试。现有证书会持续生效直到实际过期,为问题调试留出时间。

实时查看续签事件:

bash
kubectl get events -n default --field-selector reason=Issued
kubectl get events -n default --field-selector reason=Failed

正确设置 `renewBefore`: 对于面向公网的服务,90 天证书设置 30 天缓冲期是合理的。对于短生命周期的内部证书(24 小时有效期),应设置 8 小时的 renewBefore,确保即使首次尝试失败也能在过期前完成轮换。切勿将 renewBefore 设为超过证书有效期的一半 —— cert-manager 会立即尝试轮换刚签发的证书。

清理

bash
# 删除演示资源
kubectl delete ingress echo -n default
kubectl delete service echo -n default
kubectl delete deployment echo -n default
kubectl delete secret echo-tls -n default
kubectl delete certificate payments-tls -n production
kubectl delete namespace production

# 卸载 cert-manager 和 trust-manager
helm uninstall trust-manager -n cert-manager
helm uninstall cert-manager -n cert-manager
kubectl delete namespace cert-manager

# 删除 ClusterIssuers
kubectl delete clusterissuer letsencrypt-staging letsencrypt-prod \
  internal-ca selfsigned 2>/dev/null

总结

Kubernetes 将 TLS 配置完全交由用户自行处理。本文档演示了公有端和私有端的完整责任范畴。

在公有端,您通过当前的 OCI Helm 图表安装了 cert-manager,创建了基于 Let's Encrypt 的 ClusterIssuer,并观察了 cert-manager 完成 ACME HTTP-01 挑战的完整流程 —— 从创建临时验证 Pod 到将有效证书存储到 Kubernetes Secret。您还看到如何通过一行注解切换测试与生产环境,以及 cert-manager 如何在证书过期前自动续签。

在私有端,您通过自签名签发器创建了私有 CA,基于该 CA 创建了 ClusterIssuer,并为仅存在于集群内部的服务名称签发证书。通过 trust-manager 集群范围内分发 CA 证书包,使服务无需逐个配置即可相互信任。您还通过 openssl 验证了证书链,确保生产部署前的正确性。

理解证书轮换机制是区分能自信管理 TLS 团队和因证书过期半夜被叫醒团队的关键。cert-manager 自动化了续签流程,但 renewBefore 参数是您的安全余量 —— 正确设置它并学会解读续签状态至关重要。

所有 YAML 清单和 Helm 参数均可在 DevOps-Cloud-Projects GitHub 仓库 中找到。

免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人找到开发岗位。立即开始

AI 可能会生成不准确的信息,请核实重要内容

如何使用cert-manager、Let's Encrypt和内部TLS加密Kubernetes流量 | freeCodeCamp.org | traeai