konakona
Dream Afar.
konakona
SLB反代K8S时,使用Cert Manager遇到的棘手问题
SLB反代K8S时,使用Cert Manager遇到的棘手问题

前言

在装好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 。

两种方式
想要使用签发证书可以有2个方法(见官方),一种是kind:Certificate,还有一种是kind:Ingress。

当我在测试cert-manager各种问题时,我用kind:Ingress 和nginx-controller 做测试。在部署Traefik项目时,我使用kind:Certificate去触发证书申请。是一个二选一的方式,不用两个都做。

推荐阅读资料:


排查过程

propagetion check failed 错误

我遇到了这个问题,简而言之就是能够检测到我的域名,但是没有返回正确的验证文本给cert manager。

> $ kubectl -n cert-manager log cert-manager-754d9b75d9-s82gq cert-manager | tail -3  

I1221 04:07:01.192414       1 ingress.go:91] cert-manager/controller/challenges/http01/selfCheck/http01/ensureIngress "level"=0 "msg"="found one existing HTTP01 solver ingress" “dnsName”=“b-test.****.com” "related_resource_kind"="Ingress" "related_resource_name"="cm-acme-http-solver-m5wvg" "related_resource_namespace"="example-testing" "resource_kind"="Challenge" "resource_name"="letsencrypt-staging-292069265-4232741098-1586037050" "resource_namespace"="example-testing" "type"="http-01"
E1221 04:07:01.232087       1 sync.go:184] cert-manager/controller/challenges "msg"="propagation check failed" "error"="presented key (\u003c!DOCTYPE html一坨HTML代码,此处省略) did not match expected (G2OOCTGOqRA0H8jltxEynOofnrkCYofG4dKgJ0-igYk.bTsBHadaEUVsqKCLhCius-9InT_bf_jWImKRG2Xj2ww)" “dnsName”=“b-test.****.com” "resource_kind"="Challenge" "resource_name"="letsencrypt-staging-292069265-4232741098-1586037050" "resource_namespace"="example-testing" "type"="http-01"
I1221 04:07:01.232151       1 controller.go:135] cert-manager/controller/challenges "level"=0 "msg"="finished processing work item" "key"="example-testing/letsencrypt-staging-292069265-4232741098-1586037050"

我猜应该是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 是什么?
keyboard_arrow_down

http://frontend 是traefik动态配置中我设置的service:loadBalancer。就好比访问到了/index.html 一样

由于traefik部署的项目含有业务代码,配置文件太多并不方便调试,我尝试用一个nginx来测试。需要修改ClusterIssueringress.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的签发过程,我给大家说一下这个错误是在什么阶段会遇到的,离终点就差一步之遥!

https://blog.img.crazyphper.com/2019/12/image-1.png
self check 卡住了

定了方向后,我在官方issue里发现有非常多的人遇到了这个问题,但几乎都无解。有一部分人建议改成dns01,然后问题解决的(via)。

我遇到一个有意思的写法,它会使用相同的ingress而不是创建一个新的ingress(见官方)。我试了一试,还是一样的错误,总之先记录吧,一定会有用:

certmanager.k8s.io/acme-http01-edit-in-place:"true"
https://blog.img.crazyphper.com/2019/12/image-7-743x600.png

我尝试把所有可能新增的资源都用命令看一遍,在执行kubect get ing时,我发现多了一个cm-acme-http-solver-mpszd的ingress。

cert-manager会创建一个ingress 路由规则

我当然要看一看里边了, describe ing cm-acme-http-solver-mpszd ……

https://blog.img.crazyphper.com/2019/12/image-5-800x279.png
注意NodePort

我可算明白了,nmb… 这个方向有一种十拿九稳的感觉!内心激动的想放鞭炮,就像过年时一群人在桥底下的小河旁偷偷放鞭炮的兴奋!

cert manager提供TOKEN服务的做法,就是自己创建一个ingress……也对,说得通。是我之前想太复杂了,以为它需要权限去改动我的ingress controller rules。

它自己另外生成的ingress服务是基于8089端口的,可是我们的SLB没有使用8089端口。

需要将它的NodePort改为32001就可以走SLB了。赶紧回头看看官网的配置文档,找到了这个

https://blog.img.crazyphper.com/2019/12/image-3.png

官方文档里的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 服务的 ServiceServiceAccountDeployment的部署。这个traefik服务放在了default这个namespace里,你可以修改为kube-system或者任何你自建的用于管理集中服务的namespace中。或者删掉namespace,在执行kubectl apply -f filename.yaml -n <namespace> 亦可。你可以认为是通用的,我会另起一份文件来实现具体业务代码的部署。

你会发现我将443注释了,因为没有用,也不会用到,留在这里是想提醒大家,不需要443 。 我之前在测试的时候,浪费了一点时间在这上面。

如果你的服务器可以直接请求80443,就不需要像我一样使用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使用了DeploymentService,最后使用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就会去申请证书并生效啦。

赞赏
没有标签
首页      程序开发      Linux      DevOps      SLB反代K8S时,使用Cert Manager遇到的棘手问题

团哥

文章作者

继续玩我的CODE,让别人说去。 低调,就是这么自信。

发表评论

textsms
account_circle
email

konakona

SLB反代K8S时,使用Cert Manager遇到的棘手问题
前言 在装好k8s和CICD自动化部署后,再来一个HTTPS证书自动签发是不是会更香? 在使用cert-manager实现HTTPS证书中遇到了一个天选之坑,排查到最后发现无解,因为官方不支持。 我最初就是…
扫描二维码继续阅读
2019-12-25