如何方便优雅的管理 1w+个 HTTPS 证书

2022-06-16 18:33:08 +08:00
 dzdh

场景: SaaS 软件,客户可以自定义域名

现在方案: 提交证书,动态生成对应的 nginx 配置文件,nginx -s reload 。有个主机进行集中分发。

问题: 经过长时间业务发展,现在有 1w 多个客户的 1w 多个自定义域名相配也有 1w 多个证书。 服务器也越来越多,reload 一次耗时将近 1min.

求解: 像阿里云、腾讯云、蓝汛啥的 CDN 服务是咋做的。

想过用 openresty 的 lua 在 tls 握手阶段,拦截请求,通过 redis:get(domain+'.crt') redis.get(domain+'.key') 的形式。但是性能影响略大。

然后用 go+fasthttp 写了个 tlsproxy https->localhost:80 ,性能也是不理想。

求个最优解。

ps: 不同域名指向不同的 root(版本) 如 vip999.com->root /opt/www/branch/gold_vip/public

6862 次点击
所在节点    NGINX
39 条回复
harmless
2022-06-16 23:22:10 +08:00
@dzdh 我也没实际用过,不过看配置比传统的简化了不少,配合懒加载和 reload 可能可以快速刷新证书配置
harmless
2022-06-16 23:23:34 +08:00
dzdh
2022-06-16 23:29:04 +08:00
@harmless 嗯。懒加载是硬盘。后面更优的方案是 nginx plus 。hhhhh
kennylam777
2022-06-17 04:52:47 +08:00
其實在 NGiNX Ingress 的方法是用 Lua 讀取 filesystem 上的 crt/key, 然後 filesystem 上的內容是 ConfigMap/Secret 的更新, 那就可以免除一次 nginx -s reload ,畢竟要把所有 processes swap 過也是會有一點影響。

不過這種上千上萬的, 應該還是要 Lua 控制吧,只是你的 implementation 是直接在 redis get 過來,這種外部 IO 當然會慢。

可以看看 Lua NGINX Module 的 ngx.shared.DICT ,保留一份本地的證書快取,有類似 Redis 的 expire/ttl functions 可以用,然在 init_worker_by_lua 階段掛一套背景更新 DICT 的程式就好。
blackboom
2022-06-17 07:25:51 +08:00
In-memory cache
holulu
2022-06-17 07:50:01 +08:00
以前做过相似的场景,流量要求可能没有你的大。openresty 弄个接口,上传证书之后就调一下,把证书加载到 In-memory cache ,之后再开启域名的 https 访问。如果内存够大,缓存时间可以是证书的过期时间。现在证书一般最多是 2 年。如果用户把证书删了,就再弄个删除接口。
dzdh
2022-06-17 08:50:24 +08:00
@holulu 有实例或 demo 可以分享看看吗
SteveWoo
2022-06-17 09:10:24 +08:00
首先,”然后用 go+fasthttp 写了个 tlsproxy https->localhost:80 ,性能也是不理想。“,我恰好做过,性能一点问题没有。 注意还要调整服务器的参数配置(例如:文件打开数,端口范围调大点,send recv buffer )

其次,开发代理可以用二层有,即在 tcp 层 tls 这里证书校验在这里做就好了。 证书校验通过再往后做 tcp 转发,避免重复 http 解包。 这个我恰好也写过,贴段代码
```
TlsConfig = &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
// info.ServerName 这个就是域名
return GetAndCreateCert(info.ServerName)
},
}

```

