K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

本轮操作记录:ACME staging、HTTP-01 失败样本与排障实验

这一轮我要把下面这些问题变成真实证据: 1. ACME `ClusterIssuer` 到底如何注册账户 2. `privateKeySecretRef` 在 ACME issuer 上存的到底是什么 3. 带 cert-manager 注解的 Ingress 如何一路生成 `CertificateRequest -> Order -> Challenge`

Markdown17-操作记录-ACME-staging-HTTP01-失败样本与排障实验.md2026年4月10日 09:04

本轮操作记录:ACME staging、HTTP-01 失败样本与排障实验

本轮目标

这一轮我要把下面这些问题变成真实证据:

  1. ACME ClusterIssuer 到底如何注册账户
  2. privateKeySecretRef 在 ACME issuer 上存的到底是什么
  3. 带 cert-manager 注解的 Ingress 如何一路生成 CertificateRequest -> Order -> Challenge
  4. HTTP-01 solver 会自动创建哪些临时资源
  5. 为什么 cert-manager 会先做 self-check
  6. 为什么 NodePort 30080 不是 ACME HTTP-01 的充分条件
  7. 这次实验到底失败在 solver,还是失败在公网 80
  8. 生产上为什么要先打 staging,再切 production

Step 1:先核对当前 north-south 入口底座

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n ingress-nginx get svc ingress-nginx-controller -o wide

curl --noproxy '*' -sS -D - --max-time 8 http://154.9.27.60:30080/ -o /dev/null

curl --noproxy '*' -sS -D - --max-time 8 http://154.9.27.60/ -o /dev/null

我看到的关键结果

ingress-nginx Service

  • TYPE: NodePort
  • 80:30080/TCP
  • 443:30443/TCP

访问 154.9.27.60:30080

返回:

HTTP/1.1 404 Not Found

说明:

  • NodePort 本身是通的

访问 154.9.27.60:80

返回:

curl: (52) Empty reply from server

为什么这一步要先做

因为 HTTP-01 的成败,本质不是 cert-manager YAML 能不能 apply,而是:

  • 公网域名最终是否能在标准 80 端口拿到 challenge 文件

如果入口前提不满足,后面即使 solver 全部创建成功,ACME 仍然会失败。

原理解释

这里要先建立一个非常关键的前置判断:

Kubernetes NodePort 暴露模型
!=
ACME HTTP-01 协议要求

当前集群只明确暴露了:

  • 30080
  • 30443

但 ACME HTTP-01 看的不是:

  • 30080

而是:

  • 80

所以后面实验如果失败,我们就必须判断:

  • 是 cert-manager 没做对
  • 还是公网标准端口暴露模型不满足协议要求

Step 2:确认可用的 ACME 测试环境和邮箱策略

实际命令

git config --global user.email

我看到的结果

  • 16128799+hyuanjun@user.noreply.gitee.com

为什么这一步重要

ACME 账户注册需要一个有效邮箱。

这不是随便乱填都行。

cert-manager 官方排障文档里明确给过一个典型失败例子:

  • @example.com 这类占位邮箱会被判定为无效联系域

所以这次实验我没有把模板文件里硬编码成某个真实邮箱,而是:

  1. 在 repo 里把 issuer 文件做成模板
  2. 运行时从本机拿一个有效邮箱注入

我写入仓库的模板与脚本

原理解释

这里有两个工程习惯训练点:

第一:不要把生产配置写死在模板里

像邮箱、域名、DNS provider 凭据这类内容,本来就应该参数化。

第二:staging 和 production 要分开

它们至少有三层差异:

  • ACME server URL 不同
  • account 是分环境隔离的
  • trust 根不同

所以我直接把 staging / prod issuer 模板拆成两个文件。


Step 3:先创建 ACME 测试业务的命名空间和后端

