使用ACME CA为ASM网关签发证书

ACME(Automatic Certificate Management Environment)是一种用于自动化处理X.509数字证书签发请求的协议。通过ACME协议,可以自动验证证书申请者的域名所有权,然后为其签发证书。Let's Encrypt是一个非营利性的公共CA(证书颁发机构),支持ACME协议,它可以签发一般浏览器信任的证书。本文介绍如何使用cert-manager对接Let's Encrypt,为ASM网关签发浏览器信任的HTTPS证书。

前提条件

cert-manager的ACME说明

在使用cert-manager时,ACME Issuer组件负责向支持ACME协议的CA Server注册用户账户。创建ACME Issuer的过程中,cert-manager将为您生成一个私钥,这个私钥专门用于与ACME CA Server进行安全通信。公共CA(如Let's Encrypt)颁发的证书通常被客户端(如Web浏览器)所信任。这意味着当用户通过浏览器访问您的网站时,他们会自动信任该网站的SSL/TLS证书。公共CA颁发证书的主要目的是为了向浏览器证明当前服务器确实是该域名的合法服务提供者。为此,公共CA在颁发证书前需要核实申请证书的服务器实际上控制着该域名。更多关于ACME协议的定义和细节,请参见Automatic Certificate Management Environment

Solving Challenges

挑战(Challenges)是ACME协议中用于验证证书申请者对目标域名所有权的关键机制。在申请证书的过程中,ACME CA Server会要求客户端(证书申请者)完成特定的挑战,以确保只有域名的合法所有者能成功申请对应的证书,以此来提高网络安全性,避免域名冒充的风险。cert-manager支持两种主要的挑战类型:HTTP-01 Challenge和DNS-01 Challenge。

  • HTTP-01 Challenge依赖于服务器在特定HTTP URL上公开由ACME客户端生成的验证密钥。该密钥应位于一个可公网访问的URL中,其路径需包含待申请证书的域名。当ACME CA Server能按约定路径从互联网上成功检索到该密钥,即认为申请者对目标域名具备有效控制权。为了简化这一过程,当设置了HTTP-01Challenge,cert-manager会自动调整集群的Ingress设置,将指向该验证密钥URL的请求导向一个专门设立的小型Web服务。该服务承载着所需的验证密钥,用于响应ACME服务器发起的挑战验证。

  • DNS-01 Challenge是通过在DNS系统中发布一条包含特定计算得出密钥的TXT记录来完成验证。当这条TXT记录在全球DNS缓存中传播生效后,ACME CA Server可通过标准DNS查询来检索该密钥,从而确认申请者对指定域名的实际所有权。在此过程中,具备适当权限的cert-manager会自动为所使用的DNS服务商生成并提交所需TXT记录,以满足DNS-01 Challenge的要求。

说明

实际场景中,您需要确认您当前的证书颁发机构是否支持ACME协议。如果支持,则ASM网关可以通过cert-manager自动从CA处获取证书。例如,Sectigo目前已经支持了ACME协议,其原理与本文所述类似,详情请参见Overview

步骤一:准备公网域名

要使用Let's Encrypt为域名签发证书,首先需要有一个公网域名,并且需要您将公网域名指向要使用的ASM网关。具体流程,请参见您的DNS提供商的文档。如果您使用的是阿里云提供的DNS服务,请参见添加解析记录。关于Let's Encrypt的说明,请参见Getting Started

步骤二:创建对接Let's Encrypt的Issuer资源

  1. 使用数据面集群的KubeConfig,创建如下资源。

    apiVersion: cert-manager.io/v1
    kind: Issuer
    metadata:
      name: letsencrypt-prod-issuer
      namespace: istio-system
    spec:
      acme:
        email: 'te**@mail.com'    # 这个不是必填字段,但是建议填写。ACME Server可能通过该邮箱向您发送与证书相关的重要通知。
        privateKeySecretRef:
          name: letsencrypt-prod
        server: https://acme-v02.api.letsencrypt.org/directory
        solvers:
        - http01:
            ingress:
              ingressClassName: istio

    上述Issuer资源指定了一个http01类型的Solver,使用Ingress API,ingressClassNameistio。以下步骤将解释该Solver如何生效。

    说明

    cert-manager支持使用Ingress API和Gateway API两种类型的Solver。ASM也支持这两种API。本示例使用Ingress API。

  2. 等待Issuer就绪,使用以下命令,查看Issuer状态。

    kubectl -n istio-system get issuer letsencrypt-prod-issuer

    预期输出:

    NAME                      READY   AGE
    letsencrypt-prod-issuer   True    8m3s

步骤三:为ASM网关签发证书

  1. 使用数据面集群的KubeConfig,创建如下资源。

    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: istio-ingressgateway-certs
      namespace: istio-system
    spec:
      dnsNames:
      - ${测试域名}    # test.com
      issuerRef:
        group: cert-manager.io
        kind: Issuer
        name: letsencrypt-prod-issuer
      secretName: istio-ingressgateway-certs
  2. 等待Certificate就绪,使用以下命令,查看Certificate状态。

    kubectl -n istio-system get certificate istio-ingressgateway-certs

    预期输出:

    NAME                         READY   SECRET                       AGE
    istio-ingressgateway-certs   True    istio-ingressgateway-certs   59m

证书签发过程解析

创建Certificate资源之后,cert-manager会使用创建的Issuer为这个域名签发证书。这个过程中,配置的Solver就会生效。

Let's Encrypt将会使用HTTP-01 Challenge来确认:当前Server是这个域名的所有者。为此,Let's Encrypt会发送一个HTTP请求到这个域名,然后它需要获取到一个合法的返回值,才可以完成验证。在本示例中,网关上只有httpbin的路由规则,并没有任何和Challenge有关的配置。Let's Encrypt是否真的发送了Challenge请求?请求如何被响应?

您可以执行以下命令,查看网关日志,判断Let's Encrypt是否真的发送了Challenge请求。

kubectl -n istio-system logs ${网关Pod名称} | grep letsencrypt | tail -1

展开查看预期输出

{

    "authority_for": "xxxxxxx",

    "bytes_received": "0",

    "bytes_sent": "87",

    "downstream_local_address": "xx.xx.xx.xx:80",

    "downstream_remote_address": "xx.xx.xx.xx:57101",

    "duration": "0",

    "istio_policy_status": "-",

    "method": "GET",

    "path": "/.well-known/acme-challenge/JfKvfdSNmkR7UqmCQU0OSkJC3EsnP4ZUiCc28OLLLxA",

    "protocol": "HTTP/1.1",

    "request_id": "e6806d08-0469-4383-be8e-4d7506b39ec5",

    "requested_server_name": "-",

    "response_code": "200",

    "response_flags": "-",

    "route_name": "-",

    "start_time": "2024-04-08T12:04:06.153Z",

    "trace_id": "-",

    "upstream_cluster": "outbound|8089||cm-acme-http-solver-c4ch9.istio-system.svc.cluster.local",

    "upstream_host": "xx.xx.xx.xx:8089",

    "upstream_local_address": "xx.xx.xx.xx:55886",

    "upstream_response_time": "0",

    "upstream_service_time": "0",

    "upstream_transport_failure_reason": "-",

    "user_agent": "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)",

    "x_forwarded_for": "xx.xx.xx.xx"

}

可以看到网关上确实存在Let's Encrypt发出的请求,并且该请求被正常返回。在Issuer中配置了ingressClassNameistio。cert-manager会自动创建一个ingressClassNameistio的Ingress资源,将Challenge请求转发给cert-manager对应的Solver来完成验证。

在Certificate就绪之后,使用kubectl -n istio-system get ingress无法看到相关的Ingress资源,是因为cert-manager在完成证书签发后,会自动删除Solver相关的资源,包括Ingress、Service、Deployment等资源。

步骤四:验证Let's Encrypt签发的证书

  1. 创建如下网关规则,在网关的443端口上配置步骤三生成的证书。具体操作,请参见管理网关规则

    apiVersion: networking.istio.io/v1beta1
    kind: Gateway
    metadata:
      name: httpbin-https
      namespace: default
    spec:
      selector:
        istio: ingressgateway
      servers:
      - hosts:
        - ${测试域名}
        port:
          name: https
          number: 443
          protocol: HTTPS
        tls:
          credentialName: istio-ingressgateway-certs
          mode: SIMPLE
  2. 修改原有的httpbin-vs虚拟服务为以下内容。具体操作,请参见管理虚拟服务

    apiVersion: networking.istio.io/v1beta1
    kind: VirtualService
    metadata:
      name: httpbin-vs
      namespace: default
    spec:
      gateways:
        - httpbin
        - httpbin-https  # 新增这一行。
      hosts:
        - '*'
      http:
        - name: test
          route:
            - destination:
                host: httpbin.default.svc.cluster.local
                port:
                  number: 8000
  3. 在浏览器中访问https://${测试域名}

    可以看到如下形式的浏览器地址栏,单击image图标,显示连接安全,表明浏览器已经信任您的证书。

    image