阿里云, kubernetes, 技术, HTTPS, 运维, Docker

k8s 上利用 cert-manager 自动签发 TLS 证书

2020年02月27日,天气晴

很多博主的 https 证书经常容易忘记更新,虽说证书过期前都会有邮件提醒,但是万一确实忙得没时间去处理,忘记了,就会出现证书过期的情况了。

之前在服务器上自己搭博客服务的时候,用 Let's Encrypt 来自动创建并续签证书,确实省了不少事。

在我的博客部署到 k8s 之后,就一直用的一年一签的免费证书,每年更新一次,也不算特别麻烦,但是总归不够高端,我又怀念起了 Let's Encrypt

Let's Encrypt 是个好东西,k8s 也是个好东西,两个好东西怎么结合呢?搜寻了一番确实有方案,经过几天的尝试,终于弄好了。花了几天是因为第一天因为有个粗心导致的问题,导致搞了好久没成功,休息了几天再次尝试,才找到问题。

有关 k8s 的基础知识,这里不做赘述,网上教程很多,这里假设大家对 k8s 都有一定了解。

安装 cert-manager

安装 helm 到本地

$ brew install helm

添加仓库和命名空间

$ kubectl create namespace cert-manager # 创建 cert-manager 命名空间
$ kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true # 标记 cert-manager 命名空间以禁用资源验证
$ kubectl apply --validate=false -f https://raw.githubusercontent.com/jetstack/cert-manager/v0.13.1/deploy/manifests/00-crds.yaml # 安装 CustomResourceDefinition 资源
$ helm repo add jetstack https://charts.jetstack.io # 添加 Jetstack Helm repository
$ helm repo update # 更新本地 Helm chart repository

安装 cert-manager

$ helm install cert-manager --namespace cert-manager --version v0.13.1 jetstack/cert-manager

查看 cert-manager 安装情况

$ kubectl get pods --namespace cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-6cff8dc7b9-8vxws              1/1     Running   0          4d10h
cert-manager-cainjector-795c46858f-txczb   1/1     Running   0          4d10h
cert-manager-webhook-5dfc77cd74-skgsv      1/1     Running   0          4d10h

创建 ClusterIssuer

我们需要创建一个签发机构,cert-manager 提供了IssuerClusterIssuer 两种类型的签发机构,Issuer 只能用来签发自己所在命名空间下的证书,ClusterIssuer 可以签发任意命名空间下的证书,我这里用 ClusterIssuer 为例,创建 letsencrypt-prod.yaml 文件:

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  labels:
    name: letsencrypt-prod
  name: letsencrypt-prod # 自定义的签发机构名称,后面会引用
spec:
  acme:
    email: yourname@youremail.com # 你的邮箱,证书快过期的时候会邮件提醒,不过我们可以设置自动续期
    solvers:
    - http01:
        ingress:
          class: nginx
    privateKeySecretRef:
      name: letsencrypt-prod # 指示此签发机构的私钥将要存储到哪个 Secret 对象中
    server: https://acme-v02.api.letsencrypt.org/directory # acme 协议的服务端,我们用 Let's Encrypt

应用 yaml

$ kubectl create -f letsencrypt-prod.yaml  

查看状态

$ kubectl get clusterissuer
NAME               READY   AGE
letsencrypt-prod   True    51s

手动签发证书

手动签发证书测试,创建 test-monkeyrun-net-cert.yaml 文件

apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
  name: test-monkeyrun-net-cert
  namespace: test
spec:
  secretName: tls-test-monkeyrun-net # 证书保存的 secret 名
  duration: 2160h # 90d
  renewBefore: 360h # 15d
  organization:
  - jetstack
  isCA: false
  keySize: 2048
  keyAlgorithm: rsa
  keyEncoding: pkcs1
  dnsNames:
  - test.monkeyrun.net
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
    group: cert-manager.io

应用 yaml

$ kubectl apply -f test-monkeyrun-net-cert.yaml

检查是否生成证书文件

$ kubectl get certificate -n test
NAME                      READY   SECRET                   AGE
test-monkeyrun-net-cert   True    test-monkeyrun-net-tls   99m

将该证书配置到 test.monkeyrun.netingress 上,测试 https 访问,成功。

