大多数工程师都认为他们的Kubernetes集群会对其所有网络流量进行加密。但实际上并非如此。使用kubectl执行的命令确实是加密的——你的客户端与API服务器之间会通过TLS协议进行通信。而API服务器与etcd之间的通信,通常也会根据集群的配置方式进行加密。

但是,容器Pod之间的数据传输在默认情况下是明文传输的。从互联网进入你的服务的流量?只有在你明确配置了TLS加密机制后,这些流量才会被加密。至于内部服务所需的证书呢?你必须自行负责准备这些证书。

这并不是Kubernetes的设计缺陷,而是一种有意为之的选择——Kubernetes提供了相关的基础功能,但具体的实现方式则由用户来决定。问题在于,证书的管理工作极其繁琐:证书会过期,手动管理证书显然无法满足大规模部署的需求,而如果忘记及时更新证书,就可能会导致系统故障。

cert-manager正是为了解决这些问题而存在的。它作为集群内的一个控制器,会监控Certificate资源,向预先配置的证书颁发机构请求证书,将这些证书存储在Kubernetes的Secrets中,并在证书过期之前自动进行更新。你只需要指定自己的需求,cert-manager就会帮你完成这些工作,并确保相关设置始终处于有效状态。

在本文中,你将了解cert-manager的核心工作原理,学习如何使用Let’s Encrypt实现公共入口端点的TLS加密,掌握如何为内部服务配置证书颁发机构以支持加密通信,同时也会理解证书更新机制,从而确保因证书过期而导致的系统故障不再发生。

先决条件

  • 一个安装了nginx Ingress控制器的Kubernetes集群

  • 已安装Helm 3

  • 拥有你可以控制的DNS域名——这对于进行Let’s Encrypt演示是必需的

  • 对TLS有基本的了解:你知道证书、私钥以及证书颁发机构是什么

所有的演示文件都位于DevOps-Cloud-Projects GitHub仓库中。

目录

在Kubernetes中,哪些内容会被加密,哪些不会被加密?

在安装任何东西之前,明确了解集群已经保护了哪些数据,以及哪些数据仍然处于未加密状态,这是非常有必要的。

数据传输路径 是否默认被加密? 备注
kubectl → API服务器 使用集群的CA证书进行TLS加密
API服务器 → etcd 通常会 这取决于集群的配置方式——请根据您的实际设置进行确认
API服务器 → kubelet 使用TLS加密,但kubelet对证书的验证方式取决于具体配置
Pod → 同一集群内的其他Pod 不会 除非使用了服务网格或mtls机制,否则数据将以明文形式传输
互联网 → Ingress组件 不会 需要手动配置TLS加密——具体取决于Ingress资源的设置
Pod → Kubernetes API 通过服务账户令牌以及集群的CA证书进行加密

在实际应用中,最值得关注的两个方面是Pod之间的数据传输以及Ingress组件的TLS加密。本文将详细介绍如何使用Let’s Encrypt为Ingress组件配置TLS加密,以及如何使用私有CA证书实现内部服务之间的加密。

cert-manager的工作原理

cert-manager是一种Kubernetes操作器。它通过自定义资源扩展了Kubernetes API,这些自定义资源用于表示证书申请及其相关配置信息。当您创建一个Certificate资源时,cert-manager的控制器会自动捕获该请求,然后向指定的发证机构申请证书,并将获得的证书及私钥存储在Kubernetes的Secret资源中。当证书即将过期时,cert-manager会自动为其续期。

这种设计意味着您的应用程序无需关心证书的管理工作——它只需要读取由cert-manager维护的Secret资源即可;而cert-manager会负责确保这些Secret资源的有效性始终处于最新状态。

四种核心资源

cert-manager引入了四种您会经常使用的自定义资源:

资源名称 其所代表的含义
Issuer 证书颁发机构或ACME账户——具有命名空间范围的作用
ClusterIssuer 与Issuer相同,但可在整个集群范围内使用
Certificate 一份证书申请请求——用于描述您所需要的证书信息
CertificateRequest 一个单独的签名请求对象——通常由cert-manager自动生成,用户很少会直接操作它

