V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
NGINX
NGINX Trac
3rd Party Modules
Security Advisories
CHANGES
OpenResty
ngx_lua
Tengine
在线学习资源
NGINX 开发从入门到精通
NGINX Modules
ngx_echo
dzdh
V2EX  ›  NGINX

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

  •  1
     
  •   dzdh · 2022-06-16 18:33:08 +08:00 · 6385 次点击
    这是一个创建于 889 天前的主题,其中的信息可能已经有所发展或是发生改变。

    场景: 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

    第 1 条附言  ·  2022-06-16 22:59:32 +08:00
    第 2 条附言  ·  2022-06-17 13:18:11 +08:00

    最终 go fasthttp

    39 条回复    2022-06-18 00:20:25 +08:00
    privil
        1
    privil  
       2022-06-16 18:35:26 +08:00
    ……要不用 rust 写一个
    Buges
        2
    Buges  
       2022-06-16 18:38:23 +08:00 via Android
    caddy on demand tls 完全符合你的应用场景,不需要生成任何证书。再配合 dns 泛解析,用户输入一个域名提交上去,然后就可以直接访问这个域名了,证书会在第一次 tls 握手时根据 sni 自动生成。
    https://caddyserver.com/docs/automatic-https#on-demand-tls
    helone
        3
    helone  
       2022-06-16 18:42:06 +08:00
    常见的几个 cdn 厂都是用的 OpenResty 吧,redis get 你都觉得有性能影响的话,不妨考虑前面再加层 memory cache
    Showfom
        4
    Showfom  
       2022-06-16 18:42:22 +08:00
    那么多域名就一个机器做这事情么?别人都是集群一起上呀

    或者说,你那么多域名,就跑一个 nginx 服务?不会多绑定几个 IP 多跑几个 nginx 服务么?每个 nginx 服务绑定不同的 IP
    1423
        5
    1423  
       2022-06-16 19:06:06 +08:00   ❤️ 1
    用 caddy 的话,这里有一个支持把证书存储到 s3 上的拓展
    https://github.com/ss098/certmagic-s3
    harmless
        6
    harmless  
       2022-06-16 19:24:59 +08:00 via iPhone
    新版 nginx 支持动态证书加载吧
    alswl
        7
    alswl  
       2022-06-16 19:45:38 +08:00
    Nginx 管控面集群化 + DNS 管理自动化接入。
    wellsc
        8
    wellsc  
       2022-06-16 19:50:36 +08:00
    @privil 你们这群人怎么动不动换语言,不能从系统结构的层面解决问题吗
    neoblackcap
        9
    neoblackcap  
       2022-06-16 20:03:42 +08:00
    @wellsc 连 luajit 都觉得性能消耗过大,那么就只能干掉网络 IO 。如果还要再 nginx 上开发,那么就只能上 C/C++/Rust 来开发插件。所以建议 Rust 写一个插件也不算不合适。
    毕竟 cf 等企业,很多就是直接用系统语言开发一个插件,然后在 nginx/openresty 的基础上跑起来。
    ss098
        10
    ss098  
       2022-06-16 20:07:45 +08:00
    1. 对性能要求非常高的话,可以基于 Kubernetes + Cert Manager 自定义 CRDs 实现一套,可以用自己的任何支持 Kubernetes 的 Web 服务器,但需要自己开发 + 业务适配
    2. 追求简单,使用 Caddy ,上面有人贴了我写的 S3 兼容 Storage 接口,支持分布式 Stateless + 多实例部署
    qfdk
        11
    qfdk  
       2022-06-16 20:28:05 +08:00 via iPhone
    简单啊..... 其实用 Openresry 就好了 然后 nodejs 写个检测脚本. 把证书扔到 redis 里面 然后 每晚定点更新就是 还有一个月到期的时候自动更新就好
    wy315700
        12
    wy315700  
       2022-06-16 20:29:33 +08:00 via Android
    申请个 ca 自己签名
    FrankAdler
        13
    FrankAdler  
       2022-06-16 20:54:32 +08:00 via iPhone   ❤️ 1
    考虑容器吗,把一万多个客户的入口分散在不同的 pod 下,reload 的时候可能就只有部分,数量取决于你,流量入口 sni 分流,可以不需要证书。
    大厂 cdn 也不可能在一台机器上管理所有的域名啊
    cheng6563
        14
    cheng6563  
       2022-06-16 21:01:27 +08:00
    nginx 前面弄个 L4 的负载均衡,或者用 dns 动态解析弄个负载均衡,这样你就可以多节点 nginx 了。不过每个 nginx 节点还是要配全证书,可能还是有问题。

    或许可以用 go 或 rust ,自己写个 L4 的均衡负载,TLS 握手时会先发个 SNI 域名告知后台用哪个域名验证,这时把流量反代到域名对应的 nginx 节点上去,这样 nginx 只需要加载自己的证书就行了。
    joesonw
        15
    joesonw  
       2022-06-16 21:06:54 +08:00 via iPhone
    treadik 可以通过从 consul 读配置
    learningman
        16
    learningman  
       2022-06-16 21:26:04 +08:00
    @Buges 这是第三方签了张新证书,而不是用户的证书,如果用户那边配了 CAA ,证书都签不出来。要是用户的客户端做了证书装订之类的东西,你这个实现就把人家服务搞炸了。
    况且证书的数量级这个问题还是没解决,go 肯定比 C 慢
    Buges
        17
    Buges  
       2022-06-16 21:44:10 +08:00 via Android
    @learningman 场景是客户自定义域名,没要求支持客户上传自己的证书吧。
    数量应该不是问题,lz 这里慢是因为用 nginx 解析生成的巨大配置文件且启动时加载全部证书。而 caddy on-demand tls 是懒加载的,有连接来了才去申请 /加载缓存中的证书。如果性能还是不够也可以很容易地扩容,因为根本不需要你在配置文件里指定域名和证书。
    dzdh
        18
    dzdh  
    OP
       2022-06-16 22:27:47 +08:00
    @Buges
    @1423

    caddy 的那个确实也看过。但是因为用到了大量的 nginx rewrite rule 。caddy 的适配怎么样。另外不说和 nginx 的 https 性能持平吧,相差能差多少。然后就是如果是客户自己上传的证书能支持吗? 我都想直接. if https caddy -> localhost:80(nginx)了。

    自己搞了一个 go+fasthttp 是 tls.Config{ GetCertificate: func( info clientHello) *tls.Certificate 这个方案。不同域名的证书缓存在内存里. map[domain:string]*tls.Certificate


    @Showfom 是集群 nginx 的,每台服务器都是 /etc/nginx/vhosts/certs/1w 个..
    @neoblackcap 不光证书,不同域名可能 root 路径也不一样。比如 vip 客户或者定制客户或者尝鲜客户根据域名判断 root 是哪个目录比如. /opt/www/stage-2022/public 、/opt/www/dingzhi-01 ,所以 nginx 配置文件的数量也有很多。当然也有可能是我的 lua 脚本写的太垃圾了。。


    @cheng6563
    golang 怎么在 4 层获取 SNI 。


    @Buges 现在确实是客户自己上传证书,都是独立域名或二级域名的专用这个业务的证书。


    @harmless 翻了翻文档没找到,https://nginx.org/en/docs/http/configuring_https_servers.html 。懒加载可以实现一个配置文件 自动从三方存储或定制化存储甚至 rest 接口获取证书吗?
    Buges
        19
    Buges  
       2022-06-16 22:44:29 +08:00 via Android
    @dzdh caddy 有 API ,也可以写 plugin ,动态加载、自己上传证书当然也没问题。但自动证书能满足大部分需要吧,有特殊需求的客户再让他自己上传证书。
    你要是都要自己上传证书且管理的话,可能就不太适合用 caddy/nginx 这些一般的 web 服务器了,应该用自己的后端,证书存数据库里,自己实现懒加载。
    性能方面 caddy 确实慢一些,但你不是特别高并发的服务根本不用在意,绝大多数场景都不会成为短板。
    dzdh
        20
    dzdh  
    OP
       2022-06-16 22:49:24 +08:00
    @Buges 咋说呢。电商场景,平常也确实没啥事,特殊情况比如 618 天天都是做秒杀抢购的,偶尔一个小时或者十几分钟就是一个陡坡上去了。。。不知道能不能扛得住。还有这个东西我要先自己测,要不然线上流量我都不敢切过来 1%测。
    harmless
        21
    harmless  
       2022-06-16 23:22:10 +08:00 via iPhone
    @dzdh 我也没实际用过,不过看配置比传统的简化了不少,配合懒加载和 reload 可能可以快速刷新证书配置
    harmless
        22
    harmless  
       2022-06-16 23:23:34 +08:00 via iPhone
    dzdh
        23
    dzdh  
    OP
       2022-06-16 23:29:04 +08:00
    @harmless 嗯。懒加载是硬盘。后面更优的方案是 nginx plus 。hhhhh
    kennylam777
        24
    kennylam777  
       2022-06-17 04:52:47 +08:00   ❤️ 1
    其實在 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
        25
    blackboom  
       2022-06-17 07:25:51 +08:00
    In-memory cache
    holulu
        26
    holulu  
       2022-06-17 07:50:01 +08:00
    以前做过相似的场景,流量要求可能没有你的大。openresty 弄个接口,上传证书之后就调一下,把证书加载到 In-memory cache ,之后再开启域名的 https 访问。如果内存够大,缓存时间可以是证书的过期时间。现在证书一般最多是 2 年。如果用户把证书删了,就再弄个删除接口。
    dzdh
        27
    dzdh  
    OP
       2022-06-17 08:50:24 +08:00
    @holulu 有实例或 demo 可以分享看看吗
    SteveWoo
        28
    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
        29
    picone  
       2022-06-17 09:30:26 +08:00   ❤️ 1
    看看百度开源的 [BFE]( https://github.com/bfenetworks/bfe),把证书都加载到内存里,而且本身是可以通过 API 管理的,很适合 SaaS 场景。
    dzdh
        30
    dzdh  
    OP
       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
        31
    rev1si0n  
       2022-06-17 11:23:15 +08:00
    @wellsc 可能刚看到别人说 rs 多么多么高性能,多么多么快,实际上可能他自己都不会写
    TMaize
        32
    TMaize  
       2022-06-17 13:33:28 +08:00   ❤️ 1
    场景应该差不多,我们方案是用户主动解析域名到指定 CNAME

    自动通过 acme.sh 签发证书,控制 apisix 配置证书和路由规则,我还特意写了个工具 [apisix-acme]( https://github.com/TMaize/apisix-acme)
    SteveWoo
        33
    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
        34
    SteveWoo  
       2022-06-17 14:14:26 +08:00
    上面有个重要 bug 往 bucket 存 ssl hello 如果 环节失败可能会导致 conn 泄漏 这要好好处理下。
    刚翻了下原来的代码, 为了考虑各个场景,超时控制、大包检查、限流、统计,总共写了 700 多行了。
    gollwang
        35
    gollwang  
       2022-06-17 14:52:17 +08:00
    这不是现成的? https://certcloud.cn/
    dzdh
        36
    dzdh  
    OP
       2022-06-17 15:16:50 +08:00
    @SteveWoo remote 是个 http 80 也好使?
    dzdh
        37
    dzdh  
    OP
       2022-06-17 15:17:01 +08:00
    @gollwang 不一回事
    SteveWoo
        38
    SteveWoo  
       2022-06-17 15:47:37 +08:00
    @dzdh 好使。
    1423
        39
    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 完全就是代理服务器那一套了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3269 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 12:11 · PVG 20:11 · LAX 04:11 · JFK 07:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.