使用golang部署运行tls的https服务时,不用停机,高效证书下放,如何实现?

2021-04-10
11分钟阅读时长

第一部分

这篇文章主要介绍如何在应用golang语言开发http/https服务时,如何让tls自动获取证书,而不必在证书更新或重置以后,还要重启服务器来让业务重新起效,本文分成三部分,第一部分会介绍tls加密的常用加密算法进行分析总结,虽然与主干关系不特别大,但是该段络会帮你厘清一个日常使用中,非常容易被混淆的问题;第二部分会重点介绍如何部署一个不需要重启也能tls自动更新的高抽象度的http服务;第三部分会对整个文章进行总结,相信基于该文章的学习,你一定会对tls领域和流量监测、安全防护领域常见的算法有相对深刻的理解,也对如何高度抽象一个自签名的golang服务有全新的认识。那么文章开始!

我之前已经有文章分享过如果来做爬虫,用python的scrapy和基于node的puppeteer之间的优劣对比。也分享过我之前抓取某国外站点的时候,由于页面验签要正确的传输参数,在post请求中提交到后台才能返回正确的结果,这个在国内的也有很多站点有这种验签机制的存在,比如说我在写基于puppeteer的自动发布文章应用的时候,一些平台,像今日头条平台、像简书用浏览器缓存的cookies就可以避免重复登录,直接模拟浏览器登录发布文章就好了。但是像百度的百家号,只有cookie是无法成功验签的,还要获取下放的token,才能完整成功完成发布百家号的流程,总之,就是“兵来将挡,水来土淹”。

但是你有考虑过,这些平台其实也是基于tls的验签机会来保护客户端和服务器端数据交互安全性的,但是遵循的底层原理却是各有不同的。比如说JA3指纹算法,它能基于TLS客户端与服务端之间握手消息内容生成一个指纹,具体来说,就是在进行TLS握手时,客户端会发送一些包含有关自身支持的加密套件、TLS/SSL版本等信息的消息给服务器,服务器会回应类似的消息,JA3也是根据这些传输消息生成的指纹。它是基于四层网络传输协议,在第四层,即传输层被使用的。而我上面举的例子,比如说向浏览器种token,把它添加到传输报文的报文头中,服务器对于浏览器提交带着的这个token进行校验,以确定其合法性,其实它作用的是应用层协议(第七层)中使用的身份验证机制,而并非传输层(四层网络传输协议的第4层),这里要注意区分。

对于tls的生成,其实有很多算法,但是JA3算法被最广泛使用,那相比于其它算法,它有什么样的优势和劣势呢?我做了个图表进行总结,供大家参考:

算法 优点 缺点
JA3 指纹算法 可以识别 TLS 客户端版本;可以基于握手消息内容生成指纹,具有更高的精度;在不同设备和操作系统上的一致性较好; 它是一种开放标准,任何人都可以实现它并将其集成到自己的应用程序或工具中,这使它成为一个通用的、可扩展的方案; 可用来验证TLS是否被篡改,与SSL证书指纹不同,JA3算法可以检测中间人攻击等网络层面的攻击行为; 无法判断代理层的影响;无法识别使用自定义密码套件的客户端;只能用于 TLS 握手识别。
SSL/TLS 证书指纹算法 不受代理层、客户端版本等因素的影响;可以识别采用自定义密码套件的客户端。 无法识别中间人攻击;证书签发机构可能存在错误或欺诈。
HTTP 消息头指纹算法 可以识别代理层、CDN 等影响;适用范围广,可用于 HTTP 流量识别。 可能存在误判;对于加密流量而言,只能识别应用层信息。
TCP/IP 指纹算法 可以识别代理层、NAT 等影响;可以在网络层识别流量。 具有较高的误判率;对于加密流量而言,只能识别网络层信息。
DNS 指纹算法 可以在域名解析阶段进行指纹识别;不受代理层等因素的影响。 无法识别加密流量;可能存在 DNS 缓存的干扰。

