前言
在装好k8s和CICD自动化部署后,再来一个HTTPS证书自动签发是不是会更香?
在使用cert-manager实现HTTPS证书中遇到了一个天选之坑,排查到最后发现无解,因为官方不支持。
我最初就是采用Traefik ConfigMap
+ 已有证书进行配置,结果因为各种调试不成功,才会去使用Cert manager,然后遇到天坑。期间也了解到我的已有证书是通配*.domain.com,而我的诉求是a.testing.domain.com、a.staging.domain.com、a.production.domain.com,只能使用let’s encrypt自签发证书服务。
折腾了数日后,我重新研究了一下各个工具的实现原理的差异化,在深入了解Traefik 2.0 的ACME工作方式之后重拾信心,我会在文章的最后放出我实验通过的Traefik 2.0的yaml代码。如果您也打算使用Traefik实现HTTPS,就不用再阅读这篇文章了,直接跳到最后一章。并建议您看一下1.0 和 2.0的区别,就会知道网上很多搜索“traefik https”出来的TOML创建ConfigMap
的写法不是2.0的,所以一直不成功的原因。
接下来,我将这次排查cert-manager问题的过程叙述一遍,供大家解读。方便搜索而来的朋友感知这个问题。
问题根源
问题是由SLB反代引起,集群不支持80 443直接访问,统一转为NodePort 32001和32002,而Cert manager的self check使用ClusterIP 8089
端口。
环境说明
- kubernetes v1.16.3
- Helm V3(负责版本发布)
- Calico(负责内部DNS)
- Traefik 2.0.1(负责ingress路由)
- 阿里云SLB (负责负载均衡)
- 所有80和443服务使用NodePort统一为32001和32002,SLB将其转换为80和443 。
当我在测试cert-manager各种问题时,我用kind:Ingress 和nginx-controller 做测试。在部署Traefik项目时,我使用kind:Certificate去触发证书申请。是一个二选一的方式,不用两个都做。
推荐阅读资料:
- Cert-manager安装中文文档
- Set up TLS for custom Traefik ingress resources in k3s on ARM(英文)
- 这篇文章使用Traefik结合Certificate进行签发
- 不推荐使用文中提到的Issuer,请更换为ClusterIssuer,就不需要指定NameSpace了
- 这篇文章知识点丰富
- 使用 cert-manager 自动生成证书(推荐)
- 这篇文章使用nginx结合Certificate进行签发
- 这份文档讲的很全面,而且是2019年11月16日发布,推荐阅读
排查过程
propagetion check failed 错误
我遇到了这个问题,简而言之就是能够检测到我的域名,但是没有返回正确的验证文本给cert manager。
我猜应该是cert manager没有拦截到traefik的traffic,在里边加一个输出验证文本的路由。
查看traefik的logs,发现的确有cert manager 的self check的请求发送过来。
其实traefik的logs里不应该出现self check的请求,我此时还不知道cert manager的拦截方式。
> $ kubectl -n example-testing log traefik-6c4fd588c-g5898 —tail=300
我.的.节点.ip - - [21/Dec/2019:04:40:54 +0000] "GET /.well-known/acme-challenge/G2OOCTGOqRA0H8jltxEynOofnrkCYofG4dKgJ0-igYk HTTP/1.1" 200 633 "-" "-" 7312 "frontend@file" "http://frontend" 0ms
我.的.真实.ip - - [21/Dec/2019:04:47:29 +0000] "GET /css/style-10.css HTTP/1.1" 200 4861 "-" "-" 7359 "frontend@file" "http://frontend" 0ms
http://frontend 是traefik动态配置中我设置的service:loadBalancer。就好比访问到了/index.html 一样
由于traefik部署的项目含有业务代码,配置文件太多并不方便调试,我尝试用一个nginx来测试。需要修改ClusterIssuer
的ingress.class=nginx
再apply下。
创建一个nginx_example.yaml
,请注意我使用了HostPort32001和32002,因为SLB的缘故:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
app: my-nginx
replicas: 1
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: nginx:1.7.9
ports:
- name: http
containerPort: 80
hostPort: 32001
protocol: TCP
- name: https
containerPort: 443
hostPort: 32002
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: my-nginx
labels:
app: my-nginx
spec:
ports:
- port: 80
protocol: TCP
name: http
- port: 443
protocol: TCP
name: https
selector:
run: my-nginx
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: my-nginx
annotations:
kubernetes.io/ingress.class: "my-nginx"
kubernetes.io/tls-acme: "true"
cert-manager.io/cluster-issuer: "letsencrypt-staging"
certmanager.k8s.io/acme-http01-edit-in-place: "true"
spec:
rules:
- host: bilibili.staging.****.com
http:
paths:
- backend:
serviceName: my-nginx
servicePort: 443
path: /
tls:
- secretName: bilibili-test-tls
hosts:
- bilibili.staging.****.com
kubectl apply -f nginx_example.yaml
然后看一下日志,发现收到404请求:
我.的.节点.IP - - [21/Dec/2019:06:05:12 +0000] "GET /.well-known/acme-challenge/G2OOCTGOqRA0H8jltxEynOofnrkCYofG4dKgJ0-igYk HTTP/1.1" 404 168 "-" "Go-http-client/1.1" "-"
2019/12/21 06:05:22 [error] 6#0: *9849 open() "/usr/share/nginx/html/.well-known/acme-challenge/G2OOCTGOqRA0H8jltxEynOofnrkCYofG4dKgJ0-igYk" failed (2: No such file or directory), client: 47.94.90.174, server: localhost, request: "GET /.well-known/acme-challenge/G2OOCTGOqRA0H8jltxEynOofnrkCYofG4dKgJ0-igYk HTTP/1.1", host: “b-test.****.com"
404呢,让我们来回忆一下。
HTTP-01 校验原理
HTTP-01 的校验原理是给你域名指向的 HTTP 服务增加一个临时 location ,Let’s Encrypt 会发送 http 请求到 http:///.well-known/acme-challenge/,YOUR_DOMAIN 就是被校验的域名,TOKEN 是 ACME 协议的客户端负责放置的文件,在这里 ACME 客户端就是 cert-manager,它通过修改 Ingress 规则来增加这个临时校验路径并指向提供 TOKEN 的服务。Let’s Encrypt 会对比 TOKEN 是否符合预期,校验成功后就会颁发证书。此方法仅适用于给使用 Ingress 暴露流量的服务颁发证书,并且不支持泛域名证书。
现在不满足原理的运行结果。
基本可以判定是cert manager 没有找到我们设置的Ingress,所以无法设置一个临时的路由规则来提供Token。
- 是cert manager权限不够吗? —— 重头回官网文档找了一圈,貌似没权限这个说法。
- 会不会是helm v3不兼容cert manager呢? —— 上网搜了下,也看到有网友用helm v3安装cert manager并且获取证书的case。
Helm v3 Cert-Manager 不用装Tiller
cert-manager官方的helm安装要求里提到需要安装helm和tiller。因为Helm v3已经取消了tiller,自动集成在了内部,所以不用单独安装。如果你正在使用helm v3之前的版本,还是需要安装Tiller的。
越来越觉得我这个问题个例了。我重新看了下官方的FAQ,提供了一堆排查方式和命令,推荐大家阅读,没准就有能解决你问题的关键在这里边。
Waiting for http-01 challenge propagation: wrong status code ‘404’, expected ‘200’
通过FAQ中提到的命令describe challenge
,我重新定位了我的问题点,请看下面代码的Reason
:
Status:
Presented: true
Processing: true
Reason: Waiting for http-01 challenge propagation: wrong status code '404', expected '200'
State: pending
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Started 5m23s cert-manager Challenge scheduled for processing
Normal Presented 5m23s cert-manager Presented challenge using http-01 challenge mechanism
我感觉这个范围应该会小很多,因为用一个空nginx做cert manager测试的人一定不少!
分析签发过程
官方FAQ给我带来的思考,是我了解到cert manager的签发过程,我给大家说一下这个错误是在什么阶段会遇到的,离终点就差一步之遥!
定了方向后,我在官方issue里发现有非常多的人遇到了这个问题,但几乎都无解。有一部分人建议改成dns01,然后问题解决的(via)。
我遇到一个有意思的写法,它会使用相同的ingress而不是创建一个新的ingress(见官方)。我试了一试,还是一样的错误,总之先记录吧,一定会有用:
certmanager.k8s.io/acme-http01-edit-in-place:"true"
我尝试把所有可能新增的资源都用命令看一遍,在执行kubect get ing
时,我发现多了一个cm-acme-http-solver-mpszd
的ingress。
cert-manager会创建一个ingress 路由规则
我当然要看一看里边了, describe ing cm-acme-http-solver-mpszd
……
我可算明白了,nmb… 这个方向有一种十拿九稳的感觉!内心激动的想放鞭炮,就像过年时一群人在桥底下的小河旁偷偷放鞭炮的兴奋!
cert manager提供TOKEN服务的做法,就是自己创建一个ingress……也对,说得通。是我之前想太复杂了,以为它需要权限去改动我的ingress controller rules。
它自己另外生成的ingress服务是基于8089端口的,可是我们的SLB没有使用8089端口。
需要将它的NodePort改为32001就可以走SLB了。赶紧回头看看官网的配置文档,找到了这个:
官方文档里的ServicePort不存在
修改ClusterIssuer
:
apiVersion: cert-manager.io/v1alpha2
# kind: Issuer
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
#namespace: default
spec:
acme:
# You must replace this email address with your own.
# Let's Encrypt will use this to contact you about expiring
# certificates, and issues related to your account.
email: 54583315@qq.com
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
# Secret resource used to store the account's private key.
name: letsencrypt-staging
# Add a single challenge solver, HTTP01 using nginx
solvers:
- selector: {}
http01:
ingress:
serviceType: NodePort
servicePort: 32001
class: nginx
然后给了我一个惊喜:
error: error validating "le-staging.yaml": error validating data: [ValidationError(ClusterIssuer.spec.acme.solvers[0].http01): unknown field "servicePort" in io.cert-manager.v1alpha2.ClusterIssuer.spec.acme.solvers.http01, ValidationError(ClusterIssuer.spec.acme.solvers[0].http01): unknown field "serviceType" in io.cert-manager.v1alpha2.ClusterIssuer.spec.acme.solvers.http01]; if you choose to ignore these errors, turn validation off with —validate=false
emmmm,再看下API文档 ,我发现官方的写法是错的,ServiceType
应该放在ingress组里。但是API里也没有ServicePort
这个参数啊。前后不呼应的文档要命,而且不知道ServiceType
接收什么类型的数据,看了一下映射的Kubernetes core/v1.ServiceType
,只能自己尝试多用几种写法,误打误撞了。
ValidationError(ClusterIssuer.spec.acme.solvers[0].http01.ingress.serviceType): invalid type for io.cert-manager.v1alpha2.ClusterIssuer.spec.acme.solvers.http01.ingress.serviceType: got "array", expected “string";
又给了我一个漂亮的回击,原来ServiceType
只能写string
,wow!哇!So,Where I can set ServicePort ?emmm,有意思,所以我浪费了十几分钟后发现根本没有办法设置Port。
目前,由于没办法改变NodePort,可以确定是bug了(至少文章级别bug!)。
我打算去看下github/cert-manager这一块的GoLang源码,找找是哪里生成NodePort的、有没有地方配置传参。
不过先让我在官方留一个issue,想感受语法超度的可以看一下哦。
cert-manager 使用acmeSolverListenPort 8089
https://github.com/jetstack/cert-manager/blob/master/pkg/issuer/acme/http/service.go#L104
#119 使用一个k8s.io/api/core/v1
Service
类型,里边的acmeSolverListenPort
是固定的8089,没有地方可以设置。
通过观察源代码,没有地方可以传递额外的Port进去。
使用DNS01验证阿里云DNS失败
由于我使用的是阿里云DNS,网上有网友提供了一套阿里云DNS webhook的组件,要根据自己的k8s 版本修改里边的API。
由于我的alidns-webhook logs 一直打印 tls bad certificate
,所以没办法用。
最后
我还没有研究出一套行之有效的解决办法,HTTP01绝对是可以的,可能要选用cert-manager之外的服务了。
使用Traefik 2.0 ACME 完成自动签发
下边的代码提供了traefik
服务的 Service
、ServiceAccount
、Deployment
的部署。这个traefik
服务放在了default
这个namespace
里,你可以修改为kube-system
或者任何你自建的用于管理集中服务的namespace中。或者删掉namespace
,在执行kubectl apply -f filename.yaml -n <namespace>
亦可。你可以认为是通用的,我会另起一份文件来实现具体业务代码的部署。
你会发现我将443注释了,因为没有用,也不会用到,留在这里是想提醒大家,不需要443 。 我之前在测试的时候,浪费了一点时间在这上面。
如果你的服务器可以直接请求80
和443
,就不需要像我一样使用NodePort转发到32001、32002,请把hostPort
那两行去掉。
# 这份文件随便命名,最后执行 kubectl apply -f 文件名.yaml
apiVersion: v1
kind: Service
metadata:
name: traefik
spec:
ports:
- protocol: TCP
name: web
port: 80
# 不需要的服务
# - protocol: TCP
# name: websecure
# port: 443
selector:
app: traefik
---
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: default
name: traefik-ingress-controller
---
kind: Deployment
apiVersion: apps/v1
metadata:
namespace: default
name: traefik
labels:
app: traefik
spec:
replicas: 1
selector:
matchLabels:
app: traefik
template:
metadata:
labels:
app: traefik
spec:
serviceAccountName: traefik-ingress-controller
containers:
- name: traefik
image: traefik:v2.0
args:
- --api.insecure
- --accesslog
- --api.dashboard=true
- --entrypoints.web.Address=:80
- --entrypoints.websecure.Address=:443
- --providers.kubernetescrd
- --certificatesresolvers.default.acme.tlschallenge
#请将email改成你的邮箱地址
- --certificatesresolvers.default.acme.email=your@email.com
- --certificatesresolvers.default.acme.storage=acme.json
- --certificatesResolvers.sample.acme.httpChallenge.entryPoint=web
ports:
- name: web
containerPort: 80
hostPort: 32001
protocol: TCP
- name: websecure
containerPort: 443
hostPort: 32002
protocol: TCP
下面是具体的业务程序的部署了,app服务的名称叫whoami
使用了Deployment
、Service
,最后使用IngressRoute
实现路由,让pod可以被访问。同样,namespace
也可以删掉,在执行kubectl apply -f filename.yaml -n <namespace>
时指定。
这里我使用的image
是自己的habor服务,请替换为想使用的镜像地址,并将镜像指定的expose
的port(暴露端口)替换掉containerPort
的值。一般来说都是80。
# 这份文件随便命名,最后执行 kubectl apply -f 文件名.yaml
apiVersion: v1
kind: Service
metadata:
name: whoami
spec:
type: ClusterIP
ports:
- protocol: TCP
name: web
port: 80
# 不需要指定 443 的 端口
# - protocol: TCP
# name: websecure
# port: 443
selector:
app: whoami
---
kind: Deployment
apiVersion: apps/v1
metadata:
namespace: default
name: whoami
labels:
app: whoami
spec:
replicas: 1
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
imagePullSecrets:
- name: regcred
containers:
- name: whoami
image: mymymymymymy.docker.com/example/frontend:7b9028b8e157c76b6262951efb43eefa87d9d5ae
ports:
- name: web
containerPort: 80
protocol: TCP
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: simpleingressroute
namespace: default
spec:
entryPoints:
- web
routes:
- match: Host(`a.testing.domain.com`)
kind: Rule
services:
- name: whoami
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: ingressroutetls
namespace: default
spec:
entryPoints:
- websecure
routes:
- match: Host(`a.testing.domain.com`)
kind: Rule
services:
- name: whoami
port: 80
tls:
certResolver: default
在最后这份yaml
文件被执行后,才会开始往第一份yaml
文件中的app=traefik
的服务里请求和获取服务,于是第一份yaml
文件中args.cli
就会去申请证书并生效啦。
发表回复