自动签发证书

创建 test-nginx.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: test-nginx
  namespace: test
spec:
  replicas: 1
  template:
    metadata:
      labels:
        run: test-nginx
    spec:
      containers:
      - name: test-nginx
        image: nginx
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: test-nginx
  namespace: test
  labels:
    app: test-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
    name: http
  selector:
    run: test-nginx
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: test-nginx
  namespace: test
  annotations:
    kubernetes.io/ingress.class: "nginx"
    kubernetes.io/tls-acme: "true"
    certmanager.k8s.io/cluster-issuer: "letsencrypt-prod"
spec:
  rules:
  - host: test.monkeyrun.net
    http:
      paths:
      - backend:
          serviceName: test-nginx
          servicePort: 80
        path: /
  tls:
  - secretName: tls-test-monkeyrun-net
    hosts:
    - test.monkeyrun.net

删除之前手动创建的 DeploymentServiceIngressSecret 后, 应用 yaml 来自动创建

$ kubectl apply -f test-nginx.yaml

打开 https://test.monkeyrun.net 测试,成功!

通过 DNS 验证域名

刚才通过 http01 的方式验证域名会有个问题,对于已经部署上线的项目,没办法去验证,所以可以通过 dns 的方式来验证。

经过搜寻,找到了几篇文章,都是利用 kevinniu666 这位仁兄基于 jetstack/cert-manager-webhook-example 改成 alidns 的版本来搞的,不过尝试了下,他这里面 cert-manager 版本太老已经跑不起来了,从 GitHub 的 forks 树里面找到了最新的一个 fork,colprog/cert0manager-webhooks-alidns,尝试了下,也不行,他应该是改了镜像,但是不可用了。重新尝试了下上一代 fork pangzineng/cert-manager-webhook-alidns,可用。

$ git clone https://github.com/pangzineng/cert-manager-webhook-alidns.git
$ cd cert-manager-webhook-alidns
$ helm install cert-manager-webhook-alidns --namespace=cert-manager ./deploy/webhook-alidns

创建 alidns AccessKey Id 和 Secret

$ kubectl -n cert-manager create secret generic alidns-access-key-id --from-literal=accessKeyId='xxxxxxx'
$ kubectl -n cert-manager create secret generic alidns-access-key-secret --from-literal=accessKeySecret='xxxxxxx'

修改我们之前创建的 letsencrypt-prod.yaml

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  labels:
    name: letsencrypt-prod
  name: letsencrypt-prod # 自定义的签发机构名称,后面会引用
spec:
  acme:
    email: yourname@youremail.com # 你的邮箱,证书快过期的时候会邮件提醒,不过我们可以设置自动续期
    solvers:
    - dns01:
        webhook:
          config:
            accessKeyIdRef:
              key: accessKeyId
              name: alidns-access-key-id
            accessKeySecretRef:
              key: accessKeySecret
              name: alidns-access-key-secret
            regionId: 'cn-hangzhou'
            ttl: 600
          groupName: certmanager.webhook.alidns
          solverName: alidns
    privateKeySecretRef:
      name: letsencrypt-prod # 指示此签发机构的私钥将要存储到哪个 Secret 对象中
    server: https://acme-v02.api.letsencrypt.org/directory # acme 协议的服务端,我们用 Let's Encrypt

应用 yaml

$ kubectl create -f letsencrypt-prod.yaml  

查看状态

$ kubectl get clusterissuer
NAME               READY   AGE
letsencrypt-prod   True    51s

重新手动签发证书,验证,成功!

PS:需要注意的是,从 http01 认证修改到 dns01 认证后,有个坑,会一直失败,查看 cert-manager 的 Pod 日志,会发现如下错误:

cert-manager/controller/orders "msg"="Failed to determine the list of Challenge resources needed for the Order" "error"="no configured challenge solvers can be used for this challenge" "resource_kind"="Order" "resource_name"="xxx"

研究了半天都没成功,后来在 GitHub 上找到了这个 Issue,按照 demisx 这位仁兄的建议,把所有和 cert-manager 相关的东西全部删除重新用 dns01 的方式部署一遍就 OK 了。

参考链接

Author image

关于 Bean Deng