python中有JA3算法也非常常用,特别是下列一些场景:

  1. 识别恶意软件:通过 JA3 算法可以识别出具有特定 JA3 指纹的恶意软件,从而帮助网络安全人员及时发现和防范攻击。

  2. 流量识别:JA3 算法可以用于流量识别和分类,帮助工程师进行流量监控、分析等操作。

  3. 加密流量检测:由于 JA3 算法可以识别 TLS 客户端版本和加密套件,因此它可以被用来检测加密流量是否合法以及是否遵循最佳实践。

  4. 网络侦查:JA3 算法可以用于识别用户访问网站的类型、客户端操作系统、浏览器版本等信息,帮助网络侦查人员了解对方的技术能力和行为习惯。

  5. 安全策略制定:通过对 JA3 数据的统计和分析,可以了解不同客户端的使用情况,并据此制定相应的安全策略和措施,提高网络安全性。

第二部分

那如何来部署golang服务,让其支持动态更新TLS certificates而无需停机?我们知道Transport Layer Security(TLS)是一种基于SSLv3的加密协议,用于在两个站点之间加密和解密流量。换言之,TLS确保你正在访问的站点和你之间数据的传输数据不被侦测到。这是通过相互交换数字证书来实现的:一个存在于web服务器上的私有证书,另一个通常随web浏览器分发的公共证书。

在生产环境,服务都是以安全方式运行,但服务验证经过一定周期就会过期。然后对于服务响应去验证、重新生成,同时不用停机,就可以重新使用生成的验签证书。这篇文章,演示一下TLS验证是在基于golang语言的HTTPS服务是如何使用的。

这篇教程有先要满足下面这些先决条件。

  • 要对客户端-服务端模型要有基本理解
  • Golang的基础知识
配置HTTP Server

开始这篇文章之前,先演示一个简单的HTTP服务,只需要使用http.ListenAndServe函数启动一个HTTP服务,再用http.HandleFunc函数对于特定endpoint注册一个response handler

开始,配置HTTP服务:

package main
import (
    "net/http"
    "fmt"
    "log"
)

func main(){
    mux := http.NewServeMux()
    mux.HandleFun("/",func(res http.ResponseWriter, req *http.Request){
        fmt.Fprint(res, "Running Http Servcer")
    })

    srv := &http.Server{
        Addr: fmt.Sprintf(":%d",8080),
        Handler: mux,
    }
    //在8080端口运行
    log.Fatal(srv.ListenAndServe())
}

上面例子,用go run server.go,会在HTTP服务的8080端口运行,浏览器输入http://localhost:8080,你就会看到Hello World!输出在屏幕上。

srv.ListenAndServe()调用了go语言HTTP服务的标准库配置,然而,你可以使用Server结构类型来定制server

启动一个HTTPS服务,用配置调用函数srv.ListenAndServeTLS(certFile,keyFile),与srv.ListenAndServe()函数相似。ListenAndServeListenAndServeTLS两个函数在HTTP包和Server结构中都是可用的。

ListenAndServeTLSListenAndServe函数类似,除了前者对HTTPS服务的支持。

func ListenAndServeTLS(certFile string,keyFile string) error

从以上函数签名上可以看出,两者唯一的区别在于额外的certFilekeyFile参数,分别代表SSL Certificate文件的路径和private key文件。

生成private keySSL certificate

以下是生成根key和certificate的步骤:

    1. 生成根key

openssl genrsa -des3 -out rootCA.key 4096

    1. 创建和对根certificateself-signature(自签名)

openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt

接着,按下面的方式为每个服务生成certificate:

    1. 创建certificate key openssl genrsa -out localhost.key 2048
    1. 创建certificate-signing request(CSR)CSR是在哪里指定你想生成的certificate的详情。根key的拥有者将执行request来生成certificate。当创建CSR时,重要的是指定提供IP地址的Common Name,或者服务的域名,否则certificate无法验证。
openssl req -new -key localhost.key -out localhost.csr
    1. 使用TLS CSR和密钥以及CA根密钥生成证书
openssl x509 -req -in localhost.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out localhost.crt -days 500 -sha256

最后,遵循同样步骤为每个客户端生成certificate

配置一个HTTPS Server

既然有private keycertificate文件,就可以对先前的go程序做修改了,这次用ListenAndServeTLS代替。

package main

import(
 "net/http"
 "fmt"
 "log"
)

func main() {

 mux := http.NewServeMux()
 mux.HandleFunc("/", func( res http.ResponseWriter, req *http.Request ) {
    fmt.Fprint( res, "Running HTTPS Server!!" )
 })
 
 srv := &http.Server{
    Addr: fmt.Sprintf(":%d", 8443),
    Handler:           mux,
 }

 // run server on port "8443"
 log.Fatal(srv.ListenAndServeTLS("localhost.crt", "localhost.key"))
}

