Skip to content

Commit 57708db

Browse files
authored
fix(stream): support upstream client certificate (mTLS) in L4 proxy (#13596)
1 parent 496cb68 commit 57708db

6 files changed

Lines changed: 329 additions & 25 deletions

File tree

.requirements

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@
1717

1818
APISIX_PACKAGE_NAME=apisix
1919

20-
APISIX_RUNTIME=1.3.6
20+
APISIX_RUNTIME=1.3.8
2121
APISIX_DASHBOARD_COMMIT=c8d3466d3c36386d3888efbc8250cd8183c77298

apisix/init.lua

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,32 @@ local function common_phase(phase_name)
493493
end
494494

495495

496+
-- Resolve the upstream client certificate referenced by `tls.client_cert_id`
497+
-- into `api_ctx.upstream_ssl`. Shared by the http and stream subsystems.
498+
-- Returns false on error (invalid/missing referenced ssl object).
499+
local function resolve_upstream_client_cert(api_ctx)
500+
if not (api_ctx.matched_upstream and api_ctx.matched_upstream.tls and
501+
api_ctx.matched_upstream.tls.client_cert_id) then
502+
return true
503+
end
504+
505+
local cert_id = api_ctx.matched_upstream.tls.client_cert_id
506+
local upstream_ssl = router.router_ssl.get_by_id(cert_id)
507+
if not upstream_ssl or upstream_ssl.type ~= "client" then
508+
local err = upstream_ssl and
509+
"ssl type should be 'client'" or
510+
"ssl id [" .. cert_id .. "] not exits"
511+
core.log.error("failed to get ssl cert: ", err)
512+
return false
513+
end
514+
515+
core.log.info("matched upstream client ssl object, id: ", cert_id,
516+
", type: ", upstream_ssl.type)
517+
api_ctx.upstream_ssl = upstream_ssl
518+
return true
519+
end
520+
521+
496522
function _M.handle_upstream(api_ctx, route, enable_websocket)
497523
-- some plugins(ai-proxy...) request upstream by http client directly
498524
if api_ctx.bypass_nginx_upstream then
@@ -537,27 +563,12 @@ function _M.handle_upstream(api_ctx, route, enable_websocket)
537563
api_ctx.matched_upstream = route_val.upstream
538564
end
539565

540-
if api_ctx.matched_upstream and api_ctx.matched_upstream.tls and
541-
api_ctx.matched_upstream.tls.client_cert_id then
542-
543-
local cert_id = api_ctx.matched_upstream.tls.client_cert_id
544-
local upstream_ssl = router.router_ssl.get_by_id(cert_id)
545-
if not upstream_ssl or upstream_ssl.type ~= "client" then
546-
local err = upstream_ssl and
547-
"ssl type should be 'client'" or
548-
"ssl id [" .. cert_id .. "] not exits"
549-
core.log.error("failed to get ssl cert: ", err)
550-
551-
if is_http then
552-
return core.response.exit(502)
553-
end
554-
555-
return ngx_exit(1)
566+
local ok = resolve_upstream_client_cert(api_ctx)
567+
if not ok then
568+
if is_http then
569+
return core.response.exit(502)
556570
end
557-
558-
core.log.info("matched ssl: ",
559-
core.json.delay_encode(upstream_ssl, true))
560-
api_ctx.upstream_ssl = upstream_ssl
571+
return ngx_exit(1)
561572
end
562573

563574
if enable_websocket then
@@ -1385,6 +1396,11 @@ function _M.stream_preread_phase()
13851396
return
13861397
end
13871398

1399+
local ok = resolve_upstream_client_cert(api_ctx)
1400+
if not ok then
1401+
return ngx_exit(1)
1402+
end
1403+
13881404
local code, err = set_upstream(matched_route, api_ctx)
13891405
if code then
13901406
core.log.error("failed to set upstream: ", err)

apisix/upstream.lua

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,25 @@ else
4242
end
4343

4444
local set_stream_upstream_tls
45+
local set_stream_upstream_cert_and_key
4546
if not is_http then
4647
local ok, apisix_ngx_stream_upstream = pcall(require, "resty.apisix.stream.upstream")
4748
if ok then
4849
set_stream_upstream_tls = apisix_ngx_stream_upstream.set_tls
49-
else
50+
set_stream_upstream_cert_and_key = apisix_ngx_stream_upstream.set_cert_and_key
51+
end
52+
-- guard each function independently: an older runtime may expose the module
53+
-- (set_tls) without the newer mTLS C-API (set_cert_and_key)
54+
if not set_stream_upstream_tls then
5055
set_stream_upstream_tls = function ()
5156
return nil, "need to build APISIX-Runtime to support TLS over TCP upstream"
5257
end
5358
end
59+
if not set_stream_upstream_cert_and_key then
60+
set_stream_upstream_cert_and_key = function ()
61+
return nil, "need to build APISIX-Runtime to support upstream mTLS over TCP"
62+
end
63+
end
5464
end
5565

5666

@@ -160,6 +170,54 @@ local function fill_node_info(up_conf, scheme, is_stream)
160170
end
161171

162172

173+
-- Set upstream client certificate (mTLS) for the stream (L4) subsystem.
174+
-- Mirrors the http subsystem: the cert/key are parsed and cached once (the key
175+
-- is AES-decrypted at rest by fetch_pkey) and applied to the upstream SSL
176+
-- handshake through the apisix-nginx-module stream C API, so the plaintext key
177+
-- is never stringified into an nginx variable.
178+
local function set_stream_upstream_client_cert(api_ctx, up_conf)
179+
local tls = up_conf.tls
180+
if not (tls and (tls.client_cert or tls.client_cert_id)) then
181+
return true
182+
end
183+
184+
local client_cert, client_key
185+
if tls.client_cert_id then
186+
if not api_ctx.upstream_ssl then
187+
return nil, "failed to find upstream ssl object for client_cert_id"
188+
end
189+
client_cert = api_ctx.upstream_ssl.cert
190+
client_key = api_ctx.upstream_ssl.key
191+
else
192+
client_cert = tls.client_cert
193+
client_key = tls.client_key
194+
end
195+
196+
if not (client_cert and client_key) then
197+
return nil, "missing client certificate or key for upstream mTLS"
198+
end
199+
200+
-- the sni here is just for logging
201+
local sni = api_ctx.var.upstream_host
202+
local cert, err = apisix_ssl.fetch_cert(sni, client_cert)
203+
if not cert then
204+
return nil, err
205+
end
206+
207+
local key, err = apisix_ssl.fetch_pkey(sni, client_key)
208+
if not key then
209+
return nil, err
210+
end
211+
212+
local ok, err = set_stream_upstream_cert_and_key(cert, key)
213+
if not ok then
214+
return nil, err
215+
end
216+
217+
return true
218+
end
219+
220+
163221
function _M.set_by_route(route, api_ctx)
164222
if api_ctx.upstream_conf then
165223
-- upstream_conf has been set by traffic-split plugin
@@ -246,6 +304,11 @@ function _M.set_by_route(route, api_ctx)
246304
if sni then
247305
ngx_var.upstream_sni = sni
248306
end
307+
308+
local ok, err = set_stream_upstream_client_cert(api_ctx, up_conf)
309+
if not ok then
310+
return 503, err
311+
end
249312
end
250313
local node_ver = resource.get_nodes_ver(up_conf.resource_key)
251314
local resource_version = upstream_util.version(up_conf.resource_version,

ci/linux-install-openresty.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,19 @@ else
6161
sudo apt-get -y update --fix-missing
6262
sudo apt-get install -y build-essential gcc g++ cpanminus libxml2-dev libxslt-dev
6363

64-
if [ "$APISIX_RUNTIME" != "1.3.6" ]; then
64+
if [ "$APISIX_RUNTIME" != "1.3.8" ]; then
6565
echo "Please update the apisix-runtime-debug checksum for APISIX_RUNTIME=$APISIX_RUNTIME" >&2
6666
exit 1
6767
fi
6868

6969
case "$ARCH" in
7070
x86_64|amd64)
7171
DEB_ARCH="amd64"
72-
EXPECTED_SHA256="f3c3836270e4d71c7154bea3dd13005cacad5b489eacf9fab7b048907fa4d641"
72+
EXPECTED_SHA256="d617eb9dbabdaa97c9722c7b48260aa26d121c280ecbb2c5e1bdeebc6fbeeb8e"
7373
;;
7474
arm64|aarch64)
7575
DEB_ARCH="arm64"
76-
EXPECTED_SHA256="6f5ba1e4dee34f9c2593687b3e97dad53cbc1f2b90283961fd87eba62e4c9bc4"
76+
EXPECTED_SHA256="4e263650a6bfb773b53ebf5643fed791d21115e92b4b370d0cd6d43c58fd870c"
7777
;;
7878
*)
7979
echo "Unsupported architecture: $ARCH" >&2

docs/en/latest/mtls.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,8 @@ curl http://127.0.0.1:9180/apisix/admin/upstreams/1 \
208208
}
209209
}'
210210
```
211+
212+
This also works in the stream (L4) subsystem: when an upstream uses the `tls`
213+
scheme and configures `tls.client_cert`/`tls.client_key` (or
214+
`tls.client_cert_id`), APISIX presents the client certificate while
215+
establishing the TLS connection to the upstream.

0 commit comments

Comments
 (0)