APISIX SSL 协议版本配置指南:按 SNI 动态控制 TLS 协议
【免费下载链接】apisixThe Cloud-Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/ap/apisix
本文以 Apache APISIX(云原生 API 网关)的 SSL/TLS 协议版本配置为主题,系统讲解如何在config.yaml中做全局静态配置,以及如何通过 SSL 资源(Admin API)为每个 SNI(Server Name Indication)域名动态指定TLSv1.1 / TLSv1.2 / TLSv1.3协议版本。读完本文,你将掌握静态与动态两种配置方式的完整写法、优先级规则、底层实现原理,并能用 curl 快速验证配置是否生效,从而在兼容老旧客户端与保障 API 安全之间取得平衡。
概述:APISIX 的 TLS 协议支持方式
APISIX支持 TLS 协议,还支持动态的为每一个 SNI 指定不同的 TLS 协议版本。这意味着:
- 在全局层面,可以通过静态配置(
config.yaml中apisix.ssl.ssl_protocols)设定网关默认支持的 TLS 版本集合; - 在域名层面,可以通过 Admin API 为 SSL 资源设置
ssl_protocols字段,按 SNI 精确控制每个域名的 TLS 版本。
为了安全考虑,APISIX 默认使用的加密套件不支持 TLSv1.1 以及更低的版本。如果你需要启用 TLSv1.1 协议,请在 config.yaml 的配置项 apisix.ssl.ssl_ciphers 增加 TLSv1.1 协议所支持的加密套件。
需要特别说明的是,APISIX 的 SSL 资源 schema 只允许TLSv1.1、TLSv1.2、TLSv1.3三种协议版本(见下文源码分析),TLSv1.0及更早版本不会被接受,这从根上保证了默认安全基线。
静态配置:config.yaml 中的 ssl_protocols
静态配置中 config.yaml 的ssl_protocols参数会作用于 APISIX 全局,但是不能动态修改,仅当匹配的 SSL 资源未设置ssl_protocols,静态配置才会生效。
apisix: ssl: ssl_protocols: TLSv1.2 TLSv1.3 # default TLSv1.2 TLSv1.3从配置默认值看,APISIX 出厂即只开放TLSv1.2 TLSv1.3。在 conf/config.yaml.example 中同样可以看到该默认配置项(ssl_protocols: TLSv1.2 TLSv1.3 # TLS versions supported.),而 apisix/cli/config.lua 中内置的默认值也是ssl_protocols = "TLSv1.2 TLSv1.3"。
该配置属于静态配置,最终会被渲染进 nginx.conf。在 apisix/cli/ngx_tpl.lua 的 nginx 配置模板中可以看到:
ssl_protocols {* ssl.ssl_protocols *}; ssl_ciphers {* ssl.ssl_ciphers *};即 APISIX 启动时用apisix.ssl.ssl_protocols的值替换模板占位符,生成真实的ssl_protocols指令写入 nginx 配置。因此修改该值后需要重启 APISIX(重新生成并加载 nginx.conf)才能生效。
动态配置:SSL 资源中的 ssl_protocols
使用 ssl 资源中ssl_protocols字段动态的为每一个 SNI 指定不同的 TLS 协议版本。
指定 test.com 域名使用 TLSv1.2 TLSv1.3 协议版本:
{ "cert": "$cert", "key": "$key", "snis": ["test.com"], "ssl_protocols": [ "TLSv1.2", "TLSv1.3" ] }与静态配置不同,动态配置通过 Admin API 实时下发,无需重启即可生效,且可以做到按 SNI 细分:同一个网关实例上,test.com走 TLSv1.2/1.3,test2.com走 TLSv1.3,互不干扰。
动态配置的 schema 校验
从源码看,SSL 资源的ssl_protocols字段定义在 apisix/schema_def.lua:
ssl_protocols = { description = "set ssl protocols", type = "array", maxItems = 3, uniqueItems = true, items = { enum = {"TLSv1.1", "TLSv1.2", "TLSv1.3"} }, },这段 schema 意味着:
- 取值只能是
TLSv1.1、TLSv1.2、TLSv1.3三者之一; - 数组元素不能重复(
uniqueItems = true); - 最多 3 个元素(
maxItems = 3)。
任何超出该枚举的值都会被 Admin API 直接拒绝。这一点有测试用例佐证:在 t/admin/ssl5.t 中,尝试为ssl_protocols设置TLSv1.0时,返回的校验错误为:
invalid configuration: property "ssl_protocols" validation failed: failed to validate item 1: matches none of the enum values动态配置的底层生效机制
动态配置之所以能够“按 SNI 生效”,是因为 APISIX 在 TLS 握手的ClientHello 阶段(ssl_certificate_by_lua_block)就会完成 SSL 资源的匹配与协议设置。核心调用链如下:
- apisix/init.lua 中通过
apisix_ssl.server_name(true)从 ClientHello 中提取 SNI; - 用提取到的 SNI 执行
router.router_ssl.match_and_set(api_ctx, true, sni),匹配出对应的 SSL 资源; - 调用
apisix_ssl.set_protocols_by_clienthello(ngx_ctx.matched_ssl.value.ssl_protocols)设置协议版本; - apisix/ssl.lua 中的
_M.set_protocols_by_clienthello最终调用 OpenResty 的ngx_ssl_client.set_protocols(ssl_protocols)完成底层设置:
function _M.set_protocols_by_clienthello(ssl_protocols) if ssl_protocols then return ngx_ssl_client.set_protocols(ssl_protocols) end return true end也就是说,ssl_protocols为空的 SSL 资源会返回true(不额外设置),从而回落到全局静态配置;只有显式设置了ssl_protocols的 SSL 资源,才会在握手早期覆盖默认协议集合。
注意事项:两种配置的优先级
- 动态配置优先级比静态配置更高,当 ssl 资源配置项
ssl_protocols不为空时,静态配置将会被覆盖。 - 静态配置作用于全局,需要重启 apisix 才能生效。
- 动态配置可细粒度的控制每个 SNI 的 TLS 协议版本,并且能够动态修改,相比于静态配置更加灵活。
测试用例 t/node/ssl-protocols.t 也验证了这一点:当 SSL 资源不设置ssl_protocols时,Admin API 返回的值中ssl_protocols为null,此时客户端用 TLSv1.1 / TLSv1.2 / TLSv1.3 访问均成功——因为测试环境在 config.yaml 中静态配置了ssl_protocols: TLSv1.1 TLSv1.2 TLSv1.3。
使用示例一:如何指定 TLSv1.1 协议
存在一些老旧的客户端,仍然采用较低级别的 TLSv1.1 协议版本,而新的产品则使用较高安全级别的 TLS 协议版本。如果让新产品支持 TLSv1.1 可能会带来一些安全隐患。为了保证 API 的安全性,我们需要在协议版本之间进行灵活转换。
例如:test.com是老旧客户端所使用的域名,需要将其配置为 TLSv1.1;而test2.com属于新产品,同时支持 TLSv1.2、TLSv1.3 协议。
第 1 步:config.yaml 配置(全局收紧,仅允许 TLSv1.3)。
apisix: ssl: ssl_protocols: TLSv1.3 # ssl_ciphers is for reference only ssl_ciphers: ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA注意:由于 APISIX 默认加密套件不支持 TLSv1.1,此处通过
ssl_ciphers补充了 TLSv1.1 所需的套件(如ECDHE-RSA-AES256-SHA等)。该示例同时说明:要让静态配置的ssl_protocols真正包含 TLSv1.1,必须同步配置匹配的加密套件,否则握手仍会失败。修改 config.yaml 后需重启 APISIX 使静态配置生效。
第 2 步:为 test.com 域名指定 TLSv1.1 协议版本(动态覆盖全局配置)。
:::note
您可以这样从config.yaml中获取admin_key并存入环境变量:
admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g'):::
curl http://127.0.0.1:9180/apisix/admin/ssls/1 \ -H "X-API-KEY: $admin_key" -X PUT -d ' { "cert" : "'"$(cat server.crt)"'", "key": "'"$(cat server.key)"'", "snis": ["test.com"], "ssl_protocols": [ "TLSv1.1" ] }'第 3 步:为 test2.com 创建 SSL 对象,未指定 TLS 协议版本,将默认使用静态配置。
curl http://127.0.0.1:9180/apisix/admin/ssls/1 \ -H "X-API-KEY: $admin_key" -X PUT -d ' { "cert" : "'"$(cat server2.crt)"'", "key": "'"$(cat server2.key)"'", "snis": ["test2.com"] }'第 4 步:访问验证。
使用 TLSv1.3 访问 test.com 失败(因为该域名被动态限制为 TLSv1.1):
$ curl --tls-max 1.3 --tlsv1.3 https://test.com:9443 -v -k -I * Trying 127.0.0.1:9443... * Connected to test.com (127.0.0.1) port 9443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS alert, protocol version (582): * error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version * Closing connection 0 curl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version使用 TLSv1.1 访问 test.com 成功:
$ curl --tls-max 1.1 --tlsv1.1 https://test.com:9443 -v -k -I * Trying 127.0.0.1:9443... * Connected to test.com (127.0.0.1) port 9443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * TLSv1.1 (OUT), TLS handshake, Client hello (1): * TLSv1.1 (IN), TLS handshake, Server hello (2): * TLSv1.1 (IN), TLS handshake, Certificate (11): * TLSv1.1 (IN), TLS handshake, Server key exchange (12): * TLSv1.1 (IN), TLS handshake, Server finished (14): * TLSv1.1 (OUT), TLS handshake, Client key exchange (16): * TLSv1.1 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.1 (OUT), TLS handshake, Finished (20): * TLSv1.1 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.1 / ECDHE-RSA-AES256-SHA使用 TLSv1.3 访问 test2.com 成功(未设置 ssl_protocols,回落到静态配置 TLSv1.3):
$ curl --tls-max 1.3 --tlsv1.3 https://test2.com:9443 -v -k -I * Trying 127.0.0.1:9443... * Connected to test2.com (127.0.0.1) port 9443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * 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, 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, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384使用 TLSv1.1 访问 test2.com 失败:
curl --tls-max 1.1 --tlsv1.1 https://test2.com:9443 -v -k -I * Trying 127.0.0.1:9443... * Connected to test2.com (127.0.0.1) port 9443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * TLSv1.1 (OUT), TLS handshake, Client hello (1): * TLSv1.1 (IN), TLS alert, protocol version (582): * error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version * Closing connection 0 curl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version说明:
--tlsv1.x为 curl 的协议版本限定参数,某些新版本 curl(如 8.x)已将--tlsv1.1之类的独立选项移除,此时可以改用--tls-max 1.1配合--tlsv1或直接使用 openssl s_client(如openssl s_client -connect test.com:9443 -servername test.com -tls1_1)做等价验证,判定逻辑不变:期望的协议版本握手成功,被禁止的协议版本返回tlsv1 alert protocol version。
使用示例二:证书关联多个域名,但域名之间使用不同的 TLS 协议
有时候,我们可能会遇到这样一种情况,即一个证书关联了多个域名,但是它们需要使用不同的 TLS 协议来保证安全性。例如test.com域名需要使用 TLSv1.2 协议,而test2.com域名则需要使用 TLSv1.3 协议。在这种情况下,我们不能简单地为所有的域名创建一个 SSL 对象,而是需要为每个域名单独创建一个 SSL 对象,并指定相应的协议版本。这样,我们就可以根据不同的域名和协议版本来进行正确的 SSL 握手和加密通信。示例如下:
第 1 步:使用证书为 test.com 创建 ssl 对象,并指定 TLSv1.2 协议。
curl http://127.0.0.1:9180/apisix/admin/ssls/1 \ -H "X-API-KEY: $admin_key" -X PUT -d ' { "cert" : "'"$(cat server.crt)"'", "key": "'"$(cat server.key)"'", "snis": ["test.com"], "ssl_protocols": [ "TLSv1.2" ] }'第 2 步:使用与 test.com 同一证书,为 test2.com 创建 ssl 对象,并指定 TLSv1.3 协议。
curl http://127.0.0.1:9180/apisix/admin/ssls/2 \ -H "X-API-KEY: $admin_key" -X PUT -d ' { "cert" : "'"$(cat server.crt)"'", "key": "'"$(cat server.key)"'", "snis": ["test2.com"], "ssl_protocols": [ "TLSv1.3" ] }'第 3 步:访问验证。
使用 TLSv1.2 访问 test.com 成功(注意握手完成后 ALPN 协商到了 h2,说明 HTTP/2 在协议匹配成功后正常工作):
$ curl --tls-max 1.2 --tlsv1.2 https://test.com:9443 -v -k -I * Trying 127.0.0.1:9443... * Connected to test.com (127.0.0.1) port 9443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * TLSv1.2 (OUT), TLS handshake, Client hello (1): * TLSv1.2 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 * ALPN, server accepted to use h2 * Server certificate: * subject: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test.com * start date: Jul 20 15:50:08 2023 GMT * expire date: Jul 17 15:50:08 2033 GMT * issuer: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test.com * SSL certificate verify result: EE certificate key too weak (66), continuing anyway. * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x5608905ee2e0) > HEAD / HTTP/2 > Host: test.com:9443 > user-agent: curl/7.74.0 > accept: */*使用 TLSv1.3 协议访问 test.com 失败:
$ curl --tls-max 1.3 --tlsv1.3 https://test.com:9443 -v -k -I * Trying 127.0.0.1:9443... * Connected to test.com (127.0.0.1) port 9443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS alert, protocol version (582): * error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version * Closing connection 0 curl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version使用 TLSv1.3 协议访问 test2.com 成功:
$ curl --tls-max 1.3 --tlsv1.3 https://test2.com:9443 -v -k -I * Trying 127.0.0.1:9443... * Connected to test2.com (127.0.0.1) port 9443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * 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, 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, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 * ALPN, server accepted to use h2 * Server certificate: * subject: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test2.com * start date: Jul 20 16:05:47 2023 GMT * expire date: Jul 17 16:05:47 2033 GMT * issuer: C=AU; ST=Some-State; O=Internet Widgits Pty Ltd; CN=test2.com * SSL certificate verify result: EE certificate key too weak (66), continuing anyway. * Using HTTP2, server supports multi-use * Connection state changed (HTTP/2 confirmed) * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0 * Using Stream ID: 1 (easy handle 0x55569cbe42e0) > HEAD / HTTP/2 > Host: test2.com:9443 > user-agent: curl/7.74.0 > accept: */* > * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * old SSL session ID is stale, removing使用 TLSv1.2 协议访问 test2.com 失败:
$ curl --tls-max 1.2 --tlsv1.2 https://test2.com:9443 -v -k -I * Trying 127.0.0.1:9443... * Connected to test2.com (127.0.0.1) port 9443 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: /etc/ssl/certs/ca-certificates.crt * CApath: /etc/ssl/certs * TLSv1.2 (OUT), TLS handshake, Client hello (1): * TLSv1.2 (IN), TLS alert, protocol version (582): * error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version * Closing connection 0 curl: (35) error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version深入原理:ClientHello 阶段的协议下发流程
动态按 SNI 设置 TLS 协议的关键在于 APISIX 在握手早期(ssl_certificate_by_lua_block)就介入处理。完整流程可以概括为:
- 客户端发起 TLS 握手,ClientHello 中携带 SNI(如
test.com); - APISIX 通过
ngx_ssl_client.get_client_hello_server_name()提取 SNI(见 apisix/ssl.lua 中的server_name函数,未携带 SNI 时可回退到apisix.ssl.fallback_sni); - 以 SNI 为键匹配 SSL 资源(
router_ssl.match_and_set); - 若命中资源的
ssl_protocols非空,则调用ngx_ssl_client.set_protocols在握手阶段覆盖协议版本(见 apisix/init.lua 与 apisix/ssl.lua); - 之后继续正常的证书加载与握手流程。
从实现上看,静态配置作用于 nginx 全局指令ssl_protocols,动态配置则是在握手早期通过 lua-resty 的 ClientHello API 做更细粒度的覆盖,这正是“动态配置优先级更高”的底层原因。
总结与安全建议
| 对比维度 | 静态配置(config.yaml) | 动态配置(SSL 资源 ssl_protocols) |
|---|---|---|
| 作用范围 | APISIX 全局 | 单个 SNI / SSL 资源 |
| 修改方式 | 改配置文件后重启 | Admin API 实时下发 |
| 生效时机 | 重启后 | 立即生效 |
| 优先级 | 低(被动态配置覆盖) | 高 |
| 适用场景 | 设定全局安全基线 | 按域名差异化放行协议版本 |
实战建议:
- 生产环境优先采用全局收紧 + 按需放开的策略:config.yaml 只开放
TLSv1.2 TLSv1.3,确需兼容老旧客户端的域名再单独创建 SSL 资源,用ssl_protocols: ["TLSv1.1"]做定向兼容; - 启用 TLSv1.1 时务必同步在
apisix.ssl.ssl_ciphers中补充 TLSv1.1 支持的加密套件,否则即使协议放行,握手依然会因无匹配套件而失败; - 同一张证书关联多个域名、但各域名协议要求不同时,应为每个域名单独创建 SSL 资源并分别指定
ssl_protocols,而不是共用一个 SSL 对象; - 验证手段建议同时使用 curl(
--tls-max限定最大版本)与 openssl s_client,并通过握手输出中TLSv1.x (IN), TLS alert, protocol version或SSL connection using TLSv1.x / ...判断放行与拒绝结果。
若希望进一步了解 SSL 资源(证书、私钥、SNI、客户端 mTLS)的完整配置能力,可继续阅读 apisix/schema_def.lua 中 SSL schema 的其余字段定义,以及 t/node/ssl-protocols.t 与 t/admin/ssl5.t 中的完整测试用例。
【免费下载链接】apisixThe Cloud-Native API Gateway项目地址: https://gitcode.com/GitHub_Trending/ap/apisix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考