运行以上程序,将使用包含运行文件同级目录下的localhost.crt作为certFile,使用localhost.key作为keyFile启动一个HTTPS服务。再浏览器访问https://localhost:8443,或者命令行工具(CLI),可以看到如下输出:

$ curl https://localhost:8443/ --cacert rootCA.crt --key client.key --cert client.crt 

Running HTTPS Server!!

就是这样!这是大多数人启动HTTPS服务器必须做的事情。是Go管理TLS通信的默认行为和功能。

配置HTTPS服务以自动更新证书

当运行以上的HTTPS服务,你把certFilekeyFile传给了ListenAndServeTLS函数,然而,如果因为certificate过期certFilekeyFile发生变化,服务需要重启来使变化生效,为了克服这种中断导致的的短暂服务中断,可以使用net/http包的TLSConfig

cryto/tls包的TLSConfig结构会配置服务的TLS参数,包括服务证书等。所有TLSConfig的参数都是可选项,同时也要注意给TLSconfig的参数配置选项赋以空结构,就等同于赋个nil值给它。然而,配置GetCertificate字段却是相当有益的。

type Config struct{
    GetCertificate func(*ClientHelloInfo) (*Certicate,error)
}

TLSConfigGetCertificate字段会基于ClientHelloInfo返回证书。

我需要实现GetCertificate闭包函数,该函数使用tls.LoadX509KeyPair(certFile string, keyFile string) 或者 tls.X509KeyPair(certFile []byte, keyFile []byte)函数来获取证书,举两个例子:

#这是使用tls.LoadX509KeyPair(certFile string, keyFile string)函数的例子
func GetCertificate(certFile string, keyFile string) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        log.Fatal(err)
    }
    return func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
        return &cert, nil
    }
}
#这是使用tls.X509KeyPair的例子:
func GetCertificate(certData []byte, keyData []byte) func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
    cert, err := tls.X509KeyPair(certData, keyData)
    if err != nil {
        log.Fatal(err)
    }
    return func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
        return &cert, nil
    }
}
#以上两个函数使用后都会返回一个闭包,可以用作tls.config结构体中的GetCertificate字段。

现在我将使用TLSConfig字段值创建Server结构:

package main

import (
 "crypto/tls"
 "fmt"
 "log"
 "net/http"
)

func main() {

 mux := http.NewServeMux()
 mux.HandleFunc("/", func( res http.ResponseWriter, req *http.Request ) {
  fmt.Fprint( res, "Running HTTPS Server!!\n" )
 })
 
 srv := &http.Server{
  Addr: fmt.Sprintf(":%d", 8443),
  Handler:           mux,
  TLSConfig: &tls.Config{
   GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
                                               // 总是获取最新的localhost.crt和localhost.key 
                                               // 将证书文件保存在全局位置中,这样创建新证书时可以更新它们,并且该闭包函数可以引用它。
    cert, err := tls.LoadX509KeyPair("localhost.crt", "localhost.key")
    if err != nil {
     return nil, err
    }
    return &cert, nil
   },
  },
 }

 // 在8443端口运行服务
 log.Fatal(srv.ListenAndServeTLS("", ""))
}

以上程序中,我实现了GetCertificate闭包函数,通过使用LoadX509KeyPair及证收和之前创建的私有文件,返回了一个类型为Certificatecert对象。同时函数了一个error,方便调试和追踪。

由于我正在用TLS配置,为srv服务对象做预配置,我不需要给srv.ListenAndServeTLS函数调用提供certFilekeyFile。运行服务,它会像之前一样运行,但是区别点就在于,我从调用对象中抽象了所有的服务配置,因此这些配置即便更新,也会动态加载,而不必重启服务。

$ curl https://localhost:8443/ --cacert rootCA.crt --key client.key --cert client.crt 

Running HTTPS Server!!

第三部分

好了,这篇有关如何抽象TLS服务配置,达到不需要重启服务就能加载变更证书的文章就分享至些,感谢阅读,我特别将可用于tls加密的指纹算法提到第一段来讲,并把JA3指纹算法在四层服务传输协议中的使用,和浏览器token验签属于应用层(七层网络传输服务)协议中使用的身份验证机制做了区分,方便你在今后使用的过程中有更深刻的理解。

