TLS Certificate Management in APISIX#
APISIX manages SSL/TLS certificates as SSL objects stored in etcd and synchronized in real time to every data plane worker. Each SSL object carries the PEM certificate(s), encrypted private key(s), one or more SNI hostnames, and optional mTLS configuration.
SSL object key fields (schema):
| Field | Purpose |
|---|---|
sni / snis | Hostnames this cert covers (single or array) |
cert / key | Primary PEM cert and private key |
certs / keys | Additional cert-key pairs (multi-algorithm) |
client.ca | CA cert for client certificate verification (mTLS) |
ssl_protocols | Allowed TLS versions (TLSv1.1, TLSv1.2, TLSv1.3) |
type | "server" (default) or "client" |
status | 1 = enabled, 0 = disabled |
Admin API is registered in apisix/admin/ssl.lua under the resource name ssls, exposing GET/POST/PUT/DELETE/PATCH /apisix/admin/ssls/{id}.
Key security behavior: private keys are AES-128-CBC encrypted before being written to etcd and are stripped from GET responses .
Storage, Sync, and LRU Caching#
SSL objects are stored in etcd under the /ssls prefix. On worker startup, init_worker() registers an etcd watcher via core.config.new("/ssls", { automatic = true, ... }). Any change in etcd is automatically pushed to all data plane workers without a reload.
Hitless rotation is a direct consequence of this design: updating an SSL object via the Admin API propagates to workers asynchronously. The radix tree router is rebuilt lazily on the next TLS handshake when radixtree_router_ver no longer matches ssl_certificates.conf_version . In-flight connections are unaffected because NGINX handles each TLS handshake independently.
LRU caches in apisix/ssl.lua avoid re-parsing PEM on every handshake:
cert_cache— parsed certificate objects, TTL 3600 s, max 1024 entriespkey_cache— parsed (and decrypted) private key objects, same limits
fetch_cert(sni, cert) and fetch_pkey(sni, pkey) are the cache-read entry points called during every TLS handshake.
SNI-Based Radix Tree Routing#
Certificate selection during a TLS handshake runs across two NGINX Lua phases:
ssl_client_hello_by_lua_block → ssl_certificate_by_lua_block
ClientHello phase (ssl_client_hello_phase()):
- Extracts SNI from the TLS ClientHello packet using the
ngx.ssl.clienthelloAPI . If no SNI is present, falls back toapisix.ssl.fallback_snifromconfig.yaml. - Calls
match_and_set(api_ctx, match_only=true, sni)— matches the SNI against the radix tree but does not yet load the cert, saving cryptographic work until the certificate phase. - Stores
matched_sslandclient_hello_sniinngx.ctxfor the next phase .
Certificate phase (ssl_phase()):
- Reads
ngx.ctx.matched_sslset by the ClientHello phase. - Calls
_M.set(), which clears existing certs, fetches secrets if the cert/key are secret URIs, then callsset_cert_and_key()→ngx_ssl.set_cert()/ngx_ssl.set_priv_key().
Radix tree construction (create_router()):
- SNI strings are reversed before being inserted as paths (e.g.
moc.elpmaxe.*), so wildcard prefix matching naturally handles*.example.com. - The router is rebuilt in-place when
conf_versionchanges; no worker restart needed. - Only SSL objects with
type == "server"andstatus == 1(or nil) are inserted .
For mTLS, after setting the server cert, ngx_ssl.verify_client() is called with the parsed CA cert and depth from client.ca.
Deployment Architecture Patterns#
Gateway TLS Termination#
The default pattern. APISIX terminates TLS using a server-type SSL object and proxies requests to upstreams over HTTP or HTTPS. An SSL object with matching sni/snis is required for the client→APISIX segment.
Load Balancer Termination#
When a load balancer (e.g. AWS ALB, Nginx, HAProxy) sits in front of APISIX and terminates TLS, APISIX receives plain HTTP. No SSL object is required for the downstream segment. APISIX may still need SSL objects for APISIX→upstream mTLS.
mTLS (Bidirectional)#
- Client → APISIX: Add
client.cato the SSL object. APISIX verifies the client certificate during the TLS handshake viangx_ssl.verify_client(). Useskip_mtls_uri_regexto exempt specific paths. - APISIX → Upstream: Configure
tls.client_cert/tls.client_keydirectly in the upstream object. For shared wildcard certs across multiple upstreams, create atype: "client"SSL object and reference it.
TLS Passthrough (L4 Stream)#
The stream subsystem enables SNI-based routing without terminating TLS. The NGINX template declares ssl_client_hello_by_lua_block in the stream context , which captures the SNI and stores it in ngx.ctx.client_hello_sni. The stream preread phase then uses the captured SNI for route matching via the ip_port router. The original encrypted connection is proxied to the upstream intact — APISIX never decrypts the payload.
Note: In the stream subsystem,
ngx.ssl.server_name()returns the session hostname (not the original SNI) in the preread phase, so SNI must be captured inssl_client_hello_phase().
Key Files and Entry Points#
| File | Role |
|---|---|
apisix/ssl.lua | LRU cert/key caches, AES-128-CBC key encryption/decryption, server_name() helper, check_ssl_conf() validation |
apisix/ssl/router/radixtree_sni.lua | Radix tree router: init_worker(), match_and_set(), set_cert_and_key(), create_router() |
apisix/init.lua | NGINX phase entry points: ssl_client_hello_phase() (line 180), ssl_phase() (line 169) |
apisix/admin/ssl.lua | Admin API resource registration for ssls |
apisix/schema_def.lua | SSL object JSON schema definition (lines 757–870) |
apisix/stream/router/ip_port.lua | L4 stream router with SNI-based trie matching |
apisix/cli/ngx_tpl.lua | NGINX config template declaring ssl_client_hello_by_lua_block and ssl_certificate_by_lua_block |
Related documentation: SSL Certificates key concept · Admin API - SSL Certificates · Configure HTTPS between Client and APISIX · Configure mTLS between Client and APISIX