nginx配置动态ssl域名转发
应用场景
作为第三方站点服务供应商,我们需要每天对接大量的第三方域名及其ssl证书,如果我们为每个客户的域名配置一个server block的话,可能会造成大量的配置冗余,并面临批量更新的问题。实践中,最理想的对接方式是:客户只需要将他们的域名证书公私钥提供给第三方供应商,并将改域名解析到第三方站点服务供应商的网关外网ip即可完成站点对接服务。为了方便网关管理各种第三方域名对接,我们可以通过nginx的default_server来支持ssl域名的动态转发。
具体需求描述
有个客户域名为example.guest.com,解析到第三方供应商的网关外网ip,第三方网关根据客户域名,查询并设置该域名的公私钥,并代理到后端预先给客户定制(每个客户需要配置不同的主题色和logo)的站点域名test-123.domain.com。
网关配置
我方网关代理配置:
server {
listen 443 default_server ssl;
server_name _;
ssl_certificate ssl/domain.com.pem;
ssl_certificate_key ssl/domain.com.key;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5:!kEDH;
ssl_prefer_server_ciphers on;
ssl_certificate_by_lua_file 'conf/dynamic_ssl.lua';
location / {
set $userdomain default;
access_by_lua_file 'conf/cname.lua';
proxy_pass http://$userdomain;
proxy_set_header Host $userdomain;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 8080;
server_name ~^(?<subdomain>test-\d+)\.domain\.com$;
lua_check_client_abort on;
location / {
root html;
}
}
dynamic_ssl.lua的实现思路如下:
- 获取客户ssl server_name
- 通过server_name获取ssl证书的公私钥
- 更新并替换当前连接的ssl公私钥
代码示例:
local server_name = ngx_ssl.server_name()
if not server_name then
return
end
ngx_ssl.clear_certs()
-- util.get_certificates函数返回ssl证书的公私钥,此自定义函数需自己实现
local der_cert_chain,der_priv_key = util.get_certificates(server_name)
if not der_cert_chain or not der_priv_key then
return ngx.exit(403)
end
local ok1,err1 = ngx_ssl.set_der_cert(der_cert_chain)
if not ok1 then
return ngx.exit(403)
end
local ok2,err3 = ngx_ssl.set_der_priv_key(der_priv_key)
if not ok2 then
return ngx.exit(403)
end
cname.lua只需要设置$userdomain变量即可
-- util.get_cname通过$host获取我方配置的站点域名
local userdomain = util.get_cname(ngx.var.host)
if not userdomain then
ngx.exit(404)
end
ngx.var.userdomain = userdomain
return
遇到的问题
如上dynamic_ssl.lua中,如果将ngx_ssl.clear_certs()放置到脚本的第一行
如果客户第三方想直接代理到我方的域名站点test.domain.com
server {
listen 443 ssl;
server_name test.domain.com;
ssl_certificate ssl/domain.com.pem;
ssl_certificate_key ssl/domain.com.key;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5:!kEDH;
ssl_prefer_server_ciphers on;
location / {
root html;
index index.html;
}
}
客户第三方代理配置如下:
server {
listen 443 ssl;
server_name example.guest.com;
ssl_certificate ssl/guest.com.pem;
ssl_certificate_key ssl/guest.com.key;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5:!kEDH;
ssl_prefer_server_ciphers on;
location / {
proxy_pass https://test.domain.com;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
此时查看我方网关的error_log,会出现如下报错信息:
2019/12/10 01:00:58 [crit] 23950#23950: *59484960 SSL_do_handshake() failed (SSL: error:1417A179:SSL routines:tls_post_process_client_hello:cert cb error) while SSL handshaking, client: 192.168.0.1, server: 0.0.0.0:443
分析原因:
通过报错日志可以发现,ssl server_name变成了0.0.0.0,也就是说nginx网关将客户端proxy_pass过来的test.domain.com域名清掉了,重置成了0.0.0.0。
分析定位问题,参考openresty ssl_certificate_by_lua_file指令
原来ssl_certificate_by_lua_file的执行阶段为 right-before-SSL-handshake
也就是说在所有在443端口做ssl握手前都会去执行这段代码。