OpenResty®, a powerful web platform built on Nginx and LuaJIT, excels at handling complex, dynamic web serving tasks. One such advanced requirement is the ability to dynamically select and serve SSL/TLS certificates. While OpenResty offers robust mechanisms for dynamic SSL based on Server Name Indication (SNI), a more intricate challenge arises when the choice of certificate needs to be influenced by logic or data derived from an upstream application or service.
This scenario typically means that the information required to select the correct SSL certificate isn’t available directly from the client’s initial request (like SNI alone) but depends on a decision made after consulting a backend system. This article explores how to architect such solutions in OpenResty, clarifying the limitations of Nginx’s processing phases and presenting practical, effective patterns.
We’ll delve into the ssl_certificate_by_lua_block
directive, Lua scripting with the ngx.ssl
module, and architectural approaches to achieve dynamic SSL certificate loading that aligns with upstream-driven decisions.
The Challenge: Nginx Phases and SSL Handshake Timing
Nginx processes requests through a series of distinct phases. The SSL handshake, where the server presents its certificate, occurs very early in this lifecycle. OpenResty’s primary hook for dynamic SSL certificate configuration is the ssl_certificate_by_lua_block
directive. Code within this block executes before the main request processing phases (like rewrite
, access
, or content
) where typical upstream interactions (e.g., via proxy_pass
or Lua cosocket calls to application backends) occur.
This timing presents a fundamental challenge: you cannot directly proxy_pass
to a main application upstream, get its response, and then use that response to select an SSL certificate for the same, ongoing client SSL handshake. The certificate decision point has already passed.
Therefore, “dynamic SSL based on upstream response” requires careful architectural design to bridge this timing gap.
Approach 1: SNI-Driven Dynamic SSL with an External Metadata Service
This is the foundational pattern for most dynamic SSL setups in OpenResty and a key component of more complex solutions. Here, the “upstream” is a specialized, fast service (like Redis, a custom microservice, or a local database) that provides certificate information based on the SNI hostname sent by the client.
Concept:
- Client initiates an SSL connection, providing an SNI hostname.
- The
ssl_certificate_by_lua_block
executes. - Lua code within this block retrieves the SNI using
ngx.ssl.server_name()
. - It then queries an external metadata service (non-blockingly) or a shared memory cache for the SSL certificate and private key corresponding to the SNI.
- If found, the certificate and key are loaded using
ngx.ssl.set_pem_cert()
andngx.ssl.set_pem_priv_key()
.
Nginx Configuration (nginx.conf
):
|
|
Lua Script (custom_ssl_loader.lua
):
This script shows fetching from a cache and conceptually from a metadata service.
|
|
This pattern is highly efficient for serving many domains, as certificates are cached and lookups are fast. The “upstream” metadata service is specialized for certificate delivery.
Approach 2: Two-Tier Strategy for True Upstream-Response Influence
To make SSL decisions based on a response from your primary application upstream, a two-tier approach involving a client-side redirect is typically the most robust and manageable solution.
Concept:
Tier 1 (Initial Contact & Decision):
- Client connects to a generic or initial endpoint on OpenResty (e.g.,
https://initiate.example.com
). This endpoint might use a wildcard or default SSL certificate. - In a content phase handler (e.g.,
content_by_lua_block
), OpenResty makes a subrequest or proxies the request to your main application upstream. - The application upstream processes the request and its response includes information that dictates which specific hostname (and thus SSL certificate) the client should ultimately use (e.g., a tenant-specific hostname like
tenantA.example.com
). - OpenResty’s Lua script receives this information and issues an HTTP redirect (301 or 302) to the client, pointing them to the specific hostname.
- Client connects to a generic or initial endpoint on OpenResty (e.g.,
Tier 2 (Serving with Correct Certificate):
- The client’s browser follows the redirect and makes a new request to the specific hostname (e.g.,
https://tenantA.example.com
). - This new request is handled by an OpenResty server block configured for that hostname (or a wildcard that covers it).
- This server block uses the SNI-driven dynamic SSL mechanism (Approach 1) to look up and serve the correct SSL certificate for
tenantA.example.com
.
- The client’s browser follows the redirect and makes a new request to the specific hostname (e.g.,
Nginx Configuration for Tier 1 (nginx.conf
snippet):
|
|
Lua Script for Tier 1 (upstream_communicator.lua
):
|
|
Nginx Configuration for Tier 2: This would be a server block (or multiple) similar to the one shown in Approach 1, configured to handle tenantA.example.com
(and other such dynamic hostnames) and use ssl_certificate_by_lua_block
with a script like custom_ssl_loader.lua
to serve the appropriate certificate based on SNI.
This two-tier approach cleanly separates the concerns: the initial interaction determines the “what” (which hostname/certificate), and the subsequent redirected request handles the “how” (serving that specific certificate via SNI).
Implementing Certificate and Key Handling in Lua
Regardless of the approach, the core Lua logic using ngx.ssl
for setting certificates is similar:
|
|
Ensure that certificate and key data are handled securely, especially when fetched from external sources or stored in variables.
Essential Best Practices
- Aggressive Caching: Use
lua_shared_dict
to cache certificate data (PEM strings) and metadata. This significantly reduces latency for subsequent requests to the same hostname. - Non-Blocking Operations: Critically important. Any I/O within
ssl_certificate_by_lua_block
(e.g., fetching from a metadata API) must be non-blocking using Lua cosockets (e.g., vialua-resty-http
). Blocking calls here will stall SSL handshakes and severely degrade server performance. - Fallback Certificates: Always define static
ssl_certificate
andssl_certificate_key
directives in your Nginxserver
block. This ensures Nginx can start and handle requests if Lua logic fails or a dynamic certificate isn’t found. - Security:
- Protect private keys at all costs. Store them securely and transmit them over secure channels if fetched from an API.
- Ensure the metadata service providing certificate information is itself secure and trusted.
- Minimize the exposure of raw key material in logs.
- Robust Error Handling: Implement comprehensive error checking in your Lua scripts. Log errors clearly and decide whether to fall back to a default certificate or terminate the connection.
- Monitoring and Logging: Use
ngx.log
extensively to track the certificate loading process, successes, failures, and cache hits/misses. This is invaluable for debugging and operations. - Atomic Caching: When updating cached certificates (e.g., upon renewal), do so atomically to avoid serving a mismatched cert/key pair. Storing them under a versioned key or using temporary keys during update can help.
Common Pitfalls and Considerations
- Blocking Calls: The most common and impactful pitfall. Re-iterate: no blocking I/O in
ssl_certificate_by_lua_block
. - Certificate/Key Formats:
ngx.ssl.set_pem_cert/key
expect PEM-encoded data. If your source provides DER, you would usengx.ssl.set_der_cert()
/ngx.ssl.set_der_priv_key()
. - SSL Session Resumption: The
ssl_certificate_by_lua_block
handler typically runs only for full SSL handshakes, not for resumed sessions (where the original certificate is re-used). This is usually the desired behavior. - Large Number of Certificates: If managing tens of thousands of certificates, ensure your caching strategy and metadata lookup are highly optimized. Memory usage of
lua_shared_dict
also becomes a consideration. - Testing: Thoroughly test various scenarios: cache miss, cache hit, metadata service down, invalid certificate data, certificate renewal.
Debugging Your Dynamic SSL Setup
- Nginx Debug Logs: Enable debug logging for Nginx (
error_log /path/to/error.log debug;
). This will include detailed messages from thengx_http_lua_module
related tossl_certificate_by_lua*
execution. ngx.log()
in Lua: Sprinklengx.log(ngx.INFO, "message")
orngx.log(ngx.DEBUG, "message")
throughout your Lua scripts to trace execution flow and variable states.openssl s_client
: Use this command-line tool to test SSL connections and inspect the certificate served by OpenResty for a specific SNI:(See1 2 3
openssl s_client -connect your-openresty-server:443 \ -servername specific.hostname.com \ -tls1_2 # Or -tls1_3
openssl-s_client
man page for more options.)lua_code_cache off;
(Development ONLY): In yournginx.conf
under thehttp
block, setlua_code_cache off;
during development to see Lua code changes without needing to reload Nginx. Never use this in production as it severely degrades performance.- Isolate and Test Lua Modules: Test your Lua modules that fetch and process certificate data independently if possible.
Conclusion
Configuring OpenResty for dynamic SSL certificate loading based on upstream application logic is an advanced task that requires a clear understanding of Nginx’s phase model and careful architectural planning. While directly using an application upstream’s response to select a certificate for the same ongoing SSL handshake is generally infeasible, the two-tier redirect strategy provides a robust and practical solution. This approach leverages OpenResty’s strengths in both HTTP request handling (for the initial upstream call and redirect) and dynamic SSL negotiation via SNI (for the subsequent connection).
Combined with aggressive caching, non-blocking I/O, and thorough error handling, these patterns empower you to build highly flexible and scalable systems that can serve unique SSL certificates dynamically, driven by the sophisticated logic of your backend applications. OpenResty’s programmability with Lua truly shines in these complex, customized web infrastructure scenarios.