本课业务清单

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl apply \
  -f manifests/17-acme/00-namespace-acme-lab.yaml \
  -f manifests/17-acme/10-web.yaml \
  -f manifests/17-acme/20-service.yaml

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n acme-lab wait --for=condition=Available deploy/acme-web --timeout=180s

我创建了什么

Namespace

  • acme-lab

Deployment

  • acme-web

它返回一个静态页面,里面会写出:

  • pod
  • pod IP
  • node
  • host
  • issuer

Service

  • acme-web-svc

为什么要先创建业务再搞 ACME

因为 ACME 并不是“脱离业务入口存在”的。

你要 secure 的对象仍然是:

  • 一个真实 Host
  • 一个真实 Ingress
  • 一个真实业务入口

所以必须先有最小业务闭环,才能看清 cert-manager 在上面叠加了什么。


Step 4:创建 Let's Encrypt staging ClusterIssuer

实际命令

chmod +x manifests/17-acme/32-apply-issuer-template.sh

ACME_EMAIL="$(git config --global user.email)" \
KUBECONFIG=~/.kube/config-k8s-lab \
./manifests/17-acme/32-apply-issuer-template.sh \
  manifests/17-acme/30-clusterissuer-letsencrypt-staging-http01.tmpl.yaml \
  "$(git config --global user.email)"

我看到的结果

clusterissuer.cert-manager.io/letsencrypt-staging-http01 created

继续验证就绪状态

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl wait --for=condition=Ready clusterissuer/letsencrypt-staging-http01 --timeout=180s

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get clusterissuer letsencrypt-staging-http01 -o yaml | sed -n '1,220p'

我看到的关键结果

Ready

  • clusterissuer.cert-manager.io/letsencrypt-staging-http01 condition met

状态

status:
  acme:
    lastRegisteredEmail: 16128799+hyuanjun@user.noreply.gitee.com
  conditions:
  - message: The ACME account was registered with the ACME server
    reason: ACMEAccountRegistered
    status: "True"
    type: Ready

原理解释

这一段非常关键,因为它说明:

  • cert-manager 不只是认识 YAML
  • 它真的拿着 issuer 配置去和 ACME server 建立了账户关系

这里还要特别强调一个核心概念:

privateKeySecretRef 在 ACME issuer 上存的是账户私钥

它不是将来业务证书的 tls.key

它的用途是:

  • 代表这个 ACME account 的身份
  • 用于和 ACME server 建立 / 维持账户关系

所以:

ClusterIssuer Ready,只说明 ACME account 建好了,不说明业务证书已经能签出来。


Step 5:创建带 ACME 注解的 Ingress

本课 Ingress 清单

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl apply -f manifests/17-acme/40-ingress-http01.yaml

Ingress 的关键内容

  • host: 154-9-27-60.sslip.io
  • cert-manager.io/cluster-issuer: letsencrypt-staging-http01
  • tls.secretName: acme-web-tls

为什么这里故意选 staging + HTTP-01

因为这一课的重点不是“先拿一张公网可信证书”,而是:

  • 把 ACME HTTP-01 的全过程做成可观察证据

staging 的好处是:

  • 可以安全调试
  • 不容易消耗 production 配额

为什么这里故意把 host 写成一个公网风格的域名

因为 ACME 挑战必须面向公共名字空间。

如果你直接用:

  • *.local

那连“公网可验证”这一步都不成立。

所以这里我选了一个公网风格的 lab host,目的是把 challenge 逻辑真正拉起来。


Step 6:观察 ACME 控制链是否真的开始工作

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n acme-lab get \
  ingress,certificate,certificaterequest,orders.acme.cert-manager.io,\
  challenges.acme.cert-manager.io,secret,svc,pod -o wide

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n acme-lab get events --sort-by=.lastTimestamp

我看到的关键结果

业务 Ingress

  • ingress/acme-web

自动创建的证书对象

  • certificate/acme-web-tls

自动创建的请求对象

  • certificaterequest/acme-web-tls-1

