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 · 63 天前 · 4075 次点击
    这是一个创建于 63 天前的主题,其中的信息可能已经有所发展或是发生改变。

    场景: 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 条附言  ·  63 天前
    第 2 条附言  ·  63 天前

    最终 go fasthttp

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

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

    或许可以用 go 或 rust ,自己写个 L4 的均衡负载,TLS 握手时会先发个 SNI 域名告知后台用哪个域名验证,这时把流量反代到域名对应的 nginx 节点上去,这样 nginx 只需要加载自己的证书就行了。
    joesonw
        15
    joesonw  
       63 天前 via iPhone
    treadik 可以通过从 consul 读配置
    learningman
        16
    learningman  
       63 天前
    @Buges 这是第三方签了张新证书,而不是用户的证书,如果用户那边配了 CAA ,证书都签不出来。要是用户的客户端做了证书装订之类的东西,你这个实现就把人家服务搞炸了。
    况且证书的数量级这个问题还是没解决,go 肯定比 C 慢
    Buges
        17
    Buges  
       63 天前 via Android
    @learningman 场景是客户自定义域名,没要求支持客户上传自己的证书吧。
    数量应该不是问题,lz 这里慢是因为用 nginx 解析生成的巨大配置文件且启动时加载全部证书。而 caddy on-demand tls 是懒加载的,有连接来了才去申请 /加载缓存中的证书。如果性能还是不够也可以很容易地扩容,因为根本不需要你在配置文件里指定域名和证书。
    dzdh
        18
    dzdh  
    OP
       63 天前
    @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  
       63 天前 via Android
    @dzdh caddy 有 API ,也可以写 plugin ,动态加载、自己上传证书当然也没问题。但自动证书能满足大部分需要吧,有特殊需求的客户再让他自己上传证书。
    你要是都要自己上传证书且管理的话,可能就不太适合用 caddy/nginx 这些一般的 web 服务器了,应该用自己的后端,证书存数据库里,自己实现懒加载。
    性能方面 caddy 确实慢一些,但你不是特别高并发的服务根本不用在意,绝大多数场景都不会成为短板。
    dzdh
        20
    dzdh  
    OP
       63 天前
    @Buges 咋说呢。电商场景,平常也确实没啥事,特殊情况比如 618 天天都是做秒杀抢购的,偶尔一个小时或者十几分钟就是一个陡坡上去了。。。不知道能不能扛得住。还有这个东西我要先自己测,要不然线上流量我都不敢切过来 1%测。
    harmless
        21
    harmless  
       63 天前 via iPhone
    @dzdh 我也没实际用过,不过看配置比传统的简化了不少,配合懒加载和 reload 可能可以快速刷新证书配置
    dzdh
        23
    dzdh  
    OP
       63 天前
    @harmless 嗯。懒加载是硬盘。后面更优的方案是 nginx plus 。hhhhh
    kennylam777
        24
    kennylam777  
       63 天前   ❤️ 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  
       63 天前
    In-memory cache
    holulu
        26
    holulu  
       63 天前
    以前做过相似的场景,流量要求可能没有你的大。openresty 弄个接口,上传证书之后就调一下,把证书加载到 In-memory cache ,之后再开启域名的 https 访问。如果内存够大,缓存时间可以是证书的过期时间。现在证书一般最多是 2 年。如果用户把证书删了,就再弄个删除接口。
    dzdh
        27
    dzdh  
    OP
       63 天前
    @holulu 有实例或 demo 可以分享看看吗
    SteveWoo
        28
    SteveWoo  
       63 天前
    首先,”然后用 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  
       63 天前   ❤️ 1
    看看百度开源的 [BFE]( https://github.com/bfenetworks/bfe),把证书都加载到内存里,而且本身是可以通过 API 管理的,很适合 SaaS 场景。
    dzdh
        30
    dzdh  
    OP
       63 天前
    @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  
       63 天前
    @wellsc 可能刚看到别人说 rs 多么多么高性能,多么多么快,实际上可能他自己都不会写
    TMaize
        32
    TMaize  
       63 天前   ❤️ 1
    场景应该差不多,我们方案是用户主动解析域名到指定 CNAME

    自动通过 acme.sh 签发证书,控制 apisix 配置证书和路由规则,我还特意写了个工具 [apisix-acme]( https://github.com/TMaize/apisix-acme)
    SteveWoo
        33
    SteveWoo  
       63 天前
    @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  
       63 天前
    上面有个重要 bug 往 bucket 存 ssl hello 如果 环节失败可能会导致 conn 泄漏 这要好好处理下。
    刚翻了下原来的代码, 为了考虑各个场景,超时控制、大包检查、限流、统计,总共写了 700 多行了。
    gollwang
        35
    gollwang  
       63 天前
    这不是现成的? https://certcloud.cn/
    dzdh
        36
    dzdh  
    OP
       63 天前
    @SteveWoo remote 是个 http 80 也好使?
    dzdh
        37
    dzdh  
    OP
       63 天前
    @gollwang 不一回事
    SteveWoo
        38
    SteveWoo  
       63 天前
    @dzdh 好使。
    1423
        39
    1423  
       62 天前
    @SteveWoo
    @dzdh
    这其实就是个 sniproxy 了,可以单独做个开源组件,或者从现有的组建去拓展,比如看起来比较完善的 Ghostunnel
    不过可能依赖后面的 http server 支持 h2c ,只考虑 http1.1 倒是没问题
    go 的 net.Conn 是通用的抽象,tls,ss 各种科学上网只要用 go 都会把各种传输层对外体现为 net.Conn
    而两个 copy 完全就是代理服务器那一套了
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   3332 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 221ms · UTC 10:22 · PVG 18:22 · LAX 03:22 · JFK 06:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.