ssl_certificate_by_lua* ディレクティブを使用することで、証明書を動的に読み込ませることが可能です。
例
クライアントから送られてきた ClientHello の SNI 拡張に含まれているサーバ名に対応する証明書を読み込んでいます。
対応する証明書が見つからない場合は ServerHello を返さずに、Internal Error (80) のアラートが返ります。
http {
lua_shared_dict certs 10m;
server {
listen 10443 ssl;
ssl_protocols TLSv1.2;
ssl_certificate certs/www.example.com.pem;
ssl_certificate_key certs/www.example.com.key;
ssl_session_tickets on;
ssl_session_ticket_key certs/ticket.key;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_certificate_by_lua_block {
local ssl = require "ngx.ssl"
local ok, err = ssl.clear_certs()
if not ok then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local name, err = ssl.server_name()
if not name then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local ssl_certificate = string.format("conf/certs/%s.pem", name)
local ssl_certificate_key = string.format("conf/certs/%s.key", name)
local file = io.open(ssl_certificate, "r")
if not file then
return ngx.exit(ngx.ERROR)
end
local pem_cert = file:read("*all")
io.close(file)
local cert, err = ssl.cert_pem_to_der(pem_cert)
if not cert then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local ok, err = ssl.set_der_cert(cert)
if not ok then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local certs = ngx.shared.certs
certs:set(name, pem_cert)
local file = io.open(ssl_certificate_key, "r")
if not file then
return ngx.exit(ngx.ERROR)
end
local pem_priv_key = file:read("*all")
io.close(file)
local priv_key, err = ssl.priv_key_pem_to_der(pem_priv_key)
if not priv_key then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local ok, err = ssl.set_der_priv_key(priv_key)
if not ok then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
}
location / {
add_header Content-Type text/html;
content_by_lua_block {
local headers = ngx.req.get_headers()
local host = headers["Host"]
local m, err = ngx.re.match(host, "(?<hostname>[^:]+)")
if err then
ngx.log(ngx.ERR, err)
return
end
if m then
local certs = ngx.shared.certs
local cert = certs:get(m[0])
ngx.print(cert)
ngx.exit(ngx.HTTP_OK)
end
}
}
}
}
確認
curl で確認してみます。
$ curl -v -s -cacert ca.pem -X GET https://www1.example.com:10443/ 1>/dev/null
* Rebuilt URL to: ca.pem/
* Could not resolve host: ca.pem
* Closing connection 0
* Trying 127.0.0.1...
* Connected to www1.example.com (127.0.0.1) port 10443 (#1)
* TLS 1.2 connection using TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
* Server certificate: www1.example.com
* Server certificate: CA
> GET / HTTP/1.1
> Host: www1.example.com:10443
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: openresty/1.11.2.1
< Date: Sat, 03 Dec 2016 13:52:05 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
< Content-Type: text/html
<
{ [2342 bytes data]
* Connection #1 to host www1.example.com left intact
$ curl -v -s -cacert ca.pem -X GET https://www2.example.com:10443/ 1>/dev/null
* Rebuilt URL to: ca.pem/
* Could not resolve host: ca.pem
* Closing connection 0
* Trying 127.0.0.1...
* Connected to www2.example.com (127.0.0.1) port 10443 (#1)
* TLS 1.2 connection using TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
* Server certificate: www2.example.com
* Server certificate: CA
> GET / HTTP/1.1
> Host: www2.example.com:10443
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: openresty/1.11.2.1
< Date: Sat, 03 Dec 2016 13:53:24 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
< Content-Type: text/html
<
{ [2345 bytes data]
* Connection #1 to host www2.example.com left intact
Redis から読み込む
あまりファイルで管理したくないので、Redis に入れておいた証明書と秘密鍵を使用します。
ssl_certificate_by_lua_block {
local ssl = require "ngx.ssl"
local ok, err = ssl.clear_certs()
if not ok then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local name, err = ssl.server_name()
if not name then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local pem_cert, err = red:hget(name, "cert")
if not pem_cert then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
if pem_cert == ngx.null then
return ngx.exit(ngx.ERROR)
end
local pem_priv_key, err = red:hget(name, "priv_key")
if not pem_priv_key then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
if pem_priv_key == ngx.null then
return ngx.exit(ngx.ERROR)
end
local cert, err = ssl.cert_pem_to_der(pem_cert)
if not cert then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local ok, err = ssl.set_der_cert(cert)
if not ok then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local certs = ngx.shared.certs
certs:set(name, pem_cert)
local priv_key, err = ssl.priv_key_pem_to_der(pem_priv_key)
if not priv_key then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
local ok, err = ssl.set_der_priv_key(priv_key)
if not ok then
ngx.log(ngx.ERR, err)
return ngx.exit(ngx.ERROR)
end
}
更新や削除を考えると、管理用の HTTP API を用意して Redis で管理した方が楽かもしれません。