四层服务传输协议(TCP/IP模型)和七层服务传输协议(OSI模型)的对比如下:

OSI模型 TCP/IP模型
应用层(7) 应用层(4)
表示层(6)
会话层(5)
传输层(4) 传输层(3)
网络层(3) 网际层(2)
数据链路层(2) 网络接口层(1)
物理层(1)

其中,TCP/IP模型将原本的“会话层、表示层、应用层”合并为一个应用层,而将原本的“网络层、数据链路层、物理层”分别放在了网际层、网络接口层两个层级中。

下面是四层服务传输协议(TCP/IP模型)和七层服务传输协议(OSI模型)每层常见的使用场景的对比图表:

OSI 模型 TCP/IP 模型 常见的使用场景
应用层(7) 应用层(4) HTTP、FTP、SMTP等应用程序
表示层(6) 数据压缩和加密
会话层(5) 远程访问和RPC
传输层(4) 传输层(3) TCP和UDP协议
网络层(3) 网际层(2) IP、ICMP、ARP等
数据链路层(2) 网络接口层(1) 以太网、WiFi、ATM等
物理层(1) 传输介质及物理设备

在 OSI 模型中,每一层都有自己的功能和特点。应用层负责定义应用程序之间的交互规则;表示层用于对应用数据进行编码和解码;会话层为不同主机上的应用程序之间建立会话连接;传输层提供端到端的可靠数据传输服务;网络层负责将数据包从源主机传输到目标主机;数据链路层管理网络节点之间的数据帧传输;物理层负责传输介质及物理设备。

在 TCP/IP 模型中,应用层包含了 OSI 模型的应用层、表示层和会话层的功能;传输层提供端到端的可靠数据传输服务;网际层负责将数据包从源主机传输到目标主机;网络接口层管理网络节点之间的数据帧传输。

总之,在网络通信中,无论是使用 OSI 模型还是 TCP/IP 模型,每一层都有各自的功能和特点,能够互相配合完成数据传输和网络通信的任务。

这两个图表,相信可以让你对服务传输有更深刻的理解。感谢阅读。

本主题指南

技术实践与开发文档
Avatar

Aisen

Be water,my friend.
扫码关注公众号,可领取以下赠品:
《夯实基础的go语言体系建设》645页涵盖golang各大厂全部面试题,针对云原生领域更是面面俱到;
扫码加微信,可领取以下赠品:
【完整版】本人所著,原价1299元的《爱情困惑者必学的七堂课》;
100个搞定正妹完整聊天记录列表详情点这里
【完整版】时长7小时,原价699元《中国各阶层男性脱单上娶指南》;
  • 为了给wordpress的ripro主题资源站填充数据,我用golang开发了并发版爬虫和分布式爬虫两个版本,已经顺利跑通!

    为了给wordpress的ripro主题资源站填充数据,我用golang开发了并发版爬虫和分布式爬虫两个版本,已经顺利跑通!代码总量还是比较多了,具体课程中再详细讲解,这篇文章主要分享一下自己的技术选型思路,以及我觉得做这个项目过程中自己的一些感悟!包括对于系统架构的理解以及对于golang语言本身的理解。

  • 你所不知道的golang高级用法
  • 在aws上以lambda方式部署golang应用 -(上)用.zip压缩文件部署Go Lambda functions

    在使用 AWS Lambda 运行 Go 时,可以将代码打包成 .zip 文件并上传到 Lambda 上。这样做的好处是可以将所有的依赖项打包在一个文件中,而不需要在 Lambda 中安装它们。同时,也可以避免在多个文件之间进行部署和管理的复杂性。 要将 Go Lambda 函数打包为 .zip 文件,您需要将函数代码和依赖关系放置在一个文件夹中,并将其压缩为 .zip 格式。如果您的 Go 程序包含外部依赖项,则需要在函数代码中包括它们,或者将它们一起打包到 .zip 文件中。 一旦您已经打包了 Lambda 函数,就可以通过 AWS 控制台或 AWS CLI 将其上传到 Lambda。上传完成后,您可以测试该函数并将其发布到您的应用程序中。通过使用 .zip 文件打包 Lambda 函数,可以有效地简化部署和管理过程,使您的开发工作更加高效。

  • 夯实基础的go语言简明学习手册