在实际使用中,您主要会接触到ClusterIssuerCertificate这两种资源。ClusterIssuer》决定了证书的来源;而Certificate则明确了您所需要的证书的具体信息以及存储位置。

证书颁发机构与ClusterIssuer

发行机构仅能在其自己的命名空间内颁发证书,而集群发行机构则可以在任何命名空间中颁发证书。对于像Let’s Encrypt这样的共享基础设施来说,几乎总是需要使用集群发行机构;而对于特定于应用程序的内部CA而言,将发行机构限定在该应用程序的命名空间内才是更安全的选择。

cert-manager支持多种类型的发行机构。其中最常见的三种类型如下:

ACME——用于Let’s Encrypt或任何兼容ACME标准的CA颁发的公共证书。域名的所有权通过HTTP-01或DNS-01挑战机制来验证。

CA——用于由私钥存储在Kubernetes Secret中的CA签发的内部证书,这类证书用于集群内的服务间TLS通信。

自签名证书——用于生成自签名证书。单独使用这种类型的证书时用途较为有限,但在创建内部CA时,它作为初始配置步骤是必不可少的。

证书的生命周期

当您创建一个证书资源时,cert-manager会按照以下流程进行处理:

  1. 生成包含CSR(证书签名请求)的证书请求

  2. 将CSR传递给配置好的发行机构

  3. 对于使用ACME机制的发行机构:会创建一个挑战请求资源并完成相应的验证流程(具体细节详见下文)

  4. 从发行机构处接收已签名的证书

  5. 将证书及私钥存储在spec.secretName中指定的Kubernetes Secret中

  6. 监控证书的有效期——默认情况下,当证书有效期剩余2/3时,系统会自动更新证书

您的应用程序会加载这个Secret文件,而cert-manager会自动对其进行更新。大多数能够检测文件变化的应用程序都可以在不重启的情况下获取到新的证书。

ACME挑战机制:HTTP-01与DNS-01的对比

Let’s Encrypt在颁发证书之前,需要确认您确实控制着该域名。为此,ACME定义了两种验证方式。

HTTP-01的验证过程是:cert-manager会在http://.well-known/acme-challenge/这个地址创建一个临时HTTP接口,Let’s Encrypt会向该地址发送请求。如果返回的响应与预期的令牌匹配,那么验证就通过了。这种方式要求您的集群能够通过互联网在80端口上被访问。

DNS-01的验证方式是:cert-manager会在_acme-challenge.这个域名下创建一个临时DNS TXT记录,Let’s Encrypt会检查是否存在这个记录。这种方式不需要互联网接入,因此非常适合私有集群使用;同时,这也是获取通配符证书(如*.example.com)的唯一方法。

两者之间的区别在于:HTTP-01的设置较为简单,但仅适用于单个域名,并且需要具备可被互联网访问的基础设施;而DNS-01虽然需要通过API与DNS提供商进行交互,但它既适用于内部集群,也适用于通配符域名的验证。

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

Pebble 是 Let's Encrypt 的本地 ACME 测试服务器。它运行在您的集群内部,使用与 Let's Encrypt 相同的 ACME 协议来颁发证书,并且不需要公共域名或互联网连接。通过使用 Pebble,您可以在普通的集群环境中测试整个 cert-manager 的工作流程——包括挑战请求、证书颁发以及续期操作。

一旦您在本地了解了这一工作流程,那么将其切换到真实的 Let's Encrypt 环境其实只需要进行一行配置更改:将 `ClusterIssuer` 服务器的地址替换为可公开访问的集群地址,并设置相应的 DNS 记录即可。其余的配置内容都是相同的。

您将会安装 cert-manager,为 Let's Encrypt 创建一个 `ClusterIssuer` 对象,部署一个包含 Ingress 的示例应用,然后会看到证书被自动颁发并存储下来的过程。

