Skip to content

fix(stream): support upstream client certificate (mTLS) in L4 proxy#13596

Draft
AlinsRan wants to merge 4 commits into
apache:masterfrom
AlinsRan:feat/12472-stream-mtls
Draft

fix(stream): support upstream client certificate (mTLS) in L4 proxy#13596
AlinsRan wants to merge 4 commits into
apache:masterfrom
AlinsRan:feat/12472-stream-mtls

Conversation

@AlinsRan

Copy link
Copy Markdown
Contributor

Description

Fixes #12472

The stream (L4 TCP/TLS) subsystem could not present a client certificate (mTLS) when APISIX proxies to a TLS upstream, unlike the http subsystem which honors upstream tls.client_cert/client_key/client_cert_id. This was confirmed by a maintainer in the issue: apisix/upstream.lua set the stream upstream TLS (SNI/enable) but never applied the client certificate.

Root cause

The http subsystem injects the client cert per-request through the apisix-nginx-module C API ngx_http_apisix_upstream_set_cert_and_key (applied via SSL_use_certificate during the upstream handshake). The stream subsystem only exposes ngx_stream_apisix_upstream_enable_tls (a boolean flag) — there is no stream counterpart that sets the client cert, so the http mechanism cannot be reused as-is.

Approach (self-contained, no nginx/openresty-C changes)

Use the native nginx stream proxy_ssl_certificate / proxy_ssl_certificate_key directives, which support variables and inline PEM via the data: scheme (nginx >= 1.21.4, satisfied by APISIX-Runtime):

  • apisix/cli/ngx_tpl.lua: declare $upstream_mtls_cert / $upstream_mtls_key (empty by default) and add proxy_ssl_certificate/proxy_ssl_certificate_key in the stream server block (guarded by use_apisix_base). An empty value means no client certificate is presented.
  • apisix/upstream.lua: in the stream scheme == "tls" branch, fill those vars from up_conf.tls.client_cert/client_key (inline) or from the ssl object referenced by tls.client_cert_id, using the data: inline PEM form (no temp files).
  • apisix/init.lua: extract the client_cert_id -> api_ctx.upstream_ssl resolution into a shared resolve_upstream_client_cert helper and call it from stream_preread_phase too (previously it ran only on the http path).
  • Tests: t/stream-node/upstream-mtls.t (inline cert success/failure + client_cert_id).
  • Docs: note stream support in docs/en/latest/mtls.md.

No schema change needed — tls.client_cert/client_key/client_cert_id already exist on the upstream schema.

bug-triage-2026-06

AlinsRan added 4 commits June 23, 2026 04:44
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 apache#12472
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.
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: mTLS stream data to external server, APISIX not sending client certificate

1 participant