# Kubernetes cert-manager 配置 Let's Encrypt 证书

> 用 Helm 安装 cert-manager，创建 Let's Encrypt 测试和生产 ClusterIssuer，为 Ingress 或 Gateway 签发证书，并排查 Order/Challenge 问题。

Source: https://ssl.sh/zh/guides/cert-manager-kubernetes/
Last updated: 2026-11-10

在 Kubernetes 里用 cert-manager 签发 Let's Encrypt 证书分三步：用 Helm 安装 cert-manager，创建 ClusterIssuer，再给 Ingress（或 Gateway）加注解。证书保存在 Secret 里，cert-manager 会自动续期。

适用于 cert-manager 1.18 及以上（截至 2026 年 11 月为 1.21.x）、任意受支持的 Kubernetes 版本和 Helm 3。

## 前提条件

HTTP-01 验证需要公网能访问到你的 Ingress 控制器或网关。

- Kubernetes 集群和有 cluster-admin 权限的 `kubectl`，本地装好 Helm 3。
- 有公网 IP 的 Ingress 控制器或 Gateway API 实现，例如 `203.0.113.10`。还在用社区版 ingress-nginx 的，需要规划迁移，它已于 2026 年 3 月退役。
- `example.com` 的 A/AAAA 记录指向这个公网 IP；集群在中国大陆的，域名需已备案。
- 公网能通过 80 端口访问控制器（HTTP-01 需要）。80 端口不通时用第四步的 DNS-01。

## 第一步：用 Helm 安装 cert-manager

cert-manager 用官方 Helm chart 安装，`crds.enabled=true` 会同时安装 CRD：

```bash
helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \
  --version v1.21.3 \
  --namespace cert-manager --create-namespace \
  --set crds.enabled=true
kubectl get pods -n cert-manager
```

`v1.21.3` 换成 cert-manager 安装文档里的当前版本。controller、webhook、cainjector 三个 Pod 都应变为 Ready。国内集群拉取 quay.io 镜像慢时，可以先同步到自己的镜像仓库，再通过 Helm 参数覆盖镜像地址。

## 第二步：创建测试和生产 ClusterIssuer

ClusterIssuer 告诉 cert-manager 向哪个 ACME 服务器申请证书。先建测试和生产两个，用测试环境调通后再切到生产：

```yaml
# issuers.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik
```

```bash
kubectl apply -f issuers.yaml
kubectl get clusterissuer
```

`ingressClassName` 改成你的控制器的类名。想申请 45 天证书，在 `acme:` 下加 `profile: tlsserver`，见 [Let's Encrypt 证书配置文件](https://ssl.sh/zh/guides/lets-encrypt-certificate-profiles/)。

## 第三步：通过 Ingress 申请证书

给 Ingress 加注解并声明 TLS 域名和 Secret 名称，cert-manager 会自动创建 Certificate：

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
  ingressClassName: traefik
  tls:
    - hosts:
        - example.com
      secretName: example-com-tls
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
```

```bash
kubectl apply -f ingress.yaml
kubectl get certificate example-com-tls -w
```

测试环境下 `READY` 变为 `True` 后，把注解改成 `letsencrypt-prod` 并删除 Secret，重新签发受信任的证书：

```bash
kubectl annotate ingress web cert-manager.io/cluster-issuer=letsencrypt-prod --overwrite
kubectl delete secret example-com-tls
```

用 Gateway API 的，先开启 cert-manager 的 Gateway 支持，再在 Gateway 上加同样的 `cert-manager.io/cluster-issuer` 注解，带 `tls.certificateRefs` 的监听器会自动获得证书（详见 cert-manager 的 Gateway 文档）。

## 第四步（可选）：用 DNS-01 申请通配符证书

通配符证书只能用 DNS-01 验证，在 ClusterIssuer 的 `solvers` 里加一个 `dns01` 求解器，以 Cloudflare 为例：

```yaml
solvers:
  - dns01:
      cloudflare:
        apiTokenSecretRef:
          name: cloudflare-api-token
          key: api-token
```

```bash
kubectl create secret generic cloudflare-api-token -n cert-manager \
  --from-literal=api-token=your-cloudflare-api-token
```

ClusterIssuer 引用的 Secret 必须放在 cert-manager 命名空间。阿里云 DNS、DNSPod 需要社区 webhook，见常见问题。

## 验证

签发成功后，Certificate 的 `READY` 为 `True`，公网访问返回 Let's Encrypt 证书。用 [SSL 证书检查](https://ssl.sh/zh/tools/ssl-checker/) 检查公网访问，或在终端运行：

```bash
curl ssl.sh/example.com
```

在集群里：

```bash
kubectl describe certificate example-com-tls
kubectl get secret example-com-tls -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl x509 -noout -issuer -dates
```

## 自动续期

cert-manager 在证书有效期过去三分之二时自动续期（可用 `renewBefore` 或 `renewBeforePercentage` 调整），更新 Secret 后 Ingress 控制器和网关会自动加载。续期时间按证书实际有效期计算，所以 2027 年 2 月 10 日 Let's Encrypt 改为 64 天、2028 年 2 月 16 日改为 45 天时都不用改配置。保持 cert-manager 升级，留意 `Ready=False` 的 Certificate，并把公网域名加到 [ssl.sh 监控](https://ssl.sh/zh/monitor/)（开发中）：

```bash
kubectl get certificates -A
```

## 常见问题排查

按 Certificate → CertificateRequest → Order → Challenge 的顺序找卡在哪一步：

```bash
kubectl get certificaterequest,order,challenge -A
kubectl describe challenge -A
```

- **Waiting for HTTP-01 challenge propagation: wrong status code 404**：求解器的 `ingressClassName` 和控制器不一致，或 80 端口到不了控制器（安全组、未备案拦截），原因和 [certbot 连接超时](https://ssl.sh/zh/errors/certbot-timeout-during-connect/) 相同。
- 客户端报 **x509: certificate signed by unknown authority**：还在用测试环境证书，见 [x509: certificate signed by unknown authority](https://ssl.sh/zh/errors/x509-certificate-signed-by-unknown-authority/)。
- **too many certificates already issued**：在生产环境反复删除重签，见 [Let's Encrypt 频率限制](https://ssl.sh/zh/errors/lets-encrypt-rate-limit/)。
- **CAA record prevents issuance**：见 [CAA 记录阻止签发](https://ssl.sh/zh/errors/caa-record-prevents-issuance/)。
- **Secret 已存在，控制器却返回默认证书**：Ingress 的 `tls.secretName` 或命名空间和 Secret 对不上。
