Repository Reading Site
本轮操作记录:ACME staging、HTTP-01 失败样本与排障实验
这一轮我要把下面这些问题变成真实证据: 1. ACME `ClusterIssuer` 到底如何注册账户 2. `privateKeySecretRef` 在 ACME issuer 上存的到底是什么 3. 带 cert-manager 注解的 Ingress 如何一路生成 `CertificateRequest -> Order -> Challenge`
本轮操作记录:ACME staging、HTTP-01 失败样本与排障实验
本轮目标
这一轮我要把下面这些问题变成真实证据:
- ACME
ClusterIssuer到底如何注册账户 privateKeySecretRef在 ACME issuer 上存的到底是什么- 带 cert-manager 注解的 Ingress 如何一路生成
CertificateRequest -> Order -> Challenge - HTTP-01 solver 会自动创建哪些临时资源
- 为什么 cert-manager 会先做 self-check
- 为什么
NodePort 30080不是 ACME HTTP-01 的充分条件 - 这次实验到底失败在 solver,还是失败在公网
80 - 生产上为什么要先打 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: NodePort80:30080/TCP443: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 协议要求
当前集群只明确暴露了:
3008030443
但 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这类占位邮箱会被判定为无效联系域
所以这次实验我没有把模板文件里硬编码成某个真实邮箱,而是:
- 在 repo 里把 issuer 文件做成模板
- 运行时从本机拿一个有效邮箱注入
我写入仓库的模板与脚本
- 30-clusterissuer-letsencrypt-staging-http01.tmpl.yaml
- 31-clusterissuer-letsencrypt-prod-http01.tmpl.yaml
- 32-apply-issuer-template.sh
原理解释
这里有两个工程习惯训练点:
第一:不要把生产配置写死在模板里
像邮箱、域名、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 建立 / 维持账户关系
所以:
ClusterIssuerReady,只说明 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.iocert-manager.io/cluster-issuer: letsencrypt-staging-http01tls.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 自动创建了:
- 一个只负责 challenge path 的临时 Ingress
- 一个只负责暴露 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 状态互相印证:
- ACME account 是正常的
- solver pod/service/ingress 也都是正常存在的
- 真正失败的是 self-check
- 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
原理解释
这一步是最关键的因果闭环。
三条证据合在一起:
30080challenge path 能正确返回 key authorization30490solver Service 是活的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 个结论
ClusterIssuer Ready只说明 ACME account 建好了,不说明业务证书已经签好。privateKeySecretRef在 ACME issuer 上对应的是 account key。Order和Challenge是 ACME 协议状态,不是 cert-manager 私有玩具。- solver Pod / Service / Ingress 是 cert-manager 自动创建出来处理 HTTP-01 的临时资源。
- Challenge
Presented=True不等于验证已经通过。 - self-check 是 cert-manager 主动帮你挡错的第一道门。
Reason往往就是最有信息量的排障证据。127.0.0.1:30080能通,不等于公网80能通。- HTTP-01 失败时,首先查的是标准入口、LB、防火墙、NAT、云网络策略,不是先删证书资源。
- staging 应该先打通流程,再考虑切 production。
当前实验资源状态
截至本轮结束,acme-lab 里保留了失败样本,便于你后续继续观察:
ingress/acme-webcertificate/acme-web-tlscertificaterequest/acme-web-tls-1order/acme-web-tls-1-1911881332challenge/acme-web-tls-1-1911881332-3263821333pod/cm-acme-http-solver-nkzqwservice/cm-acme-http-solver-hjdz8ingress/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 更接近企业平台实践。