OpenResty で証明書の動的読み込み

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 で確認してみます。

```bash
$ 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
```

```bash
$ 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 で管理した方が楽かもしれません。