APISIX SSL 证书与 SNI 配置全指南:从单域名、通配符到多 CA 证书管理

原创2026-09-20 17:01:461,456 阅读
文章标签:API网关后端微服务云原生LLM 网关

APISIX SSL 证书与 SNI 配置全指南:从单域名、通配符到多 CA 证书管理

APISIX 是云原生 API 网关,其 HTTPS 流量通过 TLS 扩展 SNI(Server Name Indication,服务器名称指示) 动态加载多个 SSL 证书,使单个网关实例能够为不同域名提供各自的证书。本文以官方文档 docs/en/latest/certificate.md 为主体,完整讲解单 SNI、通配符 SNI、多域名、单域名多证书(ECC 与 RSA 并存)以及多 CA 证书(CA bundle)的配置方法,并结合仓库源码说明其校验逻辑与底层原理。读完本文,你将能够独立完成 APISIX 上 HTTPS 证书的创建、路由绑定与双向 TLS(mTLS)场景下的多 CA 配置。

SNI 与 SSL 对象基础

APISIX 支持通过 TLS 扩展 SNI 加载多个 SSL 证书:客户端在 TLS 握手时发送期望访问的主机名,APISIX 据此从配置中心(etcd)中匹配对应的 SSL 对象并返回相应证书。

在 APISIX 中,一个 ssl 对象的核心字段如下:

字段 类型 说明
cert string PEM 编码的 SSL 公钥证书
key string PEM 编码的 SSL 私钥
snis array 与此证书关联的主机名(SNI)列表;设置该属性时,证书必须关联有效的私钥
sni string 单主机名形式的 SNI(与 snis 二选一)
certs array PEM 编码的证书数组(用于单域名多证书场景)
keys array PEM 编码的私钥数组(与 certs 按索引一一配对)

从源码看,ssl 对象的 schema 定义在 apisix/schema_def.lua#L892-L985:

  • snis 为数组类型,minItems = 1,每个元素需满足主机名格式 host_def_pat;
  • cert、key 均为证书/私钥 scheme;
  • 对象级条件校验(if/then/else)规定:type 为 server(默认值)时,必须满足 {sni, key, cert} 或 {snis, key, cert} 两者之一;type 为 client 时只需 {key, cert}。也就是说,cert 与 key 是必填项,sni/snis 至少配置其一。

Admin API 对 ssl 资源的操作入口在 apisix/admin/ssl.lua,它基于通用资源模块 resource.new 注册了名为 ssls 的资源(kind 为 ssl),并通过 apisix_ssl.check_ssl_conf 对提交的配置做完整性校验。

准备工作:获取 Admin API 密钥

下文所有示例均通过 Admin API(默认监听 127.0.0.1:9180)写入配置。先从 conf/config.yaml 中取出 admin_key 并存入环境变量:

admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')

仓库自带的测试证书位于 t/certs/apisix.crt 与 t/certs/apisix.key(自签名证书,CN 为 test.com),以及 ECC 证书 t/certs/apisix_ecc.crt 与 t/certs/apisix_ecc.key,可用于快速验证下文示例。

配置单一域名证书(Single SNI)

最常见的场景是:一张 SSL 证书只包含一个域名。此时只需创建一个 ssl 对象,通过 snis 指定该域名即可。

第一步:创建 SSL 对象

curl http://127.0.0.1:9180/apisix/admin/ssls/1 \
-H "X-API-KEY: $admin_key" -X PUT -d '
{
     "cert" : "'"$(cat t/certs/apisix.crt)"'",
     "key": "'"$(cat t/certs/apisix.key)"'",
     "snis": ["test.com"]
}'

第二步:创建路由对象

curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -i -d '
{
    "uri": "/get",
    "hosts": ["test.com"],
    "methods": ["GET"],
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "httpbin.org": 1
        }
    }
}'

注意路由的 hosts 需要与 SSL 对象的 snis 保持一致,这样当客户端以 test.com 为 SNI 发起 HTTPS 请求时,APISIX 既能完成 TLS 握手,也能将请求匹配到该路由。

第三步:发送请求验证

APISIX 的 HTTPS 监听端口默认是 9443(见 conf/config.yaml.example#L135-L151 中 apisix.ssl.listen)。使用 curl --resolve 将 test.com 解析到本机并跳过证书校验:

curl --resolve 'test.com:9443:127.0.0.1' https://test.com:9443/get -k -vvv

* Added test.com:9443:127.0.0.1 to DNS cache
* About to connect() to test.com port 9443 (#0)
*   Trying 127.0.0.1...
* Connected to test.com (127.0.0.1) port 9443 (#0)
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*   subject: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com
*   start date: Jun 24 22:18:05 2019 GMT
*   expire date: May 31 22:18:05 2119 GMT
*   issuer: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com
*   SSL certificate verify result: self-signed certificate (18), continuing anyway.
> GET /get HTTP/2
> Host: test.com:9443
> user-agent: curl/7.81.0
> accept: */*

输出显示 TLS 握手完成、服务端返回了 CN 为 test.com 的证书,且通过 ALPN 协商到 HTTP/2(h2),说明证书加载成功。

配置通配符域名证书(Wildcard SNI)

一张证书也可以同时覆盖某个通配符域名下的所有子域,例如 *.test.com 对 www.test.com、mail.test.com 等均有效。

第一步:创建 SSL 对象,将 snis 设置为 ["*.test.com"]:

curl http://127.0.0.1:9180/apisix/admin/ssls/1 \
 -H "X-API-KEY: $admin_key" -X PUT -d '
 {
      "cert" : "'"$(cat t/certs/apisix.crt)"'",
      "key": "'"$(cat t/certs/apisix.key)"'",
      "snis": ["*.test.com"]
 }'

第二步:创建路由对象,hosts 同样使用通配符:

curl http://127.0.0.1:9180/apisix/admin/routes/1 -H "X-API-KEY: $admin_key" -X PUT -i -d '
{
    "uri": "/get",
    "hosts": ["*.test.com"],
    "methods": ["GET"],
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "httpbin.org": 1
        }
    }
}'

第三步:发送请求验证,以 www.test.com 为例:

curl --resolve 'www.test.com:9443:127.0.0.1' https://www.test.com:9443/get -k -vvv

* Added www.test.com:9443:127.0.0.1 to DNS cache
* Hostname www.test.com was found in DNS cache
*   Trying 127.0.0.1:9443...
* Connected to www.test.com (127.0.0.1) port 9443 (#0)
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com
*  start date: Jun 24 22:18:05 2019 GMT
*  expire date: May 31 22:18:05 2119 GMT
*  issuer: C=CN; ST=GuangDong; L=ZhuHai; O=iresty; CN=test.com
*  SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET /get HTTP/2
> Host: www.test.com:9443
> user-agent: curl/7.74.0
> accept: */*

仓库测试 t/admin/ssl.t#L202-L232 中的 TEST 7: wildcard host name 正是对这一场景的验证;TEST 8: store sni in snis(t/admin/ssl.t#L235)则验证了 snis 数组可以同时存放通配符与普通域名,如 {"*.foo.com", "bar.com"}。

一张证书承载多个域名(Multiple Domain)

如果一张证书本身包含多个域名(例如同时签发 www.test.com 与 mail.test.com),只需把它们全部加入 snis 数组:

{
    "snis": ["www.test.com", "mail.test.com"]
}

与单域名示例相比,区别仅在于 snis 数组元素个数;路由侧可按需为每个域名分别创建路由,或同样使用 hosts 数组匹配多个主机名。

单域名配置多张证书(ECC 与 RSA 并存)

生产环境中,为了兼容不同客户端,常常需要同一个域名同时支持 ECC(椭圆曲线) 与 RSA 两种密钥交换算法的证书。APISIX 通过 certs 与 keys 两个数组支持这一需求:

  • certs:PEM 编码的证书数组;
  • keys:PEM 编码的私钥数组;
  • APISIX 会按 相同下标 将证书与私钥配对成一个 SSL 密钥对,因此 certs 与 keys 的长度必须一致。

配置时,第一张证书和私钥仍然放在 cert 与 key 中,额外的证书和私钥放入 certs 与 keys:

{
    "cert": "<RSA 证书 PEM>",
    "key": "<RSA 私钥 PEM>",
    "certs": ["<ECC 证书 PEM>"],
    "keys": ["<ECC 私钥 PEM>"],
    "snis": ["test.com"]
}

仓库测试 t/admin/ssl.t#L353-L392 的 TEST 12: set ssl with multicerts(id: 1) 展示了用仓库内置的 ECC 证书(t/certs/apisix_ecc.crt、t/certs/apisix_ecc.key)与 RSA 证书组合配置的完整过程。

长度不一致会怎样? 从源码看,apisix/ssl.lua#L184-L236 的 check_ssl_conf 会显式比较 certs 与 keys 的数量,不一致时返回错误 mismatched number of certs and keys,拒绝写入配置。该函数还会逐个校验每一对 certs<a href="https://link.gitcode.com/i/2219fb10b82c1e69be31eef58b489c0b" target="_blank">i]/keys[i] 是否为合法的 PEM 证书与私钥,任一配对解析失败都会返回 failed to handle cert-key pair[i] 错误。对应测试见 [t/admin/ssl.t#L394-L430 的 TEST 13: mismatched certs and keys。

证书与私钥的校验与缓存机制

在 apisix/ssl.lua 中可以看到更底层的实现细节:

  • validate(cert, key)(apisix/ssl.lua#L108-L134)使用 ngx_ssl.parse_pem_cert 与 ngx_ssl.parse_pem_priv_key 解析 PEM 内容,解析失败即拒绝配置;key 为空时允许仅校验证书(用于客户端 CA 场景);
  • 解析后的证书与私钥分别缓存在 cert_cache 与 pkey_cache(LRU 缓存,TTL 为 3600 秒,容量 1024),避免每次握手重复解析 PEM;
  • 私钥还支持 AES 加密存储:aes_encrypt_pkey 对 PEM 格式的私钥调用 data_encryption.encrypt 加密后再写入 etcd,读取时由 aes_decrypt_pkey 解密(详见 apisix/ssl.lua#L87-L105),配合 conf/config.yaml.example#L163-L169 中 deployment.data_encryption 的 keyring 配置使用,可防止私钥明文落盘。

补充:握手时的 SNI 提取与回退

apisix/ssl.lua#L42-L71 的 server_name 函数说明了 SNI 的提取路径:普通 HTTPS 场景从 ngx_ssl.server_name() 获取;TLS 透传(stream)场景下 worker 不执行握手,改从 ssl_preread_server_name 读取(preread 模式);还支持基于 ClientHello 解析(clienthello 模式)。获取到的 SNI 会去除末尾点号并转为小写。若客户端未携带 SNI,则回退到 conf/config.yaml.example#L150-L151 中 apisix.ssl.fallback_sni 配置的默认域名。

配置多个 CA 证书(CA Bundle)

APISIX 在多处需要使用 CA 证书,例如:Protect Admin API、etcd with mTLS 以及部署模式等场景。这些场景通过 ssl_trusted_certificate 或 trusted_ca_cert 指定 CA 证书,而它们最终都会被转换为 OpenResty 的 lua_ssl_trusted_certificate 指令。

痛点:如果不同的通信链路(如 Admin API 与 etcd)需要不同的 CA 证书,分开配置会产生多个 lua_ssl_trusted_certificate 位置并互相覆盖。解决方案是:把多个 CA 证书打包进一个 CA bundle 文件,在需要配置 CA 的地方统一指向该文件。

以下用一个完整案例说明:客户端与 APISIX Admin API、APISIX 与 etcd 之间均启用 mTLS,分别由 foo_ca.crt 与 bar_ca.crt 两个 CA 签发证书。

参与配置的证书清单

配置项 类型 说明
foo_ca.crt CA cert 为客户端与 APISIX Admin API 的 mTLS 通信签发次级证书
foo_client.crt cert 由 foo_ca.crt 签发,客户端访问 Admin API 时用于证明身份
foo_client.key key 由 foo_ca.crt 签发,客户端访问 Admin API 所需的私钥
foo_server.crt cert 由 foo_ca.crt 签发,APISIX 侧使用,对应 admin_api_mtls.admin_ssl_cert 配置项
foo_server.key key 由 foo_ca.crt 签发,APISIX 侧使用,对应 admin_api_mtls.admin_ssl_cert_key 配置项
admin.apisix.dev domain 签发 foo_server.crt 时使用的 Common Name,客户端通过它访问 Admin API
bar_ca.crt CA cert 为 APISIX 与 etcd 的 mTLS 通信签发次级证书
bar_etcd.crt cert 由 bar_ca.crt 签发,etcd 侧使用,对应 etcd 启动参数 -cert-file
bar_etcd.key key 由 bar_ca.crt 签发,etcd 侧使用,对应 etcd 启动参数 --key-file
bar_apisix.crt cert 由 bar_ca.crt 签发,APISIX 侧使用,对应 etcd.tls.cert 配置项
bar_apisix.key key 由 bar_ca.crt 签发,APISIX 侧使用,对应 etcd.tls.key 配置项
etcd.cluster.dev SNI 签发 bar_etcd.crt 时使用的 Common Name,APISIX 与 etcd 做 mTLS 时作为 SNI,对应 etcd.tls.sni 配置项
apisix.ca-bundle CA bundle 由 foo_ca.crt 与 bar_ca.crt 合并而成,替代二者使用

第一步:创建 CA bundle 文件

cat /path/to/foo_ca.crt /path/to/bar_ca.crt > apisix.ca-bundle

第二步:启动启用了客户端认证的 etcd 集群

编写名为 Procfile-single-enable-mtls 的 goreman 配置(goreman 可通过 go get github.com/mattn/goreman 安装),启动三个节点的 etcd 集群,均开启 --client-cert-auth 并指定 --trusted-ca-file 为刚生成的 CA bundle:

# Use goreman to run `go get github.com/mattn/goreman`
etcd1: etcd --name infra1 --listen-client-urls https://127.0.0.1:12379 --advertise-client-urls https://127.0.0.1:12379 --listen-peer-urls http://127.0.0.1:12380 --initial-advertise-peer-urls http://127.0.0.1:12380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle
etcd2: etcd --name infra2 --listen-client-urls https://127.0.0.1:22379 --advertise-client-urls https://127.0.0.1:22379 --listen-peer-urls http://127.0.0.1:22380 --initial-advertise-peer-urls http://127.0.0.1:22380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle
etcd3: etcd --name infra3 --listen-client-urls https://127.0.0.1:32379 --advertise-client-urls https://127.0.0.1:32379 --listen-peer-urls http://127.0.0.1:32380 --initial-advertise-peer-urls http://127.0.0.1:32380 --initial-cluster-token etcd-cluster-1 --initial-cluster 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --cert-file /path/to/bar_etcd.crt --key-file /path/to/bar_etcd.key --client-cert-auth --trusted-ca-file /path/to/apisix.ca-bundle

使用 goreman 后台启动集群:

goreman -f Procfile-single-enable-mtls start > goreman.log 2>&1 &

第三步:更新 config.yaml

在 conf/config.yaml 中完成三类配置:Admin API 的 HTTPS 与 mTLS、APISIX 信任的 CA bundle、以及 etcd 的 mTLS 客户端证书与 SNI:

deployment:
  admin:
    admin_key
      - name: admin
        key: your-admin-key
        role: admin
    admin_listen:
      ip: 127.0.0.1
      port: 9180
    https_admin: true
    admin_api_mtls:
      admin_ssl_ca_cert: /path/to/apisix.ca-bundle
      admin_ssl_cert: /path/to/foo_server.crt
      admin_ssl_cert_key: /path/to/foo_server.key

apisix:
  ssl:
    ssl_trusted_certificate: /path/to/apisix.ca-bundle

deployment:
  role: traditional
  role_traditional:
    config_provider: etcd
  etcd:
    host:
      - "https://127.0.0.1:12379"
      - "https://127.0.0.1:22379"
      - "https://127.0.0.1:32379"
    tls:
      cert: /path/to/bar_apisix.crt
      key: /path/to/bar_apisix.key
      sni: etcd.cluster.dev

各配置项在 conf/config.yaml.example 中均有对应注释可参考:

  • deployment.admin.admin_api_mtls(conf/config.yaml.example#L815-L818):https_admin 为 true 时生效,admin_ssl_cert/admin_ssl_cert_key 指定 Admin API 的服务端证书与私钥,admin_ssl_ca_cert 指定用于验证客户端证书的 CA;
  • apisix.ssl.ssl_trusted_certificate(conf/config.yaml.example#L143):指定信任的 CA 证书文件路径,默认值为 system;
  • deployment.etcd.tls(conf/config.yaml.example#L835-L839):cert/key 为 APISIX 作为 etcd 客户端的证书与私钥,sni 指定 TLS 请求的 SNI(不设置时使用 URL 中的主机名)。

第四步:测试 APISIX Admin API

启动 APISIX,若启动成功且 logs/error.log 无异常输出,说明 APISIX 与 etcd 之间的 mTLS 通信正常。

使用 curl 模拟客户端,以 mTLS 方式访问 Admin API 并创建路由:

curl -vvv \
    --resolve 'admin.apisix.dev:9180:127.0.0.1' https://admin.apisix.dev:9180/apisix/admin/routes/1 \
    --cert /path/to/foo_client.crt \
    --key /path/to/foo_client.key \
    --cacert /path/to/apisix.ca-bundle \
    -H "X-API-KEY: $admin_key" -X PUT -i -d '
{
    "uri": "/get",
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "httpbin.org:80": 1
        }
    }
}'

curl 与 Admin API 之间 mTLS 协商成功时,会输出完整的 TLSv1.3 握手过程,其中包含双向证书交换的关键步骤(Request CERT 之后客户端回传 Certificate 与 CERT verify):

* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384

第五步:验证 APISIX 代理

通过普通 HTTP 请求验证 APISIX 已能正常代理流量到上游:

curl http://127.0.0.1:9080/get -i

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 298
Connection: keep-alive
Date: Tue, 26 Jul 2022 16:31:00 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Server: APISIX/2.14.1

...

APISIX 将请求代理到上游 httpbin.org 的 /get 路径并返回 HTTP/1.1 200 OK,证明使用 CA bundle 替代单个 CA 证书的整条链路工作正常。

小结

APISIX 的证书管理围绕 SNI 展开,核心是 ssl 对象的 cert/key/snis 三元组,并可通过 certs/keys 数组支持同域名的多算法证书。对于 mTLS 场景下的多 CA 需求,将不同 CA 合并为单个 CA bundle 文件并统一引用,即可避免 lua_ssl_trusted_certificate 指令被多次覆盖的问题。相关实现细节(schema 校验、证书解析缓存、私钥加密、SNI 提取与回退)可进一步阅读 apisix/ssl.lua、apisix/admin/ssl.lua 与 apisix/schema_def.lua,测试用例可参考 t/admin/ssl.t,mTLS 的更多应用场景见 mTLS 文档。

登录后查看全文
apisix