自动创建的 ACME Order

  • order/acme-web-tls-1-1911881332

自动创建的 Challenge

  • challenge/acme-web-tls-1-1911881332-3263821333

自动创建的 solver Pod

  • pod/cm-acme-http-solver-nkzqw

自动创建的 solver Service

  • service/cm-acme-http-solver-hjdz8

自动创建的 solver Ingress

  • ingress/cm-acme-http-solver-8s8sd

我看到的关键事件顺序

CreateCertificate    ingress/acme-web
OrderCreated         certificaterequest/acme-web-tls-1
Created Challenge    order/acme-web-tls-1-1911881332
Presented            challenge/acme-web-tls-1-1911881332-3263821333
Scheduled            pod/cm-acme-http-solver-nkzqw
Started              pod/cm-acme-http-solver-nkzqw

原理解释

这一步的价值非常大。

它证明了从 Ingress 到 ACME 中间态的整条控制链都已经被拉起来了:

Ingress
  -> ingress-shim / Certificate
  -> CertificateRequest
  -> Order
  -> Challenge
  -> solver Pod / Service / Ingress

如果一个人只会说:

  • “ACME 不工作”

但说不出这条链每个对象在做什么,那他其实还没真正掌握 ACME 自动化。


Step 7:直接读取 Challenge 和 Order,进入协议层视角

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n acme-lab get challenge acme-web-tls-1-1911881332-3263821333 -o yaml | sed -n '1,260p'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n acme-lab get order acme-web-tls-1-1911881332 -o yaml | sed -n '1,260p'

我看到的 Challenge 关键字段

spec:
  dnsName: 154-9-27-60.sslip.io
  type: HTTP-01
  token: bkHmORU8pOA2iM9cqh9UzGhuGlHELnqkWEQkaNkq7pE
  key: bkHmORU8pOA2iM9cqh9UzGhuGlHELnqkWEQkaNkq7pE.OIJrMZxlRGC5jm2Wib4qPzN9A-5CQgSQL_GlH7uJT-k
status:
  presented: true
  processing: true
  state: pending

我看到的 Order 关键字段

status:
  authorizations:
    - challenges:
        - type: http-01
        - type: tls-alpn-01
        - type: dns-01

原理解释

这一段非常重要。

token

是 ACME server 给你的挑战 token。

key

是 cert-manager 计算出的 key authorization。

也就是说:

  • 不是简单把 token 原样返回就行

Order 里的多个 challenge 类型

说明 ACME server 不是只会给你一种路。

它会给出多个可用 challenge 类型。

真正选哪一种,是由你的 solver 配置决定的。

我们这次选择的是:

  • http01

所以 cert-manager 创建了:

  • HTTP-01 类型的 Challenge

Step 8:查看 solver Ingress 和 solver Service,确认 cert-manager 到底改了什么

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n acme-lab get ingress cm-acme-http-solver-8s8sd -o yaml | sed -n '1,220p'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n acme-lab get svc cm-acme-http-solver-hjdz8 -o yaml | sed -n '1,220p'

我看到的 solver Ingress 关键内容

spec:
  ingressClassName: nginx
  rules:
    - host: 154-9-27-60.sslip.io
      http:
        paths:
          - path: /.well-known/acme-challenge/bkHmORU8pOA2iM9cqh9UzGhuGlHELnqkWEQkaNkq7pE
            pathType: Exact
            backend:
              service:
                name: cm-acme-http-solver-hjdz8
                port:
                  number: 8089

我看到的 solver Service 关键内容

spec:
  type: NodePort
  ports:
    - port: 8089
      targetPort: 8089
      nodePort: 30490

原理解释

这两段是 HTTP-01 原理最直观的证据。

你可以清楚看到 cert-manager 自动创建了:

  1. 一个只负责 challenge path 的临时 Ingress
  2. 一个只负责暴露 solver pod 的临时 Service

而且 cert-manager 官方文档也明确说明:

  • HTTP01 solver Service 默认会用 NodePort

