OpenResty (Nginx) with dynamically generated certificates

tl;dr: generate dynamic certificates on the fly with nginx.

Dynamically generated certificates can be used as a solution for web inspection (on both url and content) and blocking (for example deny executable downloads, deny uploads, deny certain urls, or specific content) secured connections. Each connection will be proxied through Nginx, and such the default nginx content filtering capabilities can be used.

On each https request the lua code will check if the certificate has been generated before already. If not, it will generate a new private key, create a certificate signing request, and sign the certificate with the defined certificate authority. The certificate authority needs to be trusted by the client browser. The generation of the certificate is guarded by a lock on the commonname, to prevent conditions when generating multiple certificates for the same commonname at the same time.

This solution builds on the work of @agentzh, one of the developers of OpenResty. The ssl-cert-by-lua branch of the lua-nginx-module enables you to use SSL functions from lua.

-- generate new private key
ngx.log(ngx.INFO, "generating key")  
local key_data, err = ssl.rsa_generate_key(2048)  
if not key_data then  
    ngx.log(ngx.ERR, "failed to generate rsa key: ", err)
    return
end  

This code generates a new private key, with 2048 bits.

local csr, err = ssl.generate_certificate_sign_request(key_data, {  
            country = "NL",
            state = "Test",
            city = "City",
            organisation = "Organisation",
            common_name = common_name
})

Create a new certificate signing request, with the common name of the host being requests (SNI).

-- create certificate using csr req
cert_data, err = ssl.sign_csr({  
                    ca = ca,
                    csr = csr
})

Sign the csr using the CA and return the new certificate.

local ok, err = ssl.set_der_priv_key(key_data)  
if not ok then  
    ngx.log(ngx.ERR, "failed to set DER priv key: ", err)
    return
end

local ok, err = ssl.set_der_cert(cert_data)  
if not ok then  
    ngx.log(ngx.ERR, "failed to set DER cert: ", err)
    return
end  

Set the current connection private key and certificate.

Installation

Download Openresty and replace the ngx_lua-0.9.15 folder with the module lua-nginx-module and the ssl-cert-by-lua branch.

Build

./configure --with-http_ssl_module --with-luajit --with-openssl=/usr/local/src/openssl-1.0.2a/ --with-ipv6

Create certificates

# generate private key
openssl genrsa -out rootCA.key 2048

# generate self-signed certificate
openssl req -x509 -new -nodes -key rootCA.key -days 1024 -out rootCA.pem

# combine both into one file
cat rootCA.key rootCA.pem > ca.key  

Configuration

The magic is in the sslcertificatebylua module of nginx.conf. You can also use sslcertificatebylua_file.

Testing

The first test will show the newly dynamic generated certificate, signed by the CA. The second test will proxy the request using the new certificate. The new certificate is not trusted, hence the -k argument.

$ openssl s_client -connect 172.16.84.202:3129 -servername www.microsoft.com

➜ CertificateSafe openssl s_client -connect 172.16.84.202:3129 -servername www.microsoft.com
CONNECTED(00000003)  
depth=0 /C=NL/ST=Test/L=City/O=Organisation/CN=www.microsoft.com  
verify error:num=20:unable to get local issuer certificate  
verify return:1  
depth=0 /C=NL/ST=Test/L=City/O=Organisation/CN=www.microsoft.com  
verify error:num=27:certificate not trusted  
verify return:1  
depth=0 /C=NL/ST=Test/L=City/O=Organisation/CN=www.microsoft.com  
verify error:num=21:unable to verify the first certificate  
verify return:1  
---
Certificate chain  
 0 s:/C=NL/ST=Test/L=City/O=Organisation/CN=www.microsoft.com
   i:/C=NL/ST=Some-State/O=Custom CA
---
Server certificate  
-----BEGIN CERTIFICATE-----
MIIECzCCAfOgAwIBAgIBATANBgkqhkiG9w0BAQsFADA0MQswCQYDVQQGEwJOTDET  
MBEGA1UECAwKU29tZS1TdGF0ZTEQMA4GA1UECgwHRGVmY2VydDAeFw0xNTA1MDQw  
ODQ4MzBaFw0yNTA1MDEwODQ4MzBaMF4xCzAJBgNVBAYTAk5MMQ0wCwYDVQQIDARU  
ZXN0MQ0wCwYDVQQHDARDaXR5MRUwEwYDVQQKDAxPcmdhbmlzYXRpb24xGjAYBgNV  
BAMMEXd3dy5taWNyb3NvZnQuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB  
CgKCAQEArnsuw5WILWWeoz+igxL0IGAJSu0xIRhTYO8kq2F8/q6jnXMwsmv7OjeX  
OifMWYaKSIEQuPlzYufjZeHbYftFXOBpSBUkahJ6dRc5Ze83M7i/geApuCA7sBMx  
kFY5Sn7foX95V037uKVfG61VCRvKkt8yYfa5DRjgGgtR4IVxfpikkxmhavUmKarE  
wyMKIMXr0bwQjY7UCYpp25YJM1wxcz5XtNxP4gO0H1APdtBicdDfSlb2gUl6awr3  
0+750hmtD40BJbIj03VHFIJduJ//lQoNkkNz39epkd0vgyznqsAT1mrz0x95zTDw  
Gi7NcDdYKEWl7l+vg8BWQ1H3NSwooQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAY  
qoBD7CtibsIYbRcYiz7jWl6PXNZYhp92jTVaJD9hGxp9aDD8s8AOaekpXCBg2H5R  
SxzPzauTCtEin1eZb5c3dXgJC4tLXyyeQrWoOp1TFW1CTgqlWeNZn3MuV7l/PdS4  
bQ63xxzRj3nQyyOshZPCv6EbXQo3d4nI7yGQwRI0zX9TL+72iHwzCsFWAyaeuu/w  
hn6KDgaWsyUUGayjskNjAofabC8cjMAgLmocxxRk7hFhsqZ3/R0WBubSw/UvRaYB  
lQ3qS42kyytm53GA7qgvpSRv/NYP7Yqe11h/FHS1fi6ajsvpEKpAOWJqXcQLwGdd  
LZqpuTpUGPmdx7z3HAFj13a38iYmMAEyrS01eow/kZmlED4FczuapgNB7bkcaOrI  
cpx8YNsz/te2S/n8kcQlExLbqhybUCUyndnafSFimeyqWzbHtlYFZXBrpiXPv316  
98nIgl5AKGbCRcG9sqxNNSPl97Mz9wej+sTc2a4O52k8u7PH05cIaLGAfs8mU/dV  
7H3m8H8/HkiSHYdqJK5WacVNhc+yurR6Xs1hQ4PID6a4jT0cWrcotXgsctgbSkSC  
RWjzZn8LaEPK5u9oaVF3UqmKHDUVlijj/vSS0negKp1bQu9KmOhMRnotPlGEMJWK  
gSG0jznxH16fbnRSCYaBx4wMTthOeImsR06ED/2mnQ==  
-----END CERTIFICATE-----
subject=/C=NL/ST=Test/L=City/O=Organisation/CN=www.microsoft.com  
issuer=/C=NL/ST=Some-State/O=Custom CA  

Check also Honos: the ngxing content filtering framework on Github.

Questions? Contact me at twitter: @remco_verhoef or send me an email remco@dutchcoders.io.