步骤 1:安装 cert-manager

目前,cert-manager 是通过 OCI Helm 图表从 quay.io/jetstack 进行分发的。使用参数 `--set crds.enabled=true` 可以在安装图表的同时也安装相应的自定义资源定义:

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` 这一配置项添加进去,是为了确保在特定的集群环境中正常运行:默认情况下,LoadBalancer 类型的服务是无法获得外部 IP 地址的(因为没有云负载均衡器),如果不移除这个配置项,`--wait` 命令会永远处于等待状态。在真实的集群环境中,可以直接使用默认的 LoadBalancer 类型。

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

确认这四个组件都已经成功运行:

kubectl get pods -n cert-manager
kubectl get pods -n ingress-nginx
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

需要注意的一点是:在特定的集群环境中,需要删除 nginx 的 admission webhook。因为在这样的环境中,nginx admission webhook 使用的是自签名证书,而 Kubernetes API 服务器无法验证这种证书。所以,在第一次尝试创建任何 Ingress 资源时,系统会报错 “failed calling webhook ‘validate.nginx.ingress.kubernetes.io’: …… x509: certificate signed by unknown authority”。为避免后续操作出现问题,请提前删除这个 webhook。

kubectl delete validatingwebhookconfiguration ingress-nginx-admission

步骤 2:安装 Pebble

Pebble 是由 JupyterHub 项目提供的本地 ACME 测试服务器。它附带了一个 CoreDNS 部署组件(pebble-coredns),Pebble 在进行 ACME 验证时会使用这个组件来解析域名。

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

确认两个 Pod 是否都在运行中:

kubectl get pods -n pebble
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 系统中都不存在——因此,在证书发放之前,我们必须让 两个 独立的解析器知道这个域名的存在:

>

解析器 被谁使用 我们需要它完成的任务
pebble-coredns(位于 pebble 命名空间中) Pebble 在发送 HTTP-01 验证请求时会使用它 echo.pebble.local 解析为 ingress-nginx 的 ClusterIP 地址
Cluster CoreDNS(位于 kube-system 命名空间中) cert-manager 在报告挑战请求准备就绪之前,会使用它进行 HTTP-01 自我检查 将对 pebble.local 的查询请求转发给 pebble-coredns

如果省略了其中任何一个环节,订单状态将会变为 invalid,因为 DNS 查询会失败。

首先获取你需要的两个 IP 地址:

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 的配置文件,使其能够使用 ingress 控制器的 IP 地址来响应 *.pebble.local 这个域名。由于 CoreDNS 的 template 插件在将整个配置块合并到一行时会出现解析错误,因此必须使用多行格式的 ConfigMap 来进行配置:

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

请验证其是否能正确响应:

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

在响应结果中,你应该能看到Address: 这一行。如果看到SERVFAIL这样的提示,请查看kubectl logs -n pebble deploy/pebble-coredns;如果出现类似not a TTL: "}"这样的错误,说明模板代码又重新被压缩成了一行。

请修复集群中的CoreDNS配置,以便cert-manager能够正确解析相同的域名。需要添加一个转发规则,将pebble.local指向pebble-coredns

cat <

现在请验证集群的解析器是否能够正确响应echo.pebble.local这个请求(系统会自动使用默认的kube-dns服务器):

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

此时应该同时出现Server: 10.96.0.10Address: 这两行结果。

步骤4:获取Pebble的CA证书并创建ClusterIssuer

Pebble使用自己签发的根证书来签署其所有的证书,这些证书保存在pebble配置映射文件中的root-cert.pem文件里。cert-manager需要信任这个CA证书才能与Pebble的ACME服务器进行通信,因此我们需要在创建ClusterIssuer时将这个证书以base64编码的形式传递进去:

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对象,在kubectl读取YAML文件之前,${CA_BUNDLE}这个shell变量会被替换到相应的位置中:

kubectl apply -f - <<

检查发行者是否已准备就绪:

kubectl get cluster issuer pebble
名称     是否准备就绪   创建时间
pebble   是        5秒前

如果“是否准备就绪”的状态仍为“否”,那么最常见的原因可能是caBundle文件格式不正确(请确认它是一条完整的Base64编码字符串,且没有换行符),或者从cert-manager命名空间无法访问Pebble。要检查是否可以访问Pebble,请执行以下命令:

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:部署一个示例应用程序

# 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
kubectl apply -f echo-app.yaml

检查这些资源是否已经创建成功:

kubectl get deploy,pod,svc -n default
名称                   是否准备就绪   创建时间   是否可用   创建时间
deployment.apps/echo   1/1     1            1           32秒前

名称                        是否准备就绪   运行状态    重启次数   创建时间
pod/echo-5665fbcfdd-mbgxj   1/1     正在运行   0          36秒前

名称                 类型        集群内部IP      外部IP         端口        创建时间
service/echo         ClusterIP   10.96.103.114           80/TCP    40秒前
service/kubernetes   ClusterIP   10.96.0.1               443/TCP   32分钟前

步骤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
kubectl apply -f echo-ingress.yaml

步骤7:观察证书的颁发过程

# 查看证书资源状态(当“READY”为True时,使用Ctrl-C暂停操作)
kubectl get certificate echo-tls -n default -w
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这一整个流程,完成时间通常不到一分钟:

kubectl get certificate,certificaterequest,order,challenge -n default
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

一旦Order流程完成,相关的Challenge会自动被删除,因此此时执行kubectl get challenge -n default通常会显示没有结果——这说明操作成功了,并非出现了故障。

如果“READY”状态超过一分钟仍然为“False”,请参考本节末尾提供的故障排除建议。

检查所颁发的证书,以确认它确实是由Pebble签发的:

kubectl get secret echo-tls -n default -o jsonpath '{.data.tls\.crt}' | \
  base64 -d | openssl x509 -noout -issuer -subject -dates
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访问echo服务器,以确认所有配置都已经正确连接起来:

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

echo服务器应该会返回一个JSON数据,注意其中"x-forwarded-proto":"https"这一字段,它证明了请求是通过TLS协议、经由nginx传递过来的。

如果证书状态始终无法变为“READY”,可以尝试以下故障排除方法:

  • kubectl describe order -n default——检查事件日志中是否出现了“DNS问题”或“连接失败”之类的信息。

  • kubectl logs -n pebble deploy/pebble --tail=50——Pebble会在验证过程中记录它尝试访问的具体URL以及出现的任何错误。

  • 如果Order状态一直处于“pending”状态且没有任何事件记录,说明cert-manager尚未完成相关处理,请等待30秒后再试。

  • 如果Order被标记为invalid,很可能是因为第3步中的DNS配置有误。请重新执行两次nslookup检查。

  • 如果应用Ingress配置时出现了与x509相关的错误,可能是因为你忽略了第一步中执行的kubectl delete validatingwebhookconfiguration ingress-nginx-admission这一操作。

步骤8:切换到Let’s Encrypt的测试环境配置(适用于真实公共域名)

Pebble已经证明了这种配置在本地环境中是可行的。现在,你需要将域名指向一个可公开访问的集群。步骤3中进行的DNS相关设置不再需要了——因为这个域名是真实存在的,所以任何解析器都能自动找到它。

首先使用Let’s Encrypt的测试环境配置。它使用的ACME协议与生产环境中的相同,但会设置较为宽松的请求速率限制,因此测试过程中出现的失败尝试不会导致你无法继续使用该服务:

# 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
kubectl apply -f cluster issuer-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的生产环境配置

当测试环境配置成功后,再使用生产环境的ClusterIssuer配置进行同样的操作。唯一的区别在于server地址的不同:

# 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
kubectl apply -f cluster issuer-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获取通配符证书

对于那些具有公共访问入口的单一域名来说,HTTP-01验证机制效果很好。但在以下两种情况下,你需要使用DNS-01:首先,当你的集群无法被公众访问时(例如内部集群、处于隔离环境中的集群,或者通过VPN连接的测试环境);其次,当你需要一份能够覆盖该域名下所有子域名的通配符证书时。

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

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

# 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角色)而非静态凭证
            # hostedZoneID: 请替换为你的主机区域ID

使用上述发证机构生成的通配符证书配置如下:

# 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天自动续订

名称为wildcard-example-com-tls的秘密文件可以被default命名空间中的任何Ingress服务引用。所有子域名——如api.example.comdashboard.example.comstaging.example.com——都由这张会自动更新的证书覆盖。

如果使用Cloudflare而非Route53,solvers部分的配置如下:

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

演示2——为服务间TLS通信设置内部CA

Let’s Encrypt证书非常适合用于公开服务的认证需求,但对于内部服务来说——比如一个gRPC微服务调用另一个微服务,或者一个Web应用与它的数据库进行交互——并不需要公众信任。此时你需要的是一个被集群所信任的CA机构,并且这个机构能够为那些在公共DNS系统中并不存在的服务名称颁发证书。

cert-manager提供的CA发证机制可以解决这个问题。你只需创建一个根CA证书,然后告知cert-manager这一信息,之后就可以使用该根CA为内部服务颁发证书了。任何信任这个根CA的服务,都会信任它所颁发的所有证书。

步骤1:创建自签名的ClusterIssuer

自签名的发证机构生成的证书是由该机构自身签署的——也就是说,它本身就是自己的CA。你可以利用这一机制作为第一步,来生成根CA证书。

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

步骤 2:创建根CA证书

使用自签发的发证机构来创建一个CA证书。isCA: true这一字段告诉cert-manager,该证书可以用于为其他证书签名:

# 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           # 10年——这是一个根CA证书
  renewBefore: 720h
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer
kubectl apply -f internal-ca.yaml
kubectl get certificate internal-ca -n cert-manager
NAME          READY   SECRET               AGE
internal-ca   True    internal-ca-secret   8s

步骤 3:创建一个由根CA证书支持的CA ClusterIssuer

现在创建一个使用刚才创建的根CA密钥的ClusterIssuer。这个发证机构将负责为你的内部服务生成证书:

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

步骤 4:为内部服务生成证书

现在为某个内部的gRPC服务生成一份证书。dnsNames字段使用了Kubernetes的内部DNS命名格式——..svc.cluster.local

# 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天进行续期
kubectl create namespace production
kubectl apply -f payments-cert.yaml
kubectl get certificate payments-tls -n production
名称           是否已准备就绪  密钥              使用时长
payments-tls   是    payments-tls-secret   6秒

密钥 payments-tls-secret 现在包含了文件 tls.crttls.keyca.crt。请将这个密钥配置到您的应用程序容器中:

# 在您的 Deployment 配置文件中添加以下内容:
volumes:
  - 名称: tls
    密钥:
      名称: payments-tls-secret
containers:
  - 名称: payments
    卷挂配置:
      - 名称: tls
        挂载路径: /etc/tls
        只读属性: 是

您的应用程序会读取 /etc/tls/tls.crt/etc/tls/tls.key 文件来配置 TLS 连接;其他需要信任这些证书的服务则会读取 /etc/tls/ca.crt 文件。

步骤 5:使用 trust-manager 分发 CA 证书包

使用自定义 CA 证书的一个问题是,所有服务都需要知道这个 CA 证书的存在。cert-manager 的辅助工具 trust-manager 通过将 CA 证书包作为 ConfigMap 分发到所有命名空间中,从而解决了这个问题:

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

创建一个 Bundle 资源,该资源会从 internal-ca-secret 中获取 CA 证书,并将其分发到整个集群中:

# ca-bundle.yaml 文件内容:
apiVersion: trust.cert-manager.io/v1alpha1
kind: Bundle
metadata:
  名称: internal-ca-bundle
spec:
  来源:
    - 密钥:
        名称: internal-ca-secret
        内容: ca.crt
  目标:
    configMap:
      名称: ca-bundle.crt
    命名空间筛选条件:
      匹配标签:
        # 将证书分发到所有带有此标签的命名空间中
        kubernetes.io/metadata.name: production
kubectl apply -f ca-bundle.yaml

几秒钟后,所有符合条件的命名空间都会拥有一个名为 internal-ca-bundle 的 ConfigMap,其中包含了 CA 证书。应用程序只需挂载这个 ConfigMap,就可以信任由集群内部生成的证书了,而无需为每个服务单独进行配置。

步骤 6:验证证书链的有效性

# 提取 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
验证结果:payments.crt: OK

证书轮换的原理

证书轮换是证书管理中最为常见的问题之一。cert-manager 会自动处理这一过程,但了解其工作原理有助于您对其进行优化或在出现问题时进行调试。

cert-manager会监控它所管理的所有Certificate资源,并检查存储在Secret中的证书是否即将过期。当证书的剩余有效期限低于renewBefore设定的阈值时,cert-manager会自动触发更新流程。默认情况下,renewBefore的值是证书总有效期的1/3——因此,有效期为90天的证书会在第60天开始进入更新阶段。

更新过程会生成一个新的CertificateRequest,然后完成整个颁发流程,并最终更新Secret中的证书信息。新的证书会原子性地替换原有的证书。那些通过文件挂载来监控证书变化的应用程序(大多数现代Web服务器和gRPC框架都属于这类应用)能够在不重启的情况下获取到新证书。

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

在输出结果中寻找以下字段:

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:  证书是有效的,且尚未过期

如果更新过程失败——例如因为无法完成HTTP-01验证请求——cert-manager会采用指数退避策略重新尝试。现有的证书会继续正常使用,直到其真正过期为止,这样你就有足够的时间来排查问题。

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

正确设置renewBefore的值非常重要:对于面向公众的服务来说,将renewBefore设置为证书有效期限的30天之前是一个合理的缓冲时间;而对于那些有效期仅为24小时的内部证书而言,应将renewBefore设置为8小时,这样即使第一次尝试失败,证书也能在到期前被及时更新。需要注意的是,千万不要将renewBefore设置得超过证书有效期限的一半——否则cert-manager会立即尝试更新刚刚颁发的证书。

清理操作

# 删除示例资源
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配置的完全权限交给了用户。在本文中,我们详细了解了与TLS配置相关的各项任务,包括那些面向公众的服务以及内部系统中的配置管理流程。

在公共环境中,您使用了当前的OCI Helm图表来安装cert-manager,创建了一个由Let’s Encrypt支持的ClusterIssuer,并观察了cert-manager如何完成整个ACME HTTP-01认证流程——从生成临时解决方案Pod到将有效的证书存储在Kubernetes的Secret中。您也了解到,将环境从测试环境切换到生产环境只需要修改一行配置注释,而且cert-manager会自动在证书过期之前为其进行续期。

在内部环境中,您利用cert-manager自签发的证书创建了一个私有的CA,以此为基础生成了ClusterIssuer,并为那些仅存在于集群内部的内部服务名称颁发了证书。您还使用了trust-manager在整个集群范围内分发这些CA证书包,这样各服务就可以无需进行单独配置就能互相信任彼此的证书。此外,您也学会了如何使用openssl工具来验证证书链,从而确保在将系统部署到生产环境之前一切正常。

了解证书更新机制是区分那些能够熟练管理TLS协议的团队与那些因为证书过期而在凌晨3点被惊醒的团队的关键。cert-manager会自动完成证书续期工作,但renewBefore字段为您提供了额外的安全保障——请确保正确设置这一参数,并掌握查看证书续期状态的方法。

本文中提到的所有YAML配置文件及Helm参数值,都可以在DevOps-Cloud-Projects的GitHub仓库中找到

Comments are closed.