最后,能不自己开发最好, 前面大家的建议都很好,如服务器和域名解析分组,nginx lua ,haproxy
picone
2022-06-17 09:30:26 +08:00
看看百度开源的 [BFE]( https://github.com/bfenetworks/bfe),把证书都加载到内存里,而且本身是可以通过 API 管理的,很适合 SaaS 场景。
dzdh
2022-06-17 10:26:26 +08:00
@SteveWoo 现在就是这么做的。但是“tls 建立连接成功后,直接做 tcp 转发”这个是怎么做的?

我现在
tlsCfg := tls.Config{ GetCertificate:...没错

tlsLn := tls.Listen("tcp",":443",&tlsCfg)

handler: servehttp() { req.port=80;req.host=$domain; fasthttp.client.do(req

server.Server(tlsLn)

在哪一步做 tcp 转发呢?能把 tls 连接的内容直接转发给 80 嘛?如果还要转发给 nginx 的 tls 那没啥意义了。
rev1si0n
2022-06-17 11:23:15 +08:00
@wellsc 可能刚看到别人说 rs 多么多么高性能,多么多么快,实际上可能他自己都不会写
TMaize
2022-06-17 13:33:28 +08:00
场景应该差不多,我们方案是用户主动解析域名到指定 CNAME

自动通过 acme.sh 签发证书,控制 apisix 配置证书和路由规则,我还特意写了个工具 [apisix-acme]( https://github.com/TMaize/apisix-acme)
SteveWoo
2022-06-17 14:00:26 +08:00
@dzdh

伪代码如下

// 分桶减少锁碰撞
// conn 对应了 host 。https keepalive 的一个连接只能是唯一的 host 。 这与 http 不同
// bucketMap := [30]map[net.Conn]string
// bucketMapMutex:=[30]sync.Mutex

cfg := &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
// 通过 info.Conn.LocalAddr() 确定 bucketMapMutex 分桶
// bucketMap[info.Conn]=info.ServerName // 连接与 host 对应好
return GetAndCreateCert(info.ServerName)
},
}

ln, err := net.Listen("tcp", ":12345")
assert(err)

lsn := tls.NewListener(ln, cfg)

for {
c, _ := lsn.Accept()
go func(conn net.Conn) /* 这个协程可以用协程池复用*/{
// 通过 info.Conn.LocalAddr() 确定 bucketMapMutex 分桶
//serverName:=bucketMap[idx][conn]
//addr:=serverName// 根据 serverName 确定后面的地址,如果无差别沦陷
remote, err := net.Dial("tcp", addr)
assert(err)// 做好 error 呴错误处理
// conn 设置 keepalive retmote 设置好 keepalive 建议搞成配置
// 优化合理设计,使一条代理只需要两个协程,做到如下内容:
// 1. 再包装一层 reader weiter 方便设置断开时间 conn.SetReadDeadline()
// 2. 原子操作协调断开
// connFlag atomic.Int32
// remoteFlag atomic.Int32
go func() {
// 3. 加上异常处理 断开 defer conn.Close remote.Close
io.Copy(conn, remote)
}()
go func() {
// 加上异常处理 断开 defer conn.Close remote.Close
io.Copy(remote, conn)
}()
}(c)
}
SteveWoo
2022-06-17 14:14:26 +08:00
上面有个重要 bug 往 bucket 存 ssl hello 如果 环节失败可能会导致 conn 泄漏 这要好好处理下。
刚翻了下原来的代码, 为了考虑各个场景,超时控制、大包检查、限流、统计,总共写了 700 多行了。
gollwang
2022-06-17 14:52:17 +08:00
这不是现成的? https://certcloud.cn/
dzdh
2022-06-17 15:16:50 +08:00
@SteveWoo remote 是个 http 80 也好使?
dzdh
2022-06-17 15:17:01 +08:00
@gollwang 不一回事
SteveWoo
2022-06-17 15:47:37 +08:00
@dzdh 好使。
1423
2022-06-18 00:20:25 +08:00
@SteveWoo
@dzdh
这其实就是个 sniproxy 了,可以单独做个开源组件,或者从现有的组建去拓展,比如看起来比较完善的 Ghostunnel
不过可能依赖后面的 http server 支持 h2c ,只考虑 http1.1 倒是没问题
go 的 net.Conn 是通用的抽象,tls,ss 各种科学上网只要用 go 都会把各种传输层对外体现为 net.Conn
而两个 copy 完全就是代理服务器那一套了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/860104

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX