APISIX SSL 证书与 SNI 配置全指南:从单域名、通配符到多 CA 证书管理
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 文档。