From 31fc7e9b5f19ea7a3e43473095d85a51c613c146 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 23 Jun 2026 04:44:36 +0800 Subject: [PATCH 1/7] fix(stream): support upstream client certificate (mTLS) in L4 proxy The stream (L4) subsystem could not present a client certificate when proxying to a TLS upstream, unlike the http subsystem. The http path injects the client cert via the apisix-nginx-module C API (set_cert_and_key), which has no stream counterpart. Instead, wire the native nginx stream proxy_ssl_certificate / proxy_ssl_certificate_key directives with variables, filled in the preread phase with the upstream tls.client_cert/client_key (or the ssl object referenced by tls.client_cert_id) using the inline data: PEM scheme. An empty value means no client certificate is presented. Fixes #12472 --- apisix/cli/ngx_tpl.lua | 8 ++ apisix/init.lua | 55 +++++---- apisix/upstream.lua | 42 +++++++ docs/en/latest/mtls.md | 5 + t/stream-node/upstream-mtls.t | 215 ++++++++++++++++++++++++++++++++++ 5 files changed, 305 insertions(+), 20 deletions(-) create mode 100644 t/stream-node/upstream-mtls.t diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 52574e28d46d..57e5f3ee8539 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -255,6 +255,14 @@ stream { set $upstream_sni "apisix_backend"; proxy_ssl_server_name on; proxy_ssl_name $upstream_sni; + + # vars are set in the preread phase to support upstream client + # certificate (mTLS) when proxying to a TLS upstream. When empty, + # nginx skips loading the certificate, so no mTLS is performed. + set $upstream_mtls_cert ""; + set $upstream_mtls_key ""; + proxy_ssl_certificate $upstream_mtls_cert; + proxy_ssl_certificate_key $upstream_mtls_key; {% end %} log_by_lua_block { diff --git a/apisix/init.lua b/apisix/init.lua index 01838da5b0f4..a7bb0fe7b645 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -493,6 +493,31 @@ local function common_phase(phase_name) end +-- Resolve the upstream client certificate referenced by `tls.client_cert_id` +-- into `api_ctx.upstream_ssl`. Shared by the http and stream subsystems. +-- Returns false on error (invalid/missing referenced ssl object). +local function resolve_upstream_client_cert(api_ctx) + if not (api_ctx.matched_upstream and api_ctx.matched_upstream.tls and + api_ctx.matched_upstream.tls.client_cert_id) then + return true + end + + local cert_id = api_ctx.matched_upstream.tls.client_cert_id + local upstream_ssl = router.router_ssl.get_by_id(cert_id) + if not upstream_ssl or upstream_ssl.type ~= "client" then + local err = upstream_ssl and + "ssl type should be 'client'" or + "ssl id [" .. cert_id .. "] not exits" + core.log.error("failed to get ssl cert: ", err) + return false + end + + core.log.info("matched ssl: ", core.json.delay_encode(upstream_ssl, true)) + api_ctx.upstream_ssl = upstream_ssl + return true +end + + function _M.handle_upstream(api_ctx, route, enable_websocket) -- some plugins(ai-proxy...) request upstream by http client directly if api_ctx.bypass_nginx_upstream then @@ -537,27 +562,12 @@ function _M.handle_upstream(api_ctx, route, enable_websocket) api_ctx.matched_upstream = route_val.upstream end - if api_ctx.matched_upstream and api_ctx.matched_upstream.tls and - api_ctx.matched_upstream.tls.client_cert_id then - - local cert_id = api_ctx.matched_upstream.tls.client_cert_id - local upstream_ssl = router.router_ssl.get_by_id(cert_id) - if not upstream_ssl or upstream_ssl.type ~= "client" then - local err = upstream_ssl and - "ssl type should be 'client'" or - "ssl id [" .. cert_id .. "] not exits" - core.log.error("failed to get ssl cert: ", err) - - if is_http then - return core.response.exit(502) - end - - return ngx_exit(1) + local ok = resolve_upstream_client_cert(api_ctx) + if not ok then + if is_http then + return core.response.exit(502) end - - core.log.info("matched ssl: ", - core.json.delay_encode(upstream_ssl, true)) - api_ctx.upstream_ssl = upstream_ssl + return ngx_exit(1) end if enable_websocket then @@ -1385,6 +1395,11 @@ function _M.stream_preread_phase() return end + local ok = resolve_upstream_client_cert(api_ctx) + if not ok then + return ngx_exit(1) + end + local code, err = set_upstream(matched_route, api_ctx) if code then core.log.error("failed to set upstream: ", err) diff --git a/apisix/upstream.lua b/apisix/upstream.lua index 1c77543a943c..d4539bb118c2 100644 --- a/apisix/upstream.lua +++ b/apisix/upstream.lua @@ -160,6 +160,43 @@ local function fill_node_info(up_conf, scheme, is_stream) end +-- Set upstream client certificate (mTLS) for the stream (L4) subsystem. +-- Unlike the http subsystem, the stream proxy has no per-request C API to +-- inject the client cert into the SSL connection, so we rely on the native +-- nginx `proxy_ssl_certificate`/`proxy_ssl_certificate_key` directives, which +-- accept inline PEM via the `data:` scheme and support variables. The vars are +-- declared empty in the stream server block and filled here in the preread +-- phase; an empty value means no client certificate is presented. +local function set_stream_upstream_client_cert(api_ctx, up_conf) + local tls = up_conf.tls + if not (tls and (tls.client_cert or tls.client_cert_id)) then + return true + end + + local client_cert, client_key + if tls.client_cert_id then + if not api_ctx.upstream_ssl then + return nil, "failed to find upstream ssl object for client_cert_id" + end + client_cert = api_ctx.upstream_ssl.cert + client_key = api_ctx.upstream_ssl.key + else + client_cert = tls.client_cert + client_key = tls.client_key + end + + if not (client_cert and client_key) then + return nil, "missing client certificate or key for upstream mTLS" + end + + -- `data:` lets nginx read the PEM from the variable value directly, + -- avoiding any temporary file on disk. + ngx_var.upstream_mtls_cert = "data:" .. client_cert + ngx_var.upstream_mtls_key = "data:" .. client_key + return true +end + + function _M.set_by_route(route, api_ctx) if api_ctx.upstream_conf then -- upstream_conf has been set by traffic-split plugin @@ -246,6 +283,11 @@ function _M.set_by_route(route, api_ctx) if sni then ngx_var.upstream_sni = sni end + + local ok, err = set_stream_upstream_client_cert(api_ctx, up_conf) + if not ok then + return 503, err + end end local node_ver = resource.get_nodes_ver(up_conf.resource_key) local resource_version = upstream_util.version(up_conf.resource_version, diff --git a/docs/en/latest/mtls.md b/docs/en/latest/mtls.md index d6cfc0122919..0119702caee4 100644 --- a/docs/en/latest/mtls.md +++ b/docs/en/latest/mtls.md @@ -208,3 +208,8 @@ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 \ } }' ``` + +This also works in the stream (L4) subsystem: when an upstream uses the `tls` +scheme and configures `tls.client_cert`/`tls.client_key` (or +`tls.client_cert_id`), APISIX presents the client certificate while +establishing the TLS connection to the upstream. diff --git a/t/stream-node/upstream-mtls.t b/t/stream-node/upstream-mtls.t new file mode 100644 index 000000000000..c2e1565c33d1 --- /dev/null +++ b/t/stream-node/upstream-mtls.t @@ -0,0 +1,215 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX; + +my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx'; +my $version = eval { `$nginx_binary -V 2>&1` }; + +if ($version !~ m/\/apisix-nginx-module/) { + plan(skip_all => "apisix-nginx-module not installed"); +} else { + plan('no_plan'); +} + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("stream_enable", 1); + + # An mTLS-enabled upstream: it requires (and verifies) a client + # certificate signed by mtls_ca. A connection without a valid client + # certificate is rejected during the TLS handshake. + my $stream_config = $block->stream_config // ''; + $stream_config .= <<_EOC_; + server { + listen 8765 ssl; + ssl_certificate ../t/certs/mtls_server.crt; + ssl_certificate_key ../t/certs/mtls_server.key; + ssl_client_certificate ../t/certs/mtls_ca.crt; + ssl_verify_client on; + + content_by_lua_block { + local sock = ngx.req.socket() + local data = sock:receive("1") + ngx.say("hello mtls upstream") + } + } +_EOC_ + + $block->set_value("extra_stream_config", $stream_config); + } +}); + +run_tests(); + +__DATA__ + +=== TEST 1: set upstream (with client cert) & stream_route +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local json = require("toolkit.json") + local ssl_cert = t.read_file("t/certs/mtls_client.crt") + local ssl_key = t.read_file("t/certs/mtls_client.key") + local data = { + scheme = "tls", + type = "roundrobin", + nodes = { + ["127.0.0.1:8765"] = 1, + }, + tls = { + client_cert = ssl_cert, + client_key = ssl_key, + } + } + local code, body = t.test('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + json.encode(data) + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t.test('/apisix/admin/stream_routes/1', + ngx.HTTP_PUT, + [[{ + "remote_addr": "127.0.0.1", + "upstream_id": "1" + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 2: hit route, upstream mTLS succeeds with client cert +--- stream_request +mmm +--- stream_response +hello mtls upstream +--- no_error_log +[error] + + + +=== TEST 3: set upstream WITHOUT client cert +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local code, body = t.test('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "scheme": "tls", + "type": "roundrobin", + "nodes": { + "127.0.0.1:8765": 1 + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 4: hit route, upstream rejects connection without client cert +--- stream_request +mmm +--- stream_response eval +qr// +--- error_log +upstream SSL certificate verify error + + + +=== TEST 5: set upstream client cert via client_cert_id (ssl object) +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin") + local json = require("toolkit.json") + local ssl_cert = t.read_file("t/certs/mtls_client.crt") + local ssl_key = t.read_file("t/certs/mtls_client.key") + + local data = { + cert = ssl_cert, + key = ssl_key, + type = "client", + } + local code, body = t.test('/apisix/admin/ssls/1', + ngx.HTTP_PUT, + json.encode(data) + ) + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + local code, body = t.test('/apisix/admin/upstreams/1', + ngx.HTTP_PUT, + [[{ + "scheme": "tls", + "type": "roundrobin", + "nodes": { + "127.0.0.1:8765": 1 + }, + "tls": { + "client_cert_id": "1" + } + }]] + ) + if code >= 300 then + ngx.status = code + end + ngx.say(body) + } + } +--- request +GET /t +--- response_body +passed + + + +=== TEST 6: hit route, upstream mTLS succeeds with client_cert_id +--- stream_request +mmm +--- stream_response +hello mtls upstream +--- no_error_log +[error] From 60926de3e1bf236ecd6d9eee010a58c95f4e3fe9 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 23 Jun 2026 05:36:25 +0800 Subject: [PATCH 2/7] test(stream): fix mTLS cert path and ssl_certificate grep collision The stream upstream-mtls test referenced the upstream server certs via `../t/certs/...`, which resolves to `t/servroot/conf/../t/certs/...` (a non-existent path) and made nginx fail to start. Use the same `../../certs/...` prefix as other .t tests (e.g. healthcheck-https.t), which resolves to the repo `t/certs/` directory. The new `proxy_ssl_certificate` directive in every stream server block made the per-port PROXY protocol cli test mis-match: its plain-vs-TLS `grep -E "ssl_certificate "` matched the substring inside `proxy_ssl_certificate `. Anchor the grep on a leading whitespace boundary so it only matches the downstream `ssl_certificate` directive. --- t/cli/test_stream_proxy_protocol.sh | 4 ++-- t/stream-node/upstream-mtls.t | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/t/cli/test_stream_proxy_protocol.sh b/t/cli/test_stream_proxy_protocol.sh index a71ad6ba476b..4a517ec4a536 100755 --- a/t/cli/test_stream_proxy_protocol.sh +++ b/t/cli/test_stream_proxy_protocol.sh @@ -214,7 +214,7 @@ apisix: ' > conf/config.yaml make init -if ! block_with_listen 9101 | grep -E "ssl_certificate " > /dev/null; then +if ! block_with_listen 9101 | grep -E "[[:space:]]ssl_certificate " > /dev/null; then echo "failed: tls port in the to-upstream block should render ssl_certificate" exit 1 fi @@ -222,7 +222,7 @@ if ! block_with_listen 9101 | grep -E "proxy_protocol on;" > /dev/null; then echo "failed: 9101 should send PROXY protocol upstream" exit 1 fi -if block_with_listen 9100 | grep -E "ssl_certificate " > /dev/null; then +if block_with_listen 9100 | grep -E "[[:space:]]ssl_certificate " > /dev/null; then echo "failed: the plain block must not render ssl_certificate" exit 1 fi diff --git a/t/stream-node/upstream-mtls.t b/t/stream-node/upstream-mtls.t index c2e1565c33d1..1d74c98c21d8 100644 --- a/t/stream-node/upstream-mtls.t +++ b/t/stream-node/upstream-mtls.t @@ -38,9 +38,9 @@ add_block_preprocessor(sub { $stream_config .= <<_EOC_; server { listen 8765 ssl; - ssl_certificate ../t/certs/mtls_server.crt; - ssl_certificate_key ../t/certs/mtls_server.key; - ssl_client_certificate ../t/certs/mtls_ca.crt; + ssl_certificate ../../certs/mtls_server.crt; + ssl_certificate_key ../../certs/mtls_server.key; + ssl_client_certificate ../../certs/mtls_ca.crt; ssl_verify_client on; content_by_lua_block { From 7807be1038e432b8ba185455d559750dccd6bec8 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 23 Jun 2026 06:23:00 +0800 Subject: [PATCH 3/7] test(stream): declare upstream mTLS cert vars in stream test server --- t/APISIX.pm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/t/APISIX.pm b/t/APISIX.pm index e0b86560b040..5b1719ac3d6a 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -563,6 +563,10 @@ _EOC_ proxy_ssl_server_name on; proxy_ssl_name \$upstream_sni; set \$upstream_sni "apisix_backend"; + set \$upstream_mtls_cert ""; + set \$upstream_mtls_key ""; + proxy_ssl_certificate \$upstream_mtls_cert; + proxy_ssl_certificate_key \$upstream_mtls_key; _EOC_ } From ebbc6edf849d5c7bae6f89b15b6d92aca161b206 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Tue, 23 Jun 2026 07:11:34 +0800 Subject: [PATCH 4/7] fix(stream): decrypt upstream client key before data: PEM injection The stream mTLS path fed `tls.client_key` (or the ssl object key) directly into the `data:` proxy_ssl_certificate_key variable. That key is stored AES-encrypted at rest, so nginx received base64 ciphertext instead of PEM and failed with "cannot load certificate key". Decrypt it with aes_decrypt_pkey (a no-op for plaintext PEM) before building the data: value, matching the http path which decrypts via fetch_pkey. Also correct the no-client-cert test assertion: an mTLS upstream that rejects a certless handshake logs "client sent no required SSL certificate", not "upstream SSL certificate verify error" (which only applies to proxy_ssl_verify server-cert checks). --- apisix/upstream.lua | 10 +++++++++- t/stream-node/upstream-mtls.t | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apisix/upstream.lua b/apisix/upstream.lua index d4539bb118c2..e3c7d4c0a57c 100644 --- a/apisix/upstream.lua +++ b/apisix/upstream.lua @@ -189,10 +189,18 @@ local function set_stream_upstream_client_cert(api_ctx, up_conf) return nil, "missing client certificate or key for upstream mTLS" end + -- The private key is stored AES-encrypted at rest (see encrypt_conf and the + -- ssl object), so decrypt it back to PEM before handing it to nginx. The + -- certificate is always stored as plaintext PEM. + local key, err = apisix_ssl.aes_decrypt_pkey(client_key) + if not key then + return nil, err + end + -- `data:` lets nginx read the PEM from the variable value directly, -- avoiding any temporary file on disk. ngx_var.upstream_mtls_cert = "data:" .. client_cert - ngx_var.upstream_mtls_key = "data:" .. client_key + ngx_var.upstream_mtls_key = "data:" .. key return true end diff --git a/t/stream-node/upstream-mtls.t b/t/stream-node/upstream-mtls.t index 1d74c98c21d8..72d91c4b5104 100644 --- a/t/stream-node/upstream-mtls.t +++ b/t/stream-node/upstream-mtls.t @@ -152,7 +152,7 @@ mmm --- stream_response eval qr// --- error_log -upstream SSL certificate verify error +client sent no required SSL certificate while SSL handshaking From 4770c6de650172da619f4b45682fcf096fd3d281 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Thu, 25 Jun 2026 09:50:01 +0800 Subject: [PATCH 5/7] feat(stream): apply upstream mTLS via apisix-nginx-module C-API Replace the interim proxy_ssl_certificate data: plumbing with the new stream set_cert_and_key C-API shipped in APISIX-Runtime 1.3.8 (apisix-nginx-module 1.19.6). The cert/key are parsed and cached once and applied at the upstream SSL handshake, so the decrypted private key is never stringified into an nginx variable. Bump APISIX_RUNTIME 1.3.6 -> 1.3.8. --- .requirements | 2 +- apisix/cli/ngx_tpl.lua | 8 ------- apisix/upstream.lua | 36 ++++++++++++++++++----------- ci/linux-install-openresty.sh | 6 ++--- t/APISIX.pm | 4 ---- t/cli/test_stream_proxy_protocol.sh | 4 ++-- 6 files changed, 28 insertions(+), 32 deletions(-) diff --git a/.requirements b/.requirements index 75e381cbc58a..1e23078f82b4 100644 --- a/.requirements +++ b/.requirements @@ -17,5 +17,5 @@ APISIX_PACKAGE_NAME=apisix -APISIX_RUNTIME=1.3.6 +APISIX_RUNTIME=1.3.8 APISIX_DASHBOARD_COMMIT=c8d3466d3c36386d3888efbc8250cd8183c77298 diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index 57e5f3ee8539..52574e28d46d 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -255,14 +255,6 @@ stream { set $upstream_sni "apisix_backend"; proxy_ssl_server_name on; proxy_ssl_name $upstream_sni; - - # vars are set in the preread phase to support upstream client - # certificate (mTLS) when proxying to a TLS upstream. When empty, - # nginx skips loading the certificate, so no mTLS is performed. - set $upstream_mtls_cert ""; - set $upstream_mtls_key ""; - proxy_ssl_certificate $upstream_mtls_cert; - proxy_ssl_certificate_key $upstream_mtls_key; {% end %} log_by_lua_block { diff --git a/apisix/upstream.lua b/apisix/upstream.lua index e3c7d4c0a57c..ea4fee9fbcf4 100644 --- a/apisix/upstream.lua +++ b/apisix/upstream.lua @@ -42,14 +42,19 @@ else end local set_stream_upstream_tls +local set_stream_upstream_cert_and_key if not is_http then local ok, apisix_ngx_stream_upstream = pcall(require, "resty.apisix.stream.upstream") if ok then set_stream_upstream_tls = apisix_ngx_stream_upstream.set_tls + set_stream_upstream_cert_and_key = apisix_ngx_stream_upstream.set_cert_and_key else set_stream_upstream_tls = function () return nil, "need to build APISIX-Runtime to support TLS over TCP upstream" end + set_stream_upstream_cert_and_key = function () + return nil, "need to build APISIX-Runtime to support upstream mTLS over TCP" + end end end @@ -161,12 +166,10 @@ end -- Set upstream client certificate (mTLS) for the stream (L4) subsystem. --- Unlike the http subsystem, the stream proxy has no per-request C API to --- inject the client cert into the SSL connection, so we rely on the native --- nginx `proxy_ssl_certificate`/`proxy_ssl_certificate_key` directives, which --- accept inline PEM via the `data:` scheme and support variables. The vars are --- declared empty in the stream server block and filled here in the preread --- phase; an empty value means no client certificate is presented. +-- Mirrors the http subsystem: the cert/key are parsed and cached once (the key +-- is AES-decrypted at rest by fetch_pkey) and applied to the upstream SSL +-- handshake through the apisix-nginx-module stream C API, so the plaintext key +-- is never stringified into an nginx variable. local function set_stream_upstream_client_cert(api_ctx, up_conf) local tls = up_conf.tls if not (tls and (tls.client_cert or tls.client_cert_id)) then @@ -189,18 +192,23 @@ local function set_stream_upstream_client_cert(api_ctx, up_conf) return nil, "missing client certificate or key for upstream mTLS" end - -- The private key is stored AES-encrypted at rest (see encrypt_conf and the - -- ssl object), so decrypt it back to PEM before handing it to nginx. The - -- certificate is always stored as plaintext PEM. - local key, err = apisix_ssl.aes_decrypt_pkey(client_key) + -- the sni here is just for logging + local sni = api_ctx.var.upstream_host + local cert, err = apisix_ssl.fetch_cert(sni, client_cert) + if not cert then + return nil, err + end + + local key, err = apisix_ssl.fetch_pkey(sni, client_key) if not key then return nil, err end - -- `data:` lets nginx read the PEM from the variable value directly, - -- avoiding any temporary file on disk. - ngx_var.upstream_mtls_cert = "data:" .. client_cert - ngx_var.upstream_mtls_key = "data:" .. key + local ok, err = set_stream_upstream_cert_and_key(cert, key) + if not ok then + return nil, err + end + return true end diff --git a/ci/linux-install-openresty.sh b/ci/linux-install-openresty.sh index 2c8fd81bb3ff..7db860dbb8c1 100755 --- a/ci/linux-install-openresty.sh +++ b/ci/linux-install-openresty.sh @@ -61,7 +61,7 @@ else sudo apt-get -y update --fix-missing sudo apt-get install -y build-essential gcc g++ cpanminus libxml2-dev libxslt-dev - if [ "$APISIX_RUNTIME" != "1.3.6" ]; then + if [ "$APISIX_RUNTIME" != "1.3.8" ]; then echo "Please update the apisix-runtime-debug checksum for APISIX_RUNTIME=$APISIX_RUNTIME" >&2 exit 1 fi @@ -69,11 +69,11 @@ else case "$ARCH" in x86_64|amd64) DEB_ARCH="amd64" - EXPECTED_SHA256="f3c3836270e4d71c7154bea3dd13005cacad5b489eacf9fab7b048907fa4d641" + EXPECTED_SHA256="d617eb9dbabdaa97c9722c7b48260aa26d121c280ecbb2c5e1bdeebc6fbeeb8e" ;; arm64|aarch64) DEB_ARCH="arm64" - EXPECTED_SHA256="6f5ba1e4dee34f9c2593687b3e97dad53cbc1f2b90283961fd87eba62e4c9bc4" + EXPECTED_SHA256="4e263650a6bfb773b53ebf5643fed791d21115e92b4b370d0cd6d43c58fd870c" ;; *) echo "Unsupported architecture: $ARCH" >&2 diff --git a/t/APISIX.pm b/t/APISIX.pm index 5b1719ac3d6a..e0b86560b040 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -563,10 +563,6 @@ _EOC_ proxy_ssl_server_name on; proxy_ssl_name \$upstream_sni; set \$upstream_sni "apisix_backend"; - set \$upstream_mtls_cert ""; - set \$upstream_mtls_key ""; - proxy_ssl_certificate \$upstream_mtls_cert; - proxy_ssl_certificate_key \$upstream_mtls_key; _EOC_ } diff --git a/t/cli/test_stream_proxy_protocol.sh b/t/cli/test_stream_proxy_protocol.sh index 4a517ec4a536..a71ad6ba476b 100755 --- a/t/cli/test_stream_proxy_protocol.sh +++ b/t/cli/test_stream_proxy_protocol.sh @@ -214,7 +214,7 @@ apisix: ' > conf/config.yaml make init -if ! block_with_listen 9101 | grep -E "[[:space:]]ssl_certificate " > /dev/null; then +if ! block_with_listen 9101 | grep -E "ssl_certificate " > /dev/null; then echo "failed: tls port in the to-upstream block should render ssl_certificate" exit 1 fi @@ -222,7 +222,7 @@ if ! block_with_listen 9101 | grep -E "proxy_protocol on;" > /dev/null; then echo "failed: 9101 should send PROXY protocol upstream" exit 1 fi -if block_with_listen 9100 | grep -E "[[:space:]]ssl_certificate " > /dev/null; then +if block_with_listen 9100 | grep -E "ssl_certificate " > /dev/null; then echo "failed: the plain block must not render ssl_certificate" exit 1 fi From abf2a45672cdcd2afcd4ab7432bda354a9aafc27 Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Thu, 25 Jun 2026 12:55:04 +0800 Subject: [PATCH 6/7] fix(stream): redact ssl object in log + guard mTLS C-API per function - init.lua: log only client ssl id/type instead of delay-encoding the whole ssl object, which could emit certificate/key material at info level. - upstream.lua: guard set_tls and set_cert_and_key independently so an older runtime exposing the stream module without set_cert_and_key falls back gracefully instead of calling a nil value. --- apisix/init.lua | 3 ++- apisix/upstream.lua | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apisix/init.lua b/apisix/init.lua index a7bb0fe7b645..f975e71dd7a1 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -512,7 +512,8 @@ local function resolve_upstream_client_cert(api_ctx) return false end - core.log.info("matched ssl: ", core.json.delay_encode(upstream_ssl, true)) + core.log.info("matched upstream client ssl object, id: ", cert_id, + ", type: ", upstream_ssl.type) api_ctx.upstream_ssl = upstream_ssl return true end diff --git a/apisix/upstream.lua b/apisix/upstream.lua index ea4fee9fbcf4..ef89f2a22f84 100644 --- a/apisix/upstream.lua +++ b/apisix/upstream.lua @@ -48,10 +48,15 @@ if not is_http then if ok then set_stream_upstream_tls = apisix_ngx_stream_upstream.set_tls set_stream_upstream_cert_and_key = apisix_ngx_stream_upstream.set_cert_and_key - else + end + -- guard each function independently: an older runtime may expose the module + -- (set_tls) without the newer mTLS C-API (set_cert_and_key) + if not set_stream_upstream_tls then set_stream_upstream_tls = function () return nil, "need to build APISIX-Runtime to support TLS over TCP upstream" end + end + if not set_stream_upstream_cert_and_key then set_stream_upstream_cert_and_key = function () return nil, "need to build APISIX-Runtime to support upstream mTLS over TCP" end From ec6b27777b40482a70b36d266937b5a0c9d9b7fc Mon Sep 17 00:00:00 2001 From: AlinsRan Date: Thu, 25 Jun 2026 12:56:45 +0800 Subject: [PATCH 7/7] test(stream): add standard harness setup to upstream-mtls.t Add repeat_each(1)/no_long_string()/no_shuffle()/no_root_location() per the test harness convention; no_shuffle keeps the setup-then-hit ordering stable. --- t/stream-node/upstream-mtls.t | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/t/stream-node/upstream-mtls.t b/t/stream-node/upstream-mtls.t index 72d91c4b5104..fd4c753f8484 100644 --- a/t/stream-node/upstream-mtls.t +++ b/t/stream-node/upstream-mtls.t @@ -16,6 +16,11 @@ # use t::APISIX; +repeat_each(1); +no_long_string(); +no_shuffle(); +no_root_location(); + my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx'; my $version = eval { `$nginx_binary -V 2>&1` };