这进一步说明:

cert-manager 已经尽最大努力把 challenge 暴露出来了,但是否满足 ACME 成功条件,还取决于你的 north-south 暴露模型。


Step 9:确认真正的失败原因不是“资源没起来”,而是 self-check 失败

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n acme-lab describe challenge acme-web-tls-1-1911881332-3263821333

我看到的关键结果

Status:
  Presented:   true
  Processing:  true
  Reason:      Waiting for HTTP-01 challenge propagation: failed to perform self check GET request 'http://154-9-27-60.sslip.io/.well-known/acme-challenge/bkHmORU8pOA2iM9cqh9UzGhuGlHELnqkWEQkaNkq7pE': Get "http://154-9-27-60.sslip.io/.well-known/acme-challenge/bkHmORU8pOA2iM9cqh9UzGhuGlHELnqkWEQkaNkq7pE": dial tcp 154.9.27.60:80: connect: connection refused
  State:       pending

原理解释

这段输出直接把问题说透了。

Presented: true

说明:

  • cert-manager 已经成功“呈现”了 challenge

也就是:

  • solver 资源都创建好了

Processing: true

说明:

  • challenge 仍在处理流程中

State: pending

说明:

  • 还没进入 valid
  • 也还没被 ACME server 标记为最终失败

最关键的是 Reason

它明确指出:

  • self-check GET 失败
  • 访问的是标准 http://...:80/...
  • 失败原因是 connect: connection refused

这说明失败层次已经被精确定位:

不是 solver 没创建,不是 cert-manager 不会算 token,不是 ingress-shim 没补对象,而是目标域名的标准 80 端口不可达。


Step 10:从 cert-manager 日志验证 self-check 的重复重试机制

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n cert-manager logs deploy/cert-manager --since=15m | \
grep -E 'acme-lab|acme-web|letsencrypt-staging-http01|http01|challenge'

我看到的关键日志

ACME 账户注册

"generating acme account private key"
"verified existing registration with ACME server"

solver 资源已存在

"found one existing HTTP01 solver pod"
"found one existing HTTP01 solver Service for challenge resource"
"found one existing HTTP01 solver ingress"

self-check 反复失败

"propagation check failed"
... dial tcp 154.9.27.60:80: connect: connection refused

原理解释

这一段日志和 Challenge 状态互相印证:

  1. ACME account 是正常的
  2. solver pod/service/ingress 也都是正常存在的
  3. 真正失败的是 self-check
  4. cert-manager 会按固定节奏重复检查

这正好对应 cert-manager 官方文档对 Challenge lifecycle 的描述:

  • presented
  • self-check
  • 自检不通过就持续重试

这一步非常重要,因为它训练你从:

  • “表层 kubectl get”

进入:

  • “控制器日志 + 状态机”

的层面看问题。


Step 11:证明 solver 本身其实是好的,只是公网 80 没打通

这是本轮最关键的一步。

实际命令 1:从节点本机走 NodePort 30080 打 challenge path

ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@154.9.27.60 \
  "curl -sS -D - http://127.0.0.1:30080/.well-known/acme-challenge/bkHmORU8pOA2iM9cqh9UzGhuGlHELnqkWEQkaNkq7pE -H 'Host: 154-9-27-60.sslip.io'"

我看到的结果

HTTP/1.1 200 OK
...

bkHmORU8pOA2iM9cqh9UzGhuGlHELnqkWEQkaNkq7pE.OIJrMZxlRGC5jm2Wib4qPzN9A-5CQgSQL_GlH7uJT-k

这说明什么

说明:

  • challenge 内容已经能通过 ingress-nginx + solver Ingress + solver Service + solver Pod 正确返回

也就是说:

  • HTTP-01 的应用层路由没有问题

实际命令 2:直接打 solver Service 的 NodePort 30490

ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@154.9.27.60 \
  "curl -sS -D - http://127.0.0.1:30490/"

我看到的结果

HTTP/1.1 200 OK

这说明什么

说明:

  • solver Service
  • solver Pod

本身也是活着的。


实际命令 3:直接访问节点本机 80

ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout=8 root@154.9.27.60 \
  'curl -sS -D - http://127.0.0.1/.well-known/acme-challenge/bkHmORU8pOA2iM9cqh9UzGhuGlHELnqkWEQkaNkq7pE -H "Host: 154-9-27-60.sslip.io" || true'

我看到的结果

curl: (7) Failed to connect to 127.0.0.1 port 80 after 0 ms: Couldn't connect to server

原理解释

这一步是最关键的因果闭环。

三条证据合在一起:

  1. 30080 challenge path 能正确返回 key authorization
  2. 30490 solver Service 是活的
  3. 80 根本没人监听

所以可以严谨得出结论:

不是 solver 坏了
不是 cert-manager 坏了
不是 challenge 内容错了
而是公网标准入口 80 不满足 HTTP-01 的前提

这就是专家式排障。


Step 12:回头看,这次实验为什么虽然失败,但信息量极高

很多失败样本其实没有教学价值。

比如:

  • issuer 都没创建成功
  • challenge 根本没出来

那种失败只能教你“配置写错了”。

这次失败样本的价值在于它把几乎整条 ACME 链都跑出来了:

  • ACME account 注册成功
  • staging issuer Ready
  • Ingress 触发了 Certificate
  • Certificate 触发了 CertificateRequest
  • CertificateRequest 触发了 Order
  • Order 触发了 Challenge
  • Challenge 触发了 solver Pod / Service / Ingress
  • challenge path 在 NodePort 上已经可以返回正确内容

最后只差一步:

  • 公网标准 80

所以这次失败样本非常适合拿来训练你的分层定位能力。


Step 13:这轮实验真正学到的 10 个结论

  1. ClusterIssuer Ready 只说明 ACME account 建好了,不说明业务证书已经签好。
  2. privateKeySecretRef 在 ACME issuer 上对应的是 account key。
  3. OrderChallenge 是 ACME 协议状态,不是 cert-manager 私有玩具。
  4. solver Pod / Service / Ingress 是 cert-manager 自动创建出来处理 HTTP-01 的临时资源。
  5. Challenge Presented=True 不等于验证已经通过。
  6. self-check 是 cert-manager 主动帮你挡错的第一道门。
  7. Reason 往往就是最有信息量的排障证据。
  8. 127.0.0.1:30080 能通,不等于公网 80 能通。
  9. HTTP-01 失败时,首先查的是标准入口、LB、防火墙、NAT、云网络策略,不是先删证书资源。
  10. staging 应该先打通流程,再考虑切 production。

当前实验资源状态

截至本轮结束,acme-lab 里保留了失败样本,便于你后续继续观察:

  • ingress/acme-web
  • certificate/acme-web-tls
  • certificaterequest/acme-web-tls-1
  • order/acme-web-tls-1-1911881332
  • challenge/acme-web-tls-1-1911881332-3263821333
  • pod/cm-acme-http-solver-nkzqw
  • service/cm-acme-http-solver-hjdz8
  • ingress/cm-acme-http-solver-8s8sd

这意味着:

  • cert-manager 还会继续做 self-check 重试

这在教学上是有价值的,因为你随时都能继续查看状态和日志。


下一步最自然的推进

下一课可以有两种走法:

走法一:继续 HTTP-01

把:

  • 公网 80
  • challenge path
  • staging success

真正做成成功样本。

走法二:切到 DNS-01

把:

  • _acme-challenge
  • TXT 记录
  • wildcard
  • DNS API 凭据治理

讲透。

如果目标是平台专家,我更建议下一步重点推进:

  • DNS-01 + wildcard + 生产级自动签发

因为这比单点打通 HTTP-01 